Tra gennaio 2022 e marzo 2026, Ansel ha registrato 297 commit non di merge riguardanti la griglia del tavolo luminoso, le sue miniature e la loro pipeline di rendering e caching. Ho cercato di arrangiarmi con il trasandato codice del tavolo luminoso di Darktable, solo sgrassato, il più a lungo possibile, ma purtroppo era puro debito tecnico ed era penosamente lento.

In effetti, Darktable «gestisce» la scadente qualità del suo tavolo luminoso riducendone le dimensioni: i pannelli laterali di sinistra e di destra occupano molta superficie di visualizzazione, il che lascia ancora meno area da ridisegnare per il tavolo luminoso. Poiché Ansel ha rimosso il pannello laterale di destra, fondendone il contenuto con quello di sinistra e con il menu globale, c’era più superficie da disegnare, più lavoro per la CPU da svolgere, e il pessimo design del tavolo luminoso è diventato tanto più dannoso.

Quel progetto è ora completo. Questo articolo è una mappa di ciò che è cambiato, di ciò che è stato rimosso, di ciò che è stato riprogettato, di come funziona il nuovo comportamento e di ciò che gli utenti ne guadagnano concretamente.

Una breve storia del cattivo design

Per capire perché la riscrittura del 2022-2026 abbia finito per toccare così tanti file, è utile guardare un passo più indietro.

La vista del tavolo luminoso, src/views/lighttable.c, aveva accumulato responsabilità per più di un decennio. È nata come il luogo che gestiva la griglia del file-manager e lo zoom di base, poi ha gradualmente assorbito l’anteprima completa, l’anteprima fissa, il culling, la memoria dello stato dei pannelli, il raggruppamento, il riordinamento tramite drag-and-drop, la navigazione da tastiera, le politiche di sovrapposizione, il coordinamento della filmstrip e una grande quantità di instradamento delle scorciatoie. Se guardi la cronologia precedente al 2022, molti commit riguardano semplicemente il fatto di impedire che queste cose si combattano tra loro: salti di offset durante lo zoom, bug nello scorrimento dell’anteprima, selezione che non resta sincronizzata, culling e anteprima che rientrano in uno stato mezzo rotto, visibilità dei pannelli non ripristinata correttamente, bordi di gruppo che presentano glitch, e valutazioni o etichette colore che spostano la griglia in modo imprevisto.

Questo dimostra dunque una completa incapacità di tenere traccia del ciclo di vita di un’immagine in cache (o di qualsiasi dato in quel software maledetto, se è per questo), pur essendo del tutto a proprio agio e rilassati al riguardo. Ho affrontato questo problema semplificando l’asincronicità in background, ma un’altra opzione sarebbe stata usare un segnale. Non riesco a farmene una ragione: i 3 tizi che ci hanno lavorato per oltre 10 anni scrivono codice C da un tempo doppio rispetto a me. Nel 2018 non sapevo nemmeno cosa fosse GLib.

Nel caso ti interessasse, il modo in cui l’ho affrontato è :

  • i widget delle miniature mantengono una variabile di stato che rappresenta la validità della propria immagine,
  • quando GTK vuole ridisegnare la miniatura, se la variabile di stato dice «non valida», il widget della miniatura genera un thread in background che calcola una pipeline per creare l’immagine mancante,
  • quando il thread in background restituisce il risultato, inserisce l’immagine di output nella miniatura che l’ha chiamato, tramite puntatore, in una surface (in cache), aggiorna la variabile di stato a «valida», poi invia un evento GTK di accodamento del ridisegno sul widget (non sull’intera griglia),
  • se il widget è invisibile, non succede nulla (GTK non accoda eventi di ridisegno ai widget invisibili). La volta successiva in cui GTK vuole ridisegnare l’immagine, usa la surface in cache all’interno del widget,
  • le modifiche alla cronologia e ai metadati inviano un segnale «image info changed» che pubblica l’ID dell’immagine. La thumbtable ha un gestore di segnale in ascolto su di esso, che trova la miniatura per ID nella griglia (da una hashtable, quindi O(N)) e reimposta la sua variabile di stato a «non valida». Di nuovo, se 100 immagini sono interessate dalla modifica e tutte sono invisibili, non succede nulla.

L’architettura sembra più complessa, ma in realtà è meno codice ed è semplicemente robusta: non c’è nulla da indovinare.

Una riscrittura dell’era COVID ha tentato di rifattorizzare quella roba in src/dtgtk/thumbtable.c, che ben presto è entrato a far parte della stessa deriva. Una volta comparsa l’astrazione dedicata thumbtable, questa è diventata il controllore del traffico tra stato della collezione, scorrimento, offset, immagini attive, navigazione da tastiera, drag-and-drop, scorrimento fluido, visibilità delle sovrapposizioni, layout zoomabile, sincronizzazione del culling e sincronizzazione della filmstrip, reinventando molte funzionalità native di GTK in un modo peggiore e incompleto (il tipico modo di Darktable). La cronologia precedente al 2022 è piena di commit che correggono il mouse-over dopo lo scorrimento, ricalcolano righe e offset, mantengono visibili le immagini selezionate dopo i cambi di collezione, evitano il lag quadratico su Home/Fine, correggono l’allineamento zoomabile, fanno in modo che le immagini nascoste o compresse non rompano la logica dell’immagine attiva, e limitano il numero di segnali emessi perché troppi aggiornamenti indipendenti venivano concatenati insieme. In altre parole, thumbtable.c era già diventato il luogo in cui logica della vista, durata di vita delle miniature e navigazione della collezione erano costretti a incontrarsi, molto prima che iniziasse la riscrittura del 2022.

