Kimi K3 के साथ Lulucat Notes की रेंडरिंग पाइपलाइन का पुनर्निर्माण
हमारी Core Graphics टाइल पाइपलाइन छह छोटे चरणों में Metal की point-sprite पाइपलाइन बन गई, और हर चरण को एक वास्तविक iPad पर सत्यापित किया गया। कोड को Fireworks पर Kimi K3 के साथ जोड़ी-प्रोग्रामिंग से लिखा गया।
पिछले हफ़्ते, Lulucat Notes में लासो चयन को खींचने पर एक दिखाई देने वाली लहर पैदा होती थी: कुछ स्क्रीन टाइलें चयन को उसके नए स्थान पर दिखाती थीं, जबकि दूसरी उसी फ़्रेम में पुरानी स्थिति दिखाती रहती थीं। हमने पूरी रेंडरिंग पाइपलाइन — Core Graphics बिटमैप और CATiledLayer — को छह छोटे चरणों में Metal से बदल दिया; अगला चरण शुरू करने से पहले हर चरण को एक वास्तविक iPad पर सत्यापित किया गया।
कोड को Fireworks पर चल रहे Moonshot के खुले मॉडल Kimi K3 के साथ जोड़ी-प्रोग्रामिंग से लिखा गया। इंसान दिशा देता, निर्णय लेता और परीक्षण करता था; मॉडल ने लगभग हर पंक्ति लिखी।
Pipeline
पुरानी पाइपलाइन में ड्रॉ करने के दो तरीके थे, जो धीरे-धीरे अलग होते गए: सस्ते प्रदर्शन के लिए स्ट्रोक्स को बिटमैप में समाहित किया जाता था, और टाइलों को अधिक विवरण चाहिए होने पर उन्हें वेक्टर के रूप में फिर से ड्रॉ किया जाता था। नई पाइपलाइन का ठीक एक विचार है: स्याही जैसी हर चीज़ point sprite है। पेन स्ट्रोक, हाइलाइटर स्वेप और इरेज़र डैब एक ही 32-byte vertex हैं — position, diameter, color — जिन्हें वही shader pair स्ट्रोक की arc length पर एक point की दूरी पर रखे GPU-rasterized circles के रूप में ड्रॉ करता है।
![]()
एक point की दूरी पर रखे गए circles की शृंखला गणितीय रूप से पूर्ण capsule से लगभग 0.075 points अलग होती है — हमारे canvas density पर pixel का पाँचवाँ हिस्सा। बदले में, तीन उपकरण एक code path में सिमट जाते हैं, और GPU वही करता है जिसमें वह सबसे अच्छा है।
इस विचार के आसपास architecture सरल है। Committed ink एक ही 4096² texture में रहती है। UIScrollView बना रहता है, लेकिन उसे केवल gesture engine तक सीमित कर दिया गया है: उसका contentOffset और zoomScale हर फ़्रेम viewport uniform को feed करते हैं, इसलिए panning और zooming कुछ नहीं लिखते। हर फ़्रेम में पाँच draw operations होते हैं:
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 में लिखे जाते हैं। Stroke commit उसके stamps जोड़ता है। Erasing, deleting, moving और undo scissor rectangle के भीतर प्रभावित क्षेत्र को replay करते हैं: क्षेत्र साफ़ करो, उससे intersect करने वाले strokes फिर से draw करो, बस। Partial erasure पिछली पोस्ट की ownership semantics बनाए रखती है — erasure उसी stroke की होती है जिससे वह ink हटाती है — हर erased stroke को scratch texture में draw करके, उसके अपने erasure paths को destination-out blending (
जिस selection ने यह सब शुरू किया था, वह अब point sprites के रूप में भी draw होती है। उसे खींचने से एक uniform offset अपडेट होता है। Texture में कोई लेखन नहीं, tiles में कोई invalidation नहीं — लहर संरचनात्मक रूप से गायब है, केवल कम नहीं की गई है।
प्रहरी: एक पिक्सेल अंतर
हमने Core Graphics renderer को हटाया नहीं। उसे ऑफ़लाइन reference implementation तक सीमित कर दिया, और हर Metal परिवर्तन को डिवाइस पर कैप्चर किए गए वास्तविक stroke data के आधार पर उसके विरुद्ध pixel comparison पास करना पड़ता है। स्वीकृति मानदंड “एक जैसे पिक्सेल” नहीं है — दो सही rasterizers anti-aliased edges पर कुछ gray levels तक वैध रूप से अलग हो सकते हैं। मानदंड संरचनात्मक है: कोई ink missing नहीं, कोई offset नहीं, कोई color drift नहीं और ink से दूर कहीं भी बड़ा difference नहीं।
![]()
इस harness ने ऑफ़लाइन renderer बनाते समय मिले पाँच में से चार bugs पकड़ लिए; सभी का diagnosis output को pixel level तक crop करके हुआ: Swift/Metal struct का stride mismatch (28 bytes बनाम 32, क्योंकि Metal float4 को 16 पर align करता है — स्क्रीन color blocks से भर गई), [[point_size]] को fragment shader में varying की तरह पढ़ा नहीं जा सकता था (हर stamp square निकला), एक command buffer पर दो render encoders का साथ मौजूद होना (सब काला) और missing start dab, जिससे तेज़ strokes का पहला millimeter अदृश्य हो गया।
छह चरण, एक पुनर्लेखन नहीं
स्थानांतरण योजना छह स्वतंत्र रूप से जारी किए जा सकने वाले चरणों की थी: पिक्सेल अंतर पास करने वाला ऑफ़लाइन रेंडरर; बिना किसी दृश्य बदलाव वाला डिस्प्ले शेल; GPU पर live stroke; GPU पर selection; texture में सीधे लिखने वाले mutations; high-zoom vector re-drawing (दूसरे चरण में समाहित किया गया, क्योंकि “zero visual change” के लिए यह ज़रूरी था)। हर चरण के अंत में एक इंसान — simulator नहीं, screenshot diff नहीं — डेस्क पर रखे iPad Pro पर लिखता, मिटाता, zoom करता और drag करता था।
डिवाइस ने तीन बग पकड़े, जिन्हें हर स्वचालित जाँच ने छोड़ दिया था। 100% zoom से ऊपर स्ट्रोक्स दो बार ड्रॉ हो रहे थे — नीचे नरम texture, ऊपर तीखे sprites — जिससे हल्का blur दिखता था और इंसान ने उसे seconds में नोटिस कर लिया। किसी stroke को commit करने पर एक फ़्रेम के लिए flicker होता था, क्योंकि पुराना overlay texture update के साथ असमय cross-fade होता था। और 170% zoom से ऊपर हर note गायब हो जाता था: visibility-culling rectangle अपने scaled coordinate space में contentOffset का उपयोग करता था, इसलिए zoom करने पर strokes से दूर drift होता जाता था। तीनों one-line-to-one-function fixes थे, और कोई भी उस test में नहीं था जिसे हम पहले से लिख सकते थे, क्योंकि हमें पता ही नहीं था कि क्या देखना है। UI-first consumer app के लिए इसी वजह से इंसान loop में रहता है।
Kimi K3 के साथ काम कैसा लगता है
सबसे पहले, तेज़। “चर्चा करो, लिखो, बनाओ, इंस्टॉल करो, देखो” का चक्र मिनटों में चलता था, और जल्दी जवाब देने वाला मॉडल बदल देता है कि एक दिन में आप कितने चक्र चला सकते हैं।
दूसरा, यह जरूरत से ज़्यादा इंजीनियरिंग नहीं करता। यह codebase स्पष्ट आंतरिक नियमों पर चलता है — launch से पहले backward-compatibility scaffolding नहीं, और complexity केवल तभी जब कोई device इसे ज़रूरी साबित करे — और K3 को याद दिलाए बिना उनका पालन करता है। उसने “बाद के लिए” spatial indexes नहीं जोड़े, हर call को defensive checks में नहीं लपेटा और speculative abstraction नहीं बनाई। इसे निर्देश देना ऐसे सक्षम सहकर्मी के साथ काम करने जैसा लगता है जिसने आंतरिक नियम पढ़ रखे हैं और वास्तव में उन पर विश्वास करता है।
तीसरा, इसे tools दीजिए और यह उत्साह से उनका उपयोग करता है। हमने image utilities — view, एक pixel region तक crop और resize — जोड़ीं, और मॉडल ने ऊपर बताए गए पाँच harness bugs का diagnosis करने के लिए अपने renderer के output को सक्रिय रूप से crop करना शुरू कर दिया। Tool मौजूद होने से उसे देखने की याद आई।
दूसरी बात: K3 ने इस कहानी के ज़्यादातर bugs लिखे, coordinate-space वाला bug भी, जिससे notes गायब हो गए थे। इसकी सीमाएँ वास्तविक हैं। काम सुरक्षित इसलिए नहीं हुआ कि मॉडल सही था; harness ने rendering drift पकड़ी और इंसान ने उपयोग का अनुभव पकड़ा। फिर भी, रोज़मर्रा में मैं इसे उन frontier closed models — Opus-class systems — से विश्वसनीय रूप से अलग नहीं कर पाता था जिन्हें हम भी इस्तेमाल करते हैं। कुछ पहलुओं में यह साफ़ बेहतर था: तेज़, और codebase को defensive design से भरने की प्रवृत्ति बहुत कम।
अब से हम कैसे बनाना चाहते हैं
हम बड़े specification आधारित agentic development से आगे बढ़ चुके हैं — वह शैली जिसमें आप मॉडल को बड़ा specification देते हैं और जो बनकर आता है उसे स्वीकार कर लेते हैं। Failure mode खराब code नहीं है; वह code है जिसे कोई नहीं समझता।
यहाँ जो काम आया और जिसे हम रखेंगे: छोटे चरण, प्रत्येक शुरू होने से पहले चर्चा किया हुआ, build होने से पहले इंसान द्वारा समझा हुआ और उस device पर verified जिस पर वह चलेगा। मॉडल का काम तेज़, सटीक और uncertainty के बारे में ईमानदार होना है। इंसान का काम judgment, taste और e2e verification है — खासकर UI-first consumer software के लिए, जहाँ spec यह नहीं बता सकता कि “सही” कैसा महसूस होता है। Fireworks पर Kimi K3 ठीक इसी loop के लिए बना हुआ साबित हुआ: loop को tight रखने के लिए पर्याप्त तेज़, steps को छोटा और साफ़ रखने के लिए पर्याप्त समझदार।
लहर गायब है, pipeline दो की जगह एक विचार है, और जो प्रक्रिया हमें यहाँ तक लाई वह बनी रहेगी।