Entre janvier 2022 et mars 2026, Ansel a intégré 297 commits (hors fusions) concernant la grille de la table lumineuse, ses vignettes, ainsi que leur pipeline de rendu et leur mise en cache. J’ai essayé de me contenter du code brouillon de la table lumineuse de Darktable, seulement dégraissé, aussi longtemps que j’ai pu mais malheureusement, ce n’était que de la dette technique pure et c’était atrocement lent.

En effet, Darktable « gère » la médiocrité de sa table lumineuse en réduisant sa taille : les panneaux latéraux gauche et droit occupent une grande surface d’affichage, ce qui laisse encore moins de place à repeindre pour la table lumineuse. Comme Ansel a supprimé le panneau latéral droit, fusionnant son contenu avec celui de gauche et le menu global, il y avait plus de surface à peindre, plus de travail CPU à effectuer, et la conception désastreuse de la table lumineuse en devenait d’autant plus nuisible.

Ce projet est maintenant achevé. Cet article est une cartographie de ce qui a changé, de ce qui a été supprimé, de ce qui a été repensé, du fonctionnement du nouveau comportement, et de ce que les utilisateurs y gagnent réellement.

Un bref historique de la mauvaise conception

Pour comprendre pourquoi la réécriture de 2022-2026 a fini par toucher autant de fichiers, il est utile de remonter d’un cran en arrière.

La vue table lumineuse, src/views/lighttable.c, accumulait des responsabilités depuis plus d’une décennie. Elle a commencé comme l’endroit gérant la grille du gestionnaire de fichiers et le zoom de base, puis a progressivement absorbé l’aperçu complet, l’aperçu épinglé, le tri, la mémoire de l’état des panneaux, le regroupement, la réorganisation par glisser-déposer, la navigation au clavier, les politiques de surcouches, la coordination avec la pellicule et une grande partie du routage des raccourcis. Si vous regardez l’historique d’avant 2022, beaucoup de commits ne servent qu’à empêcher tout cela de se disputer : sauts de position lors du zoom, bugs de défilement de l’aperçu, sélection ne restant pas synchronisée, tri et aperçu se ré-entrant dans un état à moitié cassé, visibilité des panneaux mal restaurée, bordures de groupe qui buguent, et notes ou étiquettes de couleur déplaçant la grille de façon inattendue.

Cela montre donc une incapacité complète à suivre le cycle de vie d’une image en cache (ou de n’importe quelle donnée dans ce logiciel maudit, d’ailleurs), tout en étant parfaitement à l’aise et détendu avec cela. J’ai abordé ce problème en simplifiant l’asynchronicité d’arrière-plan, mais une autre option aurait été d’utiliser un signal. Je n’arrive pas à me faire à l’idée que les 3 gars qui ont travaillé là-dessus pendant plus de 10 ans écrivaient du code C depuis deux fois plus longtemps que moi. En 2018, je ne savais même pas ce qu’était GLib.

Au cas où cela vous intéresse, la manière dont je l’ai abordé est la suivante :

  • les widgets de vignette conservent une variable d’état représentant la validité de leur propre image,
  • lorsque GTK veut redessiner la vignette, si la variable d’état indique « invalide », le widget de vignette lance un thread d’arrière-plan calculant un pipeline pour créer l’image manquante,
  • lorsque le thread d’arrière-plan retourne, il place l’image de sortie dans la vignette qui l’a appelé, par pointeur, dans une surface (en cache), met à jour la variable d’état à « valide », puis envoie un événement GTK de mise en file d’attente de redessin sur le widget (pas sur toute la grille),
  • si le widget est invisible, rien ne se passe (GTK ne met pas en file d’attente d’événements de redessin pour les widgets invisibles). La prochaine fois que GTK veut redessiner l’image, il utilise la surface en cache depuis l’intérieur du widget,
  • les changements d’historique et de métadonnées envoient un signal « image info changed » qui publie l’ID de l’image. Le thumbtable a un gestionnaire de signal à l’écoute de cela, qui trouve la vignette par ID dans la grille (depuis une table de hachage, donc O(N)) et réinitialise sa variable d’état à « invalide ». Là encore, si 100 images sont ciblées par le changement et qu’elles sont toutes invisibles, rien ne se passe.

L’architecture semble plus complexe, mais c’est en réalité moins de code et c’est tout simplement robuste : il n’y a pas de devinette.

Une réécriture de l’époque du COVID a tenté de refactoriser cela en src/dtgtk/thumbtable.c, qui est vite devenu partie prenante de la même dérive. Une fois l’abstraction dédiée du thumbtable apparue, elle est devenue le contrôleur de trafic entre l’état de la collection, le défilement, les décalages, les images actives, la navigation au clavier, le glisser-déposer, le défilement fluide, la visibilité des surcouches, la disposition zoomable, la synchronisation du tri et la synchronisation de la pellicule, réinventant de nombreuses fonctionnalités natives de GTK d’une manière pire et incomplète (la manière typique de Darktable). L’historique d’avant 2022 est plein de commits corrigeant le survol après défilement, recalculant les lignes et les décalages, gardant les images sélectionnées visibles après les changements de collection, évitant le ralentissement quadratique sur Début/Fin, corrigeant l’alignement zoomable, faisant en sorte que les images cachées ou repliées ne cassent pas la logique d’image active, et limitant le nombre de signaux émis parce que trop de mises à jour indépendantes étaient enchaînées les unes aux autres. En d’autres termes, thumbtable.c était déjà devenu l’endroit où la logique de vue, la durée de vie des vignettes et la navigation dans la collection étaient forcées de se rencontrer, bien avant que la réécriture de 2022 ne commence.

