Wspomnienia gościa, który spędził zbyt wiele czasu na sprzątaniu cudzego gówna i płaceniu za cudze złe decyzje, odcinek #zbyt wiele.
Kopiowanie/wklejanie historii i stylów to kluczowe funkcje w Anselu i te, które sprawiają, że zasługuje on (lub nie) na miano „aplikacji do pracy”. Są to jednak zarazem jedne z najtrudniejszych do poprawnego zaimplementowania wewnętrznie. Użytkownicy widzą listę edycji, ale pod maską edycje te zależą od kolejności w potoku, instancji modułów i masek. Jeśli dwa zdjęcia mają różne topologie potoku, naiwne kopiowanie edycji może wywołać niespójności.
Ta aktualizacja czyni scalanie historii solidnym, spójnym i przejrzystym, po żmudnej pracy nad czyszczeniem i upraszczaniem kodu. Wprowadza również jasną obsługę błędów, gdy idealne scalenie jest matematycznie niemożliwe.
Krótka historia złego projektu
Do początku 2019 roku Darktable był zaprojektowany wokół stałego potoku: moduły miały kolejność, o której decydował w czasie kompilacji skrypt w Pythonie , stworzony przez założyciela Darktable, Johannesa Hanikę w 2011 roku. Jednej rzeczy można się po Jo spodziewać: że zrobi swoją matematykę poprawnie, a więc ten skrypt robi dokładnie to, co należy robić w takich okolicznościach:
- pozwól programistom deklarować moduły,
- pozwól im zdefiniować, które moduły powinny iść przed każdym modułem, w leniwy i wygodny sposób, który pozwala po prostu powiedzieć „A powinno być przed C”, „B powinno być przed A”, „D powinno być przed A” itd.
- zamień to na graf skierowany , który jest po prostu obiektem matematycznym sklejającym wszystkie te ograniczenia w całość,
- rozwiąż graf skierowany jednym z algorytmów sortowania topologicznego , które sięgają lat 60. XX wieku,
- zapisz numer indeksu każdego modułu w potoku i gotowe.
Niestety, ten stały potok oznaczał, że zmiana względnej kolejności modułów kiedykolwiek później psułaby starsze historie. Co było do bani, ponieważ popełniono pewne błędy projektowe z pozycją transformaty wyświetlacza i modułu krzywej bazowej, które znajdowały się wcześnie w potoku i sprawiały, że kolory wariowały w sytuacjach HDR. Aby to naprawić, musiałem stworzyć moduł filmic jako kolejny moduł, żeby móc umieścić go na końcu potoku. A wszystkie pozostałe moduły odniesione do wyświetlacza musiałyby zostać zduplikowane w kodzie, aby móc wstawić je tam, gdzie potrzeba, bez psucia starszych edycji (w tym moich). Nie trzeba dodawać, że stały potok miał już swoje za sobą, a ja byłem zwolennikiem tej zmiany.
Przy stałym potoku scalanie historii ze sobą, czyli kopiowanie/wklejanie historii między zdjęciami lub stosowanie stylów do zdjęć (co ostatecznie sprowadza się do tego samego), było łatwe: było to jedynie kwestią zastąpienia parametrów modułów i masek 1:1. Nawet przy modułach wieloinstancyjnych, które wprowadzono około 2014 roku, nie było tak źle, ponieważ wszystkie instancje były zmuszone do bycia następującymi po sobie i numerowane względem siebie. Tak więc, po utworzeniu brakujących instancji, na pozycji, która była przewidywalna i niezmienna (w kolejności sekwencyjnej po instancji bazowej), nadal było to zastępowanie parametrów modułów i masek 1:1.
Ale bardzo potrzebna funkcja zmiany kolejności potoku, w czasie działania i przez użytkowników, która pojawiła się wraz z Darktable 3.0, została zrobiona w najgorszy możliwy sposób. Deweloper, który ją zaimplementował, obsłużył indeksowanie za pomocą tablic priorytetów zmiennoprzecinkowych, co nie jest właściwą strukturą danych dla tego problemu. Do tego kod zarządzania historią nie został zrefaktoryzowany i uproszczony przed rozszerzeniem, lecz naprędce wklejony w miejscu z minimalnymi zmianami, co uczyniło go naprawdę złożonym i zaciemnionym. Pascal Obry, który zaakceptował tę zmianę w 2019 roku, musiał przepisać cały backend zmiany kolejności potoku w 2020 roku, używając właściwej struktury danych dla danego zadania (list wiązanych ), ponieważ poprzednia była krucha i niemożliwa do utrzymania.
Ale scalanie potoków, które mogą mieć różne liczby modułów, uporządkowane w nieprzewidywalnych i niezmienniczych miejscach, nie może zostać zrobione w żaden inny sposób niż za pomocą sortowania topologicznego w czasie działania, ponieważ nie jest to już zwykły problem historii (czyli migawek parametrów modułów), to problem dwojaki, który obejmuje historie, ale też topologię potoku. Innymi słowy: musimy rozwiązać, gdzie wstawić instancje modułów, które istnieją w potoku źródłowym, ale nie w docelowym. A jednak skrypt w Pythonie robiący to w czasie kompilacji został usunięty w 2019 roku, a funkcja nigdy nie została przeniesiona do C.
Tak więc sposób, w jaki Darktable po dziś dzień obsługuje scalanie potoków, opiera się na heurystykach naprędce zbudowanych z paradygmatu stałego potoku i ogólnie zepsutych, z wyjątkiem tych ładnych przypadków, które pasują do warunków potoków z 2018 roku i wcześniejszych:
- jeśli Twoje potoki źródłowy i docelowy mają tę samą liczbę modułów uporządkowanych w ten sam sposób, wszystko idzie dobrze.
- jeśli Twój potok źródłowy ma dodatkowe instancje modułów w porównaniu z docelowym, ale te instancje wszystkie znajdują się bezpośrednio po instancji bazowej, wszystko nadal idzie dobrze,
- ale… jeśli masz dodatkowe instancje, czy to w potoku źródłowym, czy docelowym, i zostały one przesunięte w potoku, to zachowanie jest nieokreślone, nieprzewidywalne, a ja kopiowałem-wklejałem historię, wstrzymując oddech, przez ponad 5 lat,
- ponadto istnieje lista modułów „ogrodzeniowych”, które potrzebują konkretnej względnej kolejności (jak kalibracja koloru absolutnie musi iść po wejściowym profilu koloru), ale nie ma sposobu, aby to wymusić, nie ma sposobu, aby to naprawić, gdy zdarzają się złe kolejności, jedynie ciche komunikaty o błędach w konsoli.
Połącz to z maskami rastrowymi, gdzie moduł ponownie używający maski rastrowej absolutnie musi znajdować się dalej w potoku niż moduł, który ją wytwarza, i gdzie, swoją drogą, możesz chcieć skopiować producenta wraz z konsumentem, albo przynajmniej dostać ostrzeżenie, jeśli tego nie zrobisz… i masz przepis na szaleństwo.
W praktyce będziesz musiał otworzyć każde zdjęcie i sprawdzić stos modułów w ciemni, co jest boleśnie powolne.
Ponieważ Darktable jest zdefiniowany przez złe zarządzanie priorytetami, mnóstwo kosmetyki otrzymało mnóstwo pracy: pozwoli Ci edytować zdjęcia za pomocą padów PlayStation, ma zaraz dostać funkcje maskowania oparte na głębokim uczeniu, ale nadal udaje mu się spieprzyć podstawowe podstawy, bez żadnej poprawy przez 6 lat pomimo pozornej aktywności w projekcie.
Problem (w skrócie)
Wcześniej wklejanie historii próbowało scalać moduły jeden po drugim w miarę postępu. Działało to w prostych przypadkach, ale stawało się zawodne, gdy:
- zdjęcie źródłowe miało inną kolejność potoku,
- istniało wiele instancji tego samego modułu,
- lub w grę wchodziły maski i mieszanie.
Ponieważ scalanie nie rozwiązywało całego problemu kolejności, wynik mógł zależeć od wielu rzeczy. W niektórych przypadkach ostateczny potok mógł skończyć niespójny ze stosem historii.
Poprawka
Zaimplementowałem algorytm sortowania topologicznego w C. A dokładniej, ChatGPT to zrobił, a ja to sprawdziłem (więcej o tym poniżej). 304 linie kodu, co jak na C jest bardzo niewiele.
Teraz traktujemy kolejność potoku jako zbiór ograniczeń, który musi zostać rozwiązany globalnie, a nie moduł po module. W praktyce oznacza to:
- Najpierw rozwiązujemy topologię potoku, a historię wywoływania (parametry modułów) na końcu. Dwa jasne kroki, które umożliwiają obsługę błędów.
- Scalanie oblicza jedną, prawidłową kolejność potoku, która spełnia zarówno źródło, jak i cel, kiedy tylko jest to możliwe, bez względu na to, ile jest instancji modułów i jak są uporządkowane.
- Jeśli ograniczenia są niekompatybilne, Ansel potrafi wyjaśnić konflikt i zapytać, którą kolejność zachować.
- Jeśli konflikt jest nierozwiązywalny, Ansel zgłasza to jasno zamiast produkować zepsuty lub niestabilny potok i daje Ci opcje, aby go naprawić.
- Ograniczenia są nakładane również na moduły używające i konsumujące maski rastrowe, a ostrzeżenie jest wystawiane, jeśli użytkownik maski rastrowej zostaje skopiowany bez producenta.
- Ograniczenia zakodowane na sztywno są nakładane na moduły, które wymagają siebie nawzajem przed sobą, ze względów technicznych (rekonstrukcja świateł przed demozaikowaniem, wejściowy profil koloru przed kalibracją koloru itd.). Te ograniczenia są obsługiwane tak samo jak inne i rozwiązywane razem (bez przypadku szczególnego).
Co do drugiej części, historii we właściwym znaczeniu, jedną rzecz trzeba tu wyjaśnić. Historia Darktable i Ansela przypomina bardzo listę cofnij/ponów migawek parametrów modułów: każdy element historii jest powiązany z modułem i reprezentuje jego wewnętrzny stan parametrów i masek. Wczytując historię do węzłów potoku (węzły będące filtrami pikseli przypiętymi do „modułów”, które widzisz w GUI), czytamy historię od dołu do góry i kopiujemy każdy element/migawkę do modułów, co oznacza, że późniejsze migawki zawsze nadpisują wcześniejsze.
To znaczy, historia jest uporządkowana według czasu modyfikacji przez użytkownika (znów… pomyśl o liście cofnij/ponów migawek), a nie według kolejności węzłów potoku. Ale tam, gdzie robi się to mylące, jest to, że elementy historii przechowują też pozycję modułu w potoku, co oznacza, że zmiana kolejności modułów pozostawia elementy historii. To myliło deweloperów Darktable, a znacznie bardziej użytkowników. Rozdzielmy więc oba pojęciowo, zarówno w naszych umysłach, jak i w oprogramowaniu.
Topologia (kolejność węzłów potoku) jest niezależna od historii. W przeciwieństwie do Darktable, które zarządza wszystkim poprzez elementy historii (w tym kolejnością potoku), co jest aktywnie szkodliwe zarówno pod względem zrozumienia, jak i złożoności kodu (te i tak idą w parze), my rozwiązujemy potok jako samodzielny zbiór węzłów (modułów), a następnie ponownie dopasowujemy węzły do ich ostatniego etapu historii. Zajęło mi kilka lat, aby przejrzeć całe przytłaczające zaciemnienie panujące w tym oprogramowaniu, pogrzebane w skopiowanym kodzie, i ujrzeć światło: po wyabstrahowaniu problem jest całkiem łatwy.
Tak więc, gdy topologia jest już rozwiązana, pozostają nam 3 tryby scalania historii:
- replace: historia źródłowa zastępuje cały cel, obowiązkowe moduły jak demozaikowanie dla zdjęć RAW mogą nadal zostać dodane na wierzch (tak że kopiowanie-wklejanie jest bezpieczne między JPEG a RAW). Ten tryb nie prowadzi do żadnego sortowania topologicznego, jest to bezpośrednia kopia historii i kolejności potoku.
- append: historia źródłowa idzie na wierzch celu, więc elementy historii, które celują w te same moduły w źródle i celu, zostają nadpisane przez źródło,
- appstart: historia źródłowa idzie na spód celu, więc elementy historii, które celują w te same moduły w źródle i celu, zostają nadpisane przez cel.
Kolejność potoku rozwiązana przez sortowanie topologiczne jest aktualizowana w ostatnich elementach historii, zarówno w trybie append, jak i appstart, co oznacza, że cofnięcie się w historii cofnie również sortowanie topologiczne. Historie celowo nie są kompresowane podczas scalania, aby użytkownicy zachowali możliwość cofnięcia scalenia zarówno za pomocą funkcji cofnij/ponów, jak i przez cofnięcie się w przyborniku historii, w ciemni, przed punktem scalenia.
Krótko mówiąc: wklejanie historii jest teraz deterministyczne, bezpieczne, nieniszczące, nawet w przypadku złożonych edycji.
Jak używać
Będzie jedna historia źródłowa (którą kopiujesz) i jedna historia docelowa (do której wklejasz). To samo będzie stosowane ze stylami, gdy zostaną ponownie zaimplementowane; historia źródłowa będzie zdefiniowana przez styl zamiast przez inne zdjęcie, a reszta będzie taka sama.
W menu globalnym Edytuj → Tryb wklejania historii możesz wybrać spośród append, appstart lub replace. Ustawienie jest globalne w całej aplikacji. Określa ono, która historia (źródłowa czy docelowa) ma pierwszeństwo poprzez nadpisywanie wspólnych modułów.
W Edytuj → Tryb wklejania węzłów możesz włączyć/wyłączyć Kopiuj kolejność modułów. Jeśli wyłączone, kolejność potoku celu jest zachowywana bez zmian. Jeśli włączone, staramy się jak najlepiej zaimportować kolejność potoku źródłowego do celu.
Jak wcześniej, w menu Edytuj masz opcje skopiowania/wklejenia wszystkiego lub tylko wybranych modułów (poprzez okno modalne). Dostępne są skróty globalne, edytowalne przez użytkownika. Zauważ, że kopiowanie-wklejanie historii jest wyraźnie zabronione w widoku ciemni, nawet z taśmy filmowej, ponieważ dwuznaczne jest ustalenie, czy chcesz kopiować między miniaturami, z miniatury do głównego zdjęcia, czy odwrotnie. Na stole podświetlanym wybierasz źródło, kopiujesz, wybierasz cel, wklejasz i wszystko jest jasne.
Otóż jest ważne założenie, które trzeba mieć na uwadze: moduły, które mają tę samą nazwę instancji (domyślnie numer instancji lub nazwę zdefiniowaną przez użytkownika), są uważane za tę samą jednostkę w historii docelowej i źródłowej. Tak więc każda Ekspozycja (niebo) zostanie scalona z każdym innym modułem Ekspozycja (niebo) (z rozróżnieniem wielkości liter), i powinna istnieć tylko jedna instancja Ekspozycja (niebo) w historii docelowej i źródłowej. Wcześniej kod używał numerów instancji, co jest bardziej kruche, ponieważ są one narzucane przez oprogramowanie i inkrementowane w kolejności tworzenia, co nie ma znaczenia dla użytkowników.
GUI i obsługa błędów
Piękno nowego rozwiązania polega na tym, że nie musisz otwierać ciemni, aby zobaczyć bałagan, który stworzyłeś, kopiując-wklejając śmieci; możesz go przejrzeć, zanim jakakolwiek szkoda zostanie wyrządzona Twoim edycjom, na stole podświetlanym. Ponadto, gdy solwer nie zdoła znaleźć rozwiązania, co zdarza się przy niekompatybilnych ograniczeniach (A powinno być poprzedzone przez B, ale B powinno być poprzedzone przez A) lub cyklach (więcej poniżej), potrafi powiedzieć, co zawodzi, zgłosić to i albo poprosić użytkownika o wprowadzenie danych, aby to naprawić, albo powrócić do najrozsądniejszej ścieżki. Pokażę Ci:
Cykle trywialne
Ekspozycja 1 jest przed Ekspozycją w historii źródłowej, ale po niej w historii docelowej. Zbiór ograniczeń kończy się na Ekspozycja → Ekspozycja 1 → Ekspozycja, co jest niewykonalne. Oto co dzieje się w Anselu:

Te trywialne cykle obejmujące bezpośrednich sąsiadów są wychwytywane przed rozwiązywaniem, więc nie przerywają przepływu sterowania.
Cykle nietrywialne
Te nietrywialne cykle obejmują kilka modułów i nie mogą zostać wykryte przed próbą rozwiązania grafu skierowanego. Gdy to się dzieje:

W tym przypadku nie ma nic do zrobienia: automatycznie ponowimy próbę, używając kolejności docelowej, ponieważ zdarza się to zazwyczaj przy próbie scalenia kolejności źródłowej z docelową.
Zapomniane maski rastrowe
Każdy moduł, który używa maski rastrowej, powinien zostać skopiowany wraz ze swoim modułem wytwarzającym maskę, chyba że planujesz rozwiązać to sam później. Na wypadek, gdyby to była pomyłka, jeśli spróbujesz tego:

Masz szansę przerwać scalanie teraz, jeśli nie tego chciałeś.
GUI raportu scalania (nowe)
Podstawy
Okno dialogowe raportu jest zaprojektowane, aby odpowiedzieć na proste pytanie użytkownika: „co dokładnie stało się z moim potokiem?”
Pokazuje cztery potoki obok siebie:
- Oryginalny (potok docelowy przed scaleniem),
- Źródłowy (zdjęcie, z którego kopiowałeś),
- Nadpisanie (gdzie edycje źródłowe zastąpiły edycje docelowe),
- Docelowy (ostateczny potok po scaleniu).

