Lulucat

Kimi K3 کے ساتھ Lulucat Notes کی rendering pipeline ازسرِنو بنانا

Gaoge ZhangGaoge Zhang

ہماری Core Graphics tile pipeline چھ چھوٹے مراحل میں Metal point-sprite pipeline بن گئی، اور ہر مرحلے کی ایک حقیقی iPad پر توثیق کی گئی۔ کوڈ Fireworks پر Kimi K3 کے ساتھ pair-program کیا گیا۔

تازہ کاری

گزشتہ ہفتے، Lulucat Notes میں lasso selection کو drag کرنے سے ایک نمایاں لہر پیدا ہوئی: ایک ہی frame میں کچھ screen tiles selection کو اس کی نئی position پر دکھاتی تھیں، جبکہ کچھ اب بھی پرانی position دکھا رہی تھیں۔ ہم نے پوری rendering pipeline — Core Graphics bitmap اور CATiledLayer — کو Metal سے بدل دیا؛ یہ کام چھ چھوٹے مراحل میں کیا گیا، اور اگلا مرحلہ شروع کرنے سے پہلے ہر مرحلہ ایک حقیقی iPad پر verify کیا گیا۔

مصنفیت کے بارے میں ایک اور بات، کیونکہ یہ اس تحریر کے دوسرے نصف کے لیے اہم ہے: کوڈ Moonshot کے open model Kimi K3 کے ساتھ pair-program کیا گیا، جو Fireworks پر چل رہا تھا۔ انسان نے رہنمائی کی، فیصلے کیے اور جانچ کی؛ model نے تقریباً ہر سطر لکھی۔

پائپ لائن

پرانی pipeline میں drawing کی دو قسمیں تھیں جو آہستہ آہستہ ایک دوسرے سے الگ ہوتی گئیں: strokes کو کم خرچ display کے لیے bitmap میں bake کیا جاتا، اور جب بھی tiles کو مزید detail درکار ہوتی تو انہیں vectors کے طور پر دوبارہ draw کیا جاتا۔ نئی pipeline کا صرف ایک خیال ہے: جو کچھ بھی ink جیسا ہے وہ point sprite ہے۔ pen stroke، highlighter sweep اور eraser dab ایک ہی 32-byte vertex ہیں — position، diameter، color — اور ایک ہی shader pair انہیں GPU-rasterized circles کے طور پر draw کرتا ہے، جو stroke کی arc length کے ساتھ ایک point کے فاصلے پر رکھے گئے ہیں۔

اسی منحنی stroke کے تین panels: input touch points، arc length کے مطابق منحنی پر ایک point کے فاصلے پر رکھے گئے circular stamps، اور composite ہو کر بننے والا solid stroke۔

ایک point کے فاصلے پر circles کی زنجیر ایک mathematically perfect capsule سے تقریباً 0.075 points ہٹ جاتی ہے — ہماری canvas density پر ایک pixel کا پانچواں حصہ۔ بدلے میں، تینوں tools ایک ہی code path میں سمٹ جاتے ہیں، اور GPU وہی کرتا ہے جس میں وہ سب سے ماہر ہے۔

اسی خیال کے گرد architecture سادہ ہے۔ Committed ink ایک واحد 4096² texture میں رہتی ہے۔ UIScrollView برقرار ہے، مگر اسے ایک خالص gesture engine تک محدود کر دیا گیا ہے: اس کا contentOffset اور zoomScale ہر frame ایک viewport uniform کو feed کرتے ہیں، اس لیے 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 میں لکھتے ہیں۔ ایک stroke کو commit کرنے سے اس کے stamps append ہو جاتے ہیں۔ Erasing، deleting، moving اور undoing ایک scissor rectangle کے پیچھے متاثرہ region کو replay کرتے ہیں: region صاف کریں، اس سے intersect کرنے والے strokes دوبارہ draw کریں، بس۔ Partial erasure پچھلی پوسٹ کی ownership semantics برقرار رکھتی ہے — ایک erasure اسی stroke کی ہوتی ہے جس سے وہ ink ہٹاتی ہے — ہر erased stroke کو scratch texture میں draw کر کے، destination-out blending () سے اس کی اپنی erasure paths subtract کر کے، اور نتیجہ دوبارہ composite کر کے۔ Scratch isolation eraser کو paper یا پڑوسی strokes کے اندر سے کاٹنے سے روکتی ہے۔

