Entre enero de 2022 y marzo de 2026, Ansel incorporó 297 commits (sin contar las fusiones) relacionados con la cuadrícula de la mesa de luz, sus miniaturas, y su pipeline de renderizado y cacheado. Intenté apañármelas con el desaliñado código de la mesa de luz de Darktable, solo desengrasado, durante todo el tiempo que pude, pero por desgracia era pura deuda técnica y era dolorosamente lento.

En efecto, Darktable “gestiona” la cutrez de su mesa de luz reduciendo su tamaño: los paneles laterales izquierdo y derecho ocupan mucha superficie de pantalla, lo que deja aún menos área para que la mesa de luz se repinte. Puesto que Ansel eliminó el panel lateral derecho, fusionando su contenido con el izquierdo y con el menú global, había más superficie que pintar, más trabajo de CPU que hacer, y el pésimo diseño de la mesa de luz se volvió todavía más perjudicial.

Ese proyecto ya está completo. Este artículo es un mapa de lo que cambió, lo que se eliminó, lo que se rediseñó, cómo funciona el nuevo comportamiento y qué ganan realmente los usuarios con ello.

Una breve historia de mal diseño

Para entender por qué la reescritura de 2022-2026 acabó tocando tantos archivos, conviene mirar un paso más atrás.

La vista de la mesa de luz, src/views/lighttable.c, llevaba más de una década acumulando responsabilidades. Empezó como el lugar que manejaba la cuadrícula del gestor de archivos y el zoom básico, luego fue absorbiendo gradualmente la vista previa completa, la vista previa fija, el culling, la memoria del estado de los paneles, la agrupación, la reordenación por arrastrar y soltar, la navegación con teclado, las políticas de superposición, la coordinación de la tira de película y un montón de enrutamiento de atajos. Si miras el historial anterior a 2022, muchos commits tratan simplemente de evitar que estos se peleen entre sí: saltos de desplazamiento al hacer zoom, errores de desplazamiento en la vista previa, la selección que no se mantenía sincronizada, el culling y la vista previa reentrando en un estado medio roto, la visibilidad de los paneles que no se restauraba correctamente, bordes de grupo con fallos visuales, y valoraciones o etiquetas de color que movían la cuadrícula de forma inesperada.

Así que esto muestra una incapacidad total para llevar el control del ciclo de vida de una imagen cacheada (o de cualquier dato en ese maldito software, ya puestos), sintiéndose al mismo tiempo completamente cómodo y relajado con ello. Aborqué este problema simplificando la asincronía en segundo plano, pero otra opción habría sido usar una señal. No consigo superar el hecho de que los 3 tipos que trabajaron en esto durante 10 años llevan escribiendo código C el doble de tiempo que yo. En 2018, yo ni siquiera sabía qué era GLib.

Por si te interesa, la manera en que lo abordé es:

  • los widgets de miniatura mantienen una variable de estado que representa la validez de su propia imagen,
  • cuando GTK quiere redibujar la miniatura, si la variable de estado dice “inválida”, el widget de miniatura genera un hilo en segundo plano que computa un pipeline para crear la imagen que falta,
  • cuando el hilo en segundo plano retorna, coloca la imagen de salida en la miniatura que lo invocó, por puntero, en una superficie (cacheada), actualiza la variable de estado a “válida”, y luego envía un evento de redibujado en cola de GTK sobre el widget (no sobre toda la cuadrícula),
  • si el widget es invisible, no ocurre nada (GTK no encola eventos de redibujado para widgets invisibles). La próxima vez que GTK quiera redibujar la imagen, usa la superficie cacheada de dentro del widget,
  • los cambios de historial y metadatos envían una señal de “información de imagen cambiada” que publica el ID de la imagen. El thumbtable tiene un manejador de señal escuchando eso, que encuentra la miniatura por ID en la cuadrícula (a partir de una tabla hash, así que O(N)) y reinicia su variable de estado a “inválida”. De nuevo, si el cambio afecta a 100 imágenes y todas ellas son invisibles, no ocurre nada.

La arquitectura parece más compleja, pero en realidad es menos código y es sencillamente robusta: no hay conjeturas.

Una reescritura de la era COVID intentó refactorizar eso en src/dtgtk/thumbtable.c, que pronto pasó a formar parte de la misma deriva. Una vez que apareció la abstracción dedicada del thumbtable, se convirtió en el controlador de tráfico entre el estado de la colección, el desplazamiento, los desplazamientos (offsets), las imágenes activas, la navegación con teclado, el arrastrar y soltar, el desplazamiento suave, la visibilidad de las superposiciones, la disposición con zoom, la sincronización del culling y la sincronización de la tira de película, reinventando muchas funciones nativas de GTK de una manera peor e incompleta (la típica manera de Darktable). El historial anterior a 2022 está lleno de commits que arreglan el paso del ratón por encima tras el desplazamiento, recalculan filas y offsets, mantienen visibles las imágenes seleccionadas tras cambios de colección, evitan la lentitud cuadrática con Inicio/Fin, arreglan la alineación con zoom, hacen que las imágenes ocultas o colapsadas no rompan la lógica de imagen activa, y limitan el número de señales emitidas porque se estaban encadenando demasiadas actualizaciones independientes. En otras palabras, thumbtable.c ya se había convertido en el lugar donde la lógica de vista, el tiempo de vida de las miniaturas y la navegación de colección se veían forzados a encontrarse, mucho antes de que empezara la reescritura de 2022.

