facett-core 0.1.18

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The pick lane answers through a real egui host** (needs `--features wgpu`).
//!
//! `tests/gpu_picking.rs` proves the id pass itself against a hand-made `Device`/`Queue`.
//! This file proves the thing that keeps it from being **orphaned**: that a widget
//! holding nothing but an `egui_wgpu::RenderState` can install the lane and get a click
//! resolved through the shipped [`pick_at`] entry point.
//!
//! That distinction is the whole reason this file exists. GFX_V2 item 2 on this same
//! track was a compute pass that ran every frame, compacted survivors, bumped an
//! indirect counter — and **nothing read either buffer**. It had zero tests, and
//! deleting the entire pass would not have failed one. A beautifully proven pick pass
//! that no host can reach is the same disease with better documentation.
//!
//! Run: `cargo test -p facett-core --features wgpu --test gpu_pick_host -- --nocapture`
#![cfg(feature = "wgpu")]

use egui::{pos2, Rect};
use facett_core::engine::pick::PickId;
use facett_core::render::gpu::pick_host::{install_pick_host, pick_at, pick_host_installed, pick_passes_run};
use facett_core::render::gpu::picking::PickBatch;

/// A real `egui_wgpu::RenderState` — the actual object an eframe host hands its
/// widgets — with no window.
///
/// **It does not return `Option`, because it never could.** The signature used to be
/// `Option<egui_wgpu::RenderState>` returning an unconditional `Some(..)`, and all four
/// tests opened with `let Some(rs) = render_state() else { eprintln!("no WebGPU-class
/// adapter — skipped"); return; };`. Those four `else` arms were **dead code**: on a host
/// with no adapter `egui_kittest::wgpu::create_render_state` panics inside
/// `egui_kittest-0.35.0/src/wgpu.rs:77` long before it can hand back a `None`. MEASURED
/// on oden 2026-08-22 with the Vulkan and GL ICDs hidden from the loader: all four tests
/// fail there, and not one of them printed the "skipped" line.
///
/// That panic is the RIGHT outcome — loud beats a quiet `ok` — so nothing about the
/// behaviour changes here. What changes is that the file no longer carries four
/// unreachable branches that read like adapter handling. A skip path nobody can take is
/// the same untested claim as an assertion nobody can fail.
fn render_state() -> egui_wgpu::RenderState {
    let setup = egui_kittest::wgpu::default_wgpu_setup();
    egui_kittest::wgpu::create_render_state(setup, Default::default())
}

const W: u32 = 160;
const H: u32 = 100;

/// Two objects filling the left and right halves, in PHYSICAL px.
fn two_halves() -> (PickBatch, PickId, PickId) {
    let left = PickId::new(7, 1001);
    let right = PickId::new(7, 2002);
    let mut b = PickBatch::new();
    b.push_quad(Rect::from_min_max(pos2(0.0, 0.0), pos2(80.0, H as f32)), left);
    b.push_quad(Rect::from_min_max(pos2(80.0, 0.0), pos2(W as f32, H as f32)), right);
    (b, left, right)
}

/// **The wiring proof.** Install into a real `RenderState`, then resolve two clicks
/// through `pick_at` and get the two different objects back — plus a click on empty
/// space returning a genuine miss.
///
/// The pass counter is asserted to have MOVED, which is what attributes the answer to
/// the device. On its own it would be the state round-trip LAW 2 forbids (a pass over
/// nothing still bumps it), so it is only ever checked alongside the ids.
#[test]
fn a_host_with_only_a_render_state_resolves_clicks_through_pick_at() {
    let rs = render_state();
    let (batch, left, right) = two_halves();
    assert_ne!(left, right, "the two objects must be DISTINGUISHABLE or this proves nothing");

    // Gate 1, BEFORE installing: the lane declines rather than inventing an answer.
    // This must run first — `INSTALLED` is a process-wide latch and never goes back.
    let before = pick_at(&rs, &batch, (W, H), pos2(20.0, 50.0), 1.0, false);
    assert!(
        before.is_none() || pick_host_installed(),
        "with no host installed, pick_at must return None — a fabricated NOTHING would \
         look exactly like a real click on the background and silently deselect"
    );

    assert!(install_pick_host(&rs), "a fresh RenderState must accept the host");
    assert!(pick_host_installed(), "installing must arm the latch that unlocks the lane");
    assert!(!install_pick_host(&rs), "installing twice must be a no-op, not a second host");

    let passes_before = pick_passes_run();

    // The left object.
    let a = pick_at(&rs, &batch, (W, H), pos2(20.0, 50.0), 1.0, false).expect("the lane must answer");
    assert_eq!(a, left, "clicking the left half read {:#010x}, expected {:#010x}", a.0, left.0);
    // The right object — a DIFFERENT id, from the same host, same batch.
    let b = pick_at(&rs, &batch, (W, H), pos2(120.0, 50.0), 1.0, false).expect("the lane must answer");
    assert_eq!(b, right, "clicking the right half read {:#010x}, expected {:#010x}", b.0, right.0);
    assert_ne!(a, b, "two clicks on two objects must not return one id");

    assert!(
        pick_passes_run() >= passes_before + 2,
        "two clicks must have submitted two id passes — got {} more",
        pick_passes_run() - passes_before
    );
    eprintln!("[gpu_pick_host] resolved {:#010x} and {:#010x} through a real RenderState", a.0, b.0);
}

