Tussen januari 2022 en maart 2026 landde Ansel 297 niet-merge commits met betrekking tot het lichttafel-raster, de miniaturen daarvan en hun renderpipeline en caching. Ik heb geprobeerd het zo lang mogelijk uit te houden met de sjofele Darktable-lichttafelcode, alleen ontvet, maar helaas was het pure technische schuld en het was pijnlijk traag.

Darktable „beheert" namelijk de belabberdheid van zijn lichttafel door de omvang ervan te verkleinen: de linker- en rechterzijpanelen nemen veel weergaveoppervlak in beslag, waardoor er nog minder ruimte overblijft voor de lichttafel om opnieuw te tekenen. Sinds Ansel het rechterzijpaneel verwijderde en de inhoud daarvan samenvoegde met het linkerpaneel en het globale menu, was er meer oppervlak om te tekenen, meer CPU-werk te doen, en werd het verschrikkelijke ontwerp van de lichttafel des te schadelijker.

Dat project is nu voltooid. Dit artikel is een kaart van wat er veranderd is, wat er verwijderd is, wat er opnieuw ontworpen is, hoe het nieuwe gedrag werkt, en wat gebruikers er daadwerkelijk aan hebben.

Een korte geschiedenis van slecht ontwerp

Om te begrijpen waarom de herschrijving van 2022-2026 uiteindelijk zoveel bestanden raakte, helpt het om een stap verder terug te kijken.

De lichttafelweergave, src/views/lighttable.c, verzamelde al meer dan tien jaar verantwoordelijkheden. Het begon als de plek die het bestandsbeheerder-raster en basiszoomen afhandelde, en absorbeerde daarna geleidelijk het volledige voorbeeld, plakkend voorbeeld, culling, geheugen van paneelstatus, groepering, herordenen met slepen-en-neerzetten, toetsenbordnavigatie, overlaybeleid, filmstrip-coördinatie en veel sneltoetsroutering. Als je naar de geschiedenis van vóór 2022 kijkt, gaan veel commits er gewoon over om te voorkomen dat deze met elkaar vechten: offset-sprongen bij het zoomen, scrollbugs in het voorbeeld, selectie die niet gesynchroniseerd blijft, culling en voorbeeld die in een half kapotte staat opnieuw binnengaan, paneelzichtbaarheid die niet goed hersteld wordt, groepsranden die glitchen, en waarderingen of kleurlabels die het raster onverwacht verplaatsen.

Dit toont dus een volledig onvermogen om de levenscyclus van een gecachte afbeelding bij te houden (of van welke data dan ook in die godvergeten software, wat dat betreft), terwijl men er volledig comfortabel en ontspannen bij blijft. Ik pakte dit probleem aan door de achtergrond-asynchroniciteit te vereenvoudigen, maar een andere optie zou geweest zijn om een signaal te gebruiken. Ik kan er niet overheen komen dat de 3 kerels die hier meer dan 10 jaar aan gewerkt hebben, twee keer zo lang C-code hebben geschreven als ik. In 2018 wist ik niet eens wat GLib was.

Voor het geval je geïnteresseerd bent, de manier waarop ik het aanpakte is :

  • miniatuur-widgets houden een statusvariabele bij die hun eigen afbeeldingsgeldigheid vertegenwoordigt,
  • wanneer GTK de miniatuur opnieuw wil tekenen, en de statusvariabele zegt „ongeldig", spawnt de miniatuur-widget een achtergrondthread die een pipeline berekent om de ontbrekende afbeelding te maken,
  • wanneer de achtergrondthread terugkeert, plaatst hij de uitvoerafbeelding in de miniatuur die hem aanriep, via pointer, in een (gecacht) surface, werkt de statusvariabele bij naar „geldig", en stuurt dan een GTK queue-redraw-event op de widget (niet het hele raster),
  • als de widget onzichtbaar is, gebeurt er niets (GTK plaatst geen redraw-events in de wachtrij voor onzichtbare widgets). De volgende keer dat GTK de afbeelding opnieuw wil tekenen, gebruikt het het gecachte surface van binnen de widget,
  • wijzigingen in geschiedenis en metadata sturen een „afbeeldingsinfo gewijzigd"-signaal dat het afbeeldings-ID publiceert. De thumbtable heeft één signaalhandler die daarnaar luistert, die de miniatuur op ID in het raster vindt (uit een hashtable, dus O(N)) en de statusvariabele ervan reset naar „ongeldig". Nogmaals, als 100 afbeeldingen doelwit zijn van de wijziging en allemaal onzichtbaar zijn, gebeurt er niets.

De architectuur lijkt complexer, maar het is eigenlijk minder code en het is gewoon robuust: er wordt niet gegokt.

Een herschrijving uit het COVID-tijdperk probeerde dat te refactoren naar src/dtgtk/thumbtable.c, dat al snel deel werd van dezelfde afdrijving. Zodra de aparte thumbtable-abstractie verscheen, werd het de verkeersregelaar tussen collectiestatus, scrollen, offsets, actieve afbeeldingen, toetsenbordnavigatie, slepen-en-neerzetten, vloeiend scrollen, overlayzichtbaarheid, zoombare lay-out, culling-synchronisatie en filmstrip-synchronisatie, waarbij veel native GTK-functies opnieuw uitgevonden werden op een slechtere en onvolledige manier (de typische Darktable-manier). De geschiedenis van vóór 2022 zit vol met commits die mouse-over na scrollen repareren, rijen en offsets herberekenen, geselecteerde afbeeldingen zichtbaar houden na collectiewijzigingen, kwadratische vertraging bij Home/End vermijden, zoombare uitlijning repareren, zorgen dat verborgen of ingeklapte afbeeldingen de actieve-afbeeldingslogica niet breken, en het aantal uitgezonden signalen beperken omdat te veel onafhankelijke updates aan elkaar geketend werden. Met andere woorden, thumbtable.c was al de plek geworden waar weergavelogica, levensduur van miniaturen en collectienavigatie gedwongen werden elkaar te ontmoeten, lang voordat de herschrijving van 2022 begon.