Il refactoring della thumbtable ha fatto comparire un nuovo file, quando il vecchio codice del tavolo luminoso e della filmstrip ha iniziato a essere fattorizzato in un widget di miniatura comune e nella thumbtable come oggetto base di tutto questo : src/dtgtk/thumbnail.c. Quella era la direzione giusta in linea di principio: invece di disegnare le miniature separatamente in più punti, un unico widget poteva centralizzare attivazione, selezione, sovrapposizioni, stelle, contrassegni di rifiuto, drag-and-drop, bordi di gruppo e comportamento zoomabile. Ma ha seguito un percorso simile molto rapidamente : la cronologia precedente al 2022 mostra quanto velocemente quel widget comune sia diventato sovraccarico. Ben presto si occupava di sovrapposizioni estese, callback aggiornate da MIPMAP, giunzioni di culling, barre di scorrimento, gestione dell’immagine attiva, callback della filmstrip, navigazione dell’anteprima completa e posizionamento delle icone regolato via CSS. In altre parole, thumbnail.c non si limitava a disegnare una miniatura. È diventato la linea di faglia condivisa tra tavolo luminoso, filmstrip, culling e anteprima.

La filmstrip è nata come una striscia di navigazione dedicata, poi ha acquisito la centratura sull’immagine attiva, il drag-and-drop, le interazioni con la mappa, le operazioni di copia/incolla, lo scorrimento fluido, le correzioni HiDPI, la personalizzazione CSS e infine parti del nuovo sistema di callback delle miniature. Di nuovo, nessuna di quelle funzioni è irragionevole di per sé. Il problema è che la filmstrip ha finito per avere giusto abbastanza comportamento personalizzato da divergere dal tavolo luminoso, pur cercando ancora di riutilizzare parte dello stesso macchinario delle miniature.

In aggiunta a ciò, c’era l’incoerente paradigma di «selezione» in Darktable, venduto come un «flusso di lavoro senza clic», per cui molte operazioni di scrittura potevano essere gestite senza selezionare esplicitamente l’immagine (o le immagini) da modificare. Questo portava a molti effetti indesiderati e incidenti, con conseguente perdita di dati (attribuire la valutazione a stelle sbagliata all’immagine sbagliata, il che la faceva sparire dalla collezione corrente se la filtravi per valutazione) e a tediose sessioni di annullamento. Ma anche l’intera euristica della selezione dell’immagine era fragile: muovere il mouse nella finestra poteva rubare il focus all’immagine che avevi esplicitamente bloccato con un clic o una selezione da tastiera, ma non sempre.

Per non parlare del fatto che quell’«agire al passaggio del mouse» attivava diverse query SQL nel database della libreria, al passaggio su una nuova immagine, per ottenere i metadati aggiornati dell’immagine e aggiornare il contenuto dei moduli dei metadati (metadati, metadati EXIF & IPTC, tag). Tutto ciò perché non potevano essere messi in cache, e non potevano essere messi in cache perché Darktable è incapace di tracciare il ciclo di vita dei suoi dati, quindi ha bisogno di aggiornare tutto continuamente. E così facendo ti prosciugherà la batteria.

Quindi, quando la riscrittura è iniziata dopo aver forkato Ansel nel 2022, il problema non era che un cattivo commit avesse rotto il tavolo luminoso. Il problema era che tre strati di storia erano impilati l’uno sull’altro:

  • lighttable.c che portava ancora il fardello di diverse (inutili) modalità di navigazione, codificate lì come ripensamenti,
  • thumbtable.c che concentrava la logica di scorrimento, offset, immagine attiva e navigazione della collezione, che si sovrapponeva in parte al backend della collezione e della selezione,
  • thumbnail.c che concentrava sempre più comportamento GUI condiviso più codice SQL.

Ecco perché il lavoro successivo doveva essere architetturale. A quel punto, non c’era alcun modo realistico di continuare a correggere i sintomi uno per uno.

Modifiche al front-end

Il culling non è mai stato davvero (concettualmente) una vista separata

Un pezzo importante di quella sottrazione era la modalità culling . A febbraio 2023, le viste dedicate di culling e anteprima sono state rimosse perché erano diventate mal intrecciate con scorciatoie, cambi di vista e ramificazioni per casi speciali. Il codice era aggrovigliato e ingombrante, ma il vero problema era nel design stesso: era una soluzione sovradimensionata a un problema molto più semplice, risolta nello strato sbagliato.

L’esigenza era isolare un insieme arbitrario di immagini che non erano necessariamente contigue nella collezione corrente, al fine di decidere quale sarebbe stata quella da tenere. Ciò non richiedeva un nuovo layout (o due…); richiedeva un filtro per restringere la collezione a una selezione arbitraria. Avevamo già filtri per restringere le collezioni per valutazione, etichetta colore, stato modificato/non modificato, ecc.

Quella differenza conta. Una vista culling dedicata duplica problemi che il tavolo luminoso deve già risolvere: quale insieme di immagini è attivo, come viene ripristinata la selezione all’uscita dalla vista culling, come vengono instradate le scorciatoie, come vengono inizializzati lo zoom e lo stato delle miniature, e cosa succede quando si torna alla griglia. Ogni miglioramento deve quindi essere implementato due volte, una nella logica del tavolo luminoso e una nella logica del culling, e le due versioni divergono. Per non parlare del fatto che la modalità culling aveva una modalità statica e una modalità dinamica, che differivano sia nel modo in cui si interagiva con esse sia nella loro implementazione, e pochissimi utenti capivano di cosa si trattasse.

In Ansel, il culling è tornato come filtro di collezione, dapprima come un modo semplice per restringere l’insieme corrente, poi in seguito esplicitamente come pulsante filtro Restringi alla selezione. Ciò significa che l’intento rivolto all’utente è rimasto lo stesso (ridurre la collezione corrente a una selezione arbitraria di immagini), ma l’implementazione è cambiata completamente: invece di entrare in una vista speciale, resti nel tavolo luminoso e dici allo strumento di filtraggio di mostrare solo le immagini attualmente selezionate o solo le immagini che corrispondono al criterio di restrizione corrente.

