Lulucat

Kimi K3 ഉപയോഗിച്ച് Lulucat Notes-ന്റെ റെൻഡറിംഗ് പൈപ്പ്ലൈൻ പുനർനിർമ്മിക്കുന്നു

Gaoge ZhangGaoge Zhang

ഞങ്ങളുടെ Core Graphics tile pipeline ആറ് ചെറിയ ഘട്ടങ്ങളിലൂടെ Metal point-sprite pipeline ആയി മാറി; ഓരോ ഘട്ടവും ഒരു യഥാർത്ഥ iPad-ൽ verify ചെയ്തു. Fireworks-ൽ Kimi K3-നൊപ്പം pair programming ചെയ്താണ് code എഴുതിയത്.

പുതുക്കിയത്

കഴിഞ്ഞ ആഴ്ച, Lulucat Notes-ൽ ഒരു lasso selection വലിച്ചുനീക്കുമ്പോൾ ദൃശ്യമായ ഒരു തരംഗം ഉണ്ടായി: ഒരേ frame-ൽ ചില screen tiles selection-നെ പുതിയ സ്ഥാനത്ത് കാണിച്ചപ്പോൾ മറ്റുള്ളവ പഴയ സ്ഥാനത്ത് തന്നെയായിരുന്നു. Core Graphics bitmap-ും CATiledLayer-ഉം ചേർന്ന മുഴുവൻ rendering pipeline-നെ Metal ഉപയോഗിച്ച് മാറ്റിസ്ഥാപിച്ചു. അടുത്ത ഘട്ടം തുടങ്ങുന്നതിന് മുമ്പ് ഓരോന്നും യഥാർത്ഥ iPad-ൽ പരിശോധിച്ച ആറ് ചെറിയ ഘട്ടങ്ങളിലൂടെയായിരുന്നു ഈ മാറ്റം.

Fireworks-ൽ പ്രവർത്തിക്കുന്ന Moonshot-ന്റെ open model ആയ Kimi K3-നൊപ്പം pair programming ചെയ്താണ് code എഴുതിയത്. മനുഷ്യൻ നയിക്കുകയും തീരുമാനിക്കുകയും test ചെയ്യുകയും ചെയ്തു; model മിക്കവാറും എല്ലാ വരികളും എഴുതി.

പൈപ്പ്ലൈൻ

പഴയ pipeline-ൽ പതുക്കെ പരസ്പരം അകന്നുപോയ drawing-ന്റെ രണ്ട് രീതികളുണ്ടായിരുന്നു: വിലകുറഞ്ഞ display-നായി strokes bitmap-ലേക്ക് bake ചെയ്തു, tiles-ന് കൂടുതൽ detail ആവശ്യമാകുമ്പോഴെല്ലാം vectors ആയി വീണ്ടും വരച്ചു. പുതിയ pipeline-ന് കൃത്യമായി ഒരു ആശയമേയുള്ളൂ: മഷിപോലെയുള്ളതെല്ലാം point sprite ആണ്. ഒരു pen stroke, ഒരു highlighter sweep, ഒരു eraser dab — ഇവ മൂന്നും ഒരേ 32-byte vertex ആണ് — position, diameter, color — stroke-ന്റെ arc length-നൊപ്പം ഒരു point അകലത്തിൽ ഇടവിട്ട് നിർത്തിയ GPU-rasterized circles ആയി അതേ shader pair വരയ്ക്കുന്നു.

ഒരേ curved stroke-ന്റെ മൂന്ന് panels: input touch points, arc length അനുസരിച്ച് curve-ൽ ഇടവിട്ട് നിർത്തിയ circular stamps, ഒപ്പം composite ചെയ്ത solid stroke.

ഒരു point spacing-ൽ circles-ന്റെ ഒരു chain ഗണിതപരമായി പൂർണ്ണമായ capsule-ൽ നിന്ന് ഏകദേശം 0.075 points വ്യതിചലിക്കുന്നു — ഞങ്ങളുടെ canvas density-യിൽ ഒരു pixel-ന്റെ അഞ്ചിലൊന്ന്. പകരമായി, മൂന്ന് tools ഒരൊറ്റ code path-ലേക്ക് ചുരുങ്ങുന്നു; GPU താൻ ഏറ്റവും നന്നായി ചെയ്യുന്ന ജോലി ചെയ്യുന്നു.