De thumbtable-refactor deed een nieuw bestand verschijnen, toen de oude lichttafel- en filmstripcode begonnen werd te factoren tot een gemeenschappelijke miniatuur-widget en thumbtable als basisobject van dat alles : src/dtgtk/thumbnail.c. Dat was in principe de juiste richting: in plaats van miniaturen apart op meerdere plaatsen te tekenen, kon één widget activering, selectie, overlays, sterren, afwijsvlaggen, slepen-en-neerzetten, groepsranden en zoombaar gedrag centraliseren. Maar het volgde al heel snel een soortgelijk pad : de geschiedenis van vóór 2022 laat zien hoe snel die gemeenschappelijke widget overbelast raakte. Het handelde al snel uitgebreide overlays, MIPMAP-bijgewerkte callbacks, culling-verbindingen, scrollbalken, actieve-afbeeldingsafhandeling, filmstrip-callbacks, navigatie in volledig voorbeeld en CSS-afgestemde pictogramplaatsing af. Met andere woorden, thumbnail.c tekende niet zomaar een miniatuur. Het werd de gedeelde breuklijn tussen lichttafel, filmstrip, culling en voorbeeld.

De filmstrip begon als een aparte navigatiestrook, kreeg toen centrering op de actieve afbeelding, slepen-en-neerzetten, kaartinteracties, kopiëren/plakken-bewerkingen, vloeiend scrollen, HiDPI-fixes, CSS-aanpassing en uiteindelijk delen van het nieuwe miniatuur-callbacksysteem. Nogmaals, geen van die functies is op zichzelf onredelijk. Het probleem is dat de filmstrip eindigde met net genoeg aangepast gedrag om af te wijken van de lichttafel, terwijl hij toch nog deel van dezelfde miniatuurmachinerie probeerde te hergebruiken.

Bovendien was er het inconsistente „selectie"-paradigma in Darktable, verkocht als een „geen-klik-workflow", zodat veel schrijfbewerkingen afgehandeld konden worden zonder expliciet de te beïnvloeden afbeelding(en) te selecteren. Dat leidde tot veel ongewenste effecten en ongelukken, met dataverlies tot gevolg (de verkeerde sterwaardering toekennen aan de verkeerde afbeelding, waardoor die zou verdwijnen uit de huidige collectie als je die op waardering filterde) en vervelende undo-sessies. Maar de hele heuristiek van afbeeldingsselectie was ook broos: de muis in het venster bewegen kon de focus stelen van de foto die je expliciet vergrendelde met een klik of een toetsenbordselectie, maar niet altijd.

Om nog maar te zwijgen van het feit dat dat „handel bij muis-hover" verschillende SQL-queries in de bibliotheekdatabase triggerde, bij het hoveren over een nieuwe afbeelding, om bijgewerkte afbeeldingsmetadata op te halen en de inhoud van de metadatamodules te vernieuwen (metadata, EXIF- & IPTC-metadata, tags). Dit is allemaal omdat ze niet gecacht konden worden, en ze konden niet gecacht worden omdat Darktable niet in staat is de levenscyclus van zijn data bij te houden, dus moet het voortdurend alles vernieuwen. En het zal je batterij daarbij leegtrekken.

Dus toen de herschrijving begon nadat Ansel in 2022 geforkt was, was het probleem niet dat één slechte commit de lichttafel had gebroken. Het probleem was dat drie lagen geschiedenis op elkaar gestapeld waren:

  • lighttable.c droeg nog steeds de bagage van meerdere (nutteloze) bladermodi, daar gecodeerd als bijgedachten,
  • thumbtable.c concentreerde scroll-, offset-, actieve-afbeeldings- en collectienavigatielogica, die deels overlapte met de collectie- en selectie-backend,
  • thumbnail.c concentreerde steeds meer gedeeld GUI-gedrag plus SQL-code.

Daarom moest het latere werk architecturaal zijn. Op dat punt was er geen realistische manier om de symptomen één voor één te blijven repareren.

Front-end-wijzigingen

Culling was (conceptueel) nooit echt een aparte weergave

Een belangrijk onderdeel van die aftrekking was de culling-modus . In februari 2023 werden de aparte culling- en voorbeeldweergaven verwijderd omdat ze slecht verweven waren geraakt met sneltoetsen, weergaveschakeling en speciale-gevallenvertakking. De code was verstrengeld en omslachtig, maar het echte probleem zat in het ontwerp zelf: het was een overontworpen oplossing voor een veel eenvoudiger probleem, opgelost in de verkeerde laag.

De behoefte was om een willekeurige verzameling foto’s te isoleren die niet noodzakelijk aaneengesloten waren in de huidige collectie, om te beslissen welke de bewaarde zou zijn. Dat had geen nieuwe lay-out nodig (of twee…); het had een filter nodig om de collectie te beperken tot een willekeurige selectie. We hadden al filters om collecties te beperken op waardering, kleurlabel, bewerkte/onbewerkte status, enz.

Dat verschil is belangrijk. Een aparte culling-weergave dupliceert problemen die de lichttafel al moet oplossen: welke afbeeldingsverzameling actief is, hoe de selectie hersteld wordt bij het verlaten van de culling-weergave, hoe sneltoetsen gerouteerd worden, hoe zoom- en miniatuurstatus geïnitialiseerd worden, en wat er gebeurt wanneer je teruggaat naar het raster. Elke verbetering moet dan twee keer geïmplementeerd worden, één keer in de lichttafellogica en één keer in de culling-logica, en beide versies drijven uit elkaar. Om nog maar te zwijgen van het feit dat de culling-modus een statische modus en een dynamische modus had, die zowel verschilden in hoe je ermee interacteerde als in hun implementatie, en heel weinig gebruikers begrepen waar ze over gingen.