image
La nuova barra degli strumenti di filtraggio, unificata. I criteri di filtraggio sono inclusivi, e le icone si comportano come pulsanti di spunta: deseleziona per nascondere le immagini corrispondenti, seleziona tutto per mostrare tutto (un menu contestuale con il clic destro ti offre una scorciatoia per farlo in un solo passaggio). I suggerimenti appaiono al passaggio del mouse per maggiori dettagli. La prima icona aggiorna la collezione corrente in base ai filtri. Ad esempio, se mostri le immagini valutate 3 stelle e retrocedi un'immagine a 2 stelle, questa non verrà rimossa automaticamente dalla collezione corrente finché non la aggiorni manualmente.

Per gli utenti, il beneficio è pratico. Il culling è ora componibile con il resto della logica di filtraggio invece di vivere al di fuori di essa. Puoi combinarlo con valutazioni, etichette colore o ricerca testuale perché è semplicemente un altro filtro di collezione. La gestione delle scorciatoie è più semplice perché c’è una vista speciale in meno a contendersi la proprietà delle scorciatoie. E le correzioni allo scorrimento, alla selezione, alla durata di vita delle miniature o al comportamento delle sovrapposizioni beneficiano automaticamente anche il culling, perché il culling ora usa la stessa infrastruttura del tavolo luminoso invece di una parallela.

Quella è stata anche l’occasione per rivedere completamente la barra degli strumenti di filtraggio, che mescolava pulsanti e logica: un elenco combobox per le valutazioni (da rifiutato a 5 stelle), associato a un elenco combobox di confronto (≠ = > < >= <=), ma pulsanti a interruttore per le etichette colore. Darktable 4.0 ha sostituito quel sistema con un insieme sovradimensionato di widget configurabili che usavano paradigmi GUI non standard che dovevano essere spiegati nei suggerimenti. Ansel ha appiattito il design: tutto è un pulsante a interruttore che funziona in modalità «includi» all’interno del proprio gruppo, permettendo selezioni complesse senza una GUI complessa:

  1. hai 3 gruppi (valutazioni, etichette colore, stato di modifica), più restringi alla selezione e ricerca testuale,
  2. tra questi gruppi, i filtri sono esclusivi, ovvero costituiscono un AND logico,
  3. all’interno di questi gruppi, i filtri sono additivi, ovvero costituiscono un OR logico.

Ad esempio, nello screenshot qui sopra, tutto è attivato, quindi il filtraggio è in pratica disabilitato (lasciamo passare tutto). Nell’esempio qui sotto, filtriamo tutte le immagini che hanno un’etichetta colore impostata e che sono già state modificate, indipendentemente dalla loro valutazione (tutti i pulsanti di valutazione sono attivati):

image

Selezione : mescolare stati del backend e della GUI ti si ritorce sempre contro

Le immagini raggruppate erano un’altra linea di faglia nascosta. Parte della vecchia logica cercava di tenere conto del raggruppamento nelle query SQL, durante la generazione di collezioni di immagini dal database della libreria, ma bordi di gruppo, stati di hover, comportamento di selezione e visibilità effettiva sono concetti della GUI. Quella discrepanza creava bug subdoli: tutte le immagini di un gruppo potevano essere selezionate, oppure alcune ignorate secondo regole euristiche che non corrispondevano a ciò che l’utente vedeva sullo schermo.

Ho corretto ciò riportando le decisioni di visualizzazione dei gruppi nel codice della GUI. SQL fornisce l’elenco delle immagini; l’interfaccia decide quali membri raggruppati sono visibili, compressi o sotto il mouse. Dopodiché, le funzioni aggiunte nel 2025 sono diventate molto più semplici: i bordi di gruppo potevano essere reimplementati, resi opzionali, estesi alla filmstrip e resi più visibili al passaggio del mouse. I suggerimenti potevano essere popolati in modo pigro solo quando l’utente ci passa sopra con il mouse. Anche una richiesta SQL per miniatura al momento dell’inizializzazione poteva sparire perché l’interfaccia aveva già abbastanza stato locale per sapere quando le informazioni di gruppo erano effettivamente necessarie.

Per gli utenti, il beneficio non è astratto. I bordi di gruppo ora significano ciò che mostrano, le selezioni corrispondono esattamente allo stato di gruppo visibile, e le immagini raggruppate non danno più la sensazione di essere gestite per metà dal database e per metà dall’interfaccia.

Un altro problema nascosto era la selezione. Nel vecchio codice, la selezione era troppo intrecciata con le parti interne della collezione, l’accesso SQL grezzo e la logica di fallback. Sembra innocuo finché un’immagine non viene rimossa dalla collezione corrente, un gruppo non viene compresso, o non avviene un cambio di vista mentre un’altra parte del codice presuppone ancora il modello di selezione precedente. In Darktable, le selezioni di immagini erano gestite come un backend SQL, poiché vengono memorizzate nella libreria per essere ripristinate alla sessione successiva. Questo non è un problema finché il backend della selezione non viene manomesso per indovinare quali immagini sono visibili nella GUI, e la GUI gestisce effettivamente la selezione attraverso il backend. Questa è la peggiore suddivisione possibile della funzionalità tra gli strati backend e GUI.

Ecco perché il 2025 contiene un intero grappolo di commit relativi alla selezione: riscrivere l’API di selezione, rimuovere la vecchia nozione di main_image, unificare i getter/setter dell’immagine attiva, correggere la selezione a intervalli, ripristinare lo stato di selezione attraverso i cambi di vista, e aggiungere fallback su rowid quando scroll_to_selected non riesce più a trovare l’id dell’immagine originale. La riscrittura ha cambiato l’architettura: la thumbtable ora calcola selezioni visibili e significative per l’utente e passa un elenco di id di immagini allo strato di selezione, invece che la GUI cerchi di ricostruire il significato a posteriori dallo stato della collezione. L’API SQL di selezione è diventata un semplice backend «salva su/ripristina da database».

Il beneficio concreto per l’utente è che la selezione a intervalli con Shift + clic e l’«immagine su cui agire» sono What You See Is What You Get: non puoi selezionare un’immagine che non è visibile sullo schermo, e il backend non deve indovinare cosa è visibile. E lo scorrimento di ritorno all’immagine selezionata ora fallisce molto meno spesso nei casi limite. Il problema nascosto non era una scorciatoia rotta; era che la semantica della selezione viveva nello strato sbagliato.

