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}