26.07.2026 12:31

Moonlight Animate: druga tura po audycie — żeby praca nie znikała

⏱️ 7 min read
Share:

Jakiś czas temu przejechaliśmy Moonlight Animate całościowym przeglądem kodu — nie „czy coś się wywala", tylko: gdzie ta aplikacja może po cichu zjeść czyjąś pracę. Pierwsza tura zamknęła dwanaście rzeczy najgorszych. To jest tura druga, i szczerze — jest ciekawsza od pierwszej, bo prawie nic z tego nie objawiało się jako crash. Objawiało się jako „hm, dziwne" albo wcale.

Otwarcie pliku, które kasowało to, nad czym pracowałeś

Wczytywanie projektu .mart działało tak: najpierw wyczyść dokument docelowy, potem czytaj plik. Kolejność brzmi niewinnie, dopóki plik nie okaże się ucięty — a wtedy parser wychodził z błędem w jednym z około trzydziestu miejsc, zostawiając po sobie pusty dokument. Jeśli wczytywałeś do świeżej zakładki, nic się nie działo. Ale ta sama ścieżka obsługuje edycję smart-obiektów i współpracę — czyli sytuacje, w których po drugiej stronie jest żywy dokument z twoją pracą.

Teraz wczytywanie jest atomowe wobec celu: robimy migawkę przed startem i przy jakimkolwiek niepowodzeniu wracamy do stanu sprzed próby. Migawka jest tania, bo kafle są współdzielone — kosztuje wskaźniki, nie piksele.

Uszkodzony plik, który udawał zdrowy

Druga rzecz z tej samej rodziny, i bardziej podstępna. Kafel, którego nie dało się rozpakować, był po cichu pomijany. Plik otwierał się „poprawnie", z dziurami w obrazie. Ktoś mógł tego nie zauważyć od razu, nacisnąć Ctrl+S — i dziury nadpisywały nieuszkodzony oryginał. Awaria dysku zamieniała się w trwałą stratę przez jeden odruchowy skrót.

Teraz uszkodzenia są liczone i pokazywane, a plik traci powiązanie ze swoją ścieżką: dokument liczy się jako niezapisany, a najbliższy zapis otwiera „Zapisz jako". Odzyskujesz, co się dało, pod nową nazwą — oryginał zostaje nietknięty. Dokładnie ta sama zasada, którą stosuje odzyskiwanie po awarii.

„Scal w dół" nakładało efekty dwa razy

To był mój faworyt, bo wygląda jak drobiazg, a jest cichym niszczeniem obrazu. Scalanie wypala w piksele pięć rzeczy: maskę rastrową, maskę wektorową, styl warstwy, smart-filtry i przypięte wypaczenie punktu zbiegu. Po scaleniu czyściło dwie z nich. Pozostałe trzy zostawały aktywne na warstwie, która już je miała w pikselach — więc były nakładane ponownie. Rozmycie robiło się podwójne, maska podnosiła się do kwadratu i robiła ciemniejsza. I to się zapisywało.

Przy okazji wyszła rzecz gorsza. Scalanie nie sprawdzało w ogóle, jakiego typu są warstwy. Warstwa korekcyjna, referencer, graf węzłów — one nie mają własnych pikseli; render zwraca dla nich pusty bufor. Scalenie takiej warstwy nie „łączyło" niczego, tylko kasowało zawartość tej drugiej. Teraz polecenie po prostu odmawia, tak jak od zawsze odmawiało dla grup.

Historia, która kasowała się sama

Panel Historii ma budżet pamięci. Licznik tego budżetu odejmował inną liczbę niż dodawał — bo komenda potrafi zmienić swój rozmiar w trakcie życia. Przy odpowiedniej sekwencji cofnięć licznik wpadał w niedomiar i stawał się astronomiczny, po czym mechanizm przycinania uznawał, że jest wiecznie ponad budżetem, i zostawiał dokładnie jeden krok cofania. Do końca sesji.

Nie załatałem objawu. Usunąłem asymetrię: rejestr kosztów jest teraz prowadzony równolegle, więc odejmujemy zawsze dokładnie tę wartość, którą wcześniej dodaliśmy.

Drugi problem tego samego panelu: kafle współdzielone (a takie są wszystkie migawki, dopóki nie zaczniesz malować po nich na nowo) liczyły się jako świeża pamięć u każdego posiadacza. Dwie operacje na całym dokumencie potrafiły „przekroczyć" gigabajtowy budżet, choć realne zużycie nie drgnęło ani o bajt — i historia znikała cała. Teraz każdy posiadacz płaci swój udział.

Migawki, które trafiały do cudzego dokumentu

Panel Historii pozwala robić nazwane migawki i wracać do nich. Lista tych migawek nie czyściła się przy przełączeniu zakładki. Czyli: robisz migawkę w dokumencie A, przechodzisz do B, klikasz „Powrót do migawki" — i do B wjeżdżają warstwy, rozmiar płótna i tryb koloru z A.

