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

nordisk/facett

facett-flow/src/gpu.rs

//! Resident curve renderer built on Facett's L0 GPU host plumbing. One resource
//! entry per facet identity, so multiple panes cannot overwrite each other's data.
use crate::scene::{Geometry, Link, Point};
use egui_wgpu::wgpu;
use facett_core::render::{
    gpu::{
        self, PassClockSlot,
        graphcloud::checked_buffer,
        offscreen::{OFFSCREEN_FORMAT, OffscreenColorDepth},
    },
    lane,
};
use std::{collections::HashMap, sync::Arc};
use web_time::Instant;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
    viewport: [f32; 4],
    motion: [f32; 4],
    input: [f32; 4],
    middle: [f32; 4],
    output: [f32; 4],
    edge: [f32; 4],
    pulse: [f32; 4],
    detail: [f32; 4],
}
#[cfg(feature = "host")]
mod variant;

struct Resident {
    #[cfg(feature = "host")]
    attributes: Option<variant::Resident>,
    generation: u64,
    /// The last egui frame this pane was prepared in — the eviction clock.
    last_frame: u64,
    nodes: u32,
    edges: u32,
    uniform: wgpu::Buffer,
    bind: wgpu::BindGroup,
    _points: wgpu::Buffer,
    _links: wgpu::Buffer,
    target: OffscreenColorDepth,
}
pub struct FlowRenderer {
    #[cfg(feature = "host")]
    variant: variant::Pipelines,
    layout: wgpu::BindGroupLayout,
    node: wgpu::RenderPipeline,
    edge: wgpu::RenderPipeline,
    pulse: wgpu::RenderPipeline,
    panes: HashMap<egui::Id, Resident>,
    format: wgpu::TextureFormat,
    clock: PassClockSlot,
    /// Offscreen targets ever created — a new pane makes one, a new generation reuses
    /// the pane's (`tests/gpu_panes.rs`).
    targets_made: u64,
}
pub fn lane_name() -> &'static str {
    std::any::type_name::<FlowRenderer>()
}
/// Install through Facett's checked, nonblocking lifecycle. Validation failures
/// are recorded in the shared lane and leave the bounded CPU preview active.
pub fn install_renderer(state: &egui_wgpu::RenderState) -> bool {
    let installed = gpu::install_renderer_checked(state, FlowRenderer::new);
    lane::note_reports_frames(lane_name());
    installed
}
impl FlowRenderer {
    fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("flow layout"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::VERTEX,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::VERTEX,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("flow shader"),
            source: wgpu::ShaderSource::Wgsl(include_str!("flow.wgsl").into()),
        });
        let pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("flow pipeline layout"),
            bind_group_layouts: &[Some(&layout)],
            immediate_size: 0,
        });
        let pipeline = |vs: &str, fs: &str| {
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some(vs),
                layout: Some(&pll),
                vertex: wgpu::VertexState {
                    module: &shader,
                    entry_point: Some(vs),
                    buffers: &[],
                    compilation_options: Default::default(),
                },
                fragment: Some(wgpu::FragmentState {
                    module: &shader,
                    entry_point: Some(fs),
                    targets: &[Some(wgpu::ColorTargetState {
                        format: OFFSCREEN_FORMAT,
                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                        write_mask: wgpu::ColorWrites::ALL,
                    })],
                    compilation_options: Default::default(),
                }),
                primitive: wgpu::PrimitiveState::default(),
                depth_stencil: None,
                multisample: gpu::msaa_state(),
                multiview_mask: None,
                cache: None,
            })
        };
        Self {
            #[cfg(feature = "host")]
            variant: variant::Pipelines::new(device, &layout),
            node: pipeline("node_vs", "node_fs"),
            edge: pipeline("edge_vs", "edge_fs"),
            pulse: pipeline("pulse_vs", "pulse_fs"),
            layout,
            panes: HashMap::new(),
            format,
            clock: PassClockSlot::default(),
            targets_made: 0,
        }
    }
    fn upload(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        id: egui::Id,
        geometry: &Geometry,
    ) -> Result<(), String> {
        if self
            .panes
            .get(&id)
            .is_some_and(|r| r.generation == geometry.generation)
        {
            return Ok(());
        }
        let start = Instant::now();
        // Empty storage buffers still need one full element for shader validation.
        // Borrowed, never cloned: the geometry is already resident in an Arc.
        let no_point = [<Point as bytemuck::Zeroable>::zeroed()];
        let no_link = [<Link as bytemuck::Zeroable>::zeroed()];
        let points: &[Point] = if geometry.points.is_empty() {
            &no_point
        } else {
            &geometry.points
        };
        let links: &[Link] = if geometry.links.is_empty() {
            &no_link
        } else {
            &geometry.links
        };
        let point_buffer = checked_buffer(
            device,
            queue,
            "flow points",
            bytemuck::cast_slice(points),
            wgpu::BufferUsages::STORAGE,
        )?;
        let link_buffer = checked_buffer(
            device,
            queue,
            "flow connections",
            bytemuck::cast_slice(links),
            wgpu::BufferUsages::STORAGE,
        )?;
        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("flow camera / phase"),
            size: std::mem::size_of::<Uniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("flow resident bind"),
            layout: &self.layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: uniform.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: point_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: link_buffer.as_entire_binding(),
                },
            ],
        });
        // A new generation keeps the pane's offscreen target (and its blit pipeline).
        let target = match self.panes.remove(&id) {
            Some(old) => old.target,
            None => {
                self.targets_made += 1;
                OffscreenColorDepth::new(device, self.format)
            }
        };
        self.panes.insert(
            id,
            Resident {
                #[cfg(feature = "host")]
                attributes: None,
                generation: geometry.generation,
                last_frame: 0,
                nodes: geometry.points.len() as u32,
                edges: geometry.links.len() as u32,
                uniform,
                bind,
                _points: point_buffer,
                _links: link_buffer,
                target,
            },
        );
        lane::note_upload(
            lane_name(),
            start.elapsed().as_micros() as u64,
            ((points.len() + links.len()) * 16) as u64,
        );
        Ok(())
    }
}
/// `(resident panes, offscreen targets ever made)` of the renderer installed on `rs`,
/// `(0, 0)` without one. The witness for the pane rule in `tests/gpu_panes.rs`; not API.
#[doc(hidden)]
pub fn residency(rs: &egui_wgpu::RenderState) -> (usize, u64) {
    rs.renderer
        .read()
        .callback_resources
        .get::<FlowRenderer>()
        .map_or((0, 0), |r| (r.panes.len(), r.targets_made))
}
struct Callback {
    #[cfg(feature = "host")]
    attributes: Option<Arc<crate::host::Attributes>>,
    #[cfg(feature = "host")]
    extra: variant::Uniforms,
    id: egui::Id,
    geometry: Arc<Geometry>,
    uniform: Uniforms,
    background: wgpu::Color,
    size: [u32; 2],
    frame: u64,
}
impl egui_wgpu::CallbackTrait for Callback {
    fn prepare(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        _screen: &egui_wgpu::ScreenDescriptor,
        encoder: &mut wgpu::CommandEncoder,
        resources: &mut egui_wgpu::CallbackResources,
    ) -> Vec<wgpu::CommandBuffer> {
        let Some(r) = resources.get_mut::<FlowRenderer>() else {
            return vec![];
        };
        // A pane drawn neither last frame nor this one is gone (closed tab, dropped
        // facet): free its buffers and offscreen target — facett-map's pane rule.
        r.panes.retain(|_, p| p.last_frame + 1 >= self.frame);
        if let Err(why) = r.upload(device, queue, self.id, &self.geometry) {
            r.panes.remove(&self.id);
            lane::note_gpu_error(lane_name(), &why);
            return vec![];
        }
        #[cfg(feature = "host")]
        if let Err(why) = r.variant.upload(
            device,
            queue,
            r.panes.get_mut(&self.id).unwrap(),
            self.attributes.as_deref(),
            &self.extra,
        ) {
            r.panes.remove(&self.id);
            lane::note_gpu_error(lane_name(), &why);
            return vec![];
        }
        let Some(pane) = r.panes.get_mut(&self.id) else {
            return vec![];
        };
        pane.last_frame = self.frame;
        pane.target.ensure(device, self.size[0], self.size[1]);
        let Some(view) = pane.target.color_view() else {
            return vec![];
        };
        queue.write_buffer(&pane.uniform, 0, bytemuck::bytes_of(&self.uniform));
        if let Some(clock) = r.clock.get_or_init(device, queue, lane_name()) {
            clock.begin(device, encoder, self.frame);
        }
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("flow curves and packets"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(self.background),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_bind_group(0, &pane.bind, &[]);
            #[cfg(feature = "host")]
            let extended = if let Some(attr) = &pane.attributes {
                pass.set_bind_group(1, &attr.bind, &[]);
                pass.set_pipeline(&r.variant.edge);
                pass.draw(0..self.uniform.detail[0] as u32 * 6, 0..pane.edges);
                if self.uniform.motion[2] > 0.5 {
                    pass.set_pipeline(&r.variant.pulse);
                    pass.draw(0..6, 0..pane.edges.saturating_mul(attr.packets));
                }
                if attr.arrows {
                    pass.set_pipeline(&r.variant.arrow);
                    pass.draw(0..3, 0..pane.edges);
                }
                pass.set_pipeline(&r.variant.node);
                pass.draw(0..6, 0..pane.nodes);
                true
            } else {
                false
            };
            #[cfg(not(feature = "host"))]
            let extended = false;
            if !extended {
                pass.set_pipeline(&r.edge);
                pass.draw(0..self.uniform.detail[0] as u32 * 6, 0..pane.edges);
                if self.uniform.motion[2] > 0.5 {
                    pass.set_pipeline(&r.pulse);
                    pass.draw(0..6, 0..pane.edges);
                }
                pass.set_pipeline(&r.node);
                pass.draw(0..6, 0..pane.nodes);
            }
        }
        if let Some(clock) = r.clock.ready_mut() {
            clock.end(encoder);
        }
        lane::note_frame(lane_name());
        vec![]
    }
    fn paint(
        &self,
        info: egui::PaintCallbackInfo,
        pass: &mut wgpu::RenderPass<'static>,
        resources: &egui_wgpu::CallbackResources,
    ) {
        if let Some(pane) = resources
            .get::<FlowRenderer>()
            .and_then(|r| r.panes.get(&self.id))
            && pane.generation == self.geometry.generation
            && pane.target.ready()
        {
            pane.target.blit(&info, pass);
        }
    }
}
pub(crate) fn draw(
    ui: &egui::Ui,
    rect: egui::Rect,
    geometry: Arc<Geometry>,
    centre: egui::Pos2,
    scale: f32,
    seconds: f32,
    moving: bool,
) -> bool {
    // Headless / CPU hosts can use the exact same facet without this GPU install.
    if !lane::is_installed(lane_name()) {
        return false;
    }
    let ppp = ui.ctx().pixels_per_point();
    let t = facett_core::theme(ui);
    // Facett's shared blit copies colours into egui's gamma-space framebuffer.
    // Use its colour helper so the GPU pane matches the surrounding host palette.
    let colour = gpu::color32_to_f32;
    let bg = colour(t.bg);
    let w = (rect.width() * ppp).round().max(1.0) as u32;
    let h = (rect.height() * ppp).round().max(1.0) as u32;
    let uniform = Uniforms {
        viewport: [w as f32, h as f32, centre.x, centre.y],
        motion: [
            scale * ppp,
            seconds,
            if moving { 1.0 } else { 0.0 },
            if geometry.points.len() > 1000 {
                0.13
            } else {
                0.4
            },
        ],
        input: colour(t.point),
        middle: colour(t.accent),
        output: colour(t.glow),
        edge: colour(t.edge),
        pulse: colour(t.accent),
        detail: [geometry.curve_segments(scale * ppp) as f32, 0.0, 0.0, 0.0],
    };
    ui.painter().add(egui_wgpu::Callback::new_paint_callback(
        rect,
        Callback {
            #[cfg(feature = "host")]
            attributes: None,
            #[cfg(feature = "host")]
            extra: <variant::Uniforms as bytemuck::Zeroable>::zeroed(),
            id: ui.id(),
            geometry,
            uniform,
            background: wgpu::Color {
                r: bg[0] as f64,
                g: bg[1] as f64,
                b: bg[2] as f64,
                a: 1.0,
            },
            size: [w, h],
            frame: ui.ctx().cumulative_frame_nr(),
        },
    ));
    true
}