In Ansel kwam de culling terug als een collectiefilter, eerst als een eenvoudige manier om de huidige verzameling te versmallen, en later expliciet als de Beperken tot selectie-filterknop. Dat betekent dat de bedoeling voor de gebruiker hetzelfde bleef (de huidige collectie terugbrengen tot een willekeurige selectie afbeeldingen), maar de implementatie veranderde volledig: in plaats van een speciale weergave binnen te gaan, blijf je in de lichttafel en vertel je het filtergereedschap om alleen de momenteel geselecteerde afbeeldingen te tonen of alleen de afbeeldingen die overeenkomen met het huidige versmallingscriterium.

image
De nieuwe, geünificeerde filterwerkbalk. Filtercriteria zijn inclusief, en de pictogrammen gedragen zich als aanvinkknoppen: vink uit om overeenkomende afbeeldingen te verbergen, vink alles aan om alles te tonen (een contextmenu bij rechtsklikken geeft je een snelkoppeling om dat in één stap te doen). Tooltips verschijnen bij hover voor meer details. Het eerste pictogram vernieuwt de huidige collectie afhankelijk van de filters. Als je bijvoorbeeld afbeeldingen toont die met 3 sterren gewaardeerd zijn en je degradeert een afbeelding naar 2 sterren, wordt die niet automatisch uit de huidige collectie verwijderd totdat je die handmatig vernieuwt.

Voor gebruikers is het voordeel praktisch. Culling is nu samenstelbaar met de rest van de filterlogica in plaats van erbuiten te leven. Je kunt het combineren met waarderingen, kleurlabels of tekstzoeken omdat het gewoon nog een collectiefilter is. Sneltoetsafhandeling is eenvoudiger omdat er één speciale weergave minder is die om het eigenaarschap van sneltoetsen strijdt. En fixes voor scrollen, selectie, levensduur van miniaturen of overlaygedrag komen automatisch ook culling ten goede, omdat culling nu dezelfde lichttafelinfrastructuur gebruikt in plaats van een parallelle.

Dat was ook een kans om de filterwerkbalk te herzien, die knoppen en logica vermengde: een combobox-lijst voor waarderingen (van afgewezen tot 5 sterren), gekoppeld aan een vergelijkings-combobox-lijst (≠ = > < >= <=), maar schakelknoppen voor kleurlabels. Darktable 4.0 verving dat met een overontworpen set configureerbare widgets die niet-standaard GUI-paradigma’s gebruikten die in tooltips uitgelegd moesten worden. Ansel heeft het ontwerp platgeslagen: alles is een schakelknop die in een „include"-modus binnen zijn groep werkt, wat complexe selecties mogelijk maakt zonder complexe GUI:

  1. je hebt 3 groepen (waarderingen, kleurlabels, bewerkte status), plus beperken tot selectie en tekstzoeken,
  2. tussen die groepen zijn de filters exclusief, wat betekent dat ze een logische AND zijn,
  3. binnen die groepen zijn de filters additief, wat betekent dat ze een logische OR zijn.

In de schermafbeelding hierboven is bijvoorbeeld alles aangevinkt, dus is het filteren in de praktijk uitgeschakeld (we laten alles binnen). In het voorbeeld hieronder filteren we alle afbeeldingen binnen die een kleurlabel hebben ingesteld en al bewerkt zijn, ongeacht hun waardering (alle waarderingsknoppen zijn aangevinkt):

image

Selectie : backend- en GUI-statussen vermengen bijt je altijd in je reet

Gegroepeerde afbeeldingen waren nog een verborgen breuklijn. Een deel van de oude logica probeerde rekening te houden met groepering in SQL-queries, bij het genereren van collecties van afbeeldingen uit de bibliotheekdatabase, maar groepsranden, hover-statussen, selectiegedrag en daadwerkelijke zichtbaarheid zijn GUI-concepten. Die mismatch creëerde subtiele bugs: alle afbeeldingen uit een groep konden geselecteerd worden, of sommige genegeerd volgens heuristische regels die niet overeenkwamen met wat de gebruiker op het scherm zag.

Ik repareerde dat door groepsweergavebeslissingen terug in de GUI-code te verplaatsen. SQL levert de lijst met afbeeldingen; de interface beslist welke gegroepeerde leden zichtbaar, ingeklapt of gehoverd zijn. Daarna werden de in 2025 toegevoegde functies veel eenvoudiger: groepsranden konden opnieuw geïmplementeerd worden, optioneel gemaakt, uitgebreid naar de filmstrip, en beter zichtbaar gemaakt bij hover. Tooltips konden lui gevuld worden, alleen wanneer de gebruiker erover hovert. Eén SQL-verzoek per miniatuur bij initialisatie kon ook verdwijnen omdat de interface al genoeg lokale status had om te weten wanneer groepsinformatie daadwerkelijk nodig was.

Voor gebruikers is het voordeel niet abstract. Groepsranden betekenen nu wat ze tonen, selecties komen precies overeen met de zichtbare groepsstatus, en gegroepeerde afbeeldingen voelen niet langer aan alsof ze half beheerd worden door de database en half door de interface.

Nog een verborgen probleem was selectie. In de oude code was selectie te verstrengeld met collectie-internals, ruwe SQL-toegang en fallback-logica. Dat klinkt onschuldig totdat een afbeelding uit de huidige collectie verwijderd wordt, een groep ingeklapt wordt, of een weergaveschakeling plaatsvindt terwijl een ander deel van de code nog steeds uitgaat van het vorige selectiemodel. In Darktable werden afbeeldingsselecties afgehandeld als een SQL-backend, aangezien ze in de bibliotheek opgeslagen worden om bij de volgende sessie hersteld te worden. Dit is geen probleem totdat de selectie-backend gehackt wordt om te raden welke afbeeldingen zichtbaar zijn in de GUI, en de GUI de selectie daadwerkelijk via de backend afhandelt. Dit is de slechtst mogelijke splitsing van functionaliteit tussen backend- en GUI-lagen.

