Lulucat

Prestavba vykresľovacieho pipeline Lulucat Notes s Kimi K3

Gaoge ZhangGaoge Zhang

Náš dlaždicový pipeline v Core Graphics sa v šiestich malých krokoch zmenil na pipeline s point spritmi v Metale, pričom každý krok sme overili na skutočnom iPade. Kód vznikol párovým programovaním s Kimi K3 na Fireworks.

Aktualizované

Minulý týždeň spôsobovalo presúvanie lasového výberu naprieč Lulucat Notes viditeľnú vlnu: niektoré dlaždice obrazovky ukazovali výber na novej pozícii, kým iné v tej istej snímke stále ukazovali starú. Celý vykresľovací pipeline — bitmapu Core Graphics plus CATiledLayer — sme nahradili Metalom v šiestich malých krokoch, pričom každý sme overili na skutočnom iPade pred začatím ďalšieho.

Ešte jedna poznámka o autorstve, pretože na nej záleží v druhej polovici tohto príspevku: kód vznikol párovým programovaním s Kimi K3, otvoreným modelom od Moonshot bežiacim na Fireworks. Človek riadil, rozhodoval a testoval; model napísal takmer každý riadok.

Pipeline

Starý pipeline mal dva spôsoby kreslenia, ktoré sa pomaly rozchádzali: ťahy sa pre lacné zobrazenie zapekali do bitmapy a znova sa kreslili ako vektory vždy, keď dlaždice potrebovali viac detailov. Nový pipeline má presne jednu myšlienku: všetko, čo pripomína atrament, je point sprite. Ťah perom, prejdenie zvýrazňovačom a ťuknutie gumou sú ten istý 32-bajtový vertex — poloha, priemer, farba — vykreslený tým istým párom shaderov ako kruhy rastrované na GPU, rozmiestnené vo vzdialenosti jedného bodu pozdĺž oblúkovej dĺžky ťahu.

Tri panely toho istého zahnutého ťahu: vstupné dotykové body, kruhové odtlačky rozmiestnené pozdĺž krivky podľa oblúkovej dĺžky a zložený plný ťah.

Pri rozstupe jedného bodu sa reťaz kruhov od matematicky dokonalej kapsuly odchýli asi o 0.075 bodu — pätinu pixela pri našej hustote plátna. Výmenou sa tri nástroje zrútia do jednej cesty kódu a GPU robí to, v čom je najlepší.

Okolo tejto myšlienky je architektúra jednoduchá. Potvrdený atrament žije v jednej textúre 4096². UIScrollView prežíva, zredukovaný na čistý engine pre gestá: jeho contentOffset a zoomScale každý snímok napájajú uniform viewportu, takže posúvanie a približovanie nič nezapisujú. Každý snímok pozostáva z piatich vykreslení:

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

Úpravy zapisujú do textúry priamo. Potvrdenie ťahu pridá jeho odtlačky. Mazanie, odstraňovanie, presúvanie a vrátenie späť prehrávajú ovplyvnenú oblasť s použitím scissor obdĺžnika: vyčistiť oblasť, prekresliť ťahy, ktoré ju pretínajú, hotovo. Čiastočné vymazanie si zachováva sémantiku vlastníctva z predošlého príspevku — mazanie patrí ťahu, z ktorého odstraňuje atrament — tým, že každý vymazaný ťah nakreslí do scratch textúry, odčíta jeho vlastné cesty vymazania miešaním destination-out () a výsledok zloží späť. Scratch izolácia bráni gume prehryznúť papier alebo susedné ťahy.

Výber, ktorý to všetko odštartoval, sa teraz tiež vykresľuje ako point sprity. Jeho presúvanie aktualizuje jeden uniform offset. Žiadne zápisy do textúry, žiadne invalidácie dlaždíc — vlna je štrukturálne preč, nie iba zmiernená.

Strážca: pixelový diff

Vykresľovač Core Graphics sme neodstránili. Zredukovali sme ho na offline referenčnú implementáciu a každá zmena v Metale musí na reálnych dátach ťahov zachytených v zariadení prejsť pixelovým porovnaním voči nemu. Kritérium prijatia nie sú „identické pixely“ — dva správne rasterizéry sa oprávnene nezhodnú o niekoľko úrovní šedej pozdĺž antialiasovaných hrán. Brána je štrukturálna: žiadny chýbajúci atrament, žiadny posun, žiadny drift farby a žiadny veľký rozdiel nikde mimo atramentu.

Tri výrezy tých istých ručne písaných poznámok: vykreslené Core Graphics, vykreslené Metal point spritmi a ich pixelový rozdiel zosilnený šesťnásobne, ukazujúci iba slabé obrysy pozdĺž hrán ťahov.

Tento harness zachytil štyri z piatich chýb, na ktoré sme narazili pri stavbe offline vykresľovača; všetky sme diagnostikovali orezaním výstupu na úroveň pixelov: nezhoda stride štruktúry Swift/Metal (28 bajtov verzus 32, pretože Metal zarovnáva float4 na 16 — obrazovka sa zaplnila farebnými blokmi), [[point_size]] nečitateľné ako varying vo fragment shaderi (každý odtlačok vyšiel štvorcový), súčasné používanie dvoch render encoderov na jednom command bufferi (všetko čierne) a chýbajúci začiatočný odtlačok, ktorý nechal prvý milimeter rýchlych ťahov neviditeľný.

