Lulucat

चॉक टूल का प्रदर्शन: पूरी स्क्रीन वाले पास से scissor आयतों तक

Gaoge ZhangGaoge Zhang

Lulucat Notes का चॉक टूल घनी handwriting वाले क्षेत्रों में धीमा हो गया था। मुख्य अड़चन 3,571 इनपुट नमूने नहीं थे — हर फ़्रेम में होने वाले 70 पूरी स्क्रीन वाले scratch पास थे। अस्वीकार की गई कम-रिज़ॉल्यूशन कैश और हर स्ट्रोक के लिए बना scissor आयत बाकी कहानी बताते हैं।

iPad पर 255% ज़ूम में Lulucat Notes का कटा हुआ दृश्य, जिसमें शास्त्रीय चीनी पाठ की लाल और नीली चॉक से लिखी हस्तलिपि और ऐप टूलबार का कुछ हिस्सा दिखाई देता है।

डिवाइस के लिए बनी अंतिम बिल्ड में लाल और नीली चॉक, 255% ज़ूम पर 155 स्ट्रोक।

Lulucat Notes के चॉक टूल में प्रदर्शन की एक खास समस्या थी: खाली क्षेत्र में लिखना सहज लगता था, लेकिन पहले से चॉक स्ट्रोक से भरे क्षेत्र में जाते ही पेन की नोक पीछे रह जाती थी। उसी क्षेत्र में लिखना जारी रखने पर कैनवस को खिसकाना भी धीरे-धीरे धीमा होने लगा।

साधारण हस्तलिपि का एक पृष्ठ इसे शुरू करने के लिए पर्याप्त था: 300% ज़ूम, स्थानीय क्षेत्र में दिखाई देने वाले 70 चॉक स्ट्रोक और कुल 3,571 इनपुट नमूना-बिंदु। खाली क्षेत्र सहज बने रहे; केवल वही क्षेत्र धीमा हुआ जहाँ स्ट्रोक केंद्रित थे।

सुधार के बाद, उसी पृष्ठ पर 255% ज़ूम में नया लिखना जारी रखा जा सकता है, और पेन को स्क्रीन पर रखने तथा कैनवस खिसकाने — दोनों के दौरान मौजूदा स्ट्रोक अपनी पूरी स्पष्टता बनाए रखते हैं।

iPad पर 255% ज़ूम में Lulucat Notes, लाल और नीली चॉक की हस्तलिपि दिखाते हुए। टेक्स्ट "天行健,君子以自强不息;地势坤,君子以厚德载物" है — एक शास्त्रीय चीनी पाठ। ऊपर दाएँ कोने में नीला Lulucat मैस्कॉट है। नीचे के टूलबार में 155 स्ट्रोक की गिनती, Save, Clear और 255% ज़ूम स्लाइडर दिखता है।

अंतिम डिवाइस स्क्रीनशॉट, कुल 155 स्ट्रोक। इस ज़ूम स्तर पर पेन को स्क्रीन पर रखने या कैनवस खिसकाने के दौरान मौजूदा स्ट्रोक की स्पष्टता कुछ समय के लिए भी नहीं बदलती।

चॉक को scratch टेक्सचर की ज़रूरत क्यों है

एक साधारण पेन हर गोल stamp को source-over blending के साथ ink टेक्सचर पर सीधे composite कर सकता है। चॉक grain-gating की एक परत जोड़ती है: रेंडरर पहले पूरे स्ट्रोक के body coverage और depth को जमा करता है, फिर fixed grain टेक्सचर से तय करता है कि किन स्थानों पर चॉक की धूल मिलेगी, और अंत में परिणाम को मौजूदा ink पर composite करता है।

यह scratch टेक्सचर एक चॉक स्ट्रोक को अलग रखती है। यह ज़रूरी है क्योंकि एक ही स्ट्रोक के stamps बहुत अधिक overlap करते हैं; अगर हर stamp पर अलग-अलग grain-gating की जाती, तो स्ट्रोक की centerline पर रंग बार-बार जमा होता और चॉक के छिद्र sampling density के साथ खिसकते।

ज़्यादा ज़ूम पर Lulucat Notes मौजूदा viewport में दिखाई देने वाले vector स्ट्रोक को फिर से बनाता है। पुराना कार्यान्वयन हर दिखाई देने वाले चॉक स्ट्रोक के लिए ये चरण चलाता था:

  1. मुख्य render encoder समाप्त करना;
  2. scratch टेक्सचर साफ़ करना;
  3. इस एक चॉक स्ट्रोक को scratch में draw करना;
  4. मुख्य render encoder को फिर से खोलना;
  5. पूरी स्क्रीन वाले triangle से scratch को drawable पर वापस composite करना।

