gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-cpu/src/pruebas.rs Rust · 345 lines · 10.6 KB History Who touched what raw

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

1// =============================================================================
2//  pruebas :: certificación por PÍXELES, no por aserción
3// -----------------------------------------------------------------------------
4//  Todo lo que se afirma acá se mide contando píxeles del pixmap resultante y
5//  comparando números — nada de mirar PNGs (regla 8 del repo: renderizar y
6//  mirar cuesta tokens; la evidencia puede ser texto). Cada test imprime lo que
7//  midió cuando falla, así que un rojo dice QUÉ salió y no sólo que salió mal.
8// =============================================================================
9
10use alloc::vec;
11
12use vello_cpu::kurbo::{Affine, Circle, Rect, RoundedRect, Stroke};
13use vello_cpu::peniko::color::{AlphaColor, PremulRgba8, Srgb};
14use vello_cpu::peniko::{BlendMode, Fill};
15
16use crate::escena::Escena;
17use crate::lienzo::{volcar, FormatoPixel};
18use crate::pintor::Pintor;
19
20const ROJO: AlphaColor<Srgb> = AlphaColor::from_rgba8(255, 0, 0, 255);
21const VERDE: AlphaColor<Srgb> = AlphaColor::from_rgba8(0, 255, 0, 255);
22
23/// Pinta una escena sobre un lienzo de `w`x`h` y devuelve los píxeles.
24fn pintar(w: u16, h: u16, escena: &Escena) -> vello_cpu::Pixmap {
25    let mut pintor = Pintor::new(w, h);
26    let mut lienzo = pintor.lienzo_nuevo();
27    pintor.pintar(escena, &mut lienzo);
28    lienzo
29}
30
31fn en(p: &vello_cpu::Pixmap, x: u16, y: u16) -> PremulRgba8 {
32    p.data()[y as usize * p.width() as usize + x as usize]
33}
34
35/// Cuántos píxeles no son totalmente transparentes.
36fn cubiertos(p: &vello_cpu::Pixmap) -> usize {
37    p.data().iter().filter(|c| c.a > 0).count()
38}
39
40#[test]
41fn rellenar_rectangulo_da_los_pixeles_exactos() {
42    let mut e = Escena::new();
43    e.fill(
44        Fill::NonZero,
45        Affine::IDENTITY,
46        ROJO,
47        None,
48        &Rect::new(10.0, 10.0, 30.0, 20.0),
49    );
50    let p = pintar(64, 64, &e);
51
52    // 20x10 = 200 píxeles exactos: bordes rectos y alineados a la grilla, así
53    // que no hay antialiasing que ensucie la cuenta.
54    assert_eq!(cubiertos(&p), 200, "área cubierta");
55    let dentro = en(&p, 15, 15);
56    assert_eq!(
57        (dentro.r, dentro.g, dentro.b, dentro.a),
58        (255, 0, 0, 255),
59        "el interior debe ser rojo opaco"
60    );
61    assert_eq!(en(&p, 5, 5).a, 0, "fuera del rect no se toca nada");
62}
63
64#[test]
65fn la_esquina_redondeada_produce_antialiasing() {
66    let mut e = Escena::new();
67    e.fill(
68        Fill::NonZero,
69        Affine::IDENTITY,
70        ROJO,
71        None,
72        &RoundedRect::new(8.0, 8.0, 56.0, 56.0, 12.0),
73    );
74    let p = pintar(64, 64, &e);
75
76    // Un borde curvo rasterizado con AA deja píxeles a medio cubrir. Si el
77    // rasterizador cayera en un camino sin AA, este conteo sería CERO — que es
78    // justo la regresión que este test existe para atrapar.
79    let parciales = p.data().iter().filter(|c| c.a > 0 && c.a < 255).count();
80    assert!(
81        parciales > 20,
82        "esperaba antialiasing en las curvas, hubo {parciales} píxeles parciales"
83    );
84    // La esquina misma queda fuera del redondeo.
85    assert_eq!(en(&p, 8, 8).a, 0, "la esquina se recorta por el radio");
86    assert_eq!(en(&p, 32, 32).a, 255, "el centro está lleno");
87}
88
89#[test]
90fn la_capa_de_recorte_corta_lo_que_sobra() {
91    let mut e = Escena::new();
92    // Capa recortada a la mitad izquierda...
93    e.push_layer(
94        Fill::NonZero,
95        BlendMode::default(),
96        1.0,
97        Affine::IDENTITY,
98        &Rect::new(0.0, 0.0, 32.0, 64.0),
99    );
100    // ...y adentro, un relleno que cubre TODO el lienzo.
101    e.fill(
102        Fill::NonZero,
103        Affine::IDENTITY,
104        ROJO,
105        None,
106        &Rect::new(0.0, 0.0, 64.0, 64.0),
107    );
108    e.pop_layer();
109    let p = pintar(64, 64, &e);
110
111    assert_eq!(cubiertos(&p), 32 * 64, "sólo debe sobrevivir la mitad izquierda");
112    assert_eq!(en(&p, 10, 32).a, 255, "izquierda pintada");
113    assert_eq!(en(&p, 50, 32).a, 0, "derecha recortada");
114}
115
116#[test]
117fn la_capa_con_alfa_atenua_lo_de_adentro() {
118    let mut e = Escena::new();
119    e.push_layer(
120        Fill::NonZero,
121        BlendMode::default(),
122        0.5,
123        Affine::IDENTITY,
124        &Rect::new(0.0, 0.0, 64.0, 64.0),
125    );
126    e.fill(
127        Fill::NonZero,
128        Affine::IDENTITY,
129        ROJO,
130        None,
131        &Rect::new(0.0, 0.0, 64.0, 64.0),
132    );
133    e.pop_layer();
134    let p = pintar(64, 64, &e);
135
136    let c = en(&p, 32, 32);
137    assert!(
138        (120..=135).contains(&c.a),
139        "alfa 0,5 sobre opaco debería dar ~128, dio {}",
140        c.a
141    );
142}
143
144#[test]
145fn append_traslada_la_escena_hija() {
146    // Hija: un cuadrado de 10x10 en el origen.
147    let mut hija = Escena::new();
148    hija.fill(
149        Fill::NonZero,
150        Affine::IDENTITY,
151        VERDE,
152        None,
153        &Rect::new(0.0, 0.0, 10.0, 10.0),
154    );
155
156    let mut madre = Escena::new();
157    madre.append(&hija, Some(Affine::translate((20.0, 30.0))));
158    let p = pintar(64, 64, &madre);
159
160    assert_eq!(cubiertos(&p), 100, "el área no cambia al trasladar");
161    assert_eq!(en(&p, 25, 35).a, 255, "el cuadrado viajó a (20,30)");
162    assert_eq!(en(&p, 5, 5).a, 0, "y ya no está en el origen");
163}
164
165#[test]
166fn append_sin_transformacion_es_concatenar() {
167    let mut hija = Escena::new();
168    hija.fill(
169        Fill::NonZero,
170        Affine::IDENTITY,
171        VERDE,
172        None,
173        &Rect::new(0.0, 0.0, 10.0, 10.0),
174    );
175    let mut madre = Escena::new();
176    madre.fill(
177        Fill::NonZero,
178        Affine::IDENTITY,
179        ROJO,
180        None,
181        &Rect::new(20.0, 20.0, 30.0, 30.0),
182    );
183    madre.append(&hija, None);
184
185    assert_eq!(madre.len(), 2, "dos órdenes, sin envoltorios");
186    let p = pintar(64, 64, &madre);
187    assert_eq!(en(&p, 5, 5).g, 255, "la hija quedó donde estaba");
188    assert_eq!(en(&p, 25, 25).r, 255, "y la madre también");
189}
190
191#[test]
192fn append_compone_la_transformacion_por_izquierda() {
193    // La hija ya viene escalada x2; la madre la traslada. El orden importa: si
194    // se compusiera al revés, la traslación saldría escalada al doble.
195    let mut hija = Escena::new();
196    hija.fill(
197        Fill::NonZero,
198        Affine::scale(2.0),
199        VERDE,
200        None,
201        &Rect::new(0.0, 0.0, 5.0, 5.0),
202    );
203    let mut madre = Escena::new();
204    madre.append(&hija, Some(Affine::translate((10.0, 10.0))));
205    let p = pintar(64, 64, &madre);
206
207    // 5x5 escalado x2 = 10x10, trasladado a (10,10) => ocupa [10,20).
208    assert_eq!(cubiertos(&p), 100, "10x10 tras el escalado");
209    assert_eq!(en(&p, 11, 11).a, 255, "arranca en (10,10)");
210    assert_eq!(en(&p, 21, 21).a, 0, "y termina en (20,20)");
211}
212
213#[test]
214fn el_trazo_dibuja_contorno_y_no_relleno() {
215    let mut e = Escena::new();
216    e.stroke(
217        &Stroke::new(4.0),
218        Affine::IDENTITY,
219        ROJO,
220        None,
221        &Rect::new(16.0, 16.0, 48.0, 48.0),
222    );
223    let p = pintar(64, 64, &e);
224
225    assert_eq!(en(&p, 32, 32).a, 0, "el centro queda hueco");
226    assert_eq!(en(&p, 32, 16).a, 255, "el borde de arriba está trazado");
227}
228
229#[test]
230fn el_rect_difuso_degrada_hacia_afuera() {
231    let mut e = Escena::new();
232    e.draw_blurred_rounded_rect(
233        Affine::IDENTITY,
234        Rect::new(20.0, 20.0, 44.0, 44.0),
235        ROJO,
236        6.0,
237        5.0,
238    );
239    let p = pintar(64, 64, &e);
240
241    let centro = en(&p, 32, 32).a;
242    let borde = en(&p, 32, 19).a;
243    let lejos = en(&p, 32, 4).a;
244    assert!(centro > 200, "el centro de la sombra es sólido ({centro})");
245    assert!(
246        borde > 0 && borde < centro,
247        "el borde difumina: centro={centro} borde={borde}"
248    );
249    assert!(lejos < borde, "y se apaga con la distancia ({lejos})");
250}
251
252#[test]
253fn una_forma_arbitraria_se_aplana_a_camino() {
254    // Un círculo no es Rect ni RoundedRect: fuerza la rama `Forma::Camino`.
255    let mut e = Escena::new();
256    e.fill(
257        Fill::NonZero,
258        Affine::IDENTITY,
259        ROJO,
260        None,
261        &Circle::new((32.0, 32.0), 16.0),
262    );
263    let p = pintar(64, 64, &e);
264
265    // πr² = π*256 ≈ 804. Con AA el conteo de «algo cubierto» queda algo por
266    // encima; la banda es generosa a propósito, lo que se certifica es que
267    // rasterizó un disco y no un cuadrado (1024) ni nada (0).
268    let n = cubiertos(&p);
269    assert!(
270        (750..=950).contains(&n),
271        "un disco de r=16 debería cubrir ~804 px, cubrió {n}"
272    );
273    assert_eq!(en(&p, 32, 32).a, 255, "el centro del disco está lleno");
274    assert_eq!(en(&p, 2, 2).a, 0, "las esquinas no");
275}
276
277#[test]
278fn reset_vacia_la_escena() {
279    let mut e = Escena::new();
280    e.fill(
281        Fill::NonZero,
282        Affine::IDENTITY,
283        ROJO,
284        None,
285        &Rect::new(0.0, 0.0, 10.0, 10.0),
286    );
287    assert_eq!(e.len(), 1);
288    e.reset();
289    assert!(e.is_empty(), "reset deja la escena vacía");
290    assert_eq!(cubiertos(&pintar(32, 32, &e)), 0, "y no pinta nada");
291}
292
293#[test]
294fn el_volcado_respeta_stride_y_orden_de_canales() {
295    let mut e = Escena::new();
296    e.fill(
297        Fill::NonZero,
298        Affine::IDENTITY,
299        ROJO,
300        None,
301        &Rect::new(0.0, 0.0, 4.0, 4.0),
302    );
303    let p = pintar(4, 4, &e);
304
305    // Framebuffer más ancho que el pixmap: 8 px de stride para 4 de contenido.
306    // Es el caso real —el firmware alinea las filas— y el que produce la imagen
307    // en diagonal si uno confunde stride con ancho.
308    const STRIDE: usize = 8 * 4;
309    let mut fb = vec![0u8; STRIDE * 4];
310    let filas = volcar(&p, &mut fb, STRIDE, FormatoPixel::Bgra8);
311    assert_eq!(filas, 4, "cuatro filas volcadas");
312
313    // Primer píxel en BGRA: azul=0, verde=0, rojo=255.
314    assert_eq!(&fb[0..4], &[0, 0, 255, 255], "BGRA del rojo");
315    // La zona de relleno del stride (píxeles 4..8 de la fila 0) queda intacta.
316    assert_eq!(&fb[16..32], &[0u8; 16], "el relleno del stride no se pisa");
317    // Segunda fila arranca en STRIDE, no en 4*4.
318    assert_eq!(&fb[STRIDE..STRIDE + 4], &[0, 0, 255, 255], "fila 2 en su lugar");
319
320    // El mismo pixmap en RGBA invierte los dos extremos.
321    let mut fb2 = vec![0u8; STRIDE * 4];
322    volcar(&p, &mut fb2, STRIDE, FormatoPixel::Rgba8);
323    assert_eq!(&fb2[0..4], &[255, 0, 0, 255], "RGBA del rojo");
324}
325
326#[test]
327fn el_volcado_recorta_en_vez_de_entrar_en_panico() {
328    let mut e = Escena::new();
329    e.fill(
330        Fill::NonZero,
331        Affine::IDENTITY,
332        ROJO,
333        None,
334        &Rect::new(0.0, 0.0, 16.0, 16.0),
335    );
336    let p = pintar(16, 16, &e);
337
338    // Framebuffer de 8x8 para un pixmap de 16x16: no entra ni a lo ancho ni a
339    // lo alto. Debe copiar la intersección y seguir viviendo.
340    const STRIDE: usize = 8 * 4;
341    let mut fb = vec![0u8; STRIDE * 8];
342    let filas = volcar(&p, &mut fb, STRIDE, FormatoPixel::Bgra8);
343    assert_eq!(filas, 8, "se copian las filas que entran");
344    assert_eq!(&fb[0..4], &[0, 0, 255, 255], "y lo copiado es correcto");
345}