O que acontece quando um bando de fotógrafos amadores, transformados em desenvolvedores amadores, acompanhados por uma turma de desenvolvedores de back-end que criam bibliotecas para desenvolvedores, decide trabalhar sem método nem estrutura em um software profissional destinado a usuários finais, cuja competência central (colorimetria e psicofísica) situa-se em algum ponto entre uma graduação em fotografia e um mestrado em ciências aplicadas, ao mesmo tempo que promete entregar 2 versões por ano sem gestão de projeto? Tudo isso, é claro, em um projeto de onde os fundadores e a primeira geração de desenvolvedores seguiram em frente e fugiram?

Adivinhe!

Degradando recursos básicos

Os anos 2020 chegam 40 anos atrasados para reinventar os paradigmas de interação entre usuário e computador, seja o modo como usamos o teclado e o mouse para operar a interface, seja o comportamento de um navegador de arquivos. Desde os anos 1980, todos os equipamentos informáticos de uso geral convergiram para uma semântica mais ou menos unificada, na qual a tecla escape fecha o aplicativo atual, o duplo clique abre arquivos e a roda do mouse rola a visualização atual. O Darktable1 tem um prazer deslocado em ignorar tudo isso, e as mudanças recentes pioram as coisas: agora é obrigatório ler a documentação para realizar tarefas tão simples quanto ordenar arquivos ou atribuir atalhos de teclado a ações da interface gráfica.

Grupos de módulos

Tudo começa com a reformulação dos grupos de módulos , em 2020, que esconde a decisão de não decidir  sobre uma ordem unificada dos módulos.

image

Desde 2018 , luto para limpar a interface gráfica do Darktable, e em particular a organização dos módulos. Uma interface gráfica deveria promover as boas práticas dispondo as ferramentas na ordem típica em que devem ser usadas. As más práticas são aquelas que aumentam o risco de inconsistências colorimétricas ou de edição circular, na qual é preciso voltar atrás para repercutir mudanças feitas depois, mesmo que as más práticas possam funcionar em casos simples. No contexto do processamento de imagem, tarefa altamente técnica em que muita coisa fica oculta dos usuários finais por baixo da interface, as boas práticas também permitem que pessoas pouco qualificadas usem o software de uma forma que reduz a probabilidade de erros.

Essa ordem de uso é ditada principalmente por considerações técnicas, como a ordem de aplicação dos módulos na sequência do pipeline e o uso de máscaras desenhadas e paramétricas, cujo efeito depende dos módulos a montante. Ignorar essas considerações equivale a procurar encrenca, ainda que a moda em voga nos anos 2010 e 2020 seja acreditar que as tecnologias digitais funcionam desligadas de qualquer realidade material, para a única felicidade do usuário.

Por exemplo, a restrição imposta ao projeto do pipeline para permitir uma ordem arbitrária de uso dos módulos cria problemas matematicamente insolúveis  no que diz respeito ao cálculo das coordenadas dos nós das máscaras, e nenhuma solução programada é possível (a não ser relaxar essa restrição), porque a matemática disse não.

Só que uma parte significativa dos usuários-programadores que gravitam em torno do projeto no Github e na lista de discussão de desenvolvimento continua convencida de que não existe fluxo de trabalho bom ou ruim, apenas preferências pessoais, o que provavelmente é verdade quando se pratica uma arte sem restrição de tempo, de orçamento ou de resultado. Sendo assim, os módulos deveriam poder ser reordenados à vontade, seja no pipeline, seja no fluxo de trabalho. A confusão vem do fato de que a edição não destrutiva é erroneamente vista como assíncrona (o que quase seria o caso se não usássemos máscaras nem modos de mesclagem), enquanto o pipeline de pixels é sequencial e mais próximo de uma lógica de camadas, como a encontramos no Adobe Photoshop, Gimp, Krita, etc.

Desacoplar a ordem dos módulos na interface da ordem do pipeline equivale a permitir todo tipo de uso, mesmo patológico, e obriga a escrever páginas e páginas  de documentação para alertar, explicar o que fazer, como e por quê; documentação que ninguém vai ler, para acabar perguntando em loop as mesmas coisas toda semana em todos os fóruns.

Nessa história, todo mundo perde tempo graças a um design de interface que tenta ser tão flexível que não pode ser tornado seguro e robusto por padrão. No corpo humano, cada articulação tem alguns graus de liberdade ao longo de certos eixos; se cada articulação pudesse girar 340 ° em torno de cada eixo do espaço 3D, a estrutura seria instável por ser flexível demais, e incapaz de trabalhar com cargas elevadas. A metáfora se aplica ao software profissional. Nadamos no culto à carga  do FLOSS, onde as pessoas adoram ter a ilusão de escolha, isto é, receber muitas opções das quais a maioria é inutilizável ou perigosa, em detrimento da simplicidade (KISS ), e onde a maioria dos usuários não entende as implicações de cada opção (e não tem o menor desejo de entender).

Na ausência de um consenso sobre a ordenação dos módulos na interface, uma ferramenta complicada, frágil e pesada foi introduzida no fim de 2020 para permitir que cada usuário configure a disposição dos módulos em abas. Ela oferece muitas opções inúteis e armazena a disposição atual no banco de dados, usando o nome traduzido dos módulos, o que significa que mudar o idioma da interface faz você perder suas predefinições. Inteiramente configurável, ela deixa o usuário decidir como se prejudicar, sem nenhum guia de boas práticas. Essa porcaria é codificada em 4000 linhas misturando alegremente requisições SQL no meio do código GTK da interface , e as predefinições são criadas por meio de macros de compilador redundantes , enquanto os módulos sempre tiveram uma flag binária , que permite definir seu grupo padrão… modularmente.

Mais importante, ela substitui um recurso simples e eficiente, disponível até o Darktable 3.2:

image

Um clique sobre o nome do módulo o ativa, um segundo o adiciona à coluna de favoritos, um terceiro o oculta da interface. Tudo isso permitindo predefinições e armazenando a disposição atual em texto simples no arquivo darktablerc. Simples e robusto, codificado em 688 linhas  de código legível e bem estruturado, o recurso portanto não era divertido o suficiente para o desenvolvedor diletante de meia-idade, e era urgente substituí-lo por um sistema labiríntico.

Atalhos de teclado

Em 2021 foi adicionado o que chamo de grande turducken  MIDI. O objetivo é estender a interface dos atalhos de teclado (já estendida em 2019 para dar suporte a “atalhos dinâmicos”, permitindo combinar ações de mouse e teclado), para dar suporte a dispositivos MIDI e… controles de videogame.

No fim de 2022, ou seja, um ano e meio depois desse recurso, na pesquisa que conduzi , menos de 10 % dos usuários possuem um dispositivo MIDI, e apenas 2 % o usam com o Darktable. Compare com os 45 % de usuários que possuem uma mesa digitalizadora (tipo Wacom), cujo suporte no Darktable ainda é tão deficiente que apenas 6 % a usam. Além da má gestão de prioridades, o que não tolero aqui são os efeitos colaterais introduzidos por essa mudança e o custo global que ela teve, a começar pelo fato de que ela não importa os atalhos definidos pelo usuário de versões anteriores à 3.2, e torna a configuração de novos atalhos terrivelmente complicada.

Antes do grande turducken, apenas uma lista limitada de ações da interface podia ser mapeada para atalhos de teclado ou mistos (teclado + mouse). Essa lista era curada manualmente pelos desenvolvedores. O grande turducken MIDI permite mapear todas as ações da interface para atalhos, apresentando aos usuários uma lista de vários milhares de entradas configuráveis, na qual é difícil encontrar as únicas 3 de que você realmente precisa, e o mecanismo de busca textual é básico demais para ajudar:

image

