tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1// =============================================================================
2// pintor :: reproducir una `Escena` grabada sobre `vello_cpu`
3// -----------------------------------------------------------------------------
4// Acá se cruza la frontera entre los dos modelos. La `Escena` guarda órdenes
5// autocontenidas —cada una trae su transform, su pincel y su forma—; el
6// `RenderContext` de vello_cpu tiene ESTADO, y espera que se lo fijes antes de
7// pedirle el trazo. Reproducir es, entonces, para cada orden: fijar el estado
8// que esa orden declara y disparar la primitiva.
9//
10// No se intenta minimizar los `set_*` comparando con el estado anterior. Sería
11// la optimización obvia, y es prematura: `set_transform` es escribir un Affine
12// en un struct, mientras que rasterizar el path que sigue es órdenes de
13// magnitud más caro. Medirlo antes de complicarlo.
14// =============================================================================
15
16use alloc::sync::Arc;
17use alloc::vec::Vec;
18
19use vello_cpu::kurbo::{Affine, Rect};
20use vello_cpu::peniko::color::{PremulRgba8, Srgb};
21use vello_cpu::peniko::{Brush, ImageAlphaType, ImageFormat};
22use vello_cpu::{Level, Pixmap, RenderContext, RenderMode, RenderSettings, Resources};
23
24use crate::escena::{Escena, Forma, Orden};
25
26/// El rasterizador: un `RenderContext` de vello_cpu con sus recursos, listo
27/// para recibir escenas una y otra vez.
28///
29/// Se sostiene entre fotogramas a propósito. El `RenderContext` tiene búferes
30/// internos (el de paths, sobre todo) que se reusan, y `Resources` guarda el
31/// registro de imágenes: recrearlo por frame sería tirar ese trabajo.
32pub struct Pintor {
33 ctx: RenderContext,
34 recursos: Resources,
35 ancho: u16,
36 alto: u16,
37}
38
39impl Pintor {
40 /// Un pintor para un lienzo de `ancho` x `alto` píxeles.
41 ///
42 /// El nivel SIMD sale de [`nivel_simd`]: escalar en bare-metal, detectado en
43 /// Linux. `OptimizeSpeed` es obligatorio y no una preferencia: con sólo la
44 /// feature `u8_pipeline` compilada, pedir `OptimizeQuality` (que quiere el
45 /// pipeline f32) haría entrar a vello_cpu por un camino que no existe.
46 pub fn new(ancho: u16, alto: u16) -> Self {
47 let ajustes = RenderSettings {
48 level: nivel_simd(),
49 num_threads: 0,
50 render_mode: RenderMode::OptimizeSpeed,
51 };
52 Self {
53 ctx: RenderContext::new_with(ancho, alto, ajustes),
54 recursos: Resources::new(),
55 ancho,
56 alto,
57 }
58 }
59
60 /// Ancho del lienzo en píxeles.
61 pub fn ancho(&self) -> u16 {
62 self.ancho
63 }
64
65 /// Alto del lienzo en píxeles.
66 pub fn alto(&self) -> u16 {
67 self.alto
68 }
69
70 /// Reproduce la escena y deja el resultado en `destino`.
71 ///
72 /// `destino` debe medir exactamente lo mismo que el pintor. El pixmap NO se
73 /// limpia acá: quien llama decide si el fotograma arranca de un fondo o
74 /// encima de lo anterior (Llimphi siempre pinta un fondo opaco de primera
75 /// orden, así que limpiar sería trabajo duplicado).
76 pub fn pintar(&mut self, escena: &Escena, destino: &mut Pixmap) {
77 self.ctx.reset();
78 for orden in escena.ordenes() {
79 self.ejecutar(orden);
80 }
81 self.ctx.flush();
82 self.ctx.render_to_pixmap(&mut self.recursos, destino);
83 }
84
85 /// Un pixmap del tamaño del pintor, listo para recibir un fotograma.
86 pub fn lienzo_nuevo(&self) -> Pixmap {
87 Pixmap::new(self.ancho, self.alto)
88 }
89
90 // -------------------------------------------------------------------------
91
92 fn ejecutar(&mut self, orden: &Orden) {
93 match orden {
94 Orden::Rellenar {
95 regla,
96 xf,
97 pincel,
98 xf_pincel,
99 forma,
100 } => {
101 self.ctx.set_fill_rule(*regla);
102 self.ctx.set_transform(*xf);
103 self.fijar_pincel(pincel, *xf_pincel);
104 match forma {
105 Forma::Rect(r) => self.ctx.fill_rect(r),
106 otra => self.ctx.fill_path(&otra.a_camino()),
107 }
108 }
109 Orden::Trazar {
110 estilo,
111 xf,
112 pincel,
113 xf_pincel,
114 forma,
115 } => {
116 self.ctx.set_stroke(estilo.clone());
117 self.ctx.set_transform(*xf);
118 self.fijar_pincel(pincel, *xf_pincel);
119 match forma {
120 Forma::Rect(r) => self.ctx.stroke_rect(r),
121 otra => self.ctx.stroke_path(&otra.a_camino()),
122 }
123 }
124 Orden::AbrirCapa {
125 regla,
126 mezcla,
127 alfa,
128 xf,
129 recorte,
130 } => {
131 self.ctx.set_fill_rule(*regla);
132 self.ctx.set_transform(*xf);
133 self.ctx.push_layer(
134 Some(&recorte.a_camino()),
135 Some(*mezcla),
136 Some(*alfa),
137 None,
138 None,
139 );
140 }
141 Orden::AbrirMascaraLuminancia {
142 regla,
143 alfa,
144 xf,
145 recorte,
146 } => self.abrir_mascara_luminancia(*regla, *alfa, *xf, recorte),
147 Orden::CerrarCapa => self.ctx.pop_layer(),
148 Orden::Imagen { imagen, xf } => {
149 let Some(pixmap) = pixmap_de_peniko(&imagen.image) else {
150 return;
151 };
152 let (w, h) = (pixmap.width() as f64, pixmap.height() as f64);
153 let fuente = vello_cpu::Image {
154 image: vello_cpu::ImageSource::Pixmap(Arc::new(pixmap)),
155 sampler: imagen.sampler,
156 };
157 self.ctx.set_transform(*xf);
158 self.ctx.set_paint(fuente);
159 self.ctx.reset_paint_transform();
160 self.ctx.set_fill_rule(vello_cpu::peniko::Fill::NonZero);
161 self.ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
162 }
163 Orden::Glifos {
164 fuente,
165 tamano,
166 xf,
167 pincel,
168 hint,
169 glifos,
170 } => {
171 // El pincel y el transform se fijan ANTES de abrir el builder:
172 // `glyph_run` toma prestado el contexto hasta que se consume, y
173 // `fijar_pincel` necesita `&mut self`.
174 self.ctx.set_transform(*xf);
175 self.fijar_pincel(pincel, None);
176 self.ctx
177 .glyph_run(&mut self.recursos, fuente)
178 .font_size(*tamano)
179 .hint(*hint)
180 .fill_glyphs(glifos.iter().map(|g| vello_cpu::Glyph {
181 id: g.id,
182 x: g.x,
183 y: g.y,
184 }));
185 }
186 Orden::RectDifuso {
187 xf,
188 rect,
189 color,
190 radio,
191 desvio,
192 } => {
193 self.ctx.set_transform(*xf);
194 self.ctx.set_paint(*color);
195 self.ctx.reset_paint_transform();
196 self.ctx
197 .fill_blurred_rounded_rect(rect, *radio as f32, *desvio as f32);
198 }
199 }
200 }
201
202 /// Traduce un pincel de peniko al `PaintType` de vello_cpu.
203 ///
204 /// Sólido y degradado son el mismo tipo de un lado y del otro. La variante
205 /// imagen no: peniko la lleva como bytes crudos con formato, vello_cpu como
206 /// un `Pixmap` premultiplicado. Un pincel-imagen dentro de un `fill` es raro
207 /// en Llimphi (las imágenes entran por `draw_image`), así que si la
208 /// conversión no cuadra se cae a transparente en vez de abortar el frame.
209 fn fijar_pincel(&mut self, pincel: &Brush, xf_pincel: Option<Affine>) {
210 // La transformación del pincel es la que posiciona un degradado dentro
211 // de la forma. vello la compone como `transform * brush_transform`, y
212 // vello_cpu hace EXACTAMENTE lo mismo con su `paint_transform`
213 // (`render.rs:336`), así que el mapeo es directo y no hay que
214 // pre-multiplicar nada a mano. Sin esto los degradados salen anclados al
215 // origen del mundo en vez de a la caja que los lleva.
216 match xf_pincel {
217 Some(t) => self.ctx.set_paint_transform(t),
218 None => self.ctx.reset_paint_transform(),
219 }
220 match pincel {
221 Brush::Solid(c) => self.ctx.set_paint(*c),
222 Brush::Gradient(g) => self.ctx.set_paint(g.clone()),
223 Brush::Image(img) => match pixmap_de_peniko(&img.image) {
224 Some(p) => self.ctx.set_paint(vello_cpu::Image {
225 image: vello_cpu::ImageSource::Pixmap(Arc::new(p)),
226 sampler: img.sampler,
227 }),
228 None => self
229 .ctx
230 .set_paint(vello_cpu::peniko::color::AlphaColor::<Srgb>::TRANSPARENT),
231 },
232 }
233 }
234
235 /// Abre una capa enmascarada por luminancia.
236 ///
237 /// Es el único de los ocho métodos que vello_cpu no da hecho: su
238 /// `push_mask_layer` quiere una `Mask` construida sobre un pixmap del
239 /// TAMAÑO COMPLETO del lienzo, no una forma. Así que se rasteriza la forma
240 /// aparte —blanco sobre negro, en un contexto propio— y de ese pixmap sale
241 /// la máscara. Cuesta un lienzo extra; el compositor lo usa una sola vez
242 /// (`MaskMode::Luminance`), así que se paga cuando de verdad se pide.
243 fn abrir_mascara_luminancia(
244 &mut self,
245 regla: vello_cpu::peniko::Fill,
246 alfa: f32,
247 xf: Affine,
248 recorte: &Forma,
249 ) {
250 let mut aparte = RenderContext::new_with(
251 self.ancho,
252 self.alto,
253 RenderSettings {
254 level: nivel_simd(),
255 num_threads: 0,
256 render_mode: RenderMode::OptimizeSpeed,
257 },
258 );
259 aparte.set_fill_rule(regla);
260 aparte.set_transform(xf);
261 aparte.set_paint(vello_cpu::peniko::color::AlphaColor::<Srgb>::WHITE);
262 match recorte {
263 Forma::Rect(r) => aparte.fill_rect(r),
264 otra => aparte.fill_path(&otra.a_camino()),
265 }
266 aparte.flush();
267
268 let mut lienzo = Pixmap::new(self.ancho, self.alto);
269 let mut recursos = Resources::new();
270 aparte.render_to_pixmap(&mut recursos, &mut lienzo);
271
272 let mascara = vello_cpu::Mask::new_luminance(&lienzo);
273 self.ctx
274 .push_layer(None, None, Some(alfa), Some(mascara), None);
275 }
276}
277
278/// El nivel SIMD con el que rasterizar.
279///
280/// Con `std` (Linux) se detecta en tiempo de ejecución y se aprovecha lo que la
281/// máquina tenga. Sin `std` (wawa) se usa el camino escalar: `Level::new()` se
282/// apoya en `is_x86_feature_detected!`, que no existe sin sistema operativo.
283/// Que el escalar alcance no es un consuelo — el spike de W0 midió 909-5.000 fps
284/// a 480x400 rasterizando así.
285fn nivel_simd() -> Level {
286 #[cfg(feature = "std")]
287 {
288 Level::try_detect().unwrap_or_else(Level::fallback)
289 }
290 #[cfg(not(feature = "std"))]
291 {
292 Level::fallback()
293 }
294}
295
296/// Convierte los bytes crudos de una imagen de peniko en un `Pixmap`
297/// premultiplicado, que es lo único que vello_cpu sabe muestrear.
298///
299/// Devuelve `None` si el blob no mide lo que el ancho/alto declaran — una
300/// imagen mal formada no debe tumbar el fotograma entero.
301fn pixmap_de_peniko(datos: &vello_cpu::peniko::ImageData) -> Option<Pixmap> {
302 let ancho = u16::try_from(datos.width).ok()?;
303 let alto = u16::try_from(datos.height).ok()?;
304 let pixeles = (datos.width as usize).checked_mul(datos.height as usize)?;
305 let crudo: &[u8] = datos.data.as_ref();
306 if crudo.len() < pixeles * 4 {
307 return None;
308 }
309
310 let mut salida: Vec<PremulRgba8> = Vec::with_capacity(pixeles);
311 let premultiplicada = datos.alpha_type == ImageAlphaType::AlphaPremultiplied;
312 for trozo in crudo.chunks_exact(4).take(pixeles) {
313 // peniko sólo define dos ordenamientos de canal, y ambos llegan acá.
314 let (r, g, b, a) = match datos.format {
315 ImageFormat::Rgba8 => (trozo[0], trozo[1], trozo[2], trozo[3]),
316 ImageFormat::Bgra8 => (trozo[2], trozo[1], trozo[0], trozo[3]),
317 _ => (trozo[0], trozo[1], trozo[2], trozo[3]),
318 };
319 salida.push(if premultiplicada {
320 PremulRgba8 { r, g, b, a }
321 } else {
322 // Premultiplicar con redondeo al más cercano: `+ 127` en vez de
323 // truncar. Truncar sesga TODO hacia oscuro y se nota como un halo
324 // sucio en los bordes suaves de los íconos.
325 let p = |c: u8| (((c as u32) * (a as u32) + 127) / 255) as u8;
326 PremulRgba8 {
327 r: p(r),
328 g: p(g),
329 b: p(b),
330 a,
331 }
332 });
333 }
334 Some(Pixmap::from_parts(salida, ancho, alto))
335}