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

nordisk/facett

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)
    }
}