iPad का drawable 2732×2048 था — लगभग 5.6 million pixels। हर चॉक स्ट्रोक एक scratch pass और पूरी स्क्रीन वाला composite शुरू करता था। 70 चॉक स्ट्रोक का अर्थ लगभग 141 render encoders और 70 पूरी स्क्रीन वाले composites था।

दिखाई देने वाले चॉक स्ट्रोक की संख्या और drawable pixel count मानें। केवल pixel coverage के साथ बढ़ने वाले काम को देखें तो पुराना कार्यान्वयन लगभग

के बराबर थी। हर चॉक स्ट्रोक के साथ fixed render-pass overhead भी जुड़ा था, इसलिए वह लागत भी के साथ रैखिक रूप से बढ़ती थी। 3,571 input points का योगदान केवल द्वितीयक लागत था। स्थानीय स्ट्रोक की संख्या के साथ जो बढ़ रहा था, वह हर स्ट्रोक से शुरू होने वाला पूरी स्क्रीन का काम था।

माप 12.9-inch iPad Pro (5th generation, M1) पर iPadOS 18.6.2 के साथ लिए गए। हमने बदलाव से पहले और बाद में उसी viewport के GPU timestamps की तुलना की; इस खास iPad पर उसी Debug device build में command-buffer timestamps का उपयोग किया — नीचे इसे LucasPad कहा गया है। नीचे दी गई सीमाएँ कई फ़्रेम के लॉग में दिखने वाली सामान्य घट-बढ़ हैं, किसी जारी की गई version के frame-rate commitments नहीं। 70 दिखाई देने वाले चॉक स्ट्रोक पर एक फ़्रेम के लिए आम तौर पर 52–60 ms GPU time लगता था; लगभग 120 स्ट्रोक वाले क्षेत्र में GPU time बढ़कर 77–80 ms हो गया।

scratch और composite पास के पूरी स्क्रीन वाले आयतों के क्षेत्रफल से अनुमान लगाने पर, प्रति फ़्रेम सैद्धांतिक कार्यक्षेत्र लगभग 783 million pixels से बढ़कर 1.34 billion pixels हो गया। यह संख्या आयतों के क्षेत्रफल का योग है और fragment invocation counts, video-memory read/write bytes या GPU hardware counters के बराबर नहीं है। Metal का fast clear, attachment load/store और pass switching GPU तथा driver के नियंत्रण में ही रहते हैं।

यह भी बताता है कि खाली क्षेत्र सहज क्यों बने रहे। दृश्यता के आधार पर की गई छँटाई viewport के बाहर के स्ट्रोक को छोड़ देती है; खाली क्षेत्र में लगभग शून्य होता है, जबकि घने क्षेत्र में बढ़ता रहता है।

0.85 ms पर गलत जवाब

ऐप में पहले से पूरे पृष्ठ की ink टेक्सचर थी, जिसे प्रति point दो pixels पर bake किया गया था। हमने लिखते, कैनवस खिसकाते और ज़ूम करते समय इस टेक्सचर को सीधे दिखाने और केवल मौजूदा Apple Pencil स्ट्रोक को live vector रखने की कोशिश की; interaction समाप्त होने के बाद एक अतिरिक्त फ़्रेम high-resolution vector result को फिर से render करता।

यह तरीका बहुत अच्छा चला। उसी घने क्षेत्र में 300% ज़ूम पर GPU time घटकर 0.84–0.85 ms हो गया और मौजूदा चॉक स्ट्रोक की संख्या के साथ बढ़ना बंद हो गया।

वास्तविक डिवाइस पर समस्या उतनी ही स्पष्ट थी। 300% ज़ूम पर लगभग छह screen pixels per point चाहिए थे, लेकिन कैश केवल दो देती थी। Apple Pencil के स्क्रीन को छूते ही सभी मौजूदा स्ट्रोक नरम, कम-रिज़ॉल्यूशन वाली छवि बन जाते; Pencil उठाने पर वे पूरी स्पष्टता में तुरंत लौट आते।

परीक्षण करने वाले ने एक बात कही: “जब मैं लिखता हूँ, पूरा कैनवस धुंधला हो जाता है। Pencil उठाते ही यह साफ़ हो जाता है।”