La refactorisation du thumbtable a fait apparaître un nouveau fichier, lorsque l’ancien code de la table lumineuse et de la pellicule a commencé à être factorisé en un widget de vignette commun et un thumbtable comme objet de base de tout cela : src/dtgtk/thumbnail.c. C’était la bonne direction en principe : au lieu de dessiner les vignettes séparément à plusieurs endroits, un seul widget pouvait centraliser l’activation, la sélection, les surcouches, les étoiles, les marques de rejet, le glisser-déposer, les bordures de groupe et le comportement zoomable. Mais il a suivi un chemin similaire très rapidement : l’historique d’avant 2022 montre à quelle vitesse ce widget commun est devenu surchargé. Il a vite eu à gérer les surcouches étendues, les callbacks de mise à jour MIPMAP, les jonctions de tri, les barres de défilement, la gestion de l’image active, les callbacks de la pellicule, la navigation en aperçu complet et le placement des icônes ajusté par CSS. En d’autres termes, thumbnail.c ne se contentait pas de dessiner une vignette. Il est devenu la ligne de faille partagée entre la table lumineuse, la pellicule, le tri et l’aperçu.

La pellicule a commencé comme une bande de navigation dédiée, puis a gagné le centrage sur l’image active, le glisser-déposer, les interactions avec la carte, les opérations de copier/coller, le défilement fluide, les correctifs HiDPI, la personnalisation CSS et finalement des parties du nouveau système de callbacks de vignette. Là encore, aucune de ces fonctionnalités n’est déraisonnable en soi. Le problème est que la pellicule a fini avec juste assez de comportement personnalisé pour diverger de la table lumineuse tout en essayant de réutiliser une partie de la même machinerie de vignette.

En plus de cela, il y avait le paradigme incohérent de « sélection » dans Darktable, vendu comme un « workflow sans clic », de sorte que de nombreuses opérations d’écriture pouvaient être effectuées sans sélectionner explicitement la ou les images à affecter. Cela conduisait à beaucoup d’effets indésirables et d’accidents, entraînant des pertes de données (attribuer la mauvaise note d’étoiles à la mauvaise image, ce qui la ferait disparaître de la collection courante si vous la filtriez par note) et de fastidieuses sessions d’annulation. Mais toute l’heuristique de sélection d’image était fragile elle aussi : déplacer la souris dans la fenêtre pouvait voler le focus à l’image que vous aviez explicitement verrouillée d’un clic ou d’une sélection au clavier, mais pas toujours.

Sans parler du fait que cet « agir au survol de la souris » déclenchait plusieurs requêtes SQL dans la base de données de la bibliothèque, lors du survol d’une nouvelle image, pour obtenir des métadonnées d’image à jour et rafraîchir le contenu des modules de métadonnées (métadonnées, métadonnées EXIF & IPTC, étiquettes). Tout cela parce qu’elles ne pouvaient pas être mises en cache, et elles ne pouvaient pas être mises en cache parce que Darktable est incapable de suivre le cycle de vie de ses données, il a donc besoin de tout rafraîchir en permanence. Et il videra votre batterie ce faisant.

Alors quand la réécriture a commencé après avoir forké Ansel en 2022, le problème n’était pas qu’un mauvais commit avait cassé la table lumineuse. Le problème était que trois couches d’historique étaient empilées les unes sur les autres :

  • lighttable.c portant encore le fardeau de plusieurs modes de navigation (inutiles), codés là après coup,
  • thumbtable.c concentrant la logique de défilement, de décalage, d’image active et de navigation dans la collection, qui chevauchait en partie le backend de collection et de sélection,
  • thumbnail.c concentrant de plus en plus de comportement d’interface partagé plus du code SQL.

C’est pourquoi le travail ultérieur a dû être architectural. À ce stade, il n’y avait aucun moyen réaliste de continuer à corriger les symptômes un par un.

Changements front-end

Le tri n’a jamais vraiment été (conceptuellement) une vue séparée

Une pièce importante de cette soustraction était le mode de tri . En février 2023, les vues dédiées de tri et d’aperçu ont été supprimées parce qu’elles étaient devenues mal entrelacées avec les raccourcis, le changement de vue et le branchement de cas particuliers. Le code était emmêlé et lourd, mais le vrai problème résidait dans la conception elle-même : c’était une solution sur-ingéniérée à un problème bien plus simple, résolu dans la mauvaise couche.

Le besoin était d’isoler un ensemble arbitraire d’images qui n’étaient pas nécessairement contiguës dans la collection courante, afin de décider laquelle serait celle à garder. Cela ne nécessitait pas une nouvelle disposition (ou deux…) ; cela nécessitait un filtre pour restreindre la collection à une sélection arbitraire. Nous avions déjà des filtres pour restreindre les collections par note, étiquette de couleur, statut édité/non édité, etc.

Cette différence a de l’importance. Une vue de tri dédiée duplique des problèmes que la table lumineuse doit déjà résoudre : quel ensemble d’images est actif, comment la sélection est restaurée à la sortie de la vue de tri, comment les raccourcis sont routés, comment le zoom et l’état des vignettes sont initialisés, et ce qui se passe quand vous revenez à la grille. Chaque amélioration doit alors être implémentée deux fois, une fois dans la logique de la table lumineuse et une fois dans la logique de tri, et les deux versions divergent. Sans parler du fait que le mode de tri avait un mode statique et un mode dynamique, qui différaient à la fois dans la façon dont vous interagissiez avec eux et dans leur implémentation, et très peu d’utilisateurs comprenaient de quoi il s’agissait.

Dans Ansel, le tri est revenu sous forme de filtre de collection, d’abord comme un simple moyen de restreindre l’ensemble courant, puis plus tard explicitement sous forme du bouton de filtre Restreindre à la sélection. Cela signifie que l’intention côté utilisateur est restée la même (réduire la collection courante à une sélection arbitraire d’images), mais l’implémentation a complètement changé : au lieu d’entrer dans une vue spéciale, vous restez dans la table lumineuse et dites à l’outil de filtrage de n’afficher que les images actuellement sélectionnées ou seulement les images correspondant au critère de restriction courant.

image
La nouvelle barre d'outils de filtrage unifiée. Les critères de filtrage sont inclusifs, et les icônes se comportent comme des cases à cocher : décochez pour masquer les images correspondantes, cochez tout pour tout afficher (un menu contextuel au clic droit vous donne un raccourci pour le faire en une seule étape). Des infobulles apparaissent au survol pour plus de détails. La première icône rafraîchit la collection courante en fonction des filtres. Par exemple, si vous affichez les images notées 3 étoiles et que vous rétrogradez une image à 2 étoiles, elle ne sera pas évincée automatiquement de la collection courante tant que vous ne l'aurez pas rafraîchie manuellement.

