Lulucat

أداء أداة الطباشير: من تمريرات ملء الشاشة إلى مستطيلات القص

Gaoge ZhangGaoge Zhang

تباطأت أداة الطباشير في Lulucat Notes داخل مناطق الكتابة الكثيفة. لم يكن عنق الزجاجة هو 3,571 عينة إدخال، بل 70 تمريرة خدش بملء الشاشة في كل إطار. وتحكي ذاكرة مؤقتة منخفضة الدقة رُفضت ومستطيل قص لكل ضربة بقية القصة.

منظر مقتص من Lulucat Notes على iPad عند تكبير 255%، يعرض كتابة بالطباشير باللونين الأحمر والأزرق لمقطع صيني كلاسيكي، مع ظهور أجزاء من شريط أدوات التطبيق.

طباشير أحمر وأزرق على نسخة الاختبار في الجهاز، مع 155 ضربة عند تكبير 255%.

كانت أداة الطباشير في Lulucat Notes تعاني من مشكلة أداء محددة: كانت الكتابة فوق مساحة فارغة تبدو سلسة، لكن عند الانتقال إلى منطقة ممتلئة أصلًا بضربات الطباشير كان رأس القلم يتأخر. وكان الاستمرار في الكتابة في المنطقة نفسها يبطئ تحريك اللوحة تدريجيًا أيضًا.

كانت صفحة واحدة من الكتابة العادية كافية لإظهار المشكلة: تكبير 300%، و70 ضربة طباشير ظاهرة في المنطقة المحلية، بإجمالي 3,571 نقطة عينة إدخال. ظلت المناطق الفارغة سلسة؛ وحدها المنطقة التي تركزت فيها الضربات أصبحت بطيئة.

بعد الإصلاح، يمكن للصفحة نفسها استقبال كتابة جديدة عند تكبير 255%، وتحافظ الضربات الموجودة على وضوحها الكامل أثناء الضغط بالقلم وأثناء التحريك.

Lulucat Notes على iPad عند تكبير 255%، تعرض كتابة بالطباشير باللونين الأحمر والأزرق. يقرأ النص «天行健,君子以自强不息;地势坤,君子以厚德载物» — وهو مقطع صيني كلاسيكي. يجلس تميمة Lulucat زرقاء في الزاوية العليا اليمنى. يعرض شريط الأدوات السفلي عدد الضربات 155، وأزرار Save وClear، وشريط تمرير تكبير 255%.

لقطة من اختبار الجهاز، بإجمالي 155 ضربة. عند مستوى التكبير هذا، لا يتغير وضوح الضربات الموجودة مؤقتًا، لا أثناء الضغط بالقلم ولا أثناء التحريك.

لماذا تحتاج الطباشير إلى نسيج خدش

يمكن لقلم عادي تركيب كل ختم دائري مباشرة فوق نسيج الحبر باستخدام مزج source-over. أما الطباشير فيضيف طبقة لحجب الحبيبات: يجمع المصيّر أولًا تغطية الجسم وعمقه لضربة كاملة، ثم يستخدم نسيج حبيبات ثابتًا لتحديد المواضع التي تتلقى غبار الطباشير، وأخيرًا يركّب النتيجة فوق الحبر الموجود.

يعزل نسيج الخدش هذا ضربة طباشير واحدة. والعزل مهم لأن الأختام داخل الضربة نفسها تتراكب بكثافة؛ ولو حُجبت كل بصمة حبيباتيًا على حدة، لتراكم اللون مرارًا على الخط المركزي للضربة، ولتغيرت مسام الطباشير مع كثافة أخذ العينات.

عند مستويات التكبير العالية، تعيد Lulucat Notes رسم ضربات المتجهات الظاهرة في منفذ العرض الحالي. وكان التنفيذ القديم ينفذ الخطوات التالية لكل ضربة طباشير ظاهرة:

  1. إنهاء مشفّر التصيير الرئيسي؛
  2. مسح نسيج الخدش؛
  3. رسم ضربة الطباشير هذه في الخدش؛
  4. إعادة فتح مشفّر التصيير الرئيسي؛
  5. تركيب الخدش مرة أخرى على القابل للرسم باستخدام مثلث بملء الشاشة.

