Kimi K3 দিয়ে Lulucat Notes-এর rendering pipeline পুনর্গঠন
আমাদের Core Graphics tile pipeline ছয়টি ছোট ধাপে Metal point-sprite pipeline-এ পরিণত হয়েছে; প্রতিটি ধাপ বাস্তব iPad-এ যাচাই করা হয়েছে। Fireworks-এ Kimi K3-এর সঙ্গে pair-programming করে কোড লেখা হয়েছে।
গত সপ্তাহে Lulucat Notes জুড়ে lasso selection টানলে একটি স্পষ্ট ঢেউ দেখা যেত: একই frame-এ স্ক্রিনের কিছু tile selection-কে তার নতুন অবস্থানে দেখাত, আর অন্যগুলি পুরনো অবস্থানেই রাখত। আমরা পুরো rendering pipeline—Core Graphics bitmap এবং CATiledLayer—Metal দিয়ে প্রতিস্থাপন করেছি, ছয়টি ছোট ধাপে; পরের ধাপে যাওয়ার আগে প্রতিটি ধাপ বাস্তব iPad-এ যাচাই করা হয়েছে।
কোডটি Fireworks-এ চলা Moonshot-এর open model Kimi K3-এর সঙ্গে pair-programming করে লেখা হয়েছে। মানুষটি নেতৃত্ব দিয়েছে, সিদ্ধান্ত নিয়েছে এবং test করেছে; model-টি প্রায় প্রতিটি line লিখেছে।
Pipeline
পুরনো pipeline-এ আঁকার দুটি পদ্ধতি ধীরে ধীরে আলাদা হয়ে গিয়েছিল: সস্তায় display করার জন্য stroke-গুলি bitmap-এ baked থাকত, আর tile-এ বেশি detail দরকার হলে vector হিসেবে আবার আঁকা হত। নতুন pipeline-এর একটিই ধারণা: ink-এর মতো সবকিছুই একটি point sprite। একটি pen stroke, একটি highlighter sweep এবং একটি eraser dab—সবই একই 32-byte vertex: position, diameter, color। এগুলি একই shader pair দিয়ে আঁকা GPU-rasterized circle, যেগুলি stroke-এর arc length বরাবর এক point দূরত্বে বসানো।
![]()
এক point spacing-এ circle-এর একটি chain mathematically perfect capsule থেকে প্রায় 0.075 point বিচ্যুত হয়—আমাদের canvas density-তে এক pixel-এর এক-পঞ্চমাংশ। বিনিময়ে তিনটি tool একটি code path-এ মিশে যায়, এবং GPU তার সবচেয়ে ভালো কাজটি করে।
এই ধারণাকে ঘিরে architecture-টি সরল। Committed ink একটি 4096² texture-এ থাকে। UIScrollView টিকে আছে, কিন্তু সেটিকে কেবল gesture engine-এ নামিয়ে আনা হয়েছে: তার contentOffset এবং zoomScale প্রতি frame-এ একটি viewport uniform-এ যায়, তাই pan এবং zoom কিছু write করে না। প্রতি frame-এ পাঁচটি draw হয়:
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
এডিটগুলি সরাসরি texture-এ লেখে। একটি stroke commit করলে তার stamp যোগ হয়। Erase, delete, move এবং undo-তে scissor rectangle-এর আড়ালে প্রভাবিত region replay করা হয়: region clear করো, যেসব stroke সেটিকে intersect করে সেগুলি redraw করো, শেষ। Partial erasure আগের পোস্ট থেকে পাওয়া ownership semantics বজায় রাখে—যে stroke থেকে ink সরানো হচ্ছে, erasure তারই মালিকানায়—কারণ প্রতিটি erased stroke একটি scratch texture-এ আঁকা হয়, destination-out blending (
যে selection এই সব শুরু করেছিল, সেটিও এখন point sprite হিসেবে আঁকা হয়। সেটি drag করলে একটি uniform offset update হয়। Texture-এ কোনও write নেই, tile invalidation-ও নেই—ঢেউটি কমানো নয়, গঠনগতভাবেই উধাও।
Gatekeeper: একটি pixel diff
আমরা Core Graphics renderer মুছে ফেলিনি। সেটিকে offline reference implementation-এ নামিয়ে এনেছি, এবং প্রতিটি Metal change-কে on-device capture করা বাস্তব stroke data-এর বিরুদ্ধে একটি pixel comparison পাস করতে হয়। গ্রহণযোগ্যতার মানদণ্ড “identical pixels” নয়—দুটি সঠিক rasterizer anti-aliased edge বরাবর স্বাভাবিকভাবেই কয়েকটি gray level-এ আলাদা হতে পারে। Gate-টি কাঠামোগত: কোনও ink missing নয়, কোনও offset নয়, কোনও color drift নয়, এবং ink থেকে দূরের কোথাও বড় difference নয়।
![]()
এই harness offline renderer তৈরি করার সময় আমরা যে পাঁচটি bug পেয়েছিলাম, তার মধ্যে চারটি ধরেছিল—সবকটিই output-কে pixel level-এ crop করে diagnose করা হয়েছিল: একটি Swift/Metal struct stride mismatch (28 bytes বনাম 32, কারণ Metal float4-কে 16-এ align করে—স্ক্রিন color blocks-এ ভরে গিয়েছিল), fragment shader-এ [[point_size]] varying হিসেবে unreadable ছিল (প্রতিটি stamp square হয়ে বেরোত), একটি command buffer-এ দুটি render encoder একসঙ্গে ছিল (সবকিছু কালো), এবং start dab না থাকায় দ্রুত stroke-এর প্রথম millimeter অদৃশ্য হয়ে যেত।
ছয়টি ধাপ, একবারে rewrite নয়
Migration plan-টি ছিল ছয়টি independently shippable ধাপ: pixel diff পাস করা offline renderer; zero visual change-সহ display shell; GPU-তে live stroke; GPU-তে selection; সরাসরি texture-এ write করা mutations; high-zoom vector re-drawing (step two-তে fold করা হয়েছিল, কারণ “zero visual change” সেটিই দাবি করেছিল)। প্রতিটি ধাপের শেষে ডেস্কে থাকা iPad Pro-তে একজন মানুষ লিখত, মুছত, zoom করত এবং drag করত—simulator নয়, screenshot diff নয়।
Device এমন তিনটি bug ধরেছিল, যেগুলি প্রতিটি automated check এড়িয়ে গিয়েছিল। 100% zoom-এর ওপরে stroke দুবার আঁকা হচ্ছিল—নিচে soft texture, ওপরে sharp sprite—যা একজন মানুষ কয়েক সেকেন্ডেই একটি হালকা blur হিসেবে টের পেয়েছিল। একটি stroke commit করলে এক frame flicker করত, কারণ পুরনো overlay texture update-এর সঙ্গে sync না রেখে cross-fade হয়ে বেরোত। আর 170% zoom-এর ওপরে প্রতিটি note অদৃশ্য হয়ে যেত: visibility-culling rectangle তার scaled coordinate space-এ contentOffset ব্যবহার করছিল, তাই zoom করার সঙ্গে সঙ্গে stroke থেকে সরে যাচ্ছিল। তিনটিরই সমাধান ছিল এক line থেকে এক function-এর fix, এবং আগে থেকে লেখা কোনও test-এ এগুলির কোনোটিই থাকত না, কারণ কী খুঁজতে হবে তা আমরা জানতাম না। UI-first consumer app-এ তাই মানুষকে loop-এ রাখা হয়।
Kimi K3-এর সঙ্গে কাজ করার অভিজ্ঞতা
সবার আগে, এটি দ্রুত। “আলোচনা করো, লেখো, build করো, install করো, দেখো”—এই loop কয়েক মিনিটে চলত, আর দ্রুত উত্তর দেওয়া একটি model দিনে কতগুলি loop চালানোর সুযোগ দেয়, সেটিই বদলে দেয়।
দ্বিতীয়ত, এটি over-engineer করে না। এই codebase explicit house rules মেনে চলে—launch-এর আগে backward-compatibility scaffolding নয়, আর device প্রয়োজনীয়তা প্রমাণ না করা পর্যন্ত complexity নয়—এবং K3-কে মনে করিয়ে না দিলেও সে সেগুলি অনুসরণ করে। এটি “পরে লাগবে” বলে spatial index যোগ করেনি, প্রতিটি call defensive check-এ মুড়িয়ে দেয়নি, speculative abstraction বানায়নি। তাকে prompt করা এমন একজন competent colleague-এর সঙ্গে কাজ করার মতো, যে house rules পড়েছে এবং সত্যিই সেগুলিতে বিশ্বাস করে।
তৃতীয়ত, tools দিলে সে আগ্রহের সঙ্গে ব্যবহার করে। আমরা image utilities wired up করেছিলাম—view, pixel region-এ crop, resize—আর model নিজের renderer output proactive ভাবে crop করে উপরের পাঁচটি harness bug diagnose করতে শুরু করেছিল। Tool থাকায় তাকে দেখতে মনে পড়েছিল।
অন্য অর্ধেকটিও মনে রাখতে হবে: এই গল্পের বেশিরভাগ bug K3-ই লিখেছিল, যার মধ্যে note অদৃশ্য করে দেওয়া coordinate-space bug-টিও ছিল। তার সীমাবদ্ধতা বাস্তব। কাজটিকে safe করেছিল model-এর সবসময় সঠিক থাকা নয়; rendering drift harness-এ ধরা পড়া এবং feel-এর ত্রুটি মানুষ ধরে ফেলা। তবু প্রতিদিনের কাজে আমরা যে frontier closed model-গুলিও ব্যবহার করি—Opus-class systems—সেগুলির থেকে K3-কে নির্ভরযোগ্যভাবে আলাদা করতে পারিনি। কিছু ক্ষেত্রে এটি স্পষ্টতই ভালো ছিল: দ্রুততর, এবং defensive design দিয়ে codebase ফুলিয়ে তোলার প্রবণতা অনেক কম।
এখন থেকে কীভাবে build করতে চাই
আমরা বড়-spec agentic development-এর পদ্ধতি ছেড়ে দিয়েছি—যে পদ্ধতিতে একটি বড় specification model-এর হাতে দিয়ে শেষে যা তৈরি হয়, সেটিই মেনে নেওয়া হয়। Failure mode খারাপ code নয়; failure mode হল এমন code যা কেউ বোঝে না।
এখানে যা কাজ করেছে, এবং আমরা ধরে রাখব: ছোট ধাপ, শুরুর আগে প্রতিটি নিয়ে আলোচনা, build হওয়ার আগে মানুষের বোঝা, এবং যে device-এ code চলবে সেই device-এই প্রতিটি ধাপ যাচাই। Model-এর কাজ দ্রুত, precise এবং uncertainty নিয়ে honest থাকা। মানুষের কাজ judgment, taste এবং e2e verification—বিশেষ করে UI-first consumer software-এ, যেখানে “right” কেমন লাগে তা specification বর্ণনা করতে পারে না। Fireworks-এ Kimi K3 এই loop-এর জন্য ঠিকমতো তৈরি বলে মনে হয়েছে: loop tight রাখার মতো দ্রুত, আর ধাপগুলিকে ছোট ও পরিষ্কার রাখার মতো বুদ্ধিমান।
ঢেউটি চলে গেছে, pipeline দুটি ধারণার বদলে একটি ধারণায় দাঁড়িয়েছে, আর যে process আমাদের সেখানে পৌঁছে দিয়েছে, সেটিই থেকে যাচ্ছে।