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}