La correzione consiste nel separare chiaramente ciò che appartiene alla gestione del backend e della libreria da ciò che appartiene agli stati e alle interazioni della GUI, e nel fornire un’interfaccia rigida tra i due. Ciò rende le diverse parti del codice meglio racchiuse, immuni ai cambiamenti nelle altre parti, e gestite centralmente.

I glitch di hover e focus derivavano dalla collisione di diversi sistemi validi

Il passaggio del mouse, il passaggio da tastiera, il focus e la gestione del clic erano ciascuno implementato per ragioni legittime, ma non condividevano un unico punto di arbitraggio. Darktable ora ha persino un orgoglioso slogan pubblicitario scritto sullo sfondo del tavolo luminoso, quando la collezione è vuota, che elogia quel flusso di lavoro «senza clic» che ti permette di sovrascrivere i metadati dell’immagine senza volerlo o addirittura senza saperlo. A me sembra come una casa automobilistica che pubblicizza un’auto progettata perché tu possa usare il volante con le ginocchia (e usare le mani per bere birra).

Ecco perché il vecchio comportamento del tavolo luminoso poteva sembrare incoerente senza alcun singolo bug ovvio: era rotto per progettazione. Cliccare su un pulsante di sovrapposizione per valutare l’immagine poteva anche selezionare l’intera miniatura, ma valutare da tastiera no. Il movimento da tastiera e il movimento del mouse potevano entrambi credere di possedere l’immagine «corrente» e attivare ogni sorta di modifiche distruttive ai metadati semplicemente passando sopra l’immagine e senza chiedere conferma. Filtrare le immagini per valutazione a stelle e cambiare inavvertitamente la valutazione di un’immagine poteva farla sparire in modo imprevisto. Widget nascosti potevano comunque ricevere logica relativa all’hover. Il focus GTK poteva decidere una cosa mentre lo strato della vista se ne aspettava un’altra.

Le correzioni qui riguardavano meno l’aggiunta di nuove funzioni e più la scelta di un unico proprietario per lo stato. Il dispatch dello stato di hover è stato centralizzato. La navigazione da tastiera ha acquisito un punto di partenza sensato e un percorso di annullamento esplicito con Shift+Ctrl+A. La visualizzazione del focus è stata resa esplicita. I widget nascosti hanno smesso di partecipare alla logica di hover che non potevano visualizzare. Cliccare sui pulsanti di sovrapposizione ha smesso di trasferirsi nella selezione della miniatura. Nessuna (sovra)scrittura di metadati viene più effettuata senza la pressione esplicita di un pulsante; gli eventi di hover sono tutti di sola lettura.

Ansel ha due semplici regole :

  • qualsiasi azione che (sovra)scriverà (meta)dati viene effettuata solo su immagini esplicitamente selezionate,
  • la selezione esplicita viene effettuata solo interagendo con qualcosa di «concreto»: un clic del mouse o la pressione di un tasto.

Poi, gli eventi di hover sono riservati agli eventi di sola lettura.

Il beneficio per gli utenti è che il tavolo luminoso reagisce più come un unico modello di interazione. Il problema nascosto non era che l’hover fosse rotto. Era che diversi tipi di hover erano contemporaneamente «giusti» e quindi collettivamente sbagliati.

Modifiche al backend

Riparentare la thumbtable tra tavolo luminoso e filmstrip

Uno dei problemi più profondamente nascosti era la proprietà dei widget. In Darktable, la stessa griglia thumbtable veniva riparentata tra i contesti del tavolo luminoso e della filmstrip. Sulla carta, ciò evitava la duplicazione del codice e la proliferazione dei widget, e suona intelligente. In pratica, faceva dipendere lo stato di scorrimento, l’inizializzazione delle miniature, la garbage collection e la durata di vita degli eventi da dove il widget era vissuto più di recente. Per non parlare del fatto che il riparentamento era lento, quindi passare avanti e indietro tra tavolo luminoso e camera oscura era ritardato di circa 1 s, il tempo necessario a GTK per ricalcolare le nuove dimensioni delle miniature, eventualmente chiedere alla cache nuove miniature, e ridisegnare i widget.

Ciò si manifestava in modi molto concreti. La posizione di scorrimento poteva saltare o diventare incoerente. Le miniature potevano essere nascoste, mostrate o distrutte nel momento sbagliato. La garbage collection diventava più difficile da ragionare perché la gerarchia dei widget non era stabile. Naturalmente, tutto ciò veniva risolto commit dopo commit, ma lasciando il codice in uno stato di complessità immanutenibile.

La vera correzione è stata smettere di cercare di essere furbi. Il tavolo luminoso e la filmstrip ora hanno thumbtable separate. Ciò significa un po’ più di struttura esplicita nel codice, ma molti meno stati accidentali. Il beneficio per gli utenti è visibile in uno scorrimento più stabile, meno glitch di layout nei cambi di vista, e una filmstrip che si comporta come un vero fratello del tavolo luminoso invece che come un frammento riutilizzato.

Le modifiche a menu e layout non erano estetica

Il fatto che il menu globale sia diventato veramente globale ad aprile 2025 non era un esercizio di branding. Prima di allora, molti comandi erano implementati come se appartenessero a moduli locali anche quando erano in realtà azioni a livello di applicazione. Alcuni di essi erano nascosti interamente dietro scorciatoie da tastiera e noti solo a chi leggeva la documentazione. Molti di essi li ho scoperti solo rimuovendone il codice. Lo stesso valeva per le informazioni sull’immagine, il cambio di vista e le vecchie casse degli strumenti delle viste. Vivevano in luoghi che erano facili da aggirare nel codice una volta, ma difficili da mantenere in modo coerente tra le viste.