كانت دلالات الضربة الواحدة صحيحة، لكن نطاق العمل كان أكبر بكثير. كانت مساحة القابل للرسم في iPad هي 2732×2048 — أي نحو 5.6 مليون بكسل. وكانت كل ضربة طباشير تطلق تمريرة خدش واحدة وتركيبًا واحدًا بملء الشاشة. وكانت 70 ضربة طباشير تعني نحو 141 مشفّر تصيير و70 تركيبًا بملء الشاشة.

لنفترض أن عدد ضربات الطباشير الظاهرة هو وأن عدد بكسلات القابل للرسم هو . وبالنظر فقط إلى العمل الذي يتدرج مع تغطية البكسلات، كان التنفيذ القديم قريبًا من

وكانت كل ضربة طباشير تحمل أيضًا تكلفة ثابتة لممر التصيير، ولذلك كانت تلك التكلفة تتدرج خطيًا مع أيضًا. ولم تسهم نقاط الإدخال البالغ عددها 3,571 إلا في تكلفة ثانوية. أما ما كان يتدرج مع عدد الضربات المحلية فهو نطاق العمل بملء الشاشة الذي تطلقه كل ضربة.

أُخذت القياسات على iPad Pro مقاس 12.9-inch (الجيل الخامس، M1) يعمل بنظام iPadOS 18.6.2. وقارنّا الطوابع الزمنية لوحدة GPU من منفذ العرض نفسه قبل التغيير وبعده، باستخدام طوابع زمنية لمخزن الأوامر في نسخة جهاز Debug نفسها على جهاز iPad هذا — وسنشير إليه أدناه باسم LucasPad. النطاقات أدناه تقلبات نموذجية في سجلات متعددة الإطارات، وليست وعدًا بمعدل إطارات لنسخة موجّهة إلى المستخدمين. عند وجود 70 ضربة طباشير ظاهرة، كان الإطار الواحد يحتاج عادةً إلى 52–60 ms من زمن GPU؛ وفي منطقة تضم نحو 120 ضربة، ارتفع زمن GPU إلى 77–80 ms.

وبتقدير مساحة نطاق العمل وفق مساحة المستطيل كامل الشاشة لتمريرتي الخدش والتركيب، نما نطاق العمل النظري لكل إطار من نحو 783 مليون بكسل إلى 1.34 مليار بكسل. هذا الرقم هو مجموع مساحات المستطيلات، ولا يعادل عدد استدعاءات الأجزاء أو بايتات القراءة والكتابة في ذاكرة الفيديو أو عدادات عتاد GPU. ويظل كل من المسح السريع في Metal، وتحميل/تخزين المرفق، وتبديل الممرات تحت تحكم GPU والمشغل.

وهذا يفسر أيضًا سبب بقاء المناطق الفارغة سلسة. فتقليم الرؤية يتجاوز الضربات خارج منفذ العرض؛ ويكون في منطقة فارغة قريبًا من الصفر، بينما يستمر في منطقة كثيفة في الارتفاع.

إجابة خاطئة عند 0.85 ms

كان التطبيق يملك بالفعل نسيج حبر يغطي الصفحة كاملة، مخبوزًا عند بكسلين لكل نقطة. جربنا عرض هذا النسيج مباشرة أثناء الكتابة والتحريك والتكبير، مع إبقاء ضربة Apple Pencil الحالية فقط كمتجه حي؛ وبعد انتهاء التفاعل، كان إطار إضافي واحد يعيد تصيير نتيجة المتجه عالية الدقة.

كان هذا النهج سريعًا جدًا. ففي المنطقة الكثيفة نفسها عند تكبير 300%، انخفض زمن GPU إلى 0.84–0.85 ms ولم يعد يزداد مع عدد ضربات الطباشير الموجودة.

لكن المشكلة على الجهاز الفعلي كانت واضحة بالقدر نفسه. فعند تكبير 300%، كانت هناك حاجة إلى نحو ستة بكسلات شاشة لكل نقطة، بينما لم توفر الذاكرة المؤقتة سوى بكسلين. وما إن يلمس Apple Pencil السطح حتى تتحول كل الضربات الموجودة إلى صورة ناعمة منخفضة الدقة؛ وعند رفع القلم تعود فجأة إلى الوضوح الكامل.

