Lulucat

Lulucat Notesin renderöintiputken rakentaminen uudelleen Kimi K3:n avulla

Gaoge ZhangGaoge Zhang

Core Graphics -laattaputkemme muuttui Metalin point sprite -putkeksi kuudessa pienessä vaiheessa, joista jokainen varmistettiin aidolla iPadilla. Koodi kirjoitettiin pariohjelmointina Kimi K3:n kanssa Fireworksissa.

Päivitetty

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.

Saman kaarevan vedon kolme paneelia: syötteen kosketuspisteet, kaaren pituuden mukaan käyrälle sijoitetut pyöreät leimat ja yhdistetty yhtenäinen veto.

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 () ja yhdistämällä tuloksen takaisin. Scratch-tekstuurin eristys estää pyyhekumia pureutumasta paperin tai viereisten vetojen läpi.

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.

Kolme rajattua kuvaa samoista käsin kirjoitetuista muistiinpanoista: Core Graphicsilla renderöity, Metal-point-spriteillä renderöity ja niiden kuusinkertaisesti vahvistettu pikseliero, joka näyttää vain heikot ääriviivat vetojen reunoilta.

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ää.