La refactorización del thumbtable hizo aparecer un archivo nuevo, cuando el viejo código de la mesa de luz y de la tira de película empezó a factorizarse en un widget de miniatura común y un thumbtable como objeto base de todo aquello: src/dtgtk/thumbnail.c. Esa era la dirección correcta en principio: en lugar de dibujar las miniaturas por separado en varios sitios, un único widget podía centralizar la activación, la selección, las superposiciones, las estrellas, las marcas de rechazo, el arrastrar y soltar, los bordes de grupo y el comportamiento con zoom. Pero siguió un camino similar muy rápidamente: el historial anterior a 2022 muestra lo deprisa que ese widget común quedó sobrecargado. Pronto estaba lidiando con superposiciones extendidas, callbacks de actualización MIPMAP, uniones de culling, barras de desplazamiento, gestión de imagen activa, callbacks de la tira de película, navegación de vista previa completa y colocación de iconos ajustada con CSS. En otras palabras, thumbnail.c no se limitaba a dibujar una miniatura. Se convirtió en la línea de falla compartida entre la mesa de luz, la tira de película, el culling y la vista previa.

La tira de película empezó como una banda de navegación dedicada, luego ganó el centrado sobre la imagen activa, el arrastrar y soltar, las interacciones con el mapa, las operaciones de copiar/pegar, el desplazamiento suave, correcciones de HiDPI, personalización con CSS y, finalmente, partes del nuevo sistema de callbacks de miniaturas. De nuevo, ninguna de esas funciones es descabellada por sí sola. El problema es que la tira de película acabó con el comportamiento personalizado justo suficiente para divergir de la mesa de luz mientras seguía intentando reutilizar parte de la misma maquinaria de miniaturas.

Además de eso, estaba el incoherente paradigma de “selección” de Darktable, vendido como un “flujo de trabajo sin clic”, de modo que muchas operaciones de escritura podían gestionarse sin seleccionar explícitamente la imagen o imágenes a afectar. Eso conducía a un montón de efectos no deseados y accidentes, que resultaban en pérdida de datos (atribuir la valoración con estrellas equivocada a la imagen equivocada, lo que la haría desaparecer de la colección actual si la filtrabas por valoración) y en tediosas sesiones de deshacer. Pero toda la heurística de selección de imagen era frágil también: mover el ratón por la ventana podía robar el foco a la imagen que bloqueaste explícitamente con un clic o una selección de teclado, pero no siempre.

Por no mencionar que ese “actuar al pasar el ratón por encima” desencadenaba varias consultas SQL a la base de datos de la biblioteca, al pasar sobre una imagen nueva, para obtener metadatos actualizados de la imagen y refrescar el contenido de los módulos de metadatos (metadatos, metadatos EXIF e IPTC, etiquetas). Todo esto es porque no se podían cachear, y no se podían cachear porque Darktable es incapaz de rastrear el ciclo de vida de sus datos, así que necesita refrescarlo todo todo el tiempo. Y al hacerlo te agotará la batería.

Así que cuando empezó la reescritura tras haber hecho el fork de Ansel en 2022, el problema no era que un commit malo hubiera roto la mesa de luz. El problema era que había tres capas de historial apiladas unas sobre otras:

  • lighttable.c seguía cargando con el lastre de varios modos de navegación (inútiles), programados ahí como ideas de última hora,
  • thumbtable.c concentraba la lógica de desplazamiento, offset, imagen activa y navegación de colección, que se solapaba en parte con el backend de colección y selección,
  • thumbnail.c concentraba cada vez más comportamiento de interfaz compartido más código SQL.

Por eso el trabajo posterior tuvo que ser arquitectónico. A esas alturas, no había ninguna manera realista de seguir arreglando los síntomas uno por uno.

Cambios en el front-end

El culling nunca fue realmente (conceptualmente) una vista separada

Una pieza importante de esa sustracción fue el modo de culling . En febrero de 2023, las vistas dedicadas de culling y de vista previa fueron eliminadas porque se habían entrelazado mal con los atajos, el cambio de vistas y la ramificación de casos especiales. El código estaba enredado y era engorroso, pero el verdadero problema estaba en el diseño mismo: era una solución sobrediseñada a un problema mucho más simple, resuelto en la capa equivocada.

La necesidad era aislar un conjunto arbitrario de imágenes que no eran necesariamente contiguas en la colección actual, con el fin de decidir cuál sería la que conservar. Eso no necesitaba una nueva disposición (¿o dos…?); necesitaba un filtro para restringir la colección a una selección arbitraria. Ya teníamos filtros para restringir colecciones por valoración, etiqueta de color, estado editado/no editado, etc.

Esa diferencia importa. Una vista de culling dedicada duplica problemas que la mesa de luz ya tiene que resolver: qué conjunto de imágenes está activo, cómo se restaura la selección al salir de la vista de culling, cómo se enrutan los atajos, cómo se inicializan el zoom y el estado de las miniaturas, y qué pasa cuando vuelves a la cuadrícula. Cada mejora tiene entonces que implementarse dos veces, una en la lógica de la mesa de luz y otra en la lógica del culling, y ambas versiones se van separando. Por no mencionar que el modo de culling tenía un modo estático y un modo dinámico, que diferían tanto en cómo interactuabas con ellos como en su implementación, y muy pocos usuarios entendían de qué iban.

En Ansel, el culling volvió como un filtro de colección, primero como una manera simple de acotar el conjunto actual, y luego explícitamente como el botón de filtro Restringir a la selección. Eso significa que la intención de cara al usuario se mantuvo igual (reducir la colección actual a una selección arbitraria de imágenes), pero la implementación cambió por completo: en lugar de entrar en una vista especial, te quedas en la mesa de luz y le dices a la herramienta de filtrado que muestre solo las imágenes actualmente seleccionadas o solo las imágenes que coinciden con el criterio de acotación actual.

image
La nueva barra de herramientas de filtrado, unificada. Los criterios de filtrado son inclusivos, y los iconos se comportan como botones de verificación: desmarca para ocultar las imágenes coincidentes, marca todo para mostrarlo todo (un menú contextual con clic derecho te da un atajo para hacerlo en un solo paso). Aparecen descripciones emergentes al pasar el ratón por encima para más detalles. El primer icono refresca la colección actual según los filtros. Por ejemplo, si muestras imágenes valoradas con 3 estrellas y degradas una imagen a 2 estrellas, no será expulsada automáticamente de la colección actual hasta que la refresques manualmente.

