Entre janeiro de 2022 e março de 2026, o Ansel integrou 297 commits (sem contar merges) referentes à grade da mesa de luz, suas miniaturas e seu pipeline de renderização e caching. Tentei me virar com o desleixado código da mesa de luz do Darktable, apenas desengordurado, pelo maior tempo que consegui, mas infelizmente era pura dívida técnica e era dolorosamente lento.
De fato, o Darktable “gerencia” a porcaria de sua mesa de luz reduzindo o tamanho dela: os painéis laterais esquerdo e direito ocupam bastante superfície de exibição, o que deixa uma área ainda menor para a mesa de luz repintar. Como o Ansel removeu o painel lateral direito, fundindo seu conteúdo com o esquerdo e com o menu global, havia mais superfície para pintar, mais trabalho de CPU a fazer, e o péssimo design da mesa de luz tornou-se ainda mais prejudicial.
Esse projeto agora está concluído. Este artigo é um mapa do que mudou, do que foi removido, do que foi reformulado, de como o novo comportamento funciona e do que os usuários de fato ganham com isso.
Note
Algumas gírias da aplicação usadas abaixo, para leitores que não vivem dentro do código-fonte:
- Mesa de luz: a visualização de navegação do Ansel, onde você gerencia, ordena, seleciona e inspeciona muitas imagens de uma vez.
- Sala escura: a visualização de edição do Ansel, onde você trabalha em uma imagem em detalhe.
- Tira de filme: a faixa horizontal de miniaturas mostrada abaixo ou ao lado de outra visualização, usada para navegação rápida pela coleção atual.
- Miniatura: a pequena imagem de pré-visualização mostrada na mesa de luz ou na tira de filme, junto com selos, sobreposições e metadados.
- Thumbtable: o widget interno que gerencia toda uma grade ou faixa de miniaturas, incluindo rolagem, seleção, redesenho e tempo de vida da miniatura.
- Sobreposição: a informação extra desenhada sobre uma miniatura, como estrelas, marcas de rejeição, rótulos de cor, bordas de grupo ou metadados.
- Coleção: o conjunto atual de imagens mostrado pela mesa de luz após aplicar os filtros ativos, a busca e as regras de agrupamento.
- Imagens agrupadas: várias imagens vinculadas entre si para que possam ser mostradas como uma única pilha recolhida ou como membros expandidos do mesmo grupo.
- Filtro de coleção: a ferramenta da mesa de luz que restringe a coleção atual por texto, avaliação, rótulos de cor ou outros critérios. No Ansel, ela também absorveu o que costumava ser chamado de modo de triagem (culling).
- Cache de mipmap: o subsistema que armazena e recupera imagens de pré-visualização pré-computadas em vários tamanhos, para que o Ansel não tenha que decodificar e renderizar o arquivo de origem do zero toda vez que uma miniatura é mostrada.
- Hash de histórico: uma impressão digital interna do histórico de edição de uma imagem, usada para saber se uma miniatura em cache ainda corresponde ao estado de edição atual.
Uma breve história de design ruim
Para entender por que a reescrita de 2022-2026 acabou tocando em tantos arquivos, ajuda olhar um passo mais para trás.
A visualização da mesa de luz, src/views/lighttable.c, vinha acumulando responsabilidades por mais de uma década. Começou como o lugar que cuidava da grade do gerenciador de arquivos e do zoom básico, e então, gradualmente, absorveu a pré-visualização completa, a pré-visualização fixa, a triagem, a memória de estado dos painéis, o agrupamento, o reordenamento por arrastar e soltar, a navegação por teclado, as políticas de sobreposição, a coordenação da tira de filme e um monte de roteamento de atalhos. Se você olhar o histórico anterior a 2022, muitos commits são apenas sobre impedir que essas coisas briguem entre si: saltos de deslocamento ao dar zoom, bugs de rolagem da pré-visualização, seleção que não permanecia em sincronia, triagem e pré-visualização reentrando num estado meio quebrado, visibilidade dos painéis não sendo restaurada corretamente, bordas de grupo com falhas, e avaliações ou rótulos de cor movendo a grade inesperadamente.
- 2015 : redesenhar toda a mesa de luz porque algumas miniaturas não atualizavam corretamente … (note também o código de desenho Cairo pintando bordas de grupo logo antes das consultas SQLite e da busca de miniaturas no cache… pelo preço promocional de 3 por 1).
- … depois revertido porque drenava a CPU (sem brincadeira…),
- 2020, refatoração: … feito de novo em algum momento como uma função de timeout , que já não tem sucesso garantido (por que 250 ms?), e removido em fevereiro de 2020,
- … feito de novo e re-removido em abril de 2020 ,
- … porque foi movido para a miniatura ,
- … reforçado em outubro de 2020 , ou seja, simplesmente usado como um
g_timeoutdeveria ser, porque ele podia chamar uma miniatura removida e causar uma falha de segmentação (e então travar), - … até que eu me livrei dele definitivamente em fevereiro de 2025 , para lidar com isso… sabe como, alimentando diretamente o ponteiro da miniatura para o trabalho em segundo plano encarregado de criar a imagem e deixando-o chamar o redesenho direcionado quando estiver pronto (e lidar de forma elegante com o caso em que a miniatura foi removida antes de o pipeline concluir).
Isso mostra, portanto, uma incapacidade completa de acompanhar o ciclo de vida de uma imagem em cache (ou de qualquer dado naquele software maldito, aliás), enquanto se está completamente confortável e relaxado com isso. Abordei essa questão simplificando a assincronicidade em segundo plano, mas outra opção teria sido usar um sinal. Não consigo superar o fato de que os 3 caras que trabalharam nisso ao longo de 10 anos escrevem código C há o dobro do tempo que eu. Em 2018, eu nem sabia o que era a GLib.
Caso você tenha interesse, a maneira como abordei isso é :
- os widgets de miniatura mantêm uma variável de estado representando a validade da própria imagem,
- quando o GTK quer redesenhar a miniatura, se a variável de estado diz “inválida”, o widget de miniatura gera uma thread em segundo plano computando um pipeline para criar a imagem que falta,
- quando a thread em segundo plano retorna, ela coloca a imagem de saída na miniatura que a chamou, por ponteiro, em uma superfície (em cache), atualiza a variável de estado para “válida” e então envia um evento de fila de redesenho do GTK sobre o widget (não sobre a grade inteira),
- se o widget está invisível, nada acontece (o GTK não enfileira eventos de redesenho para widgets invisíveis). Na próxima vez que o GTK quiser redesenhar a imagem, ele usa a superfície em cache de dentro do widget,
- alterações de histórico e metadados enviam um sinal “informação de imagem alterada” que publica o ID da imagem. A thumbtable tem um manipulador de sinal escutando isso, que encontra a miniatura por ID na grade (a partir de uma hashtable, então
O(N)) e redefine sua variável de estado para “inválida”. Novamente, se 100 imagens forem alvo da alteração e todas estiverem invisíveis, nada acontece.
A arquitetura parece mais complexa, mas na verdade é menos código e é simplesmente robusta: não há adivinhação.
Uma reescrita da era COVID tentou refatorar aquilo em src/dtgtk/thumbtable.c, que logo passou a fazer parte da mesma deriva. Assim que a abstração dedicada da thumbtable apareceu, ela se tornou o controlador de tráfego entre estado da coleção, rolagem, deslocamentos, imagens ativas, navegação por teclado, arrastar e soltar, rolagem suave, visibilidade de sobreposições, layout com zoom, sincronização de triagem e sincronização da tira de filme, reinventando muitos recursos nativos do GTK de uma forma pior e incompleta (o jeito típico do Darktable). O histórico anterior a 2022 está cheio de commits corrigindo o mouse-over após rolagem, recalculando linhas e deslocamentos, mantendo imagens selecionadas visíveis após alterações na coleção, evitando lentidão quadrática no Home/End, corrigindo o alinhamento com zoom, fazendo com que imagens ocultas ou recolhidas não quebrassem a lógica de imagem ativa, e limitando o número de sinais emitidos porque atualizações independentes demais estavam sendo encadeadas juntas. Em outras palavras, thumbtable.c já havia se tornado o lugar onde a lógica de visualização, o tempo de vida das miniaturas e a navegação da coleção eram forçados a se encontrar, muito antes de a reescrita de 2022 começar.
A refatoração da thumbtable fez surgir um novo arquivo, quando o antigo código da mesa de luz e da tira de filme começou a ser fatorado em um widget de miniatura comum e uma thumbtable como objeto base de tudo isso : src/dtgtk/thumbnail.c. Essa era a direção certa em princípio: em vez de desenhar miniaturas separadamente em vários lugares, um único widget poderia centralizar ativação, seleção, sobreposições, estrelas, marcas de rejeição, arrastar e soltar, bordas de grupo e comportamento com zoom. Mas ele seguiu um caminho semelhante muito rapidamente : o histórico anterior a 2022 mostra com que rapidez esse widget comum ficou sobrecarregado. Logo estava lidando com sobreposições estendidas, callbacks atualizados por MIPMAP, junções de triagem, barras de rolagem, tratamento de imagem ativa, callbacks da tira de filme, navegação de pré-visualização completa e posicionamento de ícones ajustado por CSS. Em outras palavras, thumbnail.c não apenas desenhava uma miniatura. Ele se tornou a linha de falha compartilhada entre mesa de luz, tira de filme, triagem e pré-visualização.
A tira de filme começou como uma faixa de navegação dedicada, e então ganhou centralização na imagem ativa, arrastar e soltar, interações com mapa, operações de copiar/colar, rolagem suave, correções de HiDPI, customização por CSS e, por fim, partes do novo sistema de callbacks de miniatura. De novo, nenhum desses recursos é irracional por si só. O problema é que a tira de filme acabou com comportamento customizado o suficiente para divergir da mesa de luz enquanto ainda tentava reutilizar parte da mesma maquinaria de miniaturas.
Além disso, havia o inconsistente paradigma de “seleção” no Darktable, vendido como um “fluxo de trabalho sem cliques”, de modo que muitas operações de escrita podiam ser feitas sem selecionar explicitamente a(s) imagem(ns) a afetar. Isso levou a muitos efeitos indesejados e acidentes, resultando em perda de dados (atribuir a avaliação de estrelas errada à imagem errada, o que a faria desaparecer da coleção atual se você a filtrasse por avaliação) e em tediosas sessões de desfazer. Mas toda a heurística de seleção de imagem também era frágil: mover o mouse na janela podia roubar o foco da foto que você travou explicitamente com um clique ou uma seleção por teclado, mas nem sempre.
Sem falar que esse “agir ao passar o mouse” disparava várias consultas SQL no banco de dados da biblioteca, ao passar o mouse sobre uma nova imagem, para obter metadados atualizados da imagem e atualizar o conteúdo dos módulos de metadados (metadados, metadados EXIF e IPTC, etiquetas). Tudo isso porque eles não podiam ser cacheados, e não podiam ser cacheados porque o Darktable é incapaz de rastrear o ciclo de vida de seus dados, então precisa atualizar tudo o tempo todo. E vai drenar sua bateria fazendo isso.
Então, quando a reescrita começou depois de ter bifurcado o Ansel em 2022, a questão não era que um commit ruim havia quebrado a mesa de luz. A questão era que três camadas de histórico estavam empilhadas umas sobre as outras:
lighttable.cainda carregando a bagagem de vários (inúteis) modos de navegação, codificados ali como reflexões posteriores,thumbtable.cconcentrando a lógica de rolagem, deslocamento, imagem ativa e navegação de coleção, que se sobrepunha em parte ao backend de coleção e seleção,thumbnail.cconcentrando cada vez mais comportamento de GUI compartilhado, além de código SQL.
É por isso que o trabalho posterior teve que ser arquitetural. Naquele ponto, não havia maneira realista de continuar corrigindo os sintomas um por um.
Alterações no front-end
A triagem nunca foi realmente (conceitualmente) uma visualização separada
Uma peça importante dessa subtração foi o modo de triagem . Em fevereiro de 2023, as visualizações dedicadas de triagem e pré-visualização foram removidas porque haviam se tornado mal entrelaçadas com atalhos, troca de visualizações e ramificações de caso especial. O código estava emaranhado e pesado, mas o problema real estava no próprio design: era uma solução superdimensionada para um problema muito mais simples, resolvido na camada errada.
A necessidade era isolar um conjunto arbitrário de fotos que não eram necessariamente contíguas na coleção atual, a fim de decidir qual delas seria a escolhida. Isso não precisava de um novo layout (ou dois…); precisava de um filtro para restringir a coleção a uma seleção arbitrária. Já tínhamos filtros para restringir coleções por avaliação, rótulo de cor, status editado/não editado, etc.
Essa diferença importa. Uma visualização de triagem dedicada duplica problemas que a mesa de luz já tem que resolver: qual conjunto de imagens está ativo, como a seleção é restaurada ao sair da visualização de triagem, como os atalhos são roteados, como o zoom e o estado da miniatura são inicializados, e o que acontece quando você volta para a grade. Cada melhoria então precisa ser implementada duas vezes, uma vez na lógica da mesa de luz e outra na lógica de triagem, e as duas versões acabam divergindo. Sem falar que o modo de triagem tinha um modo estático e um modo dinâmico, que diferiam tanto na forma como você interagia com eles quanto em sua implementação, e pouquíssimos usuários entendiam do que se tratavam.
No Ansel, a triagem voltou como um filtro de coleção, primeiro como uma forma simples de restringir o conjunto atual, e depois explicitamente como o botão de filtro Restringir à seleção. Isso significa que a intenção voltada ao usuário permaneceu a mesma (reduzir a coleção atual a uma seleção arbitrária de imagens), mas a implementação mudou completamente: em vez de entrar em uma visualização especial, você permanece na mesa de luz e diz à ferramenta de filtragem para mostrar apenas as imagens atualmente selecionadas ou apenas as imagens que correspondem ao critério de restrição atual.

