1use super::*;
2
3#[derive(Default)]
6pub(crate) struct StreamtubeResources {
7 pub(crate) pipeline: Option<DualPipeline>,
9 pub(crate) wireframe_pipeline: Option<DualPipeline>,
11 pub(crate) bgl: Option<wgpu::BindGroupLayout>,
13}
14
15#[derive(Default)]
17pub(crate) struct RibbonResources {
18 pub(crate) pipeline: Option<DualPipeline>,
20 pub(crate) pipeline_additive: Option<DualPipeline>,
22 pub(crate) pipeline_premultiplied: Option<DualPipeline>,
24 pub(crate) wireframe_pipeline: Option<DualPipeline>,
26 pub(crate) bgl: Option<wgpu::BindGroupLayout>,
28}
29
30impl DeviceResources {
31 pub(crate) fn ensure_streamtube_pipeline(&mut self, device: &wgpu::Device) {
36 if self.streamtube.pipeline.is_some() {
37 return;
38 }
39
40 let streamtube_bgl = crate::resources::builders::uniform_bgl(
41 device,
42 "streamtube_bgl",
43 wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
44 );
45
46 let shader = crate::resources::builders::wgsl_module(
47 device,
48 "streamtube_shader",
49 crate::resources::builders::wgsl_source!("streamtube"),
50 );
51
52 let layout = crate::resources::builders::standard_scene_layout(
53 device,
54 "streamtube_pipeline_layout",
55 &self.camera_bind_group_layout,
56 &streamtube_bgl,
57 );
58
59 let ribbon_bgl = crate::resources::builders::uniform_texture_sampler_bgl(
63 device,
64 "ribbon_bgl",
65 wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
66 wgpu::ShaderStages::FRAGMENT,
67 );
68 let ribbon_layout = crate::resources::builders::standard_scene_layout(
69 device,
70 "ribbon_pipeline_layout",
71 &self.camera_bind_group_layout,
72 &ribbon_bgl,
73 );
74
75 let ribbon_shader = crate::resources::builders::wgsl_module(
77 device,
78 "ribbon_shader",
79 crate::resources::builders::wgsl_source!("ribbon"),
80 );
81 let additive_blend = wgpu::BlendState {
82 color: wgpu::BlendComponent {
83 src_factor: wgpu::BlendFactor::One,
84 dst_factor: wgpu::BlendFactor::One,
85 operation: wgpu::BlendOperation::Add,
86 },
87 alpha: wgpu::BlendComponent {
88 src_factor: wgpu::BlendFactor::One,
89 dst_factor: wgpu::BlendFactor::One,
90 operation: wgpu::BlendOperation::Add,
91 },
92 };
93 let premultiplied_blend = wgpu::BlendState {
94 color: wgpu::BlendComponent {
95 src_factor: wgpu::BlendFactor::One,
96 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
97 operation: wgpu::BlendOperation::Add,
98 },
99 alpha: wgpu::BlendComponent {
100 src_factor: wgpu::BlendFactor::One,
101 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
102 operation: wgpu::BlendOperation::Add,
103 },
104 };
105 use crate::resources::builders::{DualPipelineDesc, build_dual_pipeline};
106 self.streamtube.bgl = Some(streamtube_bgl);
107 self.ribbon.bgl = Some(ribbon_bgl);
108 self.streamtube.pipeline = Some(build_dual_pipeline(
109 device,
110 &DualPipelineDesc {
111 label: "streamtube_pipeline",
112 layout: &layout,
113 shader: &shader,
114 vertex_entry: "vs_main",
115 fragment_entry: "fs_main",
116 vertex_buffers: &[Vertex::buffer_layout()],
117 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
118 topology: wgpu::PrimitiveTopology::TriangleList,
119 cull_mode: Some(wgpu::Face::Back),
120 depth_write: true,
121 depth_compare: wgpu::CompareFunction::Less,
122 sample_count: self.sample_count,
123 ldr_format: self.target_format,
124 },
125 ));
126 self.streamtube.wireframe_pipeline = Some(build_dual_pipeline(
129 device,
130 &DualPipelineDesc {
131 label: "streamtube_wireframe_pipeline",
132 layout: &layout,
133 shader: &shader,
134 vertex_entry: "vs_main",
135 fragment_entry: "fs_main",
136 vertex_buffers: &[Vertex::buffer_layout()],
137 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
138 topology: wgpu::PrimitiveTopology::LineList,
139 cull_mode: None,
140 depth_write: true,
141 depth_compare: wgpu::CompareFunction::Less,
142 sample_count: self.sample_count,
143 ldr_format: self.target_format,
144 },
145 ));
146 self.ribbon.pipeline = Some(build_dual_pipeline(
150 device,
151 &DualPipelineDesc {
152 label: "ribbon_pipeline",
153 layout: &ribbon_layout,
154 shader: &ribbon_shader,
155 vertex_entry: "vs_main",
156 fragment_entry: "fs_main",
157 vertex_buffers: &[Vertex::buffer_layout()],
158 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
159 topology: wgpu::PrimitiveTopology::TriangleList,
160 cull_mode: None,
161 depth_write: true,
162 depth_compare: wgpu::CompareFunction::Less,
163 sample_count: self.sample_count,
164 ldr_format: self.target_format,
165 },
166 ));
167 self.ribbon.pipeline_additive = Some(build_dual_pipeline(
168 device,
169 &DualPipelineDesc {
170 label: "ribbon_pipeline_additive",
171 layout: &ribbon_layout,
172 shader: &ribbon_shader,
173 vertex_entry: "vs_main",
174 fragment_entry: "fs_main",
175 vertex_buffers: &[Vertex::buffer_layout()],
176 blend: Some(additive_blend),
177 topology: wgpu::PrimitiveTopology::TriangleList,
178 cull_mode: None,
179 depth_write: false,
180 depth_compare: wgpu::CompareFunction::Less,
181 sample_count: self.sample_count,
182 ldr_format: self.target_format,
183 },
184 ));
185 self.ribbon.pipeline_premultiplied = Some(build_dual_pipeline(
186 device,
187 &DualPipelineDesc {
188 label: "ribbon_pipeline_premultiplied",
189 layout: &ribbon_layout,
190 shader: &ribbon_shader,
191 vertex_entry: "vs_main",
192 fragment_entry: "fs_main",
193 vertex_buffers: &[Vertex::buffer_layout()],
194 blend: Some(premultiplied_blend),
195 topology: wgpu::PrimitiveTopology::TriangleList,
196 cull_mode: None,
197 depth_write: false,
198 depth_compare: wgpu::CompareFunction::Less,
199 sample_count: self.sample_count,
200 ldr_format: self.target_format,
201 },
202 ));
203 self.ribbon.wireframe_pipeline = Some(build_dual_pipeline(
205 device,
206 &DualPipelineDesc {
207 label: "ribbon_wireframe_pipeline",
208 layout: &ribbon_layout,
209 shader: &ribbon_shader,
210 vertex_entry: "vs_main",
211 fragment_entry: "fs_main",
212 vertex_buffers: &[Vertex::buffer_layout()],
213 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
214 topology: wgpu::PrimitiveTopology::LineList,
215 cull_mode: None,
216 depth_write: true,
217 depth_compare: wgpu::CompareFunction::Less,
218 sample_count: self.sample_count,
219 ldr_format: self.target_format,
220 },
221 ));
222 }
223
224 pub(crate) fn upload_streamtube_per_frame(
231 &mut self,
232 device: &wgpu::Device,
233 queue: &wgpu::Queue,
234 item: &crate::renderer::StreamtubeItem,
235 wireframe: bool,
236 ) -> StreamtubeGpuData {
237 const SIDES: usize = 12; let radius = item.radius.max(f32::EPSILON);
240
241 let mut verts: Vec<Vertex> = Vec::new();
242 let mut indices: Vec<u32> = Vec::new();
243
244 let positions = &item.positions;
245 let mut strip_start = 0usize;
246
247 for &strip_len in &item.strip_lengths {
248 let strip_len = strip_len as usize;
249 let strip_end = (strip_start + strip_len).min(positions.len());
250 let pts: Vec<glam::Vec3> = positions[strip_start..strip_end]
251 .iter()
252 .map(|&p| glam::Vec3::from(p))
253 .collect();
254 strip_start += strip_len;
255
256 if pts.len() < 2 {
257 continue;
258 }
259
260 let t0 = (pts[1] - pts[0]).normalize_or_zero();
263 if t0.length_squared() < 1e-10 {
264 continue;
265 }
266 let ref_v = if t0.x.abs() < 0.9 {
268 glam::Vec3::X
269 } else {
270 glam::Vec3::Y
271 };
272 let mut u = t0.cross(ref_v).normalize(); let ring_base = verts.len() as u32;
276 let n_rings = pts.len();
277
278 for (k, &pt) in pts.iter().enumerate() {
279 let tangent = if k + 1 < pts.len() {
281 (pts[k + 1] - pt).normalize_or_zero()
282 } else {
283 (pt - pts[k - 1]).normalize_or_zero()
284 };
285
286 if k > 0 {
288 let t_prev = (pts[k] - pts[k - 1]).normalize_or_zero();
289 let axis = t_prev.cross(tangent);
291 let sin_a = axis.length().min(1.0);
292 if sin_a > 1e-6 {
293 let cos_a = t_prev.dot(tangent).clamp(-1.0, 1.0);
294 let ax = axis / sin_a;
295 u = u * cos_a + ax.cross(u) * sin_a + ax * ax.dot(u) * (1.0 - cos_a);
297 u = u.normalize_or_zero();
298 }
299 }
300
301 let v = tangent.cross(u).normalize_or_zero();
302
303 for s in 0..SIDES {
305 let theta = 2.0 * std::f32::consts::PI * (s as f32) / (SIDES as f32);
306 let nx = theta.cos() * u.x + theta.sin() * v.x;
307 let ny = theta.cos() * u.y + theta.sin() * v.y;
308 let nz = theta.cos() * u.z + theta.sin() * v.z;
309 let normal = glam::Vec3::new(nx, ny, nz);
310 let world_pos = pt + normal * radius;
311 verts.push(Vertex {
312 position: world_pos.to_array(),
313 normal: normal.to_array(),
314 colour: [1.0, 1.0, 1.0, 1.0], uv: [0.0, 0.0],
316 tangent: [1.0, 0.0, 0.0, 1.0],
317 });
318 }
319
320 if k > 0 {
324 let r0 = ring_base + ((k - 1) * SIDES) as u32;
325 let r1 = ring_base + (k * SIDES) as u32;
326 for s in 0..SIDES {
327 let s1 = (s + 1) % SIDES;
328 indices.push(r0 + s as u32);
329 indices.push(r0 + s1 as u32);
330 indices.push(r1 + s as u32);
331
332 indices.push(r0 + s1 as u32);
333 indices.push(r1 + s1 as u32);
334 indices.push(r1 + s as u32);
335 }
336 }
337 }
338
339 {
342 let last_ring = ring_base + ((n_rings - 1) * SIDES) as u32;
343 let tangent = (pts[n_rings - 1] - pts[n_rings - 2]).normalize_or_zero();
344 let cap_center_idx = verts.len() as u32;
345 verts.push(Vertex {
346 position: pts[n_rings - 1].to_array(),
347 normal: tangent.to_array(),
348 colour: [1.0, 1.0, 1.0, 1.0],
349 uv: [0.0, 0.0],
350 tangent: [1.0, 0.0, 0.0, 1.0],
351 });
352 for s in 0..SIDES {
353 let s1 = (s + 1) % SIDES;
354 indices.push(cap_center_idx);
355 indices.push(last_ring + s as u32);
356 indices.push(last_ring + s1 as u32);
357 }
358 }
359
360 {
363 let tangent = (pts[0] - pts[1]).normalize_or_zero();
364 let cap_center_idx = verts.len() as u32;
365 verts.push(Vertex {
366 position: pts[0].to_array(),
367 normal: tangent.to_array(),
368 colour: [1.0, 1.0, 1.0, 1.0],
369 uv: [0.0, 0.0],
370 tangent: [1.0, 0.0, 0.0, 1.0],
371 });
372 for s in 0..SIDES {
373 let s1 = (s + 1) % SIDES;
374 indices.push(cap_center_idx);
375 indices.push(ring_base + s1 as u32);
376 indices.push(ring_base + s as u32);
377 }
378 }
379 }
380
381 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
383 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
384
385 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
386 label: Some("streamtube_vbuf"),
387 size: vert_bytes.len().max(std::mem::size_of::<Vertex>()) as u64,
388 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
389 mapped_at_creation: false,
390 });
391 if !vert_bytes.is_empty() {
392 queue.write_buffer(&vertex_buffer, 0, vert_bytes);
393 }
394
395 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
396 label: Some("streamtube_ibuf"),
397 size: idx_bytes.len().max(12) as u64,
398 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
399 mapped_at_creation: false,
400 });
401 if !idx_bytes.is_empty() {
402 queue.write_buffer(&index_buffer, 0, idx_bytes);
403 }
404
405 let index_count = indices.len() as u32;
406
407 let edge_indices = crate::resources::mesh::geometry::generate_edge_indices(&indices);
409 let edge_bytes: &[u8] = bytemuck::cast_slice(&edge_indices);
410 let edge_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
411 label: Some("streamtube_edge_ibuf"),
412 size: edge_bytes.len().max(8) as u64,
413 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
414 mapped_at_creation: false,
415 });
416 if !edge_bytes.is_empty() {
417 queue.write_buffer(&edge_index_buffer, 0, edge_bytes);
418 }
419 let edge_index_count = edge_indices.len() as u32;
420
421 #[repr(C)]
423 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
424 struct StreamtubeUniform {
425 model: [[f32; 4]; 4],
426 colour: [f32; 4],
427 radius: f32,
428 use_vertex_colour: u32,
429 unlit: u32,
430 opacity: f32,
431 wireframe: u32,
432 _pad: [f32; 3],
433 }
434 let uniform_data = StreamtubeUniform {
435 model: item.model,
436 colour: item.colour,
437 radius,
438 use_vertex_colour: 0,
439 unlit: item.settings.unlit as u32,
440 opacity: item.settings.opacity,
441 wireframe: wireframe as u32,
442 _pad: [0.0; 3],
443 };
444 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
445 label: Some("streamtube_uniform_buf"),
446 size: std::mem::size_of::<StreamtubeUniform>() as u64,
447 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
448 mapped_at_creation: false,
449 });
450 queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
451
452 let bgl = self
453 .streamtube
454 .bgl
455 .as_ref()
456 .expect("ensure_streamtube_pipeline not called");
457 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
458 label: Some("streamtube_uniform_bg"),
459 layout: bgl,
460 entries: &[wgpu::BindGroupEntry {
461 binding: 0,
462 resource: uniform_buf.as_entire_binding(),
463 }],
464 });
465
466 StreamtubeGpuData {
467 vertex_buffer,
468 index_buffer,
469 index_count,
470 edge_index_buffer,
471 edge_index_count,
472 wireframe,
473 uniform_bind_group,
474 blend: crate::renderer::SpriteBlend::AlphaBlend,
475 _uniform_buf: uniform_buf,
476 }
477 }
478
479 pub fn upload_streamtube(
485 &mut self,
486 device: &wgpu::Device,
487 queue: &wgpu::Queue,
488 item: &crate::renderer::StreamtubeItem,
489 ) -> crate::resources::StreamtubeId {
490 self.ensure_streamtube_pipeline(device);
491 let gpu = self.upload_streamtube_per_frame(device, queue, item, false);
492 self.content.streamtube_store.insert(gpu)
493 }
494
495 pub fn drop_streamtube(&mut self, id: crate::resources::StreamtubeId) -> bool {
497 self.content.streamtube_store.remove(id)
498 }
499
500 pub fn replace_streamtube(
502 &mut self,
503 device: &wgpu::Device,
504 queue: &wgpu::Queue,
505 id: crate::resources::StreamtubeId,
506 item: &crate::renderer::StreamtubeItem,
507 ) -> bool {
508 if !self.content.streamtube_store.contains(id) {
509 return false;
510 }
511 self.ensure_streamtube_pipeline(device);
512 let gpu = self.upload_streamtube_per_frame(device, queue, item, false);
513 self.content.streamtube_store.replace(id, gpu)
514 }
515
516 pub(crate) fn upload_tube_per_frame(
526 &mut self,
527 device: &wgpu::Device,
528 queue: &wgpu::Queue,
529 item: &crate::renderer::TubeItem,
530 wireframe: bool,
531 ) -> StreamtubeGpuData {
532 let sides = (item.sides.max(3)) as usize;
533
534 let (use_vertex_colour, lut_rgba): (u32, Option<[[u8; 4]; 256]>) =
536 if !item.scalars.is_empty() {
537 let lut = self
538 .content
539 .builtin_colourmap_ids
540 .and_then(|ids| {
541 let preset_id = item
542 .colourmap_id
543 .unwrap_or(ids[crate::resources::BuiltinColourmap::Viridis as usize]);
544 self.content.colourmaps_cpu.get(preset_id.0).copied()
545 })
546 .unwrap_or([[128u8; 4]; 256]);
547 (1, Some(lut))
548 } else {
549 (0, None)
550 };
551
552 let scalar_min = item
553 .scalar_range
554 .map(|r| r.0)
555 .unwrap_or_else(|| item.scalars.iter().cloned().fold(f32::INFINITY, f32::min));
556 let scalar_max = item.scalar_range.map(|r| r.1).unwrap_or_else(|| {
557 item.scalars
558 .iter()
559 .cloned()
560 .fold(f32::NEG_INFINITY, f32::max)
561 });
562 let scalar_range = (scalar_max - scalar_min).max(f32::EPSILON);
563
564 let scalar_to_colour = |idx: usize| -> [f32; 4] {
566 if let Some(ref lut) = lut_rgba {
567 let s = *item.scalars.get(idx).unwrap_or(&0.0);
568 let t = ((s - scalar_min) / scalar_range).clamp(0.0, 1.0);
569 let lut_idx = ((t * 255.0).round() as usize).min(255);
570 let c = lut[lut_idx];
571 [
572 c[0] as f32 / 255.0,
573 c[1] as f32 / 255.0,
574 c[2] as f32 / 255.0,
575 c[3] as f32 / 255.0,
576 ]
577 } else {
578 item.colour
579 }
580 };
581
582 let mut verts: Vec<Vertex> = Vec::new();
583 let mut indices: Vec<u32> = Vec::new();
584
585 let positions = &item.positions;
586 let mut strip_start = 0usize;
587
588 for &strip_len in &item.strip_lengths {
589 let strip_len = strip_len as usize;
590 let strip_end = (strip_start + strip_len).min(positions.len());
591 let pts: Vec<glam::Vec3> = positions[strip_start..strip_end]
592 .iter()
593 .map(|&p| glam::Vec3::from(p))
594 .collect();
595 let pts_scalar_start = strip_start;
596 strip_start += strip_len;
597
598 if pts.len() < 2 {
599 continue;
600 }
601
602 let t0 = (pts[1] - pts[0]).normalize_or_zero();
604 if t0.length_squared() < 1e-10 {
605 continue;
606 }
607 let ref_v = if t0.x.abs() < 0.9 {
608 glam::Vec3::X
609 } else {
610 glam::Vec3::Y
611 };
612 let mut u = t0.cross(ref_v).normalize();
613
614 let ring_base = verts.len() as u32;
615 let n_rings = pts.len();
616
617 for (k, &pt) in pts.iter().enumerate() {
618 let tangent = if k + 1 < pts.len() {
619 (pts[k + 1] - pt).normalize_or_zero()
620 } else {
621 (pt - pts[k - 1]).normalize_or_zero()
622 };
623
624 if k > 0 {
625 let t_prev = (pts[k] - pts[k - 1]).normalize_or_zero();
626 let axis = t_prev.cross(tangent);
627 let sin_a = axis.length().min(1.0);
628 if sin_a > 1e-6 {
629 let cos_a = t_prev.dot(tangent).clamp(-1.0, 1.0);
630 let ax = axis / sin_a;
631 u = u * cos_a + ax.cross(u) * sin_a + ax * ax.dot(u) * (1.0 - cos_a);
632 u = u.normalize_or_zero();
633 }
634 }
635
636 let v = tangent.cross(u).normalize_or_zero();
637
638 let point_radius = item
640 .radius_attribute
641 .as_ref()
642 .and_then(|ra| ra.get(pts_scalar_start + k).copied())
643 .unwrap_or(item.radius)
644 .max(f32::EPSILON);
645
646 let vertex_colour = scalar_to_colour(pts_scalar_start + k);
647
648 for s in 0..sides {
649 let theta = 2.0 * std::f32::consts::PI * (s as f32) / (sides as f32);
650 let nx = theta.cos() * u.x + theta.sin() * v.x;
651 let ny = theta.cos() * u.y + theta.sin() * v.y;
652 let nz = theta.cos() * u.z + theta.sin() * v.z;
653 let normal = glam::Vec3::new(nx, ny, nz);
654 let world_pos = pt + normal * point_radius;
655 verts.push(Vertex {
656 position: world_pos.to_array(),
657 normal: normal.to_array(),
658 colour: vertex_colour,
659 uv: [0.0, 0.0],
660 tangent: [1.0, 0.0, 0.0, 1.0],
661 });
662 }
663
664 if k > 0 {
665 let r0 = ring_base + ((k - 1) * sides) as u32;
666 let r1 = ring_base + (k * sides) as u32;
667 for s in 0..sides {
668 let s1 = (s + 1) % sides;
669 indices.push(r0 + s as u32);
670 indices.push(r0 + s1 as u32);
671 indices.push(r1 + s as u32);
672
673 indices.push(r0 + s1 as u32);
674 indices.push(r1 + s1 as u32);
675 indices.push(r1 + s as u32);
676 }
677 }
678 }
679
680 {
682 let last_ring = ring_base + ((n_rings - 1) * sides) as u32;
683 let tangent = (pts[n_rings - 1] - pts[n_rings - 2]).normalize_or_zero();
684 let cap_colour = scalar_to_colour(pts_scalar_start + n_rings - 1);
685 let cap_center_idx = verts.len() as u32;
686 verts.push(Vertex {
687 position: pts[n_rings - 1].to_array(),
688 normal: tangent.to_array(),
689 colour: cap_colour,
690 uv: [0.0, 0.0],
691 tangent: [1.0, 0.0, 0.0, 1.0],
692 });
693 for s in 0..sides {
694 let s1 = (s + 1) % sides;
695 indices.push(cap_center_idx);
696 indices.push(last_ring + s as u32);
697 indices.push(last_ring + s1 as u32);
698 }
699 }
700
701 {
703 let tangent = (pts[0] - pts[1]).normalize_or_zero();
704 let cap_colour = scalar_to_colour(pts_scalar_start);
705 let cap_center_idx = verts.len() as u32;
706 verts.push(Vertex {
707 position: pts[0].to_array(),
708 normal: tangent.to_array(),
709 colour: cap_colour,
710 uv: [0.0, 0.0],
711 tangent: [1.0, 0.0, 0.0, 1.0],
712 });
713 for s in 0..sides {
714 let s1 = (s + 1) % sides;
715 indices.push(cap_center_idx);
716 indices.push(ring_base + s1 as u32);
717 indices.push(ring_base + s as u32);
718 }
719 }
720 }
721
722 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
724 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
725
726 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
727 label: Some("tube_vbuf"),
728 size: vert_bytes.len().max(std::mem::size_of::<Vertex>()) as u64,
729 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
730 mapped_at_creation: false,
731 });
732 if !vert_bytes.is_empty() {
733 queue.write_buffer(&vertex_buffer, 0, vert_bytes);
734 }
735
736 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
737 label: Some("tube_ibuf"),
738 size: idx_bytes.len().max(12) as u64,
739 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
740 mapped_at_creation: false,
741 });
742 if !idx_bytes.is_empty() {
743 queue.write_buffer(&index_buffer, 0, idx_bytes);
744 }
745
746 let index_count = indices.len() as u32;
747
748 let edge_indices = crate::resources::mesh::geometry::generate_edge_indices(&indices);
749 let edge_bytes: &[u8] = bytemuck::cast_slice(&edge_indices);
750 let edge_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
751 label: Some("tube_edge_ibuf"),
752 size: edge_bytes.len().max(8) as u64,
753 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
754 mapped_at_creation: false,
755 });
756 if !edge_bytes.is_empty() {
757 queue.write_buffer(&edge_index_buffer, 0, edge_bytes);
758 }
759 let edge_index_count = edge_indices.len() as u32;
760
761 #[repr(C)]
762 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
763 struct TubeUniform {
764 model: [[f32; 4]; 4],
765 colour: [f32; 4],
766 radius: f32,
767 use_vertex_colour: u32,
768 unlit: u32,
769 opacity: f32,
770 wireframe: u32,
771 _pad: [f32; 3],
772 }
773 let uniform_data = TubeUniform {
774 model: item.model,
775 colour: item.colour,
776 radius: item.radius.max(f32::EPSILON),
777 use_vertex_colour,
778 unlit: item.settings.unlit as u32,
779 opacity: item.settings.opacity,
780 wireframe: wireframe as u32,
781 _pad: [0.0; 3],
782 };
783 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
784 label: Some("tube_uniform_buf"),
785 size: std::mem::size_of::<TubeUniform>() as u64,
786 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
787 mapped_at_creation: false,
788 });
789 queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
790
791 let bgl = self
792 .streamtube
793 .bgl
794 .as_ref()
795 .expect("ensure_streamtube_pipeline not called");
796 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
797 label: Some("tube_uniform_bg"),
798 layout: bgl,
799 entries: &[wgpu::BindGroupEntry {
800 binding: 0,
801 resource: uniform_buf.as_entire_binding(),
802 }],
803 });
804
805 StreamtubeGpuData {
806 vertex_buffer,
807 index_buffer,
808 index_count,
809 edge_index_buffer,
810 edge_index_count,
811 wireframe,
812 uniform_bind_group,
813 blend: crate::renderer::SpriteBlend::AlphaBlend,
814 _uniform_buf: uniform_buf,
815 }
816 }
817
818 pub fn upload_tube(
820 &mut self,
821 device: &wgpu::Device,
822 queue: &wgpu::Queue,
823 item: &crate::renderer::TubeItem,
824 ) -> crate::resources::TubeId {
825 self.ensure_streamtube_pipeline(device);
826 let gpu = self.upload_tube_per_frame(device, queue, item, false);
827 self.content.tube_store.insert(gpu)
828 }
829
830 pub fn drop_tube(&mut self, id: crate::resources::TubeId) -> bool {
832 self.content.tube_store.remove(id)
833 }
834
835 pub fn replace_tube(
837 &mut self,
838 device: &wgpu::Device,
839 queue: &wgpu::Queue,
840 id: crate::resources::TubeId,
841 item: &crate::renderer::TubeItem,
842 ) -> bool {
843 if !self.content.tube_store.contains(id) {
844 return false;
845 }
846 self.ensure_streamtube_pipeline(device);
847 let gpu = self.upload_tube_per_frame(device, queue, item, false);
848 self.content.tube_store.replace(id, gpu)
849 }
850
851 pub(crate) fn upload_ribbon_per_frame(
861 &mut self,
862 device: &wgpu::Device,
863 queue: &wgpu::Queue,
864 item: &crate::renderer::RibbonItem,
865 wireframe: bool,
866 ) -> StreamtubeGpuData {
867 let has_colour_attribute = !item.colour_attribute.is_empty();
871
872 let (use_vertex_colour, lut_rgba): (u32, Option<[[u8; 4]; 256]>) = if has_colour_attribute {
874 (1, None)
875 } else if !item.scalars.is_empty() {
876 let lut = self
877 .content
878 .builtin_colourmap_ids
879 .and_then(|ids| {
880 let preset_id = item
881 .colourmap_id
882 .unwrap_or(ids[crate::resources::BuiltinColourmap::Viridis as usize]);
883 self.content.colourmaps_cpu.get(preset_id.0).copied()
884 })
885 .unwrap_or([[128u8; 4]; 256]);
886 (1, Some(lut))
887 } else {
888 (0, None)
889 };
890
891 let scalar_min = item
892 .scalar_range
893 .map(|r| r.0)
894 .unwrap_or_else(|| item.scalars.iter().cloned().fold(f32::INFINITY, f32::min));
895 let scalar_max = item.scalar_range.map(|r| r.1).unwrap_or_else(|| {
896 item.scalars
897 .iter()
898 .cloned()
899 .fold(f32::NEG_INFINITY, f32::max)
900 });
901 let scalar_range = (scalar_max - scalar_min).max(f32::EPSILON);
902
903 let scalar_to_colour = |idx: usize| -> [f32; 4] {
904 if has_colour_attribute {
905 return item
906 .colour_attribute
907 .get(idx)
908 .copied()
909 .unwrap_or(item.colour);
910 }
911 if let Some(ref lut) = lut_rgba {
912 let s = *item.scalars.get(idx).unwrap_or(&0.0);
913 let t = ((s - scalar_min) / scalar_range).clamp(0.0, 1.0);
914 let lut_idx = ((t * 255.0).round() as usize).min(255);
915 let c = lut[lut_idx];
916 [
917 c[0] as f32 / 255.0,
918 c[1] as f32 / 255.0,
919 c[2] as f32 / 255.0,
920 c[3] as f32 / 255.0,
921 ]
922 } else {
923 item.colour
924 }
925 };
926
927 let mut verts: Vec<Vertex> = Vec::new();
928 let mut indices: Vec<u32> = Vec::new();
929
930 let positions = &item.positions;
931 let mut strip_start = 0usize;
932
933 for &strip_len in &item.strip_lengths {
934 let strip_len = strip_len as usize;
935 let strip_end = (strip_start + strip_len).min(positions.len());
936 let pts: Vec<glam::Vec3> = positions[strip_start..strip_end]
937 .iter()
938 .map(|&p| glam::Vec3::from(p))
939 .collect();
940 let pts_start = strip_start;
941 strip_start += strip_len;
942
943 if pts.len() < 2 {
944 continue;
945 }
946
947 let t0 = (pts[1] - pts[0]).normalize_or_zero();
949 if t0.length_squared() < 1e-10 {
950 continue;
951 }
952 let ref_v = if t0.x.abs() < 0.9 {
953 glam::Vec3::X
954 } else {
955 glam::Vec3::Y
956 };
957 let mut u = t0.cross(ref_v).normalize();
958
959 let mut strip_u: Vec<f32> = Vec::with_capacity(pts.len());
962 if item.u_attribute.is_empty() {
963 let mut cum = 0.0_f32;
964 strip_u.push(0.0);
965 for k in 1..pts.len() {
966 cum += (pts[k] - pts[k - 1]).length();
967 strip_u.push(cum);
968 }
969 let total = strip_u.last().copied().unwrap_or(1.0).max(1e-6);
970 for v in &mut strip_u {
971 *v /= total;
972 }
973 } else {
974 for k in 0..pts.len() {
975 strip_u.push(*item.u_attribute.get(pts_start + k).unwrap_or(&0.0));
976 }
977 }
978
979 let base = verts.len() as u32;
980
981 for (k, &pt) in pts.iter().enumerate() {
982 let tangent = if k + 1 < pts.len() {
983 (pts[k + 1] - pt).normalize_or_zero()
984 } else {
985 (pt - pts[k - 1]).normalize_or_zero()
986 };
987
988 if k > 0 {
990 let t_prev = (pts[k] - pts[k - 1]).normalize_or_zero();
991 let axis = t_prev.cross(tangent);
992 let sin_a = axis.length().min(1.0);
993 if sin_a > 1e-6 {
994 let cos_a = t_prev.dot(tangent).clamp(-1.0, 1.0);
995 let ax = axis / sin_a;
996 u = u * cos_a + ax.cross(u) * sin_a + ax * ax.dot(u) * (1.0 - cos_a);
997 u = u.normalize_or_zero();
998 }
999 }
1000
1001 let mut lateral = u;
1004 if let Some(ref twist) = item.twist_attribute {
1005 if let Some(&tv) = twist.get(pts_start + k) {
1006 let tv = glam::Vec3::from(tv);
1007 let proj = tv - tangent * tangent.dot(tv);
1008 if proj.length_squared() > 1e-10 {
1009 lateral = proj.normalize();
1010 }
1011 }
1012 }
1013
1014 let normal = tangent.cross(lateral).normalize_or_zero();
1015 let half_w = item
1016 .width_attribute
1017 .as_ref()
1018 .and_then(|wa| wa.get(pts_start + k).copied())
1019 .unwrap_or(item.width)
1020 * 0.5;
1021 let colour = scalar_to_colour(pts_start + k);
1022
1023 let uval = strip_u[k];
1024 verts.push(Vertex {
1027 position: (pt + lateral * half_w).to_array(),
1028 normal: normal.to_array(),
1029 colour,
1030 uv: [uval, 0.0],
1031 tangent: [1.0, 0.0, 0.0, 1.0],
1032 });
1033 verts.push(Vertex {
1035 position: (pt - lateral * half_w).to_array(),
1036 normal: normal.to_array(),
1037 colour,
1038 uv: [uval, 1.0],
1039 tangent: [1.0, 0.0, 0.0, 1.0],
1040 });
1041
1042 if k > 0 {
1044 let r0 = base + ((k - 1) * 2) as u32;
1045 let r1 = base + (k * 2) as u32;
1046 indices.push(r0);
1048 indices.push(r0 + 1);
1049 indices.push(r1);
1050 indices.push(r0 + 1);
1052 indices.push(r1 + 1);
1053 indices.push(r1);
1054 }
1055 }
1056 }
1057
1058 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1060 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1061
1062 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1063 label: Some("ribbon_vbuf"),
1064 size: vert_bytes.len().max(std::mem::size_of::<Vertex>()) as u64,
1065 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1066 mapped_at_creation: false,
1067 });
1068 if !vert_bytes.is_empty() {
1069 queue.write_buffer(&vertex_buffer, 0, vert_bytes);
1070 }
1071
1072 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1073 label: Some("ribbon_ibuf"),
1074 size: idx_bytes.len().max(12) as u64,
1075 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1076 mapped_at_creation: false,
1077 });
1078 if !idx_bytes.is_empty() {
1079 queue.write_buffer(&index_buffer, 0, idx_bytes);
1080 }
1081
1082 let index_count = indices.len() as u32;
1083
1084 let edge_indices = crate::resources::mesh::geometry::generate_edge_indices(&indices);
1085 let edge_bytes: &[u8] = bytemuck::cast_slice(&edge_indices);
1086 let edge_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1087 label: Some("ribbon_edge_ibuf"),
1088 size: edge_bytes.len().max(8) as u64,
1089 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1090 mapped_at_creation: false,
1091 });
1092 if !edge_bytes.is_empty() {
1093 queue.write_buffer(&edge_index_buffer, 0, edge_bytes);
1094 }
1095 let edge_index_count = edge_indices.len() as u32;
1096
1097 #[repr(C)]
1098 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
1099 struct RibbonUniform {
1100 model: [[f32; 4]; 4],
1101 colour: [f32; 4],
1102 radius: f32,
1103 use_vertex_colour: u32,
1104 unlit: u32,
1105 opacity: f32,
1106 wireframe: u32,
1107 has_texture: u32,
1108 _pad: [f32; 2],
1109 }
1110 let (texture_view, has_texture): (&wgpu::TextureView, u32) =
1111 if let Some(id) = item.texture_id {
1112 if let Some(tex) = self.content.textures.get(id) {
1113 (&tex.view, 1)
1114 } else {
1115 (&self.content.fallback_lut_view, 0)
1116 }
1117 } else {
1118 (&self.content.fallback_lut_view, 0)
1119 };
1120 let uniform_data = RibbonUniform {
1121 model: item.model,
1122 colour: item.colour,
1123 radius: item.width * 0.5,
1124 use_vertex_colour,
1125 unlit: item.settings.unlit as u32,
1126 opacity: item.settings.opacity,
1127 wireframe: wireframe as u32,
1128 has_texture,
1129 _pad: [0.0; 2],
1130 };
1131 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
1132 label: Some("ribbon_uniform_buf"),
1133 size: std::mem::size_of::<RibbonUniform>() as u64,
1134 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1135 mapped_at_creation: false,
1136 });
1137 queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
1138
1139 let bgl = self
1140 .ribbon
1141 .bgl
1142 .as_ref()
1143 .expect("ensure_streamtube_pipeline not called");
1144 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1145 label: Some("ribbon_uniform_bg"),
1146 layout: bgl,
1147 entries: &[
1148 wgpu::BindGroupEntry {
1149 binding: 0,
1150 resource: uniform_buf.as_entire_binding(),
1151 },
1152 wgpu::BindGroupEntry {
1153 binding: 1,
1154 resource: wgpu::BindingResource::TextureView(texture_view),
1155 },
1156 wgpu::BindGroupEntry {
1157 binding: 2,
1158 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
1159 },
1160 ],
1161 });
1162
1163 StreamtubeGpuData {
1164 vertex_buffer,
1165 index_buffer,
1166 index_count,
1167 edge_index_buffer,
1168 edge_index_count,
1169 wireframe,
1170 uniform_bind_group,
1171 blend: item.blend,
1172 _uniform_buf: uniform_buf,
1173 }
1174 }
1175
1176 pub fn upload_ribbon(
1178 &mut self,
1179 device: &wgpu::Device,
1180 queue: &wgpu::Queue,
1181 item: &crate::renderer::RibbonItem,
1182 ) -> crate::resources::RibbonId {
1183 self.ensure_streamtube_pipeline(device);
1184 let gpu = self.upload_ribbon_per_frame(device, queue, item, false);
1185 self.content.ribbon_store.insert(gpu)
1186 }
1187
1188 pub fn drop_ribbon(&mut self, id: crate::resources::RibbonId) -> bool {
1190 self.content.ribbon_store.remove(id)
1191 }
1192
1193 pub fn replace_ribbon(
1195 &mut self,
1196 device: &wgpu::Device,
1197 queue: &wgpu::Queue,
1198 id: crate::resources::RibbonId,
1199 item: &crate::renderer::RibbonItem,
1200 ) -> bool {
1201 if !self.content.ribbon_store.contains(id) {
1202 return false;
1203 }
1204 self.ensure_streamtube_pipeline(device);
1205 let gpu = self.upload_ribbon_per_frame(device, queue, item, false);
1206 self.content.ribbon_store.replace(id, gpu)
1207 }
1208
1209 pub fn begin_upload_streamtube(
1211 &mut self,
1212 device: &wgpu::Device,
1213 queue: &wgpu::Queue,
1214 item: crate::renderer::StreamtubeItem,
1215 ) -> crate::resources::JobId {
1216 let slot = crate::resources::ResultSlot::<crate::resources::StreamtubeId>::new();
1217 let slot_for_apply = slot.clone();
1218 let device_for_apply = device.clone();
1219 let queue_for_apply = queue.clone();
1220 let id = {
1221 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
1222 runner.submit_cpu(move |progress| {
1223 progress.set(0.9);
1224 Ok(crate::resources::upload_jobs::JobProduct::with_apply(
1225 Box::new(move |resources: &mut DeviceResources| {
1226 let sid =
1227 resources.upload_streamtube(&device_for_apply, &queue_for_apply, &item);
1228 slot_for_apply.set(sid);
1229 }),
1230 ))
1231 })
1232 };
1233 self.job_results
1234 .streamtube
1235 .lock()
1236 .expect("streamtube result map poisoned")
1237 .insert(id, slot);
1238 id
1239 }
1240
1241 pub fn upload_result_streamtube(
1244 &mut self,
1245 id: crate::resources::JobId,
1246 ) -> crate::error::ViewportResult<crate::resources::StreamtubeId> {
1247 let mut map = self
1248 .job_results
1249 .streamtube
1250 .lock()
1251 .expect("streamtube result map poisoned");
1252 let slot = match map.get(&id) {
1253 Some(s) => s.clone(),
1254 None => {
1255 return Err(crate::error::ViewportError::JobResultMissing {
1256 reason: "unknown id or wrong upload type",
1257 });
1258 }
1259 };
1260 match slot.take() {
1261 Some(sid) => {
1262 map.remove(&id);
1263 Ok(sid)
1264 }
1265 None => Err(crate::error::ViewportError::JobNotReady),
1266 }
1267 }
1268
1269 pub fn begin_upload_tube(
1271 &mut self,
1272 device: &wgpu::Device,
1273 queue: &wgpu::Queue,
1274 item: crate::renderer::TubeItem,
1275 ) -> crate::resources::JobId {
1276 let slot = crate::resources::ResultSlot::<crate::resources::TubeId>::new();
1277 let slot_for_apply = slot.clone();
1278 let device_for_apply = device.clone();
1279 let queue_for_apply = queue.clone();
1280 let id = {
1281 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
1282 runner.submit_cpu(move |progress| {
1283 progress.set(0.9);
1284 Ok(crate::resources::upload_jobs::JobProduct::with_apply(
1285 Box::new(move |resources: &mut DeviceResources| {
1286 let tid = resources.upload_tube(&device_for_apply, &queue_for_apply, &item);
1287 slot_for_apply.set(tid);
1288 }),
1289 ))
1290 })
1291 };
1292 self.job_results
1293 .tube
1294 .lock()
1295 .expect("tube result map poisoned")
1296 .insert(id, slot);
1297 id
1298 }
1299
1300 pub fn upload_result_tube(
1303 &mut self,
1304 id: crate::resources::JobId,
1305 ) -> crate::error::ViewportResult<crate::resources::TubeId> {
1306 let mut map = self
1307 .job_results
1308 .tube
1309 .lock()
1310 .expect("tube result map poisoned");
1311 let slot = match map.get(&id) {
1312 Some(s) => s.clone(),
1313 None => {
1314 return Err(crate::error::ViewportError::JobResultMissing {
1315 reason: "unknown id or wrong upload type",
1316 });
1317 }
1318 };
1319 match slot.take() {
1320 Some(tid) => {
1321 map.remove(&id);
1322 Ok(tid)
1323 }
1324 None => Err(crate::error::ViewportError::JobNotReady),
1325 }
1326 }
1327
1328 pub fn begin_upload_ribbon(
1330 &mut self,
1331 device: &wgpu::Device,
1332 queue: &wgpu::Queue,
1333 item: crate::renderer::RibbonItem,
1334 ) -> crate::resources::JobId {
1335 let slot = crate::resources::ResultSlot::<crate::resources::RibbonId>::new();
1336 let slot_for_apply = slot.clone();
1337 let device_for_apply = device.clone();
1338 let queue_for_apply = queue.clone();
1339 let id = {
1340 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
1341 runner.submit_cpu(move |progress| {
1342 progress.set(0.9);
1343 Ok(crate::resources::upload_jobs::JobProduct::with_apply(
1344 Box::new(move |resources: &mut DeviceResources| {
1345 let rid =
1346 resources.upload_ribbon(&device_for_apply, &queue_for_apply, &item);
1347 slot_for_apply.set(rid);
1348 }),
1349 ))
1350 })
1351 };
1352 self.job_results
1353 .ribbon
1354 .lock()
1355 .expect("ribbon result map poisoned")
1356 .insert(id, slot);
1357 id
1358 }
1359
1360 pub fn upload_result_ribbon(
1363 &mut self,
1364 id: crate::resources::JobId,
1365 ) -> crate::error::ViewportResult<crate::resources::RibbonId> {
1366 let mut map = self
1367 .job_results
1368 .ribbon
1369 .lock()
1370 .expect("ribbon result map poisoned");
1371 let slot = match map.get(&id) {
1372 Some(s) => s.clone(),
1373 None => {
1374 return Err(crate::error::ViewportError::JobResultMissing {
1375 reason: "unknown id or wrong upload type",
1376 });
1377 }
1378 };
1379 match slot.take() {
1380 Some(rid) => {
1381 map.remove(&id);
1382 Ok(rid)
1383 }
1384 None => Err(crate::error::ViewportError::JobNotReady),
1385 }
1386 }
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391 use crate::DeviceResources;
1392 use crate::renderer::{RibbonItem, StreamtubeItem, TubeItem};
1393 use crate::resources::UploadStatus;
1394
1395 fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
1396 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
1397 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1398 power_preference: wgpu::PowerPreference::LowPower,
1399 compatible_surface: None,
1400 force_fallback_adapter: false,
1401 }))
1402 .ok()?;
1403 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
1404 }
1405
1406 fn sample_streamtube() -> StreamtubeItem {
1407 StreamtubeItem {
1408 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]],
1409 strip_lengths: vec![3],
1410 radius: 0.1,
1411 ..Default::default()
1412 }
1413 }
1414
1415 fn sample_tube() -> TubeItem {
1416 TubeItem {
1417 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]],
1418 strip_lengths: vec![3],
1419 radius: 0.1,
1420 ..Default::default()
1421 }
1422 }
1423
1424 fn sample_ribbon() -> RibbonItem {
1425 RibbonItem {
1426 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]],
1427 strip_lengths: vec![3],
1428 width: 0.2,
1429 ..Default::default()
1430 }
1431 }
1432
1433 fn drive_until_ready(
1434 resources: &mut DeviceResources,
1435 device: &wgpu::Device,
1436 queue: &wgpu::Queue,
1437 id: crate::resources::JobId,
1438 label: &'static str,
1439 ) {
1440 for _ in 0..200 {
1441 resources.process_uploads(device, queue);
1442 match resources.upload_status(id) {
1443 UploadStatus::Ready => return,
1444 UploadStatus::Failed(e) => panic!("{label} upload failed: {e:?}"),
1445 UploadStatus::Pending { .. } => {
1446 std::thread::sleep(std::time::Duration::from_millis(5));
1447 }
1448 UploadStatus::Unknown => panic!("{label} job id disappeared"),
1449 }
1450 }
1451 panic!("{label} upload did not complete in time");
1452 }
1453
1454 const IDENTITY: [[f32; 4]; 4] = [
1455 [1.0, 0.0, 0.0, 0.0],
1456 [0.0, 1.0, 0.0, 0.0],
1457 [0.0, 0.0, 1.0, 0.0],
1458 [0.0, 0.0, 0.0, 1.0],
1459 ];
1460
1461 #[test]
1462 fn streamtube_default_model_is_identity() {
1463 assert_eq!(StreamtubeItem::default().model, IDENTITY);
1464 }
1465
1466 #[test]
1467 fn tube_default_model_is_identity() {
1468 assert_eq!(TubeItem::default().model, IDENTITY);
1469 }
1470
1471 #[test]
1472 fn ribbon_default_model_is_identity() {
1473 assert_eq!(RibbonItem::default().model, IDENTITY);
1474 }
1475
1476 #[test]
1477 fn streamtube_carries_non_identity_model() {
1478 let mut m = IDENTITY;
1479 m[3] = [1.0, 2.0, 3.0, 1.0];
1480 let item = StreamtubeItem {
1481 model: m,
1482 ..StreamtubeItem::default()
1483 };
1484 assert_eq!(item.model[3], [1.0, 2.0, 3.0, 1.0]);
1485 }
1486
1487 #[test]
1488 fn tube_carries_non_identity_model() {
1489 let mut m = IDENTITY;
1490 m[3] = [1.0, 2.0, 3.0, 1.0];
1491 let item = TubeItem {
1492 model: m,
1493 ..TubeItem::default()
1494 };
1495 assert_eq!(item.model[3], [1.0, 2.0, 3.0, 1.0]);
1496 }
1497
1498 #[test]
1499 fn ribbon_carries_non_identity_model() {
1500 let mut m = IDENTITY;
1501 m[3] = [1.0, 2.0, 3.0, 1.0];
1502 let item = RibbonItem {
1503 model: m,
1504 ..RibbonItem::default()
1505 };
1506 assert_eq!(item.model[3], [1.0, 2.0, 3.0, 1.0]);
1507 }
1508
1509 #[test]
1510 fn upload_streamtube_returns_valid_handle() {
1511 let Some((device, queue)) = try_make_device() else {
1512 eprintln!("skipping: no wgpu adapter available");
1513 return;
1514 };
1515 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1516 let id = resources.upload_streamtube(&device, &queue, &sample_streamtube());
1517 assert!(resources.content.streamtube_store.contains(id));
1518 assert!(resources.drop_streamtube(id));
1519 assert!(!resources.content.streamtube_store.contains(id));
1520 }
1521
1522 #[test]
1523 fn upload_tube_returns_valid_handle() {
1524 let Some((device, queue)) = try_make_device() else {
1525 eprintln!("skipping: no wgpu adapter available");
1526 return;
1527 };
1528 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1529 let start = resources.resident_bytes().scivis_bytes;
1530 let id = resources.upload_tube(&device, &queue, &sample_tube());
1531 assert!(resources.content.tube_store.contains(id));
1532 let after_upload = resources.resident_bytes().scivis_bytes;
1533 assert!(
1534 after_upload > start,
1535 "uploading a tube must increase resident scivis bytes"
1536 );
1537 assert!(resources.drop_tube(id));
1538 assert_eq!(
1539 resources.resident_bytes().scivis_bytes,
1540 start,
1541 "dropping the tube must return resident scivis bytes to the start"
1542 );
1543 }
1544
1545 #[test]
1546 fn upload_ribbon_returns_valid_handle() {
1547 let Some((device, queue)) = try_make_device() else {
1548 eprintln!("skipping: no wgpu adapter available");
1549 return;
1550 };
1551 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1552 let id = resources.upload_ribbon(&device, &queue, &sample_ribbon());
1553 assert!(resources.content.ribbon_store.contains(id));
1554 assert!(resources.drop_ribbon(id));
1555 }
1556
1557 #[test]
1558 fn begin_upload_streamtube_drains_to_handle() {
1559 let Some((device, queue)) = try_make_device() else {
1560 eprintln!("skipping: no wgpu adapter available");
1561 return;
1562 };
1563 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1564 let job = resources.begin_upload_streamtube(&device, &queue, sample_streamtube());
1565 drive_until_ready(&mut resources, &device, &queue, job, "streamtube");
1566 let id = resources.upload_result_streamtube(job).expect("ready");
1567 assert!(resources.content.streamtube_store.contains(id));
1568 let err = resources.upload_result_streamtube(job).unwrap_err();
1569 assert!(matches!(
1570 err,
1571 crate::error::ViewportError::JobResultMissing { .. }
1572 ));
1573 }
1574
1575 #[test]
1576 fn begin_upload_tube_drains_to_handle() {
1577 let Some((device, queue)) = try_make_device() else {
1578 eprintln!("skipping: no wgpu adapter available");
1579 return;
1580 };
1581 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1582 let job = resources.begin_upload_tube(&device, &queue, sample_tube());
1583 drive_until_ready(&mut resources, &device, &queue, job, "tube");
1584 let id = resources.upload_result_tube(job).expect("ready");
1585 assert!(resources.content.tube_store.contains(id));
1586 }
1587
1588 #[test]
1589 fn begin_upload_ribbon_drains_to_handle() {
1590 let Some((device, queue)) = try_make_device() else {
1591 eprintln!("skipping: no wgpu adapter available");
1592 return;
1593 };
1594 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1595 let job = resources.begin_upload_ribbon(&device, &queue, sample_ribbon());
1596 drive_until_ready(&mut resources, &device, &queue, job, "ribbon");
1597 let id = resources.upload_result_ribbon(job).expect("ready");
1598 assert!(resources.content.ribbon_store.contains(id));
1599 }
1600}
1601
1602#[derive(Clone)]
1607pub struct StreamtubeGpuData {
1608 pub(crate) vertex_buffer: wgpu::Buffer,
1610 pub(crate) index_buffer: wgpu::Buffer,
1612 pub(crate) index_count: u32,
1614 pub(crate) edge_index_buffer: wgpu::Buffer,
1616 pub(crate) edge_index_count: u32,
1618 pub(crate) wireframe: bool,
1620 pub(crate) uniform_bind_group: wgpu::BindGroup,
1622 pub(crate) blend: crate::renderer::SpriteBlend,
1625 pub(crate) _uniform_buf: wgpu::Buffer,
1627}