Para los usuarios, el beneficio es práctico. El culling ahora es componible con el resto de la lógica de filtrado en lugar de vivir fuera de ella. Puedes combinarlo con valoraciones, etiquetas de color o búsqueda de texto porque no es más que otro filtro de colección. El manejo de atajos es más simple porque hay una vista especial menos compitiendo por la propiedad de los atajos. Y las correcciones al desplazamiento, la selección, el tiempo de vida de las miniaturas o el comportamiento de las superposiciones benefician automáticamente también al culling, porque el culling ahora usa la misma infraestructura de la mesa de luz en lugar de una paralela.

Esa fue también una oportunidad para renovar la barra de herramientas de filtrado, que mezclaba botones y lógica: una lista combobox para valoraciones (de rechazada a 5 estrellas), asociada con una lista combobox de comparación (≠ = > < >= <=), pero botones de conmutación para las etiquetas de color. Darktable 4.0 reemplazó eso con un conjunto sobrediseñado de widgets configurables que usaban paradigmas de interfaz no estándar que necesitaban explicarse en descripciones emergentes. Ansel ha aplanado el diseño: todo es un botón de conmutación que funciona en modo “inclusión” dentro de su grupo, permitiendo selecciones complejas sin una interfaz compleja:

  1. tienes 3 grupos (valoraciones, etiquetas de color, estado de edición), más restringir a la selección y búsqueda de texto,
  2. entre esos grupos, los filtros son exclusivos, lo que significa que son un AND lógico,
  3. dentro de esos grupos, los filtros son aditivos, lo que significa que son un OR lógico.

Por ejemplo, en la captura de pantalla de arriba, todo está activado, así que el filtrado está en la práctica desactivado (dejamos pasar todo). En el ejemplo de abajo, filtramos para dejar entrar todas las imágenes que tienen una etiqueta de color asignada y ya han sido editadas, independientemente de su valoración (todos los botones de valoración están activados):

image

Selección: mezclar estados de backend y de interfaz siempre te acaba pasando factura

Las imágenes agrupadas eran otra línea de falla oculta. Parte de la vieja lógica intentaba tener en cuenta la agrupación en las consultas SQL, al generar colecciones de imágenes a partir de la base de datos de la biblioteca, pero los bordes de grupo, los estados de paso del ratón, el comportamiento de selección y la visibilidad real son conceptos de interfaz. Ese desajuste creaba errores sutiles: podían seleccionarse todas las imágenes de un grupo, o ignorarse algunas según reglas heurísticas que no coincidían con lo que el usuario veía en pantalla.

Arreglé eso devolviendo las decisiones de visualización de grupos al código de la interfaz. SQL proporciona la lista de imágenes; la interfaz decide qué miembros agrupados están visibles, colapsados o bajo el ratón. Después de eso, las funciones añadidas en 2025 se volvieron mucho más simples: los bordes de grupo pudieron reimplementarse, hacerse opcionales, extenderse a la tira de película y hacerse más visibles al pasar el ratón por encima. Las descripciones emergentes pudieron rellenarse de forma perezosa solo cuando el usuario pasa el ratón por encima. Una petición SQL por miniatura en el momento de la inicialización también pudo desaparecer porque la interfaz ya tenía suficiente estado local para saber cuándo se necesitaba realmente la información de grupo.

Para los usuarios, el beneficio no es abstracto. Los bordes de grupo ahora significan lo que muestran, las selecciones coinciden exactamente con el estado visible del grupo, y las imágenes agrupadas ya no dan la sensación de estar gestionadas a medias por la base de datos y a medias por la interfaz.

Otro problema oculto era la selección. En el código antiguo, la selección estaba demasiado enredada con las interioridades de la colección, el acceso SQL en crudo y la lógica de reserva (fallback). Eso suena inofensivo hasta que una imagen es expulsada de la colección actual, un grupo se colapsa, o se produce un cambio de vista mientras otra parte del código todavía asume el modelo de selección anterior. En Darktable, las selecciones de imagen se manejaban como un backend SQL puesto que se almacenan en la biblioteca para restaurarse en la siguiente sesión. Esto no es un problema hasta que el backend de selección se piratea para adivinar qué imágenes están visibles en la interfaz, y la interfaz realmente maneja la selección a través del backend. Esta es la peor división posible de funcionalidad entre las capas de backend e interfaz.

Por eso 2025 contiene todo un grupo de commits relacionados con la selección: reescribir la API de selección, eliminar la vieja noción de main_image, unificar los getters/setters de imagen activa, arreglar la selección por rango, restaurar el estado de selección a través de los cambios de vista, y añadir reservas por rowid cuando scroll_to_selected ya no puede encontrar el id de imagen original. La reescritura cambió la arquitectura: el thumbtable ahora computa selecciones visibles y con sentido para el usuario y pasa una lista de ids de imagen a la capa de selección, en lugar de que la interfaz intente aplicar ingeniería inversa al significado a partir del estado de la colección a posteriori. La API SQL de selección se ha convertido en un simple backend de “guardar en/restaurar desde la base de datos”.

El beneficio concreto para el usuario es que la selección por rango con Shift + clic y “la imagen sobre la que actuar” son lo que ves es lo que obtienes (What You See Is What You Get): no puedes seleccionar una imagen que no esté visible en pantalla, y el backend no tiene que adivinar qué es visible. Y volver a desplazarse hasta la imagen seleccionada ahora falla mucho menos a menudo en casos límite. El problema oculto no era un atajo roto; era que la semántica de la selección vivía en la capa equivocada.