Para os usuários, o benefício é prático. A triagem agora é combinável com o restante da lógica de filtragem, em vez de viver fora dela. Você pode combiná-la com avaliações, rótulos de cor ou busca de texto, porque ela é apenas mais um filtro de coleção. O tratamento de atalhos é mais simples porque há uma visualização especial a menos competindo pela posse dos atalhos. E correções na rolagem, seleção, tempo de vida da miniatura ou comportamento de sobreposição beneficiam automaticamente a triagem também, porque a triagem agora usa a mesma infraestrutura da mesa de luz em vez de uma paralela.
Essa também foi uma oportunidade de reformular a barra de ferramentas de filtragem, que misturava botões e lógica: uma lista combobox para avaliações (de rejeitado a 5 estrelas), associada a uma lista combobox de comparação (≠ = > < >= <=), mas botões de alternância para rótulos de cor. O Darktable 4.0 substituiu isso por um conjunto superdimensionado de widgets configuráveis usando paradigmas de GUI não padronizados que precisavam ser explicados em dicas de ferramenta. O Ansel achatou o design: tudo é um botão de alternância funcionando em modo “incluir” dentro de seu grupo, permitindo seleções complexas sem GUI complexa:
- você tem 3 grupos (avaliações, rótulos de cor, status de edição), além de restringir à seleção e busca de texto,
- entre esses grupos, os filtros são exclusivos, ou seja, são um
ANDlógico, - dentro desses grupos, os filtros são aditivos, ou seja, são um
ORlógico.
Por exemplo, na captura de tela acima, tudo está ativado, então a filtragem está na prática desabilitada (deixamos tudo entrar). No exemplo abaixo, filtramos para incluir todas as imagens que têm um rótulo de cor definido e que já foram editadas, independentemente de sua avaliação (todos os botões de avaliação estão ativados):

