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}