جس selection نے یہ سب شروع کیا تھا اب اسے بھی point sprites کے طور پر draw کیا جاتا ہے۔ اسے drag کرنے سے صرف ایک uniform offset update ہوتا ہے۔ صفر texture writes، صفر tile invalidations — لہر ساختی طور پر ختم ہو گئی ہے، محض کم نہیں ہوئی۔

گیٹ کیپر: ایک pixel diff

ہم نے Core Graphics renderer کو delete نہیں کیا۔ اسے ایک offline reference implementation تک محدود کر دیا، اور اب ہر Metal change کو device پر captured حقیقی stroke data پر اس کے خلاف pixel comparison پاس کرنا ہوگا۔ قبولیت کا معیار “identical pixels” نہیں ہے — دو درست rasterizers anti-aliased edges کے ساتھ چند gray levels تک جائز طور پر مختلف ہو سکتے ہیں۔ یہ gate structural ہے: کوئی missing ink نہیں، کوئی offset نہیں، کوئی color drift نہیں، اور ink سے دور کہیں بھی کوئی بڑا فرق نہیں۔

انہی ہاتھ سے لکھے گئے notes کے تین crops: Core Graphics سے render کیے گئے، Metal point sprites سے render کیے گئے، اور ان کا pixel difference چھ گنا بڑھایا گیا، جس میں stroke edges کے ساتھ صرف مدھم outlines نظر آتی ہیں۔

اس harness نے offline renderer بناتے وقت سامنے آنے والے پانچ bugs میں سے چار پکڑ لیے؛ سب کی تشخیص output کو pixel level تک crop کر کے کی گئی: Swift/Metal struct stride mismatch (28 bytes بمقابلہ 32، کیونکہ Metal float4 کو 16 پر align کرتا ہے — screen color blocks سے بھر گئی)، [[point_size]] کا fragment shader میں varying کے طور پر readable نہ ہونا (ہر stamp square نکلا)، ایک ہی command buffer پر دو render encoders کا ساتھ موجود ہونا (سب کچھ black)، اور missing start dab جس نے تیزی سے لکھے گئے strokes کے پہلے millimeter کو invisible کر دیا۔

چھ مراحل، ایک rewrite نہیں

Migration plan چھ independently shippable مراحل پر مشتمل تھا: pixel diff پاس کرتا ہوا offline renderer؛ zero visual change کے ساتھ display shell؛ GPU پر live stroke؛ GPU پر selection؛ texture میں براہِ راست لکھنے والے mutations؛ اور high-zoom vector re-drawing (“zero visual change” کے تقاضے کی وجہ سے دوسرے مرحلے میں شامل کر دیا گیا)۔ ہر مرحلہ میز پر رکھے iPad Pro پر ایک انسان کے لکھنے، مٹانے، zoom کرنے اور drag کرنے کے ساتھ ختم ہوتا تھا — simulator نہیں، screenshot diff نہیں۔

Device نے تین ایسے bugs پکڑے جنہیں ہر automated check نے miss کیا۔ 100% zoom سے اوپر strokes دو بار draw ہوتے تھے — نیچے soft texture، اوپر sharp sprites — اور ایک مدھم blur کی صورت میں نظر آتے تھے جسے انسان نے چند seconds میں محسوس کر لیا۔ ایک stroke commit کرنے پر ایک frame کے لیے flicker ہوتا تھا، کیونکہ پرانا overlay texture update کے ساتھ sync میں نہیں تھا اور cross-fade ہو کر غائب ہوتا تھا۔ 170% zoom سے اوپر ہر note غائب ہو جاتا تھا: visibility-culling rectangle اپنی scaled coordinate space میں contentOffset استعمال کرتا تھا، اس لیے zoom کرتے ہوئے strokes سے دور ہوتا جاتا تھا۔ یہ تینوں ایک لائن سے ایک function تک کی fixes تھیں، اور ان میں سے کوئی بھی ایسے test میں موجود نہیں تھی جو ہم پہلے سے لکھ سکتے تھے، کیونکہ ہمیں معلوم ہی نہیں تھا کہ انہیں تلاش کرنا ہے۔ UI-first consumer app کے لیے انسان کو loop میں رکھنے کی وجہ یہی ہے۔

