Lulucat

Ponovna izgradnja cjevovoda iscrtavanja Lulucat Notesa s Kimi K3

Gaoge ZhangGaoge Zhang

Naš cjevovod pločica Core Graphicsa postao je Metalov cjevovod točkastih spritova u šest malih koraka, a svaki je provjeren na stvarnom iPadu. Kod je pisan u paru s Kimi K3 na Fireworksu.

Ažurirano

Prošli tjedan povlačenje odabira lasom preko Lulucat Notesa stvaralo je vidljiv val: neke pločice zaslona prikazivale su odabir na novom položaju, dok su druge u istom okviru još prikazivale stari. Cijeli smo cjevovod iscrtavanja — bitmapu Core Graphicsa plus CATiledLayer — zamijenili Metalom u šest malih koraka, a svaki je provjeren na stvarnom iPadu prije početka sljedećeg.

Kod je pisan u paru s Kimi K3, otvorenim modelom Moonshota koji radi na Fireworksu. Čovjek je vodio, odlučivao i testirao; model je napisao gotovo svaki redak.

Cjevovod

Stari je cjevovod imao dvije vrste crtanja koje su se polako razdvajale: potezi su se za jeftin prikaz ugrađivali u bitmapu, a ponovno iscrtavali kao vektori kada je pločicama trebalo više detalja. Novi cjevovod ima točno jednu ideju: sve što nalikuje tinti točkasti je sprite. Potez olovke, potez highlightera i dodir gumice isti su 32-bajtni vrh — položaj, promjer, boja — koji isti par shadera iscrtava kao GPU-rasterizirane krugove razmaknute za jednu točku duž duljine luka poteza.

Tri ploče istog zakrivljenog poteza: ulazne dodirne točke, kružni otisci razmaknuti duž krivulje prema duljini luka i komponirani puni potez.

Pri razmaku od jedne točke lanac krugova odstupa od matematički savršene kapsule za oko 0.075 točaka — petinu piksela pri gustoći našeg platna. Zauzvrat se tri alata svode na jedan put koda, a GPU radi ono u čemu je najbolji.

Oko te je ideje arhitektura jednostavna. Potvrđena tinta živi u jednoj teksturi 4096². UIScrollView ostaje, sveden na čisti mehanizam gesti: njegov contentOffset i zoomScale svake se sličice prosljeđuju u uniformu viewporta, pa se pomicanjem i zumiranjem ništa ne zapisuje. Svaki se okvir sastoji od pet iscrtavanja:

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

Uređivanja izravno zapisuju teksturu. Potvrda poteza dodaje njegove otiske. Brisanje, uklanjanje, premještanje i poništavanje ponavljaju iscrtavanje pogođene regije unutar scissor pravokutnika: očisti regiju, ponovno iscrtaj poteze koji je presijecaju, gotovo. Djelomično brisanje zadržava semantiku vlasništva iz prethodne objave — brisanje pripada potezu iz kojega uklanja tintu — tako da se svaki obrisani potez iscrta u pomoćnu teksturu, njegove se vlastite putanje brisanja oduzmu blendanjem destination-out (), a rezultat se ponovno komponira. Izolacija pomoćne teksture sprječava gumicu da probije papir ili susjedne poteze.

Odabir koji je sve ovo pokrenuo sada se također crta u obliku točkastih spritova. Povlačenjem se ažurira jedan uniformni pomak. Nema upisa u teksturu, nema poništavanja pločica — val je strukturno nestao, a ne samo ublažen.

Čuvar prolaza: usporedba piksela

Nismo izbrisali renderer Core Graphicsa. Sveli smo ga na offline referentnu implementaciju, a svaka promjena u Metalu mora proći usporedbu piksela s njim na stvarnim podacima poteza snimljenima na uređaju. Kriterij prihvaćanja nije „identični pikseli” — dva ispravna rasterizatora legitimno se mogu razlikovati za nekoliko razina sive uz antialiasirane rubove. Provjera je strukturna: nema nedostatka tinte, pomaka, promjene boje ni velike razlike bilo gdje izvan tinte.

Tri izreza istih rukom pisanih bilješki: prikaz Core Graphicsom, prikaz Metalovim točkastim spritovima i njihova šest puta pojačana razlika piksela, koja pokazuje samo blijede obrise uz rubove poteza.

Ovaj je harness uhvatio četiri od pet bugova na koje smo naišli tijekom izrade offline renderera, a sve smo ih dijagnosticirali izrezivanjem izlaza na razinu piksela: neslaganje stridea strukture Swift/Metal (28 bajtova umjesto 32, jer Metal poravnava float4 na 16 — zaslon se ispunio blokovima boje), nemogućnost čitanja [[point_size]] kao varying u fragmentnom shaderu (svaki je otisak ispao kvadrat), istodobno postojanje dvaju render enkodera u jednom command bufferu (sve je bilo crno) i izostavljeni početni otisak zbog kojega je prvi milimetar brzih poteza ostao nevidljiv.