Spostare le informazioni sull’immagine nella barra del menu globale, spostare il selettore di vista in una posizione di primo livello più chiara, e in seguito rimuovere la barra degli strumenti in basso al centro sono state tutte conseguenze della stessa diagnosi: il centro dell’interfaccia dovrebbe appartenere all’interazione con l’immagine, non a residui storici. La rimozione della timeline e la decisione di non mostrare una filmstrip vuota sono esempi più piccoli della stessa logica. Persino Enter che apre l’immagine selezionata in camera oscura rientra qui: allinea il comportamento al compito primario del tavolo luminoso invece di preservare abitudini storiche accidentali.

Il beneficio per l’utente non è «il menu è più bello». È che i comandi si trovano in luoghi che corrispondono al loro ambito, e il tavolo luminoso stesso ha meno controlli a contendersi lo spazio con la griglia. Per non parlare del fatto che i «moduli» che sono in realtà griglie (non uniformi) di pulsanti sono semplicemente menu travestiti con un design peggiore, quindi ora vengono trasformati in voci di menu.

Il menu globale è stato anche l’occasione per portare nella GUI funzioni che finora vivevano solo in script shell nascosti (!): il precaricamento delle miniature per la collezione corrente e l’eliminazione delle miniature dalla cache su disco. Erano richieste da anni nella GUI, ma non trovavano posto da nessuna parte nel design dell’interfaccia di Darktable incentrato sui moduli.

Ma… quella barra laterale destra di moduli che non volevano essere menu era una benedizione, perché toglieva molta area da dipingere con le immagini e rendeva le miniature più piccole. Rimuoverla ha fatto apparire le pessime prestazioni del disegno delle miniature in tutto il loro splendore: nulla era in cache, né le surface delle immagini né i metadati, e tutto veniva recuperato di nuovo continuamente. La riscrittura era inevitabile; l’architettura era rotta.

Lo zoom è diventato utilizzabile solo dopo che le dimensioni delle miniature sono state rese coerenti

Il primo tentativo di tavolo luminoso zoomabile è fallito per una ragione strutturale: la geometria delle miniature, le dimensioni mipmap in cache e la navigazione a livello di vista non condividevano lo stesso modello. Ecco perché la funzione ha dovuto essere rimossa per prima. Se avessi continuato a perfezionarla sul posto, il risultato sarebbe stato solo una funzione rotta dall’aspetto migliore.

Il lavoro successivo ha corretto la catena di dipendenze nell’ordine opposto. Il backend della vista ha acquisito l’impalcatura per lo zoom del tavolo luminoso, poi alla cache delle miniature è stato insegnato a ragionare in modo più coerente sulle dimensioni delle miniature, è stata preparata un’API per miniature a piena risoluzione, poi sopra a ciò sono stati aggiunti il clamping, il panning e l’auto-pan basato sul baricentro. Solo una volta che gli strati inferiori si sono accordati sulla dimensione dell’immagine e sulla strategia di recupero, lo zoom al 200% è diventato una funzione difendibile.

Il beneficio per gli utenti è che lo zoom è ora legato alla cache e alla navigazione invece di combatterle. Trascinare per fare panning, Shift + trascinare attraverso le miniature visibili, e il panning automatico verso il baricentro dei dettagli funzionano tutti perché la pipeline sottostante sa cosa dovrebbero essere le miniature ingrandite.

Il significato confuso di «zoom» nel tavolo luminoso è stato aggiornato: il termine «zoom» era usato sia per lo zoom esterno (numero di immagini per riga, che influisce indirettamente sulla loro dimensione visibile) sia per lo zoom interno (ingrandimento all’interno del riquadro dell’immagine). Quindi ora abbiamo colonne per lo zoom esterno, e zoom per l’ingrandimento.

image

Non fare caso alla croce al posto del simbolo - nello spinbutton delle colonne, è un bug di GTK con il tema Breeze di KDE/Plasma .

Ciò ha reso possibile implementare una funzione che avevo in mente da molto tempo: l’auto-panning baricentrico all’ingrandimento della miniatura.

image
Zoom per adattare
image
Abilitazione dello zoom al 100 %

Puoi vedere che, quando si abilita lo zoom al 100%, le immagini vengono automaticamente allineate sul loro contenuto nonostante abbiano inquadrature e proporzioni completamente diverse. Questo usa un’analisi delle caratteristiche a decomposizione wavelet, dalla quale calcoliamo le coordinate del baricentro dei dettagli. Non è perfetto perché non coglie la stessa parte del volto tra le varie immagini, ma ci dà il volto in tutti i casi. È un progetto che avevo lasciato in sospeso da molto tempo, ma non era possibile con il precedente design del tavolo luminoso.

Inutile dire che il trascinamento nelle miniature ingrandite è stato ripristinato, ed è stato aggiunto anche il trascinamento in tutte le miniature ingrandite (Shift+Trascina). Questa funzione è stata a lungo richiesta in Darktable ma chiaramente non era possibile con il loro design di merda.

Rallentamento dei metadati autoinflitto

Il vecchio tavolo luminoso continuava a porre al database le stesse domande una miniatura alla volta. Su una collezione piccola questo è facile da non notare. Su una più grande, emerge come micro-scatti quando si apre la vista, si mostrano le sovrapposizioni, si passa sopra i gruppi o si aggiornano dati sensibili alla cronologia. Il problema nascosto non era che l’SQL grezzo fosse lento in termini assoluti; era la ripetizione e la tempistica di quelle query.

La correzione è arrivata a tappe. I metadati di sola lettura hanno iniziato a essere messi in cache dalle strutture delle immagini. Poi, a febbraio 2026, i metadati per un’intera collezione potevano essere recuperati con una sola query SQL invece di una query per immagine. La thumbtable poteva popolare la cache delle immagini a partire dalla collezione che stava per visualizzare. Il caching delle informazioni delle miniature è stato rifattorizzato in un helper e in seguito reincorporato in dt_image_t, in modo che i metadati non dovessero rimbalzare tra strutture parallele. Anche piccole modifiche come la rimozione dei ping dell’history hash al ridisegno contano qui, perché eliminano round-trip invisibili dai percorsi critici.

Molte inutili query SQL per miniatura sono state rimosse, insieme al codice SQL nel codice GUI del tavolo luminoso, così che gli strati funzionali sono ora suddivisi correttamente.

