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/gpu_lane_two_panes.rs

//! **TWO MAP PANES IN ONE FRAME MUST EACH SHOW THEIR OWN MAP** — lane GG #37.
//!
//! # The defect
//!
//! Since lane MP-a the GPU lane draws colour+depth into its OWN offscreen target in
//! `MapPaintCallback::prepare` and `paint` blits one textured triangle into egui's pass.
//! There was ONE such target per `GpuMapRenderer` (`renderer.offscreen`), cleared at the
//! top of every `prepare`, and ONE blit UV uniform written from `paint` through
//! `queue.write_buffer` — which lands before the whole submit, not between two callbacks.
//!
//! korp draws a main map pane and can draw an inset (a second `MapFacet`) in the same
//! frame. egui-wgpu runs every callback's `prepare` first, then the render pass with every
//! callback's `paint`. So with two facets:
//!
//! * `prepare(main)` clears the target and draws the main map;
//! * `prepare(inset)` clears the SAME target and draws the inset over the blank;
//! * `paint(main)` and `paint(inset)` both sample the one texture the inset left behind,
//!   and both through the one UV rect the LAST `paint` wrote.
//!
//! Last `prepare` wins, and the main pane shows either nothing or the inset's pixels.
//!
//! # The oracle
//!
//! Two facets side by side in one `SURFACE`: pane A carries ONE pure-red motorway, pane B
//! ONE pure-blue one, each stamped with a `tile_key` so both take the TILE lane korp
//! ships on. The frame is read back and each pane's region is required to
//!
//! 1. contain its own ink and none of the other's, and
//! 2. be PIXEL-IDENTICAL to the same pane rendered alone — the two-pane frame may not
//!    change a single pixel of either map.
//!
//! The absent-ink controls (`a_lone_red_pane_has_no_blue_ink_anywhere`, and its twin) pin
//! the detector: red and blue appear nowhere in `MapTheme::default()`, so a detector that
//! answered "found it" on a plain map cannot pass.
//!
//! Then the budget: `offscreen_target_allocs()` must not move across a run of still
//! frames with both panes up (a target is allocated on a NEW pane key or a size change,
//! never per frame), and `offscreen_targets_live()` must drop back to one when pane B
//! stops being drawn — a pane that disappears frees its VRAM.
//!
//! Run (needs a real device, opens NO window):
//! ```text
//! env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=integrated \
//!   cargo test -p facett-map --features wgpu --test gpu_lane_two_panes -- --test-threads=1 --nocapture
//! ```
#![cfg(feature = "wgpu")]

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

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

/// **The shared one-device-at-a-time lock** — `common/one_gpu.rs`. Two device bring-ups
/// at once take this binary out with a hard `signal: 11`; every rendering file in this
/// directory takes the same turn.
#[path = "common/one_gpu.rs"]
mod one_gpu;
/// **The device opt-in, shared** — `common/gpu_gate.rs`, lane GG #5.
#[path = "common/gpu_gate.rs"]
mod gpu_gate;

/// The whole offscreen surface — the "window".
const SURFACE: egui::Vec2 = egui::vec2(900.0, 640.0);
/// Pane A (the "main" map): left half, inset from the edges.
const PANE_A: egui::Rect = egui::Rect::from_min_max(egui::pos2(20.0, 20.0), egui::pos2(440.0, 620.0));
/// Pane B (the "inset" map): right half. Disjoint from A so a pane rendered alone is
/// comparable pixel-for-pixel with the same pane in the two-pane frame.
const PANE_B: egui::Rect = egui::Rect::from_min_max(egui::pos2(460.0, 20.0), egui::pos2(880.0, 620.0));

const CLON: f64 = 18.07;
const CLAT: f64 = 59.32;
const ZOOM: f32 = 12.0;

/// The two inks — neither appears anywhere in `MapTheme::default()`.
const RED: egui::Color32 = egui::Color32::from_rgb(255, 0, 0);
const BLUE: egui::Color32 = egui::Color32::from_rgb(0, 0, 255);