ऑप्टिमाइज़ेशन हटा दी गई। 0.85 ms सबसे कम मापा गया परिणाम था, लेकिन यह स्वीकार्य चॉक टूल नहीं था। मौजूदा स्ट्रोक लिखने की प्रतिक्रिया का हिस्सा हैं; पेन को स्क्रीन पर रखते समय उनकी स्पष्टता नहीं बदल सकती।

हर चॉक स्ट्रोक को अपने आयत तक सीमित करना

अंतिम सुधार ने हर स्ट्रोक की scratch और हर स्ट्रोक के compositing को बनाए रखा और केवल pixel कार्यक्षेत्र घटाया। हर स्ट्रोक के पास पहले से canvas bounding box था, जो उसके सभी stamp radii के union से निकाला गया था। रेंडरर इस bounding box को मौजूदा viewport के drawable coordinates में बदलता है और antialiasing margin के लिए इसमें दो pixels जोड़ता है:

इसके बाद वही scissor आयत तीन कामों के लिए इस्तेमाल होता है: scratch साफ़ करना, स्ट्रोक draw करना और परिणाम को मुख्य surface पर वापस composite करना।

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

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

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

यही तर्क 4096² ink टेक्सचर तैयार करने और आंशिक रीप्ले के लिए भी इस्तेमाल होता है, इसलिए अधिक ज़ूम वाला प्रदर्शन और स्थिर ink layer चॉक के दो अलग व्यवहार पैदा नहीं करते।

यहाँ दो विवरण आसानी से छूट सकते हैं।

पहला, render pass का loadAction = .clear attachment load stage में होता है और rasterization scissor से सीमित नहीं होता। इसे इस्तेमाल करते रहने पर पूरी scratch टेक्सचर फिर भी साफ़ होती। सुधारा गया pass .dontCare का उपयोग करता है, फिर scissor के भीतर clear_fragment draw करता है। यह आयत पूरी तरह लिखा जाता है और composite केवल उसी आयत को पढ़ता है, इसलिए पुराने attachment contents को load करने की ज़रूरत नहीं होती।

दूसरा, हर चॉक स्ट्रोक का composite पूरा होने के बाद बाहरी scissor को फिर से स्थापित करना ज़रूरी है। अगर स्थिति बहाल करने वाली यह पंक्ति छोड़ दी जाए, तो बाद के पेन, चित्र या चयन पिछले चॉक स्ट्रोक की bounds से clip होते रहेंगे और गायब स्ट्रोक या गायब चित्र जैसे दिखाई देंगे।

चॉक grain अब भी आयत के भीतर के local UVs से नहीं, बल्कि absolute canvas coordinates से sample होता है। Scissor खिसकाने से केवल यह बदलता है कि GPU कौन-से pixels process करता है; यह नहीं बदलता कि हर pixel grain टेक्सचर के किस स्थान को पढ़ता है। इसलिए पास-पास के आयत texture seams नहीं बनाते और कैनवस खिसकाने से grain drift नहीं करता।

केवल pixel workload पर विचार करें तो नया कार्यक्षेत्र लगभग

के बराबर है, जहाँ मौजूदा स्क्रीन पर -वें चॉक स्ट्रोक के axis-aligned bounding-box का क्षेत्रफल है। Render encoders की संख्या कम नहीं हुई, लेकिन अब हर clear और composite स्ट्रोक के screen bounding box तक सीमित है।

एक ही रंग के चॉक स्ट्रोक को एक साथ क्यों नहीं बनाया गया

पृष्ठ पर अधिकांश चॉक स्ट्रोक का रंग और घनत्व एक जैसा है, इसलिए दर्जनों स्ट्रोक को scratch में एक साथ draw करके केवल एक बार composite करना आकर्षक लगता है। इससे render passes और कम हो जाते, लेकिन overlapping क्षेत्रों में रंग और grain का अर्थ बदल जाता।

एक जान-बूझकर सरल किया हुआ मामला लें: किसी pixel पर दो स्ट्रोक ठीक वही grain-gate value साझा करते हैं, और उनकी body coverages और हैं। वास्तविक shader में gate प्रत्येक stroke की pressure depth पर भी निर्भर करता है; यह अधिक सरल मामला अकेले ही यह दिखाने के लिए पर्याप्त है कि batching सामान्य रूप से equivalent नहीं है। मौजूदा per-stroke compositing देता है

जबकि bodies को पहले merge करके फिर एक single gate लगाने पर मिलता है