Il beneficio per l’utente è preciso: l’apertura di collezioni grandi si blocca meno, i display carichi di sovrapposizioni esitano meno, e gli aggiornamenti di cronologia o metadati si propagano con meno pause visibili.

Anche la cache mipmap doveva essere sistemata

La cache mipmap è lo strato che carica le immagini RAW e le miniature pre-generate nella RAM, e le svuota quando la memoria scarseggia. Era una delle fonti nascoste più profonde di bug visibili. Quando tutto era allineato, funzionava abbastanza bene. Quando il file sorgente era più piccolo del previsto, quando un’anteprima JPEG incorporata era strana, quando l’history hash era indefinito, o quando l’invalidazione della cache su disco rimaneva indietro rispetto alle modifiche, l’utente non avrebbe sperimentato «un bug della cache». Avrebbe visto miniature obsolete, anteprime rotte o immagini che si rifiutavano di aggiornarsi.

Ecco perché gran parte del lavoro del 2025 sulla cache sembra chirurgico. La logica di allocazione dei buffer è stata riscritta perché la proprietà era troppo aggrovigliata. La gestione degli input sottodimensionati è stata migliorata perché le assunzioni sulla dimensione dell’input si propagavano troppo lontano. Le anteprime incorporate hanno smesso di essere scartate in modo così aggressivo perché le anteprime troppo piccole erano comunque utili per la coerenza tra i livelli di zoom. Le anteprime JPEG sidecar hanno iniziato a essere usate quando presenti invece delle miniature RAW incorporate. L’invalidazione della cache è stata irrobustita, e le miniature in cache venivano davvero rimosse dal disco quando avrebbero dovuto esserlo. Le mipmap rigenerate hanno iniziato a riscrivere il loro hash nel database in sincronia con lo stato della cronologia, e il caso dell’history hash indefinito ha smesso di lasciare le mipmap non salvate.

Il bug più subdolo che ho trovato era sepolto nell’insensata complessità dell’intreccio tra thumbtable, vista e cache mipmap. Mentre il rendering delle miniature era delegato a un thread separato per le prestazioni (come dovrebbe), e più di un thread poteva essere avviato per elaborare (presumibilmente) più miniature contemporaneamente (eventualmente usando più GPU), tutti i thread di elaborazione e la GUI in realtà competevano per bloccare le cache mipmap e delle immagini, il che non faceva altro che bloccare la GUI mentre le pipeline erano in esecuzione. È solo perché lavoravo partendo dal presupposto di aver riscritto le cose secondo i canoni, e sapevo che il modo canonico dovrebbe essere più veloce di così, che ho continuato a scavare finché non ho trovato perché ancora non era veloce come previsto, semplificando tutto strato dopo strato lungo il percorso.

Il beneficio per gli utenti è che la cache è più affidabile. Dopo le modifiche, è meno probabile che il tavolo luminoso mostri una miniatura obsoleta. Su file difficili, la generazione dell’anteprima fallisce meno spesso. In visite ripetute alla stessa collezione, la cache su disco si comporta più come una cache e meno come un archivio di screenshot obsoleti. Ma tutto questo, senza dover aggiornare/ricalcolare/ridisegnare tutto continuamente solo per essere sicuri.

Inoltre, le opzioni per usare i JPEG incorporati o forzare un ricalcolo sono state spostate dalle preferenze al menu globale e possono essere modificate durante l’esecuzione:

image

Questo è un altro esempio in cui ripulire e semplificare il backend ha spianato la strada all’estensione del frontend e a nuove funzioni che hanno semplicemente senso.

Thread-safety e race condition

Una ragione classica per cui questi bug sono durati così a lungo è che richiedevano all’utente di essere più veloce del codice: scorrere rapidamente, lasciare una vista mentre le miniature sono ancora in costruzione, ridimensionare mentre una surface in background è ancora in produzione, o chiudere un widget appena prima che un thread worker invii un aggiornamento.

Ecco perché il lavoro sulla thread-safety del 2025-2026 conta. Il recupero delle miniature è stato spostato più in profondità nei processi in background, poi in seguito esplicitamente differito così che il rendering della camera oscura potesse mantenere la priorità. La distruzione delle miniature è stata spostata in punti di pulizia più sicuri. I processi in background che producono miniature hanno imparato ad annullarsi quando il widget che servivano scompariva. Le surface delle immagini sono state protette da mutex. I puntatori liberati sono stati azzerati. I buffer delle immagini sono stati spostati su percorsi di allocazione meglio gestiti. Il punto di tutto ciò non era «più thread»; era impedire ai vecchi thread di scrivere in uno stato morto o di competere per i lock.

Il risultato per l’utente è meno segfault, meno glitch casuali durante lo scorrimento veloce, e meno casi in cui l’interfaccia sembra correre contro sé stessa sotto carico.

Anche diversi lock dei thread, aggiunti negli anni per rattoppare i problemi, sono stati rimossi. Molti erano ridondanti (ma non lo si vedeva sotto la folle complessità dell’intera cosa), alcuni danneggiavano attivamente le prestazioni, e tutti nascondevano una cattiva progettazione. Ora abbiamo meno punti di locking, ma il risultato è più leggibile e più robusto.

Di conseguenza, i widget delle miniature sono ora oggetti completamente autoconclusi. Gestiscono internamente i propri thread della pipeline di rendering delle miniature, che interagiscono direttamente con la propria superficie di immagine memorizzata in cache, così possono generarli o terminarli da soli. Questa immagine in cache viene invalidata solo quando la cronologia dell’immagine cambia, cosa che il backend della cronologia di sviluppo rende esplicita. Poiché il widget della miniatura conosce il proprio stato (visibile o no, bisognoso di aggiornamento dell’immagine o no, dimensione, modalità focus-peaking, ecc.), diventa molto più robusto rispetto al tentativo di gestire tutte quelle cose da livelli di più alto livello che non riescono a comunicare tra loro.