fn gated(name: &str) -> bool {
    gpu_gate::gated("facett-map", name)
}

/// ONE unfilled motorway (so the GPU LINE lane takes it and it survives the LOD gate at
/// [`ZOOM`]), stamped with `tile_key` so `add_gpu_tiled_callback` — korp's path — takes
/// the facet. `horizontal` runs it east–west through the camera centre, otherwise
/// north–south, so the two panes' content differs in geometry as well as ink.
fn way(id: i64, tile_key: u64, ink: egui::Color32, horizontal: bool) -> Way {
    let theme = facett_core::Theme::default();
    let d = 0.0040;
    let pts = if horizontal {
        [(CLON - d, CLAT), (CLON, CLAT), (CLON + d, CLAT)]
    } else {
        [(CLON, CLAT - d * 0.5), (CLON, CLAT), (CLON, CLAT + d * 0.5)]
    };
    let tags = vec![("highway".to_owned(), "motorway".to_owned())];
    let mut w = Way::from_lonlat(id, &pts, &tags, &theme).expect("a 3-point motorway");
    w.osm_id = id;
    w.tile_key = tile_key;
    w.fill = None;
    w.stroke = ink;
    w.style.color = ink;
    w.width = 6.0;
    w
}

fn facet(name: &str, pane: egui::Rect, w: Way) -> MapFacet {
    let view = MapView::new(LatLon::new(CLAT, CLON), ZOOM, (pane.width(), pane.height()));
    let mut f = MapFacet::new(name, view, MapTheme::default());
    f.push_layer(Box::new(WaysLayer::new("osm-roads", vec![w])));
    assert!(
        f.gpu_lane_needs_grouping_for_test(&[0]),
        "fixture: a stamped way must take the TILE lane, or this file tests the lane korp does not ship on"
    );
    f
}

fn fresh_render_state() -> egui_wgpu::RenderState {
    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);
    rs
}

/// The harness: pane A when `show_a`, pane B when `show_b`, both read live so one
/// harness can drop a pane mid-run. Every step is rendered (the upload-once latch puts
/// the payload on the first frame's callback — `gpu_lane_paint_order::drive`).
struct Panes {
    h: egui_kittest::Harness<'static>,
    show_a: Arc<AtomicBool>,
    show_b: Arc<AtomicBool>,
    /// The `RenderState` the harness draws through, kept so the still-frame gate's
    /// counters can be read off the renderer the callback installed. `RenderState` is
    /// `Arc`-backed, so this clone sees the live one (`fill_edge_aa::probe_gpu`).
    rs: egui_wgpu::RenderState,
}

impl Panes {
    fn new(a: bool, b: bool) -> Self {
        use egui_kittest::wgpu::WgpuTestRenderer;
        let show_a = Arc::new(AtomicBool::new(a));
        let show_b = Arc::new(AtomicBool::new(b));
        let (sa, sb) = (Arc::clone(&show_a), Arc::clone(&show_b));
        let mut fa = facet("pane-a-red", PANE_A, way(1, 1, RED, true));
        let mut fb = facet("pane-b-blue", PANE_B, way(2, 2, BLUE, false));
        let rs = fresh_render_state();
        let seam = rs.clone();
        let h = egui_kittest::Harness::builder()
            .with_size(SURFACE)
            .renderer(WgpuTestRenderer::from_render_state(rs))
            .build_ui(move |ui| {
                if sa.load(Ordering::Relaxed) {
                    let mut child = ui.new_child(egui::UiBuilder::new().max_rect(PANE_A));
                    child.set_clip_rect(PANE_A);
                    fa.ui(&mut child);
                }
                if sb.load(Ordering::Relaxed) {
                    let mut child = ui.new_child(egui::UiBuilder::new().max_rect(PANE_B));
                    child.set_clip_rect(PANE_B);
                    fb.ui(&mut child);
                }
            });
        Self { h, show_a, show_b, rs: seam }
    }