La solución es separar claramente lo que pertenece a la gestión de backend y de biblioteca de lo que pertenece a los estados e interacciones de la interfaz, y proporcionar una interfaz rígida entre ambos. Eso hace que las distintas partes del código estén mejor encapsuladas, sean inmunes a los cambios en las demás partes, y se gestionen de forma centralizada.

Los fallos de paso del ratón y de foco venían de varios sistemas válidos colisionando

El paso del ratón por encima, el paso con teclado, el foco y el manejo de clics estaban implementados cada uno por razones legítimas, pero no compartían un único punto de arbitraje. Darktable ahora incluso tiene un orgulloso eslogan de marketing escrito en el fondo de la mesa de luz, cuando la colección está vacía, alabando ese flujo de trabajo “sin clic” que te permite sobrescribir los metadatos de una imagen sin querer o incluso sin saberlo. Eso me suena a una compañía de coches anunciando un coche diseñado para que puedas usar el volante con las rodillas (y usar las manos para beber cerveza).

Por eso el comportamiento más antiguo de la mesa de luz podía sentirse incoherente sin ningún error evidente único: estaba roto por diseño. Hacer clic en un botón de superposición para valorar la imagen también podía seleccionar toda la miniatura, pero valorar desde el teclado no lo hacía. El movimiento de teclado y el movimiento de ratón podían ambos creer que poseían la imagen “actual” y desencadenar toda clase de cambios destructivos de metadatos con solo pasar el ratón por encima de la imagen y sin pedir confirmación. Filtrar imágenes por valoración con estrellas y cambiar inadvertidamente la valoración de una imagen podía hacer que desapareciera inesperadamente. Widgets ocultos podían seguir recibiendo lógica relacionada con el paso del ratón. El foco de GTK podía decidir una cosa mientras la capa de vista esperaba otra.

Las correcciones aquí tenían menos que ver con añadir funciones nuevas que con elegir un único propietario del estado. El despacho de estados de paso se centralizó. La navegación con teclado ganó un punto de partida sensato y una ruta de cancelación explícita con Shift+Ctrl+A. La visualización del foco se hizo explícita. Los widgets ocultos dejaron de participar en la lógica de paso del ratón que no podían mostrar. Hacer clic en los botones de superposición dejó de filtrarse hacia la selección de la miniatura. No se hace más (sobre)escritura de metadatos sin una pulsación explícita de botón, los eventos de paso del ratón son todos de solo lectura.

Ansel tiene dos reglas simples:

  • cualquier acción que vaya a (sobre)escribir (meta)datos se hace solo sobre imágenes explícitamente seleccionadas,
  • la selección explícita se hace solo interactuando con algo “duro”: un clic del ratón o una pulsación de tecla.

Entonces, los eventos de paso del ratón quedan reservados a eventos de solo lectura.

El beneficio para los usuarios es que la mesa de luz reacciona más como un único modelo de interacción. El problema oculto no era que el paso del ratón estuviera roto. Era que varios tipos de paso del ratón eran simultáneamente “correctos” y por tanto colectivamente incorrectos.

Cambios en el backend

Reparentar el thumbtable entre la mesa de luz y la tira de película

Uno de los problemas más profundamente ocultos era la propiedad de los widgets. En Darktable, la misma cuadrícula thumbtable se reparentaba entre los contextos de la mesa de luz y de la tira de película. Sobre el papel, eso evitaba la duplicación de código y la proliferación de widgets, y suena ingenioso. En la práctica, hacía que el estado de desplazamiento, la inicialización de miniaturas, la recolección de basura y el tiempo de vida de los eventos dependieran de dónde había vivido el widget más recientemente. Por no mencionar que reparentar era lento, así que ir y venir entre la mesa de luz y el cuarto oscuro se retrasaba alrededor de 1 s, el tiempo necesario para que GTK recalculara los nuevos tamaños de miniatura, posiblemente pidiera a la caché nuevas miniaturas, y redibujara los widgets.

Esto afloraba de maneras muy concretas. La posición de desplazamiento podía saltar o volverse incoherente. Las miniaturas podían ocultarse, mostrarse o destruirse en el momento equivocado. La recolección de basura se volvía más difícil de razonar porque la jerarquía de widgets no era estable. Por supuesto, todo eso se resolvió commit tras commit, pero dejando el código en un estado de complejidad inmantenible.

La verdadera solución fue dejar de intentar ser ingenioso. La mesa de luz y la tira de película ahora tienen thumbtables separados. Eso significa un poco más de estructura explícita en el código, pero muchos menos estados accidentales. El beneficio para los usuarios es visible en un desplazamiento más estable, menos fallos de disposición al cambiar de vista, y una tira de película que se comporta como un hermano en condiciones de la mesa de luz en lugar de como un fragmento reutilizado.

Los cambios de menú y de disposición no eran cosmética

Que el menú global se volviera verdaderamente global en abril de 2025 no fue un ejercicio de marca. Antes de eso, muchos comandos estaban implementados como si pertenecieran a módulos locales aun cuando eran realmente acciones a nivel de aplicación. Algunos de ellos estaban ocultos por completo tras atajos de teclado y solo conocidos por quienes leían la documentación. A muchos de ellos los descubrí solo al eliminar su código. Lo mismo ocurría con la información de imagen, el cambio de vistas y las viejas cajas de herramientas de vista. Vivían en lugares fáciles de sortear en el código una vez, pero difíciles de mantener de forma coherente entre vistas.

Mover la información de imagen a la barra de menú global, mover el conmutador de vistas a una ubicación de nivel superior más clara, y más tarde eliminar la barra de herramientas del centro inferior fueron todas consecuencias del mismo diagnóstico: el centro de la interfaz debería pertenecer a la interacción con la imagen, no a restos históricos. La eliminación de la línea de tiempo y la decisión de no mostrar una tira de película vacía son ejemplos más pequeños de la misma lógica. Incluso que Enter abra la imagen seleccionada en el cuarto oscuro encaja aquí: alinea el comportamiento con el trabajo principal de la mesa de luz en lugar de preservar hábitos históricos accidentales.