Questo non era possibile con la progettazione di Darktable, perché aggiungeva/rimuoveva continuamente i widget delle miniature in modo dinamico alla vista corrente del tavolo luminoso, a seconda della posizione della riga fluttuante, il che era un tentativo di gestire i rallentamenti dovuti a tutti i thread che competevano per l’accesso alla cache. Ma inoltre, i widget delle miniature di Darktable cercavano di acquisire immediatamente un’immagine dalla cache mipmap, al momento della loro creazione, il che faceva impennare l’uso della CPU e dell’I/O di memoria, e di fatto congelava l’interfaccia. Ma «risolvere» ciò riducendo l’aspettativa di vita dei widget delle miniature rendeva impossibile lasciare che gestissero il proprio stato internamente, quindi doveva essere fatto dai livelli di alto livello, che dovevano comunicare tra loro per aggiornare gli stati, cosa che avveniva in alcuni punti ma a scapito di una complessità insopportabile.

Invece di ciò, le miniature di Ansel vengono tutte inizializzate in una volta sola, ma acquisiscono pigramente un’immagine dalla cache mipmap solo una volta che diventano visibili, per poi memorizzarla internamente in cache. Questo mantiene reattivo lo scorrimento della thumbtable, anche con una raccolta di 500 immagini che stanno tutte generando la propria immagine.

Anteprime di importazione migliorate come effetto collaterale

La finestra di importazione è stata interessata da questo lavoro per la stessa ragione del tavolo luminoso: ha bisogno di un’estrazione rapida delle anteprime, di ispezione dei metadati e di un comportamento di fallback sensato su file parzialmente supportati. Una volta riscritti i percorsi della mipmap e delle miniature, il caricamento delle anteprime raw nell’importazione è diventato più veloce, e anche il supporto per TIFF/DNG è migliorato.

Questo è un buon esempio del perché la riscrittura doveva avvenire in basso nello stack. Se il meccanismo di anteprima sottostante è inefficiente o fragile, sia il tavolo luminoso sia l’importazione ereditano lo stesso dolore. Una volta riscritto quel meccanismo, entrambi ne hanno beneficiato.

Cambiamenti architetturali globali

Nel mio articolo precedente, ho mostrato come la cache della pipeline di lavoro renda quasi istantaneo il passaggio avanti e indietro tra tavolo luminoso e camera oscura, perché non ha bisogno di ricalcolare l’intera immagine. Il lavoro qui svolto sull’interfaccia grafica risolve lo stesso problema dei ritardi nel cambio di vista, ma a livello dell’interfaccia grafica. Quindi non c’è alcun ritardo quando si passa tra le due viste.

Questo è importante perché il ritardo nel cambio di vista è stato usato come scusa per duplicare le funzioni (moduli/caselle degli strumenti) tra il tavolo luminoso e la camera oscura, il che non fa che aumentare il disordine dell’interfaccia grafica. Quindi tutto questo miglioramento del backend rende possibile migliorare la progettazione dell’interfaccia grafica specializzando ogni vista per un unico compito:

  • gestione dei metadati per il tavolo luminoso (più ovviamente la cernita),
  • editing delle immagini per la camera oscura.

Di conseguenza, le caselle degli strumenti metadati e etichette sono state rimosse dalla camera oscura.

Quante righe di codice ha risparmiato la riscrittura?

Contando solo le righe non di commento e non vuote con cloc, e confrontando l’ultimo albero prima del 1° gennaio 2022 con l’albero attuale per i file principali qui discussi, la riscrittura ha risparmiato 4.714 righe di codice complessivamente.

Il perimetro di quel conteggio è:

  • l’intera directory data/themes/ prima del 2022, confrontata con l’odierno 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,
  • più i file eliminati del tutto dalla riprogettazione o dalla pulizia dell’interfaccia grafica delle raccolte: src/dtgtk/culling.[ch], src/libs/tools/view_toolbox.c, src/libs/collect.h e src/libs/recentcollect.c.

All’interno di quel perimetro, il totale è passato da 15.257 righe di codice prima del 2022 a 10.543 oggi. La riscrittura ha quindi risparmiato 4.714 righe di codice complessivamente. Il risparmio maggiore è venuto dall’eliminazione totale della vista di cernita (src/dtgtk/culling.c) (-1.406 righe), dalla riduzione del tavolo luminoso (src/views/lighttable.c) (-976), dalla rimozione di 7 vecchi fogli di stile dei temi e dal loro accorpamento in uno solo (-864 in tutto data/themes/), dalla riduzione di src/dtgtk/thumbnail.c (-460), src/dtgtk/thumbtable.c (-458), dall’eliminazione di src/libs/recentcollect.c (-358), e dalla riduzione di src/views/view.c (-314). Alcuni file sono effettivamente cresciuti, in particolare src/libs/tools/filter.c (+215) e src/libs/collect.c (+157), perché parte dell’obiettivo era riportare i comportamenti per casi speciali come la cernita e i vecchi rami dell’interfaccia grafica delle raccolte in un’infrastruttura condivisa più semplice, invece di tenerli in viste parallele e moduli laterali.

Il conteggio delle righe non è di per sé una metrica di qualità. Molte riscritture si limitano a spostare il codice qua e là. Ma qui il numero corrisponde al cambiamento di progettazione: meno comportamenti duplicati, meno viste parallele, meno hack compensativi, e meno punti in cui l’interfaccia grafica, il livello di vista e la cache dovevano tutti risolvere due volte lo stesso problema.

Tutto ciò che ho corretto, l’ho corretto semplificando la logica e il codice. Non era ammessa alcuna soluzione temporanea.

Benchmark

Tutti i tempi di esecuzione sono stati calcolati su un portatile Lenovo ThinkPad P51 (CPU Intel Xeon E3-1505M v6 @ 3.00GHz, GPU Nvidia Quadro M2200 con 4 GB di VRAM, 32 GB di RAM, display 4K), CPU in modalità prestazioni, Linux Fedora 41 con desktop KDE/Plasma. I tempi di esecuzione della pixel pipeline non sono confrontati (fuori dal perimetro; vedi l’articolo precedente). Ansel Master è preso al commit 09749f1d  (21 feb. 2026).