Repare no uso de “efeitos”, sobre os quais a documentação  não ajuda em nada. É apenas por dedução (porque o código também não está comentado ) que acabei entendendo que são emulações de interações típicas de desktop (mouse e teclado) destinadas a ser usadas com dispositivos MIDI e controles de gamepad (mas alguém ainda precisa me explicar o que Ctrl-Toggle, Right-activate ou Right-Toggle significam em termos de interação típica de desktop).

O que é inaceitável é que o uso do teclado numérico está quebrado por design , notadamente para atribuir avaliações numeradas (estrelas) às miniaturas na mesa de luz. De fato, as teclas modificadoras (numlock e capslock) não são decodificadas corretamente pela coisa, e os números são tratados de forma diferente conforme sejam digitados no teclado “de texto” típico ou no teclado numérico. Assim, o 1 do teclado numérico é decodificado como Keypad End, não importa o estado do numlock. Foi assim que tive de configurar os atalhos numéricos em um teclado francês BÉPO e duplicar a configuração para o teclado numérico:

image

Basta lembrar que Shift+" e Kp End ambos significam 1 e lembrar de duplicar todos os atalhos para o teclado numérico e o resto do teclado. Em suma, quebramos uma expectativa básica do usuário e mandamos as críticas de design às favas. A regressão é mencionada em todos os fóruns do Darktable, mas parece não incomodar ninguém.

A correção deste bug deste recurso foi feita no Ansel  e as teclas do teclado numérico são remapeadas para teclas padrão diretamente no código , num total de 100 linhas de código incluindo comentários. Fazer essa correção foi realmente muito difícil: li a documentação do Gtk  e segui o exemplo deles linha por linha. 2 anos esperando por isso…

A cereja do bolo é que, mais uma vez, substituímos 1306 linhas de código claro e estruturado  por uma monstruosidade de quase 4400 linhas , com pérolas como:

  1. o laço while da morte (fonte ):
 1  gboolean applicable;
 2  while((applicable =
 3           (c->key_device == s->key_device && c->key == s->key && c->press >= (s->press & ~DT_SHORTCUT_LONG) &&
 4           ((!c->move_device && !c->move) ||
 5             (c->move_device == s->move_device && c->move == s->move)) &&
 6           (!s->action || s->action->type != DT_ACTION_TYPE_FALLBACK ||
 7            s->action->target == c->action->target))) &&
 8        !g_sequence_iter_is_begin(*current) &&
 9        (((c->button || c->click) && (c->button != s->button || c->click != s->click)) ||
10         (c->mods       && c->mods != s->mods ) ||
11         (c->direction  & ~s->direction       ) ||
12         (c->element    && s->element         ) ||
13         (c->effect > 0 && s->effect > 0      ) ||
14         (c->instance   && s->instance        ) ||
15         (c->element    && s->effect > 0 && def &&
16          def->elements[c->element].effects != def->elements[s->element].effects ) ))
17  {
18    *current = g_sequence_iter_prev(*current);
19    c = g_sequence_get(*current);
20  }
  1. O switch case contendo if aninhados em 2 níveis (fonte ):
 1  switch(owner->type)
 2  {
 3  case DT_ACTION_TYPE_IOP:
 4    vws = DT_VIEW_DARKROOM;
 5    break;
 6  case DT_ACTION_TYPE_VIEW:
 7    {
 8      dt_view_t *view = (dt_view_t *)owner;
 9
10      vws = view->view(view);
11    }
12    break;
13  case DT_ACTION_TYPE_LIB:
14    {
15      dt_lib_module_t *lib = (dt_lib_module_t *)owner;
16
17      const gchar **views = lib->views(lib);
18      while(*views)
19      {
20        if     (strcmp(*views, "lighttable") == 0)
21          vws |= DT_VIEW_LIGHTTABLE;
22        else if(strcmp(*views, "darkroom") == 0)
23          vws |= DT_VIEW_DARKROOM;
24        else if(strcmp(*views, "print") == 0)
25          vws |= DT_VIEW_PRINT;
26        else if(strcmp(*views, "slideshow") == 0)
27          vws |= DT_VIEW_SLIDESHOW;
28        else if(strcmp(*views, "map") == 0)
29          vws |= DT_VIEW_MAP;
30        else if(strcmp(*views, "tethering") == 0)
31          vws |= DT_VIEW_TETHERING;
32        else if(strcmp(*views, "*") == 0)
33          vws |= DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
34                 DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
35        views++;
36      }
37    }
38    break;
39  case DT_ACTION_TYPE_BLEND:
40    vws = DT_VIEW_DARKROOM;
41    break;
42  case DT_ACTION_TYPE_CATEGORY:
43    if(owner == &darktable.control->actions_fallbacks)
44      vws = 0;
45    else if(owner == &darktable.control->actions_lua)
46      vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
47            DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
48    else if(owner == &darktable.control->actions_thumb)
49    {
50      vws = DT_VIEW_DARKROOM | DT_VIEW_MAP | DT_VIEW_TETHERING | DT_VIEW_PRINT;
51      if(!strcmp(action->id,"rating") || !strcmp(action->id,"color label"))
52        vws |= DT_VIEW_LIGHTTABLE; // lighttable has copy/paste history shortcuts in separate lib
53    }
54    else
55      fprintf(stderr, "[find_views] views for category '%s' unknown\n", owner->id);
56    break;
57  case DT_ACTION_TYPE_GLOBAL:
58    vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
59          DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
60    break;
61  default:
62    break;
63  }
  1. O switch case aninhado do demônio, com cláusulas aditivas sorrateiramente escondidas (fonte ):
 1case DT_ACTION_ELEMENT_ZOOM:
 2    ;
 3    switch(effect)
 4    {
 5    case DT_ACTION_EFFECT_POPUP:
 6      dt_bauhaus_show_popup(widget);
 7      break;
 8    case DT_ACTION_EFFECT_RESET:
 9      move_size = 0;
10    case DT_ACTION_EFFECT_DOWN:
11      move_size *= -1;
12    case DT_ACTION_EFFECT_UP:
13      _slider_zoom_range(bhw, move_size);
14      break;
15    case DT_ACTION_EFFECT_TOP:
16    case DT_ACTION_EFFECT_BOTTOM:
17      if((effect == DT_ACTION_EFFECT_TOP) ^ (d->factor < 0))
18        d->max = d->hard_max;
19      else
20        d->min = d->hard_min;
21      gtk_widget_queue_draw(widget);
22      break;
23    default:
24      fprintf(stderr, "[_action_process_slider] unknown shortcut effect (%d) for slider\n", effect);
25      break;
26    }

Os programadores entendem do que estou falando; para os demais, saibam apenas que eu não entendo mais do que vocês o que isso faz: é código de merda, e se não houver vários bugs escondidos aí dentro, será pura sorte. Caçar bugs neste chiqueiro é arqueologia de fundo de esgoto, ainda mais considerando que o Darktable não possui documentação para desenvolvedores e, na ausência de comentários significativos no código, qualquer modificação do código mencionado necessariamente começará por uma fase de engenharia reversa que se torna cada vez mais difícil com o passar do tempo.

O verdadeiro problema desse tipo de código é que você não pode melhorá-lo sem reescrevê-lo mais ou menos por inteiro: para corrigi-lo, primeiro é preciso entendê-lo, mas a razão pela qual ele precisa ser corrigido é justamente que ele não é compreensível e é perigoso a longo prazo. Chamamos isso de dívida técnica . Em suma, todo o trabalho investido nesse recurso vai gerar trabalho extra, porque é insensato manter esse tipo de código no meio de uma base de código de várias centenas de milhares de linhas e esperar que ele não exploda na nossa cara um dia.

É ainda mais ridículo no contexto de um aplicativo de código aberto/livre onde a maior parte da equipe é composta por programadores sem formação. Desenvolvedores inteligentes escrevem código compreensível por idiotas, e vice-versa.

Filtros de coleção

