#![allow(clippy::too_many_lines, reason = "self-contained instanced+bloom pipeline set")]
use egui_wgpu::wgpu;
use crate::render::gpu::offscreen::{OffscreenColorDepth, OFFSCREEN_FORMAT};
use crate::render::gpu::GaussianBlur;
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct NodeInstance {
pub center: [f32; 2],
pub radius: f32,
pub _pad: f32,
pub color: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct EdgeInstance {
pub a: [f32; 2],
pub b: [f32; 2],
pub width: f32,
pub _pad: f32,
pub color: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CloudPoint {
pub pos: [f32; 3],
pub color: u32,
}
impl CloudPoint {
#[must_use]
pub fn new(pos: [f32; 3], color: egui::Color32) -> Self {
let [r, g, b, a] = color.to_array();
Self { pos, color: u32::from(r) | (u32::from(g) << 8) | (u32::from(b) << 16) | (u32::from(a) << 24) }
}
}
pub type CloudEdge = [u32; 2];
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CloudCamera {
pub viewport: [f32; 4],
pub cam: [f32; 4],
pub edge: [f32; 4],
}
impl CloudCamera {
#[must_use]
pub fn frame(w: f32, h: f32, yaw: f32, emissive: f32, shrink: f32) -> Self {
Self {
viewport: [w.max(1.0), h.max(1.0), w * 0.5, h * 0.5],
cam: [yaw, w.min(h) * 0.40, emissive, shrink],
edge: [1.5, 0.6, 0.0, 0.0],
}
}
#[must_use]
pub fn plane(size_px: [f32; 2], origin_px: [f32; 2], scale_px: f32, radius_px: f32, edge_half_px: f32, emissive: f32) -> Self {
Self {
viewport: [size_px[0].max(1.0), size_px[1].max(1.0), origin_px[0], origin_px[1]],
cam: [0.0, scale_px, emissive, radius_px.max(0.5) / 5.5],
edge: [edge_half_px.max(0.5), 0.6, 0.0, 0.0],
}
}
}
pub struct CloudBuffers {
bg: wgpu::BindGroup,
pub node_count: u32,
pub edge_count: u32,
pub generation: u64,
_pts: wgpu::Buffer,
_edg: wgpu::Buffer,
}
enum Src<'a> {
Instances(&'a GraphScene),
Cloud(&'a CloudBuffers),
}
#[derive(Clone, Default)]
pub struct GraphScene {
pub background: [f32; 4],
pub nodes: Vec<NodeInstance>,
pub edges: Vec<EdgeInstance>,
}
#[derive(Clone, Copy, Debug)]
pub struct BloomParams {
pub threshold: f32,
pub knee: f32,
pub intensity: f32,
pub exposure: f32,
pub sigma: f32,
}
impl Default for BloomParams {
fn default() -> Self {
Self { threshold: 1.0, knee: 0.6, intensity: 1.3, exposure: 1.0, sigma: 3.0 }
}
}
impl BloomParams {
#[must_use]
pub fn off(self) -> Self {
Self { intensity: 0.0, ..self }
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct SceneUniforms {
viewport: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct PostUniforms {
params: [f32; 4],
}
pub struct GraphRender {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
pub node_instances: u32,
pub edge_instances: u32,
}
impl GraphRender {
#[must_use]
pub fn mean_luma(&self) -> f64 {
let mut sum = 0.0f64;
for px in self.rgba.as_chunks::<4>().0 {
sum += 0.299 * px[0] as f64 + 0.587 * px[1] as f64 + 0.114 * px[2] as f64;
}
let n = (self.width * self.height) as f64;
if n > 0.0 { sum / (n * 255.0) } else { 0.0 }
}
#[must_use]
pub fn luma_at(&self, x: u32, y: u32) -> f64 {
if x >= self.width || y >= self.height {
return 0.0;
}
let i = ((y * self.width + x) * 4) as usize;
(0.299 * self.rgba[i] as f64 + 0.587 * self.rgba[i + 1] as f64 + 0.114 * self.rgba[i + 2] as f64) / 255.0
}
}
pub struct GraphGpu {
node_pipeline: wgpu::RenderPipeline,
edge_pipeline: wgpu::RenderPipeline,
bright_pipeline: wgpu::RenderPipeline,
composite_pipeline: wgpu::RenderPipeline,
scene_bgl: wgpu::BindGroupLayout,
sample_bgl: wgpu::BindGroupLayout,
composite_bgl: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
scene_u: wgpu::Buffer,
post_u: wgpu::Buffer,
cloud_node_pipeline: wgpu::RenderPipeline,
cloud_edge_pipeline: wgpu::RenderPipeline,
cloud_bgl: wgpu::BindGroupLayout,
cloud_u: wgpu::Buffer,
scene_bg: wgpu::BindGroup,
blur: GaussianBlur,
ldr_format: wgpu::TextureFormat,
}
impl GraphGpu {
pub fn new(device: &wgpu::Device, ldr_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("graph3d_gpu"),
source: wgpu::ShaderSource::Wgsl(crate::render::wgsl::GRAPHCLOUD_WGSL.into()),
});
let scene_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("graph3d_scene_bgl"),
entries: &[uniform_entry(0, wgpu::ShaderStages::VERTEX)],
});
let sample_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("graph3d_sample_bgl"),
entries: &[
uniform_entry(0, wgpu::ShaderStages::FRAGMENT),
tex_entry(1),
sampler_entry(2),
],
});
let composite_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("graph3d_composite_bgl"),
entries: &[
uniform_entry(0, wgpu::ShaderStages::FRAGMENT),
tex_entry(1),
sampler_entry(2),
tex_entry(3),
],
});
let additive = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
};
let node_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("graph3d_node_pll"),
bind_group_layouts: &[Some(&scene_bgl)],
immediate_size: 0,
});
let node_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("graph3d_node_pipeline"),
layout: Some(&node_pll),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("node_vs"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<NodeInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 0, shader_location: 0 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32, offset: 8, shader_location: 1 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x4, offset: 16, shader_location: 2 },
],
})],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: crate::render::gpu::msaa_state(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("node_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: OFFSCREEN_FORMAT,
blend: Some(additive),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let edge_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("graph3d_edge_pll"),
bind_group_layouts: &[Some(&scene_bgl)],
immediate_size: 0,
});
let edge_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("graph3d_edge_pipeline"),
layout: Some(&edge_pll),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("edge_vs"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<EdgeInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 0, shader_location: 0 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 8, shader_location: 1 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32, offset: 16, shader_location: 2 },
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x4, offset: 24, shader_location: 3 },
],
})],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: crate::render::gpu::msaa_state(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("edge_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: OFFSCREEN_FORMAT,
blend: Some(additive),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let bright_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("graph3d_bright_pll"),
bind_group_layouts: &[Some(&sample_bgl)],
immediate_size: 0,
});
let bright_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("graph3d_bright_pipeline"),
layout: Some(&bright_pll),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("post_vs"),
buffers: &[],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: crate::render::gpu::msaa_state(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("bright_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: OFFSCREEN_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let composite_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("graph3d_composite_pll"),
bind_group_layouts: &[Some(&composite_bgl)],
immediate_size: 0,
});
let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("graph3d_composite_pipeline"),
layout: Some(&composite_pll),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("post_vs"),
buffers: &[],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: crate::render::gpu::msaa_state(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("composite_fs"),
targets: &[Some(wgpu::ColorTargetState {
format: ldr_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("graph3d_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let mkbuf = |label: &str, size: u64| device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let scene_u = mkbuf("graph3d_scene_u", std::mem::size_of::<SceneUniforms>() as u64);
let storage_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
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 cloud_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("graph3d_cloud_bgl"),
entries: &[uniform_entry(0, wgpu::ShaderStages::VERTEX), storage_entry(1), storage_entry(2)],
});
let cloud_pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("graph3d_cloud_pll"),
bind_group_layouts: &[Some(&cloud_bgl)],
immediate_size: 0,
});
let mk_cloud_pipeline = |label: &str, vs: &str, fs: &str| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&cloud_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(additive),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: crate::render::gpu::msaa_state(),
multiview_mask: None,
cache: None,
})
};
let cloud_node_pipeline = mk_cloud_pipeline("graph3d_cloud_node_pipeline", "cloud_node_vs", "node_fs");
let cloud_edge_pipeline = mk_cloud_pipeline("graph3d_cloud_edge_pipeline", "cloud_edge_vs", "edge_fs");
let cloud_u = mkbuf("graph3d_cloud_u", std::mem::size_of::<CloudCamera>() as u64);
let scene_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("graph3d_scene_bg"),
layout: &scene_bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: scene_u.as_entire_binding() }],
});
Self {
node_pipeline,
edge_pipeline,
cloud_node_pipeline,
cloud_edge_pipeline,
cloud_bgl,
cloud_u,
bright_pipeline,
composite_pipeline,
scene_bgl,
sample_bgl,
composite_bgl,
sampler,
scene_u,
post_u: mkbuf("graph3d_post_u", std::mem::size_of::<PostUniforms>() as u64),
scene_bg,
blur: GaussianBlur::new(device, OFFSCREEN_FORMAT),
ldr_format,
}
}
pub fn write_viewport(&self, queue: &wgpu::Queue, w: u32, h: u32) {
queue.write_buffer(&self.scene_u, 0, bytemuck::bytes_of(&SceneUniforms { viewport: [w.max(1) as f32, h.max(1) as f32, 0.0, 0.0] }));
}
#[allow(clippy::too_many_arguments)]
pub fn encode_scene(
&self,
encoder: &mut wgpu::CommandEncoder,
color_view: &wgpu::TextureView,
node_buf: Option<&wgpu::Buffer>,
node_count: u32,
edge_buf: Option<&wgpu::Buffer>,
edge_count: u32,
background: [f32; 4],
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graph3d_cloud_scene_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: background[0] as f64,
g: background[1] as f64,
b: background[2] as f64,
a: background[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if let (Some(eb), true) = (edge_buf, edge_count > 0) {
pass.set_pipeline(&self.edge_pipeline);
pass.set_bind_group(0, &self.scene_bg, &[]);
pass.set_vertex_buffer(0, eb.slice(..));
pass.draw(0..6, 0..edge_count);
}
if let (Some(nb), true) = (node_buf, node_count > 0) {
pass.set_pipeline(&self.node_pipeline);
pass.set_bind_group(0, &self.scene_bg, &[]);
pass.set_vertex_buffer(0, nb.slice(..));
pass.draw(0..6, 0..node_count);
}
}
pub fn upload_cloud(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
points: &[CloudPoint],
edges: &[CloudEdge],
generation: u64,
) -> Result<CloudBuffers, String> {
let dead_pt = [CloudPoint::default()];
let dead_ed: [CloudEdge; 1] = [[0, 0]];
let pts = checked_buffer(
device,
queue,
"graph3d_cloud_pts",
bytemuck::cast_slice(if points.is_empty() { &dead_pt } else { points }),
wgpu::BufferUsages::STORAGE,
)?;
let edg = checked_buffer(
device,
queue,
"graph3d_cloud_edg",
bytemuck::cast_slice(if edges.is_empty() { &dead_ed } else { edges }),
wgpu::BufferUsages::STORAGE,
)?;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("graph3d_cloud_bg"),
layout: &self.cloud_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: self.cloud_u.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: pts.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: edg.as_entire_binding() },
],
});
Ok(CloudBuffers {
bg,
node_count: points.len() as u32,
edge_count: edges.len() as u32,
generation,
_pts: pts,
_edg: edg,
})
}
pub fn write_cloud_camera(&self, queue: &wgpu::Queue, cam: &CloudCamera) {
queue.write_buffer(&self.cloud_u, 0, bytemuck::bytes_of(cam));
}
pub fn encode_cloud(
&self,
encoder: &mut wgpu::CommandEncoder,
color_view: &wgpu::TextureView,
cloud: &CloudBuffers,
background: [f32; 4],
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graph3d_cloud_model_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: background[0] as f64,
g: background[1] as f64,
b: background[2] as f64,
a: background[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if cloud.edge_count > 0 {
pass.set_pipeline(&self.cloud_edge_pipeline);
pass.set_bind_group(0, &cloud.bg, &[]);
pass.draw(0..6, 0..cloud.edge_count);
}
if cloud.node_count > 0 {
pass.set_pipeline(&self.cloud_node_pipeline);
pass.set_bind_group(0, &cloud.bg, &[]);
pass.draw(0..6, 0..cloud.node_count);
}
}
pub fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
w: u32,
h: u32,
scene: &GraphScene,
bloom: BloomParams,
) -> GraphRender {
self.render_src(device, queue, w, h, Src::Instances(scene), scene.background, bloom)
}
#[allow(clippy::too_many_arguments)] pub fn render_cloud(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
w: u32,
h: u32,
cloud: &CloudBuffers,
background: [f32; 4],
bloom: BloomParams,
) -> GraphRender {
self.render_src(device, queue, w, h, Src::Cloud(cloud), background, bloom)
}
#[allow(clippy::too_many_arguments)] fn render_src(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
w: u32,
h: u32,
src: Src<'_>,
background: [f32; 4],
bloom: BloomParams,
) -> GraphRender {
use wgpu::util::DeviceExt;
let w = w.max(1);
let h = h.max(1);
queue.write_buffer(&self.scene_u, 0, bytemuck::bytes_of(&SceneUniforms { viewport: [w as f32, h as f32, 0.0, 0.0] }));
queue.write_buffer(
&self.post_u,
0,
bytemuck::bytes_of(&PostUniforms { params: [bloom.threshold, bloom.knee, bloom.intensity, bloom.exposure] }),
);
let mut offscreen = OffscreenColorDepth::new(device, self.ldr_format);
offscreen.ensure(device, w, h);
let scene_view = offscreen.color_view().expect("offscreen ensured");
let bright = device.create_texture(&wgpu::TextureDescriptor {
label: Some("graph3d_bright"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: OFFSCREEN_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let bright_view = bright.create_view(&Default::default());
let ldr = device.create_texture(&wgpu::TextureDescriptor {
label: Some("graph3d_ldr"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: crate::render::gpu::NO_MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: self.ldr_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let ldr_view = ldr.create_view(&Default::default());
self.blur.ensure(device, queue, w, h, bloom.sigma);
let scene = match src {
Src::Instances(s) => Some(s),
Src::Cloud(_) => None,
};
let node_buf = scene.is_some_and(|s| !s.nodes.is_empty()).then(|| {
let scene = scene.expect("checked");
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("graph3d_nodes"),
contents: bytemuck::cast_slice(&scene.nodes),
usage: wgpu::BufferUsages::VERTEX,
})
});
let edge_buf = scene.is_some_and(|s| !s.edges.is_empty()).then(|| {
let scene = scene.expect("checked");
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("graph3d_edges"),
contents: bytemuck::cast_slice(&scene.edges),
usage: wgpu::BufferUsages::VERTEX,
})
});
let scene_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("graph3d_scene_bg"),
layout: &self.scene_bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: self.scene_u.as_entire_binding() }],
});
let bright_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("graph3d_bright_bg"),
layout: &self.sample_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: self.post_u.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(scene_view) },
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.sampler) },
],
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("graph3d_enc") });
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graph3d_scene_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: scene_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: background[0] as f64,
g: background[1] as f64,
b: background[2] as f64,
a: background[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
match src {
Src::Instances(sc) => {
if let Some(eb) = &edge_buf {
pass.set_pipeline(&self.edge_pipeline);
pass.set_bind_group(0, &scene_bg, &[]);
pass.set_vertex_buffer(0, eb.slice(..));
pass.draw(0..6, 0..sc.edges.len() as u32);
}
if let Some(nb) = &node_buf {
pass.set_pipeline(&self.node_pipeline);
pass.set_bind_group(0, &scene_bg, &[]);
pass.set_vertex_buffer(0, nb.slice(..));
pass.draw(0..6, 0..sc.nodes.len() as u32);
}
}
Src::Cloud(cb) => {
if cb.edge_count > 0 {
pass.set_pipeline(&self.cloud_edge_pipeline);
pass.set_bind_group(0, &cb.bg, &[]);
pass.draw(0..6, 0..cb.edge_count);
}
if cb.node_count > 0 {
pass.set_pipeline(&self.cloud_node_pipeline);
pass.set_bind_group(0, &cb.bg, &[]);
pass.draw(0..6, 0..cb.node_count);
}
}
}
}
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graph3d_bright_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &bright_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.bright_pipeline);
pass.set_bind_group(0, &bright_bg, &[]);
pass.draw(0..3, 0..1);
}
let bloom_view = self.blur.blur(device, &mut encoder, &bright_view).expect("blur ensured");
let composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("graph3d_composite_bg"),
layout: &self.composite_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: self.post_u.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(scene_view) },
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.sampler) },
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(bloom_view) },
],
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graph3d_composite_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &ldr_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.composite_pipeline);
pass.set_bind_group(0, &composite_bg, &[]);
pass.draw(0..3, 0..1);
}
let bytes_per_pixel = 4u32;
let unpadded = w * bytes_per_pixel;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded = unpadded.div_ceil(align) * align;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("graph3d_readback"),
size: (padded * h) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &ldr,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded), rows_per_image: Some(h) },
},
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
device.poll(wgpu::PollType::wait_indefinitely()).ok();
rx.recv().ok();
let data = slice.get_mapped_range().expect("wgpu: get_mapped_range on a mapped buffer");
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for row in 0..h {
let start = (row * padded) as usize;
rgba.extend_from_slice(&data[start..start + unpadded as usize]);
}
drop(data);
readback.unmap();
GraphRender {
width: w,
height: h,
rgba,
node_instances: match src {
Src::Instances(sc) => sc.nodes.len() as u32,
Src::Cloud(cb) => cb.node_count,
},
edge_instances: match src {
Src::Instances(sc) => sc.edges.len() as u32,
Src::Cloud(cb) => cb.edge_count,
},
}
}
}
#[must_use]
pub fn buffer_refusal(label: &str, bytes: u64, usage: wgpu::BufferUsages, limits: &wgpu::Limits) -> Option<String> {
if bytes > limits.max_buffer_size {
return Some(format!(
"{label}: {bytes} B is over this device's max_buffer_size ({} B) — nothing drawn",
limits.max_buffer_size
));
}
if usage.contains(wgpu::BufferUsages::STORAGE) && bytes > limits.max_storage_buffer_binding_size {
return Some(format!(
"{label}: {bytes} B is over this device's max_storage_buffer_binding_size ({} B) — nothing drawn",
limits.max_storage_buffer_binding_size
));
}
None
}
pub fn checked_buffer(
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &str,
contents: &[u8],
usage: wgpu::BufferUsages,
) -> Result<wgpu::Buffer, String> {
let align = wgpu::COPY_BUFFER_ALIGNMENT;
let size = (contents.len() as u64).div_ceil(align).max(1) * align;
if let Some(why) = buffer_refusal(label, size, usage, &device.limits()) {
return Err(why);
}
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: usage | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if contents.len() as u64 == size {
queue.write_buffer(&buf, 0, contents);
} else {
let mut padded = contents.to_vec();
padded.resize(size as usize, 0);
queue.write_buffer(&buf, 0, &padded);
}
Ok(buf)
}
pub mod cloud {
use super::{GraphGpu, GraphScene};
use egui_wgpu::wgpu;
use crate::render::gpu::offscreen::OffscreenColorDepth;
pub struct CloudRenderer {
gpu: GraphGpu,
offscreen: OffscreenColorDepth,
node_buf: Option<wgpu::Buffer>,
edge_buf: Option<wgpu::Buffer>,
node_count: u32,
edge_count: u32,
cloud: Option<super::CloudBuffers>,
}
impl CloudRenderer {
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
Self {
gpu: GraphGpu::new(device, target_format),
offscreen: OffscreenColorDepth::new(device, target_format),
node_buf: None,
edge_buf: None,
node_count: 0,
edge_count: 0,
cloud: None,
}
}
}
pub struct CloudModelPaintCallback {
pub model: Option<(Vec<super::CloudPoint>, Vec<super::CloudEdge>)>,
pub generation: u64,
pub cam: super::CloudCamera,
pub background: [f32; 4],
pub px_w: u32,
pub px_h: u32,
}
impl egui_wgpu::CallbackTrait for CloudModelPaintCallback {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
sd: &egui_wgpu::ScreenDescriptor,
encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let Some(r) = resources.get_mut::<CloudRenderer>() else {
return vec![];
};
let (w, h) = if self.px_w > 0 && self.px_h > 0 {
(self.px_w, self.px_h)
} else {
(sd.size_in_pixels[0], sd.size_in_pixels[1])
};
r.offscreen.ensure(device, w, h);
if let Some((pts, eds)) = self.model.as_ref() {
match r.gpu.upload_cloud(device, queue, pts, eds, self.generation) {
Ok(c) => r.cloud = Some(c),
Err(why) => {
r.cloud = None;
crate::render::lane::note_gpu_error(std::any::type_name::<CloudRenderer>(), &why);
}
}
}
let Some(cloud) = r.cloud.as_ref() else {
return vec![];
};
r.gpu.write_cloud_camera(queue, &self.cam);
let Some(color_view) = r.offscreen.color_view() else {
return vec![];
};
r.gpu.encode_cloud(encoder, color_view, cloud, self.background);
crate::render::lane::note_frame(std::any::type_name::<CloudRenderer>());
vec![]
}
fn paint(
&self,
info: egui::PaintCallbackInfo,
pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let Some(r) = resources.get::<CloudRenderer>() else { return };
if !r.offscreen.ready() {
return;
}
let _scissor = r.offscreen.blit(&info, pass);
}
}
pub struct CloudPaintCallback {
pub scene: GraphScene,
pub px_w: u32,
pub px_h: u32,
}
impl egui_wgpu::CallbackTrait for CloudPaintCallback {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
sd: &egui_wgpu::ScreenDescriptor,
encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let Some(r) = resources.get_mut::<CloudRenderer>() else {
return vec![];
};
let (w, h) = if self.px_w > 0 && self.px_h > 0 { (self.px_w, self.px_h) } else { (sd.size_in_pixels[0], sd.size_in_pixels[1]) };
r.offscreen.ensure(device, w, h);
r.gpu.write_viewport(queue, w, h);
let make = |label: &str, bytes: &[u8]| {
if bytes.is_empty() {
return None;
}
super::checked_buffer(device, queue, label, bytes, wgpu::BufferUsages::VERTEX)
.map_err(|why| crate::render::lane::note_gpu_error(std::any::type_name::<CloudRenderer>(), &why))
.ok()
};
r.node_buf = make("graph3d_cloud_nodes", bytemuck::cast_slice(&self.scene.nodes));
r.edge_buf = make("graph3d_cloud_edges", bytemuck::cast_slice(&self.scene.edges));
r.node_count = self.scene.nodes.len() as u32;
r.edge_count = self.scene.edges.len() as u32;
let Some(color_view) = r.offscreen.color_view() else {
return vec![];
};
r.gpu.encode_scene(
encoder,
color_view,
r.node_buf.as_ref(),
r.node_count,
r.edge_buf.as_ref(),
r.edge_count,
self.scene.background,
);
crate::render::lane::note_frame(std::any::type_name::<CloudRenderer>());
vec![]
}
fn paint(
&self,
info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let Some(r) = resources.get::<CloudRenderer>() else { return };
if !r.offscreen.ready() {
return;
}
let _scissor = r.offscreen.blit(&info, render_pass);
}
}
#[must_use]
pub fn cloud_renderer_installed() -> bool {
installed_flag().load(std::sync::atomic::Ordering::Acquire)
}
fn installed_flag() -> &'static std::sync::atomic::AtomicBool {
static FLAG: std::sync::OnceLock<&'static std::sync::atomic::AtomicBool> =
std::sync::OnceLock::new();
FLAG.get_or_init(|| {
crate::render::lane::installed_flag(std::any::type_name::<CloudRenderer>())
})
}
#[must_use]
pub fn resident_generation() -> u64 {
RESIDENT_GENERATION.load(std::sync::atomic::Ordering::Relaxed)
}
static RESIDENT_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub const RESIDENT_ABOVE: usize = 5_000;
static NEXT_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
#[must_use]
pub fn fresh_generation() -> u64 {
(1u64 << 62) | NEXT_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
#[derive(Debug, Default)]
pub struct ResidentSlot {
uploaded: std::cell::Cell<u64>,
uploads: std::cell::Cell<u64>,
frames: std::cell::Cell<u64>,
}
impl ResidentSlot {
#[must_use]
pub fn needs_upload(&self, generation: u64) -> bool {
self.uploaded.get() != generation || resident_generation() != generation
}
pub fn draw(
&self,
ui: &egui::Ui,
rect: egui::Rect,
generation: u64,
build: impl FnOnce() -> (Vec<super::CloudPoint>, Vec<super::CloudEdge>),
cam: super::CloudCamera,
) -> bool {
if !cloud_renderer_installed() {
return false;
}
let ppp = ui.ctx().pixels_per_point();
if !((rect.width() * ppp).round() >= 1.0 && (rect.height() * ppp).round() >= 1.0) {
return false;
}
let upload = self.needs_upload(generation);
let drew = draw_cloud_model(ui, rect, generation, upload.then(build), cam);
if drew {
self.frames.set(self.frames.get() + 1);
if upload {
self.uploaded.set(generation);
self.uploads.set(self.uploads.get() + 1);
}
}
drew
}
#[must_use]
pub fn uploads(&self) -> u64 {
self.uploads.get()
}
#[must_use]
pub fn frames(&self) -> u64 {
self.frames.get()
}
#[must_use]
pub fn generation(&self) -> u64 {
self.uploaded.get()
}
}
#[cfg(feature = "cloud-test-gate")]
pub fn assume_renderer_installed_for_tests() {
installed_flag().store(true, std::sync::atomic::Ordering::Release);
}
pub fn install_cloud_renderer(render_state: &egui_wgpu::RenderState) {
crate::render::lane::note_reports_frames(std::any::type_name::<CloudRenderer>());
crate::render::lane::note_idle_reason(
std::any::type_name::<CloudRenderer>(),
Some(crate::render::lane::PANE_NOT_PAINTED_YET),
);
let _ = crate::render::gpu::install_renderer(render_state, CloudRenderer::new);
}
pub fn draw_cloud_model(
ui: &egui::Ui,
rect: egui::Rect,
generation: u64,
model: Option<(Vec<super::CloudPoint>, Vec<super::CloudEdge>)>,
cam: super::CloudCamera,
) -> bool {
if !cloud_renderer_installed() {
return false;
}
let ppp = ui.ctx().pixels_per_point();
let px_w = (rect.width() * ppp).round();
let px_h = (rect.height() * ppp).round();
if !(px_w >= 1.0 && px_h >= 1.0) {
return false;
}
if model.is_some() {
RESIDENT_GENERATION.store(generation, std::sync::atomic::Ordering::Relaxed);
}
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
CloudModelPaintCallback {
model,
generation,
cam,
background: [0.02, 0.022, 0.03, 1.0],
px_w: px_w as u32,
px_h: px_h as u32,
},
));
true
}
pub fn draw_cloud(
ui: &egui::Ui,
rect: egui::Rect,
use_gpu: bool,
build: impl FnOnce(f32, f32) -> GraphScene,
) -> bool {
if !use_gpu || !cloud_renderer_installed() {
return false;
}
let ppp = ui.ctx().pixels_per_point();
let px_w = (rect.width() * ppp).round();
let px_h = (rect.height() * ppp).round();
if !(px_w >= 1.0 && px_h >= 1.0) {
return false;
}
let scene = build(px_w, px_h);
if scene.nodes.is_empty() && scene.edges.is_empty() {
return false;
}
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
CloudPaintCallback { scene, px_w: px_w as u32, px_h: px_h as u32 },
));
true
}
}
#[derive(Clone, Copy, Debug)]
pub struct CloudBuilder {
pub emissive: f32,
pub edge_gain: f32,
pub background: [f32; 4],
}
impl Default for CloudBuilder {
fn default() -> Self {
Self { emissive: 2.5, edge_gain: 0.6, background: [0.02, 0.022, 0.03, 1.0] }
}
}
impl CloudBuilder {
#[must_use]
pub fn radiance(&self, c: egui::Color32, mul: f32) -> [f32; 4] {
let lin = |b: u8| {
let s = b as f32 / 255.0;
s * s
};
let k = self.emissive * mul;
[lin(c.r()) * k, lin(c.g()) * k, lin(c.b()) * k, 1.0]
}
#[must_use]
pub fn scene(
&self,
nodes: impl IntoIterator<Item = (egui::Pos2, f32, egui::Color32)>,
edges: impl IntoIterator<Item = (egui::Pos2, egui::Pos2, f32, egui::Color32)>,
) -> GraphScene {
let edges = edges
.into_iter()
.map(|(a, b, w, c)| EdgeInstance {
a: [a.x, a.y],
b: [b.x, b.y],
width: w.max(0.5),
_pad: 0.0,
color: self.radiance(c, self.edge_gain),
})
.collect();
let nodes = nodes
.into_iter()
.map(|(p, r, c)| NodeInstance {
center: [p.x, p.y],
radius: r.max(0.5),
_pad: 0.0,
color: self.radiance(c, 1.0),
})
.collect();
GraphScene { background: self.background, nodes, edges }
}
}
fn uniform_entry(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: vis,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
count: None,
}
}
fn tex_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}
}
fn sampler_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
}
}
#[must_use]
pub fn render_graph_offscreen(w: u32, h: u32, scene: &GraphScene, bloom: BloomParams) -> Option<GraphRender> {
pollster::block_on(render_graph_offscreen_async(w, h, scene, bloom))
}
async fn render_graph_offscreen_async(w: u32, h: u32, scene: &GraphScene, bloom: BloomParams) -> Option<GraphRender> {
let probe = crate::render::gputurn::probe::open(
"graph3d-gpu-offscreen",
crate::render::gputurn::probe::OnSoftware::Skip,
crate::render::gputurn::probe::downlevel,
)?;
let (device, queue) = (&probe.device, &probe.queue);
let mut gpu = GraphGpu::new(device, wgpu::TextureFormat::Rgba8Unorm);
Some(gpu.render(device, queue, w, h, scene, bloom))
}
#[must_use]
pub fn render_cloud_offscreen(
w: u32,
h: u32,
points: &[CloudPoint],
edges: &[CloudEdge],
cam: &CloudCamera,
background: [f32; 4],
bloom: BloomParams,
) -> Option<GraphRender> {
pollster::block_on(render_cloud_offscreen_async(w, h, points, edges, cam, background, bloom))
}
async fn render_cloud_offscreen_async(
w: u32,
h: u32,
points: &[CloudPoint],
edges: &[CloudEdge],
cam: &CloudCamera,
background: [f32; 4],
bloom: BloomParams,
) -> Option<GraphRender> {
let probe = crate::render::gputurn::probe::open(
"graph3d-cloud-offscreen",
crate::render::gputurn::probe::OnSoftware::Skip,
crate::render::gputurn::probe::downlevel,
)?;
let (device, queue) = (&probe.device, &probe.queue);
let mut gpu = GraphGpu::new(device, wgpu::TextureFormat::Rgba8Unorm);
let cloud = gpu.upload_cloud(device, queue, points, edges, 0).ok()?;
gpu.write_cloud_camera(queue, cam);
Some(gpu.render_cloud(device, queue, w, h, &cloud, background, bloom))
}
#[cfg(test)]
mod buffer_refusal_tests {
use super::buffer_refusal;
use egui_wgpu::wgpu;
#[test]
fn a_buffer_over_the_device_limits_is_refused_with_the_reason() {
let st = wgpu::BufferUsages::STORAGE;
let mut small = wgpu::Limits::downlevel_defaults();
small.max_buffer_size = 1 << 20;
small.max_storage_buffer_binding_size = 1 << 19;
let why = buffer_refusal("graph3d_cloud_pts", 1_600_000, st, &small).expect("over max_buffer_size");
assert!(why.contains("max_buffer_size") && why.contains("1600000"), "{why}");
let why = buffer_refusal("graph3d_cloud_pts", 600_000, st, &small).expect("over the storage binding");
assert!(why.contains("max_storage_buffer_binding_size"), "{why}");
assert_eq!(buffer_refusal("graph3d_cloud_nodes", 600_000, wgpu::BufferUsages::VERTEX, &small), None);
assert_eq!(buffer_refusal("graph3d_cloud_pts", 1_600_000, st, &wgpu::Limits::default()), None, "fits a default device");
}
}