/// A click on empty space returns a genuine miss — `Some(NOTHING)`, never `None`.
/// The two are different answers and a caller must be able to tell them apart:
/// `Some(NOTHING)` means "deselect", `None` means "ask the CPU picker".
#[test]
fn empty_space_is_some_nothing_not_none() {
    let rs = render_state();
    install_pick_host(&rs);
    // One small object; the rest of the pane is background.
    let id = PickId::new(3, 42);
    let mut batch = PickBatch::new();
    batch.push_quad(Rect::from_min_max(pos2(10.0, 10.0), pos2(30.0, 30.0)), id);

    assert_eq!(pick_at(&rs, &batch, (W, H), pos2(20.0, 20.0), 1.0, false), Some(id), "the object");
    let miss = pick_at(&rs, &batch, (W, H), pos2(120.0, 80.0), 1.0, false);
    assert_eq!(
        miss,
        Some(PickId::NOTHING),
        "a click on background must be Some(NOTHING) — a real 'you hit nothing', \
         distinct from None which means the lane declined"
    );
    assert!(miss.is_some(), "the lane DID answer; it just answered 'nothing'");
}

/// Every remaining fail-safe gate returns `None`, and each for its own reason.
#[test]
fn each_fail_safe_gate_declines_rather_than_guessing() {
    let rs = render_state();
    install_pick_host(&rs);
    let (batch, left, _) = two_halves();

    // A control: the same call with valid arguments DOES answer. Without this, every
    // assertion below would pass on a lane that was simply broken.
    assert_eq!(
        pick_at(&rs, &batch, (W, H), pos2(20.0, 50.0), 1.0, false),
        Some(left),
        "control: valid arguments must resolve, or the None assertions below are vacuous"
    );

    // gate 2 — nothing pickable.
    assert_eq!(pick_at(&rs, &PickBatch::new(), (W, H), pos2(20.0, 50.0), 1.0, false), None, "empty batch");
    // gate 3 — degenerate pane.
    assert_eq!(pick_at(&rs, &batch, (0, H), pos2(20.0, 50.0), 1.0, false), None, "zero-width pane");
    assert_eq!(pick_at(&rs, &batch, (W, 0), pos2(20.0, 50.0), 1.0, false), None, "zero-height pane");
    // gate 4 — the cursor is outside the widget (negative logical coords), or beyond it.
    assert_eq!(pick_at(&rs, &batch, (W, H), pos2(-1.0, 50.0), 1.0, false), None, "left of the pane");
    assert_eq!(pick_at(&rs, &batch, (W, H), pos2(20.0, -1.0), 1.0, false), None, "above the pane");
    assert_eq!(pick_at(&rs, &batch, (W, H), pos2(500.0, 50.0), 1.0, false), None, "right of the pane");
    assert_eq!(pick_at(&rs, &batch, (W, H), pos2(20.0, 500.0), 1.0, false), None, "below the pane");
}

/// The host survives 25 clicks in a row, all resolving correctly, and its id target is
/// sized to the pane.
///
/// **What this does and does not prove**, stated honestly. It proves the host stays in
/// `callback_resources` across many queries and that repeated clicks do not degrade —
/// a lane that leaked or reset per click fails here. It does **not** prove the target
/// and pipeline are *reused* rather than rebuilt: a per-click rebuild would return the
/// same 25 right answers and the same `flat_size`, and there is no allocation counter
/// to catch it. Non-rebuilding is instead true **by construction** —
/// `PickTarget::ensure` early-returns on an unchanged size and the pipeline sits behind
/// a `get_or_insert_with` — which LAW 5 prefers over a guard watching for it. Claiming
/// this test measures caching would be the hollow half of the assertion.
#[test]
fn the_host_persists_across_many_clicks() {
    let rs = render_state();
    install_pick_host(&rs);
    let (batch, left, right) = two_halves();
    for i in 0..25 {
        let x = if i % 2 == 0 { 20.0 } else { 120.0 };
        let want = if i % 2 == 0 { left } else { right };
        assert_eq!(pick_at(&rs, &batch, (W, H), pos2(x, 50.0), 1.0, false), Some(want), "click {i}");
    }
    // The host is the SAME object, holding a target sized to the pane.
    let guard = rs.renderer.read();
    let host = guard
        .callback_resources
        .get::<facett_core::render::gpu::pick_host::PickHost>()
        .expect("the host must still be in callback_resources after 25 clicks");
    assert_eq!(host.flat_size(), (W, H), "the id target must be allocated once at the pane size");
}