El beneficio para el usuario no es “el menú es más bonito”. Es que los comandos se encuentran en lugares que coinciden con su alcance, y la propia mesa de luz tiene menos controles compitiendo con la cuadrícula. Por no mencionar que los “módulos” que en realidad son cuadrículas (no uniformes) de botones no son más que menús disfrazados con un peor diseño, así que ahora se convierten en entradas de menú.

El menú global fue también la oportunidad de llevar a la interfaz funciones que hasta ahora solo vivían en scripts de shell ocultos (¡!): precargar las miniaturas de la colección actual y purgar miniaturas de la caché de disco. Esas se pidieron durante años en la interfaz, pero no encajaban en ningún sitio en el diseño de interfaz de Darktable, centrado en los módulos.

Pero… esa barra lateral derecha de módulos que no querían ser menús era una bendición, porque quitaba mucha área que pintar con imágenes y hacía las miniaturas más pequeñas. Eliminarla hizo aparecer en todo su esplendor el mal rendimiento del dibujado de miniaturas: nada se cacheaba, ni las superficies de imagen ni los metadatos, y todo se volvía a obtener todo el tiempo. La reescritura era inevitable; la arquitectura estaba rota.

El zoom solo se volvió usable después de que los tamaños de miniatura se hicieran consistentes

El primer intento de mesa de luz con zoom fracasó por una razón estructural: la geometría de las miniaturas, los tamaños de mipmap cacheados y la navegación a nivel de vista no compartían el mismo modelo. Por eso la función tuvo que eliminarse primero. Si hubiera seguido puliéndola en su sitio, el resultado solo habría sido una función rota con mejor aspecto.

El trabajo posterior arregló la cadena de dependencias en el orden opuesto. El backend de vista ganó el andamiaje para el zoom de la mesa de luz, luego se enseñó a la caché de miniaturas a razonar de forma más coherente sobre los tamaños de miniatura, se preparó una API de miniaturas a resolución completa, y luego se añadieron por encima de eso el recorte (clamping), el desplazamiento panorámico (panning) y el autopaneo basado en baricentro. Solo una vez que las capas inferiores se pusieron de acuerdo sobre el tamaño de imagen y la estrategia de recuperación, el zoom al 200 % pasó a ser una función defendible.

El beneficio para los usuarios es que el zoom ahora está ligado a la caché y a la navegación en lugar de pelearse con ellas. Arrastrar para panear, Shift + arrastrar a través de las miniaturas visibles, y el paneo automático hacia el baricentro de detalles funcionan todos porque el pipeline subyacente sabe qué se supone que son las miniaturas con zoom.

El confuso significado de “zoom” en la mesa de luz se ha actualizado: el término “zoom” se usaba tanto para el zoom exterior (número de imágenes por fila, que afecta indirectamente a su tamaño visible) como para el zoom interior (aumento dentro del marco de la imagen). Así que ahora tenemos columnas para el zoom exterior, y zoom para el aumento.

image

No hagas caso de la cruz en lugar del símbolo - en el spinbutton de columnas, es un error de GTK con el tema Breeze de KDE/Plasma .

Esto hizo posible implementar una función que tenía en mente desde hacía mucho tiempo: el autopaneo baricéntrico al aumentar una miniatura.

image
Zoom para ajustar
image
Activando el zoom al 100 %

Puedes ver que, al activar el zoom al 100 %, las imágenes se alinean automáticamente por su contenido a pesar de tener encuadres y relaciones de aspecto completamente distintos. Esto usa el análisis de características por descomposición en wavelets, a partir del cual computamos las coordenadas del baricentro de detalles. No es perfecto porque no capta la misma parte de la cara en todas las imágenes, pero nos da la cara en todos los casos. Es un proyecto que tenía aparcado desde hacía mucho tiempo, pero no era posible con el diseño anterior de la mesa de luz.

Ni que decir tiene que arrastrar dentro de las miniaturas con zoom se ha restablecido, y arrastrar dentro de todas las miniaturas con zoom se ha añadido también (Shift+Arrastrar). Esta función se ha pedido durante mucho tiempo en Darktable, pero claramente no era posible en su diseño de mierda.

Ralentización de metadatos autoinfligida

La vieja mesa de luz seguía haciéndole a la base de datos las mismas preguntas de una en una miniatura. En una colección pequeña esto es fácil de pasar por alto. En una más grande, aflora como microtirones al abrir la vista, mostrar superposiciones, pasar el ratón sobre grupos o refrescar datos sensibles al historial. El problema oculto no era que el SQL en crudo fuera lento en términos absolutos; era la repetición y la sincronización de esas consultas.

La solución llegó por etapas. Los metadatos de solo lectura empezaron a cachearse a partir de las estructuras de imagen. Luego, en febrero de 2026, los metadatos de toda una colección podían obtenerse con una sola consulta SQL en lugar de una consulta por imagen. El thumbtable podía sembrar la caché de imágenes a partir de la colección que estaba a punto de mostrar. El cacheado de información de miniaturas se refactorizó en un helper y más tarde se fusionó de vuelta en dt_image_t para que los metadatos no tuvieran que rebotar entre estructuras paralelas. Incluso pequeños cambios como eliminar los pings de hash de historial en el redibujado importan aquí, porque recortan viajes de ida y vuelta invisibles de las rutas críticas.

Se han eliminado un montón de consultas SQL inútiles por miniatura, junto con código SQL en el código de interfaz de la mesa de luz, así que las capas funcionales ahora están separadas correctamente.

El beneficio para el usuario es exacto: abrir colecciones grandes se atasca menos, las visualizaciones cargadas de superposiciones titubean menos, y las actualizaciones de historial o metadatos se propagan con menos pausas visibles.

