facett-graphview/src/resident.rs
//! **The RESIDENT GPU cloud lane for [`DecoratedGraphView`] at scale** (above
//! [`cloud::RESIDENT_ABOVE`] nodes, `with_gpu(true)`, a renderer installed).
//!
//! The per-frame lane ([`crate::legible::draw_gpu_cloud`]) rebuilt an id → index
//! `HashMap` and one instance per node and edge every frame, and the pane around it
//! rebuilt the unfold's `BTreeSet<String>` visible set, a second id map and the
//! legibility plan — 2.8 fps at 100 000 nodes on the t14s Radeon 890M (lane D,
//! 2026-10-01). Here all of that is computed once per MODEL (or per unfold change, or per
//! selection for the lit trace), the points + edge index pairs are uploaded once
//! ([`ResidentSlot`]), and a frame sends the camera plus the small overlays: the
//! selection ring, decoration rings + badges, decoration (cut) edges, the lit downstream
//! trace, and labels for the chips on screen.
//!
//! The cloud holds the VISIBLE nodes only (a folded subtree is not uploaded — the
//! per-frame lane drew every node regardless of the unfold), coloured by fill. A
//! selection does not dim it (nor did the per-frame lane); the trace is drawn over it.
//!
//! [`DecoratedGraphView`]: crate::decorated_view::DecoratedGraphView
use std::collections::{BTreeSet, HashMap};
use facett_core::render::gpu::graphcloud::cloud::{self, ResidentSlot};
use facett_core::render::gpu::graphcloud::{CloudCamera, CloudEdge, CloudPoint};
use crate::model::GraphModel;
/// Most lit-trace edges drawn over the cloud for a selection (the rest are in the cloud).
pub const LIT_EDGE_CAP: usize = 4_096;
/// Most labels painted in one frame on this lane (the on-screen ones, in model order).
pub const LABEL_CAP: usize = 2_000;
/// The per-model / per-unfold / per-selection work of the resident lane.
#[derive(Default)]
pub(crate) struct ResidentCache {
pub(crate) slot: ResidentSlot,
/// `(model generation, unfold generation)` the caches below were built for.
key: Option<(u64, u64)>,
model_key: Option<u64>,
pub(crate) generation: u64,
pub(crate) idx: HashMap<String, u32>,
/// Index-aligned with `model.nodes`: shown under the current unfold.
pub(crate) visible: Vec<bool>,
pub(crate) visible_count: usize,
lit_for: Option<Option<String>>,
pub(crate) lit: BTreeSet<String>,
pub(crate) lit_edges: Vec<u32>,
/// `(model, unfold, decorations)` generation [`decorated`](Self::decorated) is for.
deco_key: Option<(u64, u64, u64)>,
/// Shown nodes whose decoration draws a ring or a badge, ascending.
pub(crate) decorated: Vec<usize>,
}
impl ResidentCache {
/// Bring the caches up to `(model_gen, vis_gen)` and `selected`. `visible` is only
/// asked for when the unfold (or the model) changed.
pub(crate) fn sync(
&mut self,
model_gen: u64,
vis_gen: u64,
model: &GraphModel,
visible: impl FnOnce() -> BTreeSet<String>,
lit: impl FnOnce() -> BTreeSet<String>,
selected: Option<&String>,
) {
if self.key != Some((model_gen, vis_gen)) {
if self.model_key != Some(model_gen) {
self.idx = model.nodes.iter().enumerate().map(|(i, n)| (n.id.clone(), i as u32)).collect();
self.model_key = Some(model_gen);
self.lit_for = None;
}
let vis = visible();
self.visible = model.nodes.iter().map(|n| vis.contains(&n.id)).collect();
self.visible_count = self.visible.iter().filter(|&&v| v).count();
self.key = Some((model_gen, vis_gen));
self.generation = cloud::fresh_generation();
}
if self.lit_for.as_ref().map(Option::as_ref) != Some(selected) {
self.lit = lit();
self.lit_edges = if self.lit.is_empty() {
Vec::new()
} else {
model
.edges
.iter()
.enumerate()
.filter(|(_, e)| self.lit.contains(&e.from) && self.lit.contains(&e.to))
.map(|(i, _)| i as u32)
.collect()
};
self.lit_for = Some(selected.cloned());
}
}
/// Bring [`decorated`](Self::decorated) up to `(model_gen, vis_gen, deco_gen)`. Call
/// after [`sync`](Self::sync) (it reads the id map and the unfold).
pub(crate) fn sync_decorated(&mut self, model_gen: u64, vis_gen: u64, deco_gen: u64, deco: &crate::model::Decorations) {
if self.deco_key == Some((model_gen, vis_gen, deco_gen)) {
return;
}
let mut d: Vec<usize> = deco
.nodes
.iter()
.filter(|(_, nd)| nd.ring.is_some() || nd.badge.is_some())
.filter_map(|(id, _)| self.idx.get(id).map(|&i| i as usize))
.filter(|&i| self.shown(i))
.collect();
d.sort_unstable();
d.dedup();
self.decorated = d;
self.deco_key = Some((model_gen, vis_gen, deco_gen));
}
/// Is node `i` shown under the current unfold?
pub(crate) fn shown(&self, i: usize) -> bool {
self.visible.get(i).copied().unwrap_or(false)
}
/// Push the cloud: the camera every frame, the visible model once per generation.
/// `false` when the lane cannot draw (the caller's per-frame / CPU road then runs).
pub(crate) fn draw(&self, ui: &egui::Ui, rect: egui::Rect, model: &GraphModel, cam: CloudCamera) -> bool {
let build = || {
let mut remap = vec![u32::MAX; model.nodes.len()];
let mut pts: Vec<CloudPoint> = Vec::with_capacity(self.visible_count);
for (i, n) in model.nodes.iter().enumerate() {
if self.shown(i) {
remap[i] = pts.len() as u32;
let c = n.fill;
pts.push(CloudPoint::new([n.pos.x, -n.pos.y, 0.0], egui::Color32::from_rgba_unmultiplied(c.r, c.g, c.b, c.a)));
}
}
let eds: Vec<CloudEdge> = model
.edges
.iter()
.filter_map(|e| {
let (a, b) = (remap[*self.idx.get(&e.from)? as usize], remap[*self.idx.get(&e.to)? as usize]);
(a != u32::MAX && b != u32::MAX).then_some([a, b])
})
.collect();
(pts, eds)
};
self.slot.draw(ui, rect, self.generation, build, cam)
}
}