facett-flow/src/gpu/variant.rs
//! Optional attributes never rebuild geometry. Group 0 and the plain shader stay
//! unchanged; group 1 is allocated only after a scene actually sets a table.
use super::*;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct Uniforms {
ok: [f32; 4],
slow: [f32; 4],
error: [f32; 4],
options: [f32; 4],
}
impl Uniforms {
#[allow(unused_mut)] // optional palette and LOD uniforms
pub(super) fn from_palette(
_p: &crate::FlowPalette,
ppp: f32,
_opts: &crate::PaintOpts,
) -> Self {
let mut out = Self {
ok: [0.0; 4],
slow: [0.0; 4],
error: [0.0; 4],
options: [0.0, 1.0, ppp, 0.0],
};
#[cfg(feature = "status")]
{
out.ok = gpu::color32_to_f32(_p.status.ok);
out.slow = gpu::color32_to_f32(_p.status.slow);
out.error = gpu::color32_to_f32(_p.status.error);
}
#[cfg(feature = "lod")]
{
out.options[3] = _opts.min_packet_px.max(0.0);
}
out
}
}
pub(super) struct Resident {
generation: u64,
pub bind: wgpu::BindGroup,
uniform: wgpu::Buffer,
_nodes: wgpu::Buffer,
_edges: wgpu::Buffer,
pub arrows: bool,
pub packets: u32,
}
pub(super) struct Pipelines {
layout: wgpu::BindGroupLayout,
pub node: wgpu::RenderPipeline,
pub edge: wgpu::RenderPipeline,
pub pulse: wgpu::RenderPipeline,
pub arrow: wgpu::RenderPipeline,
}
impl Pipelines {
pub fn new(device: &wgpu::Device, base: &wgpu::BindGroupLayout) -> Self {
let entries: Vec<_> = (0..3)
.map(|binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: if binding == 0 {
wgpu::BufferBindingType::Uniform
} else {
wgpu::BufferBindingType::Storage { read_only: true }
},
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
})
.collect();
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("flow attributes"),
entries: &entries,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("flow extension"),
source: wgpu::ShaderSource::Wgsl(shader_source().into()),
});
let pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("flow extension layout"),
bind_group_layouts: &[Some(base), 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 {
node: pipeline("node_vs_x", "node_fs_x"),
edge: pipeline("edge_vs_x", "edge_fs_x"),
pulse: pipeline("pulse_vs_x", "pulse_fs"),
arrow: pipeline("arrow_vs_x", "arrow_fs_x"),
layout,
}
}
pub fn upload(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pane: &mut super::Resident,
attributes: Option<&crate::host::Attributes>,
extra: &Uniforms,
) -> Result<(), String> {
let Some(a) = attributes else {
pane.attributes = None;
return Ok(());
};
if pane
.attributes
.as_ref()
.is_none_or(|old| old.generation != a.generation)
{
let start = Instant::now();
let empty = [[0.0_f32; 4]];
let nodes = if a.nodes.is_empty() {
&empty[..]
} else {
&a.nodes
};
// One f32 speed + packed count/status/explicit-rate flags: 8 bytes/edge.
let packed: Vec<[u32; 2]> = a
.edges
.iter()
.map(|e| {
[
e[0].to_bits(),
(e[1] as u32) | ((e[2] as u32) << 8) | ((e[3] as u32) << 10),
]
})
.collect();
let empty_edge = [[0_u32; 2]];
let edges = if packed.is_empty() {
&empty_edge[..]
} else {
&packed
};
let nodes_buffer = checked_buffer(
device,
queue,
"flow node attributes",
bytemuck::cast_slice(nodes),
wgpu::BufferUsages::STORAGE,
)?;
let edges_buffer = checked_buffer(
device,
queue,
"flow edge attributes",
bytemuck::cast_slice(edges),
wgpu::BufferUsages::STORAGE,
)?;
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("flow attribute palette"),
size: 64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("flow attribute bind"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: nodes_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: edges_buffer.as_entire_binding(),
},
],
});
let packets = a
.edges
.iter()
.map(|v| v[1] as u32)
.max()
.unwrap_or(1)
.max(1);
pane.attributes = Some(Resident {
generation: a.generation,
bind,
uniform,
_nodes: nodes_buffer,
_edges: edges_buffer,
arrows: a.arrows,
packets,
});
lane::note_upload(
lane_name(),
start.elapsed().as_micros() as u64,
(nodes.len() * 16 + edges.len() * 8) as u64,
);
}
let resident = pane.attributes.as_ref().unwrap();
let mut u = *extra;
u.options[0] = f32::from(resident.arrows);
u.options[1] = resident.packets as f32;
queue.write_buffer(&resident.uniform, 0, bytemuck::bytes_of(&u));
Ok(())
}
}
fn shader_source() -> String {
let mut source = String::from(concat!(
include_str!("../flow.wgsl"),
"\n",
include_str!("../flow_ext.wgsl")
));
#[cfg(feature = "boxes")]
source.push_str(include_str!("../hooks/boxes_on.wgsl"));
#[cfg(not(feature = "boxes"))]
source.push_str(include_str!("../hooks/boxes_off.wgsl"));
#[cfg(feature = "arrows")]
source.push_str(include_str!("../hooks/arrows_on.wgsl"));
#[cfg(not(feature = "arrows"))]
source.push_str(include_str!("../hooks/arrows_off.wgsl"));
#[cfg(feature = "status")]
source.push_str(include_str!("../hooks/status_on.wgsl"));
#[cfg(not(feature = "status"))]
source.push_str(include_str!("../hooks/status_off.wgsl"));
#[cfg(feature = "rate")]
source.push_str(include_str!("../hooks/rate_on.wgsl"));
#[cfg(not(feature = "rate"))]
source.push_str(include_str!("../hooks/rate_off.wgsl"));
#[cfg(feature = "lod")]
source.push_str(include_str!("../hooks/lod_on.wgsl"));
#[cfg(not(feature = "lod"))]
source.push_str(include_str!("../hooks/lod_off.wgsl"));
source
}