//! llimphi-raster — Brocha Matemática. //! //! Traduce primitivas vectoriales (líneas, curvas de Bézier, texto) a píxeles y //! las pinta sobre un [`llimphi_hal::Frame`]. Punto de entrada: [`Renderer`]. //! //! # Dos backends detrás de la misma puerta //! //! | feature | rasterizador | cómo | //! |---------|--------------|------| //! | (default) | `vello` | compute shaders, todo en GPU | //! | `hybrid` | `vello_hybrid` vía `llimphi-hybrid` | CPU+GPU, **sin compute** | //! //! `Renderer` tiene **la misma API en los dos** —`new`, `render`, //! `render_to_view`— así que `llimphi-ui` no sabe con cuál está hablando. Lo //! único que cambia de cara afuera es el tipo de escena, y para eso está el //! alias [`Escena`]: `vello::Scene` en un caso, la grabadora de `llimphi-cpu` en //! el otro. Es el mismo truco con el que `llimphi-text` conmuta su `Lienzo` //! entre tres backends, y a propósito: quien pinta ya está escrito contra un //! alias, no contra un tipo concreto. //! //! **`hybrid` no es una preferencia de gusto.** Es el único camino a píxeles en //! GPUs cuyo compilador de shaders no digiere el compute de vello — el Mali-G57 //! del Redmi de pruebas pierde el device en el primer frame por Vulkan y aborta //! por GLES, y con `hybrid` + `Backends::GL` pinta a 119 fps. La matriz completa //! está en `02_ruway/llimphi/android/DIAGNOSTICO.md`. //! //! Las dos features son **excluyentes** por construcción: eligen escenas de //! tipos distintos. Encender `hybrid` obliga además a compilar `llimphi-text` y //! `llimphi-compositor` en su perfil `cpu`, que es el que pinta en una `Escena`. use llimphi_hal::{Frame, Hal}; pub use vello; pub use vello::kurbo; pub use vello::peniko; #[cfg(feature = "hybrid")] pub use llimphi_hybrid; #[cfg(feature = "hybrid")] pub use llimphi_hybrid::vello_hybrid; /// El tipo de escena que este rasterizador consume. /// /// Existe para que los consumidores —`llimphi-ui`, sobre todo— no nombren /// `vello::Scene` a mano y queden atados a un backend. Ver el encabezado del /// crate. #[cfg(not(feature = "hybrid"))] pub type Escena = vello::Scene; /// El tipo de escena que este rasterizador consume: la **grabadora** de /// `llimphi-cpu`, que anota las órdenes en vez de codificarlas para la GPU. /// /// Expone los mismos métodos que `vello::Scene` con las mismas firmas, así que /// quien pinta no nota el cambio. #[cfg(feature = "hybrid")] pub type Escena = llimphi_hybrid::Escena; /// Deja dicho, una vez por `Renderer`, **con qué se está pintando de verdad**: /// el rasterizador (que viaja compilado en el binario) y el backend de wgpu que /// terminó eligiéndose (que se decide al arrancar). /// /// Existe por la Regla 8: los dos datos que hacen falta para leer un cuadro /// negro son justo los dos que ningún pantallazo muestra, y en un teléfono no se /// pueden preguntar de afuera. Sale por `avisar!`, que en Android va a `logcat`. /// El backend se lee del **adapter**, no de lo que se pidió: `PRIMARY` con /// fallback puede terminar en GL sin que nadie lo haya nombrado. fn anunciar(hal: &Hal, rasterizador: &str) { let info = hal.adapter.get_info(); llimphi_hal::avisar!( "llimphi-raster: {rasterizador} sobre {:?} · {} ({:?})", info.backend, info.name, info.device_type ); } pub mod gpu; pub mod gpu_atlas; pub mod huella; pub use huella::huella_escena; pub use gpu::{ units_per_chunk, GpuBatch, GpuPipelines, DISC_INSTANCE_STRIDE, LINE_INSTANCE_STRIDE, QUAD_INSTANCE_STRIDE, RECT_INSTANCE_STRIDE, TRI_VERTEX_STRIDE, }; pub use gpu_atlas::{AtlasTextura, FORMATO_ATLAS}; /// Errores del rasterizador. #[derive(Debug)] pub enum RasterError { Init(String), Render(String), } impl std::fmt::Display for RasterError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Init(s) => write!(f, "vello init: {s}"), Self::Render(s) => write!(f, "vello render: {s}"), } } } impl std::error::Error for RasterError {} /// Rasterizador vectorial. Una instancia por surface (porque vello cachea /// resources contra un `surface_format` específico). #[cfg(not(feature = "hybrid"))] pub struct Renderer { inner: vello::Renderer, /// Ids de blob que **sabemos subidos** al atlas de imágenes de vello. /// /// Existe por un agujero de vello 0.9 que deja las imágenes en blanco sin /// error ni log — ver [`Renderer::revalidar_atlas`]. Es un `Vec` y no un /// `HashSet` a propósito: son unas pocas decenas de ids (las miniaturas /// visibles), y a ese tamaño el barrido lineal gana y no asigna. subidas: Vec, } #[cfg(not(feature = "hybrid"))] impl Renderer { /// Inicializa el rasterizador. Vello acepta cualquier textura compatible /// (Rgba8Unorm / Bgra8Unorm) en `render`, así que no se fija un formato /// en construcción. /// /// **`antialiasing_support`**: pedimos `area` solamente, no `all()`. /// `area` es el único método que `render()` usa (`AaConfig::Area` /// fijo). Pedir `all()` haría a vello compilar también pipelines /// para `msaa8` y `msaa16` que nunca se invocan — en Mali-G57 eso /// triplica el cold-start (medido: 3.7s vs ~1.2s). Si alguna app /// futura necesita MSAA, agregamos un constructor explícito. /// /// **`num_init_threads: None`**: vello paraleliza la compilación /// de shaders en `None` → todos los CPU cores. Mali-G57 viene en /// SoCs octa-core ARM; con 1 thread tardamos 2.0s, con 8 esperamos /// ~400-600ms. La compilación de shaders es 100% CPU (Rust → /// SPIR-V), el GPU no participa, así que multi-thread escala /// casi linealmente hasta saturar el queue del Naga compiler. pub fn new(hal: &Hal) -> Result { anunciar(hal, "vello (compute)"); let inner = vello::Renderer::new( &hal.device, vello::RendererOptions { use_cpu: false, antialiasing_support: vello::AaSupport { area: true, msaa8: false, msaa16: false, }, num_init_threads: None, pipeline_cache: None, }, ) .map_err(|e| RasterError::Init(e.to_string()))?; Ok(Self { inner, subidas: Vec::new() }) } /// Repara, **antes de rasterizar**, un agujero de vello 0.9 que deja las /// imágenes en blanco sin error, sin warning y sin log. /// /// La mecánica, en la fuente de vello: /// /// 1. `vello_encoding/src/resolve.rs:189` — una escena **sin patches** /// (ni imágenes, ni gradientes, ni texto) toma un atajo y devuelve /// `Images::default()`, o sea un atlas de **0×0**. /// 2. `vello/src/render.rs:166` — como ese 0×0 no coincide con el atlas /// persistente (1024×1024), vello lo trata como un cambio de tamaño y /// **libera la textura**: `recording.free_image(*proxy)`. /// 3. Su `ImageCache`, en cambio, no se entera de nada: sus residentes /// siguen marcados `dirty = false`, o sea «ya subidos». En la escena /// siguiente el atlas se re-crea vacío y **no se sube nada**. /// /// Resultado: toda imagen dibujada después queda transparente y se ve lo /// que haya debajo. En `media` lo de debajo es la placa `rgb(16,20,28)` de /// `tooth_thumb` — las miniaturas negras que sergio reportó cinco veces /// entre el 2026-07-29 y el 2026-08-11. /// /// Por qué le pega a `media` y no a las demás apps: hace falta que en el /// **mismo** `Renderer` se alternen escenas con y sin patches. media pinta /// cinco escenas por cuadro (árbol principal, capa `over` —donde vive el /// panel con las miniaturas—, y tres chicas) y alguna es de puros rellenos. /// Una app de una sola escena no lo toca nunca, y por eso ni la sonda de /// miniaturas ni el test de techo del atlas lo habían visto: los dos /// rasterizaban **una** escena por cuadro. /// /// El arreglo es contarle a vello lo que su propio caché ya no sabe: /// llevamos la lista de blobs que dimos por subidos, la **olvidamos** cuando /// pasa una escena sin patches (que es cuando la textura se libera), y /// volvemos a marcar sucia toda imagen que reaparezca. `dirty` es /// exactamente el bit que decide si se re-sube. fn revalidar_atlas(&mut self, scene: &vello::Scene) { let patches = &scene.encoding().resources.patches; if patches.is_empty() { // Este render va a liberar la textura del atlas: lo que creíamos // subido deja de estarlo. self.subidas.clear(); return; } for p in patches { let vello_encoding::Patch::Image { image, .. } = p else { continue; }; let id = image.data.id(); if !self.subidas.contains(&id) { self.subidas.push(id); // El nombre habla de overrides, pero lo único que hace es poner // `dirty = true` en el residente del atlas — que es justo el bit // que perdimos. Sobre una imagen que vello todavía no conoce es // un no-op. self.inner.mark_override_image_dirty(image); } } } /// Renderiza `scene` sobre `frame` limpiando con `base_color`. AA fija /// en area-sampling (precisión Δ < 10⁻⁹ rad del SDD). pub fn render( &mut self, hal: &Hal, scene: &vello::Scene, frame: &Frame, base_color: peniko::Color, ) -> Result<(), RasterError> { let (width, height) = frame.size(); self.render_to_view(hal, scene, frame.view(), width, height, base_color) } /// Como [`render`](Self::render) pero contra una vista de textura /// explícita (mismo formato/tamaño que la intermedia). Lo usa el /// compositor de overlay de `llimphi-ui` para rasterizar la capa de /// overlay sobre fondo transparente en su propia textura. Ojo: /// `render_to_texture` **limpia** el target con `base_color` y escribe /// todos los píxeles — no compone sobre contenido previo. pub fn render_to_view( &mut self, hal: &Hal, scene: &vello::Scene, view: &llimphi_hal::wgpu::TextureView, width: u32, height: u32, base_color: peniko::Color, ) -> Result<(), RasterError> { self.revalidar_atlas(scene); self.inner .render_to_texture( &hal.device, &hal.queue, scene, view, &vello::RenderParams { base_color, width, height, antialiasing_method: vello::AaConfig::Area, }, ) .map_err(|e| RasterError::Render(e.to_string())) } } // ============================================================================= // Backend `hybrid` — el mismo Renderer, sin compute shaders // ----------------------------------------------------------------------------- // Gemelo del de arriba: MISMOS tres métodos, mismas firmas salvo el tipo de // escena (que igual se nombra `Escena` de los dos lados). Quien lo usa no se // entera de cuál compiló. // // Las diferencias están todas escondidas acá adentro y son tres: // // · vello arma su propio encoder y hace submit por dentro de // `render_to_texture`. `llimphi-hybrid` recibe el encoder, así que este // envoltorio lo crea y lo manda — para que la simetría de la API no sea // una mentira sobre quién sincroniza. // · el destino es SIEMPRE `llimphi_hal::INTERMEDIATE_FORMAT`, no el del // swapchain: el rasterizador nunca ve la textura de presentación (ver el // doc de esa constante). Por eso el pintor se construye una vez y no hay // que recompilar pipelines por formato. // · el tamaño se ajusta por fotograma con `redimensionar`, que sólo recrea // la escena y NO el renderer. Ver el porqué en `llimphi_hybrid::Pintor`. // ============================================================================= /// Rasterizador vectorial sobre `vello_hybrid` — CPU+GPU, sin compute shaders. #[cfg(feature = "hybrid")] pub struct Renderer { pintor: llimphi_hybrid::Pintor, } #[cfg(feature = "hybrid")] impl Renderer { /// Inicializa el rasterizador. /// /// Arranca en 1x1 porque el tamaño real recién se conoce en el primer /// fotograma, y eso no cuesta nada: redimensionar no recompila pipelines /// (sólo recrea la escena, que es CPU). Lo caro —compilar los shaders del /// driver— pasa **acá**, una sola vez, que es exactamente lo que se quiere /// en una GPU móvil. pub fn new(hal: &Hal) -> Result { anunciar(hal, "vello_hybrid (sin compute)"); Ok(Self { pintor: llimphi_hybrid::Pintor::new( &hal.device, // El backend real, no el que se pidió: `Backends::PRIMARY` con // fallback puede terminar en GL sin que nadie lo declare, y de // eso depende la config del atlas de imágenes. hal.adapter.get_info().backend, llimphi_hal::INTERMEDIATE_FORMAT, 1, 1, ), }) } /// Renderiza `escena` sobre `frame` limpiando con `base_color`. pub fn render( &mut self, hal: &Hal, escena: &Escena, frame: &Frame, base_color: peniko::Color, ) -> Result<(), RasterError> { let (width, height) = frame.size(); self.render_to_view(hal, escena, frame.view(), width, height, base_color) } /// Como [`render`](Self::render) pero contra una vista de textura explícita. /// Limpia con `base_color` y compone la escena encima — igual que el camino /// vello, incluido el caso `TRANSPARENT` del overlay. pub fn render_to_view( &mut self, hal: &Hal, escena: &Escena, view: &llimphi_hal::wgpu::TextureView, width: u32, height: u32, base_color: peniko::Color, ) -> Result<(), RasterError> { self.pintor.redimensionar(width, height); let mut encoder = hal.device .create_command_encoder(&llimphi_hal::wgpu::CommandEncoderDescriptor { label: Some("llimphi-raster-hybrid"), }); self.pintor .pintar(escena, &hal.device, &hal.queue, &mut encoder, view, base_color) .map_err(|e| RasterError::Render(format!("{e:?}")))?; hal.queue.submit(core::iter::once(encoder.finish())); Ok(()) } }