Podsumowanie poprzednich odcinków
- W latach 2020–2022 Darktable przeszedł przez przedsięwzięcie masowej destrukcji, za sprawą garstki gości mających więcej wolnego czasu i dobrej woli niż faktycznych umiejętności,
- W 2022 roku zacząłem zauważać irytujące opóźnienie między interakcjami w interfejsie z suwakami a informacją zwrotną/aktualizacją owych suwaków. Z braku informacji zwrotnej stwierdzającej, że zmiana wartości została zapisana, użytkownicy mogli zmienić ją ponownie, tym samym uruchamiając dodatkowe przeliczenia potoku i skutecznie zawieszając swój komputer, ponieważ głupi interfejs nigdy nie powiedział „mam cię, teraz chwilę poczekaj".
- Odkryłem, że rozkazy przeliczenia potoku były wydawane dwukrotnie na kliknięcie (raz przy zdarzeniu „przycisk wciśnięty", raz przy „przycisk zwolniony") oraz jeszcze raz przy każdym ruchu myszy, ale również, że stany interfejsu były pozornie aktualizowane po przeliczeniu potoku.
- Naprawiłem to, niemal przepisując od nowa niestandardowe kontrolki interfejsu (biblioteka Bauhaus). Sądziłem, że powstrzymanie lekkomyślnych rozkazów przeliczenia rozwiąże problem opóźnienia: tak się nie stało. Następnie odkryłem, że żądanie nowego przeliczenia potoku przed zakończeniem poprzedniego czekało na zakończenie poprzedniego, mimo mechanizmu wyłączania zaimplementowanego wiele lat temu, który powinien był działać.
- Naprawiłem to, implementując mechanizm wyłącznika awaryjnego (kill-switch) na potokach, kierując się komentarzami w kodzie z lat 2010. i wewnętrznymi narzędziami, które równie dobrze mogły nigdy nie działać. Nie zawsze to działało, ponieważ rozkaz zabicia przychodził często z zauważalnym opóźnieniem. Po raz kolejny opóźnienie interfejsu nie zostało naprawione.
…
Odcinek 5: spłacanie długu technicznego
To, co odkryłem, naprawdę powinno trafić do podręczników informatyki, do rozdziału o tym, czego nie robić, jeśli chce się napisać w miarę niezawodną aplikację.
Otóż, ilekroć w module zmieniano parametr przetwarzania obrazu, wysyłane było żądanie dodania nowego wpisu historii do bazy danych (często więcej niż raz na interakcję, jak pokazano wyżej). Wpisy historii to nic innego jak migawka wewnętrznych parametrów jednego modułu (w tym masek). Jeśli wykryto zmianę względem poprzedniego wpisu historii, flaga PIPELINE_STATE była ustawiana na wartość DIRTY, aby wskazać, że potok będzie potrzebował przeliczenia, a wysyłano gtk_widget_queue_draw(), które — jak sama nazwa wskazuje — prosi Gtk o przerysowanie głównego podglądu ciemni oraz miniatury nawigacyjnej, ale w sposób asynchroniczny (rozumieć: kiedy tylko znajdzie na to czas, po ukończeniu wszystkiego, co wcześniej uruchomiono). To będzie miało znaczenie później.
Zajęło mi bardzo dużo czasu ustalenie, jak tak naprawdę uruchamiany był potok, ponieważ żaden fragment kodu przypisany do modułów i potoku nie zawierał niczego mówiącego „idź to policz". Innymi słowy, żaden fragment kodu modułów nie zawierał jawnej instrukcji przeliczenia.
Musiałem odtworzyć (reverse-engineering) kod potoku od drugiego końca, szukając, jak potok mógłby zostać uruchomiony, i greppując każdą opcję, aż odkryłem rzecz niewypowiedzianą: pierwsze pokolenie deweloperów Darktable podłączyło funkcję zwrotną (callback) do zdarzenia redraw na głównym podglądzie ciemni i miniaturze nawigacyjnej, ale w kompletnie niepowiązanym miejscu w kodzie. W tym callbacku interfejsu sprawdzano wartość flagi PIPELINE_STATE i albo wysyłano bezpośrednio mapę pikselową z tylnego bufora do widżetu, jeśli flaga miała wartość VALID, albo żądano przeliczenia potoku, jeśli flaga miała wartość DIRTY, a to przeliczenie samo żądało gtk_widget_queue_draw() po zakończeniu.
Ta metoda ma jedną zaletę: to leniwe kodowanie. Poza tym ma cały ogrom wad i problemów:
- nie jest przyjazna deweloperom, zwłaszcza w projekcie oprogramowania, gdzie greppowanie kodu i komentarze to cała dokumentacja, o jakiej możemy marzyć. Zrozumienie tej logiki poprzez archeologię programu zajęło wiele godzin. Jeśli wydawany jest jakiś rozkaz, chcę czytać
command_issued()we właściwym miejscu w kodzie, bo C jest już wystarczająco trudne do śledzenia bez domieszki zagadek do debugowania. - skoro
gtk_widget_queue_draw()(wywoływane dwukrotnie w najgorszym scenariuszu) jest jedynie dodawane do kolejki i przetwarzane asynchronicznie, dodaje ono wszelkie opóźnienie, na jakie mogłoby cierpieć Gtk (podczas przetwarzania innych fragmentów interfejsu lub poprzednich klatek), zanim jakiekolwiek przeliczenie potoku w ogóle zostanie uruchomione, co jest zbędne, skoro potok żyje we własnym wątku równolegle, - wielki MIDI-owy turducken, nasłuchujący zdarzeń wskaźnika, klawiatury i MIDI, aby rozdysponowywać skróty, zdawał się przeciążać globalny interfejs nasłuchiwaczami zapętlającymi się po wszystkich znanych skrótach, co sprawiało, że Gtk się zacinało do stopnia, w którym stawało się to zauważalne,
- uniemożliwia, by jakikolwiek mechanizm wyłącznika awaryjnego był użyteczny — zarówno z powodu opóźnień, jak i dlatego, że odczyty flag były przeplatane z blokadami wątków (i sytuacjami wyścigu). Ponadto oczekiwanie na uzyskanie blokady wątku potoku (muteksu) zamrażało wątek interfejsu na odpowiadający temu czas, co było prawdopodobnie jedną z przyczyn opóźnienia suwaka przed aktualizacją jego położenia,
- połączone łańcuchowo wywołania callbacku zdarzenia
redraw, poprzezgtk_widget_queue_draw(), sprzyjały „niekończącym się" pętlom zacinania złożonym z (bezużytecznych) pośrednich przerysowań, które zdawały się dotykać ludzi z wolnymi komputerami bardziej niż tych z mocarnymi bestiami. Były one szczególnie trudne do odtworzenia, w zależności od wydajności sprzętu, więc można znaleźć fora, na których ludzie są przekonani, że Darktable to najwolniejsze oprogramowanie w historii, podczas gdy inni raportują znakomitą wydajność.
Naprawiłem więc całą logikę poprzez:
- uczynienie callbacku
redrawgłupim (rysującym bezwarunkowo dowolny dostępny bufor mapy pikselowej), - obsługę jawnych przeliczeń potoku w kodzie modułów i historii, przy czym przeliczenia potoku żądają przerysowania widżetu po zakończeniu potoku (tak, to więcej kodu i jest to żmudne, ale teraz można ręcznie optymalizować przeliczenia — wydajność ma znaczenie),
- usunięcie specjalnej obsługi „zduplikowanych" elementów historii (co prowadzi do pewnego zanieczyszczenia przy pracy z maskami, to trzeba będzie naprawić później).
Można by pomyśleć, że to problem rozwiązany i robota dobrze wykonana, ale to pomijając geniuszy Darktable w tym równaniu.
Widzisz, moduły kadrowania i perspektywy to moduły specjalne: ich otwarcie włącza „tryb edycji", który wyłącza wszelkie kadrowanie, aby pokazać pełny obraz. Jest to potrzebne, aby przeciągać ramkę kadrowania (lub dostosowywać inne ustawienia położenia) z głównego podglądu, nad pełnym oryginalnym obrazem. Problem w tym, że nie było jawnego sposobu na zażądanie przeliczenia potoku… innego niż dodanie nowego elementu historii. Moduły dodawały więc fałszywy element historii (później cofany) tylko po to, by unieważnić potok i wywołać funkcję gtk_widget_queue_draw(). Ale to zanieczyszczało stos historii „pustymi" krokami, więc inny gość dodał specjalny przypadek obsługi, który scalał kroki historii, jeśli nie zaszły żadne zmiany parametrów. Ale wtedy stos historii (z modułu historii, przechowywany w bazie danych) nie podąża za stosem historii cofnij/ponów, prowadząc użytkowników do nieporozumień co do tego, co cofnij/ponów tak naprawdę robi.
I oto, panie i panowie, jak fatalny projekt sprzyja jeszcze bardziej fatalnemu projektowi w niekończącym się rozroście szaleństwa.
Pamiętaj, że to wszystko wynika z potrzeby sprawienia, by wyłącznik awaryjny potoku działał, tak byś mógł przerwać przeliczenie w połowie, gdy wiesz, że jego wynik i tak zostanie odrzucony. W tym celu musiałem więc wyprowadzić żądanie przeliczenia poza kod Gtk i wywoływać je wszędzie, gdzie to wymagane. Ale wtedy musiałem przełączyć logikę aktualizacji potoku w modułach kadrowania, perspektywy i obracania, upłynniania oraz obramowań, a wciąż muszę naprawić retusz (który jest największym utrapieniem z całej stawki).
Poza uczynieniem tego czytelniejszym i umożliwieniem optymalizacji wywołań, obecna logika uruchamia też potok poza wątkiem interfejsu, bez czekania, aż Gtk łaskawie znajdzie czas na przerysowanie klatki. Jak zwykle, ludzie z szalonymi procesorami zauważą niewielką korzyść pod względem wydajności lub żadną, co jest zapewne powodem, dla którego dla zespołu Darktable to w ogóle nie jest problem.
Odcinek 6: spłacanie zaległych odsetek od długu technicznego
Otóż w tym momencie uczyniłem przeliczenia potoku jawnymi z poziomu modułów i kontrolek interfejsu oraz rozdysponowywałem je oszczędnie (co jest zaletą jawnego ich rozdysponowywania). A mimo to zauważyłem, że zabawa z modułami pojawiającymi się późno w potoku była powolna. W rzeczywistości uruchomienie ansel -d perf pokazało, że
cały potok, zaczynając od modułu demozaikowania, był przeliczany, mimo że wchodziłem w interakcję z późnym modułem, który pobierał swoje wejście z balansu kolorów.
Darktable od zawsze miał pamięć podręczną pikseli. Zasadniczo przechowuje ona pośrednie stany obrazu, pomiędzy modułami. Zatem to, że przeliczenia potoku zaczynały się dużo poniżej bieżącego modułu, oznaczało, że była ona w większości bezużyteczna. Okazało się, że pamięć podręczna używała tylko 8 linii pamięci podręcznej, co naprawdę słabo wykorzystuje dzisiejsze szalone ilości pamięci RAM. Ale zwiększenie tego do 64 nie pomogło z chybieniami pamięci podręcznej: pamięć podręczna wciąż była w większości bezużyteczna, a większa część potoku wciąż była przeliczana.
Musimy się tu na chwilę zatrzymać. Nawet inżynier mechanik bez porządnego wykształcenia programistycznego, taki jak ja, wie, czym jest pamięć podręczna LRU :
- tworzysz stałą listę slotów (linii pamięci podręcznej),
- gdy już masz coś do zapamiętania w pamięci podręcznej, przydzielasz bufor pamięci o uprzednio znanym rozmiarze do jednego z tych slotów i przypisujesz mu unikalny identyfikator. Może to być suma kontrolna, losowy hasz albo nawet znacznik czasu, musi być tylko zawsze przyrządzany tak samo i prowadzić do czegoś unikalnego,
- gdy potrzebujesz danych powiązanych z jakimś unikalnym identyfikatorem, przeszukujesz listę slotów i szukasz, czy ten identyfikator jest znany:
- jeśli jest, pobierasz powiązany z nim bufor,
- jeśli nie jest:
- jeśli wciąż masz puste sloty, tworzysz powiązany bufor i kopiujesz dane do późniejszego ponownego użycia,
- jeśli nie masz, czyścisz najstarszy slot i wykorzystujesz go ponownie, aby pomieścić swoje nowe dane.
W tym procesie musisz znać tylko rozmiar buforów i identyfikatory. Jest to bardzo ogólne, możesz zapamiętać w pamięci podręcznej cokolwiek, nawet różne obiekty, twoja pamięć podręczna nie musi być świadoma zawartości ani nawet tego, jak generowane są identyfikatory. Jest to czyste, eleganckie, bezpretensjonalne, generyczne, powierzyłbym temu swoje życie, bo jest to znacznie solidniejsze niż jakikolwiek system bezpieczeństwa, jaki znajdziesz w nowoczesnych samochodach.
Zatem gdy coś aż tak prostego nie działa, zwykle jest to dlatego, że ktoś spróbował czegoś „sprytnego" i mu się nie udało. To, co zespół Darktable zazwyczaj robi w takim przypadku, to przewalczenie się metodą switch case przez wszystkie patologiczne przypadki brzegowe i uczynienie z tego czegoś jeszcze bardziej skomplikowanego (poprzez ręczną obsługę wszystkich wyjątków za pomocą heurystyk), tylko po to, by zapewnić, że nikt później nie będzie miał szansy odnaleźć źródłowej przyczyny błędu.
Na przykład były próby przeważania priorytetu linii pamięci podręcznej, aby zapewnić, że moduł poprzedzający ten aktualnie edytowany w interfejsie znajdzie się w pamięci podręcznej. Nie dość, że to nie działało, to jeszcze wzmacniało powiązania między kodem potoku a kodem interfejsu, w sposób, który nie był nawet bezpieczny wątkowo (dlatego to nie działało). Sprawy interfejsu powinny dziać się na wejściu i na wyjściu obliczeń potoku, a nie pomiędzy, bo znowu, różne wątki, ale też narusza to zasadę modularności (utrzymuj warstwy programu rozdzielone i zamknięte na tyle, na ile to możliwe), a to oprogramowanie musi przestać sprawiać, że wszystko zależy od wszystkiego.
Znowu zajęło mi to 8 miesięcy, wliczając obowiązkowe przerwy od tego kompletnego cyrku, aby dotrzeć do sedna problemu w sposób prowadzący do upraszczającego rozwiązania. Przedstawię odkrycia w sposób liniowy, niczym opowieść, ale miej na uwadze, że zacząłem odkrywać rzeczy w sposób mętny i losowy, bo to wszystko jest porozrzucane w kodzie źródłowym, więc będzie to wyglądać mniej chaotycznie, niż było w rzeczywistości.
Zaczynamy od unikalnego identyfikatora. Co tak naprawdę reprezentuje stan modułu w unikalny sposób? Cóż, „kryptograficzna" suma kontrolna jego wewnętrznych parametrów. Fajnie, więc Darktable miał to zaimplementowane od dawna. Tyle że nie uwzględniało to numeru instancji modułu i operowało w tym procesie wszelkiego rodzaju if. Niekompletne, nierobustne, a nawet niepotrzebne. Zhaszuj wszystko, hasz będzie reprezentował stan zmiennych.
No dobrze, ale moduły można przestawiać, więc jak zajmujemy się kolejnością w potoku? Cóż, bierzesz wszystkie hasze wszystkich modułów, w kolejności potoku, i zaczynasz akumulować liniowo. Świetnie. Tyle że Darktable miał w rzeczywistości 2 takie, jeden do celów interfejsu, który zaczynał od końca potoku (a więc w odwrotnej kolejności), jeden do celów potoku, w kolejności potoku, ale niedostępny z interfejsu (na przykład… do uzyskania histogramu), i znowu, oba mieszały to z wszelkiego rodzaju sprawdzeniami do obsługi przypadków specjalnych (próbnik kolorów, podgląd maski itd.).
Nie wspominając już o tym, że wewnętrzny stan modułu nie różni się w zależności od tego, czy jesteś w pełnym podglądzie, czy w miniaturze nawigacyjnej, w ciemni. A mimo to suma kontrolna była w pełni przeliczana dwukrotnie, raz dla każdego potoku. Właściwie, uczyńmy to czterokrotnie, skoro jest też suma kontrolna interfejsu (używana głównie dla modułów perspektywy i retuszu)
I wreszcie, choć nie mniej ważne, przy powiększeniu w ciemni obliczana jest tylko widoczna część obrazu (Region Zainteresowania, czyli ROI), co oznacza, że w naszym mechanizmie buforowania musimy śledzić, gdzie w obrazie się znajdujemy. Ale to zostało kompletnie pominięte w sumie kontrolnej. Wielki błąd tutaj, i to stary.
Więc jak to się dzieje, pytasz, że Darktable mimo wszystko dawał radę „działać"?
Cóż, poprzez mniej lub bardziej całkowite opróżnianie pamięci podręcznej przy każdej patologicznej operacji: powiększenie, przesuwanie, podgląd maski, próbnik kolorów, włączanie/wyłączanie stanu edycji modułów kadrowania i perspektywy. To sposób radzenia sobie ze spójnością bez radzenia sobie ze spójnością: podpalić ją. Czyniący ją w większości bezużyteczną, jak pokazują bardzo niskie statystyki trafień pamięci podręcznej (wystarczy uruchomić ansel -d dev, aby je zobaczyć).
Jak rozwiązałem ten problem?
- Gdy dodawany jest nowy wpis historii modułu, obliczana jest suma kontrolna parametrów, uwzględniająca parametry, maski, opcje mieszania, numer instancji, kolejność w potoku itd. Oznacza to, że wszystkie potoki współdzielą tu tę samą sumę kontrolną/identyfikator (możliwym przyszłym zastosowaniem byłoby zapisanie jej do bazy danych),
- Przed obliczeniem potoku obliczamy globalną sumę kontrolną wszystkich modułów, od początku do końca, uwzględniając stan wyświetlania maski, sumę kontrolną poprzednich modułów oraz ROI (rozmiar i współrzędne). Do tej sumy kontrolnej można później uzyskać bezpośredni dostęp, bez dodatkowych obliczeń.
- Pamięć podręczna zajmuje się tą globalną sumą kontrolną, i tylko nią. Żadnych „jeśli", żadnych „ale", żadnych heurystyk, żadnych warunków, żadnych obejść.
- Moduły mogą zażądać ominięcia pamięci podręcznej, na przykład przy użyciu próbnika kolorów. To zanieczyszcza późniejsze moduły w potoku, zanim potok zostanie obliczony, więc stan bez pamięci podręcznej jest znany wcześnie i nie wpływa na wcześniejsze w potoku moduły. To powinno być jedynie obejściem, zanim próbniki kolorów będą mogły faktycznie używać linii pamięci podręcznej bezpośrednio, i mogłoby zostać ponownie wykorzystane dla przyszłych modułów robiących niestandardowe rzeczy (malowanie?).
Korzyści:
- Wewnętrzna suma kontrolna na poziomie modułu jest obliczana raz dla wszystkich potoków,
- Ponieważ globalna suma kontrolna każdego modułu na poziomie potoku jest znana przed rozpoczęciem przeliczania potoku:
- może ona zostać użyta również do synchronizacji interfejsu, więc scaliłem obie sumy kontrolne Darktable w jedną,
- jest ona stała w zakresie potoku, co pozwala współdzielić linie pamięci podręcznej między kilkoma potokami (na przykład demozaikowaniem i odszumianiem) z ograniczonymi problemami blokowania wątków1
- Moduły robiące dziwne rzeczy mają jednolity i przewidywalny sposób żądania ominięcia pamięci podręcznej ze zdarzeń interfejsu, gdyby tego potrzebowały.
Ta logika jest nie tylko wydajniejsza (mniej obliczeń), ale też prostsza i można ją rozszerzyć o interesujące funkcje. Z perspektywy pamięci podręcznej mamy do czynienia z niczym innym jak sumą kontrolną, każdy interesujący nas stan modułu jest w niej zwinięty (convolved).
Ale, co ważniejsze, pamięć podręczna jest wreszcie użyteczna, zwłaszcza podczas poruszania się tam i z powrotem w historii edycji, używania cofnij/ponów lub włączania/wyłączania modułów. Ogólna responsywność interfejsu jest znacznie lepsza.
Jestem pewien, że istnieją nieodkryte pułapki i szczegóły, które zapomniałem podłączyć na nowo do nowej logiki, a moduł retuszu wciąż jest w większości zepsuty, ale dostosowanie do czegoś aż tak prostego powinno być wykonalne.
Tymczasem w Darktable 4.8
- Suma kontrolna potoku jest obliczana w trakcie działania potoku, więc jest nieznana na zewnątrz,
- Z tego powodu nie zdeduplikowali sum kontrolnych interfejsu vs. potoku… powodzenia w śledzeniu niespójności między nimi w przyszłości,
- Ich kod obsługi pamięci podręcznej jest ponad dwa razy większy niż mój i używa heurystyk (dotyczących typu potoku, rodzaju modułu, stanu wyświetlania masek, użycia próbnika kolorów oraz wskazówek buforowania definiowanych ręcznie w modułach), aby obejść problemy. Pamięć podręczna nie jest już agnostyczna wobec zawartości i powodzenia w debugowaniu tego spaghetti.2
- Wciąż w całości obliczają (wewnętrzną) sumę kontrolną parametrów modułu dwukrotnie, raz dla każdego potoku,
- Zajęło im to prawie 2 lata, aby do tego dotrzeć (od wersji 4.0),
- Chętnie zobaczyłbym ich statystyki trafień/chybień pamięci podręcznej (czy chcę wskrzesić swój PTSD, otwierając kiedykolwiek to badziewie? Podziękuję).
- Ludzie, którzy uważają, że posiadanie większej liczby małp machających rękami w powietrzu gwarantuje lepszą jakość, powinni przestać myśleć.
Podsumowanie
Ilość spędzonego czasu i niedawno zepsutego badziewia do naprawienia, aby do tego dojść, była zwyczajnie nie do zniesienia, ale pogarszał to kod porozrzucany w niemodularny sposób bez wyraźnego rozróżnienia między tym, co należy do (G)UI, co należy do zaplecza (backend), co należy do historii modułów, a co należy do węzłów potoku. Sama sprawa pamięci podręcznej zajęła jedynie 8 miesięcy, głównie archeologii i inżynierii odwrotnej, na dodatek do tego, co już zostało zrobione w kontrolkach interfejsu i jawnych przeliczeniach potoku.
Wciąż są problemy do naprawienia:
- liczba dostępnych linii pamięci podręcznej jest preferencją użytkownika i nie sprawdza ilości pamięci pozostałej na urządzeniu,
- moduł histogramu/skopów jest w większości zepsuty z założenia, bo był obsługiwany poprzez specjalne heurystyki (obecnie usunięte) na module, który jest niewidoczny w interfejsie (
gamma.c). Nowa logika umożliwia wymuszenie buforowania go i pobranie linii pamięci podręcznej z wątku interfejsu. - wewnętrzny histogram modułu nie jest natychmiast rysowany po wejściu do ciemni,
- obsługa próbników kolorów mogłaby zostać uproszczona i uczyniona bardziej elegancką,
- obsługa historii wciąż ma pewne przypadki brzegowe.
Jednakże, skoro odmawiam „naprawiania" czegokolwiek, jeśli moja naprawa nie czyni rzeczy prostszymi, ta strategia zaczyna się opłacać, bo kod jest o wiele bardziej liniowy, z mniejszą liczbą przypadków do przetestowania, a ostatecznie nieco szybszy. W miarę jak robię postępy, powoli staje się bardziej czytelny i łatwiejszy do naprawienia. No i, oczywiście, wstrząsanie rdzeniem oprogramowania w takim stopniu musi psuć różne rzeczy (które nie powinny się psuć, gdyby kod był modularny).
Pojawia się tu uprawnione pytanie: po co zawracać sobie głowę naprawianiem brzydkiego dziedzictwa Ansel/Darktable, a nie przesiąść się na coś lepszego, szybszego i błyszczącego (jak Vkdt)? Cóż, Vkdt (lub cokolwiek innego nowego) pozostanie surowym prototypem, konkurującym z innymi surowymi prototypami (to Open Source w pigułce), oddalonym o lata od produktu nadającego się do powszechnego użytku. Dodanie kolejnego niedokończonego/zrobionego po łebkach prototypu do krajobrazu nie przyniesie nic dobrego. Miło byłoby mieć coś nie niechlujnego i w miarę skończonego, dla odmiany. Poza tym (bardzo) stary kod Darktable jest czysty i solidny (no, w przeważającej części), to dopiero ostatnie kilka lat przybrało obrót ku najgorszemu. git blame zawsze pokazuje te same 3 nazwiska przy naprawdę fatalnych liniach, do stopnia, w którym czasem łapię się na tym, że automatycznie usuwam odpowiadające im linie, gdy zobaczę, kto je napisał, z przyzwyczajenia.
Jest też obawa, że bez względu na to, jak szybkim Vulkan uczyni Vkdt, tym, co naprawdę czyni Darktable badziewnym, są złe decyzje, złe priorytety, błędy programistyczne, niewyciągnięte wnioski, i jeśli te błędy zostaną powtórzone na Vkdt, może zająć więcej czasu uświadomienie sobie konsekwencji przy większej mocy, ale ostatecznie sprawy potoczą się tak samo. Posiadanie większych zasobów sprawia, że bardziej stać cię na bycie głupim… aż do momentu, gdy już nie i uświadamiasz sobie, jak bardzo jesteś w pułapce.
Translated from English by : Claude. In case of conflict, inconsistency or error, the English version shall prevail.
The source code actually has a 10-years-old
TODOcomment detailing how to do that. ↩︎It should be noted that “my” cache code is actualy pretty much how Roman Lebedev and Johannes Hanika wrote it 10 years ago. I simplified a couple of things, mostly removing stuff added since then, and added nothing of my own, because it’s a Garbage In/Garbage Out situation where you should rather clean your input rather than trying to handle any corner case internally through unlegible heuristics. ↩︎