La caché de mipmaps también necesitaba arreglarse

La caché de mipmaps es la capa que carga las imágenes RAW y las miniaturas pregeneradas en la RAM, y las vacía cuando la memoria escasea. Esa era una de las fuentes ocultas más profundas de errores visibles. Cuando todo se alineaba, funcionaba bastante bien. Cuando el archivo fuente era más pequeño de lo esperado, cuando una vista previa JPEG incrustada era rara, cuando el hash de historial estaba indefinido, o cuando la invalidación de la caché de disco iba por detrás de las ediciones, el usuario no experimentaría “un error de caché”. Vería miniaturas obsoletas, vistas previas rotas o imágenes que se negaban a refrescarse.

Por eso buena parte del trabajo de 2025 en la caché parece quirúrgico. La lógica de asignación de búferes se reescribió porque la propiedad estaba demasiado enredada. El manejo de entradas de tamaño insuficiente se mejoró porque las suposiciones sobre el tamaño de entrada se filtraban demasiado lejos. Las vistas previas incrustadas dejaron de descartarse tan agresivamente porque las vistas previas demasiado pequeñas seguían siendo útiles para la consistencia entre niveles de zoom. Las vistas previas JPEG adjuntas (sidecar) empezaron a usarse cuando estaban presentes en lugar de las miniaturas incrustadas del RAW. La invalidación de caché se endureció, y las miniaturas cacheadas se eliminaban realmente del disco cuando debían. Los mipmaps regenerados empezaron a escribir su hash de vuelta en la base de datos en sincronía con el estado del historial, y el caso del hash de historial indefinido dejó de dejar mipmaps sin guardar.

El error más taimado que encontré estaba enterrado en la absurda complejidad del entrelazado de thumbtable, vista y caché de mipmaps. Mientras que el renderizado de miniaturas se difería a un hilo separado por rendimiento (como debe ser), y podía iniciarse más de un hilo para (supuestamente) procesar varias miniaturas a la vez (posiblemente usando varias GPU), todos los hilos de procesamiento y la interfaz en realidad competían por bloquear las cachés de mipmap y de imagen, lo que solo hacía que la interfaz se atascara mientras los pipelines estaban en marcha. Es solo porque trabajé bajo la premisa de que había reescrito las cosas siguiendo el manual, y sabía que la manera de manual debería ser más rápida que eso, que seguí escarbando hasta que encontré por qué todavía no era tan rápida como se esperaba, simplificando todo capa por capa en el proceso.

El beneficio para los usuarios es que la caché es más fiable. Tras las ediciones, es menos probable que la mesa de luz muestre una miniatura obsoleta. En archivos problemáticos, la generación de vistas previas falla menos a menudo. En visitas repetidas a la misma colección, la caché de disco se comporta más como una caché y menos como un archivo de capturas de pantalla rancias. Pero todo eso, sin tener que refrescar/recalcular/redibujar todo todo el tiempo solo para estar seguro.

Además, las opciones de usar JPEG incrustados o forzar un recálculo se movieron de las preferencias al menú global y pueden cambiarse durante la ejecución:

image

Este es otro ejemplo en el que limpiar y simplificar el backend allanó el camino para la extensión del frontend y para nuevas funciones que simplemente tienen sentido.

Seguridad de hilos y condiciones de carrera

Una razón clásica por la que estos errores duraron tanto es que requerían que el usuario fuera más rápido que el código: desplazarse rápido, abandonar una vista mientras las miniaturas todavía se están construyendo, redimensionar mientras una superficie en segundo plano todavía se está produciendo, o cerrar un widget justo antes de que un hilo trabajador envíe una actualización.

Por eso importa el trabajo de seguridad de hilos de 2025-2026. La obtención de miniaturas se movió más adentro de los trabajos en segundo plano, y luego se difirió explícitamente para que el renderizado del cuarto oscuro pudiera conservar la prioridad. La destrucción de miniaturas se movió a puntos de limpieza más seguros. Los trabajos en segundo plano que producen miniaturas aprendieron a cancelarse a sí mismos cuando el widget al que servían desaparecía. Las superficies de imagen se protegieron con mutex. Los punteros liberados se anularon. Los búferes de imagen se movieron a rutas de asignación mejor gestionadas. El objetivo de todo esto no era “más hilos”; era impedir que los hilos viejos escribieran en estado muerto o compitieran por bloqueos.

El resultado de cara al usuario es menos segfaults, menos fallos aleatorios durante el desplazamiento rápido, y menos casos en los que la interfaz parece correr contra sí misma bajo carga.

También se eliminaron varios bloqueos de hilos, añadidos a lo largo de los años para parchear problemas. Muchos eran redundantes (aunque no lo aparentaban bajo la disparatada complejidad de todo el conjunto), algunos perjudicaban activamente el rendimiento, y todos ellos ocultaban un mal diseño. Ahora tenemos menos puntos de bloqueo, pero el resultado es más legible y más robusto.

Como resultado, los widgets de miniaturas son ahora objetos totalmente autocontenidos. Gestionan internamente sus propios hilos del pipeline de renderizado de miniaturas, que interactúan directamente con su propia superficie de imagen en caché, de modo que pueden crearlos o eliminarlos por sí mismos. Esta imagen en caché se invalida únicamente cuando cambia el historial de la imagen, lo cual queda explícito gracias al backend del historial de revelado. Como el widget de miniatura conoce su propio estado (visible o no, con necesidad de refrescar la imagen o no, tamaño, modo de resaltado de enfoque, etc.), se vuelve mucho más robusto que intentar gestionar todas esas cosas desde capas de nivel superior que no logran comunicarse entre sí.

