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().min(by_rows).min(by_fill.max(1) + 1).max(1)
35}
36
37#[cfg(not(target_arch = "wasm32"))]
38fn estimate_fill(calls: &[DrawCall]) -> usize {
39    let mut px = 0.0f32;
40    for c in calls {
41        let (a, b) = match c.kind {
42            DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. }
43            | DrawKind::TriangleG { x0, y0, x1, y1, x2, y2, .. } => {
44                let w = x0.max(x1).max(x2) - x0.min(x1).min(x2);
45                let h = y0.max(y1).max(y2) - y0.min(y1).min(y2);
46                (w, h)
47            },
48            DrawKind::Line { .. } => (0.0, 0.0),
49        };
50        px += 0.5 * a * b;
51    }
52    px.max(0.0) as usize
53}
54
55#[cfg(target_arch = "wasm32")]
56fn render_bands(_w: usize, _h: usize, _n: usize) -> usize {
57    1
58}
59
60/// Rasterise one queued call into a band starting `ysh` rows down: every y
61/// coordinate is shifted into band-local space and the band's own slices are
62/// indexed as a standalone `width × bh` framebuffer.
63#[inline]
64fn rasterize_call(
65    call: &DrawCall,
66    buf: &mut [u32],
67    zbuf: Option<&mut [f32]>,
68    width: usize,
69    height: usize,
70    ysh: f32,
71) {
72    let blended = call.mode != 0 || call.alpha < 0.999;
73    match zbuf {
74        Some(z) => match call.kind {
75            DrawKind::Triangle { x0, y0, z0, x1, y1, z1, x2, y2, z2 } => {
76                if blended {
77                    raster::fill_triangle_z_blend(
78                        buf, z, width, height, call.color, call.mode, call.alpha, x0, y0 - ysh, z0,
79                        x1, y1 - ysh, z1, x2, y2 - ysh, z2,
80                    );
81                } else {
82                    raster::fill_triangle_z(
83                        buf, z, width, height, call.color, x0, y0 - ysh, z0, x1, y1 - ysh, z1, x2,
84                        y2 - ysh, z2,
85                    );
86                }
87            },
88            DrawKind::TriangleG {
89                x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, bands,
90            } => raster::fill_triangle_gouraud_z(
91                buf, z, width, height, x0, y0 - ysh, z0, c0, x1, y1 - ysh, z1, c1, x2, y2 - ysh, z2,
92                c2, bands, call.alpha, call.mode,
93            ),
94            DrawKind::Line { x0, y0, x1, y1, .. } => {
95                if blended {
96                    raster::draw_line_blend(
97                        buf, width, height, call.color, call.mode, call.alpha, x0, y0 - ysh, x1,
98                        y1 - ysh,
99                    );
100                } else {
101                    raster::draw_line(buf, width, height, call.color, x0, y0 - ysh, x1, y1 - ysh);
102                }
103            },
104        },
105        None => match call.kind {
106            DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
107                if blended {
108                    raster::fill_triangle_blend(
109                        buf, width, height, call.color, call.mode, call.alpha, x0, y0 - ysh, x1,
110                        y1 - ysh, x2, y2 - ysh,
111                    );
112                } else {
113                    raster::fill_triangle(
114                        buf, width, height, call.color, x0, y0 - ysh, x1, y1 - ysh, x2, y2 - ysh,
115                    );
116                }
117            },
118            DrawKind::TriangleG { x0, y0, c0, x1, y1, c1, x2, y2, c2, bands, .. } => {
119                raster::fill_triangle_gouraud(
120                    buf, width, height, x0, y0 - ysh, c0, x1, y1 - ysh, c1, x2, y2 - ysh, c2, bands,
121                    call.alpha, call.mode,
122                )
123            },
124            DrawKind::Line { x0, y0, x1, y1, .. } => {
125                if blended {
126                    raster::draw_line_blend(
127                        buf, width, height, call.color, call.mode, call.alpha, x0, y0 - ysh, x1,
128                        y1 - ysh,
129                    );
130                } else {
131                    raster::draw_line(buf, width, height, call.color, x0, y0 - ysh, x1, y1 - ysh);
132                }
133            },
134        },
135    }
136}
137
138#[cfg(not(target_arch = "wasm32"))]
139struct FlushTimer(std::time::Instant);
140#[cfg(not(target_arch = "wasm32"))]
141impl Drop for FlushTimer {
142    fn drop(&mut self) {
143        crate::runtime::ling_phase_add(crate::runtime::phase::FLUSH, self.0.elapsed().as_nanos());
144    }
145}
146
147/// Tagged draw call stored in the queue.
148#[derive(Debug, Clone)]
149pub struct DrawCall {
150    /// Camera-space z of the face/edge centroid — larger = further away.
151    pub depth: f32,
152    /// Pre-lit 0x00RRGGBB colour.
153    pub color: u32,
154    /// Blend mode (0 normal · 1 add · 2 multiply · 3 screen · 4 subtract · 5 overlay).
155    pub mode: u8,
156    /// Pen opacity 0..1 (coverage for the composite).
157    pub alpha: f32,
158    pub kind: DrawKind,
159}
160
161#[derive(Debug, Clone)]
162pub enum DrawKind {
163    Triangle {
164        x0: f32,
165        y0: f32,
166        z0: f32,
167        x1: f32,
168        y1: f32,
169        z1: f32,
170        x2: f32,
171        y2: f32,
172        z2: f32,
173    },
174    /// Gouraud-interpolated + per-pixel posterised triangle (smooth cel).
175    TriangleG {
176        x0: f32,
177        y0: f32,
178        z0: f32,
179        c0: u32,
180        x1: f32,
181        y1: f32,
182        z1: f32,
183        c1: u32,
184        x2: f32,
185        y2: f32,
186        z2: f32,
187        c2: u32,
188        bands: u32,
189    },
190    Line {
191        x0: f32,
192        y0: f32,
193        z0: f32,
194        x1: f32,
195        y1: f32,
196        z1: f32,
197    },
198}
199
200/// Deferred depth-sorted draw queue.
201#[derive(Debug)]
202pub struct DepthQueue {
203    calls: Vec<DrawCall>,
204    /// Current blend mode applied to subsequent pushes (mirrors `gfx.blend`).
205    cur_mode: u8,
206    /// Current pen alpha applied to subsequent pushes (mirrors `gfx.alpha`).
207    cur_alpha: f32,
208}
209
210impl Default for DepthQueue {
211    fn default() -> Self {
212        Self { calls: Vec::new(), cur_mode: 0, cur_alpha: 1.0 }
213    }
214}
215
216impl DepthQueue {
217    /// Mirror the live pen blend mode + alpha so the next pushes capture them.
218    /// Call after `std::mem::take` so an active blend survives a mid-frame flush.
219    pub fn set_state(&mut self, mode: u8, alpha: f32) {
220        self.cur_mode = mode;
221        self.cur_alpha = alpha.clamp(0.0, 1.0);
222    }
223
224    /// Queue a filled triangle (flat per-vertex depth = the sort key).
225    pub fn push_triangle(
226        &mut self,
227        depth: f32,
228        color: u32,
229        x0: f32,
230        y0: f32,
231        x1: f32,
232        y1: f32,
233        x2: f32,
234        y2: f32,
235    ) {
236        self.calls.push(DrawCall {
237            depth,
238            color,
239            mode: self.cur_mode,
240            alpha: self.cur_alpha,
241            kind: DrawKind::Triangle { x0, y0, z0: depth, x1, y1, z1: depth, x2, y2, z2: depth },
242        });
243    }
244
245    /// Queue a filled triangle with true per-vertex camera-space depth, so the
246    /// per-pixel z-buffer can resolve interpenetration.
247    #[allow(clippy::too_many_arguments)]
248    pub fn push_triangle_zv(
249        &mut self,
250        color: u32,
251        x0: f32,
252        y0: f32,
253        z0: f32,
254        x1: f32,
255        y1: f32,
256        z1: f32,
257        x2: f32,
258        y2: f32,
259        z2: f32,
260    ) {
261        let depth = (z0 + z1 + z2) / 3.0;
262        self.calls.push(DrawCall {
263            depth,
264            color,
265            mode: self.cur_mode,
266            alpha: self.cur_alpha,
267            kind: DrawKind::Triangle { x0, y0, z0, x1, y1, z1, x2, y2, z2 },
268        });
269    }
270
271    /// Queue a Gouraud + posterised triangle (smooth cel), flat per-vertex depth.
272    #[allow(clippy::too_many_arguments)]
273    pub fn push_triangle_g(
274        &mut self,
275        depth: f32,
276        x0: f32,
277        y0: f32,
278        c0: u32,
279        x1: f32,
280        y1: f32,
281        c1: u32,
282        x2: f32,
283        y2: f32,
284        c2: u32,
285        bands: u32,
286    ) {
287        self.calls.push(DrawCall {
288            depth,
289            color: c0,
290            mode: self.cur_mode,
291            alpha: self.cur_alpha,
292            kind: DrawKind::TriangleG {
293                x0,
294                y0,
295                z0: depth,
296                c0,
297                x1,
298                y1,
299                z1: depth,
300                c1,
301                x2,
302                y2,
303                z2: depth,
304                c2,
305                bands,
306            },
307        });
308    }
309
310    /// Gouraud triangle with true per-vertex depth (for the z-buffer path).
311    #[allow(clippy::too_many_arguments)]
312    pub fn push_triangle_g_zv(
313        &mut self,
314        x0: f32,
315        y0: f32,
316        z0: f32,
317        c0: u32,
318        x1: f32,
319        y1: f32,
320        z1: f32,
321        c1: u32,
322        x2: f32,
323        y2: f32,
324        z2: f32,
325        c2: u32,
326        bands: u32,
327    ) {
328        let depth = (z0 + z1 + z2) / 3.0;
329        self.calls.push(DrawCall {
330            depth,
331            color: c0,
332            mode: self.cur_mode,
333            alpha: self.cur_alpha,
334            kind: DrawKind::TriangleG { x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, bands },
335        });
336    }
337
338    /// Queue a line segment (flat per-vertex depth).
339    pub fn push_line(&mut self, depth: f32, color: u32, x0: f32, y0: f32, x1: f32, y1: f32) {
340        self.calls.push(DrawCall {
341            depth,
342            color,
343            mode: self.cur_mode,
344            alpha: self.cur_alpha,
345            kind: DrawKind::Line { x0, y0, z0: depth, x1, y1, z1: depth },
346        });
347    }
348
349    /// Sort back-to-front and rasterise everything into `buf`.
350    ///
351    /// `zbuf`: when `Some`, a per-pixel depth buffer (camera-space z, smaller =
352    /// nearer) is used so interpenetrating triangles resolve correctly — a true
353    /// z-buffer on top of the painter's sort. When `None`, pure painter's
354    /// algorithm (the default/legacy path).
355    ///
356    /// Opaque calls (mode 0, alpha ≈ 1) take the fast direct-write path; calls
357    /// with a blend mode or alpha < 1 composite via `composite_pixel`. In the
358    /// z-buffer path, translucent calls test depth but do not write it, so they
359    /// layer over the opaque scene (back-to-front sort handles their ordering).
360    ///
361    /// Consumes `self` — call site does `mem::take` to avoid borrow conflict.
362    pub fn flush(
363        mut self,
364        buf: &mut Vec<u32>,
365        zbuf: Option<&mut Vec<f32>>,
366        reset_z: bool,
367        width: usize,
368        height: usize,
369    ) {
370        // Sort largest depth first (furthest → painted first, nearest on top).
371        // With a z-buffer the sort still helps transparency + reduces overdraw.
372        #[cfg(not(target_arch = "wasm32"))]
373        let _s = std::time::Instant::now();
374        self.calls.sort_unstable_by(|a, b| {
375            b.depth
376                .partial_cmp(&a.depth)
377                .unwrap_or(std::cmp::Ordering::Equal)
378        });
379        #[cfg(not(target_arch = "wasm32"))]
380        crate::runtime::ling_phase_add(crate::runtime::phase::SORT, _s.elapsed().as_nanos());
381        #[cfg(not(target_arch = "wasm32"))]
382        let _r = std::time::Instant::now();
383        #[cfg(not(target_arch = "wasm32"))]
384        let _guard = FlushTimer(_r);
385        let calls = &self.calls;
386        // Split the framebuffer into horizontal bands rasterised in parallel.
387        // Each band owns a disjoint slice of `buf`/`zbuf`, so a pixel is touched
388        // by exactly one thread; processing the sorted call list inside every
389        // band preserves the painter's per-pixel order. Worth the thread hop only
390        // when there is enough fill to amortise it.
391        #[cfg(not(target_arch = "wasm32"))]
392        let bands = render_bands(width, height, estimate_fill(calls));
393        #[cfg(target_arch = "wasm32")]
394        let bands = render_bands(width, height, 0);
395        match zbuf {
396            Some(z) => {
397                if z.len() != width * height {
398                    z.clear();
399                    z.resize(width * height, f32::INFINITY);
400                } else if reset_z {
401                    z.iter_mut().for_each(|v| *v = f32::INFINITY);
402                }
403                #[cfg(not(target_arch = "wasm32"))]
404                if bands > 1 {
405                    let rows = height.div_ceil(bands);
406                    buf.par_chunks_mut(rows * width)
407                        .zip(z.par_chunks_mut(rows * width))
408                        .enumerate()
409                        .for_each(|(b, (bbuf, bz))| {
410                            let ysh = (b * rows) as f32;
411                            let bh = bbuf.len() / width;
412                            for call in calls {
413                                rasterize_call(call, bbuf, Some(bz), width, bh, ysh);
414                            }
415                        });
416                    return;
417                }
418                let _ = bands;
419                for call in calls {
420                    rasterize_call(call, buf, Some(z), width, height, 0.0);
421                }
422            },
423            None => {
424                #[cfg(not(target_arch = "wasm32"))]
425                if bands > 1 {
426                    let rows = height.div_ceil(bands);
427                    buf.par_chunks_mut(rows * width).enumerate().for_each(|(b, bbuf)| {
428                        let ysh = (b * rows) as f32;
429                        let bh = bbuf.len() / width;
430                        for call in calls {
431                            rasterize_call(call, bbuf, None, width, bh, ysh);
432                        }
433                    });
434                    return;
435                }
436                let _ = bands;
437                for call in calls {
438                    rasterize_call(call, buf, None, width, height, 0.0);
439                }
440            },
441        }
442    }
443
444    pub fn is_empty(&self) -> bool {
445        self.calls.is_empty()
446    }
447
448    /// Consume the queue and send all draw calls to the WebGL backend.
449    /// Only compiled for wasm32 targets.
450    #[cfg(target_arch = "wasm32")]
451    pub fn flush_to_webgl(
452        mut self,
453        fill_r: f32,
454        fill_g: f32,
455        fill_b: f32,
456        width: usize,
457        height: usize,
458    ) {
459        // Sort back-to-front (painter's algorithm) — same as the native path.
460        self.calls.sort_unstable_by(|a, b| {
461            b.depth
462                .partial_cmp(&a.depth)
463                .unwrap_or(std::cmp::Ordering::Equal)
464        });
465        for call in &self.calls {
466            match call.kind {
467                DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
468                    crate::gfx::webgl::push_triangle(call.color, x0, y0, x1, y1, x2, y2, call.depth)
469                },
470                DrawKind::TriangleG { x0, y0, c0, x1, y1, c1, x2, y2, c2, .. } => {
471                    // WebGL path: approximate with the averaged vertex colour.
472                    let avg = {
473                        let r = ((c0 >> 16 & 0xFF) + (c1 >> 16 & 0xFF) + (c2 >> 16 & 0xFF)) / 3;
474                        let g = ((c0 >> 8 & 0xFF) + (c1 >> 8 & 0xFF) + (c2 >> 8 & 0xFF)) / 3;
475                        let b = ((c0 & 0xFF) + (c1 & 0xFF) + (c2 & 0xFF)) / 3;
476                        (r << 16) | (g << 8) | b
477                    };
478                    crate::gfx::webgl::push_triangle(avg, x0, y0, x1, y1, x2, y2, call.depth);
479                },
480                DrawKind::Line { x0, y0, x1, y1, .. } => {
481                    crate::gfx::webgl::push_line(call.color, x0, y0, x1, y1, call.depth)
482                },
483            }
484        }
485        crate::gfx::webgl::flush(fill_r, fill_g, fill_b, width, height);
486    }
487
488    /// Consume the queue and rasterise it on the GPU (wgpu) into `buf`.
489    /// Native analogue of `flush_to_webgl`: every call is already in screen
490    /// space, so we expand triangles to a vertex list and let the GPU fill
491    /// them, reading the result back into `buf`. Returns `false` if no GPU is
492    /// available (caller falls back to the CPU `flush`). Lines are not yet
493    /// emitted on this path.
494    #[cfg(feature = "gpu")]
495    pub fn flush_to_wgpu(
496        mut self,
497        buf: &mut Vec<u32>,
498        width: usize,
499        height: usize,
500        clear: [f32; 3],
501    ) -> bool {
502        use crate::gfx::wgpu_raster::Vert;
503        self.calls.sort_unstable_by(|a, b| {
504            b.depth
505                .partial_cmp(&a.depth)
506                .unwrap_or(std::cmp::Ordering::Equal)
507        });
508        let to_rgb = |c: u32| {
509            [
510                ((c >> 16) & 0xFF) as f32 / 255.0,
511                ((c >> 8) & 0xFF) as f32 / 255.0,
512                (c & 0xFF) as f32 / 255.0,
513            ]
514        };
515        let mut verts: Vec<Vert> = Vec::with_capacity(self.calls.len() * 3);
516        for call in &self.calls {
517            match call.kind {
518                DrawKind::Triangle {
519                    x0, y0, x1, y1, x2, y2, ..
520                } => {
521                    let c = to_rgb(call.color);
522                    verts.push(Vert { pos: [x0, y0], color: c });
523                    verts.push(Vert { pos: [x1, y1], color: c });
524                    verts.push(Vert { pos: [x2, y2], color: c });
525                }
526                DrawKind::TriangleG {
527                    x0, y0, c0, x1, y1, c1, x2, y2, c2, ..
528                } => {
529                    verts.push(Vert { pos: [x0, y0], color: to_rgb(c0) });
530                    verts.push(Vert { pos: [x1, y1], color: to_rgb(c1) });
531                    verts.push(Vert { pos: [x2, y2], color: to_rgb(c2) });
532                }
533                DrawKind::Line { .. } => { /* TODO: emit lines as thin quads on the GPU path */ }
534            }
535        }
536        if buf.len() < width * height {
537            buf.resize(width * height, 0);
538        }
539        crate::gfx::wgpu_raster::raster(&verts, width, height, clear, buf)
540    }
541}