Kimi K3 کے ساتھ کام کرنا کیسا ہے

سب سے پہلے، تیز۔ “discuss, write, build, install, look” کا loop چند منٹ میں چلتا تھا، اور جلد جواب دینے والا model بدل دیتا ہے کہ آپ ایک دن میں کتنے loops afford کر سکتے ہیں۔

دوسرا، یہ over-engineer نہیں کرتا۔ یہ codebase واضح house rules پر چلتی ہے — launch سے پہلے backward-compatibility scaffolding نہیں، complexity صرف تب جب کوئی device اس کی ضرورت ثابت کرے — اور K3 بغیر یاد دلائے ان پر عمل کرتا ہے۔ اس نے “بعد کے لیے” spatial indexes شامل نہیں کیے، ہر call کو defensive checks میں نہیں لپیٹا، اور speculatively abstract نہیں کیا۔ اسے prompt کرنا ایسے ہے جیسے ایک قابل colleague کے ساتھ کام کرنا جس نے house rules پڑھی ہیں اور واقعی ان پر یقین رکھتا ہے۔

تیسرا، اسے tools دیں اور وہ انہیں شوق سے استعمال کرتا ہے۔ ہم نے image utilities جوڑیں — view، کسی pixel region تک crop، resize — اور model نے اوپر والے پانچ harness bugs کی تشخیص کے لیے اپنے renderer output کو proactively crop کرنا شروع کر دیا۔ Tool کا موجود ہونا اسے دیکھنے کی یاد دلاتا ہے۔

دوسرا رخ یہ ہے کہ notes کو غائب کرنے والے coordinate-space bug سمیت، اس کہانی کے زیادہ تر bugs K3 نے لکھے۔ اس کی حدود حقیقی ہیں۔ کام کو محفوظ بنانے والی چیز model کا درست ہونا نہیں تھی؛ harness نے rendering drift پکڑی اور انسان نے feel پکڑی۔ پھر بھی، روزمرہ کے کام میں، میں اسے ان frontier closed models سے قابلِ اعتماد طور پر الگ نہیں کر سکتا تھا جنہیں ہم بھی استعمال کرتے ہیں — Opus-class systems۔ کچھ پہلوؤں میں یہ واضح طور پر بہتر تھا: زیادہ تیز، اور codebase کو defensive design سے بھرنے کا رجحان بہت کم۔

ہم اب سے کیسے بنانا چاہتے ہیں

ہم big-spec agentic development سے فارغ ہو چکے ہیں — وہ انداز جس میں آپ model کو ایک بڑی specification دیتے ہیں اور جو کچھ بھی بن کر آئے اسے قبول کر لیتے ہیں۔ failure mode برا code نہیں؛ وہ code ہے جسے کوئی نہیں سمجھتا۔

جو یہاں کام آیا، اور جسے ہم برقرار رکھیں گے، وہ ہیں چھوٹے مراحل: ہر مرحلے پر شروع ہونے سے پہلے بحث کی جاتی ہے، build سے پہلے انسان اسے سمجھتا ہے، اور اسے اس device پر verify کیا جاتا ہے جس پر وہ رہے گا۔ model کا کام تیز، درست اور uncertainty کے بارے میں ایماندار ہونا ہے۔ انسان کا کام judgment، taste اور e2e verification ہے — خاص طور پر UI-first consumer software کے لیے، جہاں specification یہ بیان نہیں کر سکتی کہ “درست” کیسا محسوس ہوتا ہے۔ Fireworks پر Kimi K3 اسی loop کے لیے اچھی طرح موزوں نکلا: loop کو tight رکھنے کے لیے کافی تیز، اور مراحل کو چھوٹا اور صاف رکھنے کے لیے کافی smart۔

لہر ختم ہو گئی، pipeline دو ideas کے بجائے ایک idea ہے، اور وہ process جس نے ہمیں وہاں پہنچایا برقرار رہے گا۔