ആ ആശയത്തിന് ചുറ്റുമുള്ള architecture ലളിതമാണ്. Committed ink ഒരൊറ്റ 4096² texture-ൽ നിലകൊള്ളുന്നു. UIScrollView നിലനിൽക്കുന്നു, പക്ഷേ ഒരു ശുദ്ധ gesture engine ആയി മാത്രം അതിന്റെ പങ്ക് ചുരുക്കിയിരിക്കുന്നു: അതിന്റെ contentOffset-ഉം zoomScale-ഉം ഓരോ frame-ലും viewport uniform-ലേക്ക് നൽകുന്നു, അതിനാൽ panning-ഉം zooming-ഉം ഒന്നും എഴുതുന്നില്ല. ഓരോ frame-ലും അഞ്ച് draws ഉണ്ട്:

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

Edits texture-ലേക്ക് നേരിട്ട് write ചെയ്യുന്നു. ഒരു stroke commit ചെയ്യുമ്പോൾ അതിന്റെ stamps ചേർക്കുന്നു. Erasing, deleting, moving, undoing എന്നിവ scissor rectangle-ന് പിന്നിൽ ബാധിച്ച region replay ചെയ്യുന്നു: region clear ചെയ്യുക, അതിനെ intersect ചെയ്യുന്ന strokes വീണ്ടും വരയ്ക്കുക, കഴിഞ്ഞു. ഭാഗിക erasure മുൻ പോസ്റ്റിലെ ownership semantics നിലനിർത്തുന്നു — ഒരു erasure അതിൽ നിന്ന് ink നീക്കം ചെയ്യുന്ന stroke-ന്റേതാണ് — ഓരോ erased stroke-ഉം scratch texture-ലേക്ക് draw ചെയ്ത്, destination-out blending () ഉപയോഗിച്ച് അതിന്റെ സ്വന്തം erasure paths subtract ചെയ്ത്, result വീണ്ടും composite ചെയ്തുകൊണ്ട്. ഈ scratch isolation ആണ് eraser paper-ലേക്കോ സമീപത്തെ strokes-ലേക്കോ കടന്നുകയറുന്നത് തടയുന്നത്.

ഇതെല്ലാം ആരംഭിച്ച selection ഇപ്പോൾ point sprites ആയും വരയ്ക്കുന്നു. അത് വലിച്ചുനീക്കുമ്പോൾ ഒരു uniform offset update ചെയ്യുന്നു. Zero texture writes, zero tile invalidations — wave mitigate ചെയ്തതല്ല; ഘടനാപരമായി ഇല്ലാതായതാണ്.

ഗേറ്റ്കീപ്പർ: ഒരു പിക്സൽ വ്യത്യാസം

ഞങ്ങൾ Core Graphics renderer delete ചെയ്തില്ല. അതിനെ ഒരു offline reference implementation ആയി ചുരുക്കി, device-ൽ capture ചെയ്ത യഥാർത്ഥ stroke data ഉപയോഗിച്ച് ഓരോ Metal change-ഉം അതിനെതിരായ pixel comparison pass ചെയ്യണം. Acceptance criterion “identical pixels” എന്നല്ല — രണ്ട് ശരിയായ rasterizers-ക്കും anti-aliased edges-ൽ കുറച്ച് gray levels വ്യത്യാസപ്പെടുന്നത് ന്യായമാണ്. മാനദണ്ഡം ഘടനാപരമാണ്: missing ink ഇല്ല, offset ഇല്ല, color drift ഇല്ല, ink-ൽ നിന്ന് അകലെയുള്ളിടത്ത് വലിയ difference ഇല്ല.

ഒരേ handwritten notes-ന്റെ മൂന്ന് crops: Core Graphics-ൽ render ചെയ്തത്, Metal point sprites-ൽ render ചെയ്തത്, ഒപ്പം അവരുടെ pixel difference ആറ് മടങ്ങ് amplify ചെയ്തത്; stroke edges-ൽ മാത്രം മങ്ങിയ outlines കാണിക്കുന്നു.

Offline renderer നിർമ്മിക്കുമ്പോൾ നേരിട്ട അഞ്ച് bugs-ൽ നാലെണ്ണം ഈ harness പിടിച്ചു. Output-നെ pixel level-ലേക്ക് crop ചെയ്ത് എല്ലാം diagnose ചെയ്തു: Swift/Metal struct stride mismatch (28 bytes-നും 32-നും ഇടയിൽ, കാരണം Metal float4-നെ 16-ലേക്ക് align ചെയ്യുന്നു — screen നിറയെ color blocks ആയി), fragment shader-ൽ [[point_size]]-നെ varying ആയി വായിക്കാൻ കഴിയാത്തത് (ഓരോ stamp-ും square ആയി), ഒരേ command buffer-ൽ രണ്ട് render encoders ഒരുമിച്ച് നിലനിന്നത് (എല്ലാം black), കൂടാതെ quick strokes-ന്റെ ആദ്യ millimeter കാണാതാക്കിയ missing start dab.

