//! El selector de formato de swapchain, certificado **sin GPU**. //! //! `elegir_formato` es puro: recibe la lista de formatos que ofrece una surface //! y el objetivo de color pedido, y devuelve con cuál configurar el swapchain. //! Por eso se puede probar contra listas inventadas —incluidas las que ninguna //! máquina de acá tiene, como un Vulkan con colorspace extendido— en vez de //! esperar a tener el hardware delante (Regla 8 de `CLAUDE.md`: evidencia de //! texto). //! //! Lo que estos tests fijan, en orden de importancia: //! //! 1. **Cero regresión**: sin pedir nada, la elección es la misma de siempre. //! 2. **Nunca un formato sRGB de hardware**, que lavaría la imagen aplicando la //! curva dos veces (la intermedia ya es lineal). //! 3. **Degradación**, no fallo: pedir 10 bits donde no hay devuelve 8. use llimphi_hal::{elegir_formato, ColorTarget}; use wgpu::TextureFormat as F; /// Lo que ofrece un compositor Wayland de escritorio normal (Mesa/Vulkan). const ESCRITORIO: &[F] = &[F::Bgra8Unorm, F::Bgra8UnormSrgb, F::Rgba8Unorm, F::Rgba8UnormSrgb]; /// Una surface con colorspace extendido: además de lo de siempre, 10 bits y /// float. Ninguna máquina de este repo la lista hoy; el selector igual tiene /// que saber qué hacer con ella. const AMPLIA: &[F] = &[ F::Bgra8Unorm, F::Rgba8Unorm, F::Rgb10a2Unorm, F::Rgba16Float, ]; #[test] fn el_default_elige_lo_de_siempre() { // El comportamiento histórico, palabra por palabra: el primer Bgra8Unorm o // Rgba8Unorm no-sRGB de la lista. assert_eq!(elegir_formato(ESCRITORIO, ColorTarget::Srgb8), Some(F::Bgra8Unorm)); assert_eq!(elegir_formato(AMPLIA, ColorTarget::Srgb8), Some(F::Bgra8Unorm)); // Y si el compositor sólo ofrece Rgba8 (orden invertido), ése. assert_eq!( elegir_formato(&[F::Rgba8UnormSrgb, F::Rgba8Unorm], ColorTarget::Srgb8), Some(F::Rgba8Unorm) ); } #[test] fn nunca_elige_un_formato_srgb_de_hardware() { // Una surface que SÓLO ofrece sRGB: no hay elección válida, y devolver // `None` es lo correcto — el caller cae al primero de la lista a sabiendas, // en vez de que el selector bendiga un formato que lava la imagen. assert_eq!( elegir_formato(&[F::Bgra8UnormSrgb, F::Rgba8UnormSrgb], ColorTarget::Srgb8), None ); for objetivo in [ColorTarget::Srgb8, ColorTarget::Rgb10, ColorTarget::Float16] { let elegido = elegir_formato(ESCRITORIO, objetivo).unwrap(); assert!( !format!("{elegido:?}").ends_with("Srgb"), "{objetivo:?} eligió {elegido:?}, que es sRGB de hardware" ); } } #[test] fn diez_bits_cuando_se_puede_y_ocho_cuando_no() { assert_eq!(elegir_formato(AMPLIA, ColorTarget::Rgb10), Some(F::Rgb10a2Unorm)); // El mismo pedido contra un escritorio normal degrada a 8 bits en vez de // fallar: pedir precisión que no hay no debe dejar la app sin ventana. assert_eq!(elegir_formato(ESCRITORIO, ColorTarget::Rgb10), Some(F::Bgra8Unorm)); } #[test] fn float16_degrada_por_toda_la_cadena() { assert_eq!(elegir_formato(AMPLIA, ColorTarget::Float16), Some(F::Rgba16Float)); // Sin float pero con 10 bits: el escalón intermedio, no el piso. assert_eq!( elegir_formato(&[F::Bgra8Unorm, F::Rgb10a2Unorm], ColorTarget::Float16), Some(F::Rgb10a2Unorm) ); // Sin ninguno de los dos: el piso. assert_eq!(elegir_formato(ESCRITORIO, ColorTarget::Float16), Some(F::Bgra8Unorm)); } #[test] fn una_surface_sin_formatos_no_paniquea() { // `get_capabilities` devuelve la lista vacía cuando la surface es // incompatible con el adapter. El selector no indexa: contesta `None` y el // caller decide (dos de los tres call sites devuelven un error legible). assert_eq!(elegir_formato(&[], ColorTarget::Srgb8), None); assert_eq!(elegir_formato(&[], ColorTarget::Float16), None); } #[test] fn el_default_del_enum_es_el_comportamiento_historico() { assert_eq!(ColorTarget::default(), ColorTarget::Srgb8); } // ── El informe: que la degradación deje de ser muda ─────────────────────── // // `elegir_formato` ya decidía bien; lo que no había era forma de saber qué // decidió. Pedir `LLIMPHI_COLOR=rgb10` en una surface de 8 bits daba una // ventana idéntica a la de siempre y ni una línea, así que el A/B en la // laptop comparaba dos ramas indistinguibles. `informe_formato` es la otra // mitad pura de la misma decisión, y se prueba igual: contra listas // inventadas, sin GPU. use llimphi_hal::informe_formato; #[test] fn el_informe_dice_concedido_cuando_el_objetivo_se_cumplio() { let linea = informe_formato(ColorTarget::Rgb10, Some(F::Rgb10a2Unorm), AMPLIA); assert!(linea.contains("rgb10"), "no nombra lo que se pidió: {linea}"); assert!(linea.contains("concedido"), "{linea}"); assert!(linea.contains("Rgb10a2Unorm"), "no nombra lo que se dio: {linea}"); assert!( !linea.contains("DEGRADADO"), "un objetivo cumplido no puede leerse como degradación: {linea}" ); } #[test] fn el_informe_grita_cuando_degrada_y_dice_contra_que() { // El caso real de esta laptop: se pide 10 bits, el compositor sólo ofrece 8. let elegido = elegir_formato(ESCRITORIO, ColorTarget::Rgb10); assert_eq!(elegido, Some(F::Bgra8Unorm), "premisa del caso"); let linea = informe_formato(ColorTarget::Rgb10, elegido, ESCRITORIO); assert!(linea.contains("DEGRADADO"), "{linea}"); assert!(linea.contains("Bgra8Unorm"), "no dice a qué degradó: {linea}"); assert!( linea.contains("Rgb10a2Unorm"), "no dice qué formato faltaba, que es el dato accionable: {linea}" ); } #[test] fn float16_que_para_en_diez_bits_tambien_es_degradacion() { // El escalón intermedio de la cadena SÍ es una degradación contra lo // pedido, aunque no sea el piso. Si el informe lo diera por bueno, un // pedido de HDR que se quedó en 10 bits pasaría por cumplido. let disponibles = &[F::Bgra8Unorm, F::Rgb10a2Unorm]; let elegido = elegir_formato(disponibles, ColorTarget::Float16); assert_eq!(elegido, Some(F::Rgb10a2Unorm), "premisa del caso"); let linea = informe_formato(ColorTarget::Float16, elegido, disponibles); assert!(linea.contains("DEGRADADO"), "{linea}"); assert!(linea.contains("float16"), "{linea}"); } #[test] fn ocho_bits_pedido_a_proposito_no_es_degradacion() { // Control negativo del control negativo: el default también se puede // pedir, y pedirlo y recibirlo no es una queja. let linea = informe_formato(ColorTarget::Srgb8, Some(F::Bgra8Unorm), ESCRITORIO); assert!(linea.contains("concedido"), "{linea}"); assert!(!linea.contains("DEGRADADO"), "{linea}"); } #[test] fn una_surface_sin_formatos_lo_dice_en_vez_de_mentir() { let linea = informe_formato(ColorTarget::Rgb10, None, &[]); assert!(linea.contains("no ofrece ningún formato"), "{linea}"); assert!(!linea.contains("concedido"), "{linea}"); }