Skip to main content

brep_render/render/
scene_sync.rs

1use super::*;
2
3/// An AUX edge's width at rest, in CSS px ([`GlobalStyles::edge_aux`]).
4const AUX_EDGE_WIDTH_PX: f32 = 1.0;
5
6impl RenderCore {
7    /// The base face color of one solid under the active settings, in LINEAR
8    /// space (converted with f64 precision so artifact bytes stay stable); a
9    /// display with an `opacity` carries it as the alpha (the blended pass
10    /// reads it, the opaque pass skips such a solid).
11    fn face_base_color(solid: &SolidDisplay, settings: &RenderSettings) -> Rgba {
12        let mut color = Self::face_base_color_opaque(solid, settings);
13        if let Some(opacity) = solid.opacity {
14            color[3] = opacity.clamp(0.0, 1.0);
15        }
16        color
17    }
18
19    fn face_base_color_opaque(solid: &SolidDisplay, settings: &RenderSettings) -> Rgba {
20        if let Some(rgb) = solid.color_override {
21            return Self::opaque_linear(rgb);
22        }
23        match settings.face_color_mode {
24            FaceColorMode::HashedBySolid => {
25                let srgb = solid_color_srgb(&solid.name);
26                [
27                    srgb_to_linear(srgb[0]) as f32,
28                    srgb_to_linear(srgb[1]) as f32,
29                    srgb_to_linear(srgb[2]) as f32,
30                    1.0,
31                ]
32            }
33            FaceColorMode::Uniform => {
34                let c = settings.face_color;
35                [
36                    srgb_to_linear(c[0] as f64) as f32,
37                    srgb_to_linear(c[1] as f64) as f32,
38                    srgb_to_linear(c[2] as f64) as f32,
39                    c[3],
40                ]
41            }
42        }
43    }
44
45    /// One sRGB model colour as an opaque LINEAR `Rgba` — the conversion the
46    /// solid's base style and the per-face palette must share.
47    fn opaque_linear(rgb: [f32; 3]) -> Rgba {
48        [
49            srgb_to_linear(rgb[0] as f64) as f32,
50            srgb_to_linear(rgb[1] as f64) as f32,
51            srgb_to_linear(rgb[2] as f64) as f32,
52            1.0,
53        ]
54    }
55
56    /// The shading params every face style carries (flat-vs-smooth in `.x`).
57    fn style_params(settings: &RenderSettings) -> [f32; 4] {
58        [if settings.flat_shading { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0]
59    }
60
61    /// Build this solid's PER-FACE style palette, de-duplicated by colour, and
62    /// the per-face index into it.
63    ///
64    /// Faces sharing a colour share one uniform buffer, so an imported body
65    /// painted in three colours costs three buffers however many faces it has —
66    /// and the draw loop can still coalesce a contiguous run of same-coloured
67    /// faces into one call. A solid with no per-face colours returns an EMPTY
68    /// palette, which is what re-arms the whole-mesh fast path.
69    fn face_style_palette(
70        &self,
71        solid: &SolidDisplay,
72        settings: &RenderSettings,
73    ) -> (Vec<StyleBuf>, Vec<Option<u32>>) {
74        let mut styles: Vec<StyleBuf> = Vec::new();
75        let mut colors: Vec<[f32; 3]> = Vec::new();
76        let mut per_face: Vec<Option<u32>> = Vec::with_capacity(solid.faces.len());
77        let params = Self::style_params(settings);
78        for face in &solid.faces {
79            let Some(rgb) = face.color_override else {
80                per_face.push(None);
81                continue;
82            };
83            let slot = match colors.iter().position(|c| *c == rgb) {
84                Some(slot) => slot,
85                None => {
86                    let style = StyleBuf::new(
87                        &self.device,
88                        &self.style_layout,
89                        solid.name.as_str(),
90                    );
91                    style.write(&self.queue, Self::opaque_linear(rgb), params);
92                    styles.push(style);
93                    colors.push(rgb);
94                    colors.len() - 1
95                }
96            };
97            per_face.push(Some(slot as u32));
98        }
99        (styles, per_face)
100    }
101
102    /// The vertex buffer's contents: positions and normals.
103    fn mesh_vertices(solid: &SolidDisplay) -> Vec<MeshVertex> {
104        solid
105            .mesh
106            .positions
107            .iter()
108            .zip(&solid.mesh.normals)
109            .map(|(&position, &normal)| MeshVertex { position, normal })
110            .collect()
111    }
112
113    /// The edge buffer's contents (one instance per polyline segment) and each
114    /// edge's instance range.
115    fn edge_segments(solid: &SolidDisplay) -> (Vec<EdgeInstance>, Vec<EdgeRange>) {
116        let mut segments: Vec<EdgeInstance> = Vec::new();
117        let mut edges = Vec::with_capacity(solid.edges.len());
118        for edge in &solid.edges {
119            let first_instance = segments.len() as u32;
120            for pair in edge.polyline.windows(2) {
121                segments.push(EdgeInstance {
122                    p0: pair[0],
123                    p1: pair[1],
124                });
125            }
126            edges.push(EdgeRange {
127                first_instance,
128                instance_count: segments.len() as u32 - first_instance,
129            });
130        }
131        (segments, edges)
132    }
133
134    /// The point buffer's contents: one instance per vertex.
135    fn point_instances(solid: &SolidDisplay) -> Vec<PointInstance> {
136        solid
137            .vertices
138            .iter()
139            .map(|v| PointInstance {
140                center: [
141                    v.position[0] as f32,
142                    v.position[1] as f32,
143                    v.position[2] as f32,
144                ],
145            })
146            .collect()
147    }
148
149    /// Re-pose a solid whose buffers hold the same SHAPE (a rigid move): rewrite
150    /// its vertex, edge and point buffers in place. `false` (nothing written)
151    /// when any count differs, so the caller rebuilds instead.
152    fn repose_solid(&self, existing: &GpuSolid, solid: &SolidDisplay) -> bool {
153        let vertices = Self::mesh_vertices(solid);
154        let (segments, edges) = Self::edge_segments(solid);
155        let points = Self::point_instances(solid);
156        let same = vertices.len() as u32 == existing.vertex_count
157            && segments.len() as u32 == existing.edge_instances
158            && edges.len() == existing.edges.len()
159            && points.len() as u32 == existing.point_count
160            && solid.faces.len() == existing.faces.len()
161            && existing.edge_buf.is_some() == !segments.is_empty()
162            && existing.point_buf.is_some() == !points.is_empty();
163        if !same {
164            return false;
165        }
166        self.queue.write_buffer(&existing.vertex_buf, 0, bytemuck::cast_slice(&vertices));
167        if let Some(buf) = &existing.edge_buf {
168            self.queue.write_buffer(buf, 0, bytemuck::cast_slice(&segments));
169        }
170        if let Some(buf) = &existing.point_buf {
171            self.queue.write_buffer(buf, 0, bytemuck::cast_slice(&points));
172        }
173        true
174    }
175
176    fn upload_solid(&self, solid: &SolidDisplay, settings: &RenderSettings) -> GpuSolid {
177        let vertices = Self::mesh_vertices(solid);
178        let vertex_buf = self
179            .device
180            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
181                label: Some(solid.name.as_str()),
182                contents: bytemuck::cast_slice(&vertices),
183                // COPY_DST: a rigid re-pose rewrites it in place.
184                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
185            });
186        let index_buf = self
187            .device
188            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
189                label: Some(solid.name.as_str()),
190                contents: bytemuck::cast_slice(&solid.mesh.indices),
191                usage: wgpu::BufferUsages::INDEX,
192            });
193        // Wireframe line-list, built PER FACE and de-duplicated inside each face.
194        //
195        // The obvious expansion — every triangle (a,b,c) → (a,b),(b,c),(c,a) —
196        // emits each of a face's INTERIOR edges twice, once from each of the two
197        // triangles that share it. That is two-thirds of the segments on a typical
198        // tessellation: twice the index data to upload and hold, and twice the
199        // vertex work per frame to draw two collinear lines on top of each other.
200        // The wire pipeline does not blend (`color_target(None)` in `pipelines.rs`),
201        // so the second draw of a segment lands on the same pixels in the same
202        // colour at the same depth — it was never visible.
203        //
204        // De-duplicating WITHIN a face, rather than across the whole mesh, is what
205        // keeps each face's segments CONTIGUOUS, which is what lets the hidden-face
206        // path below address them by range the way the shaded path addresses
207        // triangle ranges. The seam between two faces stays doubled (once per
208        // owning face) — a small fraction, and the price of a face still being able
209        // to hide its own wireframe.
210        let tri_total = solid.mesh.indices.len() / 3;
211        let mut wire_indices: Vec<u32> = Vec::with_capacity(solid.mesh.indices.len());
212        let mut wire_ranges: Vec<(u32, u32)> = Vec::with_capacity(solid.faces.len());
213        let mut covered = vec![false; tri_total];
214        let mut seen: HashSet<(u32, u32), BuildEdgeHasher> = HashSet::default();
215        let mut emit_triangle = |wire: &mut Vec<u32>,
216                                 seen: &mut HashSet<(u32, u32), BuildEdgeHasher>,
217                                 tri: usize| {
218            let idx = [
219                solid.mesh.indices[tri * 3],
220                solid.mesh.indices[tri * 3 + 1],
221                solid.mesh.indices[tri * 3 + 2],
222            ];
223            for k in 0..3 {
224                let (a, b) = (idx[k], idx[(k + 1) % 3]);
225                let key = if a < b { (a, b) } else { (b, a) };
226                if seen.insert(key) {
227                    wire.push(a);
228                    wire.push(b);
229                }
230            }
231        };
232        for face in &solid.faces {
233            let first = wire_indices.len() as u32;
234            seen.clear();
235            let start = (face.tri_start as usize).min(tri_total);
236            let end = (start + face.tri_count as usize).min(tri_total);
237            for tri in start..end {
238                covered[tri] = true;
239                emit_triangle(&mut wire_indices, &mut seen, tri);
240            }
241            wire_ranges.push((first, wire_indices.len() as u32 - first));
242        }
243        // Any triangle no face claims still belongs in the whole-mesh draw — the
244        // fast path draws the buffer end to end. It is addressed by no face range,
245        // exactly as before: the per-face path could never reach it either.
246        seen.clear();
247        for tri in 0..tri_total {
248            if !covered[tri] {
249                emit_triangle(&mut wire_indices, &mut seen, tri);
250            }
251        }
252        let wire_index_count = wire_indices.len() as u32;
253        let wire_index_buf = self
254            .device
255            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
256                label: Some(solid.name.as_str()),
257                contents: bytemuck::cast_slice(&wire_indices),
258                usage: wgpu::BufferUsages::INDEX,
259            });
260        let (face_styles, face_style_index) = self.face_style_palette(solid, settings);
261        let faces = solid
262            .faces
263            .iter()
264            .zip(&face_style_index)
265            .zip(&wire_ranges)
266            .map(|((face, style), &(wire_first, wire_count))| FaceRange {
267                first_index: face.tri_start * 3,
268                index_count: face.tri_count * 3,
269                wire_first,
270                wire_count,
271                style: *style,
272            })
273            .collect();
274
275        let (segments, edges) = Self::edge_segments(solid);
276        let occludable_edge_instances = solid
277            .edges
278            .iter()
279            .zip(&edges)
280            .find(|(edge, _)| edge.aux)
281            .map_or(segments.len() as u32, |(_, range): (_, &EdgeRange)| range.first_instance);
282        let edge_buf = (!segments.is_empty()).then(|| {
283            self.device
284                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
285                    label: Some(solid.name.as_str()),
286                    contents: bytemuck::cast_slice(&segments),
287                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
288                })
289        });
290
291        let points = Self::point_instances(solid);
292        let point_buf = (!points.is_empty()).then(|| {
293            self.device
294                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
295                    label: Some(solid.name.as_str()),
296                    contents: bytemuck::cast_slice(&points),
297                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
298                })
299        });
300
301        let base_style = StyleBuf::new(&self.device, &self.style_layout, solid.name.as_str());
302        base_style.write(
303            &self.queue,
304            Self::face_base_color(solid, settings),
305            Self::style_params(settings),
306        );
307
308        GpuSolid {
309            revision: solid.revision,
310            shape: Self::shape_of(solid),
311            vertex_count: vertices.len() as u32,
312            face_styles,
313            vertex_buf,
314            index_buf,
315            wire_index_buf,
316            wire_index_count,
317            faces,
318            edge_buf,
319            edges,
320            edge_instances: segments.len() as u32,
321            occludable_edge_instances,
322            point_buf,
323            point_count: points.len() as u32,
324            base_style,
325            boundary: None,
326            // Its pose slot is given by the sync. Uploaded as it is shown,
327            // so drawn through no pose; its buffers hold its shape's own
328            // geometry only when it is not a pose itself.
329            model_slot: 0,
330            holds_shape: solid.shape_revision == 0,
331        }
332    }
333
334    /// Bring the GPU scene in line with the display scene: new/changed solids
335    /// upload, unchanged solids keep their buffers (R10), removed solids drop
336    /// theirs. `settings_generation` invalidates per-solid style buffers only.
337    pub fn sync_scene(
338        &self,
339        gpu: &mut GpuScene,
340        scene: &RenderScene,
341        settings: &RenderSettings,
342        settings_generation: u64,
343    ) {
344        gpu.order.clear();
345        let models = gpu.models.get_or_insert_with(|| ModelArena::new(&self.device, &self.model_layout, scene.solids().len() as u32));
346        let mut seen: Vec<&str> = Vec::with_capacity(scene.solids().len());
347        for solid in scene.solids() {
348            gpu.order.push(solid.name.clone());
349            seen.push(solid.name.as_str());
350            let refresh_style = gpu.settings_generation != settings_generation;
351            match gpu.solids.get_mut(&solid.name) {
352                Some(existing) if existing.revision == solid.revision => {
353                    if refresh_style {
354                        self.restyle(existing, solid, settings);
355                    }
356                }
357                // The shape whose geometry the buffers hold, shown through a
358                // pose (an exploded view's step) or as it is: one matrix
359                // write, nothing of the mesh rewritten.
360                Some(existing)
361                    if existing.holds_shape
362                        && existing.shape == Self::shape_of(solid)
363                        && (solid.pose.is_some() || solid.shape_revision == 0) =>
364                {
365                    models.set(existing.model_slot, solid.pose.as_ref());
366                    existing.revision = solid.revision;
367                    gpu.pose_writes += 1;
368                    if refresh_style {
369                        self.restyle(existing, solid, settings);
370                    }
371                }
372                // The same shape moved rigidly (a live drag frame, its
373                // restore): rewrite positions in place, rebuild nothing.
374                Some(existing) if existing.shape == Self::shape_of(solid) && self.repose_solid(existing, solid) => {
375                    // The buffers now hold this pose itself, drawn as it is.
376                    models.set(existing.model_slot, None);
377                    existing.holds_shape = false;
378                    existing.revision = solid.revision;
379                    gpu.reposes += 1;
380                    if refresh_style {
381                        self.restyle(existing, solid, settings);
382                    }
383                }
384                _ => {
385                    let mut uploaded = self.upload_solid(solid, settings);
386                    match gpu.solids.get(&solid.name) {
387                        Some(replaced) => uploaded.model_slot = replaced.model_slot,
388                        None => uploaded.model_slot = models.alloc(),
389                    }
390                    models.set(uploaded.model_slot, None);
391                    gpu.solids.insert(solid.name.clone(), uploaded);
392                    gpu.uploads += 1;
393                }
394            }
395        }
396        let seen: std::collections::HashSet<&str> = seen.into_iter().collect();
397        gpu.solids.retain(|name, solid| {
398            let keep = seen.contains(name.as_str());
399            if !keep {
400                models.release(solid.model_slot);
401            }
402            keep
403        });
404        models.flush(&self.device, &self.queue, &self.model_layout);
405        gpu.settings_generation = settings_generation;
406    }
407
408    /// The shape a display's buffers would hold: its `shape_revision` when it
409    /// is a rigid move of another revision, else its own revision.
410    fn shape_of(solid: &SolidDisplay) -> u64 {
411        if solid.shape_revision != 0 {
412            solid.shape_revision
413        } else {
414            solid.revision
415        }
416    }
417
418    /// Rewrite a held solid's base and per-face styles for new settings.
419    fn restyle(&self, existing: &GpuSolid, solid: &SolidDisplay, settings: &RenderSettings) {
420        let params = Self::style_params(settings);
421        existing.base_style.write(&self.queue, Self::face_base_color(solid, settings), params);
422        // The per-face palette carries the same shading params, so it restyles
423        // with the base or flat shading would stop applying to individually
424        // coloured faces.
425        let mut seen: Vec<[f32; 3]> = Vec::new();
426        for face in &solid.faces {
427            let Some(rgb) = face.color_override else { continue };
428            if seen.contains(&rgb) {
429                continue;
430            }
431            if let Some(style) = existing.face_styles.get(seen.len()) {
432                style.write(&self.queue, Self::opaque_linear(rgb), params);
433            }
434            seen.push(rgb);
435        }
436    }
437
438    /// Upload a scene from scratch (artifact/one-shot path).
439    pub fn upload_scene(&self, scene: &RenderScene) -> GpuScene {
440        self.upload_scene_with(scene, &RenderSettings::artifact())
441    }
442
443    pub fn upload_scene_with(&self, scene: &RenderScene, settings: &RenderSettings) -> GpuScene {
444        let mut gpu = GpuScene::default();
445        self.sync_scene(&mut gpu, scene, settings, 0);
446        gpu
447    }
448
449    pub(super) fn ensure_targets(&mut self, width: u32, height: u32) {
450        if let Some(targets) = &self.targets {
451            if targets.width == width && targets.height == height {
452                return;
453            }
454        }
455        let size = wgpu::Extent3d {
456            width,
457            height,
458            depth_or_array_layers: 1,
459        };
460        let msaa = self.device.create_texture(&wgpu::TextureDescriptor {
461            label: Some("msaa color"),
462            size,
463            mip_level_count: 1,
464            sample_count: SAMPLES,
465            dimension: wgpu::TextureDimension::D2,
466            format: self.format,
467            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
468            view_formats: &[],
469        });
470        let depth = self.device.create_texture(&wgpu::TextureDescriptor {
471            label: Some("depth"),
472            size,
473            mip_level_count: 1,
474            sample_count: SAMPLES,
475            dimension: wgpu::TextureDimension::D2,
476            format: DEPTH_FORMAT,
477            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
478            view_formats: &[],
479        });
480        self.targets = Some(CachedTargets {
481            width,
482            height,
483            msaa_view: msaa.create_view(&Default::default()),
484            depth_view: depth.create_view(&Default::default()),
485        });
486    }
487
488    pub(super) fn write_global_styles(&self, settings: &RenderSettings) {
489        let flat = if settings.flat_shading { 1.0 } else { 0.0 };
490        let face = |c: Rgba| -> Rgba {
491            [
492                srgb_to_linear(c[0] as f64) as f32,
493                srgb_to_linear(c[1] as f64) as f32,
494                srgb_to_linear(c[2] as f64) as f32,
495                c[3],
496            ]
497        };
498        let s = &self.styles;
499        let q = &self.queue;
500        s.face_selected
501            .write(q, face(settings.face_selected_color), [flat, 0.0, 0.0, 0.0]);
502        s.face_hovered
503            .write(q, face(settings.hover_color), [flat, 0.0, 0.0, 0.0]);
504        // A context face picked or hovered: its selection colour, kept see-through.
505        let translucent = |rgba: [f32; 4], alpha: f32| [rgba[0], rgba[1], rgba[2], alpha];
506        self.context_selected.write(q, translucent(settings.face_selected_color, 0.6), [0.0; 4]);
507        self.context_hovered.write(q, translucent(settings.hover_color, 0.45), [0.0; 4]);
508        let edge_params = [settings.edge_width_px, EDGE_NUDGE, 0.0, 0.0];
509        s.edge_base.write(q, settings.edge_color, edge_params);
510        s.edge_selected.write(q, settings.edge_selected_color, edge_params);
511        s.edge_hovered.write(q, settings.hover_color, edge_params);
512        let mut hidden = settings.edge_color;
513        hidden[3] = settings.hidden_edge_alpha;
514        s.edge_hidden.write(q, hidden, edge_params);
515        // An AUX edge at rest is the theme's edge at hairline WIDTH: a board's
516        // copper outline is thousands of edges a fraction of a millimetre
517        // apart, and at the full width the outlines of a track field crowd
518        // the gaps between its tracks. (Too coarse a view does not draw them
519        // at all — `frame::AUX_EDGE_MAX_WORLD_PER_PIXEL`.)
520        s.edge_aux.write(q, settings.edge_color, [AUX_EDGE_WIDTH_PX, EDGE_NUDGE, 0.0, 0.0]);
521        // Selected-face boundary: selected-edge color, slightly wider, extra
522        // nudge so it reads as an outline on top of the fill.
523        s.boundary.write(
524            q,
525            settings.edge_selected_color,
526            [settings.edge_width_px + 1.0, EDGE_NUDGE * 1.5, 0.0, 0.0],
527        );
528        let point_params = [settings.vertex_size_px, 0.0, 0.0, 0.0];
529        s.point_base.write(q, settings.vertex_color, point_params);
530        s.point_selected
531            .write(q, settings.vertex_selected_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
532        s.point_hovered
533            .write(q, settings.hover_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
534        let axis_params = [2.0, EDGE_NUDGE, 0.0, 0.0];
535        s.axis_x.write(q, [0.91, 0.30, 0.32, 1.0], axis_params);
536        s.axis_y.write(q, [0.27, 0.80, 0.42, 1.0], axis_params);
537        s.axis_z.write(q, [0.23, 0.51, 0.96, 1.0], axis_params);
538        // Overlay-widget line width (color is per-instance from the gizmo).
539        s.overlay_line.write(q, [1.0, 1.0, 1.0, 1.0], [2.5, 0.0, 0.0, 0.0]);
540    }
541
542    /// Rebuild the selected/hovered-face boundary outline buffer for one solid
543    /// when the emphasis or the solid changed. The boundary of a face's
544    /// triangle range = mesh edges used an odd number of times inside it.
545    pub(super) fn sync_boundary(&self, gpu_solid: &mut GpuSolid, solid: &SolidDisplay, emphasis: &Emphasis) {
546        if let Some(boundary) = &gpu_solid.boundary {
547            if boundary.emphasis_generation == emphasis.generation
548                && boundary.revision == solid.revision
549            {
550                return;
551            }
552        }
553        let mut segments: Vec<EdgeInstance> = Vec::new();
554        for face in &solid.faces {
555            if face.tri_count == 0 {
556                continue;
557            }
558            let state = emphasis.face_state(&solid.name, &face.name);
559            if state == EmphasisState::Base {
560                continue;
561            }
562            let mut edge_use: HashMap<(u32, u32), u32> = HashMap::new();
563            let start = face.tri_start as usize;
564            let end = (start + face.tri_count as usize).min(solid.mesh.indices.len() / 3);
565            for tri in start..end {
566                let idx = [
567                    solid.mesh.indices[tri * 3],
568                    solid.mesh.indices[tri * 3 + 1],
569                    solid.mesh.indices[tri * 3 + 2],
570                ];
571                for k in 0..3 {
572                    let a = idx[k];
573                    let b = idx[(k + 1) % 3];
574                    let key = if a < b { (a, b) } else { (b, a) };
575                    *edge_use.entry(key).or_insert(0) += 1;
576                }
577            }
578            let mut boundary: Vec<(u32, u32)> = edge_use
579                .into_iter()
580                .filter_map(|(key, count)| (count == 1).then_some(key))
581                .collect();
582            boundary.sort_unstable();
583            for (a, b) in boundary {
584                segments.push(EdgeInstance {
585                    p0: solid.mesh.positions[a as usize],
586                    p1: solid.mesh.positions[b as usize],
587                });
588            }
589        }
590        let buf = (!segments.is_empty()).then(|| {
591            self.device
592                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
593                    label: Some("emphasis boundary"),
594                    contents: bytemuck::cast_slice(&segments),
595                    usage: wgpu::BufferUsages::VERTEX,
596                })
597        });
598        gpu_solid.boundary = Some(BoundaryBuf {
599            emphasis_generation: emphasis.generation,
600            revision: solid.revision,
601            buf,
602            count: segments.len() as u32,
603        });
604    }
605}