1use bytemuck::{Pod, Zeroable};
29use wgpu::util::DeviceExt;
30
31#[repr(C)]
35#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable, Default)]
36pub struct CustomWaveVertex {
37 pub pos: [f32; 2],
38 pub color: [f32; 4],
39}
40
41#[derive(Debug, Clone, Copy)]
46pub struct CustomWaveBatch {
47 pub start_vertex: u32,
50 pub vertex_count: u32,
52 pub dots: bool,
59 pub additive: bool,
61}
62
63pub const MAX_CUSTOM_WAVE_VERTICES: usize = 16_384;
69
70pub struct CustomWaveRenderer {
74 pipelines: [wgpu::RenderPipeline; 4],
75 vertex_buffer: wgpu::Buffer,
76 vertex_count: u32,
79 batches: Vec<CustomWaveBatch>,
81}
82
83#[inline]
84fn pipeline_index(dots: bool, additive: bool) -> usize {
85 (additive as usize) | ((dots as usize) << 1)
86}
87
88impl CustomWaveRenderer {
89 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
90 let shader = crate::pipeline_helpers::load_wgsl(
91 device,
92 "Custom Wave Shader",
93 include_str!("../shaders/custom_wave.wgsl"),
94 );
95
96 let initial: Vec<CustomWaveVertex> =
97 vec![CustomWaveVertex::default(); MAX_CUSTOM_WAVE_VERTICES];
98 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
99 label: Some("Custom Wave Vertices"),
100 contents: bytemuck::cast_slice(&initial),
101 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
102 });
103
104 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
105 label: Some("Custom Wave Layout"),
106 bind_group_layouts: &[],
107 immediate_size: 0,
108 });
109
110 let vertex_attributes = [
111 wgpu::VertexAttribute {
112 offset: 0,
113 shader_location: 0,
114 format: wgpu::VertexFormat::Float32x2,
115 },
116 wgpu::VertexAttribute {
117 offset: std::mem::size_of::<[f32; 2]>() as u64,
118 shader_location: 1,
119 format: wgpu::VertexFormat::Float32x4,
120 },
121 ];
122 let vertex_layout = wgpu::VertexBufferLayout {
123 array_stride: std::mem::size_of::<CustomWaveVertex>() as u64,
124 step_mode: wgpu::VertexStepMode::Vertex,
125 attributes: &vertex_attributes,
126 };
127
128 let make_pipeline = |label: &str,
129 topology: wgpu::PrimitiveTopology,
130 additive: bool|
131 -> wgpu::RenderPipeline {
132 let blend = crate::pipeline_helpers::blend_state_for(additive);
133 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
134 label: Some(label),
135 layout: Some(&layout),
136 vertex: wgpu::VertexState {
137 module: &shader,
138 entry_point: Some("vs_main"),
139 buffers: std::slice::from_ref(&vertex_layout),
140 compilation_options: Default::default(),
141 },
142 fragment: Some(wgpu::FragmentState {
143 module: &shader,
144 entry_point: Some("fs_main"),
145 targets: &[Some(wgpu::ColorTargetState {
146 format,
147 blend: Some(blend),
148 write_mask: wgpu::ColorWrites::ALL,
149 })],
150 compilation_options: Default::default(),
151 }),
152 primitive: wgpu::PrimitiveState {
153 topology,
154 ..Default::default()
155 },
156 depth_stencil: None,
157 multisample: wgpu::MultisampleState::default(),
158 multiview_mask: None,
159 cache: None,
160 })
161 };
162
163 let pipelines = [
164 make_pipeline(
165 "CustomWave Lines Alpha",
166 wgpu::PrimitiveTopology::LineStrip,
167 false,
168 ),
169 make_pipeline(
170 "CustomWave Lines Additive",
171 wgpu::PrimitiveTopology::LineStrip,
172 true,
173 ),
174 make_pipeline(
175 "CustomWave Dots Alpha",
176 wgpu::PrimitiveTopology::TriangleList,
177 false,
178 ),
179 make_pipeline(
180 "CustomWave Dots Additive",
181 wgpu::PrimitiveTopology::TriangleList,
182 true,
183 ),
184 ];
185
186 Self {
187 pipelines,
188 vertex_buffer,
189 vertex_count: 0,
190 batches: Vec::new(),
191 }
192 }
193
194 pub fn update(
199 &mut self,
200 queue: &wgpu::Queue,
201 vertices: &[CustomWaveVertex],
202 batches: &[CustomWaveBatch],
203 ) {
204 let n = vertices.len().min(MAX_CUSTOM_WAVE_VERTICES);
205 if vertices.len() > MAX_CUSTOM_WAVE_VERTICES {
206 log::warn!(
207 "custom-wave vertex stream truncated: {} > cap {}",
208 vertices.len(),
209 MAX_CUSTOM_WAVE_VERTICES
210 );
211 }
212 if n > 0 {
213 queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices[..n]));
214 }
215 self.vertex_count = n as u32;
216 self.batches.clear();
217 for b in batches {
219 if b.start_vertex + b.vertex_count <= self.vertex_count {
220 self.batches.push(*b);
221 }
222 }
223 }
224
225 pub fn render(&self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
229 if self.batches.is_empty() || self.vertex_count == 0 {
230 return;
231 }
232 let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
233 label: Some("Custom Wave Pass"),
234 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
235 view,
236 depth_slice: None,
237 resolve_target: None,
238 ops: wgpu::Operations {
239 load: wgpu::LoadOp::Load,
240 store: wgpu::StoreOp::Store,
241 },
242 })],
243 depth_stencil_attachment: None,
244 timestamp_writes: None,
245 occlusion_query_set: None,
246 multiview_mask: None,
247 });
248 rp.set_vertex_buffer(0, self.vertex_buffer.slice(..));
249 for b in &self.batches {
250 if b.vertex_count == 0 {
251 continue;
252 }
253 let pidx = pipeline_index(b.dots, b.additive);
254 rp.set_pipeline(&self.pipelines[pidx]);
255 let end = b.start_vertex + b.vertex_count;
256 rp.draw(b.start_vertex..end, 0..1);
257 }
258 }
259
260 pub fn vertex_count(&self) -> u32 {
263 self.vertex_count
264 }
265
266 pub fn batch_count(&self) -> usize {
268 self.batches.len()
269 }
270}
271
272#[inline]
275pub fn preset_xy_to_clip(x: f64, y: f64) -> [f32; 2] {
276 [(x * 2.0 - 1.0) as f32, (1.0 - y * 2.0) as f32]
277}
278
279pub fn point_to_dot_quad(
284 x: f64,
285 y: f64,
286 color: [f32; 4],
287 radius_clip: f32,
288) -> [CustomWaveVertex; 6] {
289 let [cx, cy] = preset_xy_to_clip(x, y);
290 let lo_x = cx - radius_clip;
291 let hi_x = cx + radius_clip;
292 let lo_y = cy - radius_clip;
293 let hi_y = cy + radius_clip;
294 let tl = CustomWaveVertex {
295 pos: [lo_x, hi_y],
296 color,
297 };
298 let tr = CustomWaveVertex {
299 pos: [hi_x, hi_y],
300 color,
301 };
302 let bl = CustomWaveVertex {
303 pos: [lo_x, lo_y],
304 color,
305 };
306 let br = CustomWaveVertex {
307 pos: [hi_x, lo_y],
308 color,
309 };
310 [tl, tr, bl, tr, br, bl]
311}
312
313pub fn segment_to_thick_quad(
324 ax: f64,
325 ay: f64,
326 color_a: [f32; 4],
327 bx: f64,
328 by: f64,
329 color_b: [f32; 4],
330 thickness_clip: f32,
331) -> [CustomWaveVertex; 6] {
332 let [ax_c, ay_c] = preset_xy_to_clip(ax, ay);
333 let [bx_c, by_c] = preset_xy_to_clip(bx, by);
334 let dx = bx_c - ax_c;
335 let dy = by_c - ay_c;
336 let len = (dx * dx + dy * dy).sqrt().max(1e-6);
337 let half = thickness_clip * 0.5;
339 let nx = -dy / len * half;
340 let ny = dx / len * half;
341 let a_minus = CustomWaveVertex {
342 pos: [ax_c - nx, ay_c - ny],
343 color: color_a,
344 };
345 let a_plus = CustomWaveVertex {
346 pos: [ax_c + nx, ay_c + ny],
347 color: color_a,
348 };
349 let b_minus = CustomWaveVertex {
350 pos: [bx_c - nx, by_c - ny],
351 color: color_b,
352 };
353 let b_plus = CustomWaveVertex {
354 pos: [bx_c + nx, by_c + ny],
355 color: color_b,
356 };
357 [a_minus, b_minus, b_plus, a_minus, b_plus, a_plus]
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn preset_xy_to_clip_center_is_origin() {
366 let p = preset_xy_to_clip(0.5, 0.5);
367 assert!((p[0] - 0.0).abs() < 1e-5);
368 assert!((p[1] - 0.0).abs() < 1e-5);
369 }
370
371 #[test]
372 fn preset_xy_to_clip_topleft_is_neg1_pos1() {
373 let p = preset_xy_to_clip(0.0, 0.0);
374 assert!((p[0] - -1.0).abs() < 1e-5);
375 assert!((p[1] - 1.0).abs() < 1e-5);
376 }
377
378 #[test]
379 fn preset_xy_to_clip_bottomright_is_pos1_neg1() {
380 let p = preset_xy_to_clip(1.0, 1.0);
381 assert!((p[0] - 1.0).abs() < 1e-5);
382 assert!((p[1] - -1.0).abs() < 1e-5);
383 }
384
385 #[test]
395 fn segment_to_thick_quad_horizontal_yields_perpendicular_offset() {
396 let q = segment_to_thick_quad(
397 0.0,
398 0.5,
399 [1.0, 0.0, 0.0, 1.0],
400 1.0,
401 0.5,
402 [0.0, 1.0, 0.0, 1.0],
403 0.01,
404 );
405 let half = 0.005f32;
410 for v in q.iter() {
411 let near_a = (v.pos[0] + 1.0).abs() < 1e-4;
413 let near_b = (v.pos[0] - 1.0).abs() < 1e-4;
414 assert!(near_a || near_b, "x off-axis: {v:?}");
415 assert!(
416 (v.pos[1].abs() - half).abs() < 1e-4,
417 "y not at ±half-thickness: {v:?}"
418 );
419 if near_a {
420 assert_eq!(v.color, [1.0, 0.0, 0.0, 1.0]);
421 }
422 if near_b {
423 assert_eq!(v.color, [0.0, 1.0, 0.0, 1.0]);
424 }
425 }
426 }
427
428 #[test]
429 fn point_to_dot_quad_centered_at_xy() {
430 let q = point_to_dot_quad(0.5, 0.5, [1.0, 0.0, 0.0, 1.0], 0.01);
431 for v in q.iter() {
433 assert!(v.pos[0].abs() <= 0.011);
434 assert!(v.pos[1].abs() <= 0.011);
435 assert_eq!(v.color, [1.0, 0.0, 0.0, 1.0]);
436 }
437 }
438
439 #[test]
440 fn pipeline_index_table() {
441 assert_eq!(pipeline_index(false, false), 0); assert_eq!(pipeline_index(false, true), 1); assert_eq!(pipeline_index(true, false), 2); assert_eq!(pipeline_index(true, true), 3); }
446}