#![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],
}
#[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.chunks_exact(4) {
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,
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(include_str!("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: &[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: wgpu::MultisampleState::default(),
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: &[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: wgpu::MultisampleState::default(),
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: wgpu::MultisampleState::default(),
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: wgpu::MultisampleState::default(),
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 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,
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 render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
w: u32,
h: u32,
scene: &GraphScene,
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: 1,
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: 1,
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 node_buf = (!scene.nodes.is_empty()).then(|| {
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.edges.is_empty()).then(|| {
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: scene.background[0] as f64,
g: scene.background[1] as f64,
b: scene.background[2] as f64,
a: scene.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) = &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..scene.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..scene.nodes.len() as u32);
}
}
{
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();
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: scene.nodes.len() as u32,
edge_instances: scene.edges.len() as u32,
}
}
}
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,
}
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,
}
}
}
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> {
use wgpu::util::DeviceExt;
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);
r.node_buf = (!self.scene.nodes.is_empty()).then(|| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("graph3d_cloud_nodes"),
contents: bytemuck::cast_slice(&self.scene.nodes),
usage: wgpu::BufferUsages::VERTEX,
})
});
r.edge_buf = (!self.scene.edges.is_empty()).then(|| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("graph3d_cloud_edges"),
contents: bytemuck::cast_slice(&self.scene.edges),
usage: wgpu::BufferUsages::VERTEX,
})
});
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);
}
}
pub(crate) static CLOUD_RENDERER_INSTALLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[must_use]
pub fn cloud_renderer_installed() -> bool {
CLOUD_RENDERER_INSTALLED.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn install_cloud_renderer(render_state: &egui_wgpu::RenderState) {
CLOUD_RENDERER_INSTALLED.store(true, std::sync::atomic::Ordering::Relaxed);
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(
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 instance = wgpu::Instance::default();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.ok()?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("graph3d-gpu-offscreen"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::default(),
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
})
.await
.ok()?;
let mut gpu = GraphGpu::new(&device, wgpu::TextureFormat::Rgba8Unorm);
Some(gpu.render(&device, &queue, w, h, scene, bloom))
}