چاک ٹول کی کارکردگی: Full-screen passes سے scissor rectangles تک
Lulucat Notes کا چاک ٹول گھنی handwriting والی جگہوں میں سست ہو گیا۔ اصل رکاوٹ 3,571 input samples نہیں تھے — بلکہ ہر frame میں 70 full-screen scratch passes تھے۔ مسترد کیا گیا low-resolution cache اور ہر stroke کے لیے ایک scissor rectangle باقی کہانی بیان کرتے ہیں۔

آخری device test build میں سرخ اور نیلا چاک، 255% zoom پر 155 strokes۔
Lulucat Notes کے چاک ٹول کو کارکردگی کے ایک خاص مسئلے کا سامنا تھا: خالی جگہ پر لکھنا رواں محسوس ہوتا تھا، لیکن جب قلم کی نوک پہلے سے چاک strokes سے بھری ہوئی جگہ میں جاتی تو پیچھے رہ جاتی۔ اسی جگہ پر لکھتے رہنے سے canvas panning بھی بتدریج سست ہو جاتی۔
عام handwriting والے ایک page سے ہی یہ مسئلہ پیدا ہو جاتا تھا: 300% zoom، local area میں 70 visible chalk strokes، اور کل 3,571 input sample points۔ خالی جگہیں رواں رہتی تھیں؛ صرف وہ حصہ سست ہوتا تھا جہاں یہ strokes جمع تھے۔
درستگی کے بعد وہی page 255% zoom پر نئی writing لیتا رہتا ہے، اور موجودہ strokes pen-down اور panning دونوں کے دوران اپنی پوری وضاحت برقرار رکھتے ہیں۔

آخری device screenshot، کل 155 strokes۔ اس zoom level پر pen-down یا panning کے دوران موجودہ strokes کی وضاحت عارضی طور پر تبدیل نہیں ہوتی۔
چاک کو scratch texture کی ضرورت کیوں ہے
ایک عام pen ہر circular stamp کو source-over blending کے ذریعے ink texture پر براہِ راست composite کر سکتا ہے۔ چاک ایک grain-gating layer شامل کرتا ہے: renderer پہلے پورے stroke کے لیے body coverage اور depth جمع کرتا ہے، پھر یہ طے کرنے کے لیے کہ کن جگہوں پر چاک کی دھول پہنچے گی ایک fixed grain texture استعمال کرتا ہے، اور آخر میں نتیجے کو موجودہ ink پر composite کرتا ہے۔
یہ scratch texture ایک chalk stroke کو الگ رکھتا ہے۔ یہ اس لیے اہم ہے کہ ایک ہی stroke کے stamps بہت زیادہ overlap کرتے ہیں؛ اگر ہر stamp کو الگ grain-gate کیا جائے تو stroke centerline بار بار آنے والا color جمع کرتی رہے گی، اور چاک کے pores sampling density کے ساتھ بدلیں گے۔
High zoom levels پر Lulucat Notes موجودہ viewport میں نظر آنے والے vector strokes کو redraw کرتا ہے۔ پرانی implementation ہر visible chalk stroke کے لیے یہ مراحل انجام دیتی تھی:
- Main render encoder ختم کرنا؛
- Scratch texture clear کرنا؛
- اس chalk stroke کو scratch میں draw کرنا؛
- Main render encoder دوبارہ کھولنا؛
- ایک full-screen triangle کے ذریعے scratch کو drawable پر واپس composite کرنا۔
ایک stroke کی rendering semantics درست تھیں، لیکن کام کا دائرہ بہت بڑا تھا۔ iPad کا drawable 2732×2048 تھا — تقریباً 5.6 million pixels۔ ہر chalk stroke ایک scratch pass اور ایک full-screen composite trigger کرتا تھا۔ 70 chalk strokes کا مطلب تقریباً 141 render encoders اور 70 full-screen composites تھا۔
Visible chalk strokes کی تعداد
کے قریب تھی۔
ہر chalk stroke کے ساتھ ایک fixed render-pass overhead بھی تھا، اس لیے یہ cost بھی
پیمائش iPadOS 18.6.2 چلانے والے 12.9-inch iPad Pro (5th generation, M1) پر کی گئی۔ ہم نے تبدیلی سے پہلے اور بعد میں اسی viewport کے GPU timestamps کا موازنہ کیا، اور اسی مخصوص iPad پر اسی Debug device build کے command-buffer timestamps استعمال کیے — آگے اسے LucasPad کہیں گے۔ نیچے دی گئی حدود multi-frame logs کی عام fluctuations ہیں، shipping frame-rate commitments نہیں۔ 70 visible chalk strokes پر ایک frame کو عموماً 52–60 ms GPU time درکار تھا؛ تقریباً 120 strokes والی جگہ میں GPU time بڑھ کر 77–80 ms ہو گیا۔
Scratch اور composite passes کے full-screen rectangle area سے اندازہ لگائیں تو فی frame theoretical work scope تقریباً 783 million pixels سے بڑھ کر 1.34 billion pixels ہو گیا۔ یہ figure rectangle areas کا مجموعہ ہے، اور fragment invocation counts، video-memory read/write bytes، یا GPU hardware counters کے برابر نہیں۔ Metal کا fast clear، attachment load/store، اور pass switching اب بھی GPU اور driver کے control میں ہے۔
یہ بھی واضح کرتا ہے کہ خالی جگہیں رواں کیوں رہیں۔ Visibility culling viewport سے باہر کے strokes کو skip کرتا ہے؛ blank area میں
0.85 ms پر ایک غلط جواب
App میں پہلے ہی ایک full-page ink texture موجود تھا جو ہر point کے لیے 2 pixels پر bake کیا گیا تھا۔ ہم نے writing، panning، اور zooming کے دوران اس texture کو براہِ راست دکھانے کی کوشش کی، اور صرف موجودہ Apple Pencil stroke کو live vector کے طور پر رکھا؛ interaction ختم ہونے کے بعد high-resolution vector result کو دوبارہ render کرنے کے لیے ایک اضافی frame آتا تھا۔
یہ طریقہ بہت اچھی کارکردگی دکھاتا تھا۔ اسی dense area میں 300% zoom پر GPU time 0.84–0.85 ms تک گر گیا اور موجودہ chalk strokes کی تعداد کے ساتھ مزید نہیں بڑھا۔
اصل device پر مسئلہ بھی اتنا ہی واضح تھا۔ 300% zoom پر ہر point کے لیے تقریباً 6 screen pixels درکار تھے، لیکن cache صرف 2 فراہم کرتا تھا۔ Apple Pencil کے touch down کرتے ہی تمام موجودہ strokes ایک نرم، low-resolution image بن جاتے؛ Pencil اٹھانے پر ہی وہ پوری وضاحت میں واپس آتے۔
Tester نے کہا: “جب میں لکھتا ہوں تو پورا canvas دھندلا ہو جاتا ہے۔ ہاتھ اٹھاتے ہی یہ دوبارہ صاف ہو جاتا ہے۔”
ہم نے یہ optimization ہٹا دی۔ 0.85 ms ناپا گیا سب سے کم نتیجہ تھا، لیکن یہ قابلِ قبول chalk tool نہیں تھا۔ موجودہ strokes writing feedback کا حصہ ہیں؛ pen-down پر ان کی وضاحت تبدیل نہیں ہو سکتی۔
ہر chalk stroke کو اس کے اپنے rectangle تک محدود کرنا
آخری درستگی نے per-stroke scratch اور per-stroke compositing برقرار رکھا، صرف ان کے pixel work scope کو کم کیا۔ ہر stroke کے پاس پہلے ہی ایک canvas bounding box تھا، جو اس کے تمام stamp radii کے union سے حاصل کیا گیا تھا۔ Renderer اس bounding box کو موجودہ viewport کے drawable coordinates میں transform کرتا ہے اور antialiasing margin کے لیے 2 pixels کا padding دیتا ہے:
اسی scissor rectangle کو پھر تین کاموں کے لیے استعمال کیا جاتا ہے: scratch clear کرنا، stroke draw کرنا، اور نتیجے کو main surface پر واپس composite کرنا۔
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
یہی logic 4096² ink texture پر baking اور partial replay کے لیے بھی استعمال ہوتی ہے، اس لیے high-zoom display اور settled ink layer چاک کے دو مختلف behaviors پیدا نہیں کرتے۔
یہاں دو تفصیلات آسانی سے نظرانداز ہو جاتی ہیں۔
پہلی، render pass کا loadAction = .clear attachment load stage کے دوران ہوتا ہے اور rasterization scissor سے محدود نہیں ہوتا۔ اسے استعمال کرتے رہنے سے پوری scratch texture پھر بھی clear ہو گی۔ درست کیا گیا pass .dontCare استعمال کرتا ہے، پھر scissor کے اندر clear_fragment draw کرتا ہے۔ یہ rectangle بعد میں مکمل طور پر لکھا جاتا ہے، اور composite صرف اسی rectangle کو پڑھتا ہے، اس لیے پرانے attachment contents کو load کرنے کی ضرورت نہیں ہوتی۔
دوسری، ہر chalk stroke کا composite مکمل ہونے کے بعد outer scissor restore کرنا ضروری ہے۔ اگر state restoration کی یہ line چھوڑ دی جائے تو بعد کے pens، images، یا selections پچھلے chalk stroke کے bounds سے clip ہوتے رہیں گے، اور strokes یا images غائب دکھائی دیں گے۔
Chalk grain اب بھی rectangle کے اندر local UVs سے نہیں بلکہ absolute canvas coordinates سے sample ہوتا ہے۔ Scissor کو حرکت دینے سے صرف وہ pixels بدلتے ہیں جنہیں GPU process کرتا ہے؛ ہر pixel جس grain texture location کو read کرتا ہے وہ نہیں بدلتی۔ اس لیے ساتھ ساتھ موجود rectangles کے درمیان texture seams نہیں بنتے، اور canvas drag کرنے سے grain drift نہیں کرتا۔
صرف pixel workload کو دیکھیں تو نیا work scope تقریباً
کے برابر ہے، جہاں
ایک ہی رنگ کے chalk strokes کو batch کیوں نہیں کیا گیا
Page پر زیادہ تر chalk strokes ایک ہی color اور density استعمال کرتے ہیں، اس لیے درجنوں strokes کو scratch میں ایک ساتھ draw کر کے صرف ایک بار composite کرنا پُرکشش لگتا ہے۔ اس سے render passes مزید کم ہو جاتے، لیکن overlapping علاقوں میں color اور grain semantics بدل جاتے ہیں۔
ایک جان بوجھ کر سادہ کیے گئے معاملے کو دیکھیں: کسی pixel پر دو strokes کی grain-gate value
جبکہ bodies کو پہلے merge کر کے ایک gate لگانے سے پیدا ہوتا ہے
دونوں کا فرق
Exact batching کے لیے ثابت کرنا ہوگا کہ stroke pixels باہم intersect نہیں کرتے، یا ہر stroke کے لیے ایک independent atlas region allocate کر کے original order میں composite کرنا ہوگا۔ آخری device test نے scissor approach برقرار رکھی، اس لیے اس round میں atlas یا اس کی management complexity شامل نہیں کی گئی۔
ایک ارب pixels سے چند ملین pixels تک واپس
آخری device test کی پیمائشیں:
| منظر | درستگی سے پہلے | Precise scissor |
|---|---|---|
| 70 visible chalk strokes, 300% zoom, writing | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 visible chalk strokes, 300% zoom | GPU 77–80 ms | 13.7–15.6 ms |
| ہر frame کے لیے theoretical rectangle scope (scratch + composite) | 783 M–1.34 B pixels | ≈ 1.7 M–3 M pixels |
ہم نے ایک device document کے 3,452 strokes اور 202,710 sample points سے clipping bounds کی بھی تصدیق کی۔ 0.5×، 1×، 2×، 3×، 5×، اور 8× zoom پر 186,408 viewport cases بنائے گئے؛ ہر وہ point sprite جو non-zero coverage پیدا کر سکتا تھا calculated scissor کے اندر رہا۔ اس check میں canvas edges، viewport edges، اور مختلف offset combinations شامل تھے۔
ٹیسٹ کیا گیا code interaction state کی بنیاد پر low-resolution LOD پر switch نہیں کرتا۔ Low zoom levels پر full-page ink texture ہی دکھائی جاتی ہے؛ high zoom levels پر visible strokes اب بھی vectors کے طور پر redraw ہوتے ہیں۔ Threshold کے اسی طرف pen-down اور panning existing strokes کو clarity کی کسی دوسری سطح سے replace نہیں کرتے۔ High-zoom vector redraw کے دوران ہر chalk stroke کا scratch clear اور composite صرف اس کے اپنے screen bounding box کو cover کرتا ہے۔
GPU time نے clarity کو capture نہیں کیا
GPU performance problems ضروری نہیں کہ data structure میں سب سے نمایاں quantity کے ساتھ scale ہوں۔ اس case میں 3,571 input points ایک آسان suspect تھے؛ frame time کا تعین 70 chalk strokes میں سے ہر ایک کے trigger کیے ہوئے full-screen work اور ان کی render-pass switching نے کیا۔
Visual semantics نے دستیاب optimizations کو بھی محدود کیا۔ Per-stroke scratch، original compositing order، اور absolute canvas grain coordinates کو من مانے طور پر نہیں ہٹایا جا سکتا تھا۔ Same color اور same density صرف یہ بتاتے ہیں کہ parameters match کرتے ہیں — یہ ثابت نہیں کرتے کہ overlapping results merge کیے جا سکتے ہیں۔
Real-device feedback نے GPU time کے لحاظ سے سب سے کم ورژن کو مسترد کر دیا۔ “پورا canvas دھندلا ہو جاتا ہے” کے remark نے وہ product constraint ظاہر کی جسے measurement اکیلی بیان نہیں کر سکی: جب Apple Pencil touch down کرتا ہے تو users موجودہ strokes کو بھی دیکھ رہے ہوتے ہیں۔
ٹیسٹ ورژن کوئی نیا interaction cache layer شامل نہیں کرتا اور clarity کم نہیں کرتا۔ High-zoom vector redraw صرف ہر chalk stroke کے کام کو اس کے اپنے screen bounding box تک محدود کرتا ہے۔ Test build کو LucasPad پر دوبارہ load کرنے کے بعد feedback تھا: “بہت اچھا لگ رہا ہے۔”