DescrizioneAnsel MasterDarktable 5.0
Tempo dall’avvio dell’app all’ultimo disegno delle miniature del tavolo luminoso (stessa raccolta)2.12 s7.49 s
Tempo per passare dal tavolo luminoso alla camera oscura (stessa immagine)0.2 s1.2 s
Tempo per scorrere (inizio->fine) la stessa raccolta di 471 immagini*0.7 s5.0 s

*: miniature precaricate nella cache su disco in entrambi i casi, 5 colonne di miniature per riga, risoluzione 4K, nessuna barra laterale destra.

Come «soluzione», Darktable 5.x ci ha benedetti con una splendida schermata di avvio, che è una confessione più che altro.

Quanto segue è stato misurato a batteria, in modalità risparmio energetico, con l’applicazione lasciata inattiva (nessuna interazione dell’utente) per 5 minuti, usando Intel Powertop. Il consumo di base dell’intero sistema operativo inattivo è dell'1,6 % di CPU. (La potenza è indicata solo per l’app, la % di CPU è indicata per l’intero sistema):

VistaAnsel MasterDarktable 5.0
Tavolo luminoso1,8 % CPU, potenza: 0,85 mW2,7 % CPU, potenza: 103 mW
Camera oscura1,8 % CPU, potenza: 7,65 mW1,8 % CPU, potenza: 22 mW

Queste cifre rappresentano il consumo energetico di base della sola interfaccia grafica (GTK, worker in background, timer programmati, ecc.). Darktable perde prestazioni attraverso l’interfaccia grafica, e il tedioso lavoro svolto nel 2023-2024 per ottimizzare i moduli di elaborazione dei pixel per 15-50 ms in più è completamente irrilevante.

Conclusione

A questo punto, sono fermamente convinto che il «progetto» Darktable attragga solo «sviluppatori» che non sarebbero in grado di fare una distinzione cognitiva tra interfaccia grafica e backend nemmeno se ne andasse della loro vita. Così i problemi dell’interfaccia grafica vengono risolti nel backend, i problemi del backend vengono risolti nell’interfaccia grafica, e questo fa crescere fuori controllo la complessità del codice con il tempo, il che poi giustifica l’aggiunta di nuove funzioni hackerando la minor quantità di codice in una base di codice che nessuno capisce più. Per non parlare del fatto che nulla di tutto ciò era documentato, quindi ho dovuto fare un doloroso reverse engineering nell’arco di diversi anni, semplificando ricorsivamente un po’ qui e un po’ là, finché non è finalmente convergito a una logica complessiva pulita.

Inutile dirlo, tutte le soluzioni temporanee e le «correzioni rapide» che erano state aggiunte sono state rimosse. Tutte risalivano a dopo il 2020, il che mostra una preoccupante tendenza al degrado della qualità del codice.

Questo progetto di pulizia mi ha rubato 4 anni di vita, non mi ha dato alcun piacere, e le persone che hanno introdotto tutte le regressioni che ho dolorosamente corretto devono essere ritenute responsabili delle conseguenze delle loro azioni. C’è un’enorme differenza tra non avere abbastanza tempo per fare le cose come si deve e consumare un mucchio di ore-uomo per peggiorare le cose. E poi, rifiutarsi di ammettere di aver peggiorato le cose, rifiutarsi di accettare che c’è un problema, e alimentare il proprio bias di conferma ascoltando solo i feedback dei contenti è la prova definitiva della stupidità.

Darktable è degenerato in una merda, e ho appena spiegato, tecnicamente, perché. Il team di Darktable e il suo forum satellite di tech bro  vorrebbero far credere alla gente che mi sono arrabbiato con loro perché non accettavano le mie modifiche, e che è tutto un problema interpersonale. Per il profano che non capisce quello che ho scritto qui e negli articoli precedenti, è più facile credere alla rabbia interpersonale che capire come una successione di cattive decisioni tecniche nell’arco di diversi anni mi abbia reso vittima dei problemi che loro hanno creato, perché ero l’unico qui a tempo pieno, che ne dipendeva per vivere. Mi sono arrabbiato con loro perché continuavano a cagare ancora e ancora sul mio uscio, e io dovevo ripetutamente ripulire. Questa è una forma di violenza che è davvero difficile da vedere e da riconoscere perché non si manifesta materialmente: è un modo di renderti la vita più difficile, quotidianamente, passo dopo passo, solo perché un branco di dilettanti di mezza età senza competenze voleva far parte di qualcosa di figo senza rendersi conto dell’effetto dannoso dei propri contributi sull’intero progetto.

Ed è toccato a me ripulire il casino perché ero apparentemente l’unico a preoccuparmi delle regressioni, degli strani bug casuali e dei crash, delle peggiori «innovazioni» dell’interfaccia grafica che hanno dissuaso perfino mia moglie dall’usare Darktable perché è semplicemente opprimente, e di tutti i nuovi rallentamenti che continuano ad accumularsi negli anni. Come mi disse Chris Elston sulla chat IRC di Darktable, nel 2022, prima che la lasciassi per sempre, riguardo a roba che avevo corretto nel 2019 e che loro hanno rotto di nuovo nel 2022: «stai zitto e correggilo». Se questa non è violenza, non so cosa lo sia. E ora stanno cercando di spargere la voce che quello tossico ero io. L’intero team è tossico. La loro sbadata cultura del lavoro è tossica. Il loro modo di entusiasmarsi per tutto, purché sia nuovo, senza ponderare il costo di manutenzione, la duplicazione delle funzioni e l’oppressione complessiva dell’utente è tossico. La loro mancanza di preoccupazione per il futuro del progetto e per le conseguenze delle scelte che fanno è tossica.

E ciò che è particolarmente tossico è che, su discuss.pixls.us, ogni post che esprime lode per me o lodi di Ansel viene segnalato e nascosto. Non c’è libertà di parola su un forum di software libero. Hanno semplicemente trasformato il comunismo in stalinismo.


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