ആറ് ഘട്ടങ്ങൾ, ഒരു പുനർഎഴുത്തല്ല

Migration plan സ്വതന്ത്രമായി ship ചെയ്യാവുന്ന ആറ് ഘട്ടങ്ങളായിരുന്നു: pixel diff pass ചെയ്യുന്ന offline renderer; zero visual change ഉള്ള display shell; GPU-യിലെ live stroke; GPU-യിലെ selection; texture-ലേക്ക് നേരിട്ട് write ചെയ്യുന്ന mutations; high-zoom vector re-drawing (“zero visual change” അത് ആവശ്യപ്പെട്ടതിനാൽ step two-ലേക്ക് fold ചെയ്തു). ഓരോ ഘട്ടവും simulator-ഓ screenshot diff-ഓ അല്ല; മേശപ്പുറത്തെ iPad Pro-ൽ മനുഷ്യൻ എഴുതുകയും, മായ്ക്കുകയും, zoom ചെയ്യുകയും, drag ചെയ്യുകയും ചെയ്താണ് അവസാനിച്ചത്.

Device ഓരോ automated check-ഉം കാണാതെ പോയ മൂന്ന് bugs പിടിച്ചു. 100% zoom-ന് മുകളിൽ strokes രണ്ട് തവണ വരച്ചു — താഴെ soft texture, മുകളിൽ sharp sprites — മനുഷ്യൻ നിമിഷങ്ങൾക്കുള്ളിൽ ശ്രദ്ധിച്ച faint blur ആയി അത് തോന്നി. ഒരു stroke commit ചെയ്യുമ്പോൾ ഒരു frame-ലേക്ക് flicker ചെയ്തു, കാരണം പഴയ overlay texture update-നൊപ്പം sync ഇല്ലാതെ cross-fade out ചെയ്തു. 170% zoom-ന് മുകളിൽ എല്ലാ notes-ഉം അപ്രത്യക്ഷമായി: visibility-culling rectangle scaled coordinate space-ൽ contentOffset ഉപയോഗിച്ചതിനാൽ zoom ചെയ്യുമ്പോൾ strokes-ൽ നിന്ന് മാറിപ്പോയി. മൂന്നും ഓരോ function-ലും ഒരു line വീതം മാറ്റുന്ന തരത്തിലുള്ള fixes ആയിരുന്നു; മുൻകൂട്ടി എഴുതാനാകുമായിരുന്ന ഒരു test-നും അവ കണ്ടെത്താനായില്ല, കാരണം അവയ്ക്കായി നോക്കണമെന്ന് ഞങ്ങൾക്കറിയില്ലായിരുന്നു. UI-first consumer app-ൽ മനുഷ്യനെ loop-ൽ നിലനിർത്തുന്നതിന്റെ കാരണം ഇതാണ്.

Kimi K3-നൊപ്പം പ്രവർത്തിക്കുന്നത് എങ്ങനെ

ആദ്യം പറയേണ്ടത്, വേഗതയാണ്. “discuss, write, build, install, look” എന്ന loop മിനിറ്റുകൾക്കുള്ളിൽ നടന്നു; വേഗത്തിൽ ഉത്തരം നൽകുന്ന ഒരു model ഒരു ദിവസം നിങ്ങൾക്ക് എത്ര loops നടത്താനാകുമെന്ന് മാറ്റുന്നു.

രണ്ടാമതായി, അത് over-engineer ചെയ്യുന്നില്ല. ഈ codebase വ്യക്തമായ house rules അനുസരിച്ചാണ് പ്രവർത്തിക്കുന്നത് — launch-ന് മുമ്പ് backward-compatibility scaffolding ഇല്ല, ഒരു device ആവശ്യമാണെന്ന് തെളിയിക്കുമ്പോൾ മാത്രം complexity — K3 ഓർമ്മിപ്പിക്കാതെ തന്നെ അവ പാലിക്കുന്നു. അത് “later” നായി spatial indexes ചേർത്തില്ല, ഓരോ call-ഉം defensive checks-ൽ wrap ചെയ്തില്ല, ഊഹാപോഹമായി abstract ചെയ്തില്ല. House rules വായിച്ച് അവയിൽ യഥാർത്ഥത്തിൽ വിശ്വസിക്കുന്ന കഴിവുള്ള ഒരു colleague-നൊപ്പം പ്രവർത്തിക്കുന്നതുപോലെയാണ് അതിന് prompt നൽകുന്നത്.