Até o Darktable 3.8, os filtros de coleção, no topo da mesa de luz, eram usados para restringir temporariamente a visualização de uma coleção. A coleção é uma extração do banco de dados de fotos com base em certos critérios, sendo o mais comum extrair o conteúdo de uma pasta (que o Darktable chama de “filmroll” para confundir todo mundo, porque um filmroll é na verdade o conteúdo de uma pasta exibido como uma lista plana em vez de uma árvore - muita gente pensa erroneamente que o Darktable não tem gerenciador de arquivos).

Sendo usuário do Darktable há mais de uma década, tenho um banco de dados de mais de 140.000 entradas. Extrair uma coleção dentre essas 140.00 fotos é uma operação lenta. Mas minhas pastas raramente contêm mais de 300 fotos. Filtrar, por exemplo, as fotos avaliadas com 2 estrelas ou mais, em uma coleção de 300 arquivos, é rápido porque é um subconjunto de 300 elementos. E alternar de um filtro para outro também é rápido. O filtro é apenas uma visualização parcial ou total de uma coleção, otimizada para um uso rápido e temporário de entra-e-sai.

Sob o pretexto de refatorar o código de filtragem, que ocupava ao todo 550 linhas , o chefe Gaston Lagaffe  fez questão de quebrar esse modelo para transformar os filtros de coleção em coleções básicas, por meio de mais de 6.000 linhas de código , sem contar as incontáveis correções de bugs que só adicionaram mais linhas2. Tudo isso, como de costume, altamente configurável e redundante com o clássico módulo de coleções , que permaneceu lá, e servido por ícones tão crípticos que tiveram de adicionar dicas de texto ao passar o mouse para esclarecer o que significam..