Pour les utilisateurs, le bénéfice est pratique. Le tri est désormais composable avec le reste de la logique de filtrage au lieu de vivre en dehors d’elle. Vous pouvez le combiner avec les notes, les étiquettes de couleur ou la recherche textuelle parce que ce n’est qu’un filtre de collection de plus. La gestion des raccourcis est plus simple parce qu’il y a une vue spéciale de moins à se disputer la propriété des raccourcis. Et les correctifs apportés au défilement, à la sélection, à la durée de vie des vignettes ou au comportement des surcouches bénéficient automatiquement au tri aussi, parce que le tri utilise désormais la même infrastructure que la table lumineuse au lieu d’une infrastructure parallèle.

C’était aussi l’occasion de remanier la barre d’outils de filtrage, qui mélangeait boutons et logique : une liste déroulante pour les notes (de rejeté à 5 étoiles), associée à une liste déroulante de comparaison (≠ = > < >= <=), mais des boutons à bascule pour les étiquettes de couleur. Darktable 4.0 a remplacé cela par un ensemble sur-ingéniéré de widgets configurables utilisant des paradigmes d’interface non standard qui devaient être expliqués dans des infobulles. Ansel a aplati la conception : tout est un bouton à bascule fonctionnant en mode « inclure » au sein de son groupe, permettant des sélections complexes sans interface complexe :

  1. vous avez 3 groupes (notes, étiquettes de couleur, statut d’édition), plus restreindre à la sélection et la recherche textuelle,
  2. entre ces groupes, les filtres sont exclusifs, c’est-à-dire un ET logique,
  3. à l’intérieur de ces groupes, les filtres sont additifs, c’est-à-dire un OU logique.

Par exemple, dans la capture d’écran ci-dessus, tout est activé, donc le filtrage est en pratique désactivé (on laisse tout passer). Dans l’exemple ci-dessous, on filtre pour ne garder que les images qui ont une étiquette de couleur définie et qui ont déjà été éditées, quelle que soit leur note (tous les boutons de note sont activés) :

image

Sélection : mélanger états backend et interface finit toujours par vous retomber dessus

Les images groupées étaient une autre ligne de faille cachée. Une partie de l’ancienne logique essayait de tenir compte du regroupement dans les requêtes SQL, lors de la génération de collections d’images depuis la base de données de la bibliothèque, mais les bordures de groupe, les états de survol, le comportement de sélection et la visibilité réelle sont des concepts d’interface. Cette inadéquation créait des bugs subtils : toutes les images d’un groupe pouvaient être sélectionnées, ou certaines ignorées selon des règles heuristiques qui ne correspondaient pas à ce que l’utilisateur voyait à l’écran.

J’ai corrigé cela en ramenant les décisions d’affichage des groupes dans le code de l’interface. Le SQL fournit la liste des images ; l’interface décide quels membres groupés sont visibles, repliés ou survolés. Après cela, les fonctionnalités ajoutées en 2025 sont devenues bien plus simples : les bordures de groupe ont pu être réimplémentées, rendues optionnelles, étendues à la pellicule, et rendues plus visibles au survol. Les infobulles ont pu être remplies paresseusement seulement lorsque l’utilisateur les survole. Une requête SQL par vignette au moment de l’initialisation a aussi pu disparaître parce que l’interface avait déjà assez d’état local pour savoir quand l’information de groupe était réellement nécessaire.

Pour les utilisateurs, le bénéfice n’est pas abstrait. Les bordures de groupe signifient maintenant ce qu’elles montrent, les sélections correspondent exactement à l’état visible du groupe, et les images groupées ne donnent plus l’impression d’être à moitié gérées par la base de données et à moitié par l’interface.

Un autre problème caché était la sélection. Dans l’ancien code, la sélection était trop emmêlée avec les rouages internes de la collection, l’accès SQL brut et la logique de repli. Cela semble inoffensif jusqu’à ce qu’une image soit évincée de la collection courante, qu’un groupe soit replié, ou qu’un changement de vue survienne alors qu’une autre partie du code suppose toujours le modèle de sélection précédent. Dans Darktable, les sélections d’images étaient gérées comme un backend SQL puisqu’elles sont stockées dans la bibliothèque pour être restaurées à la session suivante. Ce n’est pas un problème jusqu’à ce que le backend de sélection soit bidouillé pour deviner quelles images sont visibles dans l’interface, et que l’interface gère en fait la sélection à travers le backend. C’est le pire partage possible des fonctionnalités entre les couches backend et interface.

C’est pourquoi 2025 contient tout un ensemble de commits liés à la sélection : réécriture de l’API de sélection, suppression de l’ancienne notion de main_image, unification des getters/setters d’image active, correction de la sélection par plage, restauration de l’état de sélection à travers les changements de vue, et ajout de replis sur rowid lorsque scroll_to_selected ne peut plus retrouver l’id de l’image d’origine. La réécriture a changé l’architecture : le thumbtable calcule maintenant des sélections visibles et significatives pour l’utilisateur et passe une liste d’ids d’images à la couche de sélection, au lieu que l’interface essaie de reconstituer le sens à partir de l’état de la collection après coup. L’API SQL de sélection est devenue un simple backend « sauvegarder dans/restaurer depuis la base de données ».

Le bénéfice concret pour l’utilisateur est que la sélection par plage avec Maj + clic et l’« image sur laquelle agir » sont du What You See Is What You Get : vous ne pouvez pas sélectionner une image qui n’est pas visible à l’écran, et le backend n’a pas à deviner ce qui est visible. Et le retour par défilement vers l’image sélectionnée échoue désormais bien moins souvent dans les cas limites. Le problème caché n’était pas un raccourci cassé ; c’était que la sémantique de sélection vivait dans la mauvaise couche.

Le correctif consiste à séparer clairement ce qui relève du backend et de la gestion de la bibliothèque de ce qui relève des états et interactions de l’interface, et à fournir une interface rigide entre les deux. Cela rend les différentes parties du code mieux cloisonnées, immunisées contre les changements dans les autres parties, et gérées de façon centralisée.