Esto no era posible con el diseño de Darktable, porque no dejaba de añadir/eliminar dinámicamente widgets de miniaturas a la vista de mesa de luz actual, dependiendo de la posición de la línea flotante, lo cual era un intento de manejar las ralentizaciones provocadas por todos los hilos que competían por el acceso a la caché. Pero, además, los widgets de miniaturas de Darktable intentaban adquirir de inmediato una imagen de la caché mipmap, cuando se creaban, lo que hacía que el uso de CPU y de E/S de memoria se disparara, y congelaba efectivamente la interfaz. Pero “arreglar” eso reduciendo la esperanza de vida de los widgets de miniaturas hacía imposible dejar que gestionaran su propio estado internamente, así que tenía que hacerse desde capas de alto nivel, que debían comunicarse entre sí para actualizar los estados, lo cual efectivamente ocurría en algunos sitios pero a costa de una complejidad insoportable.

En lugar de eso, todas las miniaturas de Ansel se inicializan de una vez, pero adquieren de forma perezosa una imagen de la caché mipmap solo una vez que se vuelven visibles, y entonces la almacenan internamente en caché. Esto mantiene fluido el desplazamiento por la tabla de miniaturas, incluso con una colección de 500 imágenes que están todas generando su imagen.

Las previsualizaciones de importación mejoraron como efecto colateral

La ventana de importación se veía afectada por este trabajo por la misma razón que la mesa de luz: necesita una extracción rápida de previsualizaciones, inspección de metadatos y un comportamiento de reserva sensato ante archivos parcialmente compatibles. Una vez reescritos los caminos del mipmap y de las miniaturas, la carga de la previsualización raw en la importación se volvió más rápida, y también mejoró la compatibilidad con TIFF/DNG.

Este es un buen ejemplo de por qué la reescritura tenía que ocurrir en un nivel bajo de la pila. Si la maquinaria subyacente de previsualización es ineficiente o frágil, tanto la mesa de luz como la importación heredan el mismo dolor. Una vez reescrita esa maquinaria, ambas se beneficiaron.

Cambios arquitectónicos globales

En mi artículo anterior, mostré cómo la caché del pipeline de trabajo hace que ir y venir entre la mesa de luz y el cuarto oscuro sea casi instantáneo, porque no necesita recalcular toda la imagen. El trabajo realizado aquí sobre la GUI resuelve el mismo problema de los retrasos al cambiar de vista, pero a nivel de la GUI. Así que no hay retardo al alternar entre ambas vistas.

Esto es importante porque el retardo al cambiar de vista se ha usado como excusa para duplicar funciones (módulos/cajas de herramientas) entre la mesa de luz y el cuarto oscuro, lo cual solo aumenta el desorden de la GUI. Así que toda esta mejora del backend hace posible mejorar el diseño de la GUI especializando cada vista para una única tarea:

  • gestión de metadatos para la mesa de luz (además, obviamente, del descarte),
  • edición de imágenes para el cuarto oscuro.

Como resultado, las cajas de herramientas de metadatos y etiquetas se han eliminado del cuarto oscuro.

¿Cuántas líneas de código ahorró la reescritura?

Contando únicamente las líneas que no son comentarios ni están en blanco con cloc, y comparando el último árbol anterior al 1 de enero de 2022 con el árbol actual para los archivos principales que se discuten aquí, la reescritura ahorró 4.714 líneas de código en total.

El alcance de ese recuento es:

  • todo el directorio data/themes/ anterior a 2022, comparado con el actual data/themes/ansel.css,
  • src/common/mipmap_cache.[ch],
  • src/dtgtk/thumbtable.[ch],
  • src/dtgtk/thumbnail.[ch],
  • src/views/view.[ch],
  • src/views/lighttable.c,
  • src/libs/collect.c,
  • src/libs/tools/filter.c,
  • más los archivos eliminados por completo por el rediseño o por la limpieza de la GUI de colecciones: src/dtgtk/culling.[ch], src/libs/tools/view_toolbox.c, src/libs/collect.h y src/libs/recentcollect.c.

Dentro de ese alcance, el total pasó de 15.257 líneas de código antes de 2022 a 10.543 hoy. Por tanto, la reescritura ahorró 4.714 líneas de código en total. El mayor ahorro provino de eliminar por completo la vista de descarte (src/dtgtk/culling.c) (-1.406 líneas), reducir la mesa de luz (src/views/lighttable.c) (-976), eliminar 7 antiguas hojas de estilo de temas y fusionarlas en una sola (-864 en todo data/themes/), reducir src/dtgtk/thumbnail.c (-460), src/dtgtk/thumbtable.c (-458), eliminar src/libs/recentcollect.c (-358) y reducir src/views/view.c (-314). Algunos archivos sí crecieron, especialmente src/libs/tools/filter.c (+215) y src/libs/collect.c (+157), porque parte del objetivo era devolver el comportamiento de casos especiales, como el descarte y las antiguas ramas de la GUI de colecciones, a una infraestructura compartida más simple, en lugar de mantenerlos en vistas paralelas y módulos laterales.

El recuento de líneas no es por sí mismo una métrica de calidad. Muchas reescrituras se limitan a mover el código de sitio. Pero aquí el número coincide con el cambio de diseño: menos comportamiento duplicado, menos vistas paralelas, menos parches compensatorios y menos lugares donde la GUI, la capa de vistas y la caché tenían todos que resolver el mismo problema dos veces.

Todo lo que arreglé, lo arreglé simplificando la lógica y el código. No se permitió ningún apaño.

Pruebas de rendimiento

Todos los tiempos de ejecución se calcularon en un portátil Lenovo ThinkPad P51 (CPU Intel Xeon E3-1505M v6 @ 3.00GHz, GPU Nvidia Quadro M2200 con 4 GB de VRAM, 32 GB de RAM, pantalla 4K), con la CPU en modo rendimiento, Linux Fedora 41 con escritorio KDE/Plasma. Los tiempos de ejecución del pipeline de píxeles no se comparan (fuera del alcance; véase el artículo anterior). Ansel Master se toma en el commit 09749f1d  (21 de feb. de 2026).

