#![allow(dead_code)]
mod adjacency;
mod camera;
mod connections;
mod field_gpu;
mod glyphs;
mod picking;
mod post_process;
mod spatial_gpu;
mod spatial_grid_viz;
pub mod sub_emitter_gpu;
mod trails;
mod volume_render;
mod wireframe;
#[cfg(feature = "egui")]
mod egui_integration;
pub use adjacency::{AdjacencyGpu, adjacency_wgsl};
pub use camera::Camera;
pub use connections::ConnectionState;
pub use field_gpu::{FieldSystemGpu, create_particle_field_bind_group_layout};
pub use glyphs::GlyphRenderer;
pub use picking::PickingState;
pub use post_process::PostProcessState;
pub use spatial_grid_viz::SpatialGridViz;
pub use sub_emitter_gpu::SubEmitterGpu;
pub use trails::TrailState;
pub use volume_render::{VolumeConfig, VolumeRenderState};
pub use wireframe::WireframeState;
use crate::error::GpuError;
use crate::field::FieldRegistry;
#[cfg(feature = "egui")]
use crate::selection::{PendingParticleWrite, SelectedParticle, SelectedParticleData};
#[cfg(feature = "egui")]
pub use egui_integration::EguiIntegration;
use std::sync::Arc;
use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Vec3};
use wgpu::util::DeviceExt;
use winit::window::Window;
pub use spatial_gpu::SpatialGpu;
use crate::spatial::SpatialConfig;
use crate::visuals::BlendMode;
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
const WORKGROUP_SIZE: u32 = 256;
fn blend_mode_to_state(mode: BlendMode) -> wgpu::BlendState {
match mode {
BlendMode::Alpha => wgpu::BlendState::ALPHA_BLENDING,
BlendMode::Additive => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
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,
},
},
BlendMode::Multiply => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::Zero,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
},
BlendMode::Screen => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
},
BlendMode::Overlay => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::Src,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
},
BlendMode::SoftLight => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
},
BlendMode::Subtractive => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::ReverseSubtract,
},
alpha: wgpu::BlendComponent::OVER,
},
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct Uniforms {
view_proj: [[f32; 4]; 4],
time: f32,
delta_time: f32,
}
#[derive(Copy, Clone, Default)]
struct TimeCache {
time: f32,
delta_time: f32,
}
#[allow(dead_code)]
pub struct GpuState {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
pub config: wgpu::SurfaceConfiguration,
render_pipeline: wgpu::RenderPipeline,
compute_pipeline: wgpu::ComputePipeline,
particle_buffer: wgpu::Buffer,
uniform_buffer: wgpu::Buffer,
uniform_buffer_size: usize,
uniform_bind_group: wgpu::BindGroup,
compute_bind_group: wgpu::BindGroup,
depth_texture: wgpu::TextureView,
num_particles: u32,
pub camera: Camera,
spatial: Option<SpatialGpu>,
trail_state: Option<TrailState>,
connection_state: Option<ConnectionState>,
inbox_buffer: Option<wgpu::Buffer>,
inbox_bind_group: Option<wgpu::BindGroup>,
inbox_enabled: bool,
field_system: Option<FieldSystemGpu>,
field_bind_group: Option<wgpu::BindGroup>,
field_bind_group_layout: Option<wgpu::BindGroupLayout>,
empty_bind_group: Option<wgpu::BindGroup>,
volume_render: Option<VolumeRenderState>,
volume_config: Option<VolumeConfig>,
background_color: Vec3,
post_process: Option<PostProcessState>,
custom_textures: Vec<wgpu::Texture>,
custom_texture_views: Vec<wgpu::TextureView>,
custom_samplers: Vec<wgpu::Sampler>,
texture_bind_group: Option<wgpu::BindGroup>,
texture_bind_group_layout: Option<wgpu::BindGroupLayout>,
#[cfg(feature = "egui")]
egui: Option<EguiIntegration>,
#[cfg(feature = "egui")]
window: Arc<Window>,
sub_emitter: Option<SubEmitterGpu>,
spatial_grid_viz: Option<SpatialGridViz>,
wireframe_state: Option<WireframeState>,
adjacency: Option<AdjacencyGpu>,
adjacency_bind_group: Option<wgpu::BindGroup>,
adjacency_bind_group_layout: Option<wgpu::BindGroupLayout>,
particle_stride: usize,
readback_staging: Option<wgpu::Buffer>,
picking: PickingState,
render_pipeline_layout: wgpu::PipelineLayout,
compute_pipeline_layout: wgpu::PipelineLayout,
uniform_bind_group_layout: wgpu::BindGroupLayout,
blend_mode: BlendMode,
color_offset: Option<u32>,
alive_offset: u32,
scale_offset: u32,
time_cache: TimeCache,
}
impl GpuState {
#[allow(clippy::too_many_arguments)]
pub async fn new(
window: Arc<Window>,
particle_data: &[u8],
num_particles: u32,
particle_stride: usize,
compute_shader_src: &str,
render_shader_src: &str,
has_neighbors: bool,
spatial_config: SpatialConfig,
color_offset: Option<u32>,
alive_offset: u32,
scale_offset: u32,
custom_uniform_size: usize,
blend_mode: BlendMode,
trail_length: u32,
particle_size: f32,
connections_enabled: bool,
connections_radius: f32,
connections_color: Vec3,
inbox_enabled: bool,
background_color: Vec3,
post_process_shader: Option<&str>,
custom_uniform_fields: &str,
texture_registry: &crate::textures::TextureRegistry,
_texture_declarations: &str,
field_registry: &FieldRegistry,
volume_config: Option<&VolumeConfig>,
sub_emitters: &[crate::sub_emitter::SubEmitter],
spatial_grid_opacity: f32,
particle_wgsl_struct: &str,
wireframe_mesh: Option<&crate::visuals::WireframeMesh>,
wireframe_thickness: f32,
adjacency_config: Option<&crate::spatial::AdjacencyConfig>,
#[cfg(feature = "egui")] egui_enabled: bool,
) -> Result<Self, GpuError> {
let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance.create_surface(window.clone())?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.ok_or(GpuError::NoAdapter)?;
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some("Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: Default::default(),
},
None, )
.await?;
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoNoVsync, alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
let depth_texture = create_depth_texture(&device, &config);
let particle_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Particle Buffer"),
contents: particle_data,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
});
let camera = Camera::new();
let aspect = config.width as f32 / config.height as f32;
let view = camera.view_matrix();
let proj = Mat4::perspective_rh(45.0_f32.to_radians(), aspect, 0.1, 100.0);
let view_proj = proj * view;
let uniforms = Uniforms {
view_proj: view_proj.to_cols_array_2d(),
time: 0.0,
delta_time: 0.0,
};
let base_size = std::mem::size_of::<Uniforms>();
let padded_base_size = (base_size + 15) & !15;
let total_size = ((padded_base_size + custom_uniform_size) + 15) & !15;
let mut uniform_data = bytemuck::bytes_of(&uniforms).to_vec();
uniform_data.resize(total_size, 0);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Uniform Buffer"),
contents: &uniform_data,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_buffer_size = total_size;
let spatial = if has_neighbors {
Some(SpatialGpu::new(
&device,
&particle_buffer,
num_particles,
spatial_config,
particle_wgsl_struct,
))
} else {
None
};
let inbox_buffer = if inbox_enabled {
let inbox_size = (num_particles as usize) * 16;
Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Inbox Buffer"),
size: inbox_size as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}))
} else {
None
};
let adjacency = if let (Some(ref spatial), Some(adj_cfg)) = (&spatial, adjacency_config) {
Some(AdjacencyGpu::new(
&device,
&particle_buffer,
spatial,
num_particles,
adj_cfg.max_neighbors,
adj_cfg.radius,
particle_stride,
))
} else {
None
};
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let (compute_bind_group_layout, compute_bind_group) = if let Some(ref spatial) = spatial {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Compute Bind Group Layout (with neighbors)"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Compute Bind Group (with neighbors)"),
layout: &layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: particle_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: spatial.particle_indices_a.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: spatial.cell_start.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: spatial.cell_end.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: spatial.spatial_params_buffer.as_entire_binding(),
},
],
});
(layout, bind_group)
} else {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Compute Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Compute Bind Group"),
layout: &layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: particle_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
});
(layout, bind_group)
};
let texture_bind_group_layout = if !texture_registry.textures.is_empty() {
let mut layout_entries = Vec::new();
for i in 0..texture_registry.textures.len() {
layout_entries.push(wgpu::BindGroupLayoutEntry {
binding: (i * 2) as u32,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
});
layout_entries.push(wgpu::BindGroupLayoutEntry {
binding: (i * 2 + 1) as u32,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
});
}
Some(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Texture Bind Group Layout"),
entries: &layout_entries,
}))
} else {
None
};
let render_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Render Shader"),
source: wgpu::ShaderSource::Wgsl(render_shader_src.into()),
});
let mut bind_group_layouts_vec: Vec<&wgpu::BindGroupLayout> = vec![&uniform_bind_group_layout];
if let Some(ref tex_layout) = texture_bind_group_layout {
bind_group_layouts_vec.push(tex_layout);
}
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &bind_group_layouts_vec,
push_constant_ranges: &[],
});
let vertex_attributes: Vec<wgpu::VertexAttribute> = if let Some(offset) = color_offset {
vec![
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, },
wgpu::VertexAttribute {
offset: offset as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3, },
wgpu::VertexAttribute {
offset: alive_offset as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Uint32, },
wgpu::VertexAttribute {
offset: scale_offset as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32, },
]
} else {
vec![
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, },
wgpu::VertexAttribute {
offset: alive_offset as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Uint32, },
wgpu::VertexAttribute {
offset: scale_offset as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32, },
]
};
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &render_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: particle_stride as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &vertex_attributes,
}],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &render_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(blend_mode_to_state(blend_mode)),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: !matches!(blend_mode, BlendMode::Additive | BlendMode::Screen),
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Compute Shader"),
source: wgpu::ShaderSource::Wgsl(compute_shader_src.into()),
});
let (inbox_bind_group_layout, inbox_bind_group) = if let Some(ref inbox_buf) = inbox_buffer {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Inbox Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Inbox Bind Group"),
layout: &layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: inbox_buf.as_entire_binding(),
}],
});
(Some(layout), Some(bind_group))
} else {
(None, None)
};
let (field_system, field_bind_group_layout, field_bind_group) = if !field_registry.is_empty() {
let system = FieldSystemGpu::new(&device, field_registry);
let layout = create_particle_field_bind_group_layout(&device, system.field_count);
let bind_group = system.create_particle_bind_group(&device, &layout);
(Some(system), Some(layout), bind_group)
} else {
(None, None, None)
};
let volume_render = if let (Some(config), Some(ref fs)) = (&volume_config, &field_system) {
Some(VolumeRenderState::new(&device, fs, config, surface_format))
} else {
None
};
let sub_emitter = if !sub_emitters.is_empty() {
Some(SubEmitterGpu::new(
&device,
&particle_buffer,
num_particles,
sub_emitters,
particle_wgsl_struct,
))
} else {
None
};
let (adjacency_bind_group_layout, adjacency_bind_group) = if let Some(ref adj) = adjacency {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Adjacency Read Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Adjacency Read Bind Group"),
layout: &layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: adj.buffer.as_entire_binding(),
}],
});
(Some(layout), Some(bind_group))
} else {
(None, None)
};
let (compute_pipeline_layout, empty_bind_group) = {
let empty_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Empty Bind Group Layout"),
entries: &[],
});
let empty_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Empty Bind Group"),
layout: &empty_layout,
entries: &[],
});
let mut layouts: Vec<&wgpu::BindGroupLayout> = vec![&compute_bind_group_layout];
if let Some(ref inbox_layout) = inbox_bind_group_layout {
layouts.push(inbox_layout);
} else if field_bind_group_layout.is_some() || sub_emitter.is_some() || adjacency_bind_group_layout.is_some() {
layouts.push(&empty_layout);
}
if let Some(ref field_layout) = field_bind_group_layout {
layouts.push(field_layout);
} else if sub_emitter.is_some() || adjacency_bind_group_layout.is_some() {
layouts.push(&empty_layout);
}
if let Some(ref se) = sub_emitter {
layouts.push(&se.death_bind_group_layout);
} else if adjacency_bind_group_layout.is_some() {
layouts.push(&empty_layout);
}
if let Some(ref adj_layout) = adjacency_bind_group_layout {
layouts.push(adj_layout);
}
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Compute Pipeline Layout"),
bind_group_layouts: &layouts,
push_constant_ranges: &[],
});
let keep_empty = (inbox_bind_group_layout.is_none() && (field_bind_group_layout.is_some() || sub_emitter.is_some() || adjacency_bind_group_layout.is_some()))
|| (field_bind_group_layout.is_none() && (sub_emitter.is_some() || adjacency_bind_group_layout.is_some()))
|| (sub_emitter.is_none() && adjacency_bind_group_layout.is_some());
(layout, if keep_empty { Some(empty_bg) } else { None })
};
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Compute Pipeline"),
layout: Some(&compute_pipeline_layout),
module: &compute_shader,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
let trail_state = if trail_length > 0 {
Some(TrailState::new(
&device,
&particle_buffer,
&uniform_buffer,
num_particles,
trail_length,
particle_stride,
color_offset,
particle_size,
blend_mode,
config.format,
))
} else {
None
};
let connection_state = if let (true, Some(spatial_ref)) = (connections_enabled, spatial.as_ref()) {
Some(ConnectionState::new(
&device,
&particle_buffer,
&uniform_buffer,
spatial_ref,
num_particles,
connections_radius,
connections_color,
particle_stride,
blend_mode,
config.format,
))
} else {
None
};
let post_process = if let Some(shader_code) = post_process_shader {
Some(PostProcessState::new(
&device,
&uniform_buffer,
shader_code,
custom_uniform_fields,
config.width,
config.height,
config.format,
))
} else {
None
};
let mut custom_textures = Vec::new();
let mut custom_texture_views = Vec::new();
let mut custom_samplers = Vec::new();
for (_name, config) in &texture_registry.textures {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Custom Texture"),
size: wgpu::Extent3d {
width: config.width,
height: config.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&config.data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * config.width),
rows_per_image: Some(config.height),
},
wgpu::Extent3d {
width: config.width,
height: config.height,
depth_or_array_layers: 1,
},
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let filter = match config.filter {
crate::textures::FilterMode::Linear => wgpu::FilterMode::Linear,
crate::textures::FilterMode::Nearest => wgpu::FilterMode::Nearest,
};
let address_mode = match config.address_mode {
crate::textures::AddressMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
crate::textures::AddressMode::Repeat => wgpu::AddressMode::Repeat,
crate::textures::AddressMode::MirrorRepeat => wgpu::AddressMode::MirrorRepeat,
};
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: address_mode,
address_mode_v: address_mode,
address_mode_w: address_mode,
mag_filter: filter,
min_filter: filter,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
custom_textures.push(texture);
custom_texture_views.push(view);
custom_samplers.push(sampler);
}
let texture_bind_group = if let Some(ref layout) = texture_bind_group_layout {
let mut bind_group_entries = Vec::new();
for i in 0..custom_texture_views.len() {
bind_group_entries.push(wgpu::BindGroupEntry {
binding: (i * 2) as u32,
resource: wgpu::BindingResource::TextureView(&custom_texture_views[i]),
});
bind_group_entries.push(wgpu::BindGroupEntry {
binding: (i * 2 + 1) as u32,
resource: wgpu::BindingResource::Sampler(&custom_samplers[i]),
});
}
Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Texture Bind Group"),
layout,
entries: &bind_group_entries,
}))
} else {
None
};
#[cfg(feature = "egui")]
let egui = if egui_enabled {
Some(EguiIntegration::new(&device, config.format, &window))
} else {
None
};
let spatial_grid_viz = Some(SpatialGridViz::new(
&device,
&uniform_buffer,
&spatial_config,
spatial_grid_opacity,
surface_format,
));
let wireframe_state = wireframe_mesh.map(|mesh| WireframeState::new(
&device,
&particle_buffer,
&uniform_buffer,
mesh,
wireframe_thickness,
particle_size,
num_particles,
particle_stride,
color_offset,
alive_offset,
scale_offset,
blend_mode,
surface_format,
));
let picking = PickingState::new(
&device,
config.width,
config.height,
particle_stride,
color_offset,
alive_offset,
scale_offset,
);
Ok(Self {
surface,
device,
queue,
config,
render_pipeline,
compute_pipeline,
particle_buffer,
uniform_buffer,
uniform_buffer_size,
uniform_bind_group,
compute_bind_group,
depth_texture,
num_particles,
camera,
spatial,
trail_state,
connection_state,
inbox_buffer,
inbox_bind_group,
inbox_enabled,
field_system,
field_bind_group,
field_bind_group_layout,
empty_bind_group,
volume_render,
volume_config: volume_config.cloned(),
background_color,
post_process,
custom_textures,
custom_texture_views,
custom_samplers,
texture_bind_group,
texture_bind_group_layout,
#[cfg(feature = "egui")]
egui,
#[cfg(feature = "egui")]
window,
sub_emitter,
spatial_grid_viz,
wireframe_state,
adjacency,
adjacency_bind_group,
adjacency_bind_group_layout,
particle_stride,
readback_staging: None,
picking,
render_pipeline_layout,
compute_pipeline_layout,
uniform_bind_group_layout,
blend_mode,
color_offset,
alive_offset,
scale_offset,
time_cache: TimeCache::default(),
})
}
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.config.width = new_size.width;
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
self.depth_texture = create_depth_texture(&self.device, &self.config);
if let Some(ref mut pp) = self.post_process {
pp.resize(
&self.device,
&self.uniform_buffer,
self.config.width,
self.config.height,
self.config.format,
);
}
self.picking.resize(&self.device, new_size.width, new_size.height);
}
}
pub fn set_grid_opacity(&mut self, opacity: f32) {
if let Some(ref mut grid) = self.spatial_grid_viz {
grid.set_opacity(&self.queue, opacity);
}
}
pub fn set_background_color(&mut self, color: Vec3) {
self.background_color = color;
}
pub fn rebuild_render_pipeline(&mut self, render_shader_src: &str, blend_mode: BlendMode) {
let render_shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Render Shader (rebuilt)"),
source: wgpu::ShaderSource::Wgsl(render_shader_src.into()),
});
let vertex_attributes: Vec<wgpu::VertexAttribute> = if let Some(offset) = self.color_offset {
vec![
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, },
wgpu::VertexAttribute {
offset: offset as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3, },
wgpu::VertexAttribute {
offset: self.alive_offset as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Uint32, },
wgpu::VertexAttribute {
offset: self.scale_offset as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32, },
]
} else {
vec![
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3, },
wgpu::VertexAttribute {
offset: self.alive_offset as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Uint32, },
wgpu::VertexAttribute {
offset: self.scale_offset as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32, },
]
};
let new_pipeline = self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline (rebuilt)"),
layout: Some(&self.render_pipeline_layout),
vertex: wgpu::VertexState {
module: &render_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: self.particle_stride as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &vertex_attributes,
}],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &render_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: self.config.format,
blend: Some(blend_mode_to_state(blend_mode)),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: !matches!(blend_mode, BlendMode::Additive | BlendMode::Screen),
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
self.render_pipeline = new_pipeline;
self.blend_mode = blend_mode;
}
pub fn rebuild_compute_pipeline(&mut self, compute_shader_src: &str) {
let compute_shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Compute Shader (rebuilt)"),
source: wgpu::ShaderSource::Wgsl(compute_shader_src.into()),
});
let new_pipeline = self.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Compute Pipeline (rebuilt)"),
layout: Some(&self.compute_pipeline_layout),
module: &compute_shader,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
self.compute_pipeline = new_pipeline;
}
pub fn request_pick(&mut self, x: u32, y: u32) {
self.picking.request_pick(x, y);
}
pub fn selected_particle(&self) -> Option<u32> {
self.picking.selected_particle
}
pub fn clear_selection(&mut self) {
self.picking.clear_selection();
}
pub fn read_particles_sync(&mut self) -> Result<Vec<u8>, GpuError> {
let buffer_size = (self.num_particles as usize) * self.particle_stride;
let staging = self.readback_staging.get_or_insert_with(|| {
self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Readback Staging Buffer"),
size: buffer_size as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
})
});
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Readback Encoder"),
});
encoder.copy_buffer_to_buffer(
&self.particle_buffer,
0,
staging,
0,
buffer_size as u64,
);
self.queue.submit(std::iter::once(encoder.finish()));
let buffer_slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
self.device.poll(wgpu::Maintain::Wait);
rx.recv()
.map_err(|_| GpuError::BufferMapping("Channel receive failed".to_string()))?
.map_err(|e| GpuError::BufferMapping(format!("Buffer mapping failed: {}", e)))?;
let data = buffer_slice.get_mapped_range();
let result = data.to_vec();
drop(data);
staging.unmap();
Ok(result)
}
pub fn write_particles(&mut self, data: &[u8]) {
let expected_size = (self.num_particles as usize) * self.particle_stride;
assert_eq!(
data.len(),
expected_size,
"Particle data size mismatch: expected {} bytes, got {}",
expected_size,
data.len()
);
self.queue.write_buffer(&self.particle_buffer, 0, data);
}
pub fn num_particles(&self) -> u32 {
self.num_particles
}
pub fn particle_stride(&self) -> usize {
self.particle_stride
}
#[cfg(feature = "egui")]
pub fn on_window_event(&mut self, event: &winit::event::WindowEvent) -> bool {
if let Some(ref mut egui) = self.egui {
egui.on_window_event(&self.window, event)
} else {
false
}
}
#[cfg(feature = "egui")]
#[allow(dead_code)]
pub fn egui_enabled(&self) -> bool {
self.egui.is_some()
}
#[cfg(feature = "egui")]
#[allow(dead_code)]
pub fn egui_ctx(&self) -> Option<&egui::Context> {
self.egui.as_ref().map(|e| &e.ctx)
}
fn update_uniforms(&mut self, time: f32, delta_time: f32, custom_uniform_bytes: Option<&[u8]>) {
self.time_cache = TimeCache { time, delta_time };
let aspect = self.config.width as f32 / self.config.height as f32;
let view = self.camera.view_matrix();
let proj = Mat4::perspective_rh(45.0_f32.to_radians(), aspect, 0.1, 100.0);
let view_proj = proj * view;
let uniforms = Uniforms {
view_proj: view_proj.to_cols_array_2d(),
time,
delta_time,
};
let base_bytes = bytemuck::bytes_of(&uniforms);
if let Some(custom_bytes) = custom_uniform_bytes {
let mut combined = base_bytes.to_vec();
while !combined.len().is_multiple_of(16) {
combined.push(0);
}
combined.extend_from_slice(custom_bytes);
combined.resize(self.uniform_buffer_size, 0);
self.queue.write_buffer(&self.uniform_buffer, 0, &combined);
} else {
self.queue.write_buffer(&self.uniform_buffer, 0, base_bytes);
}
}
pub fn render(&mut self, time: f32, delta_time: f32, custom_uniform_bytes: Option<&[u8]>) -> Result<(), wgpu::SurfaceError> {
#[cfg(feature = "egui")]
{
self.render_with_ui(time, delta_time, custom_uniform_bytes, |_| {})
}
#[cfg(not(feature = "egui"))]
{
self.render_internal(time, delta_time, custom_uniform_bytes)
}
}
#[cfg(feature = "egui")]
pub fn render_with_ui<F>(
&mut self,
time: f32,
delta_time: f32,
custom_uniform_bytes: Option<&[u8]>,
ui_callback: F,
) -> Result<(), wgpu::SurfaceError>
where
F: FnOnce(&egui::Context),
{
self.update_uniforms(time, delta_time, custom_uniform_bytes);
let output = self.surface.get_current_texture()?;
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let egui_output_and_write = if let Some(ref mut egui) = self.egui {
egui.begin_frame(&self.window);
let selected = self.picking.selected_particle;
let selected_data = self.picking.selected_particle_data.clone();
egui.ctx.data_mut(|d| {
d.insert_temp(egui::Id::NULL, SelectedParticle(selected));
d.insert_temp(egui::Id::NULL, SelectedParticleData(selected_data));
});
ui_callback(&egui.ctx);
let pending_write = egui.ctx.data(|d| {
d.get_temp::<PendingParticleWrite>(egui::Id::NULL)
});
if pending_write.is_some() {
egui.ctx.data_mut(|d| {
d.insert_temp(egui::Id::NULL, PendingParticleWrite(None));
});
}
Some((egui.end_frame(&self.window), pending_write))
} else {
None
};
let (egui_output, pending_particle_write) = match egui_output_and_write {
Some((output, write)) => (Some(output), write),
None => (None, None),
};
let screen_descriptor = egui_wgpu::ScreenDescriptor {
size_in_pixels: [self.config.width, self.config.height],
pixels_per_point: self.window.scale_factor() as f32,
};
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
if let (Some(ref mut egui), Some(ref egui_out)) = (&mut self.egui, &egui_output) {
egui.prepare(&self.device, &self.queue, &mut encoder, egui_out, &screen_descriptor);
}
if let Some(ref spatial) = self.spatial {
spatial.execute(&mut encoder, &self.queue);
}
if let Some(ref adjacency) = self.adjacency {
adjacency.execute(&mut encoder);
}
if let Some(ref inbox_buf) = self.inbox_buffer {
let inbox_size = (self.num_particles as usize) * 16;
let zeros = vec![0u8; inbox_size];
self.queue.write_buffer(inbox_buf, 0, &zeros);
}
if let Some(ref se) = self.sub_emitter {
se.clear_buffers(&self.queue);
}
let field_bind_group = if let (Some(ref field_sys), Some(ref layout)) =
(&self.field_system, &self.field_bind_group_layout)
{
field_sys.create_particle_bind_group(&self.device, layout)
} else {
None
};
{
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.compute_pipeline);
compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
if let Some(ref inbox_bg) = self.inbox_bind_group {
compute_pass.set_bind_group(1, inbox_bg, &[]);
} else if self.field_system.is_some() || self.sub_emitter.is_some() || self.adjacency_bind_group.is_some() {
if let Some(ref empty_bg) = self.empty_bind_group {
compute_pass.set_bind_group(1, empty_bg, &[]);
}
}
if let Some(ref field_bg) = field_bind_group {
compute_pass.set_bind_group(2, field_bg, &[]);
} else if self.sub_emitter.is_some() || self.adjacency_bind_group.is_some() {
if let Some(ref empty_bg) = self.empty_bind_group {
compute_pass.set_bind_group(2, empty_bg, &[]);
}
}
if let Some(ref se) = self.sub_emitter {
compute_pass.set_bind_group(3, &se.death_bind_group, &[]);
} else if self.adjacency_bind_group.is_some() {
if let Some(ref empty_bg) = self.empty_bind_group {
compute_pass.set_bind_group(3, empty_bg, &[]);
}
}
if let Some(ref adj_bg) = self.adjacency_bind_group {
compute_pass.set_bind_group(4, adj_bg, &[]);
}
let workgroups = self.num_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
#[cfg(feature = "egui")]
if let Some(PendingParticleWrite(Some((idx, bytes)))) = pending_particle_write {
let offset = (idx as usize) * self.particle_stride;
self.queue.write_buffer(&self.particle_buffer, offset as u64, &bytes);
}
if let Some(ref se) = self.sub_emitter {
se.spawn_children(&mut encoder);
}
if let Some(ref mut field_sys) = self.field_system {
field_sys.process(&self.device, &mut encoder, &self.queue, self.time_cache.time, self.time_cache.delta_time);
if let Some(ref mut vol) = self.volume_render {
vol.update_bind_group(&self.device, field_sys);
let aspect = self.config.width as f32 / self.config.height as f32;
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, aspect, 0.1, 100.0);
let view = self.camera.view_matrix();
let view_proj = proj * view;
let inv_view_proj = view_proj.inverse();
let camera_pos = self.camera.position();
let field_idx = vol.field_index;
let field_extent = field_sys.fields[field_idx].config.world_extent;
let field_resolution = field_sys.fields[field_idx].config.resolution;
vol.update_params_with_field(
&self.queue,
inv_view_proj,
camera_pos,
field_extent,
field_resolution,
);
}
}
if let Some(ref trail) = self.trail_state {
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Trail Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&trail.compute_pipeline);
compute_pass.set_bind_group(0, &trail.compute_bind_group, &[]);
let workgroups = self.num_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
if let Some(ref conn) = self.connection_state {
self.queue.write_buffer(&conn.count_buffer, 0, &[0u8; 4]);
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Connection Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&conn.compute_pipeline);
compute_pass.set_bind_group(0, &conn.compute_bind_group, &[]);
let workgroups = self.num_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
if self.picking.has_pending_pick() {
self.picking.render(
&mut encoder,
&self.particle_buffer,
&self.uniform_bind_group,
self.num_particles,
);
}
let render_target = if let Some(ref pp) = self.post_process {
&pp.view
} else {
&view
};
let depth_target = if let Some(ref pp) = self.post_process {
&pp.depth_view
} else {
&self.depth_texture
};
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: render_target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: self.background_color.x as f64,
g: self.background_color.y as f64,
b: self.background_color.z as f64,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_target,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(ref conn) = self.connection_state {
render_pass.set_pipeline(&conn.render_pipeline);
render_pass.set_bind_group(0, &conn.render_bind_group, &[]);
render_pass.draw(0..6, 0..conn.max_connections);
}
if let Some(ref grid) = self.spatial_grid_viz {
if grid.opacity > 0.0 {
render_pass.set_pipeline(grid.pipeline());
render_pass.set_bind_group(0, grid.bind_group(), &[]);
render_pass.draw(0..6, 0..grid.line_count());
}
}
if let Some(ref trail) = self.trail_state {
render_pass.set_pipeline(&trail.render_pipeline);
render_pass.set_bind_group(0, &trail.render_bind_group, &[]);
let total_trail_instances = self.num_particles * trail.trail_length;
render_pass.draw(0..6, 0..total_trail_instances);
}
if let Some(ref wireframe) = self.wireframe_state {
render_pass.set_pipeline(wireframe.pipeline());
render_pass.set_bind_group(0, wireframe.bind_group(), &[]);
render_pass.draw(0..6, 0..wireframe.total_line_count());
} else {
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
if let Some(ref tex_bind_group) = self.texture_bind_group {
render_pass.set_bind_group(1, tex_bind_group, &[]);
}
render_pass.set_vertex_buffer(0, self.particle_buffer.slice(..));
render_pass.draw(0..6, 0..self.num_particles);
}
}
if let Some(ref vol) = self.volume_render {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Volume Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: render_target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None, timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&vol.pipeline);
render_pass.set_bind_group(0, &vol.bind_group, &[]);
render_pass.draw(0..3, 0..1); }
if let Some(ref pp) = self.post_process {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Post-Process Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: 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,
});
render_pass.set_pipeline(&pp.pipeline);
render_pass.set_bind_group(0, &pp.bind_group, &[]);
render_pass.draw(0..3, 0..1); }
if let (Some(ref egui), Some(ref egui_out)) = (&self.egui, &egui_output) {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Egui Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
let render_pass: &mut wgpu::RenderPass<'static> = unsafe {
std::mem::transmute(&mut render_pass)
};
egui.renderer().render(render_pass, &egui_out.paint_jobs, &screen_descriptor);
}
self.picking.copy_pixel(&mut encoder);
if self.picking.needs_particle_data_copy() {
self.picking.copy_particle_data(&mut encoder, &self.particle_buffer);
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
self.picking.read_result(&self.device);
self.picking.read_particle_data(&self.device);
if let (Some(ref mut egui), Some(ref egui_out)) = (&mut self.egui, &egui_output) {
egui.cleanup(egui_out);
}
Ok(())
}
#[cfg(not(feature = "egui"))]
fn render_internal(&mut self, time: f32, delta_time: f32, custom_uniform_bytes: Option<&[u8]>) -> Result<(), wgpu::SurfaceError> {
self.update_uniforms(time, delta_time, custom_uniform_bytes);
let output = self.surface.get_current_texture()?;
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
if let Some(ref spatial) = self.spatial {
spatial.execute(&mut encoder, &self.queue);
}
if let Some(ref adjacency) = self.adjacency {
adjacency.execute(&mut encoder);
}
if let Some(ref inbox_buf) = self.inbox_buffer {
let inbox_size = (self.num_particles as usize) * 16;
let zeros = vec![0u8; inbox_size];
self.queue.write_buffer(inbox_buf, 0, &zeros);
}
if let Some(ref se) = self.sub_emitter {
se.clear_buffers(&self.queue);
}
let field_bind_group = if let (Some(ref field_sys), Some(ref layout)) =
(&self.field_system, &self.field_bind_group_layout)
{
field_sys.create_particle_bind_group(&self.device, layout)
} else {
None
};
{
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.compute_pipeline);
compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
if let Some(ref inbox_bg) = self.inbox_bind_group {
compute_pass.set_bind_group(1, inbox_bg, &[]);
} else if self.field_system.is_some() || self.sub_emitter.is_some() || self.adjacency_bind_group.is_some() {
if let Some(ref empty_bg) = self.empty_bind_group {
compute_pass.set_bind_group(1, empty_bg, &[]);
}
}
if let Some(ref field_bg) = field_bind_group {
compute_pass.set_bind_group(2, field_bg, &[]);
} else if self.sub_emitter.is_some() || self.adjacency_bind_group.is_some() {
if let Some(ref empty_bg) = self.empty_bind_group {
compute_pass.set_bind_group(2, empty_bg, &[]);
}
}
if let Some(ref se) = self.sub_emitter {
compute_pass.set_bind_group(3, &se.death_bind_group, &[]);
} else if self.adjacency_bind_group.is_some() {
if let Some(ref empty_bg) = self.empty_bind_group {
compute_pass.set_bind_group(3, empty_bg, &[]);
}
}
if let Some(ref adj_bg) = self.adjacency_bind_group {
compute_pass.set_bind_group(4, adj_bg, &[]);
}
let workgroups = self.num_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
if let Some(ref se) = self.sub_emitter {
se.spawn_children(&mut encoder);
}
if let Some(ref mut field_sys) = self.field_system {
field_sys.process(&self.device, &mut encoder, &self.queue, self.time_cache.time, self.time_cache.delta_time);
if let Some(ref mut vol) = self.volume_render {
vol.update_bind_group(&self.device, field_sys);
let aspect = self.config.width as f32 / self.config.height as f32;
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, aspect, 0.1, 100.0);
let view = self.camera.view_matrix();
let view_proj = proj * view;
let inv_view_proj = view_proj.inverse();
let camera_pos = self.camera.position();
let field_idx = vol.field_index;
let field_extent = field_sys.fields[field_idx].config.world_extent;
let field_resolution = field_sys.fields[field_idx].config.resolution;
vol.update_params_with_field(
&self.queue,
inv_view_proj,
camera_pos,
field_extent,
field_resolution,
);
}
}
if let Some(ref trail) = self.trail_state {
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Trail Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&trail.compute_pipeline);
compute_pass.set_bind_group(0, &trail.compute_bind_group, &[]);
let workgroups = self.num_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
if let Some(ref conn) = self.connection_state {
self.queue.write_buffer(&conn.count_buffer, 0, &[0u8; 4]);
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Connection Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&conn.compute_pipeline);
compute_pass.set_bind_group(0, &conn.compute_bind_group, &[]);
let workgroups = self.num_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
if self.picking.has_pending_pick() {
self.picking.render(
&mut encoder,
&self.particle_buffer,
&self.uniform_bind_group,
self.num_particles,
);
}
let render_target = if let Some(ref pp) = self.post_process {
&pp.view
} else {
&view
};
let depth_target = if let Some(ref pp) = self.post_process {
&pp.depth_view
} else {
&self.depth_texture
};
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: render_target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: self.background_color.x as f64,
g: self.background_color.y as f64,
b: self.background_color.z as f64,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_target,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(ref conn) = self.connection_state {
render_pass.set_pipeline(&conn.render_pipeline);
render_pass.set_bind_group(0, &conn.render_bind_group, &[]);
render_pass.draw(0..6, 0..conn.max_connections);
}
if let Some(ref grid) = self.spatial_grid_viz {
if grid.opacity > 0.0 {
render_pass.set_pipeline(grid.pipeline());
render_pass.set_bind_group(0, grid.bind_group(), &[]);
render_pass.draw(0..6, 0..grid.line_count());
}
}
if let Some(ref trail) = self.trail_state {
render_pass.set_pipeline(&trail.render_pipeline);
render_pass.set_bind_group(0, &trail.render_bind_group, &[]);
let total_trail_instances = self.num_particles * trail.trail_length;
render_pass.draw(0..6, 0..total_trail_instances);
}
if let Some(ref wireframe) = self.wireframe_state {
render_pass.set_pipeline(wireframe.pipeline());
render_pass.set_bind_group(0, wireframe.bind_group(), &[]);
render_pass.draw(0..6, 0..wireframe.total_line_count());
} else {
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
if let Some(ref tex_bind_group) = self.texture_bind_group {
render_pass.set_bind_group(1, tex_bind_group, &[]);
}
render_pass.set_vertex_buffer(0, self.particle_buffer.slice(..));
render_pass.draw(0..6, 0..self.num_particles);
}
}
if let Some(ref vol) = self.volume_render {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Volume Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: render_target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None, timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&vol.pipeline);
render_pass.set_bind_group(0, &vol.bind_group, &[]);
render_pass.draw(0..3, 0..1); }
if let Some(ref pp) = self.post_process {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Post-Process Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: 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,
});
render_pass.set_pipeline(&pp.pipeline);
render_pass.set_bind_group(0, &pp.bind_group, &[]);
render_pass.draw(0..3, 0..1); }
self.picking.copy_pixel(&mut encoder);
if self.picking.needs_particle_data_copy() {
self.picking.copy_particle_data(&mut encoder, &self.particle_buffer);
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
self.picking.read_result(&self.device);
self.picking.read_particle_data(&self.device);
Ok(())
}
}
fn create_depth_texture(
device: &wgpu::Device,
config: &wgpu::SurfaceConfiguration,
) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Depth Texture"),
size: wgpu::Extent3d {
width: config.width,
height: config.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}