/// Free panes unused for two frames even when the host no longer paints any flow.
#[cfg(feature = "host")]
pub fn evict_idle(rs: &egui_wgpu::RenderState, frame: u64) -> usize {
    if let Some(r) = rs
        .renderer
        .write()
        .callback_resources
        .get_mut::<FlowRenderer>()
    {
        let before = r.panes.len();
        r.panes
            .retain(|_, p| p.last_frame.saturating_add(1) >= frame);
        return before - r.panes.len();
    }
    0
}
#[cfg(feature = "host")]
#[allow(clippy::too_many_arguments)] // bridge preserves the public host paint contract plus GPU tables
pub(crate) fn draw_host(
    ui: &egui::Ui,
    id: egui::Id,
    rect: egui::Rect,
    geometry: Arc<Geometry>,
    view: &crate::View,
    opts: &crate::PaintOpts,
    palette: &crate::FlowPalette,
    attributes: Option<Arc<crate::host::Attributes>>,
) -> bool {
    if !lane::is_installed(lane_name()) {
        return false;
    }
    let ppp = ui.ctx().pixels_per_point();
    let w = (rect.width() * ppp).round().max(1.0) as u32;
    let h = (rect.height() * ppp).round().max(1.0) as u32;
    let centre = view.world(rect.center());
    let colour = gpu::color32_to_f32;
    let bg = colour(palette.background);
    let uniform = Uniforms {
        viewport: [w as f32, h as f32, centre.x, centre.y],
        motion: [
            view.scale * ppp,
            opts.seconds as f32,
            if opts.moving && opts.animation == crate::Animation::Flow {
                1.0
            } else {
                0.0
            },
            if geometry.points.len() > 1000 {
                0.13
            } else {
                0.4
            },
        ],
        input: colour(palette.input),
        middle: colour(palette.middle),
        output: colour(palette.output),
        edge: colour(palette.edge),
        pulse: colour(palette.pulse),
        detail: [
            geometry.curve_segments(view.scale * ppp) as f32,
            0.0,
            0.0,
            0.0,
        ],
    };
    let extra = variant::Uniforms::from_palette(palette, ppp, opts);
    ui.painter().add(egui_wgpu::Callback::new_paint_callback(
        rect,
        Callback {
            id,
            geometry,
            uniform,
            attributes,
            extra,
            background: wgpu::Color {
                r: bg[0] as f64,
                g: bg[1] as f64,
                b: bg[2] as f64,
                a: bg[3] as f64,
            },
            size: [w, h],
            frame: ui.ctx().cumulative_frame_nr(),
        },
    ));
    true
}
#[cfg(test)]
mod layout_tests {
    #[test]
    fn plain_uniform_retains_main_adaptive_lod_layout() {
        assert_eq!(std::mem::size_of::<super::Uniforms>(), 128);
        assert_eq!(std::mem::size_of::<super::Point>(), 16);
        assert_eq!(std::mem::size_of::<super::Link>(), 16);
    }
}