Přestavba vykreslovací pipeline Lulucat Notes pomocí Kimi K3
Naše dlaždicová pipeline Core Graphics se v šesti malých krocích změnila v Metal pipeline bodových spritů, přičemž každý krok jsme ověřili na skutečném iPadu. Kód vznikal párovým programováním s Kimi K3 na Fireworks.
Minulý týden vytvořilo tažení výběru lasem napříč Lulucat Notes viditelnou vlnu: některé dlaždice obrazovky ukazovaly výběr na nové pozici, zatímco jiné ve stejném snímku stále ukazovaly starou. Celou vykreslovací pipeline — bitmapu Core Graphics plus CATiledLayer — jsme nahradili technologií Metal v šesti malých krocích; každý jsme ověřili na skutečném iPadu, než začal další.
Kód vznikal párovým programováním s Kimi K3, otevřeným modelem od Moonshot běžícím na Fireworks. Člověk určoval směr, rozhodoval a testoval; model napsal téměř každý řádek.
Pipeline
Stará pipeline měla dva druhy vykreslování, které se od sebe postupně vzdalovaly: tahy se kvůli levnému zobrazení zapečely do bitmapy a znovu se vykreslovaly jako vektory, kdykoli dlaždice potřebovaly více detailu. Nová pipeline má jedinou myšlenku: všechno, co připomíná inkoust, je bodový sprite. Tah pera, tah zvýrazňovače i otisk gumy jsou stejný 32bajtový vrchol — pozice, průměr, barva — vykreslovaný stejnou dvojicí shaderů jako kružnice rasterizované GPU, rozmístěné s rozestupem jednoho bodu podél délky oblouku tahu.
![]()
Při rozestupu jednoho bodu se řetěz kružnic od matematicky dokonalé kapsuly odchyluje asi o 0.075 bodu — při hustotě našeho plátna je to pětina pixelu. Výměnou se tři nástroje smrsknou do jediné cesty a GPU dělá to, v čem je nejlepší.
Kolem této myšlenky je architektura jednoduchá. Potvrzený inkoust žije v jediné textuře 4096². UIScrollView zůstává, ale je odsunut do role čistého enginu gest: jeho contentOffset a zoomScale každý snímek napájí uniformní proměnnou viewportu, takže posouvání ani přibližování nic nezapisuje. Každý snímek tvoří pět 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
Editace zapisují texturu přímo. Potvrzení tahu přidá jeho otisky. Mazání, odstraňování, přesouvání a undo znovu přehrají zasaženou oblast za obdélníkem scissor testu: oblast vyčistit, znovu vykreslit tahy, které ji protínají, hotovo. Částečné mazání zachovává sémantiku vlastnictví z předchozího článku — mazání patří tahu, z něhož odstraňuje inkoust — tím, že každý mazaný tah vykreslí do pomocné textury, odečte jeho vlastní mazací cesty pomocí blendování destination-out (
Výběr, který to celé odstartoval, se nyní kreslí také jako bodové sprity. Tažení mění jediný uniformní offset. Nula zápisů do textury, nula invalidací dlaždic — vlna je pryč strukturálně, ne jen zmírněná.
Strážce: pixelový diff
Vykreslovač Core Graphics jsme nesmazali. Odsunuli jsme ho do role offline referenční implementace a každá změna v implementaci Metal musí na skutečných datech tahů zachycených na zařízení projít pixelovým porovnáním s ním. Kritériem přijetí nejsou „identické pixely“ — dva správné rasterizéry se po vyhlazených hranách oprávněně liší o několik odstínů šedi. Brána je strukturální: žádný chybějící inkoust, žádný posun, žádný barevný posun a žádný velký rozdíl kdekoli mimo inkoust.
![]()
Tento harness odhalil čtyři z pěti chyb, na které jsme narazili při stavbě offline vykreslovače. Všechny jsme diagnostikovali ořezáním výstupu na úroveň pixelů: nesoulad stride struktury Swift/Metal (28 bajtů místo 32, protože Metal zarovnává float4 na 16 — obrazovku vyplnily barevné bloky), [[point_size]] nešlo ve fragment shaderu přečíst jako varying (každý otisk vyšel čtvercový), dva renderovací enkodéry existovaly současně v jednom command bufferu (všechno bylo černé) a chyběl počáteční otisk, kvůli čemuž byl první milimetr rychlých tahů neviditelný.
Šest kroků, ne jeden přepis
Plán migrace tvořilo šest samostatně vydatelných kroků: offline vykreslovač, který prochází pixelovým diffem; vrstva zobrazení bez vizuální změny; živý tah na GPU; výběr na GPU; mutace zapisující texturu přímo; vektorové překreslování při velkém přiblížení (sloučené do druhého kroku, protože „bez vizuální změny“ to vyžadovalo). Každý krok skončil tím, že člověk na iPadu Pro na stole psal, mazal, přibližoval a táhl — ne simulátor, ne porovnání screenshotů.
Zařízení odhalilo tři chyby, které každá automatizovaná kontrola přehlédla. Při zvětšení nad 100% se tahy vykreslovaly dvakrát — měkká textura pod nimi, ostré sprity nad nimi — což se projevilo jako slabé rozmazání, kterého si člověk všiml během několika sekund. Potvrzení tahu způsobilo bliknutí na jeden snímek, protože starý overlay se při prolínání skryl mimo synchronizaci s aktualizací textury. A při zvětšení nad 170% zmizely všechny poznámky: obdélník pro ořezávání viditelnosti používal contentOffset ve škálovaném souřadnicovém prostoru, takže se při zvětšování vzdaloval od tahů. Všechny tři problémy šly opravit jediným řádkem v jediné funkci a žádný z nich neexistoval v žádném testu, který jsme mohli předem napsat, protože jsme nevěděli, že je máme hledat. U spotřebitelské aplikace s UI na prvním místě proto člověk zůstává v procesu.
Jak se s Kimi K3 pracuje
Především je rychlý. Cyklus „probrat, napsat, sestavit, nainstalovat, podívat se“ trval minuty a model, který odpovídá rychle, mění počet cyklů, které si za den můžete dovolit.
Za druhé nepřekomplikovává. Tento projekt funguje podle explicitních interních pravidel — před vydáním žádné lešení zpětné kompatibility, složitost jen tehdy, když zařízení prokáže, že je nutná — a K3 se jich drží bez připomínání. Nepřidal prostorové indexy „na později“, neobalil každé volání obrannými kontrolami a nevymýšlel abstrakce pro jistotu. Zadávání promptů působí jako práce se schopným kolegou, který si přečetl interní pravidla a skutečně jim věří.
Za třetí mu dejte nástroje a bude je dychtivě používat. Zapojili jsme obrazové utility — prohlížení, ořez na pixelovou oblast a změnu velikosti — a model začal z vlastní iniciativy ořezávat výstup svého vykreslovače, aby diagnostikoval pět chyb harnessu popsaných výše. Samotná existence nástroje mu připomněla, že se má dívat.
Druhá strana: K3 napsal většinu chyb v tomto příběhu, včetně té v souřadnicovém prostoru, kvůli níž zmizely poznámky. Jeho limity jsou skutečné. Bezpečnou práci nezajistilo to, že by model měl vždy pravdu; zajistil ji harness, který zachytil posuny ve vykreslování, a člověk, který zachytil pocit z používání. A přesto jsem ho v každodenní práci nedokázal spolehlivě rozeznat od špičkových uzavřených modelů, které také používáme — systémů třídy Opus. V některých ohledech byl zjevně lepší: byl rychlejší a mnohem méně inklinoval k plnění kódové základny obranným návrhem.
Jak chceme odteď stavět
Skončili jsme s agentním vývojem podle velkých specifikací — stylem, kdy modelu předáte obsáhlou specifikaci a přijmete cokoli, co vznikne. Selháním není špatný kód; jsou jím kódy, kterým nikdo nerozumí.
To, co zde fungovalo a co si ponecháme: malé kroky, každý předem prodiskutovaný, člověkem pochopený ještě před implementací a ověřený na zařízení, na němž bude žít. Úkolem modelu je být rychlý, přesný a upřímný ohledně nejistoty. Úkolem člověka je úsudek, vkus a e2e ověřování — zvlášť u spotřebitelského softwaru s UI na prvním místě, kde specifikace nedokáže popsat, jaký má být správný pocit z používání. Kimi K3 na Fireworks se ukázal jako model přesně pro tento cyklus: dost rychlý na to, aby cyklus zůstal těsný, a dost chytrý na to, aby kroky zůstaly malé a čisté.
Vlna je pryč, pipeline je jedna myšlenka místo dvou a proces, který nás k tomu dovedl, zůstává.