Skip to main content

ling/gfx/
depth.rs

1// src/gfx/depth.rs — deferred depth-sorted draw queue (painter's algorithm).
2//
3// All 3-D draw calls (`วาดสามเหลี่ยม3มิติ`, `วาดเส้น3มิติ`) push a `DrawCall`
4// into this queue instead of rasterising immediately.  When `แสดงผล` / `present`
5// is called, the queue is sorted back-to-front by the depth tag and then
6// flushed into the pixel buffer.
7//
8// Painter's algorithm is exact for convex non-intersecting geometry and
9// produces plausible results for the Sierpiński fractal + tesseract wireframe.
10//
11// Each call also captures the current blend `mode` (0 normal · 1 add · 2 mul ·
12// 3 screen · 4 subtract · 5 overlay) and pen `alpha` so translucent 3-D FX
13// (sword slashes, ring trails, liquid orbs) composite over the scene instead of
14// painting opaque black where they fade out.
15
16// `raster` is wasm-safe (pure CPU); the software-framebuffer flush runs on web too.
17use crate::gfx::raster;
18#[cfg(not(target_arch = "wasm32"))]
19use rayon::prelude::*;
20
21/// Number of horizontal bands to rasterise a flush across. 1 = serial.
22///
23/// Banding pays off only when fill (pixels written) dominates: each band re-runs
24/// per-triangle setup, so a flush of many *tiny* triangles (e.g. text glyphs)
25/// would just multiply that setup. Gate on estimated covered area, not call count.
26#[cfg(not(target_arch = "wasm32"))]
27fn render_bands(width: usize, height: usize, est_pixels: usize) -> usize {
28    let screen = width * height;
29    if width == 0 || height < 256 || est_pixels < screen {
30        return 1;
31    }
32    let by_rows = height / 96; // keep bands ≥ ~96 rows tall
33    let by_fill = est_pixels / screen; // more overdraw → more bands worth it
34    rayon::current_num_threads()
35        .min(by_rows)
36        .min(by_fill.max(1) + 1)
37        .max(1)
38}
39
40#[cfg(not(target_arch = "wasm32"))]
41fn estimate_fill(calls: &[DrawCall]) -> usize {
42    let mut px = 0.0f32;
43    for c in calls {
44        let (a, b) = match c.kind {
45            DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. }
46            | DrawKind::TriangleG { x0, y0, x1, y1, x2, y2, .. } => {
47                let w = x0.max(x1).max(x2) - x0.min(x1).min(x2);
48                let h = y0.max(y1).max(y2) - y0.min(y1).min(y2);
49                (w, h)
50            },
51            DrawKind::Line { .. } => (0.0, 0.0),
52        };
53        px += 0.5 * a * b;
54    }
55    px.max(0.0) as usize
56}
57
58#[cfg(target_arch = "wasm32")]
59fn render_bands(_w: usize, _h: usize, _n: usize) -> usize {
60    1
61}
62
63/// Rasterise one queued call into a band starting `ysh` rows down: every y
64/// coordinate is shifted into band-local space and the band's own slices are
65/// indexed as a standalone `width × bh` framebuffer.
66#[inline]
67fn rasterize_call(
68    call: &DrawCall,
69    buf: &mut [u32],
70    zbuf: Option<&mut [f32]>,
71    width: usize,
72    height: usize,
73    ysh: f32,
74    aa: bool,
75) {
76    let blended = call.mode != 0 || call.alpha < 0.999;
77    match zbuf {
78        Some(z) => match call.kind {
79            DrawKind::Triangle { x0, y0, z0, x1, y1, z1, x2, y2, z2 } => {
80                if blended {
81                    raster::fill_triangle_z_blend(
82                        buf,
83                        z,
84                        width,
85                        height,
86                        call.color,
87                        call.mode,
88                        call.alpha,
89                        x0,
90                        y0 - ysh,
91                        z0,
92                        x1,
93                        y1 - ysh,
94                        z1,
95                        x2,
96                        y2 - ysh,
97                        z2,
98                    );
99                } else {
100                    raster::fill_triangle_z(
101                        buf,
102                        z,
103                        width,
104                        height,
105                        call.color,
106                        x0,
107                        y0 - ysh,
108                        z0,
109                        x1,
110                        y1 - ysh,
111                        z1,
112                        x2,
113                        y2 - ysh,
114                        z2,
115                    );
116                }
117            },
118            DrawKind::TriangleG { x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, bands, softness } => {
119                raster::fill_triangle_gouraud_z(
120                    buf,
121                    z,
122                    width,
123                    height,
124                    x0,
125                    y0 - ysh,
126                    z0,
127                    c0,
128                    x1,
129                    y1 - ysh,
130                    z1,
131                    c1,
132                    x2,
133                    y2 - ysh,
134                    z2,
135                    c2,
136                    bands,
137                    softness,
138                    call.alpha,
139                    call.mode,
140                    call.unlit,
141                )
142            },
143            DrawKind::Line { x0, y0, x1, y1, .. } => {
144                if aa {
145                    raster::draw_line_aa(
146                        buf,
147                        width,
148                        height,
149                        call.color,
150                        call.mode == 1,
151                        x0,
152                        y0 - ysh,
153                        x1,
154                        y1 - ysh,
155                    );
156                } else if blended {
157                    raster::draw_line_blend(
158                        buf,
159                        width,
160                        height,
161                        call.color,
162                        call.mode,
163                        call.alpha,
164                        x0,
165                        y0 - ysh,
166                        x1,
167                        y1 - ysh,
168                    );
169                } else {
170                    raster::draw_line(buf, width, height, call.color, x0, y0 - ysh, x1, y1 - ysh);
171                }
172            },
173        },
174        None => match call.kind {
175            DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
176                if blended {
177                    raster::fill_triangle_blend(
178                        buf,
179                        width,
180                        height,
181                        call.color,
182                        call.mode,
183                        call.alpha,
184                        x0,
185                        y0 - ysh,
186                        x1,
187                        y1 - ysh,
188                        x2,
189                        y2 - ysh,
190                    );
191                } else {
192                    raster::fill_triangle(
193                        buf,
194                        width,
195                        height,
196                        call.color,
197                        x0,
198                        y0 - ysh,
199                        x1,
200                        y1 - ysh,
201                        x2,
202                        y2 - ysh,
203                    );
204                }
205            },
206            DrawKind::TriangleG { x0, y0, c0, x1, y1, c1, x2, y2, c2, bands, softness, .. } => {
207                raster::fill_triangle_gouraud(
208                    buf,
209                    width,
210                    height,
211                    x0,
212                    y0 - ysh,
213                    c0,
214                    x1,
215                    y1 - ysh,
216                    c1,
217                    x2,
218                    y2 - ysh,
219                    c2,
220                    bands,
221                    softness,
222                    call.alpha,
223                    call.mode,
224                    call.unlit,
225                )
226            },
227            DrawKind::Line { x0, y0, x1, y1, .. } => {
228                if aa {
229                    raster::draw_line_aa(
230                        buf,
231                        width,
232                        height,
233                        call.color,
234                        call.mode == 1,
235                        x0,
236                        y0 - ysh,
237                        x1,
238                        y1 - ysh,
239                    );
240                } else if blended {
241                    raster::draw_line_blend(
242                        buf,
243                        width,
244                        height,
245                        call.color,
246                        call.mode,
247                        call.alpha,
248                        x0,
249                        y0 - ysh,
250                        x1,
251                        y1 - ysh,
252                    );
253                } else {
254                    raster::draw_line(buf, width, height, call.color, x0, y0 - ysh, x1, y1 - ysh);
255                }
256            },
257        },
258    }
259}
260
261#[cfg(not(target_arch = "wasm32"))]
262struct FlushTimer(std::time::Instant);
263#[cfg(not(target_arch = "wasm32"))]
264impl Drop for FlushTimer {
265    fn drop(&mut self) {
266        crate::runtime::ling_phase_add(crate::runtime::phase::FLUSH, self.0.elapsed().as_nanos());
267    }
268}
269
270/// Tagged draw call stored in the queue.
271#[derive(Debug, Clone)]
272pub struct DrawCall {
273    /// Camera-space z of the face/edge centroid — larger = further away.
274    pub depth: f32,
275    /// Pre-lit 0x00RRGGBB colour.
276    pub color: u32,
277    /// Blend mode (0 normal · 1 add · 2 multiply · 3 screen · 4 subtract · 5 overlay).
278    pub mode: u8,
279    /// Pen opacity 0..1 (coverage for the composite).
280    pub alpha: f32,
281    /// Tag written pixels [`crate::gfx::UNLIT`] (flat-shaded Gouraud triangles
282    /// only) so the toon post-process leaves them exact instead of re-shading
283    /// colour that was deliberately drawn unlit.
284    pub unlit: bool,
285    pub kind: DrawKind,
286}
287
288#[derive(Debug, Clone)]
289pub enum DrawKind {
290    Triangle {
291        x0: f32,
292        y0: f32,
293        z0: f32,
294        x1: f32,
295        y1: f32,
296        z1: f32,
297        x2: f32,
298        y2: f32,
299        z2: f32,
300    },
301    /// Gouraud-interpolated + per-pixel posterised triangle (smooth cel).
302    /// `bands < 2` disables posterisation (smooth Gouraud, unchanged look for
303    /// callers that never asked for toon bands). `softness` crossfades between
304    /// the posterised and raw interpolated colour (0 = crisp bands, 1 = smooth).
305    TriangleG {
306        x0: f32,
307        y0: f32,
308        z0: f32,
309        c0: u32,
310        x1: f32,
311        y1: f32,
312        z1: f32,
313        c1: u32,
314        x2: f32,
315        y2: f32,
316        z2: f32,
317        c2: u32,
318        bands: u32,
319        softness: f32,
320    },
321    Line {
322        x0: f32,
323        y0: f32,
324        z0: f32,
325        x1: f32,
326        y1: f32,
327        z1: f32,
328    },
329}
330
331/// Deferred depth-sorted draw queue.
332#[derive(Debug)]
333pub struct DepthQueue {
334    calls: Vec<DrawCall>,
335    /// Current blend mode applied to subsequent pushes (mirrors `gfx.blend`).
336    cur_mode: u8,
337    /// Current pen alpha applied to subsequent pushes (mirrors `gfx.alpha`).
338    cur_alpha: f32,
339}
340
341impl Default for DepthQueue {
342    fn default() -> Self {
343        Self { calls: Vec::new(), cur_mode: 0, cur_alpha: 1.0 }
344    }
345}
346
347impl DepthQueue {
348    /// Mirror the live pen blend mode + alpha so the next pushes capture them.
349    /// Call after `std::mem::take` so an active blend survives a mid-frame flush.
350    pub fn set_state(&mut self, mode: u8, alpha: f32) {
351        self.cur_mode = mode;
352        self.cur_alpha = alpha.clamp(0.0, 1.0);
353    }
354
355    /// Queue a filled triangle (flat per-vertex depth = the sort key).
356    #[allow(clippy::too_many_arguments)]
357    pub fn push_triangle(
358        &mut self,
359        depth: f32,
360        color: u32,
361        x0: f32,
362        y0: f32,
363        x1: f32,
364        y1: f32,
365        x2: f32,
366        y2: f32,
367    ) {
368        self.calls.push(DrawCall {
369            depth,
370            color,
371            mode: self.cur_mode,
372            alpha: self.cur_alpha,
373            unlit: false,
374            kind: DrawKind::Triangle { x0, y0, z0: depth, x1, y1, z1: depth, x2, y2, z2: depth },
375        });
376    }
377
378    /// Queue a filled triangle with true per-vertex camera-space depth, so the
379    /// per-pixel z-buffer can resolve interpenetration.
380    #[allow(clippy::too_many_arguments)]
381    pub fn push_triangle_zv(
382        &mut self,
383        color: u32,
384        x0: f32,
385        y0: f32,
386        z0: f32,
387        x1: f32,
388        y1: f32,
389        z1: f32,
390        x2: f32,
391        y2: f32,
392        z2: f32,
393    ) {
394        let depth = (z0 + z1 + z2) / 3.0;
395        self.calls.push(DrawCall {
396            depth,
397            color,
398            mode: self.cur_mode,
399            alpha: self.cur_alpha,
400            unlit: false,
401            kind: DrawKind::Triangle { x0, y0, z0, x1, y1, z1, x2, y2, z2 },
402        });
403    }
404
405    /// Queue a Gouraud + posterised triangle (smooth cel), flat per-vertex depth.
406    #[allow(clippy::too_many_arguments)]
407    pub fn push_triangle_g(
408        &mut self,
409        depth: f32,
410        x0: f32,
411        y0: f32,
412        c0: u32,
413        x1: f32,
414        y1: f32,
415        c1: u32,
416        x2: f32,
417        y2: f32,
418        c2: u32,
419        bands: u32,
420        unlit: bool,
421    ) {
422        self.calls.push(DrawCall {
423            depth,
424            color: c0,
425            mode: self.cur_mode,
426            alpha: self.cur_alpha,
427            unlit,
428            kind: DrawKind::TriangleG {
429                x0,
430                y0,
431                z0: depth,
432                c0,
433                x1,
434                y1,
435                z1: depth,
436                c1,
437                x2,
438                y2,
439                z2: depth,
440                c2,
441                bands,
442                softness: 0.0,
443            },
444        });
445    }
446
447    /// Gouraud triangle with true per-vertex depth (for the z-buffer path).
448    #[allow(clippy::too_many_arguments)]
449    pub fn push_triangle_g_zv(
450        &mut self,
451        x0: f32,
452        y0: f32,
453        z0: f32,
454        c0: u32,
455        x1: f32,
456        y1: f32,
457        z1: f32,
458        c1: u32,
459        x2: f32,
460        y2: f32,
461        z2: f32,
462        c2: u32,
463        bands: u32,
464        unlit: bool,
465    ) {
466        self.push_triangle_g_zv_soft(
467            x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, bands, 0.0, unlit,
468        );
469    }
470
471    /// `push_triangle_g_zv` with an explicit band-crossfade `softness`
472    /// (0 = crisp bands, 1 = fully smooth). See `DrawKind::TriangleG`.
473    #[allow(clippy::too_many_arguments)]
474    pub fn push_triangle_g_zv_soft(
475        &mut self,
476        x0: f32,
477        y0: f32,
478        z0: f32,
479        c0: u32,
480        x1: f32,
481        y1: f32,
482        z1: f32,
483        c1: u32,
484        x2: f32,
485        y2: f32,
486        z2: f32,
487        c2: u32,
488        bands: u32,
489        softness: f32,
490        unlit: bool,
491    ) {
492        let depth = (z0 + z1 + z2) / 3.0;
493        self.calls.push(DrawCall {
494            depth,
495            color: c0,
496            mode: self.cur_mode,
497            alpha: self.cur_alpha,
498            unlit,
499            kind: DrawKind::TriangleG {
500                x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, bands, softness,
501            },
502        });
503    }
504
505    /// Queue a line segment (flat per-vertex depth).
506    pub fn push_line(&mut self, depth: f32, color: u32, x0: f32, y0: f32, x1: f32, y1: f32) {
507        // Global wireframe hue-cycle: when enabled, override every line stroke's
508        // colour with a rapidly time-cycling rainbow. The screen position adds a
509        // spatial phase so strokes spread across the spectrum instead of flashing
510        // as one flat colour.
511        let color = match crate::runtime::line_hue_phase() {
512            Some(ph) => {
513                let p = (ph + (x0 + y0) as f64 * 0.006) as f32;
514                let r = ((p.sin() * 0.5 + 0.5) * 255.0) as u32;
515                let g = (((p + 2.0944).sin() * 0.5 + 0.5) * 255.0) as u32;
516                let b = (((p + 4.1888).sin() * 0.5 + 0.5) * 255.0) as u32;
517                (r << 16) | (g << 8) | b
518            },
519            None => color,
520        };
521        self.calls.push(DrawCall {
522            depth,
523            color,
524            mode: self.cur_mode,
525            alpha: self.cur_alpha,
526            unlit: false,
527            kind: DrawKind::Line { x0, y0, z0: depth, x1, y1, z1: depth },
528        });
529    }
530
531    /// Sort back-to-front and rasterise everything into `buf`.
532    ///
533    /// `zbuf`: when `Some`, a per-pixel depth buffer (camera-space z, smaller =
534    /// nearer) is used so interpenetrating triangles resolve correctly — a true
535    /// z-buffer on top of the painter's sort. When `None`, pure painter's
536    /// algorithm (the default/legacy path).
537    ///
538    /// Opaque calls (mode 0, alpha ≈ 1) take the fast direct-write path; calls
539    /// with a blend mode or alpha < 1 composite via `composite_pixel`. In the
540    /// z-buffer path, translucent calls test depth but do not write it, so they
541    /// layer over the opaque scene (back-to-front sort handles their ordering).
542    ///
543    /// Consumes `self` — call site does `mem::take` to avoid borrow conflict.
544    #[allow(clippy::ptr_arg)]
545    pub fn flush(
546        mut self,
547        buf: &mut Vec<u32>,
548        zbuf: Option<&mut Vec<f32>>,
549        reset_z: bool,
550        width: usize,
551        height: usize,
552        aa: bool,
553    ) {
554        // Sort largest depth first (furthest → painted first, nearest on top).
555        // With a z-buffer the sort still helps transparency + reduces overdraw.
556        // STABLE sort: equal-depth calls keep submission order every frame, so
557        // co-planar / same-depth overlapping primitives (e.g. adjacent boot/title
558        // glyphs at z=0) never swap draw order frame-to-frame — kills the
559        // "lit↔unlit" flicker that an unstable sort produced on ties.
560        #[cfg(not(target_arch = "wasm32"))]
561        let _s = std::time::Instant::now();
562        self.calls.sort_by(|a, b| {
563            b.depth
564                .partial_cmp(&a.depth)
565                .unwrap_or(std::cmp::Ordering::Equal)
566        });
567        #[cfg(not(target_arch = "wasm32"))]
568        crate::runtime::ling_phase_add(crate::runtime::phase::SORT, _s.elapsed().as_nanos());
569        #[cfg(not(target_arch = "wasm32"))]
570        let _r = std::time::Instant::now();
571        #[cfg(not(target_arch = "wasm32"))]
572        let _guard = FlushTimer(_r);
573        let calls = &self.calls;
574        // Split the framebuffer into horizontal bands rasterised in parallel.
575        // Each band owns a disjoint slice of `buf`/`zbuf`, so a pixel is touched
576        // by exactly one thread; processing the sorted call list inside every
577        // band preserves the painter's per-pixel order. Worth the thread hop only
578        // when there is enough fill to amortise it.
579        #[cfg(not(target_arch = "wasm32"))]
580        let bands = render_bands(width, height, estimate_fill(calls));
581        #[cfg(target_arch = "wasm32")]
582        let bands = render_bands(width, height, 0);
583        match zbuf {
584            Some(z) => {
585                if z.len() != width * height {
586                    z.clear();
587                    z.resize(width * height, f32::INFINITY);
588                } else if reset_z {
589                    z.iter_mut().for_each(|v| *v = f32::INFINITY);
590                }
591                #[cfg(not(target_arch = "wasm32"))]
592                if bands > 1 {
593                    let rows = height.div_ceil(bands);
594                    buf.par_chunks_mut(rows * width)
595                        .zip(z.par_chunks_mut(rows * width))
596                        .enumerate()
597                        .for_each(|(b, (bbuf, bz))| {
598                            let ysh = (b * rows) as f32;
599                            let bh = bbuf.len() / width;
600                            for call in calls {
601                                rasterize_call(call, bbuf, Some(bz), width, bh, ysh, aa);
602                            }
603                        });
604                    return;
605                }
606                let _ = bands;
607                for call in calls {
608                    rasterize_call(call, buf, Some(z), width, height, 0.0, aa);
609                }
610            },
611            None => {
612                #[cfg(not(target_arch = "wasm32"))]
613                if bands > 1 {
614                    let rows = height.div_ceil(bands);
615                    buf.par_chunks_mut(rows * width)
616                        .enumerate()
617                        .for_each(|(b, bbuf)| {
618                            let ysh = (b * rows) as f32;
619                            let bh = bbuf.len() / width;
620                            for call in calls {
621                                rasterize_call(call, bbuf, None, width, bh, ysh, aa);
622                            }
623                        });
624                    return;
625                }
626                let _ = bands;
627                for call in calls {
628                    rasterize_call(call, buf, None, width, height, 0.0, aa);
629                }
630            },
631        }
632    }
633
634    pub fn is_empty(&self) -> bool {
635        self.calls.is_empty()
636    }
637
638    /// Consume the queue and send all draw calls to the WebGL backend.
639    /// Only compiled for wasm32 targets.
640    #[cfg(target_arch = "wasm32")]
641    pub fn flush_to_webgl(
642        mut self,
643        fill_r: f32,
644        fill_g: f32,
645        fill_b: f32,
646        width: usize,
647        height: usize,
648    ) {
649        // Sort back-to-front (painter's algorithm) — same as the native path.
650        self.calls.sort_unstable_by(|a, b| {
651            b.depth
652                .partial_cmp(&a.depth)
653                .unwrap_or(std::cmp::Ordering::Equal)
654        });
655        for call in &self.calls {
656            match call.kind {
657                DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
658                    crate::gfx::webgl::push_triangle(call.color, x0, y0, x1, y1, x2, y2, call.depth)
659                },
660                DrawKind::TriangleG { x0, y0, c0, x1, y1, c1, x2, y2, c2, .. } => {
661                    // WebGL path: approximate with the averaged vertex colour.
662                    let avg = {
663                        let r = ((c0 >> 16 & 0xFF) + (c1 >> 16 & 0xFF) + (c2 >> 16 & 0xFF)) / 3;
664                        let g = ((c0 >> 8 & 0xFF) + (c1 >> 8 & 0xFF) + (c2 >> 8 & 0xFF)) / 3;
665                        let b = ((c0 & 0xFF) + (c1 & 0xFF) + (c2 & 0xFF)) / 3;
666                        (r << 16) | (g << 8) | b
667                    };
668                    crate::gfx::webgl::push_triangle(avg, x0, y0, x1, y1, x2, y2, call.depth);
669                },
670                DrawKind::Line { x0, y0, x1, y1, .. } => {
671                    crate::gfx::webgl::push_line(call.color, x0, y0, x1, y1, call.depth)
672                },
673            }
674        }
675        crate::gfx::webgl::flush(fill_r, fill_g, fill_b, width, height);
676    }
677
678    /// Consume the queue and rasterise it on the GPU (wgpu) into `buf`.
679    /// Native analogue of `flush_to_webgl`: every call is already in screen
680    /// space, so we expand triangles to a vertex list and let the GPU fill
681    /// them, reading the result back into `buf`. Returns `false` if no GPU is
682    /// available (caller falls back to the CPU `flush`). Lines are not yet
683    /// emitted on this path.
684    #[cfg(feature = "gpu")]
685    pub fn flush_to_wgpu(
686        mut self,
687        buf: &mut Vec<u32>,
688        width: usize,
689        height: usize,
690        clear: [f32; 3],
691    ) -> bool {
692        use crate::gfx::wgpu_raster::Vert;
693        self.calls.sort_unstable_by(|a, b| {
694            b.depth
695                .partial_cmp(&a.depth)
696                .unwrap_or(std::cmp::Ordering::Equal)
697        });
698        let to_rgb = |c: u32| {
699            [
700                ((c >> 16) & 0xFF) as f32 / 255.0,
701                ((c >> 8) & 0xFF) as f32 / 255.0,
702                (c & 0xFF) as f32 / 255.0,
703            ]
704        };
705        let mut verts: Vec<Vert> = Vec::with_capacity(self.calls.len() * 3);
706        for call in &self.calls {
707            match call.kind {
708                DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
709                    let c = to_rgb(call.color);
710                    verts.push(Vert { pos: [x0, y0], color: c });
711                    verts.push(Vert { pos: [x1, y1], color: c });
712                    verts.push(Vert { pos: [x2, y2], color: c });
713                },
714                DrawKind::TriangleG { x0, y0, c0, x1, y1, c1, x2, y2, c2, .. } => {
715                    verts.push(Vert { pos: [x0, y0], color: to_rgb(c0) });
716                    verts.push(Vert { pos: [x1, y1], color: to_rgb(c1) });
717                    verts.push(Vert { pos: [x2, y2], color: to_rgb(c2) });
718                },
719                DrawKind::Line { .. } => { /* TODO: emit lines as thin quads on the GPU path */ },
720            }
721        }
722        if buf.len() < width * height {
723            buf.resize(width * height, 0);
724        }
725        crate::gfx::wgpu_raster::raster(&verts, width, height, clear, buf)
726    }
727}