tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-hybrid/src/pintor.rs Rust · 514 siq'ikuna · 21.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1// =============================================================================
2//  pintor :: reproducir una `Escena` grabada sobre `vello_hybrid`
3// -----------------------------------------------------------------------------
4//  Hermano del `llimphi_cpu::pintor`, y a propósito casi renglón por renglón: la
5//  `Escena` guarda órdenes autocontenidas —cada una trae su transform, su pincel
6//  y su forma— y la `Scene` de hybrid tiene ESTADO, así que reproducir es, para
7//  cada orden, fijar el estado que declara y disparar la primitiva.
8//
9//  Igual que allá, no se intenta minimizar los `set_*` comparando con el estado
10//  anterior. Sigue siendo prematuro: escribir un `Affine` en un struct no se ve
11//  al lado de rasterizar el path que viene atrás.
12//
13//  Lo único que NO es igual son las imágenes, y la diferencia es estructural:
14//  hybrid las quiere en un atlas de la GPU, y subirlas necesita `device`/`queue`/
15//  `encoder`. De ahí las dos pasadas de [`Pintor::pintar`].
16// =============================================================================
17
18use std::collections::HashMap;
19use std::sync::Arc;
20
21use llimphi_cpu::escena::{Escena, Forma, Orden};
22use llimphi_cpu::kurbo::{Affine, Rect};
23use llimphi_cpu::peniko::{Brush, Color, Fill, ImageData};
24use vello_common::paint::{Image, ImageId, ImageSource};
25use vello_common::pixmap::Pixmap;
26
27pub use vello_hybrid::RenderError;
28
29/// El rasterizador: una `Scene` de vello_hybrid con su `Renderer` y su atlas,
30/// lista para recibir escenas una y otra vez.
31///
32/// Se sostiene entre fotogramas a propósito, y acá importa más que en el pintor
33/// CPU: el `Renderer` es dueño del **atlas de texturas**, así que recrearlo
34/// tiraría todas las imágenes subidas y las obligaría a volver a cruzar el bus.
35pub struct Pintor {
36    renderer: vello_hybrid::Renderer,
37    recursos: vello_hybrid::Resources,
38    /// La escena de hybrid, reusada entre frames. `reset()` conserva los búferes
39    /// internos; recrearla por frame sería regalarle trabajo al asignador.
40    gpu: vello_hybrid::Scene,
41    /// Imágenes ya subidas al atlas, por `Blob::id()` de peniko.
42    ///
43    /// La clave es la identidad del blob y no su contenido: peniko ya garantiza
44    /// que dos `Blob` con el mismo id son el mismo buffer, y hashear los bytes
45    /// de cada imagen en cada frame sería exactamente el costo que este caché
46    /// existe para evitar.
47    imagenes: HashMap<u64, ImageId>,
48    ancho: u32,
49    alto: u32,
50}
51
52impl Pintor {
53    /// Un pintor que rinde a un destino de `ancho` x `alto` en `formato`.
54    ///
55    /// `formato` tiene que ser el de la textura contra la que se va a rendir —el
56    /// del swapchain, normalmente—, porque el `Renderer` compila sus pipelines
57    /// contra él en este mismo llamado. En el Mali eso es justamente lo que hay
58    /// que sobrevivir: es acá donde el driver decide si el proceso sigue vivo.
59    pub fn new(
60        device: &wgpu::Device,
61        backend: wgpu::Backend,
62        formato: wgpu::TextureFormat,
63        ancho: u32,
64        alto: u32,
65    ) -> Self {
66        let (ancho, alto) = (ancho.max(1), alto.max(1));
67        Self {
68            renderer: vello_hybrid::Renderer::new_with(
69                device,
70                &vello_hybrid::RenderTargetConfig {
71                    format: formato,
72                    width: ancho,
73                    height: alto,
74                },
75                ajustes_para(backend),
76            ),
77            recursos: vello_hybrid::Resources::new(),
78            gpu: vello_hybrid::Scene::new(dim(ancho), dim(alto)),
79            imagenes: HashMap::new(),
80            ancho,
81            alto,
82        }
83    }
84
85    /// Ancho del destino en píxeles.
86    pub fn ancho(&self) -> u32 {
87        self.ancho
88    }
89
90    /// Alto del destino en píxeles.
91    pub fn alto(&self) -> u32 {
92        self.alto
93    }
94
95    /// Ajusta el pintor a un destino de otro tamaño.
96    ///
97    /// **No recrea el `Renderer`, y eso es lo importante.** El `width`/`height`
98    /// del `RenderTargetConfig` sólo siembra un buffer de configuración al
99    /// construirlo; el tamaño de cada fotograma viaja aparte, en el `RenderSize`
100    /// que recibe `render()`. Así que redimensionar es recrear la `Scene` —CPU
101    /// pura— y nada más.
102    ///
103    /// Recrear el `Renderer` en cambio **recompilaría los pipelines**, que es
104    /// justo la operación que en el Mali del teléfono mata el proceso
105    /// (`cmpbe_v2_compile_multiple_shaders`, ver `../android/DIAGNOSTICO.md`).
106    /// Un rotar la pantalla no puede costar eso. De paso conserva el atlas de
107    /// imágenes, que si no habría que volver a subir entero por el bus.
108    ///
109    /// Sin cambio real no toca nada. La guarda no es higiene: reconfigurar por
110    /// gusto fue el bug de `WinitSurface::resize` que se llevó una noche entera
111    /// (41.335 reconfiguraciones en 13 s).
112    pub fn redimensionar(&mut self, ancho: u32, alto: u32) {
113        let (ancho, alto) = (ancho.max(1), alto.max(1));
114        if (ancho, alto) == (self.ancho, self.alto) {
115            return;
116        }
117        self.ancho = ancho;
118        self.alto = alto;
119        self.gpu = vello_hybrid::Scene::new(dim(ancho), dim(alto));
120    }
121
122    /// Reproduce la escena y la rinde sobre `vista`.
123    ///
124    /// Va en dos pasadas y no es un rodeo: la primera sube al atlas las imágenes
125    /// que aún no están —lo único que necesita `device`/`queue`/`encoder`— y la
126    /// segunda reproduce contra la `Scene`, que es CPU pura. Mezclarlas obligaría
127    /// a llevar los tres prestados por todo el reproductor para un caso que en
128    /// una UI se da un puñado de veces por frame y casi siempre pega en el caché.
129    ///
130    /// `vista` tiene que ser del formato y tamaño con los que se construyó el
131    /// pintor.
132    pub fn pintar(
133        &mut self,
134        escena: &Escena,
135        device: &wgpu::Device,
136        queue: &wgpu::Queue,
137        encoder: &mut wgpu::CommandEncoder,
138        vista: &wgpu::TextureView,
139        color_base: Color,
140    ) -> Result<(), RenderError> {
141        self.subir_imagenes(escena, device, queue, encoder);
142
143        self.gpu.reset();
144        self.pintar_base(color_base);
145        for orden in escena.ordenes() {
146            self.ejecutar(orden);
147        }
148
149        self.renderer.render(
150            &self.gpu,
151            &mut self.recursos,
152            device,
153            queue,
154            encoder,
155            &vello_hybrid::RenderSize {
156                width: self.ancho,
157                height: self.alto,
158            },
159            vista,
160            &vello_hybrid::TextureBindings::new(),
161        )
162    }
163
164    // -------------------------------------------------------------------------
165
166    /// Pinta `color_base` a sangre como primera orden de la escena.
167    ///
168    /// **Acá hubo un error mío que costó medio diagnóstico, y queda escrito para
169    /// que nadie lo rehaga.** La versión anterior hacía esto con un pase de
170    /// `wgpu` propio, sólo por su `LoadOp::Clear`, sobre esta premisa: que
171    /// `vello::Renderer` limpia su target con el `base_color` de los
172    /// `RenderParams` y el de `vello_hybrid` entra con `LoadOp::Load`, o sea
173    /// componiendo sobre lo que ya había.
174    ///
175    /// **La premisa es falsa.** `vello_hybrid` limpia el attachment por su
176    /// cuenta, así que pisaba el clear y el `color_base` **no llegaba nunca a
177    /// la pantalla**: donde la escena no pintaba quedaba negro, no el color
178    /// pedido. Y era invisible en la práctica porque el caso normal es que el
179    /// `color_base` coincida con el fondo que igual pinta la vista raíz — los
180    /// dos se ven idénticos hasta que se los hace distintos a propósito.
181    /// `el_color_base_pinta_donde_la_escena_no_llega` los hace distintos.
182    ///
183    /// El corolario también se cae: la objeción de que «un relleno transparente
184    /// no escribe nada y el contenido viejo sobrevive» no aplica, porque el
185    /// contenido viejo ya no sobrevive — lo borra hybrid. Por eso con alfa cero
186    /// no hay nada que hacer y esto se saltea.
187    fn pintar_base(&mut self, color: Color) {
188        if color.components[3] <= 0.0 {
189            return;
190        }
191        self.gpu.set_transform(Affine::IDENTITY);
192        self.gpu.reset_paint_transform();
193        self.gpu.set_paint(color);
194        self.gpu.fill_rect(&Rect::new(
195            0.0,
196            0.0,
197            f64::from(self.ancho),
198            f64::from(self.alto),
199        ));
200    }
201
202    /// Sube al atlas toda imagen de la escena que todavía no esté.
203    ///
204    /// Recorre tanto `draw_image` como los pinceles-imagen de `fill`/`stroke`:
205    /// son dos caminos distintos hasta la misma textura, y olvidar el segundo
206    /// dejaría el degradado-imagen dibujando transparente sin decir por qué.
207    fn subir_imagenes(
208        &mut self,
209        escena: &Escena,
210        device: &wgpu::Device,
211        queue: &wgpu::Queue,
212        encoder: &mut wgpu::CommandEncoder,
213    ) {
214        for orden in escena.ordenes() {
215            let datos = match orden {
216                Orden::Imagen { imagen, .. } => Some(&imagen.image),
217                Orden::Rellenar { pincel, .. } | Orden::Trazar { pincel, .. } => match pincel {
218                    Brush::Image(img) => Some(&img.image),
219                    _ => None,
220                },
221                _ => None,
222            };
223            let Some(datos) = datos else { continue };
224            if self.imagenes.contains_key(&datos.data.id()) {
225                continue;
226            }
227            let Some(pixmap) = pixmap_de(datos) else { continue };
228            let id = self
229                .renderer
230                .upload_image(&mut self.recursos, device, queue, encoder, &pixmap);
231            self.imagenes.insert(datos.data.id(), id);
232        }
233    }
234
235    fn ejecutar(&mut self, orden: &Orden) {
236        match orden {
237            Orden::Rellenar {
238                regla,
239                xf,
240                pincel,
241                xf_pincel,
242                forma,
243            } => {
244                self.gpu.set_fill_rule(*regla);
245                self.gpu.set_transform(*xf);
246                fijar_pincel(&mut self.gpu, &self.imagenes, pincel, *xf_pincel);
247                match forma {
248                    Forma::Rect(r) => self.gpu.fill_rect(r),
249                    otra => self.gpu.fill_path(&otra.a_camino()),
250                }
251            }
252            Orden::Trazar {
253                estilo,
254                xf,
255                pincel,
256                xf_pincel,
257                forma,
258            } => {
259                self.gpu.set_stroke(estilo.clone());
260                self.gpu.set_transform(*xf);
261                fijar_pincel(&mut self.gpu, &self.imagenes, pincel, *xf_pincel);
262                match forma {
263                    Forma::Rect(r) => self.gpu.stroke_rect(r),
264                    otra => self.gpu.stroke_path(&otra.a_camino()),
265                }
266            }
267            Orden::AbrirCapa {
268                regla,
269                mezcla,
270                alfa,
271                xf,
272                recorte,
273            } => {
274                self.gpu.set_fill_rule(*regla);
275                self.gpu.set_transform(*xf);
276                self.gpu.push_layer(
277                    Some(&recorte.a_camino()),
278                    Some(*mezcla),
279                    Some(*alfa),
280                    None,
281                    None,
282                );
283            }
284            Orden::AbrirMascaraLuminancia {
285                regla,
286                alfa,
287                xf,
288                recorte,
289            } => self.abrir_mascara_luminancia(*regla, *alfa, *xf, recorte),
290            Orden::CerrarCapa => self.gpu.pop_layer(),
291            Orden::Imagen { imagen, xf } => {
292                // Si la imagen no llegó al atlas —blob mal formado, o más grande
293                // de lo que el atlas admite— se saltea el dibujo en vez de pintar
294                // un rectángulo de basura o abortar el fotograma.
295                let Some(id) = self.imagenes.get(&imagen.image.data.id()).copied() else {
296                    return;
297                };
298                let (w, h) = (
299                    f64::from(imagen.image.width),
300                    f64::from(imagen.image.height),
301                );
302                self.gpu.set_transform(*xf);
303                self.gpu.set_paint(Image {
304                    image: ImageSource::opaque_id(id),
305                    sampler: imagen.sampler,
306                });
307                self.gpu.reset_paint_transform();
308                self.gpu.set_fill_rule(Fill::NonZero);
309                self.gpu.fill_rect(&Rect::new(0.0, 0.0, w, h));
310            }
311            Orden::Glifos {
312                fuente,
313                tamano,
314                xf,
315                pincel,
316                hint,
317                glifos,
318            } => {
319                // El pincel y el transform se fijan ANTES de abrir el builder:
320                // `glyph_run` presta la escena hasta que se consume.
321                self.gpu.set_transform(*xf);
322                fijar_pincel(&mut self.gpu, &self.imagenes, pincel, None);
323                self.gpu
324                    .glyph_run(&mut self.recursos, fuente)
325                    .font_size(*tamano)
326                    .hint(*hint)
327                    .fill_glyphs(glifos.iter().map(|g| glifo::Glyph {
328                        id: g.id,
329                        x: g.x,
330                        y: g.y,
331                    }));
332            }
333            Orden::RectDifuso {
334                xf,
335                rect,
336                color,
337                radio,
338                desvio,
339            } => {
340                self.gpu.set_transform(*xf);
341                self.gpu.set_paint(*color);
342                self.gpu.reset_paint_transform();
343                self.gpu
344                    .fill_blurred_rounded_rect(rect, *radio as f32, *desvio as f32);
345            }
346        }
347    }
348
349    /// Abre una capa enmascarada por luminancia.
350    ///
351    /// **vello_hybrid no soporta máscaras**: su `push_layer` lleva un
352    /// `unimplemented!()` literal cuando el parámetro `mask` viene con algo
353    /// (`scene.rs:921`, «Mask is unsupported»). Así que hay que dar la vuelta —y
354    /// la vuelta resulta ser exacta, no una aproximación.
355    ///
356    /// El razonamiento: la máscara que este método recibe no es una imagen
357    /// arbitraria, es **una forma**. El pintor CPU la materializa rasterizando
358    /// esa forma en blanco sobre negro y tomando la luminancia; eso da 1 adentro,
359    /// 0 afuera y cobertura parcial en el borde antialiaseado. Que es, dato por
360    /// dato, lo mismo que produce un recorte por esa misma forma. Se implementa
361    /// entonces como capa recortada, sin lienzo extra ni rasterizado en CPU.
362    ///
363    /// La equivalencia depende de que la máscara SIEMPRE nazca de una forma
364    /// blanca — hoy garantizado porque el único constructor es
365    /// `Escena::push_luminance_mask_layer`, que recibe un `impl Shape`. Si algún
366    /// día la `Escena` admite enmascarar con un degradado o una imagen, esto deja
367    /// de valer y hay que volver a la máscara de verdad.
368    fn abrir_mascara_luminancia(&mut self, regla: Fill, alfa: f32, xf: Affine, recorte: &Forma) {
369        self.gpu.set_fill_rule(regla);
370        self.gpu.set_transform(xf);
371        self.gpu
372            .push_layer(Some(&recorte.a_camino()), None, Some(alfa), None, None);
373    }
374}
375
376/// Traduce un pincel de peniko al `PaintType` de vello_common.
377///
378/// Sólido y degradado son el mismo tipo de un lado y del otro. La variante
379/// imagen no: peniko la lleva como bytes crudos y hybrid como un handle al
380/// atlas, así que se resuelve por el caché que llenó `subir_imagenes`. Si no
381/// está —imagen mal formada—, se cae a transparente en vez de abortar el frame.
382fn fijar_pincel(
383    gpu: &mut vello_hybrid::Scene,
384    imagenes: &HashMap<u64, ImageId>,
385    pincel: &Brush,
386    xf_pincel: Option<Affine>,
387) {
388    // La transformación del pincel es la que ancla un degradado a la caja que lo
389    // lleva en vez de al origen del mundo. vello la compone como
390    // `transform * brush_transform`, y hybrid hace lo mismo con su
391    // `paint_transform`, así que el mapeo es directo.
392    match xf_pincel {
393        Some(t) => gpu.set_paint_transform(t),
394        None => gpu.reset_paint_transform(),
395    }
396    match pincel {
397        Brush::Solid(c) => gpu.set_paint(*c),
398        Brush::Gradient(g) => gpu.set_paint(g.clone()),
399        Brush::Image(img) => match imagenes.get(&img.image.data.id()).copied() {
400            Some(id) => gpu.set_paint(Image {
401                image: ImageSource::opaque_id(id),
402                sampler: img.sampler,
403            }),
404            None => gpu.set_paint(llimphi_cpu::peniko::color::AlphaColor::<
405                llimphi_cpu::peniko::color::Srgb,
406            >::TRANSPARENT),
407        },
408    }
409}
410
411/// Los ajustes del renderer para un backend dado.
412///
413/// **Existe por un bug real, no por gusto.** El backend GLES de wgpu decide por
414/// *heurística* si una textura es `D2` o `D2Array`: cuando
415/// `depth_or_array_layers == 1` la trata como `D2` **aunque se haya creado como
416/// array**. El atlas de imágenes de vello_hybrid es un array, así que con una
417/// sola capa el muestreo queda desalineado y **las imágenes no se dibujan** —
418/// sin error de validación, sin warning, sin nada: salen transparentes y el
419/// síntoma es una UI sin iconos.
420///
421/// vello_hybrid conoce el problema y lo esquiva forzando dos capas, pero sólo
422/// bajo `#[cfg(target_arch = "wasm32")]`, porque asume que GLES es sinónimo de
423/// WebGL (su propio TODO se pregunta cómo detectar el backend en vez del
424/// target). En Android GLES es **nativo**, así que caemos del lado no cubierto.
425///
426/// Por eso el backend es un parámetro **obligatorio** de [`Pintor::new`] y no un
427/// `Default`: olvidarlo no rompe el build ni los tests en una máquina con
428/// Vulkan — se cobra callado, y sólo en el teléfono.
429/// **Lo que cuesta, medido, y una optimización PROBADA Y DESCARTADA.** El atlas
430/// default es de 4096x4096 por capa, o sea 67 MB de memoria GPU por capa;
431/// forzar la segunda llevó el `GL mtrack` del demo Android de 219 a 401 MB y los
432/// fps de 119 a 57, en un equipo donde el lowmemorykiller ya había matado
433/// procesos ajenos. Parece obvio bajar `atlas_size` a 2048 —dos capas sumarían
434/// 33 MB, menos que la única capa de antes—, y **no funciona**: probado en el
435/// teléfono, la imagen deja de dibujarse otra vez (0 px) y los fps caen a 8. Sin
436/// entender por qué, no se toca; el tamaño se deja en el default.
437fn ajustes_para(backend: wgpu::Backend) -> vello_hybrid::RenderSettings {
438    let mut ajustes = vello_hybrid::RenderSettings::default();
439    if backend == wgpu::Backend::Gl {
440        ajustes.atlas_config.initial_atlas_count =
441            ajustes.atlas_config.initial_atlas_count.max(2);
442    }
443    ajustes
444}
445
446/// Los límites con los que hay que pedir el `Device` para que vello_hybrid
447/// rasterice bien. **Usala siempre en vez de `Limits::default().using_resolution(...)`.**
448///
449/// `vello_hybrid` dimensiona su textura de alfas —donde vive la cobertura
450/// parcial de todo camino— con `max_texture_dimension_2d` de ancho, y la
451/// direcciona **por máscara y corrimiento**: el shader hace
452/// `tex_x = i & (ancho - 1)` y `tex_y = i >> alphas_tex_width_bits`, con
453/// `alphas_tex_width_bits = ancho.trailing_zeros()`. Las dos cuentas suponen que
454/// el ancho es **potencia de dos**, y nadie lo verifica.
455///
456/// El Mali-G57 del Redmi reporta **16383**. `16383.trailing_zeros()` es **0**,
457/// así que `tex_y = i`: cada texel se busca en una fila muy afuera de la textura
458/// y GLES devuelve ceros. Resultado: alfa 0 en toda tira de cobertura parcial, o
459/// sea **cero antialiasing** y cada forma perdiendo su primer tile de 4 px. Sin
460/// error de validación y sin warning — la escena se ve, sólo que cuantizada a
461/// bloques de 4x4 y con los bordes comidos.
462///
463/// Redondear el techo hacia abajo a la potencia de dos anterior lo arregla
464/// entero, medido en metal: el círculo de la regla pasó de 0 a 1.660 píxeles
465/// antialiaseados y los caminos volvieron a caer en la columna exacta que se
466/// pidió, sin costo de fps (119 antes y después). Ver
467/// `02_ruway/llimphi/android/DIAGNOSTICO.md` §F8.
468///
469/// En una GPU que ya reporta una potencia de dos —o sea, todas las de
470/// escritorio— esto no cambia nada.
471pub fn limites_seguros(adapter: &wgpu::Adapter) -> wgpu::Limits {
472    let mut limites = wgpu::Limits::default().using_resolution(adapter.limits());
473    limites.max_texture_dimension_2d = potencia_de_dos_hacia_abajo(limites.max_texture_dimension_2d);
474    limites
475}
476
477/// La mayor potencia de dos que no pasa a `v`. Con `v == 0` devuelve 1: pedir un
478/// techo de cero texeles no es una opción representable, y saturar hacia arriba
479/// es menos malo que devolver algo que hace `trailing_zeros() == 32`.
480pub(crate) fn potencia_de_dos_hacia_abajo(v: u32) -> u32 {
481    if v == 0 {
482        return 1;
483    }
484    1 << (u32::BITS - 1 - v.leading_zeros())
485}
486
487/// Los pixmaps de vello_common se miden en `u16`. Saturar en vez de truncar:
488/// una pantalla de más de 65.535 px no existe, pero un `as u16` a la ligera
489/// convertiría un ancho absurdo en uno chiquito y el bug saldría como «se
490/// dibuja en una esquina», que es de los caros de diagnosticar.
491fn dim(v: u32) -> u16 {
492    v.clamp(1, u32::from(u16::MAX)) as u16
493}
494
495/// Convierte los bytes de una imagen de peniko en un `Pixmap` premultiplicado,
496/// que es lo que el atlas sabe recibir.
497///
498/// Se apoya en `ImageSource::from_peniko_image_data`, que es el conversor de la
499/// propia vello_common —premultiplica y reordena canales—, en vez de repetirlo
500/// acá. Lo único que agrega esta función es la guarda de tamaño: ese conversor
501/// **paniquea** si la imagen pasa de `u16::MAX` en algún lado, y un blob raro no
502/// debe tumbar el proceso.
503fn pixmap_de(datos: &ImageData) -> Option<Arc<Pixmap>> {
504    if datos.width > u32::from(u16::MAX) || datos.height > u32::from(u16::MAX) {
505        return None;
506    }
507    if datos.width == 0 || datos.height == 0 {
508        return None;
509    }
510    match ImageSource::from_peniko_image_data(datos) {
511        ImageSource::Pixmap(p) => Some(p),
512        ImageSource::OpaqueId { .. } => None,
513    }
514}