മൂന്നാമതായി, tools നൽകിയാൽ അത് ആവേശത്തോടെ ഉപയോഗിക്കും. ഞങ്ങൾ image utilities — view, ഒരു pixel region-ലേക്ക് crop, resize — wire ചെയ്തു; തുടർന്ന് model മുകളിലെ അഞ്ച് harness bugs diagnose ചെയ്യാൻ സ്വന്തം renderer output proactively crop ചെയ്യാൻ തുടങ്ങി. Tool ഉണ്ടായത് നോക്കണമെന്ന് അതിനെ ഓർമ്മിപ്പിച്ചു.

മറ്റേ പകുതി: ഈ കഥയിലെ ഭൂരിഭാഗം bugs-ഉം K3 ആണ് എഴുതിയത്, notes അപ്രത്യക്ഷമാക്കിയ coordinate-space bug ഉൾപ്പെടെ. അതിന്റെ പരിധികൾ യാഥാർത്ഥ്യമാണ്. ജോലി സുരക്ഷിതമാക്കിയത് model ശരിയായിരുന്നുവെന്നല്ല; rendering drift പിടിച്ച harness-ും അനുഭവം മനസ്സിലാക്കിയ മനുഷ്യനും ആയിരുന്നു. എന്നിട്ടും, ദിവസേനയുള്ള ജോലിയിൽ ഞങ്ങൾ ഉപയോഗിക്കുന്ന frontier closed models — Opus-class systems — ഇവയിൽ നിന്ന് അതിനെ വിശ്വസനീയമായി വേർതിരിക്കാനായില്ല. ചില കാര്യങ്ങളിൽ അത് വ്യക്തമായി മികച്ചതായിരുന്നു: വേഗമേറിയത്, codebase-നെ defensive design കൊണ്ട് നിറയ്ക്കാനുള്ള പ്രവണത വളരെ കുറവ്.

ഇനി മുതൽ ഞങ്ങൾ എങ്ങനെ നിർമ്മിക്കണം

വലിയ specification-നെ അടിസ്ഥാനമാക്കിയ agentic development ഞങ്ങൾ അവസാനിപ്പിച്ചു — model-ന് ഒരു വലിയ specification നൽകി അതിൽ നിന്ന് ലഭിക്കുന്നതെന്തും സ്വീകരിക്കുന്ന രീതി. Failure mode മോശം code അല്ല; ആരും മനസ്സിലാക്കാത്ത code ആണ്.

ഇവിടെ പ്രവർത്തിച്ചതും ഞങ്ങൾ നിലനിർത്തുന്നതും ഇതാണ്: ചെറിയ ഘട്ടങ്ങൾ; ഓരോന്നും തുടങ്ങുന്നതിന് മുമ്പ് ചർച്ച ചെയ്യുക, build ചെയ്യുന്നതിന് മുമ്പ് മനുഷ്യൻ മനസ്സിലാക്കുക, അത് പ്രവർത്തിക്കേണ്ട device-ൽ verify ചെയ്യുക. Model-ന്റെ ജോലി വേഗതയുള്ളതും കൃത്യവും uncertainty-യെക്കുറിച്ച് സത്യസന്ധവുമായിരിക്കുക. മനുഷ്യന്റെ ജോലി judgment, taste, e2e verification എന്നിവയാണ് — പ്രത്യേകിച്ച് UI-first consumer software-ൽ, “right” എങ്ങനെ അനുഭവപ്പെടുന്നു എന്ന് specification-ന് വിവരിക്കാനാകാത്തിടത്ത്. Fireworks-ലെ Kimi K3 ഈ loop-ന് യോജിച്ചാണ് രൂപപ്പെട്ടതെന്ന് തെളിഞ്ഞു: loop കെട്ടുറപ്പോടെ നിലനിർത്താൻ മതിയായ വേഗത, ഘട്ടങ്ങൾ ചെറുതും വൃത്തിയുള്ളതുമായി നിലനിർത്താൻ മതിയായ ബുദ്ധി.

തരംഗം ഇല്ലാതായി, pipeline രണ്ടിന് പകരം ഒരൊറ്റ ആശയമായി, ഞങ്ങളെ അവിടെയെത്തിച്ച പ്രക്രിയ തുടരും.