Les défauts de survol et de focus venaient de la collision de plusieurs systèmes valides

Le survol souris, le survol clavier, le focus et la gestion du clic étaient chacun implémentés pour des raisons légitimes, mais ils ne partageaient pas un unique point d’arbitrage. Darktable a même maintenant un fier argument marketing écrit en fond de la table lumineuse, lorsque la collection est vide, vantant ce workflow « sans clic » qui vous permet d’écraser les métadonnées d’une image sans le vouloir ni même le savoir. Cela me fait l’effet d’un constructeur automobile faisant la publicité d’une voiture conçue pour que vous puissiez utiliser le volant avec vos genoux (et vos mains pour boire de la bière).

C’est pourquoi l’ancien comportement de la table lumineuse pouvait sembler incohérent sans aucun bug évident isolé : il était cassé par conception. Cliquer sur un bouton de surcouche pour noter l’image pouvait aussi sélectionner toute la vignette, mais noter au clavier ne le faisait pas. Le mouvement au clavier et le mouvement de la souris pouvaient tous deux croire qu’ils possédaient l’image « courante » et déclencher toutes sortes de changements destructifs de métadonnées simplement en survolant l’image et sans demander de confirmation. Filtrer les images par note d’étoiles et changer par inadvertance la note d’une image pouvait la faire disparaître de manière inattendue. Des widgets cachés pouvaient encore recevoir de la logique liée au survol. Le focus GTK pouvait décider une chose alors que la couche de vue en attendait une autre.

Les correctifs ici consistaient moins à ajouter de nouvelles fonctionnalités qu’à choisir un unique propriétaire pour l’état. La répartition de l’état de survol a été centralisée. La navigation au clavier a gagné un point de départ sensé et un chemin d’annulation explicite avec Maj+Ctrl+A. L’affichage du focus a été rendu explicite. Les widgets cachés ont cessé de participer à une logique de survol qu’ils ne pouvaient pas afficher. Cliquer sur les boutons de surcouche a cessé de déborder sur la sélection de vignette. Plus aucune (sur)écriture de métadonnées n’est effectuée sans l’appui explicite d’un bouton, les événements de survol sont tous en lecture seule.

Ansel a deux règles simples :

  • toute action qui va (sur)écrire des (méta)données n’est effectuée que sur des images explicitement sélectionnées,
  • la sélection explicite ne se fait qu’en interagissant avec quelque chose de « dur » : un clic de souris ou une frappe au clavier.

Ensuite, les événements de survol sont réservés aux événements en lecture seule.

Le bénéfice pour les utilisateurs est que la table lumineuse réagit davantage comme un modèle d’interaction unique. Le problème caché n’était pas que le survol était cassé. C’était que plusieurs types de survol étaient simultanément « justes » et donc collectivement faux.

Changements backend

Re-parentage du thumbtable entre table lumineuse et pellicule

L’un des problèmes les plus profondément cachés était la propriété des widgets. Dans Darktable, la même grille de thumbtable était re-parentée entre les contextes table lumineuse et pellicule. Sur le papier, cela évitait la duplication de code et la prolifération de widgets, et cela semble astucieux. En pratique, cela faisait dépendre l’état de défilement, l’initialisation des vignettes, le ramasse-miettes et la durée de vie des événements de l’endroit où le widget avait le plus récemment vécu. Sans parler du fait que le re-parentage était lent, si bien que faire des allers-retours entre la table lumineuse et la chambre noire était retardé d’environ 1 s, le temps nécessaire à GTK pour recalculer les nouvelles tailles de vignettes, éventuellement demander de nouvelles vignettes au cache, et redessiner les widgets.

Cela se manifestait de façons très concrètes. La position de défilement pouvait sauter ou devenir incohérente. Les vignettes pouvaient être cachées, affichées ou détruites au mauvais moment. Le ramasse-miettes devenait plus difficile à raisonner parce que la hiérarchie des widgets n’était pas stable. Bien sûr, tout cela a été résolu commit après commit, mais en laissant le code dans un état de complexité impossible à maintenir.

Le vrai correctif était d’arrêter d’essayer d’être astucieux. La table lumineuse et la pellicule ont désormais des thumbtables séparés. Cela signifie un peu plus de structure explicite dans le code, mais bien moins d’états accidentels. Le bénéfice pour les utilisateurs est visible dans un défilement plus régulier, moins de défauts de disposition lors des changements de vue, et une pellicule qui se comporte comme un véritable frère de la table lumineuse au lieu d’un fragment réutilisé.

Les changements de menu et de disposition n’étaient pas cosmétiques

Le fait que le menu global soit devenu véritablement global en avril 2025 n’était pas un exercice de marque. Avant cela, de nombreuses commandes étaient implémentées comme si elles appartenaient à des modules locaux même lorsqu’il s’agissait réellement d’actions au niveau de l’application. Certaines d’entre elles étaient entièrement cachées derrière des raccourcis clavier et connues seulement de ceux qui lisaient la documentation. Beaucoup d’entre elles, je ne les ai découvertes qu’en supprimant leur code. Il en allait de même pour les informations d’image, le changement de vue et les anciennes boîtes à outils de vue. Elles vivaient à des endroits faciles à contourner une fois dans le code, mais difficiles à maintenir de façon cohérente à travers les vues.

Déplacer les informations d’image dans la barre de menu global, déplacer le sélecteur de vue vers un emplacement de premier niveau plus clair, et plus tard supprimer la barre d’outils en bas au centre étaient tous des conséquences du même diagnostic : le centre de l’interface devrait appartenir à l’interaction avec l’image, pas à des vestiges historiques. La suppression de la frise chronologique et la décision de ne pas afficher de pellicule vide sont de plus petits exemples de la même logique. Même le fait qu’Entrée ouvre l’image sélectionnée en chambre noire trouve sa place ici : cela aligne le comportement sur le rôle premier de la table lumineuse au lieu de préserver des habitudes historiques accidentelles.

