tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-cpu/src/lienzo.rs Rust · 100 siq'ikuna · 4.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1// =============================================================================
2//  lienzo :: del `Pixmap` al framebuffer de verdad
3// -----------------------------------------------------------------------------
4//  El último tramo, y el más tonto de todos — pero el que decide si se ve algo.
5//  `vello_cpu` entrega un `Pixmap` de `PremulRgba8` COMPACTO (ancho*alto, sin
6//  relleno). Un framebuffer real casi nunca es así: tiene un `stride` mayor que
7//  el ancho visible (la GPU alinea cada fila) y el orden de canales lo dicta el
8//  firmware, no nosotros — el GOP de UEFI reporta `Bgr` tan seguido como `Rgb`.
9//
10//  Por eso el volcado no es un `copy_from_slice`: es fila por fila, con
11//  conversión de canal. Se hace en el orden de memoria del destino para que el
12//  acceso sea secuencial; en un framebuffer sin caché de escritura combinada
13//  (el WC que el kernel programa en el PAT) saltar de acá para allá se paga muy
14//  caro.
15// =============================================================================
16
17use vello_cpu::Pixmap;
18
19/// Cómo espera los canales el framebuffer de destino.
20///
21/// Sólo los dos ordenamientos de 32 bits que aparecen en la práctica: es lo que
22/// reporta el GOP de UEFI (`PixelFormat::{Rgb, Bgr}`) y lo que ofrece virtio-gpu.
23/// Los modos de 24 bits y los paletizados no se contemplan — ningún camino de
24/// wawa los produce.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum FormatoPixel {
27    /// Byte 0 = azul, 1 = verde, 2 = rojo, 3 = ignorado. Lo más común en x86.
28    Bgra8,
29    /// Byte 0 = rojo, 1 = verde, 2 = azul, 3 = ignorado.
30    Rgba8,
31}
32
33/// Vuelca un `Pixmap` sobre un framebuffer crudo.
34///
35/// - `destino`: los bytes del framebuffer, tal como los mapeó el kernel.
36/// - `stride_bytes`: cuántos bytes hay de una fila a la siguiente. **No** es
37///   `ancho * 4`: el firmware suele pedir más. Pasarlo mal produce la imagen
38///   inclinada en diagonal, que es el síntoma clásico de confundir stride con
39///   ancho.
40/// - `formato`: el orden de canales del destino.
41///
42/// Recorta a lo que entre: si el pixmap es más grande que el framebuffer, se
43/// copia la intersección en vez de entrar en pánico. Un kernel no debe morir
44/// porque una ventana quedó más ancha que la pantalla.
45///
46/// **PISA, no compone.** El alfa del pixmap se descarta y el destino sale
47/// siempre opaco, así que un píxel que la escena dejó transparente se escribe
48/// como NEGRO. La consecuencia es la regla que el llamador tiene que respetar:
49/// *un pixmap no puede ser más grande que la escena que pintó adentro* — el
50/// sobrante no queda «como estaba», queda negro. Pasó de verdad: el cromo de
51/// arranque de wawa hacía una cáscara del ancho de la pantalla para un fixture
52/// de 320 px y dejaba un rectángulo negro de 960x72 sobre el escritorio, toda
53/// la sesión, sin que ninguna estadística lo dijera (los píxeles con `a == 0`
54/// no entran en ningún conteo del kernel). Lo certifica
55/// `el_volcado_pisa_con_negro_lo_que_la_escena_no_cubrio`.
56///
57/// Devuelve cuántas filas se copiaron de verdad.
58pub fn volcar(
59    pixmap: &Pixmap,
60    destino: &mut [u8],
61    stride_bytes: usize,
62    formato: FormatoPixel,
63) -> usize {
64    let ancho = pixmap.width() as usize;
65    let alto = pixmap.height() as usize;
66    if ancho == 0 || alto == 0 || stride_bytes < 4 {
67        return 0;
68    }
69
70    let origen = pixmap.data();
71    // Cuántos píxeles de la fila caben en el destino, y cuántas filas hay.
72    let px_por_fila = ancho.min(stride_bytes / 4);
73    let filas = alto.min(destino.len() / stride_bytes);
74
75    for y in 0..filas {
76        let fila_orig = &origen[y * ancho..y * ancho + px_por_fila];
77        let base = y * stride_bytes;
78        let fila_dest = &mut destino[base..base + px_por_fila * 4];
79        for (px, dest) in fila_orig.iter().zip(fila_dest.chunks_exact_mut(4)) {
80            // El pixmap ya viene premultiplicado y el framebuffer es opaco,
81            // así que el alfa se descarta. OJO: eso vale mientras el pixmap
82            // CUBRA lo que vuelca; donde no cubrió, esto escribe negro opaco
83            // sobre lo que hubiera debajo (ver el doc de la función).
84            match formato {
85                FormatoPixel::Bgra8 => {
86                    dest[0] = px.b;
87                    dest[1] = px.g;
88                    dest[2] = px.r;
89                }
90                FormatoPixel::Rgba8 => {
91                    dest[0] = px.r;
92                    dest[1] = px.g;
93                    dest[2] = px.b;
94                }
95            }
96            dest[3] = 255;
97        }
98    }
99    filas
100}