अंतर है। जब दो स्ट्रोक overlap करते हैं और grain gate न तो पूरा शून्य होता है न पूरा एक, परिणाम अलग होते हैं। सीधे एक साथ बनाने पर crossings में चॉक की धूल जमने का तरीका बदल जाएगा।

सटीक batching के लिए यह साबित करना होगा कि स्ट्रोक के pixels एक-दूसरे से पूरी तरह अलग हैं, या हर स्ट्रोक के लिए स्वतंत्र atlas region allocate करके मूल क्रम में composite करना होगा। डिवाइस पर अंतिम स्वीकृति में scissor तरीके को ही रखा गया, इसलिए इस चरण में atlas या उसे संभालने की जटिलता नहीं जोड़ी गई।

एक अरब पिक्सेल से वापस कुछ मिलियन तक

डिवाइस पर मिले अंतिम माप:

स्थितिसुधार से पहलेसटीक scissor
70 दिखाई देने वाले चॉक स्ट्रोक, 300% ज़ूम, लिखते समयGPU 52–60 ms≈ 9–10 ms
≈ 121 दिखाई देने वाले चॉक स्ट्रोक, 300% ज़ूमGPU 77–80 ms13.7–15.6 ms
प्रति फ़्रेम सैद्धांतिक आयत-कार्य क्षेत्र (scratch + composite)783 M–1.34 B pixels≈ 1.7 M–3 M pixels

डिवाइस के एक दस्तावेज़ से मिले 3,452 स्ट्रोक और 202,710 नमूना-बिंदुओं का उपयोग करके clipping सीमाएँ भी जाँचीं। 0.5×, 1×, 2×, 3×, 5× और 8× ज़ूम पर 186,408 viewport मामले बनाए गए; non-zero coverage पैदा कर सकने वाला हर point sprite गणना किए गए scissor के भीतर था। इस जाँच में कैनवस के किनारे, viewport के किनारे और offsets के अलग-अलग संयोजन शामिल थे।

अंतिम कोड interaction state के आधार पर low-resolution LOD पर switch नहीं करता। कम ज़ूम पर पूरे पृष्ठ की ink टेक्सचर अब भी दिखाई जाती है; अधिक ज़ूम पर दिखाई देने वाले स्ट्रोक को अब भी vectors के रूप में फिर से draw किया जाता है। threshold की उसी तरफ पेन को स्क्रीन पर रखने और कैनवस खिसकाने से मौजूदा स्ट्रोक को किसी दूसरी स्पष्टता वाली परत से नहीं बदला जाता। अधिक ज़ूम के vector redraw के दौरान हर चॉक स्ट्रोक का scratch clear और composite केवल उसके अपने screen bounding box को cover करता है।

GPU समय स्पष्टता को माप नहीं पाया

GPU प्रदर्शन की समस्याएँ डेटा-संरचना में सबसे दिखाई देने वाली संख्या के साथ ही बढ़ें, यह ज़रूरी नहीं। इस मामले में 3,571 इनपुट बिंदु आसान संदिग्ध थे; फ़्रेम समय तय करने वाला काम हर 70 चॉक स्ट्रोक से शुरू होने वाला पूरी स्क्रीन का काम और उनके render-pass बदलाव थे।

दृश्य अर्थ ने उपलब्ध ऑप्टिमाइज़ेशन को भी सीमित किया। हर स्ट्रोक की scratch, मूल compositing order और canvas के absolute grain coordinates को मनमाने ढंग से हटाया नहीं जा सकता था। समान रंग और समान घनत्व का अर्थ केवल यह है कि पैरामीटर मिलते हैं — यह साबित नहीं करता कि overlapping परिणामों को merge किया जा सकता है।

वास्तविक डिवाइस की प्रतिक्रिया ने सबसे कम GPU time वाले संस्करण को अस्वीकार कर दिया। “पूरा कैनवस धुंधला हो जाता है” वाली टिप्पणी ने वह उत्पाद-सीमा सामने रखी जिसे माप अकेले व्यक्त नहीं कर पाया था: Apple Pencil स्क्रीन को छूते समय उपयोगकर्ता मौजूदा स्ट्रोक को भी देख रहे होते हैं।

अंत में जाँचा गया कार्यान्वयन कोई नई interaction cache layer नहीं जोड़ता और clarity कम नहीं करता। अधिक ज़ूम का vector redraw हर चॉक स्ट्रोक के काम को उसके अपने screen bounding box तक सीमित करता है। LucasPad पर इसे फिर load करने के बाद प्रतिक्रिया थी: “बहुत बढ़िया लग रहा है।”