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//! 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}