Dit is waarom 2025 een heel cluster selectiegerelateerde commits bevat: het herschrijven van de selectie-API, het verwijderen van het oude main_image-begrip, het unificeren van de actieve-afbeeldings-getters/setters, het repareren van bereikselectie, het herstellen van selectiestatus over weergaveschakelingen heen, en het toevoegen van rowid-fallbacks wanneer scroll_to_selected het oorspronkelijke afbeeldings-id niet meer kan vinden. De herschrijving veranderde de architectuur: de thumbtable berekent nu zichtbare, voor de gebruiker betekenisvolle selecties en geeft een lijst van afbeeldings-id’s door aan de selectielaag, in plaats van dat de GUI achteraf betekenis probeert te reverse-engineeren uit de collectiestatus. De selectie-SQL-API is een simpele „opslaan naar/herstellen uit database"-backend geworden.

Het concrete voordeel voor de gebruiker is dat Shift + klik bereikselectie en „afbeelding om op te handelen" What You See Is What You Get zijn: je kunt geen afbeelding selecteren die niet zichtbaar is op het scherm, en de backend hoeft niet te raden wat zichtbaar is. En terugscrollen naar de geselecteerde afbeelding mislukt nu veel minder vaak in randgevallen. Het verborgen probleem was niet één kapotte sneltoets; het was dat selectiesemantiek in de verkeerde laag leefde.

De fix is om duidelijk te splitsen wat bij backend- en bibliotheekbeheer hoort van wat bij GUI-statussen en -interacties hoort, en om een rigide interface tussen beide te bieden. Dat maakt de verschillende delen van de code beter omsloten, immuun voor wijzigingen in de andere delen, en centraal beheerd.

Hover- en focus-glitches kwamen voort uit meerdere geldige systemen die botsten

Muis-hover, toetsenbord-hover, focus en klikafhandeling werden elk om legitieme redenen geïmplementeerd, maar ze deelden geen enkel arbitragepunt. Darktable heeft nu zelfs een trotse marketingpraatje geschreven op de achtergrond van de lichttafel, wanneer de collectie leeg is, dat die „klikloze" workflow prijst die je toestaat afbeeldingsmetadata te overschrijven zonder het te willen of zelfs te weten. Dat voelt voor mij als een autobedrijf dat een auto adverteert die zo ontworpen is dat je het stuur met je knieën kunt gebruiken (en je handen om bier te drinken).

Daarom kon ouder lichttafelgedrag inconsistent aanvoelen zonder één enkele voor de hand liggende bug: het was kapot door ontwerp. Klikken op een overlayknop om de afbeelding te waarderen kon ook de hele miniatuur selecteren, maar waarderen vanaf het toetsenbord niet. Toetsenbordbeweging en muisbeweging konden beide denken dat ze de „huidige" afbeelding bezaten en allerlei destructieve metadatawijzigingen triggeren louter door over de foto te hoveren en zonder om bevestiging te vragen. Afbeeldingen filteren op sterwaardering en per ongeluk de waardering van een afbeelding wijzigen kon die onverwacht doen verdwijnen. Verborgen widgets konden nog steeds hovergerelateerde logica ontvangen. GTK-focus kon één ding beslissen terwijl de weergavelaag iets anders verwachtte.

De fixes hier gingen minder over het toevoegen van nieuwe functies dan over het kiezen van één eigenaar voor de status. Over-status-dispatch werd gecentraliseerd. Toetsenbordbladeren kreeg een zinnig beginpunt en een expliciet annuleerpad met Shift+Ctrl+A. Focusweergave werd expliciet gemaakt. Verborgen widgets stopten met deelnemen aan hoverlogica die ze niet konden weergeven. Klikken op overlayknoppen stopte met lekken in miniatuurselectie. Er wordt geen metadata meer (over)geschreven zonder een expliciete knopdruk, hover-events zijn allemaal alleen-lezen.

Ansel heeft twee eenvoudige regels :

  • elke actie die (meta)data zal (over)schrijven, wordt alleen gedaan op expliciet geselecteerde afbeeldingen,
  • expliciete selectie wordt alleen gedaan door te interacteren met iets „hards": een muisklik of een toetsaanslag.

Vervolgens zijn hover-events voorbehouden aan alleen-lezen-events.

Het voordeel voor gebruikers is dat de lichttafel meer reageert als één enkel interactiemodel. Het verborgen probleem was niet dat hover kapot was. Het was dat verschillende soorten hover tegelijkertijd „juist" waren en daarom collectief fout.

Backend-wijzigingen

De thumbtable herouderen tussen lichttafel en filmstrip

Een van de dieper verborgen problemen was widget-eigenaarschap. In Darktable werd hetzelfde thumbtable-raster herouderd tussen lichttafel- en filmstrip-contexten. Op papier voorkwam dat codeduplicatie en widgetproliferatie, en dat klinkt slim. In de praktijk maakte het de scrollstatus, miniatuurinitialisatie, garbage collection en event-levensduur afhankelijk van waar de widget het meest recent geleefd had. Om nog maar te zwijgen van het feit dat herouderen traag was, dus heen en weer gaan tussen lichttafel en donkere kamer werd vertraagd met ongeveer 1 s, de tijd die GTK nodig had om nieuwe miniatuurformaten te herberekenen, mogelijk de cache om nieuwe miniaturen te vragen, en widgets opnieuw te tekenen.

Dit kwam op zeer concrete manieren aan de oppervlakte. De scrollpositie kon springen of inconsistent worden. Miniaturen konden op het verkeerde moment verborgen, getoond of vernietigd worden. Garbage collection werd moeilijker om over te redeneren omdat de widgethiërarchie niet stabiel was. Uiteraard werd dat allemaal commit na commit opgelost, maar dat liet de code in een staat van onhoudbare complexiteit achter.