Seleção : misturar estados de backend e de GUI sempre te dá dor de cabeça
As imagens agrupadas eram outra linha de falha oculta. Parte da lógica antiga tentava considerar o agrupamento nas consultas SQL, ao gerar coleções de imagens a partir do banco de dados da biblioteca, mas bordas de grupo, estados de hover, comportamento de seleção e visibilidade real são conceitos de GUI. Essa incompatibilidade criava bugs sutis: todas as imagens de um grupo podiam ser selecionadas, ou algumas ignoradas de acordo com regras heurísticas que não correspondiam ao que o usuário via na tela.
Corrigi isso movendo as decisões de exibição de grupo de volta para o código de GUI. O SQL fornece a lista de imagens; a interface decide quais membros agrupados estão visíveis, recolhidos ou sob o mouse. Depois disso, os recursos adicionados em 2025 ficaram muito mais simples: as bordas de grupo puderam ser reimplementadas, tornadas opcionais, estendidas à tira de filme e tornadas mais visíveis ao passar o mouse. As dicas de ferramenta puderam ser preenchidas de forma preguiçosa apenas quando o usuário passa o mouse sobre elas. Uma consulta SQL por miniatura no momento da inicialização também pôde desaparecer, porque a interface já tinha estado local suficiente para saber quando a informação de grupo era de fato necessária.
Para os usuários, o benefício não é abstrato. As bordas de grupo agora significam o que mostram, as seleções correspondem exatamente ao estado de grupo visível, e as imagens agrupadas não parecem mais estar meio gerenciadas pelo banco de dados e meio pela interface.
Outro problema oculto era a seleção. No código antigo, a seleção estava emaranhada demais com os detalhes internos da coleção, o acesso SQL bruto e a lógica de contingência. Isso parece inofensivo até que uma imagem seja removida da coleção atual, um grupo seja recolhido, ou uma troca de visualização aconteça enquanto outra parte do código ainda assume o modelo de seleção anterior. No Darktable, as seleções de imagem eram tratadas como um backend SQL, já que são armazenadas na biblioteca para serem restauradas na próxima sessão. Isso não é um problema até que o backend de seleção seja gambiarrado para adivinhar quais imagens estão visíveis na GUI, e a GUI de fato trate a seleção através do backend. Essa é a pior divisão possível de funcionalidade entre as camadas de backend e de GUI.
É por isso que 2025 contém todo um agrupamento de commits relacionados à seleção: reescrever a API de seleção, remover a antiga noção de main_image, unificar os getters/setters de imagem ativa, corrigir a seleção por intervalo, restaurar o estado de seleção entre trocas de visualização e adicionar contingências por rowid quando scroll_to_selected já não consegue encontrar o id da imagem original. A reescrita mudou a arquitetura: a thumbtable agora computa seleções visíveis e significativas para o usuário e passa uma lista de ids de imagem para a camada de seleção, em vez de a GUI tentar reconstruir o significado a partir do estado da coleção depois do fato. A API SQL de seleção tornou-se um simples backend de “salvar no/restaurar do banco de dados”.
O benefício concreto para o usuário é que a seleção por intervalo com Shift + clique e a “imagem sobre a qual agir” são O Que Você Vê É O Que Você Obtém: você não pode selecionar uma imagem que não esteja visível na tela, e o backend não precisa adivinhar o que está visível. E rolar de volta até a imagem selecionada agora falha muito menos vezes em casos extremos. O problema oculto não era um atalho quebrado; era que a semântica de seleção vivia na camada errada.
A correção é separar claramente o que pertence ao backend e ao gerenciamento da biblioteca daquilo que pertence aos estados e interações da GUI, e prover uma interface rígida entre os dois. Isso faz com que as diferentes partes do código fiquem melhor delimitadas, imunes a mudanças nas outras partes e gerenciadas de forma centralizada.
Falhas de hover e foco vinham de vários sistemas válidos colidindo
O hover do mouse, o hover do teclado, o foco e o tratamento de cliques eram cada um implementados por razões legítimas, mas não compartilhavam um único ponto de arbitragem. O Darktable agora tem até um orgulhoso discurso de marketing escrito ao fundo da mesa de luz, quando a coleção está vazia, elogiando aquele fluxo de trabalho “sem cliques” que permite sobrescrever metadados de imagem sem querer ou até sem saber. Isso me parece uma montadora de carros anunciando um carro projetado para você usar o volante com os joelhos (e usar as mãos para beber cerveja).
É por isso que o comportamento antigo da mesa de luz podia parecer inconsistente sem nenhum bug óbvio único: ele estava quebrado por design. Clicar em um botão de sobreposição para avaliar a imagem também podia selecionar a miniatura inteira, mas avaliar pelo teclado não. O movimento pelo teclado e o movimento pelo mouse podiam ambos achar que possuíam a imagem “atual” e disparar todo tipo de alteração destrutiva de metadados apenas por passar o mouse sobre a foto e sem pedir confirmação. Filtrar imagens por avaliação de estrelas e inadvertidamente mudar a avaliação de uma imagem podia fazê-la desaparecer inesperadamente. Widgets ocultos ainda podiam receber lógica relacionada a hover. O foco do GTK podia decidir uma coisa enquanto a camada de visualização esperava outra.
As correções aqui foram menos sobre adicionar novos recursos e mais sobre escolher um único dono para o estado. O despacho de over-state foi centralizado. A navegação por teclado ganhou um ponto de partida sensato e um caminho de cancelamento explícito com Shift+Ctrl+A. A exibição do foco foi tornada explícita. Widgets ocultos deixaram de participar da lógica de hover que não podiam exibir. Clicar em botões de sobreposição deixou de vazar para a seleção da miniatura. Nenhuma (sobre)escrita de metadados é mais feita sem um clique explícito em um botão; os eventos de hover são todos somente leitura.
O Ansel tem duas regras simples :
- qualquer ação que vá (sobre)escrever (meta)dados é feita apenas sobre imagens explicitamente selecionadas,
- a seleção explícita é feita apenas interagindo com algo “concreto”: um clique do mouse ou um pressionamento de tecla.
Então, os eventos de hover são reservados a eventos somente leitura.
O benefício para os usuários é que a mesa de luz reage mais como um único modelo de interação. O problema oculto não era que o hover estivesse quebrado. Era que vários tipos de hover estavam simultaneamente “certos” e, portanto, coletivamente errados.
Alterações no backend
Reparentando a thumbtable entre a mesa de luz e a tira de filme
Um dos problemas mais profundamente ocultos era a posse de widgets. No Darktable, a mesma grade de thumbtable era reparentada entre os contextos da mesa de luz e da tira de filme. No papel, isso evitava duplicação de código e proliferação de widgets, e soa esperto. Na prática, fazia com que o estado de rolagem, a inicialização de miniaturas, a coleta de lixo e o tempo de vida dos eventos dependessem de onde o widget havia vivido mais recentemente. Sem falar que reparentar era lento, então ir e voltar entre a mesa de luz e a sala escura era atrasado em cerca de 1 s, o tempo necessário para o GTK recomputar os novos tamanhos de miniatura, possivelmente pedir novas miniaturas ao cache e redesenhar os widgets.
Isso se manifestava de formas muito concretas. A posição de rolagem podia saltar ou ficar inconsistente. As miniaturas podiam ser ocultadas, mostradas ou destruídas no momento errado. A coleta de lixo tornou-se mais difícil de raciocinar porque a hierarquia de widgets não era estável. É claro que tudo isso foi resolvido commit após commit, mas deixando o código em um estado de complexidade impossível de manter.
A correção real foi parar de tentar ser esperto. A mesa de luz e a tira de filme agora têm thumbtables separadas. Isso significa um pouco mais de estrutura explícita no código, mas muito menos estados acidentais. O benefício para os usuários é visível numa rolagem mais estável, menos falhas de layout nas trocas de visualização e uma tira de filme que se comporta como uma irmã de verdade da mesa de luz, em vez de como um fragmento reutilizado.
O zoom só se tornou usável depois que os tamanhos das miniaturas foram tornados consistentes
A primeira tentativa de mesa de luz com zoom falhou por uma razão estrutural: a geometria da miniatura, os tamanhos de mipmap em cache e a navegação em nível de visualização não compartilhavam o mesmo modelo. É por isso que o recurso teve que ser removido primeiro. Se eu tivesse continuado a poli-lo no lugar, o resultado teria sido apenas um recurso quebrado com aparência melhor.
O trabalho posterior corrigiu a cadeia de dependências na ordem oposta. O backend de visualização ganhou o andaime para o zoom da mesa de luz, então o cache de miniaturas foi ensinado a raciocinar de forma mais coerente sobre os tamanhos de miniatura, uma API de miniatura em resolução completa foi preparada, e então clamping, panning e auto-pan baseado em baricentro foram adicionados por cima disso. Só depois que as camadas inferiores concordaram sobre o tamanho da imagem e a estratégia de recuperação é que o zoom de 200% se tornou um recurso defensável.
O benefício para os usuários é que o zoom agora está atrelado ao cache e à navegação em vez de brigar com eles. Arrastar para dar pan, Shift + arrastar por miniaturas visíveis e o pan automático em direção ao baricentro dos detalhes funcionam todos porque o pipeline subjacente sabe o que as miniaturas com zoom devem ser.
O significado confuso de “zoom” na mesa de luz foi atualizado: o termo “zoom” era usado tanto para o zoom externo (número de imagens por linha, o que afeta indiretamente seu tamanho visível) quanto para o zoom interno (ampliação dentro do quadro da imagem). Então agora temos colunas para o zoom externo e zoom para a ampliação.