Le bénéfice pour l’utilisateur n’est pas « le menu est plus joli ». C’est que les commandes se trouvent à des endroits correspondant à leur portée, et que la table lumineuse elle-même a moins de contrôles se disputant l’espace avec la grille. Sans parler du fait que les « modules » qui sont en réalité des grilles (non uniformes) de boutons ne sont que des menus déguisés avec une pire conception, ils sont donc désormais transformés en entrées de menu.

Le menu global a aussi été l’occasion d’apporter à l’interface des fonctionnalités qui ne vivaient jusqu’à présent que dans des scripts shell cachés (!) : le préchargement des vignettes pour la collection courante et la purge des vignettes du cache disque. Elles étaient demandées depuis des années dans l’interface, mais ne trouvaient leur place nulle part dans la conception de l’interface de Darktable centrée sur les modules.

Mais… cette barre latérale droite de modules qui ne voulaient pas être des menus était une bénédiction, parce qu’elle enlevait beaucoup de surface à peindre avec des images et rendait les vignettes plus petites. La supprimer a fait apparaître les mauvaises performances du dessin des vignettes dans toute leur splendeur : rien n’était mis en cache, ni les surfaces d’image ni les métadonnées, et tout était re-récupéré en permanence. La réécriture était inévitable ; l’architecture était cassée.

Le zoom n’est devenu utilisable qu’après avoir rendu les tailles de vignettes cohérentes

La première tentative de table lumineuse zoomable a échoué pour une raison structurelle : la géométrie des vignettes, les tailles de mipmap en cache et la navigation au niveau de la vue ne partageaient pas le même modèle. C’est pourquoi la fonctionnalité a d’abord dû être supprimée. Si j’avais continué à la peaufiner sur place, le résultat n’aurait été qu’une fonctionnalité cassée à l’apparence plus soignée.

Le travail ultérieur a corrigé la chaîne de dépendances dans l’ordre inverse. Le backend de vue a gagné l’échafaudage pour le zoom de la table lumineuse, puis le cache de vignettes a appris à raisonner de façon plus cohérente sur les tailles de vignettes, une API de vignette en pleine résolution a été préparée, puis le bornage, le panoramique et l’auto-panoramique basé sur le barycentre ont été ajoutés par-dessus. Ce n’est qu’une fois que les couches inférieures se sont accordées sur la taille d’image et la stratégie de récupération que le zoom à 200 % est devenu une fonctionnalité défendable.

Le bénéfice pour les utilisateurs est que le zoom est désormais lié au cache et à la navigation au lieu de les combattre. Faire glisser pour un panoramique, Maj + glisser à travers les vignettes visibles, et le panoramique automatique vers le barycentre des détails fonctionnent tous parce que le pipeline sous-jacent sait ce que sont censées être les vignettes zoomées.

Le sens confus de « zoom » dans la table lumineuse a été mis à jour : le terme « zoom » était utilisé à la fois pour le zoom extérieur (nombre d’images par ligne, ce qui affecte indirectement leur taille visible) et pour le zoom intérieur (grossissement au sein du cadre de l’image). Nous avons donc maintenant colonnes pour le zoom extérieur, et zoom pour le grossissement.

image

Ne prêtez pas attention à la croix à la place du symbole - dans le compteur de colonnes, c’est un bug de GTK avec le thème Breeze de KDE/Plasma .

Cela a permis d’implémenter une fonctionnalité que j’avais en tête depuis longtemps : l’auto-panoramique barycentrique lors du grossissement des vignettes.

image
Zoom pour ajuster
image
Activation du zoom à 100 %

Vous pouvez voir que, lors de l’activation du zoom à 100 %, les images sont automatiquement alignées sur leur contenu bien qu’elles aient un cadrage et des rapports d’aspect complètement différents. Cela utilise une analyse de caractéristiques par décomposition en ondelettes, à partir de laquelle nous calculons les coordonnées du barycentre des détails. Ce n’est pas parfait parce que cela ne cible pas la même partie du visage d’une image à l’autre, mais cela nous donne le visage dans tous les cas. C’est un projet que j’avais mis en veilleuse depuis longtemps, mais il n’était pas possible avec la conception précédente de la table lumineuse.

Il va sans dire que le glissement dans les vignettes zoomées a été rétabli, et le glissement dans toutes les vignettes zoomées a aussi été ajouté (Maj+Glisser). Cette fonctionnalité était demandée depuis longtemps dans Darktable mais n’était clairement pas possible dans leur conception merdique.

Ralentissement des métadonnées auto-infligé

L’ancienne table lumineuse posait sans cesse à la base de données les mêmes questions, une vignette à la fois. Sur une petite collection, c’est facile à manquer. Sur une plus grande, cela se manifeste par des micro-saccades à l’ouverture de la vue, à l’affichage des surcouches, au survol des groupes ou au rafraîchissement des données sensibles à l’historique. Le problème caché n’était pas que le SQL brut était lent dans l’absolu ; c’était la répétition et le rythme de ces requêtes.

Le correctif est venu par étapes. Les métadonnées en lecture seule ont commencé à être mises en cache depuis les structures d’image. Puis, en février 2026, les métadonnées de toute une collection ont pu être récupérées avec une seule requête SQL au lieu d’une requête par image. Le thumbtable pouvait amorcer le cache d’images à partir de la collection qu’il s’apprêtait à afficher. La mise en cache des infos de vignette a été refactorisée en un utilitaire puis fusionnée plus tard dans dt_image_t afin que les métadonnées n’aient pas à rebondir entre des structures parallèles. Même de petits changements comme la suppression des pings d’empreinte d’historique au redessin comptent ici, parce qu’ils suppriment des allers-retours invisibles des chemins critiques.

Beaucoup de requêtes SQL par vignette inutiles ont été supprimées, ainsi que du code SQL dans le code de l’interface de la table lumineuse, si bien que les couches fonctionnelles sont désormais correctement séparées.

Le bénéfice pour l’utilisateur est exact : l’ouverture de grandes collections bloque moins, les affichages chargés de surcouches hésitent moins, et les mises à jour d’historique ou de métadonnées se propagent avec moins de pauses visibles.