De echte fix was om te stoppen met proberen slim te zijn. Lichttafel en filmstrip hebben nu aparte thumbtables. Dat betekent iets meer expliciete structuur in de code, maar veel minder toevallige statussen. Het voordeel voor gebruikers is zichtbaar in gelijkmatiger scrollen, minder lay-out-glitches bij weergavewijzigingen, en een filmstrip die zich gedraagt als een echt zusje van de lichttafel in plaats van als een hergebruikt fragment.

Zoom werd pas bruikbaar nadat miniatuurformaten consistent gemaakt waren

De vroege poging tot een zoombare lichttafel mislukte om een structurele reden: miniatuurgeometrie, gecachte mipmap-formaten en navigatie op weergaveniveau deelden niet hetzelfde model. Daarom moest de functie eerst verwijderd worden. Als ik hem ter plekke was blijven oppoetsen, zou het resultaat alleen een beter ogende kapotte functie geweest zijn.

Het latere werk repareerde de afhankelijkheidsketen in de omgekeerde volgorde. De weergave-backend kreeg de steigers voor lichttafelzoom, daarna werd de miniatuurcache geleerd coherenter te redeneren over miniatuurformaten, werd een miniatuur-API met volledige resolutie voorbereid, en werden daarbovenop clamping, pannen en zwaartepunt-gebaseerd automatisch pannen toegevoegd. Pas toen de onderliggende lagen het eens waren over afbeeldingsformaat en ophaalstrategie, werd 200% zoom een verdedigbare functie.

Het voordeel voor gebruikers is dat zoom nu gekoppeld is aan de cache en aan navigatie in plaats van ertegen te vechten. Slepen om te pannen, Shift + slepen over zichtbare miniaturen, en automatisch pannen naar het zwaartepunt van details werken allemaal omdat de onderliggende pipeline weet wat gezoomde miniaturen horen te zijn.

De verwarrende betekenis van „zoom" in de lichttafel is bijgewerkt: de term „zoom" werd zowel gebruikt voor buitenzoomen (aantal afbeeldingen per rij, wat indirect hun zichtbare grootte beïnvloedt) als voor binnenzoomen (vergroting binnen het afbeeldingskader). Dus nu hebben we kolommen voor de buitenzoom, en zoom voor vergroting.

image

Let niet op het kruis in plaats van het --symbool in de kolommen-spinbutton, het is een bug van GTK met het KDE/Plasma-thema Breeze .

Dit maakte het mogelijk om een functie te implementeren die ik al lang in gedachten had: barycentrisch automatisch pannen bij miniatuurvergroting.

image
Zoom naar passend
image
Zoom inschakelen op 100 %

Je kunt zien dat, bij het inschakelen van 100% zoom, afbeeldingen automatisch uitgelijnd worden op hun inhoud ondanks volledig verschillende kadrering en beeldverhoudingen. Dit gebruikt functie-analyse op basis van wavelet-decompositie, waaruit we de coördinaten van het zwaartepunt van details berekenen. Het is niet perfect omdat het niet hetzelfde deel van het gezicht over de afbeeldingen heen pakt, maar het geeft ons in alle gevallen het gezicht. Dat is een project dat ik lang op de lange baan had geschoven, maar het was niet mogelijk met het vorige lichttafelontwerp.

Onnodig te zeggen dat slepen in ingezoomde miniaturen hersteld is, en slepen in alle ingezoomde miniaturen is ook toegevoegd (Shift+Slepen). Deze functie werd lang gevraagd in Darktable, maar was duidelijk niet mogelijk in hun waardeloze ontwerp.

Zelfopgelegde metadatavertraging

De oude lichttafel bleef de database dezelfde vragen stellen, één miniatuur tegelijk. Op een kleine collectie is dit makkelijk te missen. Op een grotere komt het aan de oppervlakte als micro-haperingen bij het openen van de weergave, het tonen van overlays, het hoveren over groepen of het vernieuwen van geschiedenisgevoelige data. Het verborgen probleem was niet dat ruwe SQL absoluut gezien traag was; het was de herhaling en timing van die queries.

De fix kwam in fasen. Alleen-lezen-metadata begon gecacht te worden vanuit afbeeldingsstructuren. Vervolgens kon in februari 2026 metadata voor een hele collectie opgehaald worden met één SQL-query in plaats van één query per afbeelding. De thumbtable kon de afbeeldingscache zaaien vanuit de collectie die hij op het punt stond weer te geven. Het cachen van miniatuurinfo werd gerefactord tot een helper en later teruggevoegd in dt_image_t zodat metadata niet tussen parallelle structuren hoefde te stuiteren. Zelfs kleine wijzigingen zoals het verwijderen van history-hash-pings bij het opnieuw tekenen doen er hier toe, omdat ze onzichtbare round-trips uit hete paden snijden.

Veel nutteloze SQL-queries per miniatuur zijn verwijderd, samen met SQL-code in de lichttafel-GUI-code, zodat functionele lagen nu goed gesplitst zijn.

Het gebruikersvoordeel is exact: het openen van grote collecties hapert minder, overlay-zware weergaven aarzelen minder, en geschiedenis- of metadata-updates verspreiden zich met minder zichtbare pauzes.

De mipmap-cache moest ook gerepareerd worden

De mipmap-cache is de laag die RAW-afbeeldingen en vooraf gegenereerde miniaturen in het RAM laadt, en ze leegt wanneer geheugen schaars wordt. Dat was een van de diepst verborgen bronnen van zichtbare bugs. Wanneer alles op één lijn lag, werkte het goed genoeg. Wanneer het bronbestand kleiner was dan verwacht, wanneer een ingebedde JPEG-preview vreemd was, wanneer de history-hash ongedefinieerd was, of wanneer de invalidatie van de schijfcache achterliep op bewerkingen, zou de gebruiker geen „cache-bug" ervaren. Ze zouden verouderde miniaturen zien, kapotte previews of afbeeldingen die weigerden te vernieuwen.

