Kimi K3 सोबत Lulucat Notes ची रेंडरिंग पाइपलाइन पुन्हा उभारणे
आमची Core Graphics tile pipeline सहा छोट्या पायऱ्यांत Metal point-sprite pipeline झाली; प्रत्येक पायरी खऱ्या iPad वर तपासली. कोड Fireworks वरील Kimi K3 सोबत pair programming करून लिहिला.
गेल्या आठवड्यात, Lulucat Notes मध्ये lasso selection ओढल्यावर दिसणारी एक लाट तयार होत होती: त्याच frame मध्ये काही screen tiles selection नवीन जागी दाखवत होते, तर काही अजूनही जुनी जागा दाखवत होते. आम्ही संपूर्ण rendering pipeline — Core Graphics bitmap आणि CATiledLayer — Metal ने बदलली. हा बदल सहा छोट्या पायऱ्यांत केला; पुढची पायरी सुरू करण्यापूर्वी प्रत्येक पायरी खऱ्या iPad वर तपासली.
कोड Fireworks वर चालणाऱ्या Moonshot च्या open model Kimi K3 सोबत pair programming करून लिहिला. माणसाने दिशा दिली, निर्णय घेतले आणि चाचण्या केल्या; model ने जवळपास प्रत्येक line लिहिली.
पाइपलाइन
जुन्या pipeline मध्ये हळूहळू एकमेकांपासून दूर जाणाऱ्या drawing च्या दोन पद्धती होत्या: स्वस्त display साठी strokes bitmap मध्ये bake केले जात आणि tiles ना अधिक detail लागला की vectors म्हणून पुन्हा काढले जात. नवीन pipeline मध्ये एकच कल्पना आहे: ink सारखे दिसणारे सगळे point sprite आहे. Pen stroke, highlighter sweep आणि eraser dab हे एकच 32-byte vertex आहे — position, diameter, color — जे stroke च्या arc length वर एका point च्या अंतराने ठेवलेल्या GPU-rasterized circles प्रमाणे त्याच shader pair ने draw केले जाते.
![]()
एका point च्या अंतरावर circles ची साखळी गणितीयदृष्ट्या परिपूर्ण capsule पासून सुमारे 0.075 points ने विचलित होते — आमच्या canvas density वर एका pixel चा पाचवा भाग. बदल्यात, तीन tools एका code path मध्ये एकत्र येतात आणि GPU तेच करते ज्यात ती सर्वोत्तम आहे.
त्या कल्पनेभोवती architecture साधे आहे. Committed ink एका 4096² texture मध्ये राहते. UIScrollView टिकून आहे, पण pure gesture engine म्हणून तिची भूमिका कमी केली आहे: तिचे contentOffset आणि zoomScale प्रत्येक frame ला viewport uniform ला feed होतात, त्यामुळे panning आणि zooming काहीही write करत नाहीत. प्रत्येक 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 जोडले जातात. Erase, delete, move आणि undo हे scissor rectangle च्या मागे प्रभावित region replay करतात: region clear करा, त्याला intersect करणारे strokes पुन्हा draw करा, झाले. Partial erasure मागील post मधील ownership semantics टिकवते — एखादी erasure ज्या stroke मधून ink काढते त्याच stroke ची असते — प्रत्येक erased stroke scratch texture मध्ये draw करून, destination-out blending (
ज्या selection मुळे हे सगळे सुरू झाले ते आता point sprites म्हणूनही draw केले जाते. ते ओढल्यावर एक uniform offset update होतो. Zero texture writes, zero tile invalidations — wave फक्त कमी झालेली नाही; ती structurally नाहीशी झाली आहे.
गेटकीपर: एक पिक्सेल फरक
आम्ही Core Graphics renderer delete केला नाही. त्याला offline reference implementation म्हणून demote केले, आणि device वर capture केलेल्या खऱ्या stroke data विरुद्ध प्रत्येक Metal change ने pixel comparison पास केली पाहिजे. Acceptance criterion “identical pixels” नाही — दोन अचूक rasterizers anti-aliased edges वर काही gray levels ने वेगळे असणे योग्य आहे. गेटची कसोटी संरचनात्मक आहे: missing ink नाही, offset नाही, color drift नाही आणि ink पासून दूर कुठेही मोठा difference नाही.
![]()
Offline renderer बांधताना आलेल्या पाच bugs पैकी चार या harness ने पकडले. Output pixel level पर्यंत crop करून सर्वांचे निदान झाले: Swift/Metal struct stride mismatch (28 bytes विरुद्ध 32, कारण Metal float4 ला 16 वर align करते — screen color blocks ने भरली), fragment shader मध्ये [[point_size]] varying म्हणून वाचता न येणे (प्रत्येक stamp चौकोनी झाला), एकाच command buffer वर दोन render encoders सहअस्तित्वात असणे (सगळे black), आणि सुरुवातीचा dab नसल्यामुळे जलद strokes चा पहिला millimeter अदृश्य राहणे.
सहा पायऱ्या, एक पुनर्लेखन नाही
Migration plan स्वतंत्रपणे ship करता येणाऱ्या सहा पायऱ्यांचा होता: pixel diff पास करणारा 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 मध्ये समाविष्ट केले). प्रत्येक पायरी simulator किंवा screenshot diff ने नव्हे, तर desk वरच्या iPad Pro वर माणसाने लिहून, पुसून, zoom करून आणि drag करून संपवली.
Device ने प्रत्येक automated check चुकवलेले तीन bugs पकडले. 100% zoom पेक्षा वर strokes दोनदा draw झाले — खाली soft texture, वर sharp sprites — आणि माणसाला काही सेकंदांत जाणवलेला faint blur दिसू लागला. Stroke commit करताना एका frame साठी flicker झाला, कारण जुना overlay texture update शी sync नसताना cross-fade out झाला. 170% zoom पेक्षा वर प्रत्येक note गायब झाले: visibility-culling rectangle ने scaled coordinate space मध्ये contentOffset वापरले, त्यामुळे zoom करताना ते strokes पासून दूर drift झाले. तिन्ही fixes एका function मधील एका line इतक्याच होत्या, आणि आधी लिहिता आले असते अशा कोणत्याही 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 मध्ये गुंडाळला नाही आणि speculative abstraction केले नाही. त्याला prompt करणे म्हणजे house rules वाचलेल्या आणि त्यांवर खरोखर विश्वास ठेवणाऱ्या कुशल सहकाऱ्यासोबत काम करण्यासारखे वाटते.
तिसरे, tools द्या आणि ते उत्साहाने वापरतो. आम्ही image utilities — view, pixel region पर्यंत crop, resize — जोडल्या; मग 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 दोनऐवजी एकच कल्पना राहिली, आणि आम्हाला तिथे घेऊन जाणारी प्रक्रिया कायम राहील.