Neste código de qualidade, encontraremos o while sem fim sob o switch case dentro do if dentro do if dentro do for (fonte ):

 1for(int k = 0; k < num_rules; k++)
 2  {
 3    const int n = sscanf(buf, "%d:%d:%d:%d:%399[^$]", &mode, &item, &off, &top, str);
 4
 5    if(n == 5)
 6    {
 7      if(k > 0)
 8      {
 9        c = g_strlcpy(out, "<i>   ", outsize);
10        out += c;
11        outsize -= c;
12        switch(mode)
13        {
14          case DT_LIB_COLLECT_MODE_AND:
15            c = g_strlcpy(out, _("AND"), outsize);
16            out += c;
17            outsize -= c;
18            break;
19          case DT_LIB_COLLECT_MODE_OR:
20            c = g_strlcpy(out, _("OR"), outsize);
21            out += c;
22            outsize -= c;
23            break;
24          default: // case DT_LIB_COLLECT_MODE_AND_NOT:
25            c = g_strlcpy(out, _("BUT NOT"), outsize);
26            out += c;
27            outsize -= c;
28            break;
29        }
30        c = g_strlcpy(out, "   </i>", outsize);
31        out += c;
32        outsize -= c;
33      }
34      int i = 0;
35      while(str[i] != '\0' && str[i] != '$') i++;
36      if(str[i] == '$') str[i] = '\0';
37
38      gchar *pretty = NULL;
39      if(item == DT_COLLECTION_PROP_COLORLABEL)
40        pretty = _colors_pretty_print(str);
41      else if(!g_strcmp0(str, "%"))
42        pretty = g_strdup(_("all"));
43      else
44        pretty = g_markup_escape_text(str, -1);
45
46      if(off)
47      {
48        c = snprintf(out, outsize, "<b>%s</b>%s %s",
49                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", _(" (off)"), pretty);
50      }
51      else
52      {
53        c = snprintf(out, outsize, "<b>%s</b> %s",
54                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", pretty);
55      }
56
57      g_free(pretty);
58      out += c;
59      outsize -= c;
60    }
61    while(buf[0] != '$' && buf[0] != '\0') buf++;
62    if(buf[0] == '$') buf++;
63  }

e outros if aninhados em 2 níveis dentro de switch case necessários para dar suporte aos atalhos de teclado (fonte ).

Essa última merda foi a gota d’água que fez transbordar o copo e me levou a criar o fork Ansel. Recuso-me a trabalhar em uma bomba-relógio numa equipe que não enxerga o problema e brinca com código nas horas vagas. Programar pode diverti-los, a mim não. E consertar as merdas feitas por moleques irresponsáveis com o dobro da minha idade, especialmente quando quebram coisas que arrumei 3 ou 4 anos atrás, me enfurece.

Mesa de luz

A mesa de luz passou por 2 reescritas quase completas, a primeira no início de 2019 e a segunda no fim de 2019, que adicionaram muitos recursos discutíveis como a visualização de triagem (culling) .

Rapidamente, o modo de triagem (culling) é dividido em 2 submodos: dinâmico e estático, que gerenciam o número de imagens de forma diferente. Muitos usuários ainda não entenderam a diferença 4 anos depois. Temos, portanto, a visualização padrão (gerenciador de arquivos), a mesa de luz com zoom (que ninguém usa), a triagem estática, a triagem dinâmica e o modo de pré-visualização (uma única imagem em tela cheia).

Em seguida, mais opções de exibição são adicionadas às miniaturas na mesa de luz, permitindo definir sobreposições: sobreposições permanentes básicas, sobreposições EXIF permanentes estendidas, as mesmas mas apenas ao passar o mouse, e por fim as sobreposições temporizadas ao passar o mouse (com um temporizador configurável).

O código da interface que renderiza as miniaturas e suas sobreposições precisa, portanto, levar em conta 5 visualizações diferentes e 7 variantes de exibição , ou seja, 35 combinações possíveis. O código que garante o redimensionamento correto das miniaturas precisa, assim, de um total de 220 linhas .

Mas não para por aí, porque o código que renderiza a interface das miniaturas é compartilhado também com a barra inferior “filmstrip”, o que na verdade dá 36 combinações possíveis na renderização das miniaturas. Multiplicado por 3 temas de interface com cores base diferentes, isso dá 108 conjuntos de instruções CSS para estilizar totalmente a interface… muitos dos quais foram esquecidos na reformulação gráfica do Darktable 4.0 , e como poderia ser diferente?

No Darktable 2.6, tínhamos 4193 linhas para o conjunto, que possuía apenas as visualizações de gerenciador de arquivos, mesa de luz com zoom e pré-visualização em tela cheia, com apenas 2 modos de sobreposições de miniaturas (sempre visíveis ou visíveis ao passar o mouse):

Depois do Darktable 3.0 e da adição dos modos de triagem (culling), chegamos a 6731 linhas:

Depois do Darktable 3.2 e das adições das 7 variantes de sobreposições altamente configuráveis e de alguma refatoração de código, chegamos a 8380 linhas:

No Darktable 4.2, após a correção de muitos bugs, chegamos a um total de 9264 linhas:

O número de linhas (especialmente em código que tem um prazer deslocado em ignorar as boas práticas de programação) é um indicador direto da dificuldade de depurar qualquer coisa ali dentro, mas também um indicador indireto (no caso específico do código de interface) da carga de CPU necessária para executar o software.

De fato, se você iniciar darktable -d sql e passar o mouse sobre uma miniatura na mesa de luz, obterá no terminal:

 1140.8252 [sql] darktable/src/common/image.c:311, function dt_image_film_roll(): prepare "SELECT folder FROM main.film_rolls WHERE id = ?1"
 2140.8259 [sql] darktable/src/common/image.c:387, function dt_image_full_path(): prepare "SELECT folder || '/' || filename FROM main.images i, main.film_rolls f WHERE i.film_id = f.id and i.id = ?1"
 3140.8271 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 4140.8273 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 5140.8275 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 6140.8277 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 7140.8279 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 8140.8280 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 9140.8282 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
10140.8284 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
11140.8286 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
12140.9512 [sql] darktable/src/common/act_on.c:156, function _cache_update(): prepare "SELECT imgid FROM main.selected_images WHERE imgid=104337"
13140.9547 [sql] darktable/src/common/act_on.c:156, function _cache_update(): prepare "SELECT imgid FROM main.selected_images WHERE imgid=104337"
14140.9550 [sql] darktable/src/common/act_on.c:288, function dt_act_on_get_query(): prepare "SELECT imgid FROM main.selected_images WHERE imgid =104337"
15140.9552 [sql] darktable/src/libs/metadata.c:263, function _update(): prepare "SELECT key, value, COUNT(id) AS ct FROM main.meta_data WHERE id IN (104337) GROUP BY key, value ORDER BY value"
16140.9555 [sql] darktable/src/common/collection.c:973, function dt_collection_get_selected_count(): prepare "SELECT COUNT(*) FROM main.selected_images"
17140.9556 [sql] darktable/src/libs/image.c:240, function _update(): prepare "SELECT COUNT(id) FROM main.images WHERE group_id = ?1 AND id != ?2"
18140.9558 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"

o que significa que 18 requisições SQL são feitas ao banco de dados para buscar informações da imagem, e executadas toda vez que o cursor passa sobre uma nova miniatura, sem motivo, já que os metadados não mudaram desde a passagem anterior.

No Ansel, ao remover a maioria das opções, consegui poupar 7 requisições, o que ainda não impede requisições duplicadas, mas mesmo assim melhora um pouco os tempos (os carimbos de tempo são os números no início de cada linha):

 112.614534 [sql] ansel/src/common/image.c:285, function dt_image_film_roll(): prepare "SELECT folder FROM main.film_rolls WHERE id = ?1"
 212.615225 [sql] ansel/src/common/image.c:356, function dt_image_full_path(): prepare "SELECT folder || '/' || filename FROM main.images i, main.film_rolls f WHERE i.film_id = f.id and i.id = ?1"
 312.616499 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 412.616636 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 512.616769 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 612.616853 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 712.616930 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 812.617007 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 912.617084 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
1012.617205 [sql] ansel/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (133727) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
1112.617565 [sql] ansel/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (133727) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"

O problema é que o código-fonte aninha comandos SQL dentro de funções que desenham a interface, e desemaranhar essa bagunça através das diferentes camadas herdadas da “refatoração” (que deveria simplificar o código, mas na verdade não) é, mais uma vez, arqueologia. E se o problema tivesse sido corrigido quando o código tinha 6700 linhas em 3 arquivos, não estaríamos procurando, 4 anos depois, as causas em 2500 linhas adicionais agora espalhadas em 7 arquivos diferentes (sem contar os arquivos .h).

Estamos no caso exemplar em que a “refatoração” na verdade complexificou o código e em que fundir o código das miniaturas entre o filmstrip e a mesa de luz apenas adicionou mais if internos (ramificações) aninhados em vários níveis, que complexificam ainda mais a estrutura, só para seguir cegamente o princípio de reutilização de código , que aqui entra em conflito com o princípio de modularidade , o que um desenvolvedor competente teria resolvido com [herança](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming) , porque, mesmo não sendo fácil de fazer em C, é perfeitamente possível (aliás, o Darktable usa esse princípio em código de 2009-2010).

A cosmética se sobrepõe à estabilidade

O Darktable 4.2 introduz a pré-visualização de estilos na sala escura. Isso seria ótimo se os estilos não estivessem profundamente quebrados quando usados com uma ordem de pipeline não padrão e múltiplas instâncias de módulos. O problema é que uma solução limpa e de longo prazo envolve a teoria dos grafos direcionados , e é aí que perdemos nossos amados mijadores de código copiado e colado.

No mesmo espírito, temos grandes inconsistências ao copiar e colar o histórico no modo sobrescrever quando predefinições padrão do usuário também são usadas (especialmente no módulo de balanço de branco). Mas é bem mais divertido encher a interface de merda, então isso vai ficar aí por muito tempo.

O Darktable 3.6 e o 3.8 introduziram muitas variantes do histograma: vectorscópio, forma de onda vertical, espaços de cor avançados e exóticos. Só que, se você executar darktable -d perf no terminal e abrir a sala escura, verá um monte de

123.748084 [histogram] took 0.003 secs (0.000 CPU) scope draw
223.773753 [histogram] took 0.005 secs (0.004 CPU) scope draw
323.783284 [histogram] took 0.001 secs (0.000 CPU) scope draw

toda vez que você move o cursor na janela (e nem mesmo sobre o histograma). É o histograma que é redesenhado a cada interação entre o cursor e a janela. O mesmo problema afeta muitos widgets gráficos personalizados  e sua causa não foi identificada. Note que ele não afeta o Ansel, então a causa deve estar escondida em algum lugar nas 23.000 linhas de código que removi.

Duas vezes tentei refatorar a bagunça em que esse recurso se tornou , mas a cada vez um novo recurso mais urgente era empurrado, invalidando meu trabalho. Simplesmente desisti.

O estado de deterioração do histograma é tal que uma reescrita completa levaria menos tempo do que uma refatoração, especialmente porque o histograma é amostrado tarde demais no pipeline, no espaço de cor da tela, o que torna nula a definição de um espaço de cor do histograma, já que o gamut é recortado no espaço de cor da tela de qualquer maneira. Mas adivinhe só… o Darktable 4.4 terá ainda mais opções, com a capacidade de definir harmonias de cores (fundamental para os geeks que pintam por números e editam histogramas).

O fato é que, sempre que você move o cursor, um grande número de recálculos inúteis é iniciado à toa. Quão grave é isso? Tive a ideia de medir o uso de CPU do meu sistema quando ocioso, com a ferramenta Linux powertop. O protocolo é bem simples: um laptop (CPU Intel Xeon Mobile de 6ª geração), funcionando na bateria em modo de economia de energia, retroiluminação no mínimo, abrir o aplicativo e não tocar em nada por 4 min, depois monitorar o consumo global de CPU do sistema conforme relatado pelo powertop durante o 5º minuto:

  • Sistema base (nenhum aplicativo aberto, exceto o powertop rodando em um terminal): 3,0 a 3,5 % de CPU
  • Ansel:
    • aberto na mesa de luz: 2,9 a 3,4 % de CPU,
    • aberto na sala escura: 3,8 a 4,5 % (antes de reverter os grupos de módulos para o Darktable 3.2),
    • aberto na sala escura: 3,0 a 3,5 % (depois de reverter os grupos de módulos),
  • Darktable:
    • aberto na mesa de luz: 6,6 a 7,1 % de CPU,
    • aberto na sala escura: 30,9 a 44,9 % de CPU (não, não é um erro de vírgula),

Não entendo o que o Darktable calcula quando o deixamos aberto sem tocar no computador, porque não há nada a calcular. O Darktable na mesa de luz consome sozinho tanto quanto o sistema inteiro (Fedora 37 + área de trabalho KDE + gerenciador de senhas e cliente Nextcloud rodando em segundo plano), e consome 10 vezes mais que o sistema inteiro quando aberto na sala escura.

Tudo isso aponta para um código de interface gráfica muito cheio de bugs. No Ansel, removi grande parte do código sujo, sem otimizar mais nada, e esses números apenas validam minha escolha: código sujo esconde problemas indetectáveis por meio da leitura, e simplesmente não podemos continuar por esse caminho.

Aparentemente, sou o único a achar inaceitável privar o pipeline de pixels de um terço a metade da potência da CPU para pintar uma interface idiota. De qualquer forma que se coloque, não há razão válida para que um software deixado aberto sem que se toque nele transforme o computador em uma torradeira, ainda mais porque não compramos mais gás russo.

Trabalhando contra nós mesmos

Somos fotógrafos. O fato de precisarmos de um computador para fazer fotografia é uma novidade (com 20 anos), ligada à tecnologia de imagem digital que substituiu, por todo tipo de razão (boas e ruins), uma tecnologia de 160 anos, conhecida e dominada. No processo, o fato de precisarmos de um computador e de um software para produzir imagens é pura e simplesmente sobrecarga . Forçar pessoas que não entendem de computadores a usá-los para realizar tarefas que antes conseguiam gerenciar perfeitamente à mão é também uma forma de opressão, e disfarçar isso de progresso técnico é uma forma de violência psicológica.

Software implica desenvolvimento, manutenção, documentação e gestão de projeto. São várias camadas de sobrecarga sobre as anteriores. No entanto, o fato de que a mão de obra em projetos de código aberto não pede remuneração não deveria ofuscar o fato de que o tempo gasto (perdido?) no software, em seu uso, em seu desenvolvimento, em sua manutenção, é em si um custo não reembolsável.

Os poucos exemplos acima dão uma ideia da complexificação do código-fonte, mas também de sua degradação ao longo do tempo em termos de qualidade, porque recursos básicos e robustos são substituídos por código espaguete , confuso e sorrateiramente cheio de bugs. Por trás dessa questão de legibilidade, o verdadeiro problema é tornar a manutenibilidade de médio prazo mais difícil, o que promete um futuro sombrio para o projeto, com a aprovação do mantenedor.

Nos 4 anos em que trabalho em tempo integral no Darktable, 2022 é o primeiro ano em que me vejo praticamente incapaz de identificar a causa da maioria dos bugs de interface, porque a lógica de funcionamento tornou-se muito ofuscada e o código incompreensível. O número de bugs corrigidos também está em constante diminuição, tanto em valor absoluto quanto em proporção dos pull requests mesclados, enquanto o volume de tráfego de código permanece aproximadamente constante (nota 1: as contagens de linhas de código a seguir incluem apenas arquivos C/C++/OpenCL e XML gerativos e excluem comentários 3) (nota 2: o número de issues abertas é contado ao longo da vida útil da versão anterior):

3.0 (dezembro de 2019, um ano depois da 2.6)
  • 1049 issues abertas , 66 issues fechadas / 553 pull requests mesclados (12 %),
  • 398 arquivos alterados, 66 k inserções, 22 k exclusões, (líquido: +44 k linhas),
3.2 (agosto de 2020)
  • 1028 issues abertas , 92 issues fechadas / 790 pull requests mesclados (12 %),
  • 586 arquivos alterados, 54 k inserções, 43 k exclusões (líquido: +2 k linhas),
3.4 (dezembro de 2020)
  • 981 issues abertas , 116 issues fechadas / 700 pull requests mesclados (17 %),
  • 339 arquivos alterados, 46 k inserções, 23 k exclusões (líquido: +23 k linhas),
3.6 (junho de 2021)
  • 759 issues abertas , 290 issues fechadas / 954 pull requests mesclados (30 %),
  • 433 arquivos alterados, 53 k inserções, 28 k exclusões (líquido: +25 k linhas),
3.8 (dezembro de 2021)
  • 789 issues abertas , 265 issues fechadas / 571 pull requests mesclados (46 %),
  • 438 arquivos alterados, 41 k inserções, 21 k exclusões (líquido: +20 k linhas),
4.0 (junho de 2022)
  • 632 issues abertas , 123 issues fechadas / 586 pull requests mesclados (21 %),
  • 359 arquivos alterados, 30 k inserções, 15 k exclusões (líquido: +15 k linhas),
4.2 (dezembro de 2022)
  • 595 issues abertas , 60 issues fechadas / 409 pull requests mesclados (15 %),
  • 336 arquivos alterados, 14 k inserções, 25 k exclusões (líquido: -11 k linhas),
  • (as exclusões devem-se principalmente à remoção do caminho SSE2 no código de pixels, penalizando o desempenho das CPUs Intel i5/i7 típicas em benefício das CPUs AMD Threadripper),
4.4 (junho de 2023)
  • 500 issues abertas , 97 issues fechadas / 813 pull requests mesclados (12 %),
  • 479 arquivos alterados, 57 k inserções, 41 k exclusões (líquido: +16 k linhas),

Para facilitar a comparação, vamos anualizá-los:

  • 2019: 1049 novas issues, 66 fechadas, 88 k alterações, +44 k linhas,
  • 2020: 2009 novas issues, 208 fechadas, 166 k alterações, +25 k linhas,
  • 2021: 1548 novas issues, 555 fechadas, 143 k alterações, +45 k linhas,
  • 2022: 1227 novas issues, 183 fechadas, 84 k alterações, +4 k linhas.

Parece que não sou o único a achar os bugs de 2022 muito mais difíceis de resolver, pois bem menos deles foram corrigidos em comparação com 2021, e 2023 mostra a mesma tendência até agora. A proporção entre pull requests (trabalho de fato realizado) e issues fechadas (problemas de fato resolvidos) é simplesmente ridícula.

Entre o Darktable 3.0 e o 4.0, o código da GUI cresceu 53 %, de 49 k para 75 k linhas4 (descontando comentários e linhas em branco), e chegou a 79 k linhas na 4.4. Deixando de lado a baixa qualidade dele, não tenho certeza alguma de que isso melhorou a usabilidade do software em 53 %. Na verdade, estou bastante convencido do contrário. No Ansel, até agora reduzi o código da GUI para 53 k linhas removendo pouca funcionalidade.

Tudo isso é demais e rápido demais para um bando de hobistas trabalhando à noite e nos fins de semana sem estrutura nem planejamento. A equipe do Darktable trabalha contra si mesma ao tentar abocanhar mais do que consegue mastigar, suportando opções demais, produzindo código cujo resultado depende de variáveis de ambiente demais, sendo capaz de interagir de maneiras demais. Tudo isso para evitar tomar decisões de design que poderiam ofender alguns caras ao limitar recursos e opções disponíveis. Do lado do usuário final, isso resulta em bugs contextuais impossíveis de reproduzir em outros sistemas, portanto impossíveis de corrigir.

É simples: o trabalho realizado custa cada vez mais trabalho, e a manutenção não está garantida, como mostra o declínio das issues fechadas, porque é simplesmente demais. Numa empresa, este é o momento em que você precisa estancar a hemorragia antes de ter esvaziado os cofres. Mas uma equipe de amadores que não é obrigada a entregar resultado nenhum pode suportar uma quantidade infinita de perdas. Só que o trabalho gerado pelo trabalho fica cada vez mais tedioso, frustrante e difícil com o passar do tempo, e os usuários finais são feitos reféns por uma gangue de babacas egoístas e vão pagar por isso em termos de complexidade da GUI, carga desnecessária de CPU e necessidade de reaprender como realizar tarefas básicas com o software pelo menos uma vez por ano.

Na verdade, espero que a atual equipe de destruição em massa convenientemente encontre cada vez menos tempo livre para contribuir com o projeto à medida que perceberem que se enfiaram numa rua de mão única com uma carreta, deixando a merda deles para os próximos. Mas quanto mais cedo desistirem, menos dano vão causar.

A orgia de opções e preferências, que é a estratégia predileta do Darktable para (não) gerenciar desacordos de design, cria casos de uso supercontextuais em que nenhum usuário tem as mesmas opções ativadas e em que é impossível reproduzir bugs em um ambiente diferente. E pedir aos usuários que anexem o arquivo de configuração darktablerc aos relatórios de bug também não ajudaria, já que esse arquivo tem atualmente 1287 linhas praticamente ilegíveis.

Bugs estranhos e dificilmente reproduzíveis se acumulam, até mesmo em computadores System 76  projetados especificamente para Linux, onde não podemos invocar problemas de drivers. Muitos bugs inconsistentes e aleatórios que presenciei ao dar aulas de edição não estão listados no rastreador de bugs, e é bastante claro que eles estão em algum lugar nas complexidades da orgia de if e switch case que são adicionados a um ritmo alarmante desde 2020.

Corrigir esses bugs estranhos e contextuais só pode ser feito simplificando o fluxo de controle do programa e, portanto, limitando o número de parâmetros do usuário. Mas o bando de geeks agitando os braços no projeto não quer ouvir falar disso e, pior, as “correções” de bugs geralmente só adicionam mais linhas para lidar com casos patológicos individualmente.

Na verdade, o Darktable sofre de vários problemas:

  1. Um núcleo duro de desenvolvedores bastante medíocres que têm muito tempo livre nas mãos para fazer coisas aleatórias, movidos pelas melhores intenções do mundo mas alheios aos danos que causam (as pessoas medíocres são sempre as mais disponíveis),
  2. A complacência do mantenedor, que deixa passar código sujo para ser gentil,
  3. Uma falta crítica de habilidades em matemática pura, algoritmia, processamento de sinais, ciência das cores e, de modo geral, em pensamento abstrato, que são necessárias para além do código de processamento de pixels para simplificar e fatorar recursos,
  4. Um hábito desprezível de “desenvolver” copiando e colando código pego em outro lugar do projeto ou em outros projetos FLOSS que podem ter usado uma arquitetura de pipeline diferente mas sem adaptá-lo devidamente (adaptar implica entender, e isso é pedir demais…),
  5. Uma recusa pura e simples de podar recursos para dar espaço a novos e manter um certo equilíbrio,
  6. Um viés de amostragem, em que os únicos usuários que interagem com o desenvolvimento pelo Github são programadores e falantes de inglês. O fato é que o público geral não entende o que é uma forja de código e é difícil incentivar não programadores a abrir uma conta no Github para relatar bugs. Estamos falando de uma amostra de usuários composta por mais de 44 % de programadores  e por mais de 35 % de pessoas com diploma universitário (que são, respectivamente, 6 % e 15 % na população geral).
  7. Um estilo de desenvolvimento em marcha forçada, sem planejamento nem diálogo, em que todo usuário do Github pode poluir as discussões com uma opinião desinformada sobre o trabalho em andamento. O fato é que o processamento de imagens parece fácil e inofensivo, tanto que qualquer pessoa capaz de calcular um logaritmo se sente competente. Mas os erros fundamentais na cadeia de colorimetria do Darktable estão lá para nos lembrar todos os dias do contrário.
  8. Uma falta de prioridades no âmbito do projeto quanto a quais recursos refatorar, estabilizar ou estender: todos os projetos ficam abertos ao mesmo tempo, mesmo que conflitem entre si.
  9. Uma quantidade de atividade (e-mails e notificações) impossível de acompanhar, entre comentários, discussões fora do tópico, bugs que não são bugs, propostas de alteração de código, alterações de código reais que podem impactar seu próprio trabalho em andamento, o que significa que você tem que estar em todos os lugares o tempo todo; há muito a ler, muito pouco a guardar, discussão pela discussão atrapalha a produtividade e a falta de estrutura de trabalho é a principal causa de tudo isso,
  10. Bugs não bloqueantes escondidos às pressas antes de terminarmos de entendê-los, em vez de corrigi-los de verdade e enfrentá-los na raiz, o que desloca ou até agrava os problemas a longo prazo sem deixar rastros em nenhum tipo de documentação,
  11. Um cronograma de versões que mantemos custe o que custar mesmo quando não é realista, sendo que ninguém o impõe sobre nós,
  12. Alterações de código que podem acontecer a qualquer hora em qualquer lugar, o que significa que trabalhamos sobre areia movediça e que temos que trabalhar tão rápido e tão mal quanto os outros para não ficar para trás em relação ao volume e à frequência das alterações (commits),
  13. Novos recursos que degradam a usabilidade e complicam o uso sem resolver um problema definido, que servem de projetos recreativos para desenvolvedores sem formação em design/engenharia.

Mas o mais enfurecedor é essa obstinação em substituir recursos simples e funcionais por horrores de sobre-engenharia destinados a agradar usos desviantes e marginais ao mesmo tempo em que tornam a vida de todo mundo mais difícil com listas intermináveis de opções irrefletidas. O melhor lugar para esconder uma árvore é no meio da floresta, e muitos ainda não aprenderam isso.

Confundir agitação com atividade

Todo eleitor gosta de criticar o desvio que, na política, consiste em emitir leis circunstanciais, mal escritas, para apaziguar a opinião pública depois de um evento especial, para se exibir que agimos, sendo que leis semelhantes já existem e não são aplicadas, ou não são plenamente aplicadas, por falta de meios. Chamamos isso de agitação: parece ação, soa como ação, tem o custo da ação, mas não leva a nada tangível ou prático.

A equipe de amadores sem gerenciamento de projeto se agitando em torno do Darktable produz apenas problemas futuros. No passado, o Darktable era lançado uma vez por ano com cerca de 1500 a 2000 commits  à frente da versão anterior. Esse é agora o volume de alteração alcançado em 6 meses. Um volume de “trabalho” que aumenta tão rápido sem levar a métodos de trabalho em equipe, incluindo prioridades claras para cada versão e repartição de tarefas, e sem controle de qualidade de software baseado em métricas objetivas (número de etapas ou tempo decorrido para realizar uma tarefa específica), são apenas caras pisando no pé uns dos outros enquanto empurram a própria agenda sem se importar com os outros, nem com o projeto, nem com os usuários.

O Darktable virou o clube de informática do ensino médio, onde os geeks se divertem. É, no geral, um resumo de todas as piores histórias de empresas de TI, com a diferença de que o projeto não fatura um centavo, o que torna urgente perguntarmos a nós mesmos por que impomos isso sobre nós: não há lucros para dividir, mas todo mundo divide os custos. É um ambiente de trabalho caótico e tóxico que só fabricaria burn-out se os amadores de meio período fossem obrigados a entregar resultados e tivessem que trabalhar em tempo integral. Sendo o único cara em tempo integral nisso, deixo você imaginar a quantidade de estresse e energia perdida para se manter atualizado com a cacofonia permanente, só para ter certeza de não perder os 2 % de fato relevantes para mim em meio à quantidade de ruído produzido por discussões não reguladas.

Do lado do usuário, elogiamos a efervescência do projeto Darktable (sim, há movimento), sem perceber que a disparada de commits não é atividade e sim agitação, e em particular dívida técnica que terá de ser paga não-sabemos-quando por não-sabemos-quem. A beleza de um projeto em que ninguém precisa assumir a responsabilidade pela própria bosta porque ninguém responde por nada: está escrito na licença GNU/GPL. Podemos, portanto, ferrar o trabalho dos anteriores com total impunidade.

Temos o início de um controle de qualidade, por meio dos testes de integração, que medem o erro perceptual sobre processamentos de imagens de referência, mas eles não disparam nenhuma resposta quando vemos um erro médio delta E de 1,3 (pequeno) quando a natureza da alteração deveria ter um delta E estritamente zero. Se o único teste passa (porque testamos uma única imagem SDR sobre uma foto de estúdio), nenhuma pergunta é feita sobre se a teoria é sólida e robusta. Transformamos o teste em um álibi, desde que a métrica fique abaixo do limiar de validação…

Com o lançamento do Darktable 4.0 — Geektable —, vi no Youtube pessoas começando a reclamar que esse lançamento não era muito empolgante. Depois de anos dosando as pessoas com lançamentos superlativos, recheados de novos recursos que não temos tempo de testar direito (6-8 caras que mijam 38-50 k linhas a cada 6 meses trabalhando só à noite e nos fins de semana, ainda está sonhando?), nós os viciamos na Árvore de Natal abarrotada para garantir que, no dia em que começarmos a ser responsáveis e lançar versões estáveis (portanto entediantes), isso seja usado contra nós.

A razão desse ritmo frenético de lançamentos é que as pull requests com mais de 3 meses estão sistematicamente em conflito com o branch master, dado que este é sacudido todo mês para “testes generalizados”. Mas os raros usuários que compilam o branch master não têm ideia do que precisam testar em particular, a menos que dissequem o histórico de commits do Git, o que implica entender tanto a linguagem C quanto o impacto das alterações na prática sobre o software.

Para limitar os branches de vida longa, que vão inevitavelmente acabar em conflito com o master, encontramos uma solução brilhante: lançamos 2 versões por ano, fazendo do desenvolvimento em marcha forçada baseado em código inacabado e mal testado um modo de vida, sem nunca perceber que o problema central é, primeiro, a falta de planejamento, mas também que os colaboradores começam a programar antes de terminar de definir o problema a resolver (quando há um problema real a resolver, e não só um cara que acordou tipo “seria legal se…”), trabalhando em paralelo nas duas partes sem comunicação.

A poeira não tem tempo de assentar e já estamos sacudindo a base de código de novo, sem impor fases de estabilização em que só limpamos bugs (e não estou falando do mês de congelamento de recursos antes do lançamento, mas de versões dedicadas apenas à limpeza de código). O rastreador de bugs implode nas 3 semanas seguintes a cada lançamento, porque uma parcela significativa dos usuários usa apenas os pacotes pré-compilados, que coincidem com as férias de Natal e de verão, quando eu pessoalmente tenho coisas melhores a fazer depois do sprint já estressante que é o mês anterior ao lançamento.

Desde 2021, quando atualizo meu repositório Git com as últimas alterações do master do Darktable, é sempre me perguntar o que eles quebraram desta vez. Quebramos mais rápido do que consertamos e, na maioria das vezes, as correções quebram outra coisa. Os únicos usuários que acham o Darktable estável são, na verdade, os que fazem um uso muito básico dele, o que é irônico para um aplicativo cujo argumento de venda é ser avançado.

E aí me vêm com o fato de que é trabalho gratuito como se fosse uma desculpa. Mas na verdade é uma circunstância agravante: por que impomos a nós mesmos tais condições de trabalho se nem sequer é lucrativo??? Além do fato de que esse trabalho gratuito me dá um burn-out pós-lançamento por ano, custa cada vez mais em manutenção e a manutenção é cada vez mais desprezível de fazer. Não é trabalho gratuito, é pior: é trabalho que custa sem pagar.

De qualquer forma, o trabalho é fornecido por pessoas que têm tempo e energia limitados. Se o recurso é limitado, corta essa palhaçada: estamos nas mesmas restrições de rentabilidade que uma empresa, menos os encargos sociais, exceto que nossa moeda de troca é o tempo e ela não é reembolsável. Sem gestão de prioridades, seremos ultrapassados por uma dívida técnica que não teremos recursos para manter.

O que estamos esperando para sermos felizes?

O fato de o Darktable ser um rolo compressor em modo desgovernado e sem motorista, que gera uma quantidade crescente de trabalho, é um mau cheiro para um projeto de 15 anos. Normalmente, um projeto maduro desacelera porque está completo o suficiente para ser usável e porque as pessoas que trabalham nele encontraram sua velocidade de cruzeiro e métodos de trabalho eficientes.

Estou em tempo integral nisso desde 2018, por uma renda mensal entre 800 e 900 €, e é um eufemismo dizer que é mal pago para suportar as consequências desastrosas de amadores desorganizados tentando se divertir às custas da qualidade do produto final e de sua usabilidade pelos Trouxas da informática. Aliás, os Trouxas também são desprezados, por princípio.

Se eu me enfio embaixo de uma pedra por um mês para desenvolver um espaço de cor perceptual, quando saio, é para descobrir o novo sistema labiríntico violando um pouco mais o paradigma view-model-controller e ser informado de que chego tarde demais para me opor a ele. Se tiro 3 semanas de férias em agosto, é para descobrir que o mantenedor ignorou (mais uma vez) minha revisão sobre uma alteração matemática no já mencionado espaço de cor, que exige sentar-se calmamente e pensar, tudo isso porque… precisávamos ir rápido? Por qual emergência, exatamente?

Provavelmente recebi a notificação em algum lugar no meio dos 2234 e-mails que o Github me enviou entre janeiro e agosto de 2022 (em 2021, foram 4044), sem mencionar os usuários que me acionam em todos os lugares, no Youtube, Reddit, Matrix, Github, Telegram, diretamente por e-mail, e antigamente no pixls.us (693 e-mails em 2022, 948 em 2021). Tudo isso para pessoas completamente fora de sintonia que não percebem que faço isso a semana inteira, que a fotografia pode ser o hobby delas mas é o meu trabalho, e que eu só apreciaria que as pessoas me deixassem em paz nos fins de semana e feriados. Você pode imaginar que os mais irritantes não são os que apoiam financeiramente meu trabalho. As pessoas só respeitam um trabalho se foram cobradas um preço alto por ele.

Não tenho tempo de ser pesquisador, designer, ainda por cima secretário, enquanto faço baby-sitting técnico para uma equipe de Gaston Lagaffe que precisa tanto ser treinada quanto vigiada porque é incapaz de:

  1. fazer um planejamento de desenvolvimento com uma lista de prioridades de novos recursos a trabalhar,
  2. fornecer um caderno de especificações de necessidades e problemas, com um caso de uso real, antes de correr para o editor de código e fazer qualquer coisa para desenvolver um novo recurso em busca de um problema a resolver.
  3. avaliar o custo de manutenção da alteração antes de inventar a bomba de bacon biônica por osmose reversa que só funciona em dias pares se Júpiter estiver fora de fase com Saturno,
  4. limitar as despesas e reduzir as perdas quando se enfiam em becos sem saída de design introduzindo regressões piores do que os hipotéticos benefícios esperados,
  5. assumir a responsabilidade e adiar um lançamento se o código obviamente não está pronto (ou eu não entendi nada e os acionistas vão pedir nossas cabeças se lançarmos atrasado???).

Gestão (ou gerenciamento de equipe) é uma sobrecarga que custa algum trabalho, mas a equipe do Darktable atingiu uma escala em que a falta de gestão na verdade custa mais trabalho, especialmente porque nenhum dos fundadores do projeto ainda está na equipe e os projetos de design iniciais precisam ser submetidos a engenharia reversa com grep no código toda vez que algo precisa mudar. Isso era sustentável com uma equipe reduzida em que todos se conheciam, mas o Darktable se tornou um projeto de alto tráfego durante os lockdowns da Covid e essa forma de trabalhar não é sustentável com o pessoal atual.

Vá ver o código! Compare o branch darktable-2.6.x com o darktable-4.2.x, arquivo por arquivo, e divirta-se!

Tudo o que ouvi até agora são frases prontas como “é como muitos outros projetos opensource” e “não há nada que possamos fazer a respeito”. As pessoas têm medo da quantidade de trabalho que um fork representa (recebi e-mails tentando me convencer de que isso dividia a produtividade), sem perceber a quantidade de recursos atualmente desperdiçados pelo projeto Darktable e o estresse permanente de ter que basear seu trabalho em uma base de código instável, sacudida o tempo todo. Até agora, o Ansel me custou menos cansaço e resolvi um número significativo de problemas entre os quais alguns eram relatados desde 2016 sem sinais de interesse da maldita “comunidade”.

Além disso, o Ansel fornece pacotes noturnos compilados automaticamente para Linux (.AppImage) e Windows (.exe), de modo a permitir testes generalizados reais, inclusive por pessoas incapazes de compilar o software por conta própria. Pedi isso lá em 2019 , mas, aparentemente, os geeks têm coisas melhores a fazer, e tive que investir 70 h eu mesmo para que isso acontecesse. A operação já é um sucesso e permitiu corrigir em questão de dias bugs do Windows que teriam levado semanas para serem identificados no Darktable. (E o Darktable pegou 3 semanas depois meu script de compilação de AppImage sem os devidos créditos, mas isso é um detalhe).

Falando em produtividade, lembremos que a mesa de luz foi reescrita quase inteiramente duas vezes desde 2018 (e a última versão não é melhor nem mais rápida) e a grande alteração dos filtros de coleção introduzida em abril de 2022  sobrescreveu outra alteração semelhante (mas usando apenas 600 linhas em vez de 6000) introduzida em fevereiro de 2022  (a versão de fevereiro é a que está no Ansel). Não podemos decentemente pronunciar a palavra “produtividade” quando o trabalho de um colaborador literalmente apaga o trabalho anterior de outro num prazo de um mês, por simples falta de gerenciamento de projeto. Isso se chama pisar no pé um do outro.

Então, o que estamos fazendo para resolver o problema? Sofrendo em silêncio? Vivendo na negação? Continuando a corrigir coisas que outro vai quebrar no ano seguinte quando não estivermos olhando? Continuando a fazer o Trouxa acreditar, ao longo dos fóruns de fotografia, que o opensource é tão bom quanto o proprietário, guardando como trunfo o fato de que é gratuito então você não tem direito de reclamar? Não é um pouco fácil demais e desonesto, esse discurso duplo?

Você não gostaria de parar de fazer os hábitos passarem por experiência e de confundir fatalismo com sabedoria, e em vez disso atacar o problema na sua raiz? Você não acha que você e eu merecemos algo melhor do que um software projetado por amadores cujo único talento é ter tempo livre e que podem se dar ao luxo de trabalhar de graça porque migraram para a gerência e os filhos já foram para a universidade?

Ou eu me enganei desde o começo, e o opensource se trata de dar ferramentas complicadas demais a geeks que não precisam realmente delas, ao mesmo tempo em que se tenta convencer o resto do mundo de que o open-source não é um hipernicho para desenvolvedores?

Quatro anos de trabalho para chegar aqui

Depois de 4 anos trabalhando no Darktable em tempo integral por 70 % do salário mínimo, e 2 anos suportando a insatisfação crônica de manchar meu nome contribuindo para uma merda, fiz o fork do Ansel e não vou voltar atrás.

Em 4 anos, trouxe a este software algo que fazia muita falta: um fluxo de trabalho unificado, baseado em um conjunto de módulos projetados para funcionar juntos, mas atuando cada um em um aspecto distinto, ao passo que os módulos do Darktable eram mais uma coleção de plugins díspares. Estamos falando de:

  • filmic,
  • tone equalizer,
  • o módulo de desfoques fisicamente precisos,
  • ambas as versões do color balance,
  • color calibration, incluindo a GUI para perfilar com color checkers direto na sala escura e o balanço de branco usando padrões CIE,
  • o módulo negadoctor para inverter negativos de filme baseado no Kodak Cineon,
  • o módulo diffuse and sharpen para adição e remoção de desfoque baseado em difusão térmica,
  • a reconstrução de realces por guided laplacians.

Também desenvolvi ferramentas mais fundamentais que fornecem as bases para os módulos anteriores:

  • um solucionador de equações diferenciais parciais anisotrópicas de 4ª ordem no espaço de wavelets para o diffuse and sharpen,
  • uma adaptação do anterior como o guided laplacian para reconstrução de sinal RGB por propagação de gradientes,
  • um modelo perceptual de aparência de cor que leva em conta o efeito Helmholtz-Kohlrausch no cálculo da saturação, para limitar o efeito “fluo” que normalmente vem com configurações intensas de saturação, no color balance,
  • uma ajuda teórica no desenvolvimento do filtro guiado invariante à exposição (EIGF), no tone equalizer,
  • um solucionador de equações vetoriais lineares pelo método de Choleski,
  • vários métodos de interpolação de ordem 2, 3 e baseados em radial.

Na GUI, notadamente fiz:

  • refatorar a declaração de estilo, removendo a estilização do código C para mapeá-la na folha de estilos CSS, permitindo ter múltiplos temas para a UI, inclusive definidos pelo usuário,
  • introduzir o modo de visualização focus-peaking  e o modo de avaliação de cor ISO 12 646 ,
  • introduzir o vocabulário de cores no seletor de cor global , permitindo nomear a cor selecionada a partir de suas coordenadas de cromaticidade, voltado a fotógrafos daltônicos.

Depois disso, escrevi dezenas de páginas de documentação em 2 idiomas, publiquei artigos e dezenas de horas de vídeo no YouTube para demonstrar como usar os módulos, em que contexto e para qual benefício, incluindo edições rápidas usando apenas 3 a 5 módulos para processar 75 a 80 % das fotos, não importa sua faixa dinâmica. No mundo open-source, exceto talvez por projetos apoiados por fundações (como Krita e Blender), esse nível de suporte e documentação simplesmente não existe, e não são os próprios desenvolvedores que cuidam desse trabalho.

Apesar de tudo isso, nunca tive mais de 240 doadores, para comparar com cerca de 1800 respondentes únicos que participaram das pesquisas do Darktable de 2020 e 2022 , e que declaram gastar entre 500 e 1000 €/ano  em fotografia.

Não vou ficar assistindo eles destruírem a usabilidade deste software enquanto tento me convencer de que é progresso e que não há nada que possamos fazer a respeito. Em vez de progresso, é a visão delirante de progresso de um bando de diletantes cinquentões. Faz 2 anos que fico calado pacientemente, tentando ser gentil, mas olhando a degradação de recursos básicos, complexificados para atender às modas de programadores malucos, eu deveria ter sido desprezível antes. Bancar o bonzinho não resolveu nada porque a tendência não só continuou, como acelerou, e não haverá tomada de consciência antes do ponto sem retorno. Não podemos esperar que aqueles que criaram os problemas sejam os que os resolverão.

Então, se tenho que trabalhar para uma “comunidade” que está nisso principalmente pelo aspecto de o software ser livre de assinatura, e que decidiu que meu trabalho não vale um salário mínimo, pois bem, vou fazer isso nos meus termos e com os meus padrões.

Em termos de recursos, o Darktable já tem demais e precisamos podar. Faz 10 anos que o uso e ele já vem abarrotado de coisas mal projetadas. O desafio agora é apresentar os recursos de forma inteligente e corrigir bugs irritantes antes que esses idiotas introduzam novos, ou mesmo corrigi-los da sua maneira especial: escondendo a poeira embaixo do tapete. Tendo em mente que o pipeline do Darktable tem 15 anos, e não podemos otimizá-lo muito mais do que isso, dado que ele já foi bastante torturado para evitar uma reescrita completa (e uma reescrita completa não oferece benefício se tivermos que manter o Gtk como backend gráfico, já que ele é o principal gargalo de desempenho).

As soluções de que o Darktable precisa implicam remover código e opções, não adicionar sempre mais. A robustez tem esse preço. A equipe do Darktable faz exatamente o contrário sem aprender com seus erros.

Com o Ansel, quero um jeito de terminar este trabalho em paz para que os usuários de Linux tenham uma ferramenta confiável, consistente e performática para sua fotografia artística. Antes de migrar para o Vkdt  porque o design atual está mostrando seus limites.


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

  1.  By the way, I can no longer bare the attempt of being different for the sake of it by writing “Darktable”, proper noun, without initial capital. It’s childish, it’s neither funny or disruptive, and it makes a mess of freedesktop.org  menus where the capital is anyway added to follow the standard. ↩︎

  2. The fact that bugfixes systematically add more lines of code instead of modifying existing lines is a a concerning smell that the programming logic is bad and induces too many particular cases. Rigorous programmers always try to keep their code as generic as possible to avoid spaghetti code ↩︎

  3.  You don’t need to trust me, the command to reproduce the stats is git diff release-3.4.0..release-3.6.0 --shortstat -w -G'(^[^\*# /])|(^#\w)|(^\s+[^\*#/])' -- '*.c' '*.h' '*.cpp' '*.xml.in' '*.xsl' '*CMakeLists.txt' '*.cl' ↩︎

  4. git checkout release-3.0.0 & cloc $(git ls-files -- 'src/views' 'src/gui' 'src/bauhaus' 'src/dtgtk' 'src/libs') ↩︎