Šest koraka, a ne jedno prepisivanje

Plan migracije sastojao se od šest neovisno isporučivih koraka: offline renderer koji prolazi usporedbu piksela; ljuska prikaza bez ikakve vizualne promjene; živi potez na GPU-u; odabir na GPU-u; mutacije koje izravno pišu u teksturu; vektorsko ponovno iscrtavanje pri velikom zumu (uklopljeno u drugi korak jer je „nula vizualne promjene” to zahtijevala). Nakon svakog je koraka čovjek — ne simulator, ne usporedba snimke zaslona — pisao, brisao, zumirao i povlačio na iPadu Pro na stolu.

Uređaj je uhvatio tri buga koja je svaka automatizirana provjera propustila. Iznad 100 % zuma potezi su se iscrtavali dvaput — meka tekstura ispod, oštri spritovi iznad — što je izgledalo kao blijeda zamućenost koju je čovjek primijetio u nekoliko sekundi. Potvrda poteza uzrokovala je treperenje jednog okvira jer je stari overlay nestajao cross-fadeom izvan sinkronizacije s ažuriranjem teksture. A iznad 170 % zuma svaka je bilješka nestala: pravokutnik za izrezivanje vidljivosti koristio je contentOffset u svom skaliranom koordinatnom prostoru, pa se pri zumiranju udaljavao od poteza. Sva su tri popravljena jednom linijom u jednoj funkciji, a nijedan nije postojao ni u jednom testu koji smo mogli napisati unaprijed, jer nismo znali što tražiti. Za potrošačku aplikaciju kojoj je sučelje na prvom mjestu, zato čovjek ostaje u petlji.

Kako je raditi s Kimi K3

Prije svega, brz je. Petlja „raspravljanja, pisanja, izgradnje, instaliranja i gledanja” trajala je nekoliko minuta, a model koji brzo odgovara mijenja broj petlji koje možemo priuštiti u jednom danu.

Drugo, ne komplicira. Ova baza koda radi prema izričitim kućnim pravilima — prije lansiranja nema konstrukcija za kompatibilnost unatrag, složenost se uvodi samo kad uređaj dokaže da je nužna — a K3 ih slijedi bez podsjećanja. Nije dodao prostorne indekse „za poslije”, nije svaki poziv omotao obrambenim provjerama i nije unaprijed uvodio apstrakcije. Davati mu upute nalik je radu s kompetentnim kolegom koji je pročitao kućna pravila i doista vjeruje u njih.

Treće, dajte mu alate i rado ih koristi. Povezali smo pomoćne alate za slike — pregledavanje, izrezivanje na područje piksela, promjenu veličine — i model je počeo proaktivno izrezivati vlastiti izlaz renderera kako bi dijagnosticirao pet prethodno navedenih bugova u harnessu. Alat ga je podsjetio da gleda.

Druga polovica priče: K3 je napisao većinu bugova u ovoj priči, uključujući onaj s koordinatnim prostorom zbog kojega su bilješke nestajale. Njegova su ograničenja stvarna. Ono što je rad učinilo sigurnim nikad nije bila činjenica da je model bio u pravu; sigurnost su dali harness koji je hvatao odstupanje u iscrtavanju i čovjek koji je hvatao osjećaj. Pa ipak, iz dana u dan nisam ga mogao pouzdano razlikovati od najnaprednijih zatvorenih modela koje također koristimo — sustava klase Opus. U nekim je aspektima bio očito bolji: brži i mnogo manje sklon zatrpati bazu koda obrambenim dizajnom.

Kako želimo graditi ubuduće

Završili smo s agentnim razvojem na temelju velikih specifikacija — stilom u kojem modelu predate veliku specifikaciju i prihvatite što god se pojavi. Neuspjeh nije loš kod; to je kod koji nitko ne razumije.

Ono što je ovdje funkcioniralo i što ćemo zadržati: mali koraci, svaki prethodno raspravljen, svaki razumljiv čovjeku prije izrade, svaki provjeren na uređaju na kojem će živjeti. Posao modela je biti brz, precizan i iskren u vezi s neizvjesnošću. Posao čovjeka su prosudba, ukus i e2e provjera — osobito za potrošački softver kojem je sučelje na prvom mjestu, gdje specifikacija ne može opisati kako se „ispravno” treba osjećati. Kimi K3 na platformi Fireworks pokazuje se dobro prilagođenim upravo toj petlji: dovoljno brzim da petlja ostane tijesna, dovoljno pametnim da koraci ostanu mali i čisti.

Val je nestao, cjevovod je postao jedna ideja umjesto dviju, a proces koji nas je do njega doveo ostaje.