Prestavba vykresľovacieho pipeline Lulucat Notes s Kimi K3
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.
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.
![]()
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 (
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.
![]()
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.