Daarom ziet zoveel van het werk uit 2025 in de cache er chirurgisch uit. De logica voor bufferallocatie werd herschreven omdat het eigenaarschap te verward was. De afhandeling van te kleine invoer werd verbeterd omdat aannames over invoergrootte te ver lekten. Ingebedde previews werden niet langer zo agressief weggegooid omdat te kleine previews nog steeds nuttig waren voor consistentie over zoomniveaus heen. Sidecar-JPEG-previews werden gebruikt wanneer aanwezig, in plaats van de in de RAW ingebedde miniaturen. Cache-invalidatie werd gehard, en gecachte miniaturen werden echt van de schijf verwijderd wanneer dat hoorde. Opnieuw gegenereerde mipmaps begonnen hun hash terug naar de database te schrijven, synchroon met de geschiedenisstatus, en het geval van de ongedefinieerde history-hash liet mipmaps niet langer onopgeslagen.

De sluwste bug die ik vond, lag begraven in de onzinnige complexiteit van de verweving van thumbtable, weergave en mipmap-cache. Terwijl het renderen van miniaturen uitgesteld werd naar een aparte thread voor prestaties (zoals het hoort), en meer dan één thread gestart kon worden om (naar verluidt) meerdere miniaturen tegelijk te verwerken (mogelijk met meerdere GPU’s), waren alle verwerkingsthreads en de GUI eigenlijk aan het strijden om de mipmap- en afbeeldingscaches te vergrendelen, wat er alleen maar voor zorgde dat de GUI haperde terwijl pipelines liepen. Het is alleen omdat ik onder de aanname werkte dat ik dingen volgens het boekje herschreef, en ik wist dat de manier volgens het boekje sneller zou moeten zijn dan dat, dat ik bleef graven totdat ik ontdekte waarom het nog steeds niet zo snel was als verwacht, en daarbij alles laag voor laag vereenvoudigde.

Het voordeel voor gebruikers is dat de cache betrouwbaarder is. Na bewerkingen is de lichttafel minder geneigd een verouderde miniatuur te tonen. Op lastige bestanden mislukt previewgeneratie minder vaak. Bij herhaalde bezoeken aan dezelfde collectie gedraagt de schijfcache zich meer als een cache en minder als een verouderd schermafbeeldingsarchief. Maar dat allemaal, zonder voortdurend alles te hoeven vernieuwen/herberekenen/opnieuw tekenen om er zeker van te zijn.

Ook zijn de opties om ingebedde JPEG’s te gebruiken of een herberekening te forceren verplaatst van de voorkeuren naar het globale menu en kunnen ze tijdens runtime gewijzigd worden:

image

Dit is nog een voorbeeld waar het opschonen en vereenvoudigen van de backend de weg vrijmaakte voor frontend-uitbreiding en nieuwe functies die gewoon logisch zijn.

Thread-veiligheid en race conditions

Een klassieke reden dat deze bugs zo lang bleven bestaan, is dat ze vereisten dat de gebruiker sneller was dan de code: snel scrollen, een weergave verlaten terwijl miniaturen nog gebouwd worden, formaat wijzigen terwijl een achtergrond-surface nog geproduceerd wordt, of een widget sluiten vlak voordat een worker-thread een update pusht.

Daarom doet het thread-veiligheidswerk van 2025-2026 er toe. Het ophalen van miniaturen werd dieper in achtergrondtaken verplaatst, en later expliciet uitgesteld zodat het renderen van de donkere kamer prioriteit kon behouden. Het vernietigen van miniaturen werd verplaatst naar veiligere opschoonpunten. Achtergrondtaken die miniaturen produceren, leerden zichzelf te annuleren wanneer de widget die ze bedienden verdween. Afbeeldingssurfaces werden beschermd door mutexen. Vrijgemaakte pointers werden genulld. Afbeeldingsbuffers verhuisden naar beter beheerde allocatiepaden. Het doel van dit alles was niet „meer threads"; het was om te voorkomen dat oude threads in dode status schreven of om vergrendelingen streden.

Het resultaat voor de gebruiker is minder segfaults, minder willekeurige glitches tijdens snel scrollen, en minder gevallen waarin de interface onder belasting tegen zichzelf lijkt te racen.

Verschillende threadvergrendelingen, in de loop der jaren toegevoegd om problemen te lappen, zijn ook verwijderd. Veel ervan waren overbodig (maar dat bleek niet onder de waanzinnige complexiteit van het geheel), sommige schaadden de prestaties actief, en allemaal verborgen ze slecht ontwerp. We hebben nu minder vergrendelingspunten, maar het resultaat is leesbaarder en robuuster.

Als gevolg daarvan zijn miniatuurwidgets nu volledig zelfstandige objecten. Ze beheren intern hun eigen threads voor de renderingpipeline van de miniatuur, die rechtstreeks in wisselwerking staan met hun eigen gecachete beeldoppervlak, zodat ze deze zelf kunnen aanmaken of afsluiten. Dit gecachete beeld wordt alleen ongeldig gemaakt wanneer de beeldgeschiedenis verandert, wat expliciet gemaakt wordt door de backend van de ontwikkelingsgeschiedenis. Omdat de miniatuurwidget zijn eigen toestand kent (zichtbaar of niet, wel of geen beeldverversing nodig, grootte, focus-peaking-modus, enz.), wordt hij veel robuuster dan wanneer je al die zaken zou proberen af te handelen vanuit hogere lagen die er niet in slagen met elkaar te communiceren.

Dit was niet mogelijk met het ontwerp van Darktable, omdat het voortdurend miniatuurwidgets dynamisch toevoegde aan en verwijderde uit de huidige lichttafelweergave, afhankelijk van de zwevende lijnpositie, wat een poging was om de vertragingen op te vangen van al die threads die met elkaar concurreerden om toegang tot de cache. Maar bovendien probeerden de miniatuurwidgets van Darktable onmiddellijk een beeld uit de mipmap-cache op te halen op het moment dat ze werden aangemaakt, waardoor het gebruik van CPU en geheugen-I/O piekte en de UI feitelijk bevroor. Maar dat „oplossen" door de levensverwachting van miniatuurwidgets te verkorten maakte het onmogelijk om ze hun eigen toestand intern te laten beheren, dus moest dat gedaan worden door hoge lagen, die met elkaar moesten communiceren om toestanden bij te werken, wat op sommige plaatsen ook gebeurde maar ten koste van ondraaglijke complexiteit.

