Lulucat

A Lulucat Notes renderelési folyamatának újjáépítése Kimi K3-mal

Gaoge ZhangGaoge Zhang

A Core Graphics csempés folyamatunk hat kis lépésben Metal point-sprite-folyamattá vált, mindegyiket valódi iPaden ellenőriztük. A kódot Kimi K3-mal páros programozásban írtuk a Fireworksön.

Frissítve

A múlt héten egy lasszós kijelölés húzása a Lulucat Notesban látható hullámot okozott: egyes képernyőcsempék a kijelölést az új helyén mutatták, mások viszont ugyanabban a képkockában még a régi állapotot. Az egész renderelési folyamatot — a Core Graphics bitképet és a CATiledLayer-t — Metalra cseréltük hat kis lépésben; mindegyiket valódi iPaden ellenőriztük, mielőtt a következő elkezdődött.

A kódot a Moonshot nyílt modelljével, a Kimi K3-mal, páros programozásban írtuk a Fireworksön. Az ember vezetett, döntött és tesztelt; a modell írta szinte minden sort.

A folyamat

A régi folyamatban kétféle rajzolási mód volt, amelyek lassan eltávolodtak egymástól: a vonásokat olcsó megjelenítéshez bitképbe sütöttük, majd vektorként újrarajzoltuk, amikor a csempéknek több részletre volt szükségük. Az új folyamatnak pontosan egyetlen ötlete van: minden tintaszerű dolog point sprite. Egy tollvonás, egy kiemelőhúzás és egy radírráütés ugyanaz a 32 bájtos vertex — pozíció, átmérő, szín —, amelyet ugyanaz a shaderpár a GPU által raszterizált, a vonás ívhossza mentén egy pontnyi távolságra elhelyezett körökként rajzol.

Ugyanannak a görbe vonásnak három panelje: bemeneti érintési pontok, ívhossz szerint a görbe mentén elhelyezett kör alakú lenyomatok és a kompozitált tömör vonás.

Egypontos távolságnál a körök lánca mintegy 0.075 ponttal tér el egy matematikailag tökéletes kapszulától — ez egy pixel ötöde a vásznunk sűrűségénél. Cserébe három eszköz egyetlen kódútvonallá olvad össze, a GPU pedig azt csinálja, amiben a legjobb.

Az ötlet köré épülő architektúra egyszerű. A véglegesített tinta egyetlen 4096²-es textúrában él. Az UIScrollView megmarad, de tiszta gesztusmotorrá fokoztuk le: a contentOffset és a zoomScale minden képkockában egy viewport-uniformot táplál, így a pásztázás és a nagyítás semmit sem ír. Minden képkocka öt rajzolásból áll:

flowchart TB
    subgraph frame["Every frame: five draws"]
        direction TB
        paper["1 · paper blit"] --> ink["2 · committed ink"]
        ink --> live["3 · live stroke"]
        live --> sel["4 · selection"]
        sel --> dash["5 · lasso dashes"]
    end
    commit["stroke commit<br/>append stamps"] --> tex[("ink texture<br/>4096² render target")]
    replay["regional replay<br/>erase · delete · move · undo"] --> tex
    tex -. "sampled or re-drawn" .-> ink

A módosítások közvetlenül a textúrába írnak. Egy vonás véglegesítése hozzáfűzi a lenyomatait. A radírozás, törlés, mozgatás és visszavonás egy scissor téglalappal újrajátssza az érintett régiót: törli a régiót, újrarajzolja a vele metsző vonásokat, és kész. A részleges törlés megőrzi az előző bejegyzés tulajdonlási szemantikáját — egy törlés ahhoz a vonáshoz tartozik, amelyből tintát távolít el — úgy, hogy minden törölt vonást egy scratch textúrába rajzol, kivonja a saját törlési útvonalait destination-out keveréssel (), majd az eredményt visszakompozitálja. A scratch textúra elszigetelése megakadályozza, hogy a radír átüsse a papírt vagy a szomszédos vonásokat.

A mindent elindító kijelölés most point sprite-okként is meg van rajzolva. A húzása egyetlen uniform-eltolást frissít. Nulla textúraírás, nulla csempe-érvénytelenítés — a hullám strukturálisan eltűnt, nem csak tompítottuk.

Az őrző: egy képpont-összehasonlítás

Nem töröltük a Core Graphics renderelőjét. Offline referenciaimplementációvá alakítottuk, és minden Metal-változásnak át kell mennie egy vele végzett képpont-összehasonlításon, az eszközön rögzített valódi vonásadatokon. Az elfogadási feltétel nem az „azonos képpontok” — két helyes raszterizáló jogosan néhány szürkeszinttel eltérhet az élsimítás határain. A kapu strukturális: nincs hiányzó tinta, nincs eltolás, nincs színeltolódás, és sehol nincs nagy különbség a tintától távol.

Ugyanannak a kézzel írt jegyzetnek három kivágata: Core Graphicsszal renderelve, Metal point sprite-okkal renderelve, és a képpontkülönbségük hatszorosra nagyítva, amely csak halvány körvonalakat mutat a vonásélek mentén.