    /// Render `n` frames and hand back the last one plus how many GPU-lane frames it took.
    fn frames(&mut self, n: u32) -> (u64, image::RgbaImage) {
        let before = lane::lane_use().frames();
        let mut img = None;
        for _ in 0..n {
            img = Some(self.h.render().expect("the wgpu backend produced a frame"));
            self.h.step();
        }
        (lane::lane_use().frames() - before, img.expect("at least one frame"))
    }

    /// Read one number off the installed renderer.
    fn probe<T: Default>(&self, f: impl FnOnce(&facett_map::gpu::GpuMapRenderer) -> T) -> T {
        self.rs
            .renderer
            .read()
            .callback_resources
            .get::<facett_map::gpu::GpuMapRenderer>()
            .map_or_else(T::default, f)
    }

    /// Cull compute passes this renderer has recorded, over both lanes (GG #13).
    fn culls(&self) -> u64 {
        self.probe(facett_map::gpu::GpuMapRenderer::cull_dispatch_count)
    }

    /// How many panes hold retained still-frame gate state (HO-T4).
    fn gates(&self) -> usize {
        self.probe(facett_map::gpu::GpuMapRenderer::tile_cull_gate_len)
    }

    /// `(pane key, cull dispatches)` for every pane the renderer holds a gate for.
    fn per_pane(&self) -> Vec<(u64, u64)> {
        self.probe(facett_map::gpu::GpuMapRenderer::tile_cull_dispatches_per_pane)
    }
}

/// Ink counts inside `pane`, in device pixels: `(red, blue)`.
fn ink_in(img: &image::RgbaImage, pane: egui::Rect) -> (usize, usize) {
    let sx = f64::from(img.width()) / f64::from(SURFACE.x);
    let sy = f64::from(img.height()) / f64::from(SURFACE.y);
    let (mut red, mut blue) = (0usize, 0usize);
    for (x, y, p) in img.enumerate_pixels() {
        let (lx, ly) = (f64::from(x) / sx, f64::from(y) / sy);
        if !pane.contains(egui::pos2(lx as f32, ly as f32)) {
            continue;
        }
        let [r, g, b, _] = p.0;
        if r > 200 && g < 60 && b < 60 {
            red += 1;
        }
        if b > 200 && r < 60 && g < 60 {
            blue += 1;
        }
    }
    (red, blue)
}

/// Pixels inside `pane` that differ between two frames of the same size.
fn pixels_differing_in(a: &image::RgbaImage, b: &image::RgbaImage, pane: egui::Rect) -> usize {
    assert_eq!((a.width(), a.height()), (b.width(), b.height()), "two frames of one harness size");
    let sx = f64::from(a.width()) / f64::from(SURFACE.x);
    let sy = f64::from(a.height()) / f64::from(SURFACE.y);
    a.enumerate_pixels()
        .filter(|(x, y, pa)| {
            let (lx, ly) = (f64::from(*x) / sx, f64::from(*y) / sy);
            pane.contains(egui::pos2(lx as f32, ly as f32)) && b.get_pixel(*x, *y) != *pa
        })
        .count()
}

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