In plaats daarvan worden alle miniaturen van Ansel in één keer geïnitialiseerd, maar halen ze pas lui een beeld uit de mipmap-cache zodra ze zichtbaar worden, en cachen ze het dan intern. Zo blijft het scrollen door de miniaturentabel vlot, zelfs met een collectie van 500 afbeeldingen die allemaal hun beeld genereren.

Importvoorbeelden als neveneffect verbeterd

Het importvenster werd door dit werk beïnvloed om dezelfde reden als de lichttafel: het heeft snelle voorbeeldextractie, metadata-inspectie en verstandig terugvalgedrag bij gedeeltelijk ondersteunde bestanden nodig. Zodra de mipmap- en miniatuurpaden herschreven waren, werd het laden van raw-voorbeelden bij het importeren sneller, en verbeterde ook de ondersteuning voor TIFF/DNG.

Dit is een goed voorbeeld van waarom de herschrijving laag in de stack moest gebeuren. Als de onderliggende voorbeeldmachinerie inefficiënt of fragiel is, erven zowel de lichttafel als het importeren dezelfde pijn. Zodra die machinerie herschreven was, profiteerden beide ervan.

Globale architecturale wijzigingen

In mijn vorige artikel liet ik zien hoe de cache van de werkende pipeline het heen en weer gaan tussen lichttafel en donkere kamer bijna onmiddellijk maakt, omdat het hele beeld niet opnieuw berekend hoeft te worden. Het werk dat hier aan de GUI is verricht, lost hetzelfde probleem van vertragingen bij het wisselen van weergave op, maar dan op GUI-niveau. Er is dus geen vertraging bij het wisselen tussen beide weergaven.

Dit is belangrijk omdat de vertraging bij het wisselen van weergave als excuus is gebruikt om functies (modules/gereedschapskisten) te dupliceren tussen de lichttafel en de donkere kamer, wat de GUI alleen maar rommeliger maakt. Deze hele backendverbetering maakt het dus mogelijk om het GUI-ontwerp te verbeteren door elke weergave te specialiseren voor één enkele taak:

  • metadatabeheer voor de lichttafel (plus uiteraard culling),
  • beeldbewerking voor de donkere kamer.

Als gevolg daarvan zijn de gereedschapskisten metadata en tags uit de donkere kamer verwijderd.

Hoeveel regels code heeft de herschrijving bespaard?

Als we alleen de niet-commentaar-, niet-lege regels tellen met cloc, en de laatste boom vóór 1 januari 2022 vergelijken met de huidige boom voor de hoofdbestanden die hier besproken worden, heeft de herschrijving in totaal 4.714 regels code bespaard.

De reikwijdte voor die telling is:

  • de hele map data/themes/ vóór 2022, vergeleken met de huidige 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 bestanden die door de herontwerp of het opschonen van de collectie-GUI regelrecht verwijderd zijn: src/dtgtk/culling.[ch], src/libs/tools/view_toolbox.c, src/libs/collect.h en src/libs/recentcollect.c.

Binnen die reikwijdte ging het totaal van 15.257 regels code vóór 2022 naar 10.543 vandaag. De herschrijving heeft daarmee in totaal 4.714 regels code bespaard. De grootste besparingen kwamen voort uit het regelrecht verwijderen van de culling-weergave (src/dtgtk/culling.c) (-1.406 regels), het inkrimpen van de lichttafel (src/views/lighttable.c) (-976), het verwijderen van 7 oude thema-stylesheets en het samenvouwen ervan tot één (-864 verspreid over data/themes/), het inkrimpen van src/dtgtk/thumbnail.c (-460), src/dtgtk/thumbtable.c (-458), het verwijderen van src/libs/recentcollect.c (-358) en het inkrimpen van src/views/view.c (-314). Sommige bestanden groeiden wel, met name src/libs/tools/filter.c (+215) en src/libs/collect.c (+157), omdat het deels de bedoeling was om speciaalgevalgedrag zoals culling en oude collectie-GUI-vertakkingen terug te verplaatsen naar eenvoudigere gedeelde infrastructuur in plaats van ze in parallelle weergaven en zijmodules te houden.

Het aantal regels is op zichzelf geen kwaliteitsmaatstaf. Bij tal van herschrijvingen wordt code slechts verplaatst. Maar hier komt het getal overeen met de ontwerpwijziging: minder gedupliceerd gedrag, minder parallelle weergaven, minder compenserende hacks, en minder plaatsen waar de GUI, de weergavelaag en de cache allemaal hetzelfde probleem tweemaal moesten oplossen.

Alles wat ik heb opgelost, heb ik opgelost door de logica en de code te vereenvoudigen. Er was geen omweg toegestaan.

Benchmarks

Alle looptijden zijn berekend op een Lenovo ThinkPad P51 laptop (Intel Xeon CPU E3-1505M v6 @ 3.00GHz, Nvidia Quadro M2200 GPU met 4 GB VRAM, 32 GB RAM, 4K-scherm), CPU in prestatiemodus, Linux Fedora 41 met KDE/Plasma-desktop. Looptijden van de pixelpipeline worden niet vergeleken (buiten de reikwijdte; zie het vorige artikel). Ansel Master is genomen bij commit 09749f1d  (21 feb. 2026).

BeschrijvingAnsel MasterDarktable 5.0
Tijd van het opstarten van de app tot het tekenen van de laatste lichttafelminiatuur (dezelfde collectie)2.12 s7.49 s
Tijd om te wisselen van lichttafel naar donkere kamer (dezelfde afbeelding)0.2 s1.2 s
Tijd om te scrollen (begin->einde) door dezelfde collectie van 471 afbeeldingen*0.7 s5.0 s