Ez a tesztkeret az offline renderelő építése közben talált öt hibából négyet kapott el; mindegyiket a kimenet képpontszintre történő kivágásával diagnosztizáltuk: egy Swift/Metal struct stride-eltérés (28 bájt 32 helyett, mert a Metal a float4-et 16-ra igazítja — a képernyő megtelt színblokkokkal), a [[point_size]] olvashatatlansága varyingként a fragment shaderben (minden lenyomat négyzet alakú lett), két render encoder egy parancsbufferen történő együttélése (minden fekete) és egy hiányzó kezdő folt, amely a gyors vonások első milliméterét láthatatlanná tette.

Hat lépés, nem egy nagy újraírás

Az átállási terv hat önállóan szállítható lépésből állt: offline renderelő, amely átment a képpont-összehasonlításon; megjelenítő héj nulla vizuális változással; élő vonás a GPU-n; kijelölés a GPU-n; mutációk, amelyek közvetlenül a textúrába írnak; nagy zoomú vektoros újrarajzolás (a második lépésbe beolvasztva, mert a „nulla vizuális változás” megkövetelte). Minden lépés végén egy ember — nem szimulátor, nem képernyőkép-összehasonlítás — írt, radírozott, nagyított és húzott az asztalon lévő iPad Prón.

Az eszköz három hibát talált, amelyeket minden automatizált ellenőrzés kihagyott. 100% zoom fölött a vonásokat kétszer rajzoltuk — alul lágy textúra, felül éles sprite-ok —, ami halvány elmosódásként jelent meg, amit az ember másodperceken belül észrevett. Egy vonás véglegesítésekor egy képkockára felvillant a kép, mert a régi overlay cross-fade-je nem maradt szinkronban a textúrafrissítéssel. 170% zoom fölött minden jegyzet eltűnt: a láthatósági culling téglalapja a contentOffset-et használta a skálázott koordinátaterében, így nagyításkor elcsúszott a vonásoktól. Mindhárom hibát egyetlen sor módosításával, egyetlen függvényben javítottuk ki, és egyik sem szerepelt egyetlen olyan tesztben sem, amelyet előre megírhattunk volna, mert nem tudtuk, hogy keresni kell őket. UI-first fogyasztói alkalmazásnál ezért marad az ember a hurokban.

Milyen Kimi K3-mal dolgozni

Gyors, mindenekelőtt. A „megbeszéljük, megírjuk, felépítjük, telepítjük, megnézzük” hurok percek alatt lefutott, és egy gyorsan válaszoló modell megváltoztatja, hány ciklus fér bele egy napba.

Másodszor, nem tervez túl. Ez a kódbázis kifejezett háziszabályok szerint működik — a kiadás előtt nincs visszamenőleges kompatibilitási állványzat, és csak akkor kerül bele bonyolultság, amikor egy eszköz bizonyítja, hogy szükséges —, K3 pedig emlékeztetés nélkül követi őket. Nem hozott létre térbeli indexeket „későbbre”, nem csomagolt minden hívást védekező ellenőrzésekbe, és nem absztrahált spekulatívan. Utasításokat adni neki olyan, mintha egy kompetens kollégával dolgoznál, aki elolvasta a háziszabályokat, és tényleg hisz is bennük.

Harmadszor, adj neki eszközöket, és lelkesen használja őket. Bekötöttünk képi segédprogramokat — megtekintés, kivágás egy képponttartományra, átméretezés —, és a modell elkezdte proaktívan kivágni a saját renderelő-kimenetét, hogy diagnosztizálja a fenti öt tesztkeret-hibát. Az eszköz megléte emlékeztette arra, hogy nézzen.

A másik fele: a K3 írta a történetben szereplő hibák többségét, beleértve azt a koordinátatér-hibát is, amely miatt a jegyzetek eltűntek. A korlátai valósak. A munka nem azért lett biztonságos, mert a modellnek mindig igaza volt, hanem azért, mert a tesztkeret elkapta a renderelési elcsúszást, az ember pedig felismerte a használati érzetet. És mégis, a mindennapokban nem tudtam megbízhatóan megkülönböztetni azoktól az élvonalbeli zárt modellektől, amelyeket szintén használunk — Opus-osztályú rendszerektől. Bizonyos szempontokban egyértelműen jobb volt: gyorsabb, és sokkal kevésbé hajlamos védekező tervezéssel kitömni a kódbázist.

Hogyan akarunk ezután építeni

Végeztünk a nagy specifikációra épülő agentikus fejlesztéssel — azzal a stílussal, amikor egy nagy specifikációt adsz a modellnek, és elfogadod, ami elkészül. A hibamód nem a rossz kód, hanem olyan kód, amit senki sem ért.

Ami itt működött, és amit megtartunk: kis lépések, amelyeket mind megbeszélünk a kezdés előtt, amelyeket az ember mind megért, mielőtt elkészülnek, és amelyeket azon az eszközön ellenőrzünk, amelyen élni fognak. A modell feladata, hogy gyors és pontos legyen, és őszinte a bizonytalansággal kapcsolatban. Az ember feladata az ítélőképesség, az ízlés és az e2e ellenőrzés — különösen UI-first fogyasztói szoftvereknél, ahol a specifikáció nem írhatja le, milyen érzés, amikor valami „helyes”. A Kimi K3 a Fireworkson pontosan ehhez a hurokhoz bizonyul jól illeszkedőnek: elég gyors ahhoz, hogy a hurok feszes maradjon, és elég okos ahhoz, hogy a lépések kicsik és tiszták maradjanak.

A hullám eltűnt, a folyamat két ötlet helyett egyetlen ötlet, és megmarad az a folyamat, amely idáig eljuttatott minket.