Le cache mipmap devait être corrigé lui aussi

Le cache mipmap est la couche qui charge les images RAW et les vignettes pré-générées dans la RAM, et les vide lorsque la mémoire se fait rare. C’était l’une des sources cachées les plus profondes de bugs visibles. Quand tout s’alignait, cela fonctionnait suffisamment bien. Quand le fichier source était plus petit que prévu, quand un aperçu JPEG embarqué était bizarre, quand l’empreinte d’historique était indéfinie, ou quand l’invalidation du cache disque était en retard sur les éditions, l’utilisateur ne vivait pas « un bug de cache ». Il voyait des vignettes périmées, des aperçus cassés ou des images qui refusaient de se rafraîchir.

C’est pourquoi une grande partie du travail de 2025 sur le cache a l’air chirurgical. La logique d’allocation des tampons a été réécrite parce que la propriété était trop emmêlée. La gestion des entrées sous-dimensionnées a été améliorée parce que les hypothèses sur la taille des entrées débordaient trop loin. Les aperçus embarqués ont cessé d’être écartés de façon aussi agressive parce que les aperçus trop petits restaient utiles pour la cohérence entre les niveaux de zoom. Les aperçus JPEG en fichier annexe (sidecar) ont commencé à être utilisés lorsqu’ils étaient présents au lieu des vignettes embarquées du RAW. L’invalidation du cache a été durcie, et les vignettes en cache ont réellement été supprimées du disque quand elles auraient dû l’être. Les mipmaps régénérés ont commencé à réécrire leur empreinte dans la base de données en synchronie avec l’état de l’historique, et le cas de l’empreinte d’historique indéfinie a cessé de laisser des mipmaps non sauvegardés.

Le bug le plus sournois que j’ai trouvé était enfoui dans la complexité insensée de l’entrelacement entre thumbtable, vue et cache mipmap. Alors que le rendu des vignettes était délégué à un thread séparé pour la performance (comme il se doit), et que plus d’un thread pouvait être lancé pour (soi-disant) traiter plusieurs vignettes à la fois (en utilisant éventuellement plusieurs GPU), tous les threads de traitement et l’interface se disputaient en fait le verrouillage des caches mipmap et image, ce qui ne faisait que bloquer l’interface pendant que les pipelines s’exécutaient. C’est seulement parce que j’ai travaillé en supposant que j’avais réécrit les choses dans les règles de l’art, et que je savais que la manière conforme aux règles de l’art devrait être plus rapide que ça, que j’ai continué à creuser jusqu’à trouver pourquoi ce n’était toujours pas aussi rapide qu’attendu, en simplifiant tout couche par couche au passage.

Le bénéfice pour les utilisateurs est que le cache est plus digne de confiance. Après des éditions, la table lumineuse est moins susceptible d’afficher une vignette obsolète. Sur des fichiers délicats, la génération d’aperçu échoue moins souvent. Lors de visites répétées à la même collection, le cache disque se comporte davantage comme un cache et moins comme une archive de captures d’écran périmées. Mais tout cela, sans avoir à tout rafraîchir/recalculer/redessiner en permanence juste pour être sûr.

De plus, les options pour utiliser les JPEG embarqués ou forcer un recalcul ont été déplacées des préférences vers le menu global et peuvent être modifiées durant l’exécution :

image

C’est un autre exemple où le nettoyage et la simplification du backend ont ouvert la voie à l’extension du frontend et à de nouvelles fonctionnalités qui ont simplement du sens.

Sûreté vis-à-vis des threads et conditions de course

Une raison classique pour laquelle ces bugs ont duré si longtemps est qu’ils exigeaient que l’utilisateur soit plus rapide que le code : défiler rapidement, quitter une vue alors que les vignettes sont encore en cours de construction, redimensionner alors qu’une surface d’arrière-plan est encore en cours de production, ou fermer un widget juste avant qu’un thread de travail ne pousse une mise à jour.

C’est pourquoi le travail de 2025-2026 sur la sûreté vis-à-vis des threads a de l’importance. La récupération des vignettes a été déplacée plus profondément dans les travaux d’arrière-plan, puis plus tard explicitement différée afin que le rendu de la chambre noire puisse garder la priorité. La destruction des vignettes a été déplacée vers des points de nettoyage plus sûrs. Les travaux d’arrière-plan produisant des vignettes ont appris à s’annuler eux-mêmes lorsque le widget qu’ils servaient disparaissait. Les surfaces d’image ont été protégées par des mutex. Les pointeurs libérés ont été mis à nul. Les tampons d’image sont passés à des chemins d’allocation mieux gérés. Le but de tout cela n’était pas « plus de threads » ; c’était d’empêcher les anciens threads d’écrire dans un état mort ou de se disputer les verrous.

Le résultat côté utilisateur, c’est moins de segfaults, moins de défauts aléatoires lors du défilement rapide, et moins de cas où l’interface semble se faire concurrence à elle-même sous charge.

Plusieurs verrous de threads, ajoutés au fil des années pour rustiner des problèmes, ont été supprimés eux aussi. Beaucoup étaient redondants (mais ne le montraient pas sous la complexité folle de l’ensemble), certains nuisaient activement aux performances, et tous cachaient une mauvaise conception. Nous avons maintenant moins de points de verrouillage, mais le résultat est plus lisible et plus robuste.

Par conséquent, les widgets de vignettes sont désormais des objets entièrement autonomes. Ils gèrent en interne leurs propres threads de pipeline de rendu de vignette, qui interagissent directement avec leur propre surface d’image mise en cache, de sorte qu’ils peuvent les créer ou les détruire eux-mêmes. Cette image mise en cache n’est invalidée que lorsque l’historique de l’image change, ce qui est rendu explicite par le backend de l’historique de développement. Parce que le widget de vignette connaît son propre état (visible ou non, ayant besoin d’un rafraîchissement d’image ou non, taille, mode de focus-peaking, etc.), il devient bien plus robuste qu’en essayant de gérer toutes ces choses depuis des couches de plus haut niveau qui ne parviennent pas à communiquer entre elles.

