gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-cpu/src/pintor.rs Rust · 335 líneas · 12.9 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

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}