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