re_renderer/renderer/
lines.rs

1//! Line renderer for efficient rendering of many line(strips)
2//!
3//!
4//! How it works:
5//! =================
6//!
7//! Each drawn line strip consists of a series of quads and all quads are rendered in a single draw call.
8//! The only data we upload are the user provided positions (the "skeleton" of the line so to speak) and line strip wide configurations.
9//! The quads are oriented and spanned in a vertex shader.
10//!
11//! It is tempting to use instancing and store per-instance (==quad) data in a instance-stepped vertex buffer.
12//! However, at least historically GPUs are notoriously bad at processing instances with a small batch size as
13//! [point](https://www.reddit.com/r/vulkan/comments/le74sr/why_gpu_instancing_is_slow_for_small_meshes/)
14//! [out](https://www.reddit.com/r/vulkan/comments/47kfve/instanced_rendering_performance/)
15//! […](https://www.reddit.com/r/opengl/comments/q7yikr/how_to_draw_several_quads_through_instancing/).
16//!
17//! Instead, we use a single (un-instanced) triangle list draw call and use the vertex id to orient ourselves in the vertex shader
18//! (e.g. the index of the current quad is `vertex_idx / 6` etc.).
19//! Our triangle list topology pretends that there is only a single strip, but in reality we want to render several in one draw call.
20//! So every time a new line strip starts (except on the first strip) we need to discard a quad by collapsing vertices into their predecessors.
21//!
22//! All data we fetch in the vertex shader is uploaded as textures in order to maintain WebGL compatibility.
23//! (at the full webgpu feature level we could use raw buffers instead which are easier to handle and a better match for our access pattern)
24//!
25//! Data is provided in two separate textures, the "position data texture" and the "line strip texture".
26//! The "line strip texture" contains packed information over properties that are global to a single strip (see `gpu_data::LineStripInfo`)
27//! Data in the "position data texture" is laid out a follows (see `gpu_data::PositionRadius`):
28//! ```raw
29//!                   ___________________________________________________________________
30//! position data    | pos, strip_idx | pos, strip_idx | pos, strip_idx | pos, strip_idx | …
31//!                   ___________________________________________________________________
32//! (vertex shader)  |             quad 0              |              quad 2             |
33//!                                    ______________________________________________________________
34//!                                   |               quad 1            |              quad 3        | …
35//! ```
36//!
37//! Why not a triangle *strip* instead if *list*?
38//! -----------------------------------------------
39//!
40//! As long as we're not able to restart the strip (requires indices!), we can't discard a quad in a triangle strip setup.
41//! However, this could be solved with an index buffer which has the ability to restart triangle strips (something we haven't tried yet).
42//!
43//! Another much more tricky issue is handling of line joints:
44//! Let's have a look at a corner between two line positions (line positions marked with `X`)
45//! ```raw
46//! o--------------------------o
47//!                            /
48//! X=================X       /
49//!                  //      /
50//! o---------o     //      /
51//!          /     //      /
52//!         o      X      o
53//! ```
54//! The problem is that the top right corner would move further and further outward as we decrease the angle of the joint.
55//! Instead, we generate overlapping, detached quads and handle line joints as cut-outs in the fragment shader.
56//!
57//! Line start/end caps (arrows/etc.)
58//! -----------------------------------------------
59//! Yet another place where our triangle *strip* comes in handy is that we can take triangles from superfluous quads to form pointy arrows.
60//! Again, we keep all the geometry calculating logic in the vertex shader.
61//!
62//! For all batches, independent whether we use caps or not our topology is as follow:
63//!            _________________________________________________
64//!            \  |                     |\  |                   |\
65//!             \ |  … n strip quads …  | \ | … m strip quads … | \
66//!              \|_____________________|__\|___________________|__\
67//! (start cap triangle only)         (start+end triangle)              (end triangle only)
68//!
69//!
70//! Things we might try in the future
71//! ----------------------------------
72//! * more line properties
73//! * more per-position attributes
74//! * experiment with indexed primitives to lower amount of vertices processed
75//!    * note that this would let us remove the degenerated quads between lines, making the approach cleaner and removing the "restart bit"
76//!
77
78use std::{num::NonZeroU64, ops::Range};
79
80use bitflags::bitflags;
81use enumset::{EnumSet, enum_set};
82use re_tracing::profile_function;
83use smallvec::smallvec;
84
85use crate::{
86    DebugLabel, DepthOffset, DrawableCollector, LineDrawableBuilder, OutlineMaskPreference,
87    PickingLayerObjectId, PickingLayerProcessor,
88    allocator::create_and_fill_uniform_buffer_batch,
89    draw_phases::{DrawPhase, OutlineMaskProcessor},
90    include_shader_module,
91    renderer::{DrawDataDrawable, DrawInstruction, DrawableCollectionViewInfo},
92    view_builder::ViewBuilder,
93    wgpu_resources::{
94        BindGroupDesc, BindGroupEntry, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle,
95        GpuRenderPipelineHandle, GpuRenderPipelinePoolAccessor, PipelineLayoutDesc, PoolError,
96        RenderPipelineDesc,
97    },
98};
99
100use super::{DrawData, DrawError, RenderContext, Renderer};
101
102pub mod gpu_data {
103    // Don't use `wgsl_buffer_types` since none of this data goes into a buffer, so its alignment rules don't apply.
104
105    use crate::{
106        Color32, PickingLayerObjectId, UnalignedColor32, size::SizeHalf, wgpu_buffer_types,
107    };
108
109    use super::LineStripFlags;
110
111    #[repr(C, packed)]
112    #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
113    pub struct LineVertex {
114        pub position: glam::Vec3,
115        // TODO(andreas): If we limit ourselves to 65536 line strip (we do as of writing!), we get 16bit extra storage here.
116        // We probably want to store accumulated line length in there so that we can do stippling in the fragment shader
117        pub strip_index: u32,
118    }
119    // (unlike the fields in a uniform buffer)
120    static_assertions::assert_eq_size!(LineVertex, glam::Vec4);
121
122    impl LineVertex {
123        /// Sentinel vertex used at the start and the end of the line vertex data texture to facilitate caps.
124        pub const SENTINEL: Self = Self {
125            position: glam::vec3(f32::MAX, f32::MAX, f32::MAX),
126            strip_index: u32::MAX,
127        };
128
129        /// Number of sentinel vertices, one at the start and one at the end.
130        pub const NUM_SENTINEL_VERTICES: usize = 2;
131    }
132
133    #[repr(C, packed)]
134    #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
135    pub struct LineStripInfo {
136        /// [`ecolor::Color32`] is `repr(align(4))` so we can't use it in `repr(packed)`.
137        pub color: UnalignedColor32, // alpha unused right now
138        pub stippling: u8,
139        pub flags: LineStripFlags,
140        pub radius: SizeHalf,
141    }
142    static_assertions::assert_eq_size!(LineStripInfo, [u32; 2]);
143
144    impl Default for LineStripInfo {
145        fn default() -> Self {
146            Self {
147                radius: crate::Size::new_ui_points(1.5).into(),
148                color: Color32::WHITE.into(),
149                stippling: 0,
150                flags: LineStripFlags::empty(),
151            }
152        }
153    }
154
155    /// Uniform buffer that changes once per draw data rendering.
156    #[repr(C)]
157    #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
158    pub struct DrawDataUniformBuffer {
159        pub radius_boost_in_ui_points: wgpu_buffer_types::F32RowPadded,
160        pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 1],
161    }
162
163    /// Uniform buffer that changes for every batch of line strips.
164    #[repr(C)]
165    #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
166    pub struct BatchUniformBuffer {
167        pub world_from_obj: wgpu_buffer_types::Mat4,
168        pub outline_mask_ids: wgpu_buffer_types::UVec2,
169        pub picking_object_id: PickingLayerObjectId,
170
171        pub depth_offset: f32,
172        pub triangle_cap_length_factor: f32,
173        pub triangle_cap_width_factor: f32,
174        pub _padding: f32,
175
176        pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 6],
177    }
178}
179
180/// Internal, ready to draw representation of [`LineBatchInfo`]
181#[derive(Clone)]
182struct LineStripBatch {
183    bind_group: GpuBindGroup,
184    vertex_range: Range<u32>,
185    active_phases: EnumSet<DrawPhase>,
186}
187
188/// A line drawing operation. Encompasses several lines, each consisting of a list of positions.
189/// Expected to be recreated every frame.
190#[derive(Clone)]
191pub struct LineDrawData {
192    bind_group_all_lines: Option<GpuBindGroup>,
193    bind_group_all_lines_outline_mask: Option<GpuBindGroup>,
194    batches: Vec<LineStripBatch>,
195}
196
197impl DrawData for LineDrawData {
198    type Renderer = LineRenderer;
199
200    fn collect_drawables(
201        &self,
202        _view_info: &DrawableCollectionViewInfo,
203        collector: &mut DrawableCollector<'_>,
204    ) {
205        // TODO(#1611): transparency, split drawables for some semblence of transparency ordering.
206        // TODO(#1025, #4787): Better handling of 2D objects.
207
208        for (batch_idx, batch) in self.batches.iter().enumerate() {
209            collector.add_drawable(
210                batch.active_phases,
211                DrawDataDrawable {
212                    // TODO(andreas): Don't have distance information yet. For now just always draw lines last since they're quite expensive.
213                    distance_sort_key: f32::MAX,
214                    draw_data_payload: batch_idx as _,
215                },
216            );
217        }
218    }
219}
220
221bitflags! {
222    /// Property flags for a line strip
223    ///
224    /// Needs to be kept in sync with `lines.wgsl`
225    #[repr(C)]
226    #[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
227    pub struct LineStripFlags : u8 {
228        /// Puts a equilateral triangle at the end of the line strip (excludes other end caps).
229        const FLAG_CAP_END_TRIANGLE = 0b0000_0001;
230
231        /// Adds a round cap at the end of a line strip (excludes other end caps).
232        const FLAG_CAP_END_ROUND = 0b0000_0010;
233
234        /// By default, line caps end at the last/first position of the line strip.
235        /// This flag makes end caps extend outwards.
236        const FLAG_CAP_END_EXTEND_OUTWARDS = 0b0000_0100;
237
238        /// Puts a equilateral triangle at the start of the line strip (excludes other start caps).
239        const FLAG_CAP_START_TRIANGLE = 0b0000_1000;
240
241        /// Adds a round cap at the start of a line strip (excludes other start caps).
242        const FLAG_CAP_START_ROUND = 0b0001_0000;
243
244        /// By default, line caps end at the last/first position of the line strip.
245        /// This flag makes end caps extend outwards.
246        const FLAG_CAP_START_EXTEND_OUTWARDS = 0b0010_0000;
247
248        /// Enable color gradient across the line.
249        ///
250        /// TODO(andreas): Could be moved to per batch flags.
251        const FLAG_COLOR_GRADIENT = 0b0100_0000;
252
253        /// Forces spanning the line's quads as-if the camera was orthographic.
254        ///
255        /// This is useful for lines that are on a plane that is parallel to the camera:
256        /// Without this flag, the lines will poke through the camera plane as they orient themselves towards the camera.
257        /// Note that since distances to the camera are computed differently in orthographic mode, this changes how screen space sizes are computed.
258        ///
259        /// TODO(andreas): Could be moved to per batch flags.
260        const FLAG_FORCE_ORTHO_SPANNING = 0b1000_0000;
261
262        /// Combination of flags to extend lines outwards with round caps.
263        const FLAGS_OUTWARD_EXTENDING_ROUND_CAPS =
264            LineStripFlags::FLAG_CAP_START_ROUND.bits() |
265            LineStripFlags::FLAG_CAP_END_ROUND.bits() |
266            LineStripFlags::FLAG_CAP_START_EXTEND_OUTWARDS.bits() |
267            LineStripFlags::FLAG_CAP_END_EXTEND_OUTWARDS.bits();
268    }
269}
270
271/// Data that is valid for a batch of line strips.
272pub struct LineBatchInfo {
273    pub label: DebugLabel,
274
275    /// Transformation applies to line positions
276    ///
277    /// TODO(andreas): We don't apply scaling to the radius yet. Need to pass a scaling factor like this in
278    /// `let scale = Mat3::from(world_from_obj).determinant().abs().cbrt()`
279    pub world_from_obj: glam::Affine3A,
280
281    /// Number of vertices covered by this batch.
282    ///
283    /// The batch will start with the next vertex after the one the previous batch ended with.
284    /// It is expected that this vertex is the first vertex of a new batch.
285    pub line_vertex_count: u32,
286
287    /// Optional outline mask setting for the entire batch.
288    pub overall_outline_mask_ids: OutlineMaskPreference,
289
290    /// Defines an outline mask for an individual vertex ranges (can span several line strips!)
291    ///
292    /// Vertex ranges are *not* relative within the current batch, but relates to the draw data vertex buffer.
293    ///
294    /// Having many of these individual outline masks can be slow as they require each their own uniform buffer & draw call.
295    /// This feature is meant for a limited number of "extra selections"
296    /// If an overall mask is defined as well, the per-vertex-range masks is overwriting the overall mask.
297    pub additional_outline_mask_ids_vertex_ranges: Vec<(Range<u32>, OutlineMaskPreference)>,
298
299    /// Picking object id that applies for the entire batch.
300    pub picking_object_id: PickingLayerObjectId,
301
302    /// Depth offset applied after projection.
303    pub depth_offset: DepthOffset,
304
305    /// Length factor as multiple of a line's radius applied to all triangle caps in this batch.
306    ///
307    /// This controls how far the "pointy end" of the triangle/arrow-head extends.
308    /// (defaults to 4.0)
309    pub triangle_cap_length_factor: f32,
310
311    /// Width factor as multiple of a line's radius applied to all triangle caps in this batch.
312    ///
313    /// This controls how wide the triangle/arrow-head is orthogonal to the line's direction.
314    /// (defaults to 2.0)
315    pub triangle_cap_width_factor: f32,
316}
317
318impl Default for LineBatchInfo {
319    fn default() -> Self {
320        Self {
321            label: "unknown_line_batch".into(),
322            world_from_obj: glam::Affine3A::IDENTITY,
323            line_vertex_count: 0,
324            overall_outline_mask_ids: OutlineMaskPreference::NONE,
325            additional_outline_mask_ids_vertex_ranges: Vec::new(),
326            picking_object_id: PickingLayerObjectId::default(),
327            depth_offset: 0,
328            triangle_cap_length_factor: 4.0,
329            triangle_cap_width_factor: 2.0,
330        }
331    }
332}
333
334#[derive(thiserror::Error, Debug, PartialEq, Eq)]
335pub enum LineDrawDataError {
336    #[error("Line vertex refers to unknown line strip.")]
337    InvalidStripIndex,
338
339    #[error(transparent)]
340    PoolError(#[from] PoolError),
341
342    #[error(transparent)]
343    FailedTransferringDataToGpu(#[from] crate::allocator::CpuWriteGpuReadError),
344
345    #[error(transparent)]
346    DataTextureSourceWriteError(#[from] crate::allocator::DataTextureSourceWriteError),
347}
348
349impl LineDrawData {
350    /// Transforms and uploads line strip data to be consumed by gpu.
351    ///
352    /// Try to bundle all line strips into a single draw data instance whenever possible.
353    /// If you pass zero lines instances, subsequent drawing will do nothing.
354    ///
355    /// If no batches are passed, all lines are assumed to be in a single batch with identity transform.
356    pub fn new(line_builder: LineDrawableBuilder<'_>) -> Result<Self, LineDrawDataError> {
357        let LineDrawableBuilder {
358            ctx,
359            vertices_buffer,
360            batches,
361            strips_buffer,
362            picking_instance_ids_buffer,
363            radius_boost_in_ui_points_for_outlines,
364        } = line_builder;
365
366        let line_renderer = ctx.renderer::<LineRenderer>();
367
368        if strips_buffer.is_empty() || vertices_buffer.is_empty() {
369            return Ok(Self {
370                bind_group_all_lines: None,
371                bind_group_all_lines_outline_mask: None,
372                batches: Vec::new(),
373            });
374        }
375
376        let batches = if batches.is_empty() {
377            vec![LineBatchInfo {
378                label: "LineDrawData::fallback_batch".into(),
379                line_vertex_count: vertices_buffer.len() as _,
380                ..Default::default()
381            }]
382        } else {
383            batches
384        };
385
386        const NUM_SENTINEL_VERTICES: usize = 2;
387
388        let max_texture_dimension_2d = ctx.device.limits().max_texture_dimension_2d;
389        let max_num_texels = max_texture_dimension_2d as usize * max_texture_dimension_2d as usize;
390        let max_num_vertices = max_num_texels - NUM_SENTINEL_VERTICES;
391
392        let position_texture = vertices_buffer.finish(
393            wgpu::TextureFormat::Rgba32Float,
394            "LineDrawData::position_texture",
395        )?;
396        let strip_data_texture = strips_buffer.finish(
397            wgpu::TextureFormat::Rg32Uint,
398            "LineDrawData::strip_data_texture",
399        )?;
400        let picking_instance_id_texture = picking_instance_ids_buffer.finish(
401            wgpu::TextureFormat::Rg32Uint,
402            "LineDrawData::picking_instance_id_texture",
403        )?;
404
405        let draw_data_uniform_buffer_bindings = create_and_fill_uniform_buffer_batch(
406            ctx,
407            "LineDrawData::DrawDataUniformBuffer".into(),
408            [
409                gpu_data::DrawDataUniformBuffer {
410                    radius_boost_in_ui_points: 0.0.into(),
411                    end_padding: Default::default(),
412                },
413                gpu_data::DrawDataUniformBuffer {
414                    radius_boost_in_ui_points: radius_boost_in_ui_points_for_outlines.into(),
415                    end_padding: Default::default(),
416                },
417            ]
418            .into_iter(),
419        );
420        let bind_group_all_lines = ctx.gpu_resources.bind_groups.alloc(
421            &ctx.device,
422            &ctx.gpu_resources,
423            &BindGroupDesc {
424                label: "LineDrawData::bind_group_all_lines".into(),
425                entries: smallvec![
426                    BindGroupEntry::DefaultTextureView(position_texture.handle),
427                    BindGroupEntry::DefaultTextureView(strip_data_texture.handle),
428                    BindGroupEntry::DefaultTextureView(picking_instance_id_texture.handle),
429                    draw_data_uniform_buffer_bindings[0].clone(),
430                ],
431                layout: line_renderer.bind_group_layout_all_lines,
432            },
433        );
434        let bind_group_all_lines_outline_mask = ctx.gpu_resources.bind_groups.alloc(
435            &ctx.device,
436            &ctx.gpu_resources,
437            &BindGroupDesc {
438                label: "LineDrawData::bind_group_all_lines_outline_mask".into(),
439                entries: smallvec![
440                    BindGroupEntry::DefaultTextureView(position_texture.handle),
441                    BindGroupEntry::DefaultTextureView(strip_data_texture.handle),
442                    BindGroupEntry::DefaultTextureView(picking_instance_id_texture.handle),
443                    draw_data_uniform_buffer_bindings[1].clone(),
444                ],
445                layout: line_renderer.bind_group_layout_all_lines,
446            },
447        );
448
449        // Process batches
450        let mut batches_internal = Vec::with_capacity(batches.len());
451        {
452            fn uniforms_from_batch_info(
453                batch_info: &LineBatchInfo,
454                outline_mask_ids: [u8; 2],
455            ) -> gpu_data::BatchUniformBuffer {
456                gpu_data::BatchUniformBuffer {
457                    world_from_obj: batch_info.world_from_obj.into(),
458                    outline_mask_ids: outline_mask_ids.into(),
459                    picking_object_id: batch_info.picking_object_id,
460                    depth_offset: batch_info.depth_offset as f32,
461                    triangle_cap_length_factor: batch_info.triangle_cap_length_factor,
462                    triangle_cap_width_factor: batch_info.triangle_cap_width_factor,
463                    _padding: 0.0,
464                    end_padding: Default::default(),
465                }
466            }
467
468            let uniform_buffer_bindings = create_and_fill_uniform_buffer_batch(
469                ctx,
470                "lines batch uniform buffers".into(),
471                batches.iter().map(|batch_info| {
472                    uniforms_from_batch_info(
473                        batch_info,
474                        batch_info.overall_outline_mask_ids.0.unwrap_or_default(),
475                    )
476                }),
477            );
478
479            // Generate additional "micro batches" for each line vertex range that has a unique outline setting.
480            // This is fairly costly if there's many, but easy and low-overhead if there's only few, which is usually what we expect!
481            let mut uniform_buffer_bindings_mask_only_batches =
482                create_and_fill_uniform_buffer_batch(
483                    ctx,
484                    "lines batch uniform buffers - mask only".into(),
485                    batches
486                        .iter()
487                        .flat_map(|batch_info| {
488                            batch_info
489                                .additional_outline_mask_ids_vertex_ranges
490                                .iter()
491                                .map(|(_, mask)| {
492                                    uniforms_from_batch_info(batch_info, mask.0.unwrap_or_default())
493                                })
494                        })
495                        .collect::<Vec<_>>()
496                        .into_iter(),
497                )
498                .into_iter();
499
500            let mut start_vertex_for_next_batch = 0;
501            for (batch_info, uniform_buffer_binding) in
502                batches.iter().zip(uniform_buffer_bindings.into_iter())
503            {
504                let line_vertex_range_end = (start_vertex_for_next_batch
505                    + batch_info.line_vertex_count)
506                    .min(max_num_vertices as u32);
507                let mut active_phases = enum_set![DrawPhase::Opaque | DrawPhase::PickingLayer];
508                // Does the entire batch participate in the outline mask phase?
509                if batch_info.overall_outline_mask_ids.is_some() {
510                    active_phases.insert(DrawPhase::OutlineMask);
511                }
512
513                batches_internal.push(line_renderer.create_linestrip_batch(
514                    ctx,
515                    batch_info.label.clone(),
516                    uniform_buffer_binding,
517                    start_vertex_for_next_batch..line_vertex_range_end,
518                    active_phases,
519                ));
520
521                for (range, _) in &batch_info.additional_outline_mask_ids_vertex_ranges {
522                    batches_internal.push(line_renderer.create_linestrip_batch(
523                        ctx,
524                        format!("{} strip-only {range:?}", batch_info.label).into(),
525                        uniform_buffer_bindings_mask_only_batches.next().unwrap(),
526                        range.clone(),
527                        enum_set![DrawPhase::OutlineMask],
528                    ));
529                }
530
531                start_vertex_for_next_batch = line_vertex_range_end;
532            }
533        }
534
535        Ok(Self {
536            bind_group_all_lines: Some(bind_group_all_lines),
537            bind_group_all_lines_outline_mask: Some(bind_group_all_lines_outline_mask),
538            batches: batches_internal,
539        })
540    }
541}
542
543pub struct LineRenderer {
544    render_pipeline_color: GpuRenderPipelineHandle,
545    render_pipeline_picking_layer: GpuRenderPipelineHandle,
546    render_pipeline_outline_mask: GpuRenderPipelineHandle,
547    bind_group_layout_all_lines: GpuBindGroupLayoutHandle,
548    bind_group_layout_batch: GpuBindGroupLayoutHandle,
549}
550
551impl LineRenderer {
552    fn create_linestrip_batch(
553        &self,
554        ctx: &RenderContext,
555        label: DebugLabel,
556        uniform_buffer_binding: BindGroupEntry,
557        line_vertex_range: Range<u32>,
558        active_phases: EnumSet<DrawPhase>,
559    ) -> LineStripBatch {
560        // TODO(andreas): There should be only a single bindgroup with dynamic indices for all batches.
561        //                  (each batch would then know which dynamic indices to use in the bindgroup)
562        let bind_group = ctx.gpu_resources.bind_groups.alloc(
563            &ctx.device,
564            &ctx.gpu_resources,
565            &BindGroupDesc {
566                label,
567                entries: smallvec![uniform_buffer_binding],
568                layout: self.bind_group_layout_batch,
569            },
570        );
571
572        LineStripBatch {
573            bind_group,
574            // We spawn a quad for every line skeleton vertex. Naturally, this yields one extra quad in total.
575            // Which is rather convenient because we need to ensure there are start and end triangles,
576            // so just from a number-of=vertices perspective this is correct already and the shader can take care of offsets.
577            vertex_range: (line_vertex_range.start * 6)..(line_vertex_range.end * 6),
578            active_phases,
579        }
580    }
581}
582
583impl Renderer for LineRenderer {
584    type RendererDrawData = LineDrawData;
585
586    fn create_renderer(ctx: &RenderContext) -> Self {
587        profile_function!();
588
589        let render_pipelines = &ctx.gpu_resources.render_pipelines;
590
591        let bind_group_layout_all_lines = ctx.gpu_resources.bind_group_layouts.get_or_create(
592            &ctx.device,
593            &BindGroupLayoutDesc {
594                label: "LineRenderer::bind_group_layout_all_lines".into(),
595                entries: vec![
596                    wgpu::BindGroupLayoutEntry {
597                        binding: 0,
598                        visibility: wgpu::ShaderStages::VERTEX,
599                        ty: wgpu::BindingType::Texture {
600                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
601                            view_dimension: wgpu::TextureViewDimension::D2,
602                            multisampled: false,
603                        },
604                        count: None,
605                    },
606                    wgpu::BindGroupLayoutEntry {
607                        binding: 1,
608                        visibility: wgpu::ShaderStages::VERTEX,
609                        ty: wgpu::BindingType::Texture {
610                            sample_type: wgpu::TextureSampleType::Uint,
611                            view_dimension: wgpu::TextureViewDimension::D2,
612                            multisampled: false,
613                        },
614                        count: None,
615                    },
616                    wgpu::BindGroupLayoutEntry {
617                        binding: 2,
618                        visibility: wgpu::ShaderStages::VERTEX,
619                        ty: wgpu::BindingType::Texture {
620                            sample_type: wgpu::TextureSampleType::Uint,
621                            view_dimension: wgpu::TextureViewDimension::D2,
622                            multisampled: false,
623                        },
624                        count: None,
625                    },
626                    wgpu::BindGroupLayoutEntry {
627                        binding: 3,
628                        visibility: wgpu::ShaderStages::VERTEX,
629                        ty: wgpu::BindingType::Buffer {
630                            ty: wgpu::BufferBindingType::Uniform,
631                            has_dynamic_offset: false,
632                            min_binding_size: NonZeroU64::new(std::mem::size_of::<
633                                gpu_data::DrawDataUniformBuffer,
634                            >() as _),
635                        },
636                        count: None,
637                    },
638                ],
639            },
640        );
641
642        let bind_group_layout_batch = ctx.gpu_resources.bind_group_layouts.get_or_create(
643            &ctx.device,
644            &BindGroupLayoutDesc {
645                label: "LineRenderer::bind_group_layout_batch".into(),
646                entries: vec![wgpu::BindGroupLayoutEntry {
647                    binding: 0,
648                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
649                    ty: wgpu::BindingType::Buffer {
650                        ty: wgpu::BufferBindingType::Uniform,
651                        has_dynamic_offset: false,
652                        min_binding_size: NonZeroU64::new(std::mem::size_of::<
653                            gpu_data::BatchUniformBuffer,
654                        >() as _),
655                    },
656                    count: None,
657                }],
658            },
659        );
660
661        let pipeline_layout = ctx.gpu_resources.pipeline_layouts.get_or_create(
662            ctx,
663            &PipelineLayoutDesc {
664                label: "LineRenderer::pipeline_layout".into(),
665                entries: vec![
666                    ctx.global_bindings.layout,
667                    bind_group_layout_all_lines,
668                    bind_group_layout_batch,
669                ],
670            },
671        );
672
673        let shader_module = ctx
674            .gpu_resources
675            .shader_modules
676            .get_or_create(ctx, &include_shader_module!("../../shader/lines.wgsl"));
677
678        let render_pipeline_desc_color = RenderPipelineDesc {
679            label: "LineRenderer::render_pipeline_color".into(),
680            pipeline_layout,
681            vertex_entrypoint: "vs_main".into(),
682            vertex_handle: shader_module,
683            fragment_entrypoint: "fs_main".into(),
684            fragment_handle: shader_module,
685            vertex_buffers: smallvec![],
686            render_targets: smallvec![Some(ViewBuilder::MAIN_TARGET_ALPHA_TO_COVERAGE_COLOR_STATE)],
687            primitive: wgpu::PrimitiveState {
688                topology: wgpu::PrimitiveTopology::TriangleList,
689                ..Default::default()
690            },
691            depth_stencil: Some(ViewBuilder::MAIN_TARGET_DEFAULT_DEPTH_STATE),
692            multisample: ViewBuilder::main_target_default_msaa_state(ctx.render_config(), true),
693        };
694        let render_pipeline_color =
695            render_pipelines.get_or_create(ctx, &render_pipeline_desc_color);
696        let render_pipeline_picking_layer = render_pipelines.get_or_create(
697            ctx,
698            &RenderPipelineDesc {
699                label: "LineRenderer::render_pipeline_picking_layer".into(),
700                fragment_entrypoint: "fs_main_picking_layer".into(),
701                render_targets: smallvec![Some(PickingLayerProcessor::PICKING_LAYER_FORMAT.into())],
702                depth_stencil: PickingLayerProcessor::PICKING_LAYER_DEPTH_STATE,
703                multisample: PickingLayerProcessor::PICKING_LAYER_MSAA_STATE,
704                ..render_pipeline_desc_color.clone()
705            },
706        );
707        let render_pipeline_outline_mask = render_pipelines.get_or_create(
708            ctx,
709            &RenderPipelineDesc {
710                label: "LineRenderer::render_pipeline_outline_mask".into(),
711                pipeline_layout,
712                vertex_entrypoint: "vs_main".into(),
713                vertex_handle: shader_module,
714                fragment_entrypoint: "fs_main_outline_mask".into(),
715                fragment_handle: shader_module,
716                vertex_buffers: smallvec![],
717                render_targets: smallvec![Some(OutlineMaskProcessor::MASK_FORMAT.into())],
718                primitive: wgpu::PrimitiveState {
719                    topology: wgpu::PrimitiveTopology::TriangleList,
720                    ..Default::default()
721                },
722                depth_stencil: OutlineMaskProcessor::MASK_DEPTH_STATE,
723                // Alpha to coverage doesn't work with the mask integer target.
724                multisample: OutlineMaskProcessor::mask_default_msaa_state(ctx.device_caps().tier),
725            },
726        );
727
728        Self {
729            render_pipeline_color,
730            render_pipeline_picking_layer,
731            render_pipeline_outline_mask,
732            bind_group_layout_all_lines,
733            bind_group_layout_batch,
734        }
735    }
736
737    fn draw(
738        &self,
739        render_pipelines: &GpuRenderPipelinePoolAccessor<'_>,
740        phase: DrawPhase,
741        pass: &mut wgpu::RenderPass<'_>,
742        draw_instructions: &[DrawInstruction<'_, Self::RendererDrawData>],
743    ) -> Result<(), DrawError> {
744        let pipeline_handle = match phase {
745            DrawPhase::OutlineMask => self.render_pipeline_outline_mask,
746            DrawPhase::Opaque => self.render_pipeline_color,
747            DrawPhase::PickingLayer => self.render_pipeline_picking_layer,
748            _ => unreachable!("We were called on a phase we weren't subscribed to: {phase:?}"),
749        };
750
751        let pipeline = render_pipelines.get(pipeline_handle)?;
752        pass.set_pipeline(pipeline);
753
754        for DrawInstruction {
755            draw_data,
756            drawables,
757        } in draw_instructions
758        {
759            let bind_group_draw_data = match phase {
760                DrawPhase::OutlineMask => &draw_data.bind_group_all_lines_outline_mask,
761                DrawPhase::Opaque | DrawPhase::PickingLayer => &draw_data.bind_group_all_lines,
762                _ => unreachable!("We were called on a phase we weren't subscribed to: {phase:?}"),
763            };
764            let Some(bind_group_draw_data) = bind_group_draw_data else {
765                debug_assert!(
766                    false,
767                    "Line data bind group for draw phase {phase:?} was not set despite being submitted for drawing."
768                );
769                continue;
770            };
771            pass.set_bind_group(1, bind_group_draw_data, &[]);
772
773            for drawable in *drawables {
774                let batch = &draw_data.batches[drawable.draw_data_payload as usize];
775                pass.set_bind_group(2, &batch.bind_group, &[]);
776                pass.draw(batch.vertex_range.clone(), 0..1);
777            }
778        }
779
780        Ok(())
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use crate::{Rgba, view_builder::TargetConfiguration};
787
788    use super::*;
789
790    // Regression test for https://github.com/rerun-io/rerun/issues/8639
791    #[test]
792    fn empty_strips() {
793        re_log::setup_logging();
794        re_log::PanicOnWarnScope::new();
795
796        RenderContext::new_test().execute_test_frame(|ctx| {
797            let mut view = ViewBuilder::new(ctx, TargetConfiguration::default()).unwrap();
798
799            let empty = LineDrawableBuilder::new(ctx);
800            view.queue_draw(ctx, empty.into_draw_data().unwrap());
801
802            // This is the case that triggered
803            // https://github.com/rerun-io/rerun/issues/8639
804            // The others are here for completeness.
805            let mut empty_batch = LineDrawableBuilder::new(ctx);
806            empty_batch
807                .batch("empty batch")
808                .add_strip(std::iter::empty());
809            view.queue_draw(ctx, empty_batch.into_draw_data().unwrap());
810
811            let mut empty_batch_between_non_empty = LineDrawableBuilder::new(ctx);
812            empty_batch_between_non_empty
813                .batch("non-empty batch")
814                .add_strip([glam::Vec3::ZERO, glam::Vec3::ZERO].into_iter());
815            empty_batch_between_non_empty
816                .batch("empty batch")
817                .add_strip(std::iter::empty());
818            empty_batch_between_non_empty
819                .batch("non-empty batch")
820                .add_strip([glam::Vec3::ZERO, glam::Vec3::ZERO].into_iter());
821            view.queue_draw(ctx, empty_batch_between_non_empty.into_draw_data().unwrap());
822
823            [view.draw(ctx, Rgba::BLACK).unwrap()]
824        });
825    }
826}