tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
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}