Não ligue para a cruz no lugar do símbolo - no spinbutton de colunas, é um bug do GTK com o tema Breeze do KDE/Plasma .
Isso tornou possível implementar um recurso que estava em minha mente há muito tempo: o auto-panning baricêntrico ao ampliar a miniatura.


Você pode ver que, ao habilitar o zoom de 100%, as imagens são automaticamente alinhadas em seu conteúdo apesar de terem enquadramentos e proporções completamente diferentes. Isso usa análise de recursos por decomposição de wavelets, a partir da qual computamos as coordenadas do baricentro dos detalhes. Não é perfeito porque não pega a mesma parte do rosto em todas as imagens, mas nos dá o rosto em todas as instâncias. É um projeto que deixei em banho-maria por muito tempo, mas não era possível com o design anterior da mesa de luz.
Nem preciso dizer que arrastar para dentro de miniaturas ampliadas foi reinstaurado, e arrastar para dentro de todas as miniaturas ampliadas também foi adicionado (Shift+Arrastar). Esse recurso foi pedido por muito tempo no Darktable, mas claramente não era possível no design de merda deles.
Lentidão de metadados autoinfligida
A antiga mesa de luz ficava fazendo ao banco de dados as mesmas perguntas uma miniatura por vez. Em uma coleção pequena isso é fácil de passar despercebido. Em uma maior, aparece como microengasgos ao abrir a visualização, mostrar sobreposições, passar o mouse sobre grupos ou atualizar dados sensíveis ao histórico. O problema oculto não era o SQL bruto ser lento em termos absolutos; era a repetição e o momento dessas consultas.
A correção veio em etapas. Metadados somente leitura começaram a ser cacheados a partir das estruturas de imagem. Então, em fevereiro de 2026, os metadados de toda uma coleção puderam ser buscados com uma única consulta SQL em vez de uma consulta por imagem. A thumbtable podia semear o cache de imagens a partir da coleção que estava prestes a exibir. O caching de informações de miniatura foi refatorado em um auxiliar e depois fundido de volta em dt_image_t, para que os metadados não tivessem que ficar quicando entre estruturas paralelas. Até pequenas alterações, como remover os pings de hash de histórico no redesenho, importam aqui, porque cortam idas e vindas invisíveis dos caminhos quentes.
Muitas consultas SQL por miniatura inúteis foram removidas, junto com o código SQL no código de GUI da mesa de luz, de modo que as camadas funcionais agora estão devidamente separadas.
O benefício para o usuário é exato: abrir coleções grandes trava menos, exibições pesadas de sobreposições hesitam menos, e atualizações de histórico ou metadados se propagam com menos pausas visíveis.
O cache de mipmap também precisava ser consertado
O cache de mipmap é a camada que carrega imagens RAW e miniaturas pré-geradas na RAM, e as esvazia quando a memória fica escassa. Essa era uma das fontes ocultas mais profundas de bugs visíveis. Quando tudo se alinhava, funcionava bem o suficiente. Quando o arquivo de origem era menor do que o esperado, quando uma pré-visualização JPEG embutida era esquisita, quando o hash de histórico era indefinido, ou quando a invalidação do cache em disco ficava atrasada em relação às edições, o usuário não experimentaria “um bug de cache”. Ele veria miniaturas desatualizadas, pré-visualizações quebradas ou imagens que se recusavam a atualizar.
É por isso que boa parte do trabalho de 2025 no cache parece cirúrgica. A lógica de alocação de buffer foi reescrita porque a posse estava emaranhada demais. O tratamento de entradas subdimensionadas foi melhorado porque as suposições sobre o tamanho de entrada vazavam longe demais. As pré-visualizações embutidas deixaram de ser descartadas de forma tão agressiva, porque pré-visualizações pequenas demais ainda eram úteis para a consistência entre níveis de zoom. Pré-visualizações JPEG sidecar começaram a ser usadas quando presentes, em vez das miniaturas embutidas do RAW. A invalidação de cache foi reforçada, e as miniaturas em cache foram realmente removidas do disco quando deveriam ser. Mipmaps regenerados começaram a escrever seu hash de volta no banco de dados em sincronia com o estado do histórico, e o caso do hash de histórico indefinido parou de deixar mipmaps sem salvar.
O bug mais sorrateiro que encontrei estava enterrado na complexidade sem sentido do entrelaçamento entre thumbtable, visualização e cache de mipmap. Embora a renderização de miniaturas fosse delegada a uma thread separada por desempenho (como deveria), e mais de uma thread pudesse ser iniciada para (supostamente) processar várias miniaturas de uma vez (possivelmente usando várias GPUs), todas as threads de processamento e a GUI estavam na verdade competindo para travar os caches de mipmap e de imagem, o que só fazia a GUI travar enquanto os pipelines rodavam. Foi só porque eu trabalhei sob a suposição de que reescrevi as coisas conforme o manual, e eu sabia que o jeito conforme o manual deveria ser mais rápido que aquilo, que continuei escavando até descobrir por que ainda não estava tão rápido quanto o esperado, simplificando tudo camada por camada no processo.
O benefício para os usuários é que o cache é mais confiável. Após edições, a mesa de luz tem menos probabilidade de mostrar uma miniatura obsoleta. Em arquivos problemáticos, a geração de pré-visualização falha com menos frequência. Em visitas repetidas à mesma coleção, o cache em disco se comporta mais como um cache e menos como um arquivo de capturas de tela obsoletas. Mas tudo isso, sem ter que atualizar/recomputar/redesenhar tudo o tempo todo só para ter certeza.
Além disso, as opções de usar JPEGs embutidos ou forçar um recomputo foram movidas das preferências para o menu global e podem ser alteradas durante a execução:

Este é outro exemplo em que limpar e simplificar o backend pavimentou o caminho para a extensão do frontend e novos recursos que simplesmente fazem sentido.
Segurança de threads e condições de corrida
Uma razão clássica para esses bugs durarem tanto tempo é que eles exigiam que o usuário fosse mais rápido que o código: rolar rapidamente, sair de uma visualização enquanto as miniaturas ainda estão sendo construídas, redimensionar enquanto uma superfície em segundo plano ainda está sendo produzida, ou fechar um widget logo antes de uma thread de trabalho enviar uma atualização.
É por isso que o trabalho de segurança de threads de 2025-2026 importa. A busca de miniaturas foi movida mais fundo para dentro de trabalhos em segundo plano, e depois explicitamente adiada para que a renderização da sala escura pudesse manter a prioridade. A destruição de miniaturas foi movida para pontos de limpeza mais seguros. Os trabalhos em segundo plano que produzem miniaturas aprenderam a se cancelar quando o widget que serviam desaparecia. As superfícies de imagem foram protegidas por mutexes. Ponteiros liberados foram anulados. Os buffers de imagem migraram para caminhos de alocação mais bem gerenciados. O objetivo de tudo isso não era “mais threads”; era impedir que threads antigas escrevessem em estado morto ou competissem por travas.
O resultado visível para o usuário é menos segfaults, menos falhas aleatórias durante rolagem rápida e menos casos em que a interface parece correr contra si mesma sob carga.
Vários bloqueios de threads, adicionados ao longo dos anos para remendar problemas, também foram removidos. Muitos eram redundantes (mas isso não ficava evidente sob a complexidade insana de todo o conjunto), alguns prejudicavam ativamente o desempenho, e todos escondiam um projeto ruim. Agora temos menos pontos de bloqueio, mas o resultado é mais legível e mais robusto.
Como resultado, os widgets de miniatura agora são objetos totalmente autocontidos. Eles gerenciam internamente suas próprias threads do pipeline de renderização de miniaturas, que interagem diretamente com sua própria superfície de imagem em cache, de modo que podem criá-las ou encerrá-las por conta própria. Essa imagem em cache é invalidada apenas quando o histórico da imagem muda, o que é tornado explícito pelo backend do histórico de revelação. Como o widget de miniatura conhece seu próprio estado (visível ou não, precisando de atualização de imagem ou não, tamanho, modo de realce de foco, etc.), ele se torna muito mais robusto do que tentar lidar com todas essas coisas a partir de camadas de nível mais alto que não conseguem se comunicar entre si.
Isso não era possível com o projeto do Darktable, porque ele ficava adicionando/removendo widgets de miniatura dinamicamente à visualização atual da mesa de luz, dependendo da posição da linha flutuante, o que era uma tentativa de lidar com as lentidões causadas por todas as threads que competiam pelo acesso ao cache. Mas, além disso, os widgets de miniatura do Darktable tentavam adquirir imediatamente uma imagem do cache de mipmap, quando eram criados, o que fazia o uso de CPU e de I/O de memória disparar e, na prática, congelava a interface. Mas “consertar” isso reduzindo a expectativa de vida dos widgets de miniatura tornava impossível deixá-los gerenciar seu próprio estado internamente, então isso tinha de ser feito pelas camadas de nível alto, que tinham de se comunicar entre si para atualizar estados, o que de fato acontecia em alguns lugares, mas ao custo de uma complexidade insuportável.
Em vez disso, as miniaturas do Ansel são todas inicializadas de uma vez, mas adquirem preguiçosamente uma imagem do cache de mipmap apenas quando se tornam visíveis, e então a mantêm em cache internamente. Isso mantém a rolagem da tabela de miniaturas responsiva, mesmo com uma coleção de 500 imagens que estão todas gerando sua imagem.
Pré-visualizações de importação melhoradas como efeito colateral
A janela de importação foi afetada por esse trabalho pela mesma razão que a mesa de luz foi: ela precisa de extração rápida de pré-visualização, inspeção de metadados e comportamento de fallback sensato em arquivos com suporte parcial. Uma vez que os caminhos de mipmap e de miniatura foram reescritos, o carregamento de pré-visualização de arquivos raw na importação ficou mais rápido, e o suporte a TIFF/DNG também melhorou.
Este é um bom exemplo de por que a reescrita tinha de acontecer bem embaixo na pilha. Se a maquinaria de pré-visualização subjacente é ineficiente ou frágil, tanto a mesa de luz quanto a importação herdam a mesma dor. Uma vez que essa maquinaria foi reescrita, ambas se beneficiaram.
Mudanças arquiteturais globais
No meu artigo anterior, mostrei como o cache do pipeline de trabalho torna o ir e vir entre a mesa de luz e a sala escura quase instantâneo, porque não é preciso recalcular a imagem inteira. O trabalho feito aqui na GUI resolve o mesmo problema de atrasos na troca de visualizações, mas no nível da GUI. Então não há atraso ao alternar entre as duas visualizações.
Isso é importante porque o atraso na troca de visualizações tem sido usado como desculpa para duplicar recursos (módulos/caixas de ferramentas) entre a mesa de luz e a sala escura, o que só aumenta a desordem da GUI. Então toda essa melhoria de backend torna possível melhorar o projeto da GUI especializando cada visualização em uma única tarefa:
- tratamento de metadados para a mesa de luz (além, obviamente, da seleção),
- edição de imagens para a sala escura.
Como resultado, as caixas de ferramentas de metadados e de etiquetas foram removidas da sala escura.
Quantas linhas de código a reescrita economizou?
Contando apenas linhas que não são comentários nem estão em branco com o cloc, e comparando a última árvore antes de 1º de janeiro de 2022 com a árvore atual para os principais arquivos discutidos aqui, a reescrita economizou 4.714 linhas de código no total.
O escopo dessa contagem é:
- todo o diretório
data/themes/antes de 2022, comparado aodata/themes/ansel.cssde hoje, 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,- além dos arquivos deletados de vez pela reformulação ou pela limpeza da GUI de coleção:
src/dtgtk/culling.[ch],src/libs/tools/view_toolbox.c,src/libs/collect.hesrc/libs/recentcollect.c.
Dentro desse escopo, o total passou de 15.257 linhas de código antes de 2022 para 10.543 hoje. A reescrita, portanto, economizou 4.714 linhas de código no total. As maiores economias vieram de deletar de vez a visualização de seleção (src/dtgtk/culling.c) (-1.406 linhas), encolher a mesa de luz (src/views/lighttable.c) (-976), remover 7 folhas de estilo de temas antigos e colapsá-las em uma só (-864 em data/themes/), encolher src/dtgtk/thumbnail.c (-460), src/dtgtk/thumbtable.c (-458), deletar src/libs/recentcollect.c (-358) e encolher src/views/view.c (-314). Alguns arquivos de fato cresceram, especialmente src/libs/tools/filter.c (+215) e src/libs/collect.c (+157), porque parte do objetivo era mover comportamentos de caso especial, como a seleção e ramificações antigas da GUI de coleção, de volta para uma infraestrutura compartilhada mais simples, em vez de mantê-los em visualizações paralelas e módulos laterais.
A contagem de linhas não é, por si só, uma métrica de qualidade. Muitas reescritas apenas movem código de um lugar para outro. Mas aqui o número corresponde à mudança de projeto: menos comportamento duplicado, menos visualizações paralelas, menos gambiarras compensatórias e menos lugares onde a GUI, a camada de visualização e o cache tinham todos de resolver o mesmo problema duas vezes.
Tudo o que consertei, consertei simplificando a lógica e o código. Nenhuma solução de contorno foi permitida.
Benchmarks
Todos os tempos de execução foram medidos em um laptop Lenovo ThinkPad P51 (CPU Intel Xeon E3-1505M v6 @ 3,00 GHz, GPU Nvidia Quadro M2200 com 4 GB de VRAM, 32 GB de RAM, tela 4K), CPU em modo de desempenho, Linux Fedora 41 com desktop KDE/Plasma. Os tempos de execução do pipeline de pixels não são comparados (fora do escopo; veja o artigo anterior). O Ansel Master é tomado no commit 09749f1d (21 de fev. de 2026).
| Descrição | Ansel Master | Darktable 5.0 |
|---|---|---|
| Tempo da inicialização do aplicativo até o desenho da última miniatura da mesa de luz (mesma coleção) | 2,12 s | 7,49 s |
| Tempo para alternar da mesa de luz para a sala escura (mesma imagem) | 0,2 s | 1,2 s |
| Tempo para rolar (início->fim) por toda a mesma coleção de 471 imagens* | 0,7 s | 5,0 s |
*: miniaturas pré-carregadas no cache em disco em ambos os casos, 5 colunas de miniaturas por linha, resolução 4K, sem barra lateral direita.
Como um “conserto”, o Darktable 5.x nos abençoou com uma tela de abertura deslumbrante, que é mais uma confissão do que qualquer outra coisa.
As medições a seguir foram feitas na bateria, em modo de economia de energia, com o aplicativo ocioso (sem interação do usuário) por 5 minutos, usando o Intel Powertop. O consumo de referência de todo o SO ocioso é de 1,6 % de CPU. (A potência é dada apenas para o aplicativo, o % de CPU é dado para todo o sistema):
| Visualização | Ansel Master | Darktable 5.0 |
|---|---|---|
| Mesa de luz | 1,8 % de CPU, potência: 0,85 mW | 2,7 % de CPU, potência: 103 mW |
| Sala escura | 1,8 % de CPU, potência: 7,65 mW | 1,8 % de CPU, potência: 22 mW |
Esses números representam o consumo de energia de referência apenas da GUI (GTK, workers de segundo plano, timers agendados, etc.). O Darktable está desperdiçando desempenho através da GUI, e o trabalho tedioso feito em 2023-2024 na otimização de módulos de processamento de pixels para ganhar 15-50 ms a mais é completamente irrelevante.
Conclusão
A esta altura, estou firmemente convencido de que o “projeto” Darktable só atrai “desenvolvedores” que seriam incapazes de fazer uma distinção cognitiva entre GUI e backend mesmo que suas vidas dependessem disso. Então problemas de GUI são resolvidos no backend, problemas de backend são resolvidos na GUI, e isso faz a complexidade do código crescer descontroladamente com o tempo, o que mais tarde justifica adicionar novos recursos hackeando a menor quantidade de código possível numa base de código que ninguém mais entende. Sem mencionar que nada disso foi documentado, então tive de fazer engenharia reversa disso dolorosamente ao longo de vários anos, simplificando recursivamente um pouco aqui e um pouco ali, até que finalmente convergiu para uma lógica geral limpa.
Nem preciso dizer que todas as soluções de contorno e “consertos rápidos” que foram adicionados foram removidos. Todos eles datavam de depois de 2020, o que demonstra uma tendência preocupante de degradação da qualidade do código.
Este projeto de limpeza roubou 4 anos da minha vida, não me deu prazer algum, e as pessoas que introduziram todas as regressões que consertei dolorosamente precisam ser responsabilizadas pelas consequências de seus atos. Há uma diferença enorme entre não ter tempo suficiente para fazer as coisas direito e consumir uma porção de horas-homem para piorar as coisas. E depois, recusar-se a admitir que se piorou as coisas, recusar-se a aceitar que há um problema, e alimentar o próprio viés de confirmação ouvindo apenas feedbacks de gente contente é a prova definitiva de estupidez.
O Darktable degenerou em merda, e acabei de explicar, tecnicamente, por quê. A equipe do Darktable e seu fórum-satélite de tech bros gostariam de fazer as pessoas acreditarem que fiquei bravo com eles porque não aceitariam minhas mudanças, e que é tudo um problema interpessoal. Para o leigo que não entende o que escrevi aqui e nos artigos anteriores, é mais fácil acreditar em raiva interpessoal do que entender como uma sucessão de decisões técnicas ruins ao longo de vários anos me tornou a vítima dos problemas que eles criaram, porque eu era o único cara em tempo integral aqui, dependendo disso para viver. Fiquei bravo com eles porque estavam cagando de novo e de novo na minha porta, e eu tinha de limpar repetidamente. Essa é uma forma de violência realmente difícil de enxergar e de reconhecer, porque não se manifesta materialmente: é uma maneira de tornar sua vida mais difícil, diariamente, passo a passo, só porque um bando de amadores de meia-idade sem habilidade nenhuma queria fazer parte de algo legal sem perceber o efeito nocivo de suas contribuições sobre o projeto como um todo.
E coube a mim limpar a bagunça porque eu era, aparentemente, o único a me importar com as regressões, com os bugs e travamentos aleatórios estranhos, com as piores “inovações” de GUI que afastaram até minha própria esposa de usar o Darktable porque é simplesmente esmagador, e com todas as novas lentidões que continuam se acumulando ao longo dos anos. Como Chris Elston me disse no chat IRC do Darktable, em 2022, antes de eu sair de lá para sempre, a respeito de coisas que eu havia consertado em 2019 e que eles quebraram de novo em 2022: “cala a boca e conserta”. Se isso não é violência, não sei o que é. E agora eles estão tentando espalhar a ideia de que eu é que era o tóxico. A equipe inteira é tóxica. Sua cultura de trabalho descuidada é tóxica. Sua maneira de se empolgar com tudo, contanto que seja novo, sem ponderar o custo de manutenção, a duplicação de recursos e a sobrecarga geral do usuário é tóxica. Sua falta de preocupação com o futuro do projeto e com as consequências das escolhas que fazem é tóxica.
E o que é especialmente tóxico é que, no discuss.pixls.us, todo post que exprime elogios a mim ou ao Ansel é sinalizado e ocultado. Não há liberdade de expressão num fórum de software livre. Eles simplesmente transformaram o comunismo em estalinismo.
Translated from English by : Claude. In case of conflict, inconsistency or error, the English version shall prevail.