*: miniaturen vooraf geladen in de schijfcache in beide gevallen, 5 miniatuurkolommen per rij, 4K-resolutie, geen rechterzijbalk.

Als „oplossing" heeft Darktable 5.x ons gezegend met een prachtig opstartscherm, wat meer dan wat ook een bekentenis is.

Het volgende is gemeten op batterij, in energiebesparingsmodus, met de applicatie inactief (geen gebruikersinteractie) gedurende 5 minuten, met Intel Powertop. Het basisverbruik van het hele inactieve OS is 1,6 % CPU. (Vermogen wordt alleen voor de app gegeven, % CPU wordt voor het hele systeem gegeven):

WeergaveAnsel MasterDarktable 5.0
Lichttafel1.8 % CPU, vermogen: 0.85 mW2.7 % CPU, vermogen: 103 mW
Donkere kamer1.8 % CPU, vermogen: 7.65 mW1.8 % CPU, vermogen: 22 mW

Deze cijfers vertegenwoordigen het basis-stroomverbruik van de GUI alleen (GTK, achtergrondwerkers, geplande timers, enz.). Darktable lekt prestaties weg via de GUI, en het moeizame werk dat in 2023-2024 is verricht aan het optimaliseren van pixelverwerkingsmodules voor een extra 15-50 ms is volkomen irrelevant.

Conclusie

Op dit punt ben ik er stellig van overtuigd dat het Darktable-„project" alleen „ontwikkelaars" aantrekt die geen cognitief onderscheid zouden kunnen maken tussen GUI en backend al hing hun leven ervan af. GUI-problemen worden dus opgelost in de backend, backendproblemen worden opgelost in de GUI, en dit doet de codecomplexiteit met de tijd oncontroleerbaar groeien, wat later rechtvaardigt om nieuwe functies toe te voegen door zo min mogelijk code te hacken in een codebase die niemand nog begrijpt. Om nog maar te zwijgen van het feit dat niets daarvan gedocumenteerd was, dus moest ik het over meerdere jaren pijnlijk reverse-engineeren, recursief hier een beetje en daar een beetje vereenvoudigend, tot het uiteindelijk convergeerde naar een schone algehele logica.

Onnodig te zeggen dat alle omwegen en „snelle oplossingen" die waren toegevoegd, verwijderd zijn. Ze dateerden allemaal van na 2020, wat een zorgwekkende trend in de verslechtering van de codekwaliteit aantoont.

Dit opschoningsproject heeft 4 jaar van mijn leven gestolen, mij geen plezier bezorgd, en de mensen die alle regressies introduceerden die ik pijnlijk heb opgelost, moeten ter verantwoording worden geroepen voor de gevolgen van hun daden. Er is een enorm verschil tussen niet genoeg tijd hebben om dingen goed te doen en veel mensuren verbruiken om dingen slechter te maken. En dan weigeren toe te geven dat je dingen slechter hebt gemaakt, weigeren te aanvaarden dat er een probleem is, en je bevestigingsvooroordeel voeden door alleen te luisteren naar de feedback van tevreden kampeerders, is het ultieme bewijs van domheid.

Darktable is verworden tot stront, en ik heb zojuist, technisch, uitgelegd waarom. Het Darktable-team en zijn bijbehorende forum van tech-bro’s  willen mensen doen geloven dat ik boos op hen werd omdat ze mijn wijzigingen niet wilden accepteren, en dat het allemaal een interpersoonlijk probleem is. Voor de leek die niet begrijpt wat ik hier en in de vorige artikelen heb geschreven, is het gemakkelijker om te geloven in interpersoonlijke woede dan om te begrijpen hoe een opeenvolging van slechte technische beslissingen over meerdere jaren mij tot slachtoffer maakte van de problemen die zij creëerden, omdat ik hier de enige fulltimer was, die ervan afhankelijk was voor mijn levensonderhoud. Ik werd boos op hen omdat ze telkens weer op mijn stoep stonden te schijten, en ik het herhaaldelijk moest opruimen. Dit is een vorm van geweld die echt moeilijk te zien en te herkennen is omdat het zich niet materieel manifesteert: het is een manier om je leven moeilijker te maken, dagelijks, stap voor stap, alleen maar omdat een stel amateuristische mannen van middelbare leeftijd zonder vaardigheden deel wilden uitmaken van iets cools zonder zich het schadelijke effect van hun bijdragen op het hele project te realiseren.

En het was aan mij om de rotzooi op te ruimen omdat ik blijkbaar de enige was die zich bekommerde om de regressies, de vreemde willekeurige bugs en crashes, de ergste GUI-„innovaties" die zelfs mijn eigen vrouw ervan weerhielden om Darktable te gebruiken omdat het gewoon overweldigend is, en alle nieuwe vertragingen die zich jaar na jaar bleven opstapelen. Zoals Chris Elston me in 2022 op de Darktable-IRC-chat zei, voordat ik die voorgoed verliet, over zaken die ik in 2019 had opgelost en die zij in 2022 opnieuw kapotmaakten: „hou je mond en repareer het". Als dat geen geweld is, dan weet ik het niet meer. En nu proberen ze het gerucht te verspreiden dat ik degene was die toxisch was. Het hele team is toxisch. Hun nonchalante werkcultuur is toxisch. Hun manier om enthousiast te worden over alles, zolang het maar nieuw is, zonder de onderhoudskosten, functieduplicatie en algehele overweldiging van de gebruiker te overwegen, is toxisch. Hun gebrek aan zorg voor de toekomst van het project en voor de gevolgen van de keuzes die ze maken, is toxisch.

En wat vooral toxisch is, is dat op discuss.pixls.us elke post die mijn lof of de lof van Ansel bevat, gemarkeerd en verborgen wordt. Er is geen vrijheid van meningsuiting op een forum voor vrije software. Ze hebben het communisme gewoon in stalinisme veranderd.


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