Lulucat Notesin renderöintiputken rakentaminen uudelleen Kimi K3:n avulla
Core Graphics -laattaputkemme muuttui Metalin point sprite -putkeksi kuudessa pienessä vaiheessa, joista jokainen varmistettiin aidolla iPadilla. Koodi kirjoitettiin pariohjelmointina Kimi K3:n kanssa Fireworksissa.
Viime viikolla lassovalinnan vetäminen Lulucat Notesissa sai aikaan näkyvän aallon: jotkin näytön laatat näyttivät valinnan uudessa sijainnissaan, kun taas toiset näyttivät samassa ruudussa yhä vanhan sijainnin. Korvasimme koko renderöintiputken — Core Graphics -bittikartan ja CATiledLayerin — Metalilla kuudessa pienessä vaiheessa. Jokainen vaihe varmistettiin aidolla iPadilla ennen seuraavan aloittamista.
Koodi kirjoitettiin pariohjelmointina Kimi K3:n kanssa; Moonshotin avoin malli pyöri Fireworksissa. Ihminen ohjasi, teki päätökset ja testasi; malli kirjoitti lähes jokaisen rivin.
Putki
Vanha putki sisälsi kaksi vähitellen erilleen ajautunutta piirtoa: vedot leivottiin bittikarttaan edullista näyttöä varten, ja ne piirrettiin uudelleen vektoreina aina, kun laatat tarvitsivat enemmän yksityiskohtia. Uudessa putkessa on täsmälleen yksi ajatus: kaikki mustemainen sisältö on point sprite. Kynänveto, korostuskynän pyyhkäisy ja pyyhekumin töppäys ovat sama 32-tavuinen kärkipiste — sijainti, halkaisija, väri — jonka sama shader-pari piirtää GPU-rasteroituina ympyröinä, jotka ovat vedon kaaren pituudella yhden pisteen välein.
![]()
Yhden pisteen välein sijoitettu ympyräketju poikkeaa matemaattisesti täydellisestä kapselista noin 0.075 pistettä — meidän kanvaasitiheydellämme viidesosa pikseliä. Vastineeksi kolme työkalua tiivistyy yhdeksi koodipoluksi, ja GPU tekee sen, missä se on parhaimmillaan.
Tämän idean ympärille rakentuva arkkitehtuuri on yksinkertainen. Vahvistettu muste elää yhdessä 4096²-tekstuurissa. UIScrollView säilyy, mutta on nyt pelkkä elemoottori: sen contentOffset ja zoomScale syöttävät viewport-uniformin joka ruudulla, joten panorointi ja zoomaus eivät kirjoita mitään. Jokainen ruutu sisältää viisi piirtokutsua:
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
Muokkaukset kirjoittavat suoraan tekstuuriin. Vedon vahvistaminen lisää sen leimat. Pyyhkiminen, poistaminen, siirtäminen ja kumoaminen toistavat vaikutusalueen scissor-suorakulmion rajaamalla alueella: alue tyhjennetään, sen kanssa risteävät vedot piirretään uudelleen, ja valmis. Osittainen pyyhintä säilyttää edellisen julkaisun omistajuussemantiikan — pyyhintä kuuluu vedolle, josta se poistaa mustetta — piirtämällä jokaisen pyyhityn vedon scratch-tekstuurille, vähentämällä sen omat pyyhintäpolut destination-out-sekoituksella (
Kaiken aloittanut valinta piirretään nyt myös point spriteinä. Sen vetäminen päivittää yhden uniform-siirtymän. Nolla tekstuurikirjoitusta, nolla laattainvalidointia — aalto on rakenteellisesti poissa, ei vain lievennetty.
Portinvartija: pikseliero
Emme poistaneet Core Graphics -renderöijää. Muutimme sen offline-vertailutoteutukseksi, ja jokaisen Metal-muutoksen on läpäistävä pikselivertailu sitä vasten aidolla laitteella tallennetulla vetodatalla. Hyväksymiskriteeri ei ole “identtiset pikselit” — kaksi oikeaa rasteroijaa voi perustellusti erota muutamalla harmaasävyllä antialiasoitujen reunojen kohdalla. Kriteeri on rakenteellinen: mustetta ei saa puuttua, sijainti ei saa siirtyä, väri ei saa ajautua, eikä musteen ulkopuolella saa olla suuria eroja.
![]()
Tämä testikehys löysi neljä viidestä offline-renderöijän rakentamisen aikana kohtaamastamme virheestä, ja kaikki diagnosoitiin rajaamalla tuloste pikselitasolle: Swift/Metal-rakenteen stride-poikkeama (28 tavua 32:n sijaan, koska Metal kohdistaa float4:n 16 tavuun — näyttö täyttyi värilohkoista), se, ettei [[point_size]]:a voinut lukea varying-muuttujana fragment shaderissa (jokainen leima oli neliö), kaksi render encoderia samalla command bufferilla (kaikki mustaa) ja puuttuva aloitusleima, joka teki nopeiden vetojen ensimmäisestä millimetristä näkymättömän.
Kuusi askelta, ei yksi uudelleenkirjoitus
Siirtymäsuunnitelma koostui kuudesta itsenäisesti toimitettavasta vaiheesta: offline-renderöijä, joka läpäisee pikselieron; näyttökuori ilman pienintäkään visuaalista muutosta; live-veto GPU:lla; valinta GPU:lla; muokkaukset, jotka kirjoittavat suoraan tekstuuriin; sekä vektorien uudelleenpiirto suurella zoomauksella (sisällytettiin toiseen vaiheeseen, koska «nolla visuaalista muutosta» sitä vaati). Jokainen vaihe päättyi siihen, että ihminen — ei simulaattori eikä kuvakaappausvertailu — kirjoitti, pyyhki, zoomasi ja veti pöydällä olevalla iPad Prolla.
Laite löysi kolme virhettä, jotka jokainen automaattinen tarkistus missasi. Yli 100 %:n zoomauksessa vedot piirrettiin kahdesti — pehmeä tekstuuri alla, terävät spritet päällä — ja tuloksena oli heikko sumennus, jonka ihminen huomasi sekunneissa. Vedon vahvistaminen välähti yhden ruudun ajan, koska vanha peite teki ristihäivytyksen epäsynkronissa tekstuuripäivityksen kanssa. Yli 170 %:n zoomauksessa kaikki muistiinpanot katosivat: näkyvyyden karsintasuorakulmio käytti contentOffset-arvoa skaalatussa koordinaattiavaruudessaan, joten se ajautui vedoista zoomatessa. Kaikki kolme korjattiin yhdellä rivillä yhdessä funktiossa, eikä yksikään niistä ollut missään testissä, jonka olisimme voineet kirjoittaa etukäteen, koska emme tienneet etsiä niitä. UI-ensisijaisessa kuluttajasovelluksessa juuri siksi ihminen pysyy mukana silmukassa.
Millaista on työskennellä Kimi K3:n kanssa
Nopea, ennen kaikkea. Silmukka «keskustele, kirjoita, rakenna, asenna, katso» kesti minuutteja, ja nopeasti vastaava malli muuttaa sitä, kuinka monta silmukkaa sinulla on varaa ajaa päivässä.
Toiseksi, se ei ylisuunnittele. Tässä koodikannassa noudatetaan selkeitä talon sääntöjä — ei taaksepäin yhteensopivuuden tukirakenteita ennen julkaisua, monimutkaisuutta lisätään vain, kun laite osoittaa sen välttämättömäksi — ja K3 noudattaa niitä ilman, että sitä tarvitsee muistuttaa. Se ei lisännyt tilallisia indeksejä «myöhempää varten», ei käärinyt jokaista kutsua puolustaviin tarkistuksiin eikä abstrahoinut ennakkoon. Sen kanssa työskentely tuntuu siltä kuin työskentelisi pätevän kollegan kanssa, joka on lukenut talon säännöt ja uskoo niihin oikeasti.
Kolmanneksi, anna sille työkaluja ja se käyttää niitä innokkaasti. Kytkimme kuvatyökalut — katso, rajaa pikselialueeseen, muuta kokoa — ja malli alkoi oma-aloitteisesti rajata renderöijänsä tulostetta edellä mainittujen viiden testikehysvirheen diagnosoimiseksi. Työkalu muistutti sitä katsomaan.
Toinen puoli: K3 kirjoitti suurimman osan tämän tarinan virheistä, mukaan lukien koordinaattiavaruusvirheen, joka sai muistiinpanot katoamaan. Sen rajat ovat todellisia. Työn teki turvalliseksi se, että testikehys havaitsi renderöinnin ajautumisen ja ihminen huomasi käyttötuntuman — ei se, että malli olisi ollut oikeassa. Silti en pystynyt arjessa luotettavasti erottamaan sitä käyttämistämme suljetuista huippumalleista — Opus-luokan järjestelmistä. Joillakin mittareilla se oli selvästi parempi: nopeampi ja paljon vähemmän taipuvainen täyttämään koodikantaa puolustavalla suunnittelulla.
Miten haluamme rakentaa tästä lähtien
Olemme lopettaneet suuriin spesifikaatioihin perustuvan agenttikehityksen — tyylin, jossa annat mallille laajan spesifikaation ja hyväksyt kaiken, mitä se tuottaa. Epäonnistuminen ei ole huonoa koodia; se on koodia, jota kukaan ei ymmärrä.
Se, mikä toimi tässä ja mitä jatkamme: pienet askeleet, joista jokainen käydään läpi ennen aloittamista, jotka ihminen ymmärtää ennen rakentamista ja jotka varmistetaan laitteella, jolla ne tulevat elämään. Mallin tehtävä on olla nopea, täsmällinen ja rehellinen epävarmuudesta. Ihmisen tehtävä on harkinta, maku ja e2e-varmennus — erityisesti UI-ensisijaisessa kuluttajaohjelmistossa, jossa spesifikaatio ei voi kuvata, miltä «oikea» tuntuu. Kimi K3 Fireworksissa osoittautuu juuri tähän silmukkaan sopivaksi: tarpeeksi nopea pitämään silmukan tiiviinä, tarpeeksi älykäs pitämään askeleet pieninä ja siisteinä.
Aalto on poissa, putki on yksi idea kahden sijaan, ja prosessi, joka toi meidät tänne, jää.