#[test]
fn a_main_pane_and_an_inset_pane_each_render_their_own_content_in_one_frame() {
    if !gated("a_main_pane_and_an_inset_pane_each_render_their_own_content_in_one_frame") {
        return;
    }
    let _turn = one_gpu::hold();

    // ── The controls: each pane alone. ────────────────────────────────────────
    let (fa, only_a) = Panes::new(true, false).frames(3);
    let (fb, only_b) = Panes::new(false, true).frames(3);
    save(&only_a, "only_a");
    save(&only_b, "only_b");
    assert!(fa > 0 && fb > 0, "the GPU lane encoded no frames ({fa}, {fb}) — this is the CPU painter");
    let (ra, ba) = ink_in(&only_a, PANE_A);
    let (rb, bb) = ink_in(&only_b, PANE_B);
    assert!(ra > 0, "fixture: pane A alone shows no red ink — the way never reached the device");
    assert!(bb > 0, "fixture: pane B alone shows no blue ink — the way never reached the device");
    assert_eq!((ba, rb), (0, 0), "fixture: a lone pane shows the OTHER pane's ink; the detector is broken");

    // ── The claim: both panes in ONE frame. ───────────────────────────────────
    let (fboth, both) = Panes::new(true, true).frames(3);
    save(&both, "both");
    assert!(fboth > 0, "the GPU lane encoded no frames for the two-pane frame");
    let (a_red, a_blue) = ink_in(&both, PANE_A);
    let (b_red, b_blue) = ink_in(&both, PANE_B);
    eprintln!("two panes: A has {a_red} red / {a_blue} blue px, B has {b_red} red / {b_blue} blue px");
    eprintln!("alone:     A has {ra} red px, B has {bb} blue px");

    assert!(
        a_red > 0 && a_blue == 0,
        "pane A (the main map, drawn FIRST) shows {a_red} red and {a_blue} blue px in the \
         two-pane frame; alone it shows {ra} red. The second facet's `prepare` cleared the \
         ONE shared offscreen target after the first had drawn into it, so the main pane \
         blits the inset's frame (or a blank) instead of its own"
    );
    assert!(
        b_blue > 0 && b_red == 0,
        "pane B (the inset) shows {b_blue} blue and {b_red} red px in the two-pane frame; \
         alone it shows {bb} blue"
    );

    // Pixel-identical to the lone renders: the presence of a second pane may not change
    // ONE pixel of either map.
    let da = pixels_differing_in(&both, &only_a, PANE_A);
    let db = pixels_differing_in(&both, &only_b, PANE_B);
    assert_eq!(
        (da, db),
        (0, 0),
        "with two panes up, pane A differs from its lone render in {da} px and pane B in {db} px"
    );
}

#[test]
fn a_lone_red_pane_has_no_blue_ink_anywhere() {
    if !gated("a_lone_red_pane_has_no_blue_ink_anywhere") {
        return;
    }
    let _turn = one_gpu::hold();
    let (_, img) = Panes::new(true, false).frames(3);
    let (r, b) = ink_in(&img, egui::Rect::from_min_size(egui::Pos2::ZERO, SURFACE));
    assert!(r > 0, "fixture: no red ink at all");
    assert_eq!(b, 0, "{b} blue px in a frame given NO blue way — the detector answers yes on the theme");
}

#[test]
fn a_lone_blue_pane_has_no_red_ink_anywhere() {
    if !gated("a_lone_blue_pane_has_no_red_ink_anywhere") {
        return;
    }
    let _turn = one_gpu::hold();
    let (_, img) = Panes::new(false, true).frames(3);
    let (r, b) = ink_in(&img, egui::Rect::from_min_size(egui::Pos2::ZERO, SURFACE));
    assert!(b > 0, "fixture: no blue ink at all");
    assert_eq!(r, 0, "{r} red px in a frame given NO red way — the detector answers yes on the theme");
}

/// The budget: two panes up ⇒ two targets live, ZERO allocations across still frames;
/// pane B gone ⇒ one target live (the VRAM comes back).
#[test]
fn two_panes_allocate_two_targets_once_and_a_gone_pane_frees_its_target() {
    if !gated("two_panes_allocate_two_targets_once_and_a_gone_pane_frees_its_target") {
        return;
    }
    let _turn = one_gpu::hold();
    let mut p = Panes::new(true, true);
    let (f, _) = p.frames(3);
    assert!(f > 0, "the GPU lane encoded no frames");
    let live = facett_map::gpu::offscreen_targets_live();
    assert_eq!(live, 2, "two panes in the frame, {live} offscreen target(s) live — the panes share one");

    let allocs_before = facett_map::gpu::offscreen_target_allocs();
    let (f, _) = p.frames(6);
    assert!(f > 0, "the GPU lane encoded no frames");
    let allocs_after = facett_map::gpu::offscreen_target_allocs();
    assert_eq!(
        allocs_after, allocs_before,
        "{} offscreen target allocation(s) across 6 still frames — a target must be allocated \
         on a new pane key or a size change only",
        allocs_after - allocs_before
    );

    p.show_b.store(false, Ordering::Relaxed);
    let (f, img) = p.frames(3);
    assert!(f > 0, "the GPU lane encoded no frames");
    let (_, b_blue) = ink_in(&img, PANE_B);
    assert_eq!(b_blue, 0, "pane B is no longer drawn and still shows {b_blue} blue px");
    let live = facett_map::gpu::offscreen_targets_live();
    assert_eq!(live, 1, "pane B disappeared and {live} offscreen target(s) are still live — its VRAM never came back");
    assert!(p.show_a.load(Ordering::Relaxed));
}

// ─────────────────────────────────────────────────────────────────────────────────
// HO-T4 — THE STILL-FRAME GATE IS PER RENDERER, SO AN INSET BREAKS IT.
// ─────────────────────────────────────────────────────────────────────────────────
//
// Lane GG #13 (`cc8113bf`) made the tile lane skip its per-tile uniform writes, its
// indirect resets and its cull dispatch TOGETHER when a frame is byte-identical to the
// last: 100 dispatches over 100 still frames became 0. The three things it compares
// against — `tile_cull_last`, `tile_current_last` and `tile_upload_gen` — live on the
// **renderer**.
//
// Lane GG #37 (`241f89a6`) then gave every pane its own `PaneTarget`, keyed by the
// facet's `egui::Id`, because korp draws a main map and can draw an inset in the SAME
// frame. The gate did not follow. With two panes up, the two callbacks alternate through
// the one gate: pane B's `prepare` overwrites what pane A just recorded, so pane A never
// matches on its next frame and neither does B. #13's zero-dispatch rest is LOST for as
// long as an inset is on screen — the whole cost it removed comes back, doubled.
//
// The fix is to key the gate by the same `PaneStamp::pane` #37 keys the target by, and to
// evict a pane's gate state on the same `retain(last_frame + 1 >= frame_nr)` rule, so a
// pane that goes away leaks nothing.

/// Frames to let the two panes' first uploads settle. Each pane's first frame culls (its
/// tile is fresh), and each pane's SECOND frame culls once more because the OTHER pane's
/// first upload bumped `tile_upload_gen` under it. Nothing after that may move.
const WARMUP: u32 = 4;
/// Still frames the count is measured over.
const STILL: u32 = 16;

/// **A main map and an inset, both at rest, must dispatch NO cull — each in its own
/// right** — lane HO-T4.
#[test]
fn the_still_frame_gate_holds_per_pane_with_a_main_map_and_an_inset_up() {
    if !gated("the_still_frame_gate_holds_per_pane_with_a_main_map_and_an_inset_up") {
        return;
    }
    let _turn = one_gpu::hold();

    // ── The control: ONE pane. GG #13's rest has held here since it landed, and if it
    //    has not, the number below says nothing about panes.
    let mut solo = Panes::new(true, false);
    let (f, _) = solo.frames(WARMUP);
    assert!(f > 0, "the GPU lane encoded no frames for the lone pane — this is the CPU painter");
    let solo_warm = solo.culls();
    let (f, _) = solo.frames(STILL);
    assert!(f > 0, "the GPU lane encoded no frames over the lone pane's still run");
    let solo_moved = solo.culls() - solo_warm;
    eprintln!(
        "[HO-T4 t14s/890M] ONE pane: {solo_warm} cull dispatches over {WARMUP} warmup frames, \
         then {solo_moved} over {STILL} still frames"
    );
    assert_eq!(
        solo_moved, 0,
        "one still pane re-dispatched the cull {solo_moved} times over {STILL} frames — GG #13's \
         gate is broken on its own, before any inset enters"
    );

    // ── The claim: TWO panes, both still. ────────────────────────────────────────
    let mut both = Panes::new(true, true);
    let (f, _) = both.frames(WARMUP);
    assert!(f > 0, "the GPU lane encoded no frames for the two-pane frame");
    let warm = both.culls();
    let (f, img) = both.frames(STILL);
    assert!(f > 0, "the GPU lane encoded no frames over the two-pane still run");
    let moved = both.culls() - warm;
    eprintln!(
        "[HO-T4 t14s/890M] TWO panes: {warm} cull dispatches over {WARMUP} warmup frames, \
         then {moved} over {STILL} still frames ({:.2}/frame)",
        moved as f64 / f64::from(STILL)
    );
    assert_eq!(
        moved, 0,
        "with a main map and an inset both at rest the cull dispatched {moved} times over \
         {STILL} still frames (~{:.0} per frame, one per PANE). The GG #13 gate keys on \
         `tile_cull_last` / `tile_current_last` / `tile_upload_gen`, all of which live on the \
         RENDERER while GG #37 gave each pane its own target: the two callbacks alternate \
         through the one gate and neither ever matches. Key the gate by `PaneStamp::pane`",
        moved as f64 / f64::from(STILL)
    );

    // The split, so "0 in total" cannot hide "one pane rested and the other dispatched
    // twice as often": each pane's OWN gate must have stopped letting the body through.
    let per_pane = both.per_pane();
    eprintln!("[HO-T4 t14s/890M] per-pane gate dispatches after {} frames: {per_pane:?}", WARMUP + STILL);
    assert_eq!(per_pane.len(), 2, "two panes are up and the renderer holds {} pane gate(s)", per_pane.len());
    assert!(
        per_pane.iter().all(|&(_, d)| d <= u64::from(WARMUP)),
        "a pane's own gate let the cull body through {:?} times over {} frames — with the camera \
         at rest it must stop after its warmup",
        per_pane,
        WARMUP + STILL
    );

    // ── …and the gate must not have bought that rest by serving pane A's culled set to
    //    pane B. A per-RENDERER key that "matched" would do exactly that; the ink says.
    let (a_red, a_blue) = ink_in(&img, PANE_A);
    let (b_red, b_blue) = ink_in(&img, PANE_B);
    eprintln!("[HO-T4] after {} still frames: A {a_red} red / {a_blue} blue, B {b_red} red / {b_blue} blue", WARMUP + STILL);
    assert!(
        a_red > 0 && a_blue == 0,
        "pane A shows {a_red} red and {a_blue} blue px after the still run — the gate handed it \
         the other pane's culled set"
    );
    assert!(
        b_blue > 0 && b_red == 0,
        "pane B shows {b_blue} blue and {b_red} red px after the still run — the gate handed it \
         the other pane's culled set"
    );

    // ── The bookkeeping: one gate per pane, and a gone pane leaks none of it. ────
    let gates = both.gates();
    assert_eq!(
        gates, 2,
        "two panes are up and the renderer holds {gates} pane gate(s) — the still-frame gate is \
         per RENDERER, which is the defect itself"
    );
    both.show_b.store(false, Ordering::Relaxed);
    let (f, img) = both.frames(3);
    assert!(f > 0, "the GPU lane encoded no frames after the inset went away");
    let (_, b_blue) = ink_in(&img, PANE_B);
    assert_eq!(b_blue, 0, "pane B is no longer drawn and still shows {b_blue} blue px");
    let gates = both.gates();
    assert_eq!(
        gates, 1,
        "the inset stopped being drawn and the renderer still holds {gates} pane gate(s) — a gone \
         pane's gate state must be evicted on the same rule its target is (`retain(last_frame + 1 \
         >= frame_nr)`)"
    );

    // ── And pane A, alone again and still, is back at rest. ──────────────────────
    let alone = both.culls();
    let (f, _) = both.frames(STILL);
    assert!(f > 0, "the GPU lane encoded no frames for the pane left behind");
    let after = both.culls() - alone;
    eprintln!("[HO-T4 t14s/890M] inset gone, pane A alone: {after} cull dispatches over {STILL} still frames");
    assert_eq!(after, 0, "the pane left behind re-dispatched the cull {after} times over {STILL} still frames");

    nornir_testmatrix::functional_status(
        "facett-map",
        "still_frame_gate_per_pane",
        moved == 0 && after == 0,
        &format!("2 panes, {STILL} still frames: {moved} cull dispatches (warmup {warm}); inset gone: {after}"),
    );
}