قال المختبر شيئًا واحدًا: «عندما أكتب، تصبح اللوحة كلها ضبابية. وتصفو بمجرد أن أرفع القلم.»

أُزيل هذا التحسين. كان 0.85 ms أدنى نتيجة مقاسة، لكنه لم يكن أداة طباشير مقبولة. فالضربات الموجودة جزء من تغذية الكتابة الراجعة؛ ولا يمكن أن يتغير وضوحها عند الضغط بالقلم.

حصر كل ضربة طباشير في مستطيلها الخاص

حافظ الإصلاح المنفّذ على الخدش لكل ضربة وعلى تركيب كل ضربة، ولم يقلل إلا نطاق عمل البكسلات في كليهما. وكانت لكل ضربة أصلًا علبة إحاطة على اللوحة مشتقة من اتحاد أنصاف أقطار جميع أختامها. يحوّل المصيّر علبة الإحاطة هذه إلى إحداثيات القابل للرسم في منفذ العرض الحالي، ويضيف إليها بكسلين كهامش لمكافحة التعرج:

ثم يُستخدم مستطيل القص نفسه في ثلاثة أشياء: مسح الخدش، ورسم الضربة، وتركيب النتيجة مرة أخرى على السطح الرئيسي.

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

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

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

ويُستخدم المنطق نفسه أيضًا في الخَبز وإعادة التشغيل الجزئية على نسيج الحبر 4096²، ولذلك لا ينتج عرض التكبير العالي وطبقة الحبر المستقرة سلوكين مختلفين للطباشير.

هناك تفصيلان يسهل إغفالهما هنا.

أولًا، يحدث loadAction = .clear لممر التصيير أثناء مرحلة تحميل المرفق، ولا يقيّده مقصّ التصيير. والاستمرار في استخدامه كان سيظل يمسح نسيج الخدش كله. أما الممر الثابت فيستخدم .dontCare، ثم يرسم clear_fragment داخل المقص. ويُكتب هذا المستطيل لاحقًا كاملًا، ولا يقرأ التركيب إلا المستطيل نفسه، ولذلك لا يلزم تحميل محتويات المرفق القديمة.

ثانيًا، بعد انتهاء تركيب كل ضربة طباشير، يجب استعادة المقص الخارجي. وإذا أُهمل هذا السطر من استعادة الحالة، فستظل الأقلام اللاحقة أو الصور أو التحديدات مقصوصة بحدود ضربة الطباشير السابقة، فتبدو ضربات أو صور مفقودة.

ولا تزال حبيبات الطباشير تُؤخذ من إحداثيات اللوحة المطلقة بدلًا من إحداثيات UV المحلية داخل المستطيل. إن تحريك المقص لا يغير إلا البكسلات التي يعالجها GPU؛ ولا يغير موضع نسيج الحبيبات الذي تقرأ منه كل بكسلة. لذلك لا تنتج المستطيلات المتجاورة درزات نسيجية، ولا تنجرف الحبيبات عند سحب اللوحة.

وبالنظر فقط إلى عبء البكسلات، يقترب نطاق العمل الجديد من

حيث إن هي مساحة علبة الإحاطة المحاذية للمحاور لضربة الطباشير رقم على الشاشة الحالية. لم ينخفض عدد مشفّرات التصيير، لكن كل مسح وتركيب أصبح الآن محدودًا بعلبة الإحاطة الشاشة الخاصة بالضربة.

لماذا لم نُجمّع ضربات الطباشير ذات اللون نفسه

تشترك معظم ضربات الطباشير في الصفحة في اللون والكثافة نفسيهما، ومن المغري رسم عشرات الضربات في الخدش دفعة واحدة ثم إجراء تركيب واحد فقط. كان ذلك سيقلل ممرات التصيير أكثر، لكنه يغير دلالات اللون والحبيبات في مناطق التداخل.

لنتأمل حالة مبسطة عمدًا: تشترك ضربتان في قيمة بوابة الحبيبات نفسها عند بكسل معين، ولهما تغطيتا جسم و. في الـshader الفعلي، تعتمد بوابة الحبيبات أيضًا على عمق الضغط لكل ضربة؛ وهذه الحالة الأبسط كافية لإثبات أن التجميع لا يكون مكافئًا عمومًا. ينتج التركيب الحالي لكل ضربة على حدة

