tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-hal/tests/formato.rs Rust · 162 lines · 6.9 KB History Who touched what raw

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

1//! El selector de formato de swapchain, certificado **sin GPU**.
2//!
3//! `elegir_formato` es puro: recibe la lista de formatos que ofrece una surface
4//! y el objetivo de color pedido, y devuelve con cuál configurar el swapchain.
5//! Por eso se puede probar contra listas inventadas —incluidas las que ninguna
6//! máquina de acá tiene, como un Vulkan con colorspace extendido— en vez de
7//! esperar a tener el hardware delante (Regla 8 de `CLAUDE.md`: evidencia de
8//! texto).
9//!
10//! Lo que estos tests fijan, en orden de importancia:
11//!
12//! 1. **Cero regresión**: sin pedir nada, la elección es la misma de siempre.
13//! 2. **Nunca un formato sRGB de hardware**, que lavaría la imagen aplicando la
14//!    curva dos veces (la intermedia ya es lineal).
15//! 3. **Degradación**, no fallo: pedir 10 bits donde no hay devuelve 8.
16
17use llimphi_hal::{elegir_formato, ColorTarget};
18use wgpu::TextureFormat as F;
19
20/// Lo que ofrece un compositor Wayland de escritorio normal (Mesa/Vulkan).
21const ESCRITORIO: &[F] = &[F::Bgra8Unorm, F::Bgra8UnormSrgb, F::Rgba8Unorm, F::Rgba8UnormSrgb];
22
23/// Una surface con colorspace extendido: además de lo de siempre, 10 bits y
24/// float. Ninguna máquina de este repo la lista hoy; el selector igual tiene
25/// que saber qué hacer con ella.
26const AMPLIA: &[F] = &[
27    F::Bgra8Unorm,
28    F::Rgba8Unorm,
29    F::Rgb10a2Unorm,
30    F::Rgba16Float,
31];
32
33#[test]
34fn el_default_elige_lo_de_siempre() {
35    // El comportamiento histórico, palabra por palabra: el primer Bgra8Unorm o
36    // Rgba8Unorm no-sRGB de la lista.
37    assert_eq!(elegir_formato(ESCRITORIO, ColorTarget::Srgb8), Some(F::Bgra8Unorm));
38    assert_eq!(elegir_formato(AMPLIA, ColorTarget::Srgb8), Some(F::Bgra8Unorm));
39    // Y si el compositor sólo ofrece Rgba8 (orden invertido), ése.
40    assert_eq!(
41        elegir_formato(&[F::Rgba8UnormSrgb, F::Rgba8Unorm], ColorTarget::Srgb8),
42        Some(F::Rgba8Unorm)
43    );
44}
45
46#[test]
47fn nunca_elige_un_formato_srgb_de_hardware() {
48    // Una surface que SÓLO ofrece sRGB: no hay elección válida, y devolver
49    // `None` es lo correcto — el caller cae al primero de la lista a sabiendas,
50    // en vez de que el selector bendiga un formato que lava la imagen.
51    assert_eq!(
52        elegir_formato(&[F::Bgra8UnormSrgb, F::Rgba8UnormSrgb], ColorTarget::Srgb8),
53        None
54    );
55    for objetivo in [ColorTarget::Srgb8, ColorTarget::Rgb10, ColorTarget::Float16] {
56        let elegido = elegir_formato(ESCRITORIO, objetivo).unwrap();
57        assert!(
58            !format!("{elegido:?}").ends_with("Srgb"),
59            "{objetivo:?} eligió {elegido:?}, que es sRGB de hardware"
60        );
61    }
62}
63
64#[test]
65fn diez_bits_cuando_se_puede_y_ocho_cuando_no() {
66    assert_eq!(elegir_formato(AMPLIA, ColorTarget::Rgb10), Some(F::Rgb10a2Unorm));
67    // El mismo pedido contra un escritorio normal degrada a 8 bits en vez de
68    // fallar: pedir precisión que no hay no debe dejar la app sin ventana.
69    assert_eq!(elegir_formato(ESCRITORIO, ColorTarget::Rgb10), Some(F::Bgra8Unorm));
70}
71
72#[test]
73fn float16_degrada_por_toda_la_cadena() {
74    assert_eq!(elegir_formato(AMPLIA, ColorTarget::Float16), Some(F::Rgba16Float));
75    // Sin float pero con 10 bits: el escalón intermedio, no el piso.
76    assert_eq!(
77        elegir_formato(&[F::Bgra8Unorm, F::Rgb10a2Unorm], ColorTarget::Float16),
78        Some(F::Rgb10a2Unorm)
79    );
80    // Sin ninguno de los dos: el piso.
81    assert_eq!(elegir_formato(ESCRITORIO, ColorTarget::Float16), Some(F::Bgra8Unorm));
82}
83
84#[test]
85fn una_surface_sin_formatos_no_paniquea() {
86    // `get_capabilities` devuelve la lista vacía cuando la surface es
87    // incompatible con el adapter. El selector no indexa: contesta `None` y el
88    // caller decide (dos de los tres call sites devuelven un error legible).
89    assert_eq!(elegir_formato(&[], ColorTarget::Srgb8), None);
90    assert_eq!(elegir_formato(&[], ColorTarget::Float16), None);
91}
92
93#[test]
94fn el_default_del_enum_es_el_comportamiento_historico() {
95    assert_eq!(ColorTarget::default(), ColorTarget::Srgb8);
96}
97
98// ── El informe: que la degradación deje de ser muda ───────────────────────
99//
100// `elegir_formato` ya decidía bien; lo que no había era forma de saber qué
101// decidió. Pedir `LLIMPHI_COLOR=rgb10` en una surface de 8 bits daba una
102// ventana idéntica a la de siempre y ni una línea, así que el A/B en la
103// laptop comparaba dos ramas indistinguibles. `informe_formato` es la otra
104// mitad pura de la misma decisión, y se prueba igual: contra listas
105// inventadas, sin GPU.
106
107use llimphi_hal::informe_formato;
108
109#[test]
110fn el_informe_dice_concedido_cuando_el_objetivo_se_cumplio() {
111    let linea = informe_formato(ColorTarget::Rgb10, Some(F::Rgb10a2Unorm), AMPLIA);
112    assert!(linea.contains("rgb10"), "no nombra lo que se pidió: {linea}");
113    assert!(linea.contains("concedido"), "{linea}");
114    assert!(linea.contains("Rgb10a2Unorm"), "no nombra lo que se dio: {linea}");
115    assert!(
116        !linea.contains("DEGRADADO"),
117        "un objetivo cumplido no puede leerse como degradación: {linea}"
118    );
119}
120
121#[test]
122fn el_informe_grita_cuando_degrada_y_dice_contra_que() {
123    // El caso real de esta laptop: se pide 10 bits, el compositor sólo ofrece 8.
124    let elegido = elegir_formato(ESCRITORIO, ColorTarget::Rgb10);
125    assert_eq!(elegido, Some(F::Bgra8Unorm), "premisa del caso");
126    let linea = informe_formato(ColorTarget::Rgb10, elegido, ESCRITORIO);
127    assert!(linea.contains("DEGRADADO"), "{linea}");
128    assert!(linea.contains("Bgra8Unorm"), "no dice a qué degradó: {linea}");
129    assert!(
130        linea.contains("Rgb10a2Unorm"),
131        "no dice qué formato faltaba, que es el dato accionable: {linea}"
132    );
133}
134
135#[test]
136fn float16_que_para_en_diez_bits_tambien_es_degradacion() {
137    // El escalón intermedio de la cadena SÍ es una degradación contra lo
138    // pedido, aunque no sea el piso. Si el informe lo diera por bueno, un
139    // pedido de HDR que se quedó en 10 bits pasaría por cumplido.
140    let disponibles = &[F::Bgra8Unorm, F::Rgb10a2Unorm];
141    let elegido = elegir_formato(disponibles, ColorTarget::Float16);
142    assert_eq!(elegido, Some(F::Rgb10a2Unorm), "premisa del caso");
143    let linea = informe_formato(ColorTarget::Float16, elegido, disponibles);
144    assert!(linea.contains("DEGRADADO"), "{linea}");
145    assert!(linea.contains("float16"), "{linea}");
146}
147
148#[test]
149fn ocho_bits_pedido_a_proposito_no_es_degradacion() {
150    // Control negativo del control negativo: el default también se puede
151    // pedir, y pedirlo y recibirlo no es una queja.
152    let linea = informe_formato(ColorTarget::Srgb8, Some(F::Bgra8Unorm), ESCRITORIO);
153    assert!(linea.contains("concedido"), "{linea}");
154    assert!(!linea.contains("DEGRADADO"), "{linea}");
155}
156
157#[test]
158fn una_surface_sin_formatos_lo_dice_en_vez_de_mentir() {
159    let linea = informe_formato(ColorTarget::Rgb10, None, &[]);
160    assert!(linea.contains("no ofrece ningún formato"), "{linea}");
161    assert!(!linea.contains("concedido"), "{linea}");
162}