Każda kolumna wymienia aktywne instancje modułów, w kolejności GUI. Ten widok zawiera dodatkowe znaczniki:
- Nawiasy
[nazwa]wskazują moduły, które zostały nowo wstawione. - Gwiazdka
*wskazuje moduły, które używają masek. - Pogrubiona etykieta wskazuje moduły, których względna pozycja zmieniła się między źródłem a celem.
- Strzałki nadpisania pokazują, gdzie historia źródłowa faktycznie zastąpiła edycje docelowe (z
→*, gdy maski też zostały nadpisane).
Bajer
Kolumnę docelową można przestawiać metodą przeciągnij i upuść, co oznacza, że jeśli nie jesteś zadowolony z wyniku sortowania topologicznego, możesz naprawić go sam już teraz, zanim jeszcze zostanie zapisany do Twojej bazy danych i XMP, i bez konieczności otwierania ciemni. Pozwala Ci to dostosować ostateczny potok ręcznie przed jego zaakceptowaniem albo cofnąć wszystko i nie zapisywać historii z powrotem.
Gdy zmieniasz kolejność:
- kolejność potoku jest aktualizowana natychmiast,
- wpisy historii są utrzymywane w spójności z nową kolejnością,
- a widok raportu aktualizuje odpowiednio swoje etykiety i znaczniki „przesunięto”.
Jest to pomyślane jako zawór bezpieczeństwa: nawet jeśli obliczona kolejność jest prawidłowa, nadal masz prosty sposób, aby ją dostroić.
Dlaczego to ma znaczenie
To bezpośrednio poprawia procesy pracy obejmujące edycję wsadową ze złożonymi potokami:
- kopiowanie edycji między zdjęciami,
- mieszanie źródeł/celów RAW i JPEG,
- oraz ciężkie edycje wieloinstancyjne lub oparte na maskach.
Celem jest uczynienie wklejania historii przewidywalnym, nawet gdy potoki bazowe się różnią. Ta niezawodność jest szczególnie ważna przy zaawansowanych edycjach, gdzie małe różnice w kolejności mogą zmienić wyniki.
Ta zmiana nie dodaje krzykliwych nowych funkcji — czyni jedną z najczęściej używanych funkcji godną zaufania. Scalanie historii zachowuje się teraz tak, jak oczekują użytkownicy: spójne wyniki, jasne raportowanie i bezpieczne rozwiązania awaryjne, gdy ograniczenia się kłócą.
I nie rozumiem, dlaczego 6 lat później faceci z ekipy It Works For Me®, rozbijający Darktable w zwolnionym tempie, nie rozważyli ulepszenia takiej podstawowej, a jednak krytycznej funkcji. Jeśli to nie krzyczy o złych priorytetach, to nie wiem, co będzie.
Co to umożliwiło
Chcę tu podkreślić, że całe to przepisanie stało się możliwe, ponieważ najpierw przepisałem niemal całkowicie backend obsługi historii w Anselu, bo był to bałagan:
- Były wszędzie zduplikowane funkcje, które wykonywały tę samą operację wiele razy, ale ukryte w funkcjach wywołujących/wywoływanych po całym oprogramowaniu, niektóre wywołujące operacje wejścia/wyjścia systemu plików (zapis XMP) bez powodu, jedna zapisująca historię z powrotem za każdym razem, gdy otwieraliśmy ciemnię (co psuło znacznik czasu ostatniej zmiany),
- Było kilka splecionych blokad wątków, które w zasadzie czyniły jakąkolwiek zmianę niemożliwą bez zakleszczania rzeczy,
- Był kod pobierania historii z SQLite3 wpleciony w kod C, wiele zduplikowanych zapytań SQL, żadne z nich niebezpieczne dla wątków (ponieważ sam SQLite3 nie jest bezpieczny dla wątków), więc pogrzebałem cały kod SQL wewnątrz interfejsu C, który obsługuje bezpieczeństwo wątków centralnie, i teraz cały kod C pobiera informacje o historii z bazy danych biblioteki za pomocą jednego API, co oznacza, że wiemy, że wszystko, co czyta historię, będzie ją czytać tak samo wszędzie w aplikacji,
- Niektóre części czytania, inicjalizacji i scalania historii były robione w SQL (wykorzystując instrukcje
JOIN, co ma sens, ale…), a niektóre inne były robione w C (ponieważ kontrole bezpieczeństwa modułów i inicjalizacja ustawień wstępnych to oczywiście C). To prowadziło do głupich rzeczy, jak ręczne ponowne indeksowanie elementów historii w C przed zapisem do bazy danych między przejściowymi zapisami (ponieważ SQLite3 nie gwarantuje, że elementy historii zostaną zapisane w bazie danych w tej samej kolejności, w jakiej zostały przekazane… od tego są klucze główne). Więc przepisałem całość w C, co może być nieco wolniejsze, ale zapewnia spójność danych: historie są obsługiwane dokładnie tak samo, czy wczytujemy je, aby je scalić, otwieramy ciemnię, czy eksportujemy zdjęcie. Jeśli gdzieś jest błąd, będzie wszędzie i znajdziemy go szybciej, plus naprawimy go tylko w jednym miejscu. - Kod zarządzania historią był też spleciony z kodem GUI, ale może działać również z
ansel-cli(bez GUI), co prowadziło do wielu heurystyk sprawdzających, czy mamy GUI, czy nie, w wielu miejscach.
Tak więc, gdy cała ta robota woźnego została wykonana, zacząłem dostrzegać strukturę tego, co faktycznie było robione i co trzeba było zrobić. Stamtąd jedno uproszczenie prowadziło do kolejnego, aż ChatGPT 5.2 Codex zrobił resztę. Przed 2 tygodniami to było nadal robione całkowicie ręcznie i doprowadzało mnie wielokrotnie do szaleństwa. To naprawdę tylko farba trzymała te ściany, złuszczona farba, a próba jej wyczyszczenia niszczyła wiele rzeczy, ponieważ nic w tym oprogramowaniu nie było modularne (czyli zamknięte). Coś, co zmienisz w jednym miejscu, ma nieoczekiwane konsekwencje gdzie indziej, właśnie dlatego mamy enkapsulację, modularność i wzorce projektowe, bo język programowania C nie został zaprojektowany do złożonych aplikacji desktopowych takich jak ta, i naprawdę potrzebuje dyscypliny dewelopera, aby uniknąć stania się koszmarem, którym jest.
To jest w całości vibecoded
Tak więc czyszczenie historii trwało od 2023 roku, przy radzeniu sobie z wypaleniem i depresją wywołaną oprogramowaniem. To gówniana jakość życia, nie masz pojęcia. Ci, którzy myślą, że przesadzam, nie wiedzą, co pociąga za sobą przerzucanie cudzych mózgowych odchodów przez ponad 3 lata. Bo znałem czasy, gdy to wszystko było, jeśli nie lepsze, to przynajmniej mniej skomplikowane i bardziej znośne. Aż uderzyło szaleństwo COVID-19, a idioci dostali za dużo wolnego czasu w swoich rękach, którego użyli, aby zniszczyć coś, co z grubsza działało.
A potem dowiedziałem się o ChatGPT 5.2 Codex 2 tygodnie temu i zainstalowałem go w edytorze VS Code. Zajęło mi więc 3 dni pracy, aby zrobić całą tę rzecz, którą tu przedstawiłem. Bez ChatGPT byłyby to solidne 3 tygodnie, plus niekończące się dłubanie w drobiazgach GTK. Porozmawiajmy o tym doświadczeniu.
Nie zgadzam się z tymi, którzy próbują wmówić nam, że GenAI to tylko narzędzie. Narzędzie działa tylko w mojej ręce. Nie wtedy, gdy śpię. Sprawiłem, że ChatGPT pracował dla mnie, gdy gotowałem obiad (tak, jest aż tak powolny). Nie komunikujesz się z narzędziem, po prostu używasz go najlepiej, jak potrafisz. W razie niepowodzenia, cóż, niektórzy obwiniają narzędzie, ale wszyscy wiemy, co to znaczy. Problem w tym, że ChatGPT nie ma przycisków ani suwaków, on interpretuje to, co mu mówisz, i niekoniecznie tak, jak masz to na myśli. A poza tym narzędzie nie przejmuje inicjatywy. Cóż, ChatGPT z pewnością ma opinię na temat tego, jak kod powinien wyglądać, i czasami trzeba z nim walczyć.
GenAI to stażysta. Stażysta nie ma doświadczenia i zna tylko to, czego uczą w szkole. Stażysta może przynieść świeże nowe pomysły, które kwestionują Twoje nawyki, i majaczące sugestie tak samo, które ani trochę nie są istotne dla Twojego kontekstu, a czasami nawet niewykonalne. Ale stażysta musi pracować pod ścisłym nadzorem i musi dostawać jasne, jednoznaczne instrukcje. ChatGPT jest o wiele bardziej stażystą niż narzędziem.
ChatGPT robi mnóstwo błędów i są one podstępne, ponieważ są pogrzebane w środku doskonale prawidłowych rzeczy. Ma pewne dziwne obsesje (jak sprawdzanie na NULL każdego wskaźnika, o którym już wiemy, że nie może być NULL). Więc naprawdę trzeba go pilnować. Choć przeglądanie i naprawianie jego błędów jest nadal szybsze niż napisanie całego kodu samemu, nie wspominając o tym, że mój pierwszy problem z zespołem cieśni nadgarstka był 10 lat temu, więc to zawsze o tyle mniej do wpisania. Plus, robi błędy w logice, ale żadnych literówek, a przynajmniej znacznie mniej niż ja sam.
Ale tam, gdzie ChatGPT Codex błyszczy, to w 2 rzeczach.
Po pierwsze, żmudna gra w grepowanie funkcji po całym kodzie, aby dowiedzieć się (odtworzyć metodą inżynierii wstecznej) cyklu życia danych i sprawdzić wszystkie miejsca wywołań, aby zbudować mentalny model tego, co się dzieje. To zajmuje wieki, jest bardzo obciążające poznawczo, zwłaszcza w tak gównianym kodzie. ChatGPT działa cuda, przemierzając dziesiątki plików, wyodrębniając wzorce, znajdując, co można by sfaktoryzować, i śledząc sekwencje wykonywania. Powiedzmy sobie bardzo jasno, że w dobrze utrzymanym kodzie nie powinno to być potrzebne, ponieważ kod byłby samozamknięty w modułach, odizolowany od reszty. Ale ChatGPT bardzo pomógł uczynić rzeczy bardziej modularnymi.
Po drugie, wszystko, co dotyczy GTK i GLib. Są one słabo udokumentowane w sieci, a wiele idiomatycznych wzorców interakcji jest znanych tylko deweloperom GTK. ChatGPT najwyraźniej wchłonął mnóstwo kodu open source i potrafi produkować znacznie lepszy szablonowy kod GUI niż ja mógłbym (lub chciałbym). W każdym razie, przed ChatGPT zamieniało się to w żmudne sesje googlowania informacji, a nie mogę znaleźć żadnych istotnych informacji technicznych w Google od jakiegoś 2020 roku, gdy zmienili swoje algorytmy, aby agresywnie kwestionować wszystko. Ale ja pracuję, aby rozwiązywać problemy, a wszystkie szablonowe funkcje GUI inicjujące widżety i ich właściwości w stylu deklaratywnym nie są godne mojej inteligencji, to tylko próba nieprowadzenia do literówek.
Ale żeby dać lepsze wyobrażenie, oto rodzaj promptów, które musiałem mu dawać, aby zbudować to, co właśnie przedstawiłem:
now, in _hm_try_merge_iop_order_topologically(), build early in the function a GHashtable of all modules IDs tied to mod_list, then to dev_src->iop, then to dev_dest->iop. These will be useful to compute intersection of sets later. don’t modify dev_dest->iop_order_list. For all item in the sorted list (item being a node ID tied to a module op and multi_name) :
- find out if a corresponding module instance exists in dev_dest->iop, if not create it. Because dev_dest->iop is already inited and sanitized upstream, we can safely assume that every module not found should be inserted as a new instance. If the module instance ID is found in the input mod_list, the whole content of the module (parameters, blendop, etc.) should be copied between the source instance to the destination instance. Mind the deep copies that need to happen.
- overwrite all module->iop_order values with the new index number we just found by solving
- rebuild dev_dest->iop_order_list from scratch and update the module->multi_priority accordingly
now, in dt_history_merge_module_list_into_image_advanced, the temp history needs to be built as follow:
- deimplement the force_new_modules path for now, we will come back to it later and differently,
- build a temporary history as follow: for each module in mod_list:
- get the associated history item from dev_src->history (that would be the last one matching this module on the history stack),
- get the pipeline ordering info (iop_order, instance, multi_priority) from the corresponding module in dev_dest->iop
- update the existing history item from dev_src->history with pipeline ordering, since it may have changed after the topological sort, from the original history item,
- add this history entry to the temporary history
- concatenate the temporary history with dev_dest->history, first or last depending of append or appstart mode.
Try to use methods from history.c and dev_history.c as much as possible for the history to/from module handling. Extend the existing ones if you only need minor changes.
No, revert that. It’s not ok to delete history entries past the history_end in general. Whatever is in dev->history should go into the DB. Also, it’s not a problem because the history_end is also saved into DB. The problem here is that random history items are added when reading back the history from DB. Everything up to writing history, which happened in C, was ok. Find out why we get extra history entries when reading back from DB, compared to what we have at writing time before.
at the end of dt_history_merge in history_merge.c , I want you to show a report popup window. A text label will first tell “Copy, merging pipeline in {MERGE_MODE} and history in {STRATEGY} mode”, where {MERGE_MODE} depends on merge_iop_order (merge or destination), and {STRATEGY} depends on strategy. Then I want a GtkTreeView in list mode, with 3 columns:
- the source of the copy, with image ID and filename (not the full path),
- the override,
- the destination of the copy, with image ID and filename.
In columns 1. and 3., each row will show the module instances, starting with their pipeline order, module->name and module->multi_name. Only enabled modules will show. The column 2 will draw an arrow between source and destination instances when the source history overrides the destination history. This is done by checking, in the destination history, if the last entry targeting this module matches destination history or source. In case it matches both, show nothing since it’s not an override. The pipeline nodes will be shown in reverse order to match GUI ordering, since it’s a kind of layer stack. They should both be aligned on the bottom so the early steps have a chance to be on the same row until topology diverges between both pipes
Jedną rzeczą, którą odkryłem, jest to, że można zdecydowanie być zbyt szczegółowym z ChatGPT i doprowadzić go do ściany. Gdy to się dzieje, najlepszym sposobem działania jest przejęcie kontroli ręcznie.
Koszt energetyczny tego czegoś jest nie do zniesienia, ale powiedzmy, podzielony przez jakichś 900 gości, którzy dali Anselowi gwiazdkę na Githubie (nie mam statystyk pobrań), jest to dla większego dobra. To po prostu bardziej wydajny sposób oszczędzania mojego mózgowego soku, aby myśleć o tym, co powinno zostać zrobione (projekt i architektura), zamiast o tym, jak to zrobić. Zapewne nie tak, jak dzieciaki vibecodują w dzisiejszych czasach.
Następne: style.
Translated from English by : Claude. In case of conflict, inconsistency or error, the English version shall prevail.