بينما ينتج دمج الجسمين أولًا ثم تطبيق بوابة واحدة

والفرق هو . وكلما تداخلت ضربتان وكانت بوابة الحبيبات لا تساوي صفرًا خالصًا ولا واحدًا خالصًا، اختلفت النتيجتان. ومن شأن التجميع المباشر أن يغير كيفية سقوط غبار الطباشير عند التقاطعات.

يتطلب التجميع الدقيق إثبات أن بكسلات الضربات متباعدة تمامًا، أو تخصيص منطقة مستقلة في الأطلس لكل ضربة وتركيبها بالترتيب الأصلي. وقد أبقى اختبار القبول على الجهاز على نهج القص، ولذلك لم تضف هذه الجولة أطلسًا أو تعقيد إدارته.

من مليار بكسل إلى بضعة ملايين

كانت قياسات الجهاز:

السيناريوقبل الإصلاحالقص الدقيق
70 ضربة طباشير ظاهرة، تكبير 300%، كتابةGPU 52–60 ms≈ 9–10 ms
≈ 121 ضربة طباشير ظاهرة، تكبير 300%GPU 77–80 ms13.7–15.6 ms
نطاق المستطيلات النظري لكل إطار (الخدش + التركيب)783 M–1.34 B بكسل≈ 1.7 M–3 M بكسل

وتحققنا أيضًا من حدود القص باستخدام 3,452 ضربة و202,710 نقطة عينة من مستند على الجهاز. وعند تكبير 0.5× و1× و2× و3× و5× و8×, وُلّدت 186,408 حالة منفذ عرض؛ ووقع كل راسم نقطي يمكنه إنتاج تغطية غير صفرية داخل القص المحسوب. شمل هذا الفحص حواف اللوحة، وحواف منفذ العرض، ومجموعات مختلفة من الإزاحات.

ولا يبدّل التنفيذ المختار إلى LOD منخفض الدقة بناءً على حالة التفاعل. فما زالت مستويات التكبير المنخفض تعرض نسيج الحبر للصفحة كاملة، وما زالت مستويات التكبير العالية تعيد رسم الضربات الظاهرة كمتجهات. وعلى الجانب نفسه من العتبة، لا يستبدل الضغط بالقلم والتحريك الضربات الموجودة بمستوى مختلف من الوضوح. وأثناء إعادة الرسم المتجهية عند التكبير العالي، يغطي مسح كل ضربة طباشير وتركيبها علبة إحاطتها الشاشة الخاصة فقط.

زمن GPU لم يلتقط الوضوح

لا تتدرج مشكلات أداء GPU بالضرورة مع الكمية الأكثر ظهورًا في بنية البيانات. في هذه الحالة، كانت 3,571 نقطة إدخال مشتبهًا سهلًا؛ لكن ما حدد زمن الإطار هو العمل بملء الشاشة الذي أطلقته كل واحدة من ضربات الطباشير البالغ عددها 70، إلى جانب تبديل ممرات التصيير.

كما قيّدت الدلالات البصرية التحسينات المتاحة. فلم يكن بالإمكان إزالة الخدش لكل ضربة، وترتيب التركيب الأصلي، وإحداثيات حبيبات اللوحة المطلقة كيفما اتفق. فاللون والكثافة المتطابقان يعنيان فقط أن المعلمات متطابقة؛ ولا يثبتان إمكانية دمج النتائج المتداخلة.

رفضت التغذية الراجعة من الجهاز الفعلي النسخة ذات أقل زمن GPU. وقدّم التعليق «تصبح اللوحة كلها ضبابية» قيد المنتج الذي لم تكن القياسات وحدها قد عبّرت عنه: فعندما يلمس Apple Pencil السطح، يراقب المستخدمون الضربات الموجودة أيضًا.

ولا يضيف التنفيذ المختار طبقة ذاكرة مؤقتة جديدة للتفاعل ولا يقلل الوضوح. إن إعادة الرسم المتجهية عند التكبير العالي تحد فقط عمل كل ضربة طباشير في علبة إحاطتها الشاشة الخاصة. وبعد تحميله مرة أخرى على LucasPad، أصبحت التغذية الراجعة: «تبدو رائعة.»