Cela n’était pas possible avec la conception de Darktable, car il ne cessait d’ajouter/supprimer dynamiquement des widgets de vignettes à la vue de table lumineuse courante, en fonction de la position de la ligne flottante, ce qui était une tentative de gérer les ralentissements dus à tous les threads qui se disputaient l’accès au cache. Mais en plus, les widgets de vignettes de Darktable essayaient d’acquérir immédiatement une image depuis le cache de mipmaps, au moment de leur création, ce qui faisait grimper en flèche l’utilisation du CPU et des E/S mémoire, et gelait effectivement l’interface. Mais « corriger » cela en réduisant l’espérance de vie des widgets de vignettes rendait impossible de les laisser gérer leur propre état en interne, si bien que cela devait être fait par des couches de haut niveau, qui devaient communiquer entre elles pour mettre à jour les états, ce qui se produisait bien à certains endroits, mais au prix d’une complexité insupportable.

Au lieu de cela, les vignettes d’Ansel sont toutes initialisées en une seule fois, mais elles n’acquièrent une image depuis le cache de mipmaps que de manière paresseuse, une fois qu’elles deviennent visibles, puis la mettent en cache en interne. Cela permet de conserver un défilement réactif de la table de vignettes, même avec une collection de 500 images qui génèrent toutes leur image.

Les aperçus d’importation améliorés comme effet secondaire

La fenêtre d’importation était affectée par ce travail pour la même raison que la table lumineuse : elle a besoin d’une extraction rapide des aperçus, d’une inspection des métadonnées et d’un comportement de repli sain sur les fichiers partiellement pris en charge. Une fois les chemins de mipmap et de vignette réécrits, le chargement des aperçus raw à l’importation est devenu plus rapide, et la prise en charge des TIFF/DNG s’est également améliorée.

C’est un bon exemple de la raison pour laquelle la réécriture devait avoir lieu bas dans la pile. Si la machinerie d’aperçu sous-jacente est inefficace ou fragile, la table lumineuse comme l’importation héritent de la même douleur. Une fois cette machinerie réécrite, les deux en ont bénéficié.

Changements architecturaux globaux

Dans mon article précédent, j’ai montré comment le cache du pipeline de travail rend les allers-retours entre la table lumineuse et la chambre noire presque instantanés, parce qu’il n’a pas besoin de recalculer toute l’image. Le travail effectué ici sur l’interface graphique résout le même problème de délais de changement de vue, mais au niveau de l’interface graphique. Il n’y a donc aucune latence lors du passage entre les deux vues.

C’est important parce que la latence du changement de vue a été utilisée comme excuse pour dupliquer des fonctionnalités (modules/boîtes à outils) entre la table lumineuse et la chambre noire, ce qui ne fait qu’augmenter l’encombrement de l’interface graphique. Ainsi, toute cette amélioration du backend permet d’améliorer la conception de l’interface graphique en spécialisant chaque vue pour une seule tâche :

  • la gestion des métadonnées pour la table lumineuse (plus, évidemment, le tri par élimination),
  • l’édition d’image pour la chambre noire.

Par conséquent, les boîtes à outils métadonnées et étiquettes ont été retirées de la chambre noire.

Combien de lignes de code la réécriture a-t-elle épargnées ?

En comptant uniquement les lignes non commentées et non vides avec cloc, et en comparant le dernier arbre avant le 1er janvier 2022 à l’arbre actuel pour les principaux fichiers abordés ici, la réécriture a épargné 4 714 lignes de code au total.

Le périmètre de ce décompte est :

  • l’ensemble du répertoire data/themes/ avant 2022, comparé à l’actuel 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,
  • plus les fichiers supprimés purement et simplement par la refonte ou le nettoyage de l’interface graphique des collections : src/dtgtk/culling.[ch], src/libs/tools/view_toolbox.c, src/libs/collect.h et src/libs/recentcollect.c.

Dans ce périmètre, le total est passé de 15 257 lignes de code avant 2022 à 10 543 aujourd’hui. La réécriture a donc épargné 4 714 lignes de code au total. Les plus grosses économies proviennent de la suppression pure et simple de la vue de tri par élimination (src/dtgtk/culling.c) (-1 406 lignes), de la réduction de la table lumineuse (src/views/lighttable.c) (-976), de la suppression de 7 anciennes feuilles de style de thèmes et de leur fusion en une seule (-864 à travers data/themes/), de la réduction de src/dtgtk/thumbnail.c (-460), de src/dtgtk/thumbtable.c (-458), de la suppression de src/libs/recentcollect.c (-358) et de la réduction de src/views/view.c (-314). Certains fichiers ont bien grossi, notamment src/libs/tools/filter.c (+215) et src/libs/collect.c (+157), parce qu’une partie de l’objectif était de ramener les comportements de cas particuliers, tels que le tri par élimination et les anciennes branches de l’interface graphique des collections, dans une infrastructure partagée plus simple, plutôt que de les conserver dans des vues parallèles et des modules annexes.

Le nombre de lignes n’est pas en soi une métrique de qualité. Bien des réécritures ne font que déplacer du code. Mais ici, le chiffre correspond au changement de conception : moins de comportements dupliqués, moins de vues parallèles, moins de bidouilles compensatoires et moins d’endroits où l’interface graphique, la couche de vue et le cache devaient tous résoudre deux fois le même problème.

Tout ce que j’ai corrigé, je l’ai corrigé en simplifiant la logique et le code. Aucun contournement n’était autorisé.

Benchmarks

Tous les temps d’exécution ont été calculés sur un ordinateur portable Lenovo ThinkPad P51 (CPU Intel Xeon E3-1505M v6 @ 3.00GHz, GPU Nvidia Quadro M2200 avec 4 Go de VRAM, 32 Go de RAM, écran 4K), CPU en mode performance, Linux Fedora 41 avec bureau KDE/Plasma. Les temps d’exécution du pipeline de pixels ne sont pas comparés (hors périmètre ; voir l’article précédent). Ansel Master est pris au commit 09749f1d  (21 févr. 2026).