DescripciónAnsel MasterDarktable 5.0
Tiempo desde el arranque de la aplicación hasta el dibujado de la última miniatura de la mesa de luz (misma colección)2.12 s7.49 s
Tiempo para cambiar de la mesa de luz al cuarto oscuro (misma imagen)0.2 s1.2 s
Tiempo para desplazarse (inicio->fin) por la misma colección de 471 imágenes*0.7 s5.0 s

*: miniaturas precargadas en la caché de disco en ambos casos, 5 columnas de miniaturas por fila, resolución 4K, sin barra lateral derecha.

Como “arreglo”, Darktable 5.x nos bendijo con una preciosa pantalla de bienvenida, que es más una confesión que otra cosa.

Lo siguiente se ha medido con batería, en modo de ahorro de energía, con la aplicación inactiva (sin interacción del usuario) durante 5 minutos, usando Intel Powertop. El consumo de referencia de todo el SO inactivo es del 1.6 % de CPU. (La potencia se da solo para la aplicación, el % de CPU se da para todo el sistema):

VistaAnsel MasterDarktable 5.0
Mesa de luz1.8 % CPU, potencia: 0.85 mW2.7 % CPU, potencia: 103 mW
Cuarto oscuro1.8 % CPU, potencia: 7.65 mW1.8 % CPU, potencia: 22 mW

Estas cifras representan el consumo de energía de referencia de la GUI por sí sola (GTK, procesos en segundo plano, temporizadores programados, etc.). Darktable pierde rendimiento a través de la GUI, y el tedioso trabajo realizado en 2023-2024 para optimizar los módulos de procesamiento de píxeles en un extra de 15-50 ms es completamente irrelevante.

Conclusión

A estas alturas, estoy firmemente convencido de que el “proyecto” Darktable solo atrae a “desarrolladores” que serían incapaces de hacer una distinción cognitiva entre GUI y backend aunque les fuera la vida en ello. Así que los problemas de la GUI se resuelven en el backend, los problemas del backend se resuelven en la GUI, y esto hace que la complejidad del código crezca fuera de control con el tiempo, lo que más adelante justifica añadir nuevas funciones parcheando la menor cantidad de código posible en una base de código que ya nadie entiende. Por no mencionar que nada de eso estaba documentado, así que tuve que aplicar ingeniería inversa penosamente a lo largo de varios años, simplificando recursivamente un poco aquí y un poco allá, hasta que finalmente convergió en una lógica global limpia.

Ni que decir tiene que se han eliminado todos los apaños y “arreglos rápidos” que se habían añadido. Todos ellos databan de después de 2020, lo que muestra una tendencia preocupante en la degradación de la calidad del código.

Este proyecto de limpieza me robó 4 años de vida, no me dio ningún placer, y la gente que introdujo todas las regresiones que arreglé penosamente tiene que rendir cuentas por las consecuencias de sus actos. Hay una enorme diferencia entre no tener suficiente tiempo para hacer las cosas bien y consumir un montón de horas-hombre para empeorarlas. Y luego, negarse a admitir que empeoraste las cosas, negarse a aceptar que hay un problema, y alimentar tu sesgo de confirmación escuchando únicamente los comentarios de los contentos es la prueba definitiva de la estupidez.

Darktable ha degenerado en una mierda, y acabo de explicar, técnicamente, por qué. Al equipo de Darktable y a su foro satélite de tech bros  les gustaría hacer creer a la gente que me enfadé con ellos porque no aceptaban mis cambios, y que todo es un problema interpersonal. Para el profano que no entiende lo que escribí aquí y en los artículos anteriores, es más fácil creer en un enfado interpersonal que entender cómo una sucesión de malas decisiones técnicas a lo largo de varios años me convirtió en la víctima de los problemas que ellos crearon, porque yo era el único que estaba a tiempo completo aquí, dependiendo de ello para ganarme la vida. Me enfadé con ellos porque una y otra vez se cagaban en la puerta de mi casa, y yo tenía que limpiarlo repetidamente. Esta es una forma de violencia realmente difícil de ver y de reconocer porque no se manifiesta materialmente: es una forma de hacerte la vida más difícil, a diario, paso a paso, solo porque un puñado de aficionados de mediana edad sin ninguna habilidad querían formar parte de algo guay sin darse cuenta del efecto perjudicial de sus contribuciones sobre todo el proyecto.

Y me tocó a mí limpiar el desastre porque, al parecer, era el único al que le importaban las regresiones, los raros errores y fallos aleatorios, las peores “innovaciones” de GUI que disuadieron incluso a mi propia esposa de usar Darktable porque resulta simplemente abrumador, y todas las nuevas ralentizaciones que se siguen acumulando con los años. Como me dijo Chris Elston en el chat de IRC de Darktable, en 2022, antes de que lo abandonara para siempre, a propósito de cosas que yo había arreglado en 2019 y que ellos volvieron a romper en 2022: “cállate y arréglalo”. Si eso no es violencia, no sé qué lo es. Y ahora intentan difundir el rumor de que yo era el tóxico. Todo el equipo es tóxico. Su descuidada cultura de trabajo es tóxica. Su manera de entusiasmarse con todo, mientras sea nuevo, sin sopesar el coste de mantenimiento, la duplicación de funciones y la saturación general del usuario es tóxica. Su falta de preocupación por el futuro del proyecto y por las consecuencias de las decisiones que toman es tóxica.

Y lo que es especialmente tóxico es que, en discuss.pixls.us, cada publicación que expresa mi elogio o los elogios de Ansel es marcada y ocultada. No hay libertad de expresión en un foro de software libre. Simplemente han convertido el comunismo en estalinismo.


Translated from English by : ChatGPT, Claude. In case of conflict, inconsistency or error, the English version shall prevail.