Migawki są teraz trzymane osobno dla każdego dokumentu, więc przełączanie kart je przełącza zamiast mieszać, a powrót na kartę przywraca je razem z zaznaczeniem.

Rzeczy, które po prostu jadły pamięć

Domyślny tryb Galerii rozmyć budował dziesięć pełnych kopii płótna naraz. Przy 6000×4000 to około 4,6 GB w szczycie — i całość liczona od nowa przy każdym ruchu suwaka. Teraz poziomy są przetwarzane strumieniowo, jeden naraz, a te, których nikt nie użył, w ogóle nie powstają: 1,66 GB, przy wyniku identycznym co do bitu (sprawdzonym na pięciu konfiguracjach pinezek).

Import PSD materializował każdą maskę warstwy na całym płótnie dokumentu, zamiast w jej własnym prostokącie — około 565 MB na maskę przy dużym dokumencie, plus dziesiątki milionów niepotrzebnych operacji. Teraz maska zajmuje tyle, ile maska.

Eksport animacji klatka po klatce wysyłał teksturę na kartę graficzną dla każdej klatki, ale mechanizm zwalniania działał wyłącznie podczas normalnego rysowania w oknie — czyli w ogóle nie podczas eksportu. Przy 240 klatkach 1080p to jakieś dwa gigabajty VRAM w jedną stronę.

I mój ulubiony drobiazg: cache tekstur klatek animacji nigdy nie trafiał. Klucz był budowany między innymi z identyfikatora obrazu, który tworzyliśmy tuż obok — więc z definicji był za każdym razem inny. Cache istniał, kosztował, i był w stu procentach bezużyteczny. Teraz klucz bierze się z rewizji kafli, czyli z tego, co faktycznie decyduje o zawartości.

Bezpieczeństwo

Kilka rzeczy z gatunku „to nie zaboli, dopóki ktoś nie spróbuje":

Adres pliku assetu potrafi przyjechać w danych — na przykład w udostępnionej scenie, czyli od innego użytkownika. Do tego adresu doklejaliśmy token konta. Token nie opuszcza już naszego serwera: ani przez adres podany w danych, ani przez przekierowanie.

Materiał pędzla z przesyłki używał nadanego przez nadawcę identyfikatora wprost jako nazwy pliku. Nazwa mogła zawierać przejście do katalogu wyżej. Teraz nie może — i to samo dotyczy pola, przez które dało się skłonić aplikację do odczytania dowolnego pliku z dysku.

Rozpakowywanie danych przychodzących ze współpracy alokowało pamięć na podstawie rozmiaru zadeklarowanego w tych danych. Kilkadziesiąt bajtów mogło zamówić kilka gigabajtów. Rozmiar jest teraz sprawdzany zanim cokolwiek zaalokujemy.

Wroga scena gry mogła wstrzyknąć nieskończoności do silnika fizyki — a taka wartość nie psuje jednego obiektu, tylko cały świat. Przy okazji wyszła rzecz, która wygląda jak zabezpieczenie, a nim nie jest: standardowe „przytnij do zakresu" przepuszcza NaN bez zmian, bo każde porównanie z NaN jest fałszywe. Kilka miejsc w kodzie wyglądało na zabezpieczone i nie było.

Co to daje

Nic z tego nie jest nową funkcją. Cała ta tura to czterdzieści plików zmian, po których program robi dokładnie to samo co wcześniej — tyle że nie gubi pracy, nie kasuje historii bez powodu i nie zajmuje trzy razy więcej pamięci, niż potrzebuje.

Uważam, że to jest ważniejsze od nowej funkcji. Narzędzie, któremu nie ufasz na tyle, żeby zostawić w nim dzień roboty, nie jest narzędziem — jest zabawką. A zaufanie buduje się właśnie tak: szukając rzeczy, które psują się cicho, zanim znajdzie je ktoś, kto stracił przez nie plik.

Kolejna tura obejmuje zawieszenia i resztę zgłoszeń z przeglądu. Lista jest długa i nie mam zamiaru jej ukrywać.

📚 Related posts

27.08.2026 20:55

Data, której nie cofniemy nawet my

Każdy certyfikat autorstwa dostaje datę zapisaną w blockchainie Bitcoina — takiej nie podważy nikt, my też nie. Sprawdzisz ją bez konta i bez Moonligh...
Continue reading →
27.08.2026 19:35

Twój kolor, wszędzie

Trzy tryby i siedem akcentów, wspólnych dla strony i Moonlight Animate. Motyw jedzie z kontem, profil pokazuje gościom Twoje kolory, a harmonogram sam...
Continue reading →
27.08.2026 18:31

Kto moderuje i na jakiej podstawie

Moderator to teraz osobna rola z własnym zakresem, a nie administrator w przebraniu. Odrzucenie treści wymaga powodu, który trafia do autora razem z d...
Continue reading →

📧 Stay up to date!

Join the waitlist to get notified about new Dev Blog posts and Moonlight platform launch.