Skip to main content

pineal_render/
gpu_canvas.rs

1//! Backend GPU directo del trait [`crate::Canvas`] — Fase 4 del SDD
2//! `02_ruway/llimphi/SDD.md` §"GPU directo wgpu".
3//!
4//! Implementación drop-in del trait que delega cada primitivo a un
5//! `llimphi_raster::GpuBatch`. El painter de pineal no se entera: sigue
6//! escribiendo `canvas.fill_rect(...)`, `canvas.stroke_line(...)`, etc.
7//! La app que monta la visualización elige el backend al enchufar el
8//! `paint_with` (SceneCanvas, vello, ~100 K primitivas) o
9//! `gpu_paint_with` (GpuSceneCanvas, GPU directo, 1–10 M primitivas).
10//!
11//! Trade-offs respecto a `SceneCanvas`:
12//!
13//! - **Texto**: el SDD prohíbe expresamente texto en este backend
14//!   ("Texto siempre por vello+parley"). `draw_text` queda como
15//!   **no-op silencioso** — el caller debe pintar labels en un
16//!   `View::paint_with` (vello) hermano o en `App::view_overlay`.
17//! - **Anchura de stroke**: `GpuBatch` tiene una sola `line_width` por
18//!   flush. Visualizaciones que mezclen grosores van a ver "la última
19//!   gana". Workaround: emitir cada grosor en su propio flush, o
20//!   simplemente componer todo con el grosor más común. Para los
21//!   painters densos típicos (starfield, particles, scatter) no aplica
22//!   — ahí las strokes son todas del mismo grosor.
23//! - **Clip**: `push_clip`/`pop_clip` son no-op, igual que en
24//!   `SceneCanvas` (el `View` contenedor recorta vía taffy + `clip(true)`).
25//! - **Per-vertex color en strips**: ventaja sobre vello — el GPU sí
26//!   soporta color por vértice nativo, así que `fill_triangle_strip`
27//!   preserva el color real de cada vértice (no el promedio del
28//!   triángulo). Beneficia phosphor trail, ribbons Sankey, fan radar.
29
30use crate::{Canvas, Color, Point, Rect, StrokeStyle};
31use llimphi_ui::llimphi_raster::peniko::Color as PenikoColor;
32use llimphi_ui::llimphi_raster::GpuBatch;
33
34/// Adapter que pinta primitivos de `Canvas` sobre un `GpuBatch`. El
35/// batch lo construye y flushea la app dentro del `gpu_paint_with`;
36/// `GpuSceneCanvas` vive sólo durante las calls del painter.
37pub struct GpuSceneCanvas<'a, 'b> {
38    batch: &'a mut GpuBatch<'b>,
39}
40
41impl<'a, 'b> GpuSceneCanvas<'a, 'b> {
42    pub fn new(batch: &'a mut GpuBatch<'b>) -> Self {
43        Self { batch }
44    }
45}
46
47fn to_peniko(c: Color) -> PenikoColor {
48    let to_byte = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
49    PenikoColor::from_rgba8(to_byte(c.r), to_byte(c.g), to_byte(c.b), to_byte(c.a))
50}
51
52impl<'a, 'b> Canvas for GpuSceneCanvas<'a, 'b> {
53    fn push_clip(&mut self, _rect: Rect) {}
54    fn pop_clip(&mut self) {}
55
56    fn fill_rect(&mut self, rect: Rect, color: Color) {
57        self.batch
58            .add_rect(rect.x, rect.y, rect.w, rect.h, to_peniko(color));
59    }
60
61    fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle) {
62        // 4 segmentos cerrando el rect. La line_width del batch es
63        // compartida — actualizamos en cada call (el caller asume "una
64        // anchura por flush", ver doc del módulo).
65        self.batch.line_width(stroke.width);
66        let c = to_peniko(stroke.color);
67        let tl = (rect.x, rect.y);
68        let tr = (rect.x + rect.w, rect.y);
69        let br = (rect.x + rect.w, rect.y + rect.h);
70        let bl = (rect.x, rect.y + rect.h);
71        self.batch.add_line(tl, tr, c);
72        self.batch.add_line(tr, br, c);
73        self.batch.add_line(br, bl, c);
74        self.batch.add_line(bl, tl, c);
75    }
76
77    fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle) {
78        self.batch.line_width(stroke.width);
79        self.batch
80            .add_line((a.x, a.y), (b.x, b.y), to_peniko(stroke.color));
81    }
82
83    fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle) {
84        if coords.len() < 4 {
85            return;
86        }
87        self.batch.line_width(stroke.width);
88        let c = to_peniko(stroke.color);
89        let mut prev = (coords[0], coords[1]);
90        let mut i = 2;
91        while i + 1 < coords.len() {
92            let cur = (coords[i], coords[i + 1]);
93            self.batch.add_line(prev, cur, c);
94            prev = cur;
95            i += 2;
96        }
97    }
98
99    fn fill_triangle_strip(&mut self, coords: &[f32], colors: &[Color]) {
100        let n = coords.len() / 2;
101        if n < 3 {
102            return;
103        }
104        // Expandir strip a tri list, con color real por vértice (no el
105        // promedio del backend vello). Cada triángulo del strip toma
106        // los índices (t, t+1, t+2).
107        for t in 0..n - 2 {
108            let i0 = t;
109            let i1 = t + 1;
110            let i2 = t + 2;
111            let p0 = (coords[i0 * 2], coords[i0 * 2 + 1]);
112            let p1 = (coords[i1 * 2], coords[i1 * 2 + 1]);
113            let p2 = (coords[i2 * 2], coords[i2 * 2 + 1]);
114            let c0 = colors.get(i0).copied().unwrap_or(Color::TRANSPARENT);
115            let c1 = colors.get(i1).copied().unwrap_or(Color::TRANSPARENT);
116            let c2 = colors.get(i2).copied().unwrap_or(Color::TRANSPARENT);
117            self.batch
118                .add_tri(p0, p1, p2, to_peniko(c0), to_peniko(c1), to_peniko(c2));
119        }
120    }
121
122    fn draw_text(&mut self, _p: Point, _text: &str, _color: Color, _size_px: f32) {
123        // No-op por diseño: el SDD prohíbe texto en este backend. Los
124        // labels van por un `paint_with` (vello) hermano o por el
125        // overlay del runtime.
126    }
127}