Skip to main content

viewport_lib/resources/scivis/
polyline.rs

1use super::*;
2
3/// Polyline (screen-space thick line) pipelines and their layouts. All lazily
4/// built; the uploaded polyline data lives in a separate flat store.
5#[derive(Default)]
6pub(crate) struct PolylineResources {
7    /// Polyline render pipeline. None until first polyline set is submitted.
8    pub(crate) pipeline: Option<DualPipeline>,
9    /// Clip-exempt polyline pipeline (uses fs_main_no_clip).
10    pub(crate) no_clip_pipeline: Option<DualPipeline>,
11    /// Bind group layout for polyline uniforms (group 1).
12    pub(crate) bgl: Option<wgpu::BindGroupLayout>,
13    /// Thin 1px LineList wireframe polyline pipeline.
14    pub(crate) wireframe_pipeline: Option<DualPipeline>,
15    /// Bind group layout for the wireframe polyline pipeline (group 1).
16    pub(crate) wireframe_bgl: Option<wgpu::BindGroupLayout>,
17    /// Polyline outline mask pipeline (R8Unorm). None until first selected polyline.
18    pub(crate) outline_mask_pipeline: Option<wgpu::RenderPipeline>,
19}
20
21impl DeviceResources {
22    /// Lazily create the polyline render pipeline (instanced TriangleList : screen-space thick lines).
23    ///
24    /// No-op if already created. Called from `prepare()` when `frame.scene.polylines` is non-empty.
25    pub(crate) fn ensure_polyline_pipeline(&mut self, device: &wgpu::Device) {
26        if self.polyline.pipeline.is_some() {
27            return;
28        }
29
30        let pl_bgl = crate::resources::builders::uniform_texture_sampler_bgl(
31            device,
32            "polyline_bgl",
33            wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
34            wgpu::ShaderStages::VERTEX,
35        );
36
37        let shader = crate::resources::builders::wgsl_module(
38            device,
39            "polyline_shader",
40            crate::resources::builders::wgsl_source!("polyline"),
41        );
42
43        let layout = crate::resources::builders::standard_scene_layout(
44            device,
45            "polyline_pipeline_layout",
46            &self.camera_bind_group_layout,
47            &pl_bgl,
48        );
49
50        // Instance buffer layout (112 bytes per segment):
51        //   offset   0: pos_a             vec3  : segment start (world space)
52        //   offset  12: pos_b             vec3  : segment end   (world space)
53        //   offset  24: prev_pos          vec3  : point before pos_a (for miter at A); equals pos_a if strip start
54        //   offset  36: next_pos          vec3  : point after  pos_b (for miter at B); equals pos_b if strip end
55        //   offset  48: scalar_a          f32
56        //   offset  52: scalar_b          f32
57        //   offset  56: has_prev          u32   : 1 = prev_pos is valid (interior join at A), 0 = square cap
58        //   offset  60: has_next          u32   : 1 = next_pos is valid (interior join at B), 0 = square cap
59        //   offset  64: colour_a           vec4  : direct RGBA at segment start
60        //   offset  80: colour_b           vec4  : direct RGBA at segment end
61        //   offset  96: radius_a          f32   : line width in px at A (= line_width when node_radii is empty)
62        //   offset 100: radius_b          f32   : line width in px at B
63        //   offset 104: use_direct_colour  u32   : 1 = use colour_a/b, 0 = use scalar LUT / default
64        //   offset 108: _pad              u32
65        let pl_instance_layout = wgpu::VertexBufferLayout {
66            array_stride: 112,
67            step_mode: wgpu::VertexStepMode::Instance,
68            attributes: &[
69                wgpu::VertexAttribute {
70                    offset: 0,
71                    shader_location: 0,
72                    format: wgpu::VertexFormat::Float32x3,
73                }, // pos_a
74                wgpu::VertexAttribute {
75                    offset: 12,
76                    shader_location: 1,
77                    format: wgpu::VertexFormat::Float32x3,
78                }, // pos_b
79                wgpu::VertexAttribute {
80                    offset: 24,
81                    shader_location: 2,
82                    format: wgpu::VertexFormat::Float32x3,
83                }, // prev_pos
84                wgpu::VertexAttribute {
85                    offset: 36,
86                    shader_location: 3,
87                    format: wgpu::VertexFormat::Float32x3,
88                }, // next_pos
89                wgpu::VertexAttribute {
90                    offset: 48,
91                    shader_location: 4,
92                    format: wgpu::VertexFormat::Float32,
93                }, // scalar_a
94                wgpu::VertexAttribute {
95                    offset: 52,
96                    shader_location: 5,
97                    format: wgpu::VertexFormat::Float32,
98                }, // scalar_b
99                wgpu::VertexAttribute {
100                    offset: 56,
101                    shader_location: 6,
102                    format: wgpu::VertexFormat::Uint32,
103                }, // has_prev
104                wgpu::VertexAttribute {
105                    offset: 60,
106                    shader_location: 7,
107                    format: wgpu::VertexFormat::Uint32,
108                }, // has_next
109                wgpu::VertexAttribute {
110                    offset: 64,
111                    shader_location: 8,
112                    format: wgpu::VertexFormat::Float32x4,
113                }, // colour_a
114                wgpu::VertexAttribute {
115                    offset: 80,
116                    shader_location: 9,
117                    format: wgpu::VertexFormat::Float32x4,
118                }, // colour_b
119                wgpu::VertexAttribute {
120                    offset: 96,
121                    shader_location: 10,
122                    format: wgpu::VertexFormat::Float32,
123                }, // radius_a
124                wgpu::VertexAttribute {
125                    offset: 100,
126                    shader_location: 11,
127                    format: wgpu::VertexFormat::Float32,
128                }, // radius_b
129                wgpu::VertexAttribute {
130                    offset: 104,
131                    shader_location: 12,
132                    format: wgpu::VertexFormat::Uint32,
133                }, // use_direct_colour
134            ],
135        };
136
137        self.polyline.pipeline = Some(crate::resources::builders::build_dual_pipeline(
138            device,
139            &crate::resources::builders::DualPipelineDesc {
140                label: "polyline_pipeline",
141                layout: &layout,
142                shader: &shader,
143                vertex_entry: "vs_main",
144                fragment_entry: "fs_main",
145                vertex_buffers: &[pl_instance_layout.clone()],
146                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
147                topology: wgpu::PrimitiveTopology::TriangleList,
148                cull_mode: None,
149                depth_write: true,
150                depth_compare: wgpu::CompareFunction::LessEqual,
151                sample_count: self.sample_count,
152                ldr_format: self.target_format,
153            },
154        ));
155        self.polyline.bgl = Some(pl_bgl);
156
157        self.ensure_polyline_wireframe_pipeline(device);
158    }
159
160    /// Lazily create the thin wireframe polyline pipeline (LineList, 1px).
161    ///
162    /// Reads segment endpoints from a storage buffer so no vertex buffer is needed.
163    /// Created alongside `ensure_polyline_pipeline`; no-op if already created.
164    pub(crate) fn ensure_polyline_wireframe_pipeline(&mut self, device: &wgpu::Device) {
165        if self.polyline.wireframe_pipeline.is_some() {
166            return;
167        }
168
169        let wf_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
170            label: Some("polyline_wireframe_bgl"),
171            entries: &[wgpu::BindGroupLayoutEntry {
172                binding: 0,
173                visibility: wgpu::ShaderStages::VERTEX,
174                ty: wgpu::BindingType::Buffer {
175                    ty: wgpu::BufferBindingType::Storage { read_only: true },
176                    has_dynamic_offset: false,
177                    min_binding_size: None,
178                },
179                count: None,
180            }],
181        });
182
183        let shader = crate::resources::builders::wgsl_module(
184            device,
185            "polyline_wireframe_shader",
186            crate::resources::builders::wgsl_source!("polyline_wireframe"),
187        );
188
189        let layout = crate::resources::builders::standard_scene_layout(
190            device,
191            "polyline_wireframe_pipeline_layout",
192            &self.camera_bind_group_layout,
193            &wf_bgl,
194        );
195
196        self.polyline.wireframe_bgl = Some(wf_bgl);
197        self.polyline.wireframe_pipeline = Some(crate::resources::builders::build_dual_pipeline(
198            device,
199            &crate::resources::builders::DualPipelineDesc {
200                label: "polyline_wireframe_pipeline",
201                layout: &layout,
202                shader: &shader,
203                vertex_entry: "vs_main",
204                fragment_entry: "fs_main",
205                vertex_buffers: &[],
206                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
207                topology: wgpu::PrimitiveTopology::LineList,
208                cull_mode: None,
209                depth_write: true,
210                depth_compare: wgpu::CompareFunction::LessEqual,
211                sample_count: self.sample_count,
212                ldr_format: self.target_format,
213            },
214        ));
215    }
216
217    /// Upload one [`PolylineItem`] to the GPU and return draw data.
218    ///
219    /// Converts the strip-based point list into a flat segment-instance buffer
220    /// suitable for the screen-space thick-line pipeline with miter joints.
221    ///
222    /// Each consecutive pair of points in a strip becomes one 112-byte instance
223    /// containing miter geometry, scalar colouring, direct RGBA colours, and per-vertex
224    /// radii. See the comment in `ensure_polyline_pipeline` for the full layout.
225    ///
226    /// `viewport_size` is `[width_px, height_px]` and is baked into the per-item
227    /// uniform so the vertex shader can compute correct pixel offsets.
228    pub(crate) fn upload_polyline_per_frame(
229        &mut self,
230        device: &wgpu::Device,
231        queue: &wgpu::Queue,
232        item: &crate::renderer::PolylineItem,
233        viewport_size: [f32; 2],
234    ) -> PolylineGpuData {
235        // Build the segment instance buffer (112 bytes per segment).
236        #[repr(C)]
237        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
238        struct SegInstance {
239            pos_a: [f32; 3],        // offset   0
240            pos_b: [f32; 3],        // offset  12
241            prev_pos: [f32; 3],     // offset  24
242            next_pos: [f32; 3],     // offset  36
243            scalar_a: f32,          // offset  48
244            scalar_b: f32,          // offset  52
245            has_prev: u32,          // offset  56
246            has_next: u32,          // offset  60
247            colour_a: [f32; 4],     // offset  64
248            colour_b: [f32; 4],     // offset  80
249            radius_a: f32,          // offset  96
250            radius_b: f32,          // offset 100
251            use_direct_colour: u32, // offset 104
252            _pad: u32,              // offset 108
253        }
254
255        // Determine which colour/scalar/radius source to use per segment.
256        let use_direct = !item.node_colours.is_empty() || !item.edge_colours.is_empty();
257        let use_edge_scalars = item.scalars.is_empty() && !item.edge_scalars.is_empty();
258        let use_node_radii = !item.node_radii.is_empty();
259
260        let mut instances: Vec<SegInstance> = Vec::new();
261        let positions = &item.positions;
262        let npos = positions.len();
263
264        // Collect strip ranges: (start_idx, end_idx) into `positions`.
265        let strip_ranges: Vec<(usize, usize)> = if item.strip_lengths.is_empty() {
266            vec![(0, npos)]
267        } else {
268            let mut ranges = Vec::with_capacity(item.strip_lengths.len());
269            let mut off = 0usize;
270            for &l in &item.strip_lengths {
271                ranges.push((off, off + l as usize));
272                off += l as usize;
273            }
274            ranges
275        };
276
277        let mut seg_idx_global: usize = 0; // monotonic segment counter across all strips
278
279        for &(strip_start, strip_end) in &strip_ranges {
280            let end = strip_end.min(npos);
281            for i in strip_start..end.saturating_sub(1) {
282                let j = i + 1;
283                let has_prev = i > strip_start;
284                let has_next = j + 1 < end;
285
286                // Scalar: edge_scalars (flat per segment) > per-node scalars > 0
287                let (scalar_a, scalar_b) = if use_edge_scalars {
288                    let s = item
289                        .edge_scalars
290                        .get(seg_idx_global)
291                        .copied()
292                        .unwrap_or(0.0);
293                    (s, s)
294                } else {
295                    (
296                        item.scalars.get(i).copied().unwrap_or(0.0),
297                        item.scalars.get(j).copied().unwrap_or(0.0),
298                    )
299                };
300
301                // Direct colour: node_colours (per-endpoint) > edge_colours (per-segment)
302                let (colour_a, colour_b) = if !item.node_colours.is_empty() {
303                    (
304                        item.node_colours.get(i).copied().unwrap_or([1.0; 4]),
305                        item.node_colours.get(j).copied().unwrap_or([1.0; 4]),
306                    )
307                } else if !item.edge_colours.is_empty() {
308                    let c = item
309                        .edge_colours
310                        .get(seg_idx_global)
311                        .copied()
312                        .unwrap_or([1.0; 4]);
313                    (c, c)
314                } else {
315                    ([1.0; 4], [1.0; 4])
316                };
317
318                // Radius: per-node > global line_width
319                let (radius_a, radius_b) = if use_node_radii {
320                    (
321                        item.node_radii.get(i).copied().unwrap_or(item.line_width),
322                        item.node_radii.get(j).copied().unwrap_or(item.line_width),
323                    )
324                } else {
325                    (item.line_width, item.line_width)
326                };
327
328                instances.push(SegInstance {
329                    pos_a: positions[i],
330                    pos_b: positions[j],
331                    prev_pos: if has_prev {
332                        positions[i - 1]
333                    } else {
334                        positions[i]
335                    },
336                    next_pos: if has_next {
337                        positions[j + 1]
338                    } else {
339                        positions[j]
340                    },
341                    scalar_a,
342                    scalar_b,
343                    has_prev: has_prev as u32,
344                    has_next: has_next as u32,
345                    colour_a,
346                    colour_b,
347                    radius_a,
348                    radius_b,
349                    use_direct_colour: use_direct as u32,
350                    _pad: 0,
351                });
352
353                seg_idx_global += 1;
354            }
355        }
356
357        let seg_count = instances.len() as u32;
358
359        // Allocate instance buffer (min 112 bytes so wgpu doesn't complain on empty).
360        let seg_bytes: &[u8] = bytemuck::cast_slice(&instances);
361        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
362            label: Some("polyline_vertex_buf"),
363            size: seg_bytes.len().max(112) as u64,
364            usage: wgpu::BufferUsages::VERTEX
365                | wgpu::BufferUsages::STORAGE
366                | wgpu::BufferUsages::COPY_DST,
367            mapped_at_creation: false,
368        });
369        if !seg_bytes.is_empty() {
370            queue.write_buffer(&vertex_buffer, 0, seg_bytes);
371        }
372
373        // Determine scalar range for the LUT uniform (node or edge scalars).
374        let scalar_source: &[f32] = if !item.scalars.is_empty() {
375            &item.scalars
376        } else {
377            &item.edge_scalars
378        };
379        let (has_scalar, scalar_min, scalar_max) = if !scalar_source.is_empty() {
380            let (min, max) = item.scalar_range.unwrap_or_else(|| {
381                let mn = scalar_source.iter().cloned().fold(f32::INFINITY, f32::min);
382                let mx = scalar_source
383                    .iter()
384                    .cloned()
385                    .fold(f32::NEG_INFINITY, f32::max);
386                (mn, mx)
387            });
388            (1u32, min, max)
389        } else {
390            (0u32, 0.0f32, 1.0f32)
391        };
392
393        #[repr(C)]
394        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
395        struct PolylineUniform {
396            model: [[f32; 4]; 4],     // offset  0
397            default_colour: [f32; 4], // offset 64
398            line_width: f32,          // offset 80
399            scalar_min: f32,          // offset 84
400            scalar_max: f32,          // offset 88
401            has_scalar: u32,          // offset 92
402            viewport_width: f32,      // offset 96
403            viewport_height: f32,     // offset 100
404            _pad: [f32; 2],           // offset 104 (total 112 bytes)
405        }
406        let uniform_data = PolylineUniform {
407            model: item.model,
408            default_colour: item.default_colour,
409            line_width: item.line_width,
410            scalar_min,
411            scalar_max,
412            has_scalar,
413            viewport_width: viewport_size[0].max(1.0),
414            viewport_height: viewport_size[1].max(1.0),
415            _pad: [0.0; 2],
416        };
417        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
418            label: Some("polyline_uniform_buf"),
419            size: std::mem::size_of::<PolylineUniform>() as u64,
420            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
421            mapped_at_creation: false,
422        });
423        queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
424
425        let lut_view = self
426            .content
427            .builtin_colourmap_ids
428            .and_then(|ids| {
429                let preset_id = item
430                    .colourmap_id
431                    .unwrap_or(ids[crate::resources::BuiltinColourmap::Viridis as usize]);
432                self.content.colourmap_views.get(preset_id.0)
433            })
434            .unwrap_or(&self.content.fallback_lut_view);
435
436        let lut_sampler = &self.lut_sampler;
437
438        let bgl = self
439            .polyline
440            .bgl
441            .as_ref()
442            .expect("ensure_polyline_pipeline not called");
443        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
444            label: Some("polyline_bind_group"),
445            layout: bgl,
446            entries: &[
447                wgpu::BindGroupEntry {
448                    binding: 0,
449                    resource: uniform_buf.as_entire_binding(),
450                },
451                wgpu::BindGroupEntry {
452                    binding: 1,
453                    resource: wgpu::BindingResource::TextureView(lut_view),
454                },
455                wgpu::BindGroupEntry {
456                    binding: 2,
457                    resource: wgpu::BindingResource::Sampler(lut_sampler),
458                },
459            ],
460        });
461
462        let wireframe_bind_group = self.polyline.wireframe_bgl.as_ref().map(|bgl| {
463            device.create_bind_group(&wgpu::BindGroupDescriptor {
464                label: Some("polyline_wireframe_bind_group"),
465                layout: bgl,
466                entries: &[wgpu::BindGroupEntry {
467                    binding: 0,
468                    resource: vertex_buffer.as_entire_binding(),
469                }],
470            })
471        });
472
473        PolylineGpuData {
474            vertex_buffer,
475            segment_count: seg_count,
476            bind_group,
477            _uniform_buf: uniform_buf,
478            skip_clip: false,
479            wireframe: false,
480            wireframe_bind_group,
481        }
482    }
483
484    /// Pre-upload a polyline and return a typed handle.
485    ///
486    /// The returned [`PolylineId`](crate::resources::PolylineId) refers to GPU
487    /// buffers retained by the renderer until [`drop_polyline`] is called.
488    /// Submit a [`PolylineRefItem`](crate::renderer::PolylineRefItem) on
489    /// `SceneFrame::polyline_refs` each frame to draw the polyline at a
490    /// custom model transform without rebuilding its segment buffer.
491    ///
492    /// The viewport size used for screen-space miter calculations is set
493    /// from the most recent ref-item draw of this polyline. Stationary
494    /// callers can rely on it being correct after the first frame.
495    pub fn upload_polyline(
496        &mut self,
497        device: &wgpu::Device,
498        queue: &wgpu::Queue,
499        item: &crate::renderer::PolylineItem,
500    ) -> crate::resources::PolylineId {
501        self.ensure_polyline_pipeline(device);
502        let gpu = self.upload_polyline_per_frame(device, queue, item, [1.0, 1.0]);
503        self.content.polyline_store.insert(gpu)
504    }
505
506    /// Remove a pre-uploaded polyline. Returns `true` if a polyline was
507    /// actually removed, `false` if the id was already invalid.
508    pub fn drop_polyline(&mut self, id: crate::resources::PolylineId) -> bool {
509        self.content.polyline_store.remove(id)
510    }
511
512    /// Replace the geometry of a pre-uploaded polyline, keeping the same
513    /// [`PolylineId`](crate::resources::PolylineId).
514    ///
515    /// Returns `true` if the id was valid and the polyline was replaced,
516    /// `false` if the slot was empty (call [`upload_polyline`](Self::upload_polyline) instead).
517    pub fn replace_polyline(
518        &mut self,
519        device: &wgpu::Device,
520        queue: &wgpu::Queue,
521        id: crate::resources::PolylineId,
522        item: &crate::renderer::PolylineItem,
523    ) -> bool {
524        if !self.content.polyline_store.contains(id) {
525            return false;
526        }
527        self.ensure_polyline_pipeline(device);
528        let gpu = self.upload_polyline_per_frame(device, queue, item, [1.0, 1.0]);
529        self.content.polyline_store.replace(id, gpu)
530    }
531
532    /// Start an asynchronous polyline upload.
533    ///
534    /// Returns a [`JobId`](crate::resources::JobId) immediately. The upload
535    /// runs during the next `process_uploads` call (driven by `prepare_scene`);
536    /// once the status is `Ready`, call
537    /// [`upload_result_polyline`](Self::upload_result_polyline) to take the
538    /// resulting handle.
539    ///
540    /// Ownership of `item` transfers into the worker.
541    pub fn begin_upload_polyline(
542        &mut self,
543        device: &wgpu::Device,
544        queue: &wgpu::Queue,
545        item: crate::renderer::PolylineItem,
546    ) -> crate::resources::JobId {
547        let slot = crate::resources::ResultSlot::<crate::resources::PolylineId>::new();
548        let slot_for_apply = slot.clone();
549        let device_for_apply = device.clone();
550        let queue_for_apply = queue.clone();
551
552        let id = {
553            let mut runner = self.jobs.lock().expect("upload job runner poisoned");
554            runner.submit_cpu(move |progress| {
555                progress.set(0.9);
556                Ok(crate::resources::upload_jobs::JobProduct::with_apply(
557                    Box::new(move |resources: &mut DeviceResources| {
558                        let pid =
559                            resources.upload_polyline(&device_for_apply, &queue_for_apply, &item);
560                        slot_for_apply.set(pid);
561                    }),
562                ))
563            })
564        };
565
566        self.job_results
567            .polyline
568            .lock()
569            .expect("polyline result map poisoned")
570            .insert(id, slot);
571        id
572    }
573
574    /// Take the [`PolylineId`](crate::resources::PolylineId) produced by a
575    /// completed [`begin_upload_polyline`](Self::begin_upload_polyline) job.
576    pub fn upload_result_polyline(
577        &mut self,
578        id: crate::resources::JobId,
579    ) -> crate::error::ViewportResult<crate::resources::PolylineId> {
580        let mut map = self
581            .job_results
582            .polyline
583            .lock()
584            .expect("polyline result map poisoned");
585        let slot = match map.get(&id) {
586            Some(s) => s.clone(),
587            None => {
588                return Err(crate::error::ViewportError::JobResultMissing {
589                    reason: "unknown id or wrong upload type",
590                });
591            }
592        };
593        match slot.take() {
594            Some(pid) => {
595                map.remove(&id);
596                Ok(pid)
597            }
598            None => Err(crate::error::ViewportError::JobNotReady),
599        }
600    }
601
602    /// Lazily create the clip-exempt polyline pipeline.
603    ///
604    /// Identical to the regular polyline pipeline but uses `fs_main_no_clip` so
605    /// fragments are never discarded by clip planes or clip volumes. Used for
606    /// clip object wireframe overlays which must always be fully visible.
607    pub(crate) fn ensure_polyline_no_clip_pipeline(&mut self, device: &wgpu::Device) {
608        if self.polyline.no_clip_pipeline.is_some() {
609            return;
610        }
611        // The regular pipeline (and its BGL) must exist first.
612        self.ensure_polyline_pipeline(device);
613
614        let shader = crate::resources::builders::wgsl_module(
615            device,
616            "polyline_no_clip_shader",
617            crate::resources::builders::wgsl_source!("polyline"),
618        );
619
620        let pl_bgl = self
621            .polyline
622            .bgl
623            .as_ref()
624            .expect("polyline_bgl must exist after ensure_polyline_pipeline");
625        let layout = crate::resources::builders::standard_scene_layout(
626            device,
627            "polyline_no_clip_pipeline_layout",
628            &self.camera_bind_group_layout,
629            pl_bgl,
630        );
631
632        // Vertex buffer layout is identical to the regular polyline pipeline (112 bytes/segment).
633        let pl_instance_layout = wgpu::VertexBufferLayout {
634            array_stride: 112,
635            step_mode: wgpu::VertexStepMode::Instance,
636            attributes: &[
637                wgpu::VertexAttribute {
638                    offset: 0,
639                    shader_location: 0,
640                    format: wgpu::VertexFormat::Float32x3,
641                },
642                wgpu::VertexAttribute {
643                    offset: 12,
644                    shader_location: 1,
645                    format: wgpu::VertexFormat::Float32x3,
646                },
647                wgpu::VertexAttribute {
648                    offset: 24,
649                    shader_location: 2,
650                    format: wgpu::VertexFormat::Float32x3,
651                },
652                wgpu::VertexAttribute {
653                    offset: 36,
654                    shader_location: 3,
655                    format: wgpu::VertexFormat::Float32x3,
656                },
657                wgpu::VertexAttribute {
658                    offset: 48,
659                    shader_location: 4,
660                    format: wgpu::VertexFormat::Float32,
661                },
662                wgpu::VertexAttribute {
663                    offset: 52,
664                    shader_location: 5,
665                    format: wgpu::VertexFormat::Float32,
666                },
667                wgpu::VertexAttribute {
668                    offset: 56,
669                    shader_location: 6,
670                    format: wgpu::VertexFormat::Uint32,
671                },
672                wgpu::VertexAttribute {
673                    offset: 60,
674                    shader_location: 7,
675                    format: wgpu::VertexFormat::Uint32,
676                },
677                wgpu::VertexAttribute {
678                    offset: 64,
679                    shader_location: 8,
680                    format: wgpu::VertexFormat::Float32x4,
681                },
682                wgpu::VertexAttribute {
683                    offset: 80,
684                    shader_location: 9,
685                    format: wgpu::VertexFormat::Float32x4,
686                },
687                wgpu::VertexAttribute {
688                    offset: 96,
689                    shader_location: 10,
690                    format: wgpu::VertexFormat::Float32,
691                },
692                wgpu::VertexAttribute {
693                    offset: 100,
694                    shader_location: 11,
695                    format: wgpu::VertexFormat::Float32,
696                },
697                wgpu::VertexAttribute {
698                    offset: 104,
699                    shader_location: 12,
700                    format: wgpu::VertexFormat::Uint32,
701                },
702            ],
703        };
704
705        self.polyline.no_clip_pipeline = Some(crate::resources::builders::build_dual_pipeline(
706            device,
707            &crate::resources::builders::DualPipelineDesc {
708                label: "polyline_no_clip_pipeline",
709                layout: &layout,
710                shader: &shader,
711                vertex_entry: "vs_main",
712                fragment_entry: "fs_main_no_clip",
713                vertex_buffers: &[pl_instance_layout.clone()],
714                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
715                topology: wgpu::PrimitiveTopology::TriangleList,
716                cull_mode: None,
717                depth_write: true,
718                depth_compare: wgpu::CompareFunction::LessEqual,
719                sample_count: self.sample_count,
720                ldr_format: self.target_format,
721            },
722        ));
723    }
724
725    /// Lazily create the polyline outline mask pipeline.
726    ///
727    /// Renders polyline segments into the R8 mask texture using the same
728    /// screen-space quad expansion as the regular pipeline, but outputs white
729    /// and skips clip plane / colour logic.
730    pub(crate) fn ensure_polyline_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
731        if self.polyline.outline_mask_pipeline.is_some() {
732            return;
733        }
734        self.ensure_polyline_pipeline(device);
735
736        let pl_bgl = self
737            .polyline
738            .bgl
739            .as_ref()
740            .expect("polyline_bgl must exist after ensure_polyline_pipeline");
741
742        let layout = crate::resources::builders::standard_scene_layout(
743            device,
744            "polyline_outline_mask_pipeline_layout",
745            &self.camera_bind_group_layout,
746            pl_bgl,
747        );
748
749        let shader = crate::resources::builders::wgsl_module(
750            device,
751            "polyline_outline_mask_shader",
752            crate::resources::builders::wgsl_source!("polyline_outline_mask"),
753        );
754
755        let pl_instance_layout = wgpu::VertexBufferLayout {
756            array_stride: 112,
757            step_mode: wgpu::VertexStepMode::Instance,
758            attributes: &[
759                wgpu::VertexAttribute {
760                    offset: 0,
761                    shader_location: 0,
762                    format: wgpu::VertexFormat::Float32x3,
763                },
764                wgpu::VertexAttribute {
765                    offset: 12,
766                    shader_location: 1,
767                    format: wgpu::VertexFormat::Float32x3,
768                },
769                wgpu::VertexAttribute {
770                    offset: 24,
771                    shader_location: 2,
772                    format: wgpu::VertexFormat::Float32x3,
773                },
774                wgpu::VertexAttribute {
775                    offset: 36,
776                    shader_location: 3,
777                    format: wgpu::VertexFormat::Float32x3,
778                },
779                wgpu::VertexAttribute {
780                    offset: 48,
781                    shader_location: 4,
782                    format: wgpu::VertexFormat::Float32,
783                },
784                wgpu::VertexAttribute {
785                    offset: 52,
786                    shader_location: 5,
787                    format: wgpu::VertexFormat::Float32,
788                },
789                wgpu::VertexAttribute {
790                    offset: 56,
791                    shader_location: 6,
792                    format: wgpu::VertexFormat::Uint32,
793                },
794                wgpu::VertexAttribute {
795                    offset: 60,
796                    shader_location: 7,
797                    format: wgpu::VertexFormat::Uint32,
798                },
799                wgpu::VertexAttribute {
800                    offset: 64,
801                    shader_location: 8,
802                    format: wgpu::VertexFormat::Float32x4,
803                },
804                wgpu::VertexAttribute {
805                    offset: 80,
806                    shader_location: 9,
807                    format: wgpu::VertexFormat::Float32x4,
808                },
809                wgpu::VertexAttribute {
810                    offset: 96,
811                    shader_location: 10,
812                    format: wgpu::VertexFormat::Float32,
813                },
814                wgpu::VertexAttribute {
815                    offset: 100,
816                    shader_location: 11,
817                    format: wgpu::VertexFormat::Float32,
818                },
819                wgpu::VertexAttribute {
820                    offset: 104,
821                    shader_location: 12,
822                    format: wgpu::VertexFormat::Uint32,
823                },
824            ],
825        };
826
827        self.polyline.outline_mask_pipeline =
828            Some(crate::resources::builders::build_outline_mask_pipeline(
829                device,
830                "polyline_outline_mask_pipeline",
831                &layout,
832                &shader,
833                wgpu::TextureFormat::R8Unorm,
834                &[pl_instance_layout],
835                None,
836                true,
837                wgpu::CompareFunction::LessEqual,
838            ));
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use crate::DeviceResources;
845    use crate::renderer::PolylineItem;
846    use crate::resources::UploadStatus;
847
848    fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
849        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
850        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
851            power_preference: wgpu::PowerPreference::LowPower,
852            compatible_surface: None,
853            force_fallback_adapter: false,
854        }))
855        .ok()?;
856        pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
857    }
858
859    fn sample_polyline() -> PolylineItem {
860        PolylineItem {
861            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
862            strip_lengths: vec![3],
863            ..Default::default()
864        }
865    }
866
867    #[test]
868    fn default_model_is_identity() {
869        let item = PolylineItem::default();
870        let expected = [
871            [1.0, 0.0, 0.0, 0.0],
872            [0.0, 1.0, 0.0, 0.0],
873            [0.0, 0.0, 1.0, 0.0],
874            [0.0, 0.0, 0.0, 1.0],
875        ];
876        assert_eq!(item.model, expected);
877    }
878
879    #[test]
880    fn upload_polyline_returns_valid_handle() {
881        let Some((device, queue)) = try_make_device() else {
882            eprintln!("skipping: no wgpu adapter available");
883            return;
884        };
885        let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
886        let id = resources.upload_polyline(&device, &queue, &sample_polyline());
887        assert!(resources.content.polyline_store.contains(id));
888        // drop + reupload cycles the slot.
889        assert!(resources.drop_polyline(id));
890        assert!(!resources.content.polyline_store.contains(id));
891    }
892
893    #[test]
894    fn stale_polyline_handle_does_not_alias_after_slot_reuse() {
895        // The curve stores share one macro, so this covers the whole family
896        // (tube, ribbon, glyph set, sprite set, and the rest).
897        let Some((device, queue)) = try_make_device() else {
898            eprintln!("skipping: no wgpu adapter available");
899            return;
900        };
901        let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
902
903        let id1 = resources.upload_polyline(&device, &queue, &sample_polyline());
904        assert!(resources.content.polyline_store.get(id1).is_some());
905        assert!(resources.drop_polyline(id1));
906        assert!(
907            resources.content.polyline_store.get(id1).is_none(),
908            "a dropped handle must not resolve"
909        );
910
911        let id2 = resources.upload_polyline(&device, &queue, &sample_polyline());
912        assert_eq!(id1.index(), id2.index(), "the freed slot should be reused");
913        assert_ne!(id1, id2, "the reused slot must carry a new generation");
914        assert!(resources.content.polyline_store.get(id2).is_some());
915        assert!(
916            resources.content.polyline_store.get(id1).is_none(),
917            "the stale handle must not alias the polyline now in its slot"
918        );
919    }
920
921    #[test]
922    fn replace_polyline_keeps_handle_stable() {
923        let Some((device, queue)) = try_make_device() else {
924            eprintln!("skipping: no wgpu adapter available");
925            return;
926        };
927        let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
928        let id = resources.upload_polyline(&device, &queue, &sample_polyline());
929        let mut updated = sample_polyline();
930        updated.line_width = 5.0;
931        assert!(resources.replace_polyline(&device, &queue, id, &updated));
932        assert!(resources.content.polyline_store.contains(id));
933    }
934
935    #[test]
936    fn begin_upload_polyline_drains_to_handle() {
937        let Some((device, queue)) = try_make_device() else {
938            eprintln!("skipping: no wgpu adapter available");
939            return;
940        };
941        let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
942        let job = resources.begin_upload_polyline(&device, &queue, sample_polyline());
943
944        // Not ready yet.
945        let err = resources.upload_result_polyline(job).unwrap_err();
946        assert!(matches!(err, crate::error::ViewportError::JobNotReady));
947
948        for _ in 0..200 {
949            resources.process_uploads(&device, &queue);
950            match resources.upload_status(job) {
951                UploadStatus::Ready => break,
952                UploadStatus::Failed(e) => panic!("polyline upload failed: {e:?}"),
953                UploadStatus::Pending { .. } => {
954                    std::thread::sleep(std::time::Duration::from_millis(5));
955                }
956                UploadStatus::Unknown => panic!("polyline job id disappeared"),
957            }
958        }
959
960        let id = resources.upload_result_polyline(job).expect("ready result");
961        assert!(resources.content.polyline_store.contains(id));
962
963        // Second take of the same id should now report missing.
964        let err = resources.upload_result_polyline(job).unwrap_err();
965        assert!(matches!(
966            err,
967            crate::error::ViewportError::JobResultMissing { .. }
968        ));
969    }
970
971    #[test]
972    fn non_identity_model_is_carried_on_item() {
973        // A translation of (3, 4, 5) in the last column. Round-trips through
974        // the public field so consumers can set it before upload.
975        let m = [
976            [1.0, 0.0, 0.0, 0.0],
977            [0.0, 1.0, 0.0, 0.0],
978            [0.0, 0.0, 1.0, 0.0],
979            [3.0, 4.0, 5.0, 1.0],
980        ];
981        let item = PolylineItem {
982            model: m,
983            ..PolylineItem::default()
984        };
985        assert_eq!(item.model[3], [3.0, 4.0, 5.0, 1.0]);
986    }
987}
988
989/// Per-frame GPU data for one polyline item, created in `prepare()`.
990#[derive(Clone)]
991pub struct PolylineGpuData {
992    /// Instance buffer: `[xa, ya, za, xb, yb, zb, scalar_a, scalar_b]` per segment (32 bytes).
993    pub(crate) vertex_buffer: wgpu::Buffer,
994    /// Number of line segments (instances).  Draw call: `draw(0..6, 0..segment_count)`.
995    pub(crate) segment_count: u32,
996    /// Bind group (group 1): uniform + LUT texture + sampler.
997    pub(crate) bind_group: wgpu::BindGroup,
998    // Keep the uniform buffer alive for the lifetime of this struct.
999    pub(crate) _uniform_buf: wgpu::Buffer,
1000    /// When true, renders with the clip-exempt pipeline (no clip plane or clip volume test).
1001    /// Used for clip object wireframe overlays that must always be fully visible.
1002    pub(crate) skip_clip: bool,
1003    /// When true, render as thin 1px lines using the wireframe pipeline instead of thick billboards.
1004    pub(crate) wireframe: bool,
1005    /// Bind group for the wireframe pipeline (group 1: segment storage buffer).
1006    /// None when the wireframe pipeline has not been created yet.
1007    pub(crate) wireframe_bind_group: Option<wgpu::BindGroup>,
1008}