facett-demo/src/main.rs
//! **facett-demo** binary — wrap the headless [`DemoState`](facett_demo::DemoState)
//! in an eframe (glow) window with a live toolbar:
//!
//! - a **palette switcher** over every [`Theme::ALL`](facett::Theme) palette (the
//! whole deck + every facet re-themes live),
//! - the deck **fx**: a **glow** toggle (blooms the active facet) and a
//! **send-raven** button (a `RavenSprite` swoops onto a row),
//! - a **command palette** overlay (facett-menu, `Ctrl+P` / the toolbar button),
//! - an **about / legend** panel listing every component this app showcases.
//!
//! This is the living gallery for the D4 compliance proof: one window, every
//! facett component, fake-but-realistic data, switchable across all palettes.
use eframe::egui;
use facett::LookTheme;
use facett_demo::DemoState;
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
// ── `--version` / `-V`: print the build stamp and EXIT before the window opens. ──
// The nordisk LAW: an operator must always be able to tell WHICH build is running.
// This runs before eframe brings up any viewport so `--version` never boots the GUI.
if std::env::args().skip(1).any(|a| a == "--version" || a == "-V") {
println!("{}", facett_demo::version_line());
return Ok(());
}
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1280.0, 800.0])
.with_min_inner_size([900.0, 560.0])
.with_title("facett-demo — every component, every palette"),
// Force the eframe wgpu backend onto the DISCRETE GPU: Vulkan-only
// backends (never a GL/llvmpipe fallback), HighPerformance power
// preference, and a custom adapter selector that rejects the `Cpu`
// software rasteriser (llvmpipe/lavapipe) and prefers the NVIDIA/RTX
// `DiscreteGpu`. The chosen adapter is logged at startup. Without this
// the demo has been landing on software/iGPU (~0.1 fps) on Loki.
wgpu_options: facett_demo::gpu_select::discrete_gpu_wgpu_options(),
..Default::default()
};
eframe::run_native(
"facett-demo",
native_options,
Box::new(|cc| {
// Start on the look preset matching the running OS (Linux → Windows-dark,
// the documented default); the switcher re-themes live. `apply` installs
// the full Style AND publishes the legacy palette bridge.
LookTheme::from_os(cc.egui_ctx.os()).apply(&cc.egui_ctx);
// Install the ported wgpu map renderer into the eframe RenderState so
// the map tab's GPU vector-basemap callback has a GpuMapRenderer to
// draw with (it gracefully falls back to the CPU painter without one).
if let Some(rs) = cc.wgpu_render_state.as_ref() {
facett::map::gpu::install_renderer(rs);
#[cfg(feature = "flow-wgpu")]
facett::flow::gpu::install_renderer(rs);
// Install the depth-tested 3D-map renderer too, so the OSM 3D tab
// draws the extruded city through a real GPU Z-buffer (not the CPU
// painter's depth-sort). Graceful fallback without a RenderState.
#[cfg(feature = "map3d-wgpu")]
facett::map3d::gpu::install_renderer(rs);
// Install the INSTANCED 3D-GRAPH CLOUD renderer. Without it the
// graph3d / warehouse-deck / codevault panes tessellate every node
// disc and edge line on ONE CPU core each frame — the measured wall
// in `.nornir/graph3d-rotate-perf-design.md` (projection 0.02 ms/frame
// at 30k nodes, but 2.47M triangles of egui tessellation = 33.75
// ms/frame single-threaded). With it installed, `with_gpu(true)`
// draws the cloud as GPU instances. The lane is fail-safe: a pane that
// opted in without this call falls back to the CPU painter rather than
// rendering blank (facett-graph3d::graph_gpu::cloud).
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
facett::core::render::gpu::graphcloud::cloud::install_cloud_renderer(rs);
// ARM THE GFX v2 LANES. This one call is the difference between the
// `GFX v2` tab showing the renderer track and showing a CPU
// imitation of it: it hands the device over and installs the item-8
// pick host, and the item-4 label collider installs itself on first
// draw. Without it `pick_at` returns `None` (the CPU lane answers),
// `LabelLane::draw` returns false (the CPU painter draws the names),
// and the tab says "device: NOT ARMED" on screen.
//
// It is here, beside the other three installs, for the reason the
// `gpu` feature's own comment records: the instanced GPU cloud
// existed and was proven by tests, but no host armed it, so the live
// GPU path was theoretical and the shipped demo ran the CPU painter
// on an RTX 4090. An un-armed showcase of the renderer track would
// be that same bug wearing this tab's name.
#[cfg(feature = "gfx-v2-gpu")]
facett_demo::gfx_v2::arm(rs);
// LEND THE LAYOUT LANE THE DEVICE. Same defect, fifth instance: the
// compute-shader force layout existed, was measured (100k nodes: 23.7 s
// per CPU step vs 983 ms for fifty GPU steps) and no host lent it a
// device — so the shipped demo laid graphs out on the CPU. With the
// loan, a graph at scale (>= BH_THRESHOLD) runs as a job across a few
// frames instead of converging inside `positions()` on the paint path.
// Below that size, and if this returns false, the CPU lane is unchanged.
#[cfg(feature = "graph-layout-gpu")]
facett_graphnav::install_layout_device(&rs.device, &rs.queue);
}
// NATIVE DEEP-LINK: `$FACETT_DEMO_TAB="GFX v2"` opens that tab on a cold
// start — the desktop twin of the wasm `#<tab>` URL fragment `wasm_start`
// below already honours, and the half that was missing. Without it the
// only ways onto a given tab are a human click or the auto-demo tour, and
// the tour rotates a fixed showcase subset that never reaches `GFX v2`.
let mut app = DemoApp::new();
app.demo.apply_initial_tab(std::env::var("FACETT_DEMO_TAB").ok().as_deref());
// `$FACETT_DEMO_GEN="sbm:100000:1"`: the size + generator for that tab (the twin
// of the web page's `?gen=`).
app.demo.apply_gen_link(std::env::var("FACETT_DEMO_GEN").ok().as_deref());
Ok(Box::new(app))
}),
)
}
// On wasm the real entry point is `wasm_start` below (run by the wasm-bindgen
// `start` shim once the module loads). `main` still has to exist for the bin to
// link, but it does nothing — eframe drives the canvas from `wasm_start`.
#[cfg(target_arch = "wasm32")]
fn main() {}
// ── wasm: the LIVE WEB DEMO entry point ─────────────────────────────────────
// The browser counterpart of `main`: eframe's `WebRunner` mounts the same
// `DemoApp` (the full 27-component deck, incl. the Sverige showcase) onto the
// `<canvas id="facett_demo">` in
// `index.html`, on eframe's **wgpu / WebGPU** backend — the GPU map, 3D-map, graph
// cloud and layout lanes are installed below. The demo is built
// `--no-default-features --features <DEMO_WASM_FEATURES>` for wasm, so
// `region-clip`/osm-katana is dropped and `osm_tabs::load()` uses its synthetic
// in-memory sample (there is no filesystem in the browser).
//
// `#[wasm_bindgen(start)]` runs this automatically when the module is imported by
// the loader snippet in `index.html`.
/// The wasm twin of the install block in `main`: one call per GPU feature the
/// `facett-demo-wasm` row turns on (`facett_build_thing::DEMO_WASM_FEATURES`), and
/// nothing for a feature it does not — installing a lane the bundle cannot dispatch to
/// is the reverse lie. The guard reads this function's body against that axis.
#[cfg(target_arch = "wasm32")]
fn wasm_install_gpu_lanes(rs: &eframe::egui_wgpu::RenderState) {
#[cfg(feature = "flow-wgpu")]
facett::flow::gpu::install_renderer(rs);
#[cfg(feature = "map-wgpu")]
facett::map::gpu::install_renderer(rs);
#[cfg(feature = "map3d-wgpu")]
facett::map3d::gpu::install_renderer(rs);
// ONE renderer, two lanes: `gpu` (graph3d) and `graph-wgpu` (the 2-D GraphView)
// both draw through facett-core `graphcloud`.
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
facett::core::render::gpu::graphcloud::cloud::install_cloud_renderer(rs);
// The LOAN, not a renderer: without it every graph >= BH_THRESHOLD converges on
// wasm32's single thread (row 13, `facet-wrapped-wheel.md`).
#[cfg(feature = "graph-layout-gpu")]
facett_graphnav::install_layout_device(&rs.device, &rs.queue);
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn wasm_start() {
// Pretty panics in the browser console (otherwise wasm panics are opaque).
console_error_panic_hook::set_once();
// Deep-link: `…/facett/#OSM 3D` opens that tab on load — so the headless
// screenshotter (and a shared link) can land on any tab without clicking.
// `?gen=<source>:<size>:<seed>` sets the deep-linked tab's size + generator
// (`DemoState::apply_gen_link`), e.g. `?gen=community:100000#Graphs`.
let initial_gen: Option<String> = web_sys::window()
.and_then(|w| w.location().search().ok())
.and_then(|s| {
s.trim_start_matches('?')
.split('&')
.find_map(|kv| kv.strip_prefix("gen=").map(|v| v.replace("%3A", ":").replace("%3a", ":")))
});
let initial_tab: Option<String> = web_sys::window()
.and_then(|w| w.location().hash().ok())
.map(|h| {
h.trim_start_matches('#').replace("%20", " ").replace('+', " ").trim().to_string()
})
.filter(|s| !s.is_empty());
wasm_bindgen_futures::spawn_local(async move {
use facett::core::adapter_retry::{self, Next};
// NO GPU IS SAID PLAINLY, never a black canvas (owner, 2026-10-01: "loading,
// then black" — the console said only "No available adapters."). Before eframe
// touches the canvas: `navigator.gpu` + `isSecureContext` (never retried), then
// `requestAdapter()` with facett-core's bounded retry. Same policy and the same
// words as every facet-wrapped mount (`facett_core::adapter_retry`).
adapter_retry::record_reason("");
adapter_retry::record_attempts(0);
if let Some(p) = browser_preflight() {
show_no_gpu(adapter_retry::preflight_message(p));
return;
}
let mut attempt = 0;
let adapter = loop {
attempt += 1;
adapter_retry::record_attempts(attempt);
if let Some(a) = request_adapter().await {
break a;
}
match adapter_retry::after_failure(attempt, "requestAdapter() returned null (NotFound)") {
Next::RetryAfter(ms) => {
log::warn!("facett-demo: no WebGPU adapter on attempt {attempt}; retrying in {ms} ms");
sleep_ms(ms).await;
}
Next::Fail(msg) => {
show_no_gpu(&msg);
return;
}
}
};
// A SOFTWARE ADAPTER IS NOT A GPU (owner, 2026-10-02). SwiftShader answers
// requestAdapter() — on loki under VK_LOADER_DRIVERS_SELECT='*nvidia*', on any
// headless Chrome without --enable-gpu — and the lanes installed on it panicked
// in the first resident upload. Refused here, before eframe or any lane exists,
// with facett-core's predicate (the probes refuse runs with the same one).
if let Some(what) = software_adapter_of(&adapter) {
show_no_gpu(&adapter_retry::software_adapter_message(&what));
return;
}
// Grab the canvas the loader created (id="facett_demo").
let Some(canvas) = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id("facett_demo"))
.and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok())
else {
show_no_gpu("facett-demo: the page has no <canvas id=\"facett_demo\">");
return;
};
let start_result = eframe::WebRunner::new()
.start(
canvas,
eframe::WebOptions::default(),
Box::new(move |cc| {
// Same look bridge as native: install the OS-matched preset; the
// in-app switcher re-themes live.
LookTheme::from_os(cc.egui_ctx.os()).apply(&cc.egui_ctx);
// PUT SOMETHING ON THE LANES THIS BUNDLE SELECTS. The pages build has
// been `--features map-wgpu,map3d-wgpu,…` (`DEMO_WASM_FEATURES`) since
// the WebGPU backend landed, and until 2026-09-19 this closure still
// carried the glow-era comment "no wgpu RenderState on the web path"
// and made NO install call — the select-without-install defect
// `facet-wrapped::install_gpu_lanes` exists to end, alive in the
// sibling entry point. Same block as `main` above, same guard
// (`xtask/tests/wasm_bundle_installs_the_lanes_it_selects.rs`).
if let Some(rs) = cc.wgpu_render_state.as_ref() {
wasm_install_gpu_lanes(rs);
}
// navgraph (dev mode): `?nav=labels` starts the overlay, and the
// browser's own Ctrl+G is stopped so the chord can be typed.
facett::core::navgraph::web::install(&cc.egui_ctx);
let mut app = DemoApp::new();
// `apply_initial_tab` PINS the deep-linked tab (the attract tour would
// otherwise leave it after 5 s idle) — the same call native makes.
app.demo.apply_initial_tab(initial_tab.as_deref());
app.demo.apply_gen_link(initial_gen.as_deref());
Ok(Box::new(app))
}),
)
.await;
if let Err(e) = start_result {
// The adapter was there a moment ago; whatever eframe hit, say it on the page.
let err = format!("{e:?}");
let msg = match adapter_retry::after_failure(adapter_retry::MAX_ATTEMPTS, &err) {
Next::Fail(m) => m,
Next::RetryAfter(_) => format!("WebGPU renderer failed to start: {err}"),
};
show_no_gpu(&msg);
}
});
}
/// `navigator.gpu` present and `isSecureContext`, read reflectively (web-sys's WebGPU
/// bindings sit behind `web_sys_unstable_apis`; only existence is needed).
#[cfg(target_arch = "wasm32")]
fn browser_preflight() -> Option<facett::core::adapter_retry::Preflight> {
use wasm_bindgen::JsValue;
let window = web_sys::window()?;
let has_gpu = js_sys::Reflect::get(window.navigator().as_ref(), &JsValue::from_str("gpu"))
.map(|v| !v.is_undefined() && !v.is_null())
.unwrap_or(false);
let secure = js_sys::Reflect::get(window.as_ref(), &JsValue::from_str("isSecureContext"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false);
facett::core::adapter_retry::preflight(has_gpu, secure)
}
/// `await navigator.gpu.requestAdapter()` → the adapter, or `None` for null / a throw.
#[cfg(target_arch = "wasm32")]
async fn request_adapter() -> Option<wasm_bindgen::JsValue> {
use wasm_bindgen::JsValue;
let window = web_sys::window()?;
let gpu = js_sys::Reflect::get(window.navigator().as_ref(), &JsValue::from_str("gpu")).ok()?;
let f = js_sys::Reflect::get(&gpu, &JsValue::from_str("requestAdapter")).ok()?;
let f = f.dyn_into::<js_sys::Function>().ok()?;
let p = f.call0(&gpu).ok()?.dyn_into::<js_sys::Promise>().ok()?;
let a = wasm_bindgen_futures::JsFuture::from(p).await.ok()?;
(!a.is_null() && !a.is_undefined()).then_some(a)
}
/// `Some(what)` when `adapter` (a `GPUAdapter`) is a CPU rasteriser: its `info`
/// strings and `isFallbackAdapter` (on `info` in current Chrome, on the adapter in
/// older ones) through `facett_core::adapter_retry::software_adapter`.
#[cfg(target_arch = "wasm32")]
fn software_adapter_of(adapter: &wasm_bindgen::JsValue) -> Option<String> {
use wasm_bindgen::JsValue;
let info = js_sys::Reflect::get(adapter, &JsValue::from_str("info")).unwrap_or(JsValue::UNDEFINED);
let s = |k: &str| js_sys::Reflect::get(&info, &JsValue::from_str(k)).ok().and_then(|v| v.as_string()).unwrap_or_default();
let flag = |o: &JsValue| js_sys::Reflect::get(o, &JsValue::from_str("isFallbackAdapter")).ok().and_then(|v| v.as_bool());
let fallback = flag(&info).or_else(|| flag(adapter)).unwrap_or(false);
let what = facett::core::adapter_retry::software_adapter(&s("vendor"), &s("architecture"), &s("device"), &s("description"), fallback);
if let Some(w) = &what {
log::error!("facett-demo: the WebGPU adapter is a software rasteriser ({w}) — refusing it");
}
what
}
/// `setTimeout` as a future (the retry backoff).
#[cfg(target_arch = "wasm32")]
async fn sleep_ms(ms: u32) {
let p = js_sys::Promise::new(&mut |resolve, _| {
if let Some(w) = web_sys::window() {
let _ = w.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32);
}
});
let _ = wasm_bindgen_futures::JsFuture::from(p).await;
}
/// Say "no GPU" ON THE PAGE: hide the canvas, drop the spinner, and put the plain
/// message where the demo would have been. Also recorded for a headless driver:
/// `window.__facett_state()` answers `{"gpu":"no GPU","gpu_reason":…}`.
#[cfg(target_arch = "wasm32")]
fn show_no_gpu(msg: &str) {
log::error!("facett-demo: {msg}");
facett::core::adapter_retry::record_reason(msg);
wasm_state::publish(facett::core::adapter_retry::no_gpu_state(msg).to_string());
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
if let Some(c) = doc.get_element_by_id("facett_demo") {
let _ = c.set_attribute("style", "display:none");
}
if let Some(l) = doc.get_element_by_id("loading") {
l.remove();
}
if let Ok(div) = doc.create_element("div") {
div.set_id("facett_no_gpu");
let _ = div.set_attribute(
"style",
"position:absolute;inset:0;display:flex;align-items:center;justify-content:center;\
padding:2em;box-sizing:border-box;color:#e8b04a;background:#0b0d10;\
font:500 16px/1.5 system-ui,sans-serif;text-align:center;white-space:pre-wrap",
);
div.set_text_content(Some(msg));
if let Some(b) = doc.body() {
let _ = b.append_child(&div);
}
}
}
// `JsCast::dyn_into` is needed for the canvas downcast above (wasm only).
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast as _;
// ── wasm: the BROWSER-READABLE state hook (`window.__facett_state()`) ───────────
// The "verify the SHIPPED artifact without a human" seam that was missing. There is
// no filesystem in the browser, so the native `$FACETT_DEMO_STATE` dump is a no-op
// on wasm. Instead the running app publishes its whole `state_json` (every tab +
// control + the `runtrace` "what RAN" ledger) into a process-global string each
// frame, and `__facett_state` — exported to JS — returns it. A headless Chromium
// drive (or the browser console) reads `window.__facett_state()`, parses the JSON,
// and sees exactly what executed in the LIVE wasm bundle: e.g. that the Sverige tab
// rendered (`trace.ran["deck.render:Sverige"] > 0`) and its `11M` / `⚡ select`
// handlers fired (`trace.ran["PopGraphTab::ctl:11M"]`). This proves the shipped
// surface RAN, as data.
#[cfg(target_arch = "wasm32")]
mod wasm_state {
use std::sync::Mutex;
static LATEST: Mutex<String> = Mutex::new(String::new());
/// Publish the latest whole-demo `state_json` string (called once per frame).
pub fn publish(json: String) {
if let Ok(mut g) = LATEST.lock() {
*g = json;
}
}
/// Read the latest published state (the JS hook's backing store).
pub fn latest() -> String {
LATEST.lock().map(|g| g.clone()).unwrap_or_default()
}
}
/// `window.__facett_state()` — return the live demo's whole `state_json` (incl. the
/// `runtrace` "what RAN" ledger) as a JSON string, for a headless drive / the
/// browser console to read what actually executed in the shipped wasm bundle.
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(js_name = __facett_state)]
pub fn facett_state() -> String {
wasm_state::latest()
}
/// The eframe app: a [`DemoState`] plus the live toolbar/menu wiring.
struct DemoApp {
demo: DemoState,
/// Whether to write the whole-demo `state_json` to the `$FACETT_DEMO_STATE`
/// channel every frame. This is a **robot-UI test seam**, not interactive work —
/// it deep-serialises all ~27 facets (a tree that GROWS as the live sim accrues
/// warehouse rows + iceberg snapshots) and `fs::write`s it to disk EVERY frame.
/// Paying that on a normal interactive run was the reported lag/stutter (a 5080
/// idles while the CPU frame-serialises + fsyncs). We only turn it on when an
/// external driver actually asked for the channel by setting the env var.
#[cfg(not(target_arch = "wasm32"))]
dump_state: bool,
/// The per-tab state copies the page's every-frame publish serves inactive tabs from.
#[cfg(target_arch = "wasm32")]
state_cache: facett::core::DeckStateCache,
}
impl DemoApp {
fn new() -> Self {
let mut demo = DemoState::new();
// Opt the deck into fx so the toolbar's glow toggle has something to drive.
demo.deck.fx_mut().glow = false;
// Turn on the AUTO-DEMO / attract mode (#82) for the LIVE binary — the SAME
// `DemoApp::new` constructs both the native window and the wasm web canvas, so
// the kiosk loop (auto-advance after 5 s idle, cancel on interaction, re-arm)
// runs on desktop AND in the browser. Headless robot drives build `DemoState`
// directly and leave it off, so they stay deterministic.
demo.attract_enabled = true;
Self {
demo,
// Only dump when the state channel is explicitly requested (a robot drive /
// external tool set `$FACETT_DEMO_STATE`). A human interactive run leaves it
// unset → zero per-frame serialise + disk write → the frame is GPU-bound again.
#[cfg(not(target_arch = "wasm32"))]
dump_state: std::env::var_os("FACETT_DEMO_STATE").is_some(),
#[cfg(target_arch = "wasm32")]
state_cache: facett::core::DeckStateCache::default(),
}
}
}
impl eframe::App for DemoApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
#[cfg(feature = "flow-wgpu")]
if let Some(rs) = _frame.wgpu_render_state() {
facett::flow::gpu::evict_idle(rs, ui.ctx().cumulative_frame_nr());
}
// ── Robot-UI drive-half (EPIC-R parity with nornir's viz) ──
// Poll the control channel ($FACETT_DEMO_CMD) BEFORE drawing, so a switched
// tab / palette / command is reflected in THIS frame. Consume-once.
if self.demo.poll_control_channel() {
ui.ctx().request_repaint();
}
// One render path shared with the robot-UI test (`DemoState::draw_app`),
// so the binary and the robot drive exactly the same UI (CLI/GUI parity).
self.demo.draw_app(ui);
// ── Robot-UI read-half ──
// Dump the whole demo's aggregated state_json to $FACETT_DEMO_STATE so an
// external driver sees what's on screen (every control + tab + component).
// On wasm there's no filesystem, so we ALSO publish the state into the
// `window.__facett_state()`-backed global so a headless browser drive reads
// exactly what ran in the SHIPPED bundle.
#[cfg(target_arch = "wasm32")]
{
// Rolling: the tab on screen every frame, one inactive tab per frame in
// turn (`DemoState::state_json_rolling`) — not all 81 tabs every frame.
wasm_state::publish(self.demo.publish_rolling(&mut self.state_cache));
}
#[cfg(not(target_arch = "wasm32"))]
if self.dump_state {
self.demo.dump_state();
}
}
}