Reconstrucció de la pipeline de renderització de Lulucat Notes amb Kimi K3
La nostra pipeline de rajoles de Core Graphics es va convertir en una pipeline de point sprites de Metal en sis passos petits, cadascun verificat en un iPad real. El codi es va programar en parella amb Kimi K3 a Fireworks.
La setmana passada, arrossegar una selecció de llaç per Lulucat Notes produïa una ona visible: algunes rajoles de la pantalla mostraven la selecció a la nova posició, mentre que d’altres encara mostraven l’antiga, en el mateix fotograma. Vam substituir tota la pipeline de renderització — bitmap de Core Graphics més CATiledLayer — per Metal, en sis passos petits, cadascun verificat en un iPad real abans de començar el següent.
El codi es va programar en parella amb Kimi K3, el model obert de Moonshot que s’executava a Fireworks. La persona humana dirigia, decidia i feia les proves; el model va escriure gairebé totes les línies.
La pipeline
La pipeline antiga tenia dos tipus de dibuix que s’anaven separant a poc a poc: els traços es fixaven en un bitmap per mostrar-los de manera econòmica, i es tornaven a dibuixar com a vectors quan les rajoles necessitaven més detall. La nova pipeline té una sola idea: tot el que s’assembla a tinta és un point sprite. Un traç de ploma, una passada de ressaltador i una taca de goma d’esborrar són el mateix vèrtex de 32 bytes — posició, diàmetre, color — dibuixat pel mateix parell de shaders, com a cercles rasteritzats per la GPU i espaiats un punt al llarg de la longitud d’arc del traç.
![]()
Amb un espaiat d’un punt, una cadena de cercles es desvia aproximadament 0.075 punts d’una càpsula matemàticament perfecta — una cinquena part d’un píxel a la densitat del nostre llenç. A canvi, tres eines es redueixen a un únic camí de codi, i la GPU fa allò que sap fer millor.
Al voltant d’aquesta idea, l’arquitectura és senzilla. La tinta confirmada viu en una única textura de 4096². El UIScrollView es conserva, relegat a un motor de gestos pur: el seu contentOffset i zoomScale alimenten un uniform de viewport a cada fotograma, de manera que desplaçar i fer zoom no escriu res. Cada fotograma consta de cinc dibuixos:
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
Les edicions escriuen directament a la textura. Confirmar un traç n’afegeix els segells. Esborrar, eliminar, moure i desfer tornen a reproduir la regió afectada darrere d’un rectangle de scissor: esborra la regió, torna a dibuixar els traços que hi intersequen i ja està. L’esborrat parcial conserva la semàntica de propietat de l’article anterior — un esborrat pertany al traç del qual elimina tinta — dibuixant cada traç esborrat en una textura auxiliar, restant els seus propis camins d’esborrat amb una mescla destination-out (
La selecció que ho va iniciar tot ara també es dibuixa com a point sprites. Arrossegar-la actualitza un únic desplaçament de uniform. Cap escriptura a la textura, cap invalidació de rajoles: l’ona ha desaparegut estructuralment, no està mitigada.
El guardià: una diferència de píxels
No vam eliminar el renderitzador de Core Graphics. El vam relegar a una implementació de referència fora de línia, i cada canvi a Metal ha de superar una comparació de píxels amb ell, basada en dades de traços reals capturades al dispositiu. El criteri d’acceptació no és «píxels idèntics» — dos rasteritzadors correctes poden diferir legítimament en uns quants nivells de gris al llarg de les vores antialiased. La barrera és estructural: cap tinta absent, cap desplaçament, cap deriva de color i cap diferència gran enlloc fora de la tinta.
![]()
Aquest arnès va detectar quatre dels cinc errors que vam trobar mentre construíem el renderitzador fora de línia, tots diagnosticats retallant la sortida fins al nivell de píxel: una discrepància en el stride de l’estructura Swift/Metal (28 bytes en lloc de 32, perquè Metal alinea float4 a 16 — la pantalla es va omplir de blocs de color), [[point_size]] no es podia llegir com a varying al fragment shader (cada segell sortia quadrat), dos render encoders coexistien en un mateix command buffer (tot era negre), i faltava la taca inicial, cosa que deixava invisible el primer mil·límetre dels traços ràpids.
Sis passos, no una reescriptura
El pla de migració constava de sis passos que es podien publicar de manera independent: renderitzador fora de línia que superés la diferència de píxels; carcassa de visualització sense cap canvi visual; traç en directe a la GPU; selecció a la GPU; mutacions que escrivien directament a la textura; redibuix vectorial amb zoom alt (integrat en el segon pas, perquè «cap canvi visual» ho exigia). Cada pas acabava amb una persona escrivint, esborrant, fent zoom i arrossegant a l’iPad Pro que hi havia damunt la taula, no pas amb un simulador ni amb una diferència de captures de pantalla.
El dispositiu va detectar tres errors que totes les comprovacions automatitzades havien passat per alt. Per sobre del 100% de zoom, els traços es dibuixaven dues vegades — textura suau a sota, sprites nítids a sobre —, cosa que es percebia com un desenfocament lleu que la persona va notar en qüestió de segons. Confirmar un traç feia pampallugues durant un fotograma perquè la superposició antiga desapareixia amb una transició creuada desincronitzada amb l’actualització de la textura. I per sobre del 170% de zoom, totes les notes desapareixien: el rectangle de filtratge de visibilitat utilitzava contentOffset en el seu espai de coordenades escalat, de manera que s’allunyava dels traços a mesura que feies zoom. Les tres solucions consistien en canvis d’una línia en una funció, i cap d’aquests errors no existia en cap prova que haguéssim pogut escriure abans, perquè no sabíem que calia buscar-los. En una aplicació de consum UI-first, per això cal mantenir una persona en el circuit.
Com és treballar amb Kimi K3
Ràpid, abans que res. El cicle de «discutir, escriure, construir, instal·lar, mirar» durava minuts, i un model que respon de pressa canvia quants cicles et pots permetre en un dia.
En segon lloc, no sobredissenya. Aquesta base de codi funciona amb regles explícites de la casa — cap bastida de compatibilitat enrere abans del llançament, complexitat només quan un dispositiu demostra que és necessària — i K3 les segueix sense que calgui recordar-les-hi. No va afegir índexs espacials «per més endavant», no va embolcallar cada crida amb comprovacions defensives i no va abstraure de manera especulativa. Fer-li prompts és com treballar amb un col·lega competent que ha llegit les regles de la casa i realment hi creu.
En tercer lloc, si li dones eines, les utilitza amb ganes. Vam connectar utilitats d’imatge — visualitzar, retallar una regió de píxels, canviar la mida — i el model va començar a retallar proactivament la sortida del seu propi renderitzador per diagnosticar els cinc errors de l’arnès anteriors. Tenir l’eina li recordava que havia de mirar.
L’altra meitat: K3 va escriure la majoria dels errors d’aquesta història, inclòs el de l’espai de coordenades que feia desaparèixer les notes. Els seus límits són reals. El que va fer segur el treball no va ser que el model tingués raó; va ser que l’arnès detectava la deriva de renderització i la persona detectava la sensació. I, tanmateix, en el dia a dia, no podia distingir-lo de manera fiable dels models tancats capdavanters que també fem servir — sistemes de classe Opus. En alguns eixos era clarament millor: més ràpid i molt menys inclinat a omplir la base de codi de disseny defensiu.
Com volem construir a partir d’ara
Ja hem acabat amb el desenvolupament agèntic basat en grans especificacions — l’estil en què lliures a un model una especificació extensa i acceptes el que en surt. El mode de fallada no és el codi dolent; és un codi que ningú entén.
El que va funcionar aquí, i que mantindrem: passos petits, cadascun discutit abans de començar, cadascun entès per la persona abans de construir-lo, cadascun verificat al dispositiu on viurà. La feina del model és ser ràpid, precís i honest sobre la incertesa. La feina de la persona és aportar criteri, gust i verificació e2e — especialment en programari de consum UI-first, on l’especificació no pot descriure com se sent allò que és «correcte». Kimi K3 a Fireworks resulta estar molt ben adaptat precisament a aquest cicle: prou ràpid per mantenir-lo ajustat, prou intel·ligent per mantenir els passos petits i nets.
L’ona ha desaparegut, la pipeline és una sola idea en lloc de dues i el procés que ens hi ha portat es queda.