Šesť krokov, nie jedno prepísanie

Plán migrácie pozostával zo šiestich nezávisle nasaditeľných krokov: offline vykresľovač prechádzajúci pixelovým diffom; zobrazovací obal s nulovou vizuálnou zmenou; živý ťah na GPU; výber na GPU; mutácie zapisujúce priamo do textúry; vektorové prekresľovanie pri veľkom priblížení (zložené do kroku dva, pretože to vyžadovala „nulová vizuálna zmena“). Každý krok skončil tým, že človek — nie simulátor, nie diff snímky obrazovky — písal, gumoval, približoval a presúval na iPade Pro na stole.

Zariadenie zachytilo tri chyby, ktoré unikli všetkým automatizovaným kontrolám. Pri priblížení nad 100 % sa ťahy vykreslili dvakrát — mäkká textúra naspodku, ostré sprity navrchu — čo sa javilo ako slabé rozmazanie, ktoré človek zazrel v priebehu sekúnd. Potvrdenie ťahu blikalo jeden snímok, pretože starý prekryv sa pomocou cross-fade vytrácal mimo synchronizácie s aktualizáciou textúry. A pri priblížení nad 170 % každá poznámka zmizla: obdĺžnik cullingu viditeľnosti používal contentOffset vo svojom škálovanom súradnicovom priestore, takže sa pri približovaní odťahoval od ťahov. Každá z týchto troch chýb sa dala opraviť jedným riadkom v jednej funkcii a žiadna z nich neexistovala v žiadnom teste, ktorý by sme predtým mohli napísať, pretože sme nevedeli, že ich máme hľadať. Pri spotrebiteľskej aplikácii zameranej na UI je to dôvod, prečo človek zostáva v slučke: nedajú sa vymenovať spôsoby, akými sa „pocit“ rozbíja.

Aké je pracovať s Kimi K3

Rýchly, v prvom rade. Slučka „diskutovať, písať, zostaviť, nainštalovať, pozrieť“ trvala niekoľko minút a model, ktorý odpovedá rýchlo, mení, koľko slučiek si môžete dovoliť za deň.

Po druhé, nepre-inžinieruje. Táto kódová základňa funguje podľa explicitných interných pravidiel — žiadne pomocné vrstvy spätnej kompatibility pred štartom, zložitosť iba vtedy, keď ju zariadenie preukáže ako nevyhnutnú — a K3 ich dodržiava bez pripomínania. Nepridával priestorové indexy „na neskôr“, nezabaľoval každé volanie do defenzívnych kontrol, špekulatívne neabstrahoval. Promptovať ho pripomína prácu s kompetentným kolegom, ktorý si prečítal interné pravidlá a skutočne im verí.

Po tretie, dajte mu nástroje a on ich ochotne používa. Napojili sme obrazové utility — zobraziť, orezať na pixelovú oblasť, zmeniť veľkosť — a model začal proaktívne orezávať vlastný výstup vykresľovača, aby diagnostikoval päť chýb harnessu vyššie. Mať nástroj mu pripomenulo, aby sa pozrel.

Druhá strana: K3 napísal väčšinu chýb v tomto príbehu vrátane tej súradnicovo-priestorovej, ktorá dala poznámky zmiznúť. Jeho limity sú reálne. Čo prácu robilo bezpečnou, nikdy nebolo, že má model pravdu; bol to harness zachytávajúci odchýlky vo vykresľovaní a človek zachytávajúci pocit. A predsa, v každodennom fungovaní som ho nedokázal spoľahlivo odlíšiť od špičkových uzavretých modelov, ktoré tiež používame — systémov triedy Opus. V niektorých ohľadoch bol zjavne lepší: rýchlejší a oveľa menej naklonený nafukovať codebase defenzívnym dizajnom.

Ako chceme odteraz stavať

Skončili sme s agentným vývojom založeným na veľkých špecifikáciách — štýlom, keď modelu odovzdáte veľkú špecifikáciu a prijmete, čo pristane. Režim zlyhania nie je zlý kód; je to kód, ktorému nikto nerozumie.

Čo tu fungovalo a čo si ponecháme: malé kroky, každý prediskutovaný pred začatím, každý pochopený človekom predtým, než sa postaví, každý overený na zariadení, na ktorom bude žiť. Úloha modelu je byť rýchly, presný a úprimný k neistote. Úloha človeka je úsudok, vkus a e2e overenie — obzvlášť pre UI-first spotrebiteľský softvér, kde špecifikácia nedokáže opísať, ako sa „cíti správne“. Kimi K3 na Fireworks sa ukázal byť dobre prispôsobený práve na túto slučku: dostatočne rýchly na to, aby slučku udržal tesnú, a dostatočne múdry na to, aby kroky udržal malé a čisté.

Vlna je preč, pipeline je jedna myšlienka namiesto dvoch a proces, ktorý nás tam dostal, zostáva.