DescriptionAnsel MasterDarktable 5.0
Temps entre le démarrage de l’application et le dessin de la dernière vignette de la table lumineuse (même collection)2.12 s7.49 s
Temps pour passer de la table lumineuse à la chambre noire (même image)0.2 s1.2 s
Temps pour faire défiler (début->fin) la même collection de 471 images*0.7 s5.0 s

*: vignettes préchargées dans le cache disque dans les deux cas, 5 colonnes de vignettes par ligne, résolution 4K, sans barre latérale droite.

En guise de « correctif », Darktable 5.x nous a gratifiés d’un splendide écran de démarrage, ce qui est un aveu plus qu’autre chose.

Les mesures suivantes ont été effectuées sur batterie, en mode économie d’énergie, avec l’application laissée inactive (aucune interaction de l’utilisateur) pendant 5 minutes, à l’aide d’Intel Powertop. La consommation de référence de tout le système d’exploitation inactif est de 1.6 % de CPU. (La puissance est donnée pour l’application seule, le % de CPU est donné pour l’ensemble du système) :

VueAnsel MasterDarktable 5.0
Table lumineuse1.8 % CPU, puissance : 0.85 mW2.7 % CPU, puissance : 103 mW
Chambre noire1.8 % CPU, puissance : 7.65 mW1.8 % CPU, puissance : 22 mW

Ces chiffres représentent la consommation électrique de référence de l’interface graphique seule (GTK, workers en arrière-plan, minuteries planifiées, etc.). Darktable perd des performances à travers l’interface graphique, et le travail fastidieux réalisé en 2023-2024 sur l’optimisation des modules de traitement de pixels pour gagner 15 à 50 ms supplémentaires est complètement hors de propos.

Conclusion

À ce stade, je suis fermement convaincu que le « projet » Darktable n’attire que des « développeurs » incapables de faire une distinction cognitive entre interface graphique et backend, même si leur vie en dépendait. Ainsi, les problèmes d’interface graphique sont résolus dans le backend, les problèmes de backend sont résolus dans l’interface graphique, et cela fait croître la complexité du code de manière incontrôlable avec le temps, ce qui justifie plus tard d’ajouter de nouvelles fonctionnalités en bidouillant le moins de code possible dans une base de code que plus personne ne comprend. Sans compter que rien de tout cela n’était documenté, si bien que j’ai dû le rétro-concevoir péniblement sur plusieurs années, en simplifiant récursivement un peu ici et un peu là, jusqu’à ce que cela converge finalement vers une logique d’ensemble propre.

Inutile de préciser que tous les contournements et « correctifs rapides » qui avaient été ajoutés ont été supprimés. Tous dataient d’après 2020, ce qui témoigne d’une tendance préoccupante à la dégradation de la qualité du code.

Ce projet de nettoyage m’a volé 4 ans de ma vie, ne m’a procuré aucun plaisir, et les personnes qui ont introduit toutes les régressions que j’ai péniblement corrigées doivent être tenues responsables des conséquences de leurs actes. Il y a une énorme différence entre ne pas avoir assez de temps pour faire les choses correctement et consommer un grand nombre d’heures de travail pour empirer les choses. Et ensuite, refuser d’admettre que l’on a empiré les choses, refuser d’accepter qu’il y a un problème, et nourrir son biais de confirmation en n’écoutant que les retours des gens satisfaits est la preuve ultime de la stupidité.

Darktable s’est dégradé en une merde, et je viens d’expliquer, techniquement, pourquoi. L’équipe de Darktable et son forum satellite de tech bros  voudraient faire croire aux gens que je me suis fâché contre eux parce qu’ils n’acceptaient pas mes changements, et que tout cela n’est qu’un problème interpersonnel. Pour le profane qui ne comprend pas ce que j’ai écrit ici et dans les articles précédents, il est plus facile de croire à une colère interpersonnelle que de comprendre comment une succession de mauvaises décisions techniques sur plusieurs années a fait de moi la victime des problèmes qu’ils ont créés, parce que j’étais le seul ici à travailler à temps plein, en en dépendant pour vivre. Je me suis fâché contre eux parce qu’ils venaient encore et encore chier sur mon pas de porte, et que je devais nettoyer à répétition. C’est une forme de violence vraiment difficile à voir et à reconnaître parce qu’elle ne se manifeste pas matériellement : c’est une façon de rendre votre vie plus difficile, au quotidien, pas à pas, juste parce qu’une bande d’amateurs quinquagénaires sans compétences voulaient faire partie de quelque chose de cool sans se rendre compte de l’effet préjudiciable de leurs contributions sur l’ensemble du projet.

Et c’est à moi qu’il est revenu de nettoyer le gâchis parce que j’étais apparemment le seul à me soucier des régressions, des bugs et plantages aléatoires bizarres, des pires « innovations » d’interface graphique qui ont dissuadé même ma propre femme d’utiliser Darktable parce que c’est tout simplement écrasant, et de tous les nouveaux ralentissements qui ne cessent de s’accumuler au fil des ans. Comme Chris Elston me l’a dit sur le chat IRC de Darktable, en 2022, avant que je le quitte à jamais, à propos de choses que j’avais corrigées en 2019 et qu’ils avaient de nouveau cassées en 2022 : « ferme-la et corrige ». Si ce n’est pas de la violence, je ne sais pas ce que c’est. Et maintenant ils essaient de répandre l’idée que c’était moi le toxique. Toute l’équipe est toxique. Leur culture de travail négligente est toxique. Leur façon de s’enthousiasmer pour tout, du moment que c’est nouveau, sans réfléchir au coût de maintenance, à la duplication des fonctionnalités et à l’accablement général de l’utilisateur, est toxique. Leur manque de préoccupation pour l’avenir du projet et pour les conséquences des choix qu’ils font est toxique.

Et ce qui est particulièrement toxique, c’est que, sur discuss.pixls.us, chaque message qui fait mon éloge ou celui d’Ansel est signalé et masqué. Il n’y a pas de liberté d’expression sur un forum de logiciel libre. Ils ont juste transformé le communisme en stalinisme.


Translated from English by : Aurélien Pierre, ChatGPT, Claude, Claude (Anthropic). In case of conflict, inconsistency or error, the English version shall prevail.