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