Lulucat

चॉक साधनाची कामगिरी: पूर्ण-स्क्रीन पासपासून scissor आयतांपर्यंत

Gaoge ZhangGaoge Zhang

दाट हस्तलिखित भागांत Lulucat Notes चे चॉक साधन मंदावत होते. मुख्य अडथळा 3,571 input samples नव्हते — प्रत्येक फ्रेममध्ये होणारे 70 full-screen scratch passes होते. नाकारलेला low-resolution cache आणि प्रत्येक stroke साठीचा scissor rectangle उरलेली कहाणी सांगतात.

iPad वर 255% zoom मध्ये Lulucat Notes चे cropped view; लाल आणि निळ्या चॉकने लिहिलेला चिनी शास्त्रीय उतारा आणि app toolbar चा काही भाग दिसतो.

अंतिम तपासणीसाठी वापरलेल्या device build मधील लाल आणि निळी चॉक, 255% झूमवर 155 strokes.

Lulucat Notes च्या चॉक साधनाला एक विशिष्ट कार्यक्षमता समस्या होती: रिकाम्या भागात लिहिणे सुरळीत वाटत होते, पण आधीच चॉक strokes ने भरलेल्या भागात गेल्यावर pen tip मागे पडू लागायची. त्याच भागात लिहित राहिल्यावर canvas पॅनिंग देखील हळूहळू मंद होत गेले.

साध्या हस्तलिखिताचे एक पान ही समस्या निर्माण करण्यासाठी पुरेसे होते: 300% झूम, स्थानिक भागात दिसणारे 70 chalk strokes, एकूण 3,571 input sample points. रिकामे भाग सुरळीत राहिले; strokes जिथे दाट होते तो भागच मंद झाला.

दुरुस्तीनंतर त्याच पानावर 255% झूममध्ये नवीन लेखन सुरू ठेवता येते, आणि pen-down तसेच पॅनिंगदरम्यान विद्यमान strokes पूर्ण स्पष्टता राखतात.

iPad वर 255% zoom मध्ये Lulucat Notes लाल आणि निळी चॉक handwriting दाखवते. मजकूर "天行健,君子以自强不息;地势坤,君子以厚德载物" असा आहे — एक चिनी शास्त्रीय उतारा. वरच्या उजव्या कोपऱ्यात निळा Lulucat mascot आहे. खालच्या toolbar मध्ये stroke count 155, Save, Clear आणि 255% zoom slider दिसतो.

अंतिम तपासणीसाठी वापरलेल्या device चा स्क्रीनशॉट, एकूण 155 strokes. या झूम पातळीवर pen-down किंवा पॅनिंग यापैकी कशामुळेही विद्यमान strokes ची स्पष्टता तात्पुरती बदलत नाही.

चॉकला scratch texture का लागते

सामान्य pen प्रत्येक circular stamp source-over blending वापरून ink texture वर थेट composite करू शकतो. चॉक एक grain-gating layer जोडते: renderer आधी संपूर्ण stroke साठी body coverage आणि depth accumulate करतो, मग कोणत्या positions वर चॉकची धूळ मिळेल हे ठरवण्यासाठी fixed grain texture वापरतो, आणि शेवटी result existing ink वर composite करतो.

ही scratch texture एक चॉक stroke वेगळा ठेवते. एकाच stroke मधील stamps मोठ्या प्रमाणावर overlap होत असल्याने हे वेगळेपण महत्त्वाचे आहे; प्रत्येक stamp ला स्वतंत्रपणे grain gate केल्यास stroke centerline वर repeated color साचेल आणि sampling density सोबत चॉकचे pores सरकतील.

जास्त झूम पातळीवर Lulucat Notes सध्याच्या viewport मध्ये दिसणारे vector strokes पुन्हा रेखाटते. जुनी अंमलबजावणी प्रत्येक दिसणाऱ्या chalk stroke साठी हे टप्पे करत होती:

  1. main render encoder end करणे;
  2. scratch texture clear करणे;
  3. हा एक chalk stroke scratch मध्ये draw करणे;
  4. main render encoder पुन्हा open करणे;
  5. full-screen triangle वापरून scratch drawable वर परत composite करणे.

एका stroke चे semantics बरोबर होते, पण कामाची व्याप्ती खूप मोठी होती. iPad चा drawable 2732×2048 — साधारण 5.6 million pixels — होता. प्रत्येक chalk stroke मुळे एक scratch pass आणि एक full-screen composite होत असे. 70 chalk strokes म्हणजे अंदाजे 141 render encoders आणि 70 full-screen composites.

Visible chalk strokes ची संख्या आणि drawable pixel count मानू. Pixel coverage सोबत वाढणारे कामच विचारात घेतल्यास जुनी अंमलबजावणी जवळपास अशी होती:

प्रत्येक chalk stroke सोबत ठरावीक render-pass overhead देखील होता, त्यामुळे तो खर्च सुद्धा सोबत रेषीय वाढत होता. 3,571 input points मुळे फक्त दुय्यम खर्च वाढत होता. स्थानिक stroke count सोबत वाढणारी गोष्ट म्हणजे प्रत्येक stroke ने trigger केलेली full-screen कामाची व्याप्ती.

मोजमापे iPadOS 18.6.2 चालणाऱ्या 12.9-inch iPad Pro (5th generation, M1) वर घेतली. या particular iPad वरील त्याच Debug device build मध्ये command-buffer timestamps वापरून बदलापूर्वी आणि नंतरच्या त्याच viewport चे GPU timestamps compare केले — पुढे याला LucasPad म्हणू. खालील श्रेणी multi-frame logs मधील नेहमीच्या चढ-उतार आहेत; त्या पुढे वितरित होणाऱ्या आवृत्तीच्या frame-rate हमी नाहीत. 70 visible chalk strokes असताना एका frame ला साधारण 52–60 ms GPU time लागत होता; सुमारे 120 strokes असलेल्या भागात GPU time 77–80 ms पर्यंत वाढला.

Scratch आणि composite passes च्या full-screen rectangle area वरून अंदाज घेतल्यास, प्रत्येक फ्रेममधील सैद्धांतिक कामाची व्याप्ती साधारण 783 million pixels वरून 1.34 billion pixels पर्यंत वाढली. हा आकडा rectangle areas ची बेरीज आहे; तो fragment invocation counts, video-memory read/write bytes किंवा GPU hardware counters यांच्याशी equivalent नाही. Metal चे fast clear, attachment load/store आणि pass switching अजूनही GPU आणि driver च्या नियंत्रणाखाली आहेत.

रिकामे भाग सुरळीत का राहिले हेही यामुळे स्पष्ट होते. Visibility culling viewport च्या बाहेरचे strokes वगळते; रिकाम्या भागात जवळपास zero असते, तर दाट भागात वाढत राहते.

0.85 ms वर मिळालेले चुकीचे उत्तर

App मध्ये आधीच प्रति point दोन pixels वर baked केलेली पूर्ण पानाची ink texture होती. लेखन, पॅनिंग आणि झूमदरम्यान ही texture थेट दाखवून फक्त सध्याचा Apple Pencil stroke live vector ठेवण्याचा प्रयत्न केला; interaction संपल्यानंतर आणखी एका फ्रेममध्ये high-resolution vector result पुन्हा render केला जात असे.

हा मार्ग खूप चांगला कार्यरत झाला. त्याच dense area मध्ये 300% zoom वर GPU time 0.84–0.85 ms पर्यंत कमी झाला आणि existing chalk strokes च्या संख्येसोबत तो वाढेनासा झाला.

प्रत्यक्ष device वरील समस्या देखील तितकीच स्पष्ट होती. 300% झूमवर प्रत्येक point साठी सुमारे सहा screen pixels लागायचे, पण cache फक्त दोन देत होता. Apple Pencil touch-down करताच विद्यमान strokes सगळे मऊ, low-resolution image मध्ये बदलले; Pencil उचलल्यावर ते पुन्हा पूर्ण स्पष्टतेवर आले.

परीक्षक म्हणाला: “मी लिहित असताना संपूर्ण canvas धूसर होतो. हात सोडल्यावर तो पुन्हा स्पष्ट होतो.”

हे optimization काढून टाकले. 0.85 ms हा मोजलेला सर्वात कमी परिणाम होता, पण ते स्वीकारण्याजोगे चॉक साधन नव्हते. विद्यमान strokes writing feedback चा भाग आहेत; pen-down वेळी त्यांची स्पष्टता बदलू शकत नाही.

प्रत्येक chalk stroke ला त्याच्या स्वतःच्या rectangle मध्ये मर्यादित करणे

अंतिम दुरुस्तीने प्रत्येक stroke चे scratch आणि compositing कायम ठेवले आणि फक्त pixel कामाची व्याप्ती कमी केली. प्रत्येक stroke कडे त्याच्या सर्व stamp radii च्या union मधून मिळालेला canvas bounding box आधीपासून होता. Renderer हा bounding box सध्याच्या viewport च्या drawable coordinates मध्ये transform करतो आणि antialiasing margin साठी two pixels ने pad करतो.

Scratch clear करणे, stroke draw करणे आणि result main surface वर परत composite करणे या तीनही कामांसाठी तोच scissor rectangle वापरला जातो.

let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)

scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)

mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)

4096² ink texture वरच्या baking आणि partial replay साठीही हेच logic वापरले जाते, त्यामुळे high-zoom display आणि स्थिर ink layer चॉकचे दोन वेगळे वर्तन तयार करत नाहीत.

इथे दोन details सहज चुकतात.

पहिले, render pass चे loadAction = .clear attachment load stage मध्ये होते आणि rasterization scissor ने मर्यादित होत नाही. ते वापरत राहिल्यास संपूर्ण scratch texture तरीही clear होईल. ठरावीक pass .dontCare वापरतो आणि मग scissor च्या आत clear_fragment रेखाटतो. हा rectangle नंतर पूर्णपणे लिहिला जातो आणि composite फक्त तोच rectangle read करतो; त्यामुळे जुने 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 हलवल्याने GPU कोणते pixels process करते तेवढे बदलते; प्रत्येक pixel कोणती grain texture location वाचतो ते बदलत नाही. त्यामुळे adjacent rectangles मध्ये texture seams तयार होत नाहीत आणि canvas drag केल्यावर grain drift होत नाही.

फक्त pixel workload विचारात घेतल्यास नवीन कामाची व्याप्ती जवळपास अशी आहे:

इथे म्हणजे सध्याच्या screen वरील -व्या chalk stroke च्या axis-aligned bounding-box चे area. Render encoders ची संख्या कमी झालेली नाही, पण प्रत्येक clear आणि composite आता stroke च्या screen bounding box पुरते मर्यादित आहे.

समान रंगाचे chalk strokes batch न करण्याचे कारण

पानावरील बहुतेक chalk strokes मध्ये एकच रंग आणि घनता असते, त्यामुळे डझनभर strokes scratch मध्ये एकाच वेळी रेखाटून एकदाच composite करण्याचा मोह होतो. यामुळे render passes आणखी कमी होतील, पण overlap होणाऱ्या भागांतील रंग आणि grain चे semantics बदलतील.

जाणूनबुजून सोपे केलेले उदाहरण पाहू. एखाद्या pixel वर दोन strokes समान grain-gate value share करतात आणि body coverages आहेत. प्रत्यक्ष shader मध्ये gate प्रत्येक stroke च्या pressure depth वरही अवलंबून असतो; batching सर्वसाधारणपणे equivalent नसते हे दाखवण्यासाठी हे अधिक सोपे उदाहरण पुरेसे आहे. सध्याचे per-stroke compositing देते:

तर bodies आधी merge करून नंतर एकच gate लावल्यास मिळते:

फरक आहे. दोन strokes overlap झाले आणि grain gate पूर्ण zero किंवा पूर्ण one नसला की प्रत्येक वेळी परिणाम वेगळे येतात. थेट batching केल्यास crossings वर chalk dust पडण्याची पद्धत बदलते.

अचूक batching साठी stroke pixels परस्पर वेगळे आहेत हे सिद्ध करावे लागेल, किंवा प्रत्येक stroke साठी स्वतंत्र atlas region allocate करून मूळ क्रमाने composite करावे लागेल. अंतिम device acceptance ने scissor मार्ग कायम ठेवला, त्यामुळे या टप्प्यात atlas किंवा त्याची व्यवस्थापनातील गुंतागुंत आणली नाही.

एका billion pixels पासून पुन्हा काही million pixels पर्यंत

अंतिम device मोजमापे:

परिस्थितीFix पूर्वीअचूक scissor
दिसणारे 70 chalk strokes, 300% zoom, लिहितानाGPU 52–60 ms≈ 9–10 ms
≈ 121 दिसणारे chalk strokes, 300% zoomGPU 77–80 ms13.7–15.6 ms
प्रत्येक frame मधील सैद्धांतिक rectangle scope (scratch + composite)783 M–1.34 B पिक्सेल≈ 1.7 M–3 M पिक्सेल

Device document मधील 3,452 strokes आणि 202,710 sample points वापरून clipping bounds देखील तपासले. 0.5×, 1×, 2×, 3×, 5× आणि 8× झूमवर 186,408 viewport cases तयार केले; non-zero coverage निर्माण करू शकणारा प्रत्येक point sprite computed scissor च्या आत होता. या तपासणीत canvas edges, viewport edges आणि offset combinations चे विविध प्रकार समाविष्ट होते.

अंतिम code interaction state नुसार low-resolution LOD वर बदलत नाही. Low zoom levels वर full-page ink texture अजूनही display होते; high zoom levels वर visible strokes अजूनही vectors म्हणून redraw होतात. Threshold च्या त्याच बाजूला pen-down आणि panning existing strokes ला वेगळ्या clarity level ने replace करत नाहीत. High-zoom vector redraw दरम्यान प्रत्येक chalk stroke चे scratch clear आणि composite फक्त त्याचे screen bounding box cover करतात.

GPU time ने clarity capture केली नाही

GPU कार्यक्षमता समस्या data structure मधील सर्वात दिसणाऱ्या प्रमाणासोबतच वाढतील असे नाही. या प्रकरणात 3,571 input points हा सहज संशय घेता येणारा भाग होता; frame time ठरवणारे काम म्हणजे 70 chalk strokes पैकी प्रत्येकाने निर्माण केलेले full-screen काम आणि त्यांचे render-pass switching.

दृश्य semantics ने उपलब्ध optimizations वरही मर्यादा आणली. प्रत्येक stroke चे scratch, मूळ compositing order आणि absolute canvas grain coordinates मनाप्रमाणे काढता येत नाहीत. एकच रंग आणि एकच घनता म्हणजे parameters जुळतात एवढेच — overlap झालेले परिणाम merge करता येतील हे त्यातून सिद्ध होत नाही.

प्रत्यक्ष device वरील अभिप्रायाने सर्वात कमी GPU time असलेली आवृत्ती नाकारली. “संपूर्ण canvas धूसर होतो” या अभिप्रायाने measurement एकट्याने न दाखवलेली product constraint स्पष्ट केली: Apple Pencil touch-down करताना वापरकर्ते विद्यमान strokes कडेही पाहत असतात.

अंतिम आवृत्ती नवीन interaction cache layer आणत नाही आणि स्पष्टता कमी करत नाही. High-zoom vector redraw प्रत्येक chalk stroke चे काम फक्त त्याच्या screen bounding box पर्यंत मर्यादित करते. LucasPad वर पुन्हा load केल्यानंतर अभिप्राय असा होता: “खूप छान दिसते.”