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

nordisk/facett

facett-demo/src/gfx_v2/mod.rs

//! **GFX v2** — the renderer track, on show and self-evidencing.
//!
//! One tab that exercises three of the GFX_V2 items together over [`Scene`], a
//! single scene of real Swedish geography:
//!
//! | item | what this tab does with it |
//! |------|----------------------------|
//! | **6 — 2D is a constrained 3D** | The two panes are **not** two views. The 3D view is the only one stored; the 2D view is produced from it by [`View::constrain_2d`]. Toggle *Constrain* and the right pane collapses onto the left one — and the tab reports the measured worst-case per-place disagreement in pixels. |
//! | **8 — GPU picking** | Clicking either pane runs an `R32Uint` id pass on the device and reads one texel back. The picked [`PickId`] resolves to a real place name. Item 8 shipped with no consumer at all; this is it. |
//! | **4 — GPU label collision** | The place names are laid out into the epaint font atlas and handed to the device, where three compute passes claim grid cells by `atomicMax` priority and append the winners to a `DrawIndexedIndirect`. See [`labels`]. |
//!
//! ## The trap this tab is built to avoid
//!
//! Five closed GFX_V2 items turned out to be "the feature was already there and
//! nothing was watching it": flowsim's GPU lane was proven and never called, item
//! 2's `cull.wgsl` was orphaned so deleting the whole pass failed no test, and a
//! pixel assertion passed at 486 px with **zero** device frames because the CPU
//! painter drew it.
//!
//! So no claim in this tab's [`state_json`](GfxV2Tab::state_json) is "a frame was
//! drawn". Every GPU claim is device-side and separately falsifiable:
//!
//! * `pick.passes_run` — `facett_core`'s process-wide count of id passes actually
//!   submitted, **paired** with `pick.picked_id`, because a pass over an empty
//!   batch would bump the counter while proving nothing.
//! * `pick.declined` — how many clicks the GPU lane refused (no host, empty batch,
//!   degenerate pane, probe outside). A lane that silently declines every click
//!   looks exactly like a lane that works, so the refusals are counted too.
//! * `labels.*` — read back out of the **indirect draw buffer** the device wrote:
//!   the instance count the draw actually uses, plus how many labels won the
//!   collision and how many were suppressed.
//! * `gpu.adapter` — the adapter name. A GPU test that skips gracefully goes green
//!   having run no shader; if this field is absent, nothing ran.
//!
//! ## Arming
//!
//! The device lanes need an [`egui_wgpu::RenderState`], which a `Facet` cannot
//! reach from a `&mut Ui`. The host hands it over once via [`arm`] — `main.rs` at
//! startup, or a test with `egui_kittest`'s `create_render_state`. Un-armed, every
//! lane reports itself off and the CPU painter draws; nothing pretends.

pub mod scene;
#[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
pub mod labels;

use egui::{Pos2, Rect, Sense, Stroke, StrokeKind, Ui, Vec2};
use facett_core::engine::pick::PickId;
use facett_core::render::View;
use serde_json::json;

pub use scene::{Place, Projected, Scene, LAYER_CITY, LAYER_PLACE};

/// The stable tab title — the deck / discovery-contract / LEGEND key.
pub const TITLE: &str = "GFX v2";

/// The field of view the 3D lane uses, in degrees.
const FOV_DEG: f32 = 45.0;

// ─────────────────────────────────────────────────────────────────────────────
// Arming — the host's one-time hand-over of the device.
// ─────────────────────────────────────────────────────────────────────────────

#[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
mod armed {
    use std::sync::RwLock;

    static RENDER_STATE: RwLock<Option<egui_wgpu::RenderState>> = RwLock::new(None);

    /// Hand the device to the GFX v2 lanes and install the pick host.
    ///
    /// Returns whether this call was the FIRST to arm (a later call replaces the
    /// armed device and returns `false`). A process-wide slot
    /// rather than a field on the tab, for the same reason
    /// `facett_core::render::gpu::install_pick_host` is one: the deck builds its
    /// facets before `eframe` has a `RenderState` to give, and threading an
    /// `Option<RenderState>` through every constructor buys nothing — the device is
    /// process-wide in fact, so modelling it as anything else is a lie that costs
    /// plumbing.
    ///
    /// **A `RwLock<Option<_>>` and not a `OnceLock`, and the difference is load
    /// bearing.** A `OnceLock` can be armed exactly once per process, and an
    /// `egui_kittest` test binary legitimately builds SEVERAL devices — the
    /// harness's `WgpuTestRenderer::from_render_state` refuses a `RenderState` that
    /// has already been used, so every harness needs a fresh one. With a `OnceLock`
    /// the second harness would paint through a device the lanes were not armed
    /// with, the collider would be installed in the wrong `CallbackResources`, and
    /// the compute pass would silently never run while the host-side frame counter
    /// happily counted frames. That is not hypothetical: it is the first thing this
    /// tab's device test caught. Last arm wins.
    pub fn arm(render_state: &egui_wgpu::RenderState) -> bool {
        let mut slot = RENDER_STATE.write().expect("the arm slot is never poisoned");
        let first = slot.is_none();
        *slot = Some(render_state.clone());
        // Install unconditionally: `install_pick_host` is itself idempotent per
        // device, and a second host would be a second set of GPU resources for one
        // device.
        facett_core::render::gpu::install_pick_host(render_state);
        first
    }

    /// The armed device, if the host has handed one over. A clone — `RenderState` is
    /// a bundle of `Arc`s, so this is cheap and it is what lets the slot be
    /// re-armable without handing out a borrow of the lock.
    pub fn render_state() -> Option<egui_wgpu::RenderState> {
        RENDER_STATE.read().ok()?.clone()
    }

    /// The adapter's reported name — the honest answer to "did anything actually
    /// run on a GPU". A graceful skip leaves this `None`.
    pub fn adapter_name() -> Option<String> {
        Some(RENDER_STATE.read().ok()?.as_ref()?.adapter.get_info().name)
    }
}

#[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
pub use armed::{adapter_name, arm, render_state};

/// Un-armed stand-ins for builds without the GPU lane (wasm, or the feature off).
/// They exist so the tab body needs no `cfg` at every call site — LAW: one writer.
#[cfg(not(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32"))))]
mod armed_off {
    pub fn adapter_name() -> Option<String> {
        None
    }
}
#[cfg(not(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32"))))]
pub use armed_off::adapter_name;

// ─────────────────────────────────────────────────────────────────────────────
// The tab.
// ─────────────────────────────────────────────────────────────────────────────

/// Which pane a click landed in.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Pane {
    /// The left pane — the 3D view collapsed by [`View::constrain_2d`].
    TwoD,
    /// The right pane — the stored 3D view (or the constrained one, when the
    /// *Constrain* toggle is on).
    ThreeD,
}

impl Pane {
    pub fn as_str(self) -> &'static str {
        match self {
            Pane::TwoD => "2d",
            Pane::ThreeD => "3d",
        }
    }
}

/// One recorded pick outcome, kept so `state_json` can report the *whole* answer
/// rather than only the happy path.
#[derive(Clone, Debug, Default)]
struct PickRecord {
    /// `Some(id)` for a resolved click — including [`PickId::NOTHING`], which is a
    /// real click on empty space and NOT the same thing as a declined lane.
    id: Option<PickId>,
    pane: Option<Pane>,
    /// The place the id resolved to, if any.
    name: Option<&'static str>,
    /// Clicks the GPU lane answered.
    resolved: u64,
    /// Clicks the GPU lane declined (`pick_at` returned `None`) — no host, empty
    /// batch, degenerate pane, or a probe outside the pane.
    declined: u64,
    /// Clicks that fell through to the CPU lane because the GPU declined.
    cpu_fallbacks: u64,
    /// The last id the CPU fallback produced, so the two lanes can be compared.
    cpu_id: Option<PickId>,
}

/// **GFX v2** — the item 4 / 6 / 8 showcase over one real scene.
pub struct GfxV2Tab {
    title: String,
    scene: Scene,

    /// **The only view.** The 2D lane is derived from this by
    /// [`View::constrain_2d`] every frame — see [`Self::view_2d`]. Storing a second
    /// `View` for the 2D pane is exactly the twinning item 6 removed.
    view_3d: View,
    /// When set, the 3D pane renders the *constrained* view too, so the two panes
    /// must agree pixel-for-pixel.
    constrain: bool,
    /// Draw the collision-grid cells the label lane claims into.
    show_grid: bool,
    /// Drive the GPU label lane (item 4). Off ⇒ the CPU painter draws the names.
    gpu_labels: bool,

    pick: PickRecord,
    /// Worst per-place |Δ| in points between the two panes' projections while
    /// *Constrain* is on. `None` until a constrained frame has been measured.
    parity_max_pt: Option<f32>,

    #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
    labels: labels::LabelLane,
}

impl Default for GfxV2Tab {
    fn default() -> Self {
        Self::new(TITLE)
    }
}

impl GfxV2Tab {
    /// Build the tab over the Sweden scene, framed so the whole country is visible.
    pub fn new(title: impl Into<String>) -> Self {
        let scene = Scene::sweden();
        let c = scene.centre();
        let (_, h) = scene.extent();
        // Sit back far enough that the scene's height fills the frustum at FOV_DEG.
        // Derived from the scene rather than a tuned constant, so the framing cannot
        // silently stop matching the data.
        let distance = (h * 0.5) / (FOV_DEG.to_radians() * 0.5).tan() * 1.25;
        // `from_orbit` rather than `perspective_3d`, so this crate needs no `glam`
        // edge of its own: `V3` is facett-core's own plain vector and `from_orbit`
        // maps elevation → tilt inside the view core (the ONE writer).
        let view_3d = View::from_orbit(
            facett_core::render::camera::V3::new(c[0], 0.0, c[1]),
            0.0,
            std::f32::consts::FRAC_PI_2 - 0.60,
            distance,
            FOV_DEG.to_radians(),
        );
        Self {
            title: title.into(),
            scene,
            view_3d,
            constrain: false,
            show_grid: false,
            gpu_labels: true,
            pick: PickRecord::default(),
            parity_max_pt: None,
            #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
            labels: labels::LabelLane::default(),
        }
    }

    /// **Item 6, as a derivation rather than a duplicate.** The 2D view IS the 3D
    /// view collapsed onto the constraint surface: tilt and azimuth to zero,
    /// projection to ortho, terrain flat, post-FX off, and the perspective scale at
    /// the target converted into the equivalent `px_per_unit`.
    ///
    /// It is a method, not a field, on purpose. A stored 2D view is a second copy of
    /// the camera that can drift from the first — which is the bug class item 6
    /// existed to remove, so the demo must not reintroduce it.
    pub fn view_2d(&self, half_h: f32) -> View {
        let mut v = self.view_3d;
        v.constrain_2d(half_h);
        v
    }

    /// The view a given pane renders with.
    fn view_for(&self, pane: Pane, half_h: f32) -> View {
        match pane {
            Pane::TwoD => self.view_2d(half_h),
            Pane::ThreeD if self.constrain => self.view_2d(half_h),
            Pane::ThreeD => self.view_3d,
        }
    }

    /// The scene, for hosts and tests.
    pub fn scene(&self) -> &Scene {
        &self.scene
    }

    /// The last picked id, if a click has been resolved.
    pub fn picked(&self) -> Option<PickId> {
        self.pick.id
    }

    /// The name the last pick resolved to.
    pub fn picked_name(&self) -> Option<&'static str> {
        self.pick.name
    }

    /// Turn the GPU label lane on or off (the host's / a robot's switch).
    pub fn set_gpu_labels(&mut self, on: bool) {
        self.gpu_labels = on;
    }

    /// Turn the item-6 constraint on or off.
    pub fn set_constrain(&mut self, on: bool) {
        self.constrain = on;
    }

    /// **Read the label lane's indirect draw buffer back off the device.**
    ///
    /// This is the tab's device-side proof hook for item 4: the `instance_count` the
    /// indirect draw actually uses, plus how many labels won the collision and how
    /// many were suppressed. Nothing on the CPU chose any of those numbers.
    ///
    /// It **blocks** on a `device.poll` — roughly a frame of latency — so it is a
    /// button and a test hook, never part of the paint loop. `None` when no device is
    /// armed or the collider has not been installed by a first draw yet.
    #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
    pub fn read_label_device_stats(&mut self) -> Option<labels::DeviceStats> {
        self.labels.read_device_stats()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Painting.
// ─────────────────────────────────────────────────────────────────────────────

/// A borrowed (rect, projected) pair — the only two things the pick batch needs, so
/// a caller that has projected a pane can build a batch without owning a `PaneFrame`.
#[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
struct PaneFrameRef<'a> {
    rect: Rect,
    projected: &'a [Projected],
}

/// The projected scene for one pane, plus the geometry the pick lane needs.
struct PaneFrame {
    view: View,
    rect: Rect,
    projected: Vec<Projected>,
}

impl GfxV2Tab {
    /// Project the scene for one pane. The ONE place a pane's geometry is
    /// computed — the painter, the label anchors and the pick quads all read this,
    /// so a label can never end up describing a different projection than the dot
    /// it sits on.
    fn frame(&self, pane: Pane, rect: Rect) -> PaneFrame {
        let half_h = rect.height() * 0.5;
        let view = self.view_for(pane, half_h);
        let centre = (rect.center().x, rect.center().y);
        let projected = self.scene.project(&view, centre, half_h);
        PaneFrame { view, rect, projected }
    }

    /// Radius in points for a place — cities read larger, by real population weight.
    fn radius(p: &Place) -> f32 {
        if p.is_city() {
            2.5 + (p.weight * 0.20).min(5.0)
        } else {
            1.6
        }
    }

    /// Paint the dots. Names are drawn by the label lane (GPU) or, when that is
    /// off, by [`Self::paint_cpu_labels`].
    fn paint_dots(&self, ui: &Ui, f: &PaneFrame, theme: &facett_core::Theme) {
        let painter = ui.painter_at(f.rect);
        for (p, pr) in self.scene.places.iter().zip(&f.projected) {
            if !pr.visible {
                continue;
            }
            let pos = Pos2::new(pr.x, pr.y);
            if !f.rect.contains(pos) {
                continue;
            }
            let picked = self.pick.id == Some(p.id);
            let col = if picked {
                theme.accent
            } else if p.is_city() {
                theme.text
            } else {
                theme.text_dim
            };
            let r = Self::radius(p);
            painter.circle_filled(pos, r, col);
            if picked {
                // The pick answer, made visible: a ring on the thing that was hit.
                painter.circle_stroke(pos, r + 4.0, Stroke::new(1.5, theme.accent));
            }
        }
    }

    /// The CPU label fallback — deliberately **naive**: it draws every name with no
    /// collision resolution at all.
    ///
    /// That is the point. Flip *GPU labels* off and the pane turns into the
    /// unreadable pile-up that the device-side collision pass exists to prevent, so
    /// the difference between the two is visible rather than asserted. A CPU
    /// declutter here would hide exactly the thing this tab is showing.
    fn paint_cpu_labels(&self, ui: &Ui, f: &PaneFrame, theme: &facett_core::Theme) {
        let painter = ui.painter_at(f.rect);
        for (p, pr) in self.scene.places.iter().zip(&f.projected) {
            if !pr.visible || !f.rect.contains(Pos2::new(pr.x, pr.y)) {
                continue;
            }
            let size = if p.is_city() { 12.0 } else { 9.0 };
            painter.text(
                Pos2::new(pr.x + Self::radius(p) + 3.0, pr.y),
                egui::Align2::LEFT_CENTER,
                p.name,
                egui::FontId::proportional(size),
                if p.is_city() { theme.text } else { theme.text_dim },
            );
        }
    }

    /// Draw the collision grid the label lane claims cells in, so the mechanism is
    /// visible and not merely described.
    fn paint_grid(&self, ui: &Ui, f: &PaneFrame, theme: &facett_core::Theme) {
        let painter = ui.painter_at(f.rect);
        let cols = facett_core::label_grid::GRID_W;
        let rows = facett_core::label_grid::GRID_H;
        let cw = f.rect.width() / cols as f32;
        let ch = f.rect.height() / rows as f32;
        let col = theme.text_dim.linear_multiply(0.18);
        // Every 8th line only — 128x96 lines at once is noise, not information.
        for i in (0..=cols).step_by(8) {
            let x = f.rect.left() + i as f32 * cw;
            painter.line_segment([Pos2::new(x, f.rect.top()), Pos2::new(x, f.rect.bottom())], Stroke::new(1.0, col));
        }
        for j in (0..=rows).step_by(8) {
            let y = f.rect.top() + j as f32 * ch;
            painter.line_segment([Pos2::new(f.rect.left(), y), Pos2::new(f.rect.right(), y)], Stroke::new(1.0, col));
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Picking (item 8) — the CPU lane, and the device lane.
// ─────────────────────────────────────────────────────────────────────────────

impl GfxV2Tab {
    /// The CPU pick: nearest place whose disc contains the probe, painter order.
    ///
    /// This is the FALLBACK, and it is also the oracle the device lane is checked
    /// against — `pick_at` returning `None` means the GPU declined, and a caller
    /// that treated that as "nothing there" would silently deselect. So the CPU
    /// answer is always computed and always reported.
    fn cpu_pick(&self, f: &PaneFrame, probe: Pos2) -> PickId {
        let mut best = PickId::NOTHING;
        for i in self.pick_order() {
            let p = &self.scene.places[i];
            let pr = f.projected[i];
            if !pr.visible {
                continue;
            }
            let d = Vec2::new(pr.x - probe.x, pr.y - probe.y).length();
            if d <= Self::radius(p) + 3.0 {
                // Later in `pick_order` wins, matching the painter-order depth the
                // GPU batch is built with, so the two lanes answer the same question.
                best = p.id;
            }
        }
        best
    }

    /// **The ONE pick order**, shared by the CPU lane and the device batch:
    /// addresses first, cities last, so a city is on top.
    ///
    /// This exists because of a real behaviour found by the test below. The scene's
    /// addresses cluster *on* the city centres by construction (that is what
    /// `address_for` does), so with plain scene order a click on "Stockholm" returned
    /// one of the anonymous addresses sitting on top of it — technically the
    /// painter-order answer, and the wrong object to hand a user.
    ///
    /// Both lanes read this list, rather than each imposing its own order, because
    /// their DEFAULT tie-breaks are opposite: a colour pass with no depth answers
    /// "whichever quad was submitted last", while a CPU containment scan naturally
    /// answers "the first one found". For overlapping features those are different
    /// objects — a wrong click, not a cosmetic difference — so the order has to be
    /// one writer or the GPU and CPU lanes silently disagree exactly where the scene
    /// is densest.
    fn pick_order(&self) -> Vec<usize> {
        let mut idx: Vec<usize> = (0..self.scene.places.len()).collect();
        idx.sort_by_key(|&i| u8::from(self.scene.places[i].is_city()));
        idx
    }

    /// Handle a click in a pane: run the device pick if armed, else the CPU one.
    fn on_click(&mut self, pane: Pane, f: &PaneFrame, probe_screen: Pos2, _ppp: f32) {
        let local = Pos2::new(probe_screen.x - f.rect.left(), probe_screen.y - f.rect.top());
        let cpu = self.cpu_pick(f, probe_screen);

        #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
        let gpu = self.device_pick(f, local, _ppp);
        #[cfg(not(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32"))))]
        let gpu: Option<PickId> = None;
        let _ = local;

        match gpu {
            Some(id) => {
                self.pick.resolved += 1;
                self.pick.id = Some(id);
            }
            None => {
                // The GPU lane DECLINED. Count it, fall back, and keep both answers
                // so `state_json` cannot report a GPU pick that never happened.
                self.pick.declined += 1;
                self.pick.cpu_fallbacks += 1;
                self.pick.id = Some(cpu);
            }
        }
        self.pick.cpu_id = Some(cpu);
        self.pick.pane = Some(pane);
        self.pick.name = self.pick.id.and_then(|id| self.scene.by_id(id)).map(|p| p.name);
    }

    /// Build the device pick batch for a pane and resolve one probe through it.
    ///
    /// `pane_px` is the pane's extent in **physical** pixels and the vertices are in
    /// pane-local physical pixels — the space `pick_at` documents. Building the
    /// batch in screen space instead is the measured 32.6 px misplacement that
    /// `flowsim`'s host records, and every pixel assertion stayed green through it.
    #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
    fn device_pick(&self, f: &PaneFrame, local: Pos2, ppp: f32) -> Option<PickId> {
        use facett_core::render::gpu::pick_at;

        let rs = render_state()?;
        let batch = self.pick_batch(&f.projected, f.rect, ppp);
        let pane_px = ((f.rect.width() * ppp) as u32, (f.rect.height() * ppp) as u32);
        pick_at(&rs, &batch, pane_px, local, ppp, true)
    }

    /// **The ONE pick-batch writer.** Build the device batch for a projected pane, in
    /// pane-local PHYSICAL pixels with the depth that makes the device agree with
    /// [`Self::cpu_pick`].
    ///
    /// Public because the device test drives it. A test that re-implemented this
    /// would be asserting against its own copy of the geometry — which is how a
    /// batch built in screen space instead of pane space passes every test and is
    /// still measurably 32.6 px out of place in the app.
    #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
    pub fn pick_batch(
        &self,
        projected: &[Projected],
        rect: Rect,
        ppp: f32,
    ) -> facett_core::render::gpu::PickBatch {
        use facett_core::render::gpu::PickBatch;
        let f = PaneFrameRef { rect, projected };
        let mut batch = PickBatch::new();
        // The SAME order the CPU lane scans in — see `pick_order`.
        let visible: Vec<usize> =
            self.pick_order().into_iter().filter(|&i| f.projected[i].visible).collect();
        let n = visible.len();
        for (order, &i) in visible.iter().enumerate() {
            let p = &self.scene.places[i];
            let pr = &f.projected[i];
            let r = Self::radius(p);
            // Pane-local, in physical pixels.
            let cx = (pr.x - f.rect.left()) * ppp;
            let cy = (pr.y - f.rect.top()) * ppp;
            let rr = (r + 3.0) * ppp;
            let rect = Rect::from_min_max(Pos2::new(cx - rr, cy - rr), Pos2::new(cx + rr, cy + rr));
            // Depth, so the device tie-break matches `cpu_pick`'s "later in
            // `pick_order` wins".
            //
            // The index is REVERSED, and that is not a stylistic choice — it was
            // measured. `painter_depth(i, n)` returns `(i + 0.5) / n`, so a later
            // index is a LARGER depth, and the id pipeline's depth compare is
            // `CompareFunction::Less`: the smallest depth wins. Passing the paint
            // index straight through therefore makes the FIRST-submitted quad win,
            // which is the bottom of the paint order rather than the top.
            //
            // Found by the device test in `tests/gfx_v2_device.rs`: with the plain
            // index, clicking Stockholm returned `0x02000001` — an anonymous address
            // sitting under it — while the CPU lane correctly returned the city. Two
            // lanes, two different objects, from one click.
            //
            // Reversing here fixes THIS consumer without touching item 8's shipped
            // helper, whose name (`..._in_painter_order`) and the tie-break it
            // actually produces with `depth_test: true` do not agree. That belongs to
            // whoever owns the pick lane; recorded here so it is not lost.
            batch.push_quad_in_painter_order(rect, p.id, n - 1 - order, n);
        }
        batch
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// The Facet impl.
// ─────────────────────────────────────────────────────────────────────────────

impl facett_core::Facet for GfxV2Tab {
    fn title(&self) -> &str {
        &self.title
    }

    fn ui(&mut self, ui: &mut Ui) {
        facett::core::navgraph::declare_own_draw_order(ui); // atoms follow the draw order (navgraph §9)
        let theme = facett_core::theme(ui);
        self.controls(ui, &theme);
        ui.separator();

        // Two panes, side by side, over the SAME scene.
        let avail = ui.available_size();
        let pane_w = ((avail.x - 8.0) * 0.5).max(64.0);
        let pane_h = avail.y.max(64.0);

        let mut frames: Vec<(Pane, PaneFrame)> = Vec::with_capacity(2);
        ui.horizontal(|ui| {
            for pane in [Pane::TwoD, Pane::ThreeD] {
                let (id, rect) = ui.allocate_space(Vec2::new(pane_w, pane_h));
                let f = self.frame(pane, rect);

                // A real interactive surface, and an AccessKit node with a STABLE
                // domain key (the pane's name, never a loop index) so the robot can
                // find it by role/label rather than by pixel.
                let resp = facett_core::a11y::node(
                    ui,
                    id,
                    ("gfx_v2_pane", pane.as_str()),
                    Sense::click(),
                    rect,
                    facett_core::a11y::Semantics::button(match pane {
                        Pane::TwoD => "gfx-v2 2d pane",
                        Pane::ThreeD => "gfx-v2 3d pane",
                    }),
                );

                let painter = ui.painter_at(rect);
                painter.rect_filled(rect, 4.0, theme.panel_bg);
                painter.rect_stroke(rect, 4.0, Stroke::new(1.0, theme.text_dim.linear_multiply(0.5)), StrokeKind::Inside);

                if self.show_grid {
                    self.paint_grid(ui, &f, &theme);
                }
                self.paint_dots(ui, &f, &theme);

                let mut labels_on_device = false;
                #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
                if self.gpu_labels {
                    labels_on_device = self.labels.draw(ui, &f.rect, &self.scene, &f.projected, &theme);
                }
                if !labels_on_device {
                    // The GPU lane did not take the frame — draw the names on the
                    // CPU. `draw` returning false is the ONLY licence to skip the
                    // CPU painter; opting in without that check is how flowsim's
                    // overlay went silently missing.
                    self.paint_cpu_labels(ui, &f, &theme);
                }

                self.pane_caption(ui, &f, pane, &theme, labels_on_device);

                if let Some(pos) = resp.interact_pointer_pos() && resp.clicked() {
                    let ppp = ui.ctx().pixels_per_point();
                    self.on_click(pane, &f, pos, ppp);
                }
                frames.push((pane, f));
                ui.add_space(8.0);
            }
        });

        // Item 6's measurement: with the constraint on, both panes ran the SAME
        // derived view, so every projected place must land in the same place. This
        // is the applied output — projected pixels — not a state round-trip.
        if self.constrain && frames.len() == 2 {
            let (_, a) = &frames[0];
            let (_, b) = &frames[1];
            let mut worst = 0.0f32;
            for (pa, pb) in a.projected.iter().zip(&b.projected) {
                // Compare pane-LOCAL positions: the two panes sit at different
                // screen offsets, so raw screen coordinates would differ by the
                // pane pitch and the check would be measuring the layout.
                let ax = pa.x - a.rect.left();
                let ay = pa.y - a.rect.top();
                let bx = pb.x - b.rect.left();
                let by = pb.y - b.rect.top();
                worst = worst.max((ax - bx).abs()).max((ay - by).abs());
            }
            self.parity_max_pt = Some(worst);
        }
    }

    fn state_json(&self) -> serde_json::Value {
        let half_h = 300.0; // the reported view is measured at a fixed reference height
        let v2 = self.view_2d(half_h);
        let v3 = self.view_3d;

        json!({
            // ── the scene ────────────────────────────────────────────────────
            "scene": {
                "source": "knut_popsim::geo (real WGS-84 Swedish geography)",
                "places": self.scene.places.len(),
                "cities": knut_popsim::geo::CITIES.len(),
                "distinct_names": self.scene.distinct_names(),
                "bounds": self.scene.bounds,
            },
            // ── item 6: 2D is a constrained 3D ───────────────────────────────
            "view": {
                "constrain": self.constrain,
                "two_d": {
                    "is_constrained_2d": v2.is_constrained_2d(),
                    "tilt_deg": v2.tilt_deg(),
                    "azimuth_deg": v2.azimuth_deg(),
                    "ortho": v2.projection.is_ortho(),
                    "any_post": v2.post.any(),
                },
                "three_d": {
                    "is_constrained_2d": v3.is_constrained_2d(),
                    "tilt_deg": v3.tilt_deg(),
                    "azimuth_deg": v3.azimuth_deg(),
                    "ortho": v3.projection.is_ortho(),
                    "any_post": v3.post.any(),
                    "distance": v3.distance,
                },
                // The measured worst-case per-place disagreement, in points, between
                // the two panes while the constraint is on. 0.0 is the claim; any
                // other number means the derivation is not exact.
                "parity_max_pt": self.parity_max_pt,
            },
            // ── item 8: GPU picking ──────────────────────────────────────────
            "pick": {
                "picked_id": self.pick.id.map(|i| i.0),
                "picked_layer": self.pick.id.map(|i| i.layer()),
                "picked_feature": self.pick.id.map(|i| i.feature()),
                "picked_name": self.pick.name,
                "pane": self.pick.pane.map(|p| p.as_str()),
                "resolved": self.pick.resolved,
                "declined": self.pick.declined,
                "cpu_fallbacks": self.pick.cpu_fallbacks,
                "cpu_id": self.pick.cpu_id.map(|i| i.0),
                // Device-side: id passes actually submitted, process-wide. Only
                // meaningful paired with `picked_id` — a pass over an empty batch
                // would bump this while proving nothing.
                "passes_run": pick_passes_run(),
                "host_installed": pick_host_installed(),
            },
            // ── item 4: GPU label collision ──────────────────────────────────
            "labels": self.labels_json(),
            // ── the honest renderer answer ───────────────────────────────────
            "gpu": {
                "adapter": adapter_name(),
                "armed": adapter_name().is_some(),
            },
        })
    }

    fn update_json(&mut self, msg_json: &str) {
        let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) else { return };
        if let Some(b) = v["constrain"].as_bool() {
            self.constrain = b;
        }
        if let Some(b) = v["gpu_labels"].as_bool() {
            self.gpu_labels = b;
        }
        if let Some(b) = v["show_grid"].as_bool() {
            self.show_grid = b;
        }
    }
}

// ── the process-wide device counters, with un-armed stand-ins ────────────────

#[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
fn pick_passes_run() -> u64 {
    facett_core::render::gpu::pick_passes_run()
}
#[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
fn pick_host_installed() -> bool {
    facett_core::render::gpu::pick_host_installed()
}
#[cfg(not(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32"))))]
fn pick_passes_run() -> u64 {
    0
}
#[cfg(not(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32"))))]
fn pick_host_installed() -> bool {
    false
}

impl GfxV2Tab {
    #[cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
    fn labels_json(&self) -> serde_json::Value {
        self.labels.state_json(self.gpu_labels)
    }
    #[cfg(not(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32"))))]
    fn labels_json(&self) -> serde_json::Value {
        json!({ "lane": "cpu", "requested": self.gpu_labels, "reason": "gfx-v2-gpu feature off" })
    }

    /// The control strip. Every control is a real, robot-clickable widget whose
    /// effect is visible in `state_json`.
    fn controls(&mut self, ui: &mut Ui, theme: &facett_core::Theme) {
        ui.horizontal_wrapped(|ui| {
            ui.checkbox(&mut self.constrain, "Constrain 3D → 2D (item 6)");
            ui.checkbox(&mut self.gpu_labels, "GPU labels (item 4)");
            ui.checkbox(&mut self.show_grid, "Collision grid");
            ui.separator();
            match adapter_name() {
                Some(a) => {
                    ui.label(egui::RichText::new(format!("device: {a}")).color(theme.accent).small());
                }
                None => {
                    // Say so, loudly. A tab that looks GPU-driven while the CPU
                    // painter does the work is this fleet's signature failure.
                    ui.label(
                        egui::RichText::new("device: NOT ARMED — CPU painter")
                            .color(theme.point)
                            .small(),
                    );
                }
            }
            if let Some(n) = self.pick.name {
                ui.separator();
                ui.label(egui::RichText::new(format!("picked: {n}")).color(theme.accent).small());
            } else if self.pick.id == Some(PickId::NOTHING) {
                ui.separator();
                ui.label(egui::RichText::new("picked: (empty space)").color(theme.text_dim).small());
            }
        });
    }

    /// The per-pane caption: which view, and what the lanes did.
    fn pane_caption(&self, ui: &Ui, f: &PaneFrame, pane: Pane, theme: &facett_core::Theme, on_device: bool) {
        let painter = ui.painter_at(f.rect);
        let what = match pane {
            Pane::TwoD => "2D — View::constrain_2d(3D)",
            Pane::ThreeD if self.constrain => "3D — constrained (must match 2D)",
            Pane::ThreeD => "3D — perspective",
        };
        let lane = if on_device { "labels: GPU collision" } else { "labels: CPU (no declutter)" };
        let flag = if f.view.is_constrained_2d() { "is_constrained_2d = true" } else { "is_constrained_2d = false" };
        for (i, line) in [what, flag, lane].iter().enumerate() {
            painter.text(
                f.rect.left_top() + Vec2::new(8.0, 8.0 + i as f32 * 13.0),
                egui::Align2::LEFT_TOP,
                *line,
                egui::FontId::monospace(10.0),
                if i == 0 { theme.text } else { theme.text_dim },
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use facett_core::Facet;

    /// **Item 6, as this tab makes it structural.** The 2D view is not stored — it
    /// is derived — and the derivation lands exactly on the constraint surface.
    ///
    /// Red-when-broken: give the tab a second stored `View` for the 2D pane, or drop
    /// the `constrain_2d` call, and this fails.
    #[test]
    fn the_two_d_view_is_the_three_d_view_constrained() {
        let tab = GfxV2Tab::default();
        assert!(!tab.view_3d.is_constrained_2d(), "the stored view is a real perspective 3D view");
        let v2 = tab.view_2d(300.0);
        assert!(v2.is_constrained_2d(), "the derived view satisfies every 2D constraint");
        assert_eq!(v2.tilt_deg(), 0.0, "tilt collapses to plan");
        assert_eq!(v2.azimuth_deg(), 0.0, "azimuth collapses to north-up");
        assert!(v2.projection.is_ortho(), "projection collapses to ortho");
        assert!(!v2.post.any(), "the post-FX bypass is derived FROM the view");
        // The target is preserved — the collapse changes the camera, not the subject.
        assert_eq!(v2.target.x, tab.view_3d.target.x);
        assert_eq!(v2.target.z, tab.view_3d.target.z);
    }

    /// The derivation is a pure function of the stored view, so two panes running it
    /// project **bit-identically** — which is what makes the on-screen parity
    /// measurement meaningful rather than a tolerance nobody checks.
    #[test]
    fn the_derived_view_projects_bit_identically_twice() {
        let tab = GfxV2Tab::default();
        let rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(600.0, 600.0));
        let a = tab.frame(Pane::TwoD, rect);
        let mut t2 = GfxV2Tab::default();
        t2.set_constrain(true);
        let b = t2.frame(Pane::ThreeD, rect);
        assert_eq!(a.projected.len(), b.projected.len());
        for (i, (pa, pb)) in a.projected.iter().zip(&b.projected).enumerate() {
            assert_eq!(pa.x.to_bits(), pb.x.to_bits(), "place {i} x is bit-equal");
            assert_eq!(pa.y.to_bits(), pb.y.to_bits(), "place {i} y is bit-equal");
        }
    }

    /// And the constraint is not vacuous: WITHOUT it the two panes disagree by a
    /// large, visible amount. A parity check whose two sides are identical by
    /// construction proves nothing — this is the red half of the pair.
    #[test]
    fn without_the_constraint_the_two_panes_genuinely_disagree() {
        let tab = GfxV2Tab::default();
        let rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(600.0, 600.0));
        let two = tab.frame(Pane::TwoD, rect);
        let three = tab.frame(Pane::ThreeD, rect);
        let worst = two
            .projected
            .iter()
            .zip(&three.projected)
            .map(|(a, b)| (a.x - b.x).abs().max((a.y - b.y).abs()))
            .fold(0.0f32, f32::max);
        assert!(worst > 20.0, "a perspective view must differ from its plan by more than {worst} pt");
    }

    /// `state_json` reports the whole pick answer, including the parts that would
    /// hide a dead lane: how many clicks were DECLINED and whether a device is
    /// armed at all.
    #[test]
    fn state_json_cannot_claim_a_gpu_that_is_not_there() {
        let tab = GfxV2Tab::default();
        let s = tab.state_json();
        assert_eq!(s["scene"]["places"], 14 + scene::PLACE_COUNT);
        assert!(s["pick"].get("declined").is_some(), "declined clicks are reported");
        assert!(s["pick"].get("host_installed").is_some(), "the host latch is reported");
        assert!(s["gpu"].get("adapter").is_some(), "the adapter field always exists");
        // Un-armed in a plain unit test: `armed` must say so rather than defaulting
        // to something reassuring.
        if s["gpu"]["adapter"].is_null() {
            assert_eq!(s["gpu"]["armed"], false, "no adapter ⇒ not armed");
        }
    }

    /// The CPU pick lane answers with a real place, and with `NOTHING` off-target —
    /// the oracle the device lane is compared against.
    ///
    /// The city assertion is the one that found a real bug: the scene's addresses
    /// cluster ON the city centres, so in plain scene order a click on Stockholm
    /// returned an anonymous address drawn over it. `pick_order` is the fix, and
    /// this is the guard that keeps it.
    #[test]
    fn clicking_a_city_picks_the_city_not_an_address_stacked_on_it() {
        let tab = GfxV2Tab::default();
        let rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 800.0));
        let f = tab.frame(Pane::TwoD, rect);

        // Every visible city must be pickable AS a city, not as whatever landed on
        // top of it. Checking one city would pass on a lucky sparse one.
        let mut checked = 0;
        for (i, p) in tab.scene.places.iter().enumerate() {
            if !p.is_city() {
                continue;
            }
            let pr = f.projected[i];
            if !pr.visible || !rect.contains(Pos2::new(pr.x, pr.y)) {
                continue;
            }
            let hit = tab.cpu_pick(&f, Pos2::new(pr.x, pr.y));
            assert_eq!(hit.layer(), LAYER_CITY, "clicking {} must pick a city, got layer {}", p.name, hit.layer());
            assert_eq!(hit, p.id, "and specifically {}", p.name);
            checked += 1;
        }
        assert!(checked >= 10, "at least 10 cities were on screen to check, got {checked}");

        // Far outside every dot.
        let miss = tab.cpu_pick(&f, Pos2::new(rect.right() - 1.0, rect.top() + 1.0));
        assert!(miss.is_nothing(), "empty space picks NOTHING");
    }

    /// **The two lanes must scan the SAME order**, or they disagree exactly where the
    /// scene is densest. The device batch is built by walking `pick_order` and
    /// stamping painter-order depth, so this asserts the order itself puts every
    /// city after every address.
    ///
    /// Red-when-broken: revert `pick_order` to plain scene order and this fails,
    /// which is the drift that would otherwise only show up as a wrong click.
    #[test]
    fn both_pick_lanes_share_one_order_with_cities_on_top() {
        let tab = GfxV2Tab::default();
        let order = tab.pick_order();
        assert_eq!(order.len(), tab.scene.places.len(), "the order covers every place exactly once");
        let mut seen = order.clone();
        seen.sort_unstable();
        seen.dedup();
        assert_eq!(seen.len(), order.len(), "no place appears twice");

        let first_city = order.iter().position(|&i| tab.scene.places[i].is_city()).expect("a city");
        // Everything before the first city is an address; everything after is a city.
        assert!(
            order[..first_city].iter().all(|&i| !tab.scene.places[i].is_city()),
            "addresses come first",
        );
        assert!(
            order[first_city..].iter().all(|&i| tab.scene.places[i].is_city()),
            "and cities are the contiguous tail — so painter-order depth puts them on top",
        );
    }

    /// `update_json` is the robot's write surface for the two toggles that change
    /// what the tab proves.
    #[test]
    fn update_json_drives_the_toggles() {
        let mut tab = GfxV2Tab::default();
        assert_eq!(tab.state_json()["view"]["constrain"], false);
        tab.update_json(r#"{"constrain": true}"#);
        assert_eq!(tab.state_json()["view"]["constrain"], true);
        tab.update_json(r#"{"gpu_labels": false}"#);
        assert_eq!(tab.state_json()["labels"]["requested"], false);
    }
}