مسار الرسومات HAR

توضّح هذه الصفحة مسار الرسومات الكاملة في أداة العرض عالية التوفّر (HAR)، مع تتبُّع تدفق البيانات من مستند تصميم Figma إلى وحدات البكسل النهائية المعروضة على الشاشة.

نظرة عامة

يحوّل خط أنابيب العرض تعريفات واجهة المستخدم ذات المستوى العالي إلى أوامر رسومات ذات مستوى منخفض، ويعرضها بكفاءة على شاشات الأجهزة. تم تصميم مسار العرض هذا للتطبيقات المهمة جدًا للسلامة في السيارات، مع التركيز على العرض الحتمي، والإدارة الفعّالة للحالة، والتفاعل القوي مع الأنظمة الفرعية لرسومات المنصة، مثل Direct Rendering Manager (DRM) وGeneric Buffer Management (GBM).

يمكن تقسيم مسار الإحالة الناجحة إلى أربع مراحل رئيسية:

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

مخطّط HAR البياني

الشكل 1: مخطط انسيابي لرسومات HAR

المرحلة 1: العرض المسبق

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

‫1.1 أساسيات DesignCompose

تم إنشاء مسار HAR استنادًا إلى منظومة DesignCompose المتكاملة.

  • المصدر: تم تصميم واجهة المستخدم في Figma وتصديرها باستخدام المكوّن الإضافي DesignCompose.
  • التعريف: الناتج هو مثيل من DesignComposeDefinition، وهو تمثيل متسلسل للتصميم (العُقد والأنماط وخيارات المنتج).
  • ربط البيانات: يستخدم نموذج واجهة المستخدم للتطبيق وحدات ماكرو إجرائية (على سبيل المثال، #[Design(node = "#speed")]) لربط حقول بنية Rust بشكل صريح بعُقد محدّدة الاسم في مستند Figma. يتيح ذلك لحالة التطبيق التحكّم تلقائيًا في خصائص العناصر المرئية.

تتضمّن هذه الأسس المكوّنات الرئيسية التالية:

  • الدالة المخفّضة: تعمل كحلقة معالجة الأحداث مركزية، وتعالج الإجراءات وتعدّل الحالة الحالية. يوفّر إطار العمل DefaultReducer، ولكن يمكن توفير عملية تنفيذ مخصّصة لبرنامج تقليل البيانات إذا لزم الأمر.
  • العنصر Presenter: يربط الحالة الحالية بنموذج واجهة المستخدم. يتم تحديد السمة Presenter من خلال حزمة إطار العمل harry، ويتم توفير تنفيذ مرجعي (UIModelPresenter) في حزمة harry-app-core.
  • نموذج واجهة المستخدم: ينشئ عمليات تخصيص استنادًا إلى الحالة الحالية. يتم إنشاء رمز نموذج واجهة المستخدم باستخدام ماكرو DesignDocument الذي توفّره حزمة derive_customizations. يوفر البنية UIModel في الحزمة harry-app-core مثالاً على ذلك.
  • Squoosh: يوفّر بنية بيانات SquooshView ومستودعًا للمتغيرات، ويُستخدم لعرض واجهة المستخدم وفقًا للتصميم. يتم تحميل مستند تصميم مسلسَل من خلال حزمة dc_bundle من مكتبة DesignCompose، ويتم تحويله إلى شجرة من بنى SquooshView لتحقيق أداء فعال في وقت التشغيل.

‫1.2 حلقة التخفيض

يتم تشغيل سلسلة المعالجة من خلال الإجراءات. يحدّد إطار العمل Actionsالنوع المُعدَّد الذي يحدّد الإجراءات الداخلية التي يستخدمها إطار العمل نفسه، ولكنّه يتضمّن أيضًا صيغة CustomAction تتيح للمستخدمين تحديد إجراءات إضافية خاصة بالتطبيق (على سبيل المثال، UpdateVehicleSpeed أو ButtonPress).

يوفّر إطار العمل أيضًا السمة StateAction التي تسهّل تنفيذ الإجراءات التي تؤثّر في حالة التطبيق وتؤدي اختياريًا إلى آثار جانبية يتم إرجاعها إلى التطبيق من أداة الاختزال لمعالجتها. يقدّم النوع CustomActions enum في الحزمة harry-app-core مثالاً مفصّلاً على ذلك.

في ما يلي مخطط أساسي لحلقة المخفّض:

  • معالجة الإجراء: يتلقّى Reducer إجراءً ويعدّل الحالة الحالية. وهي البيانات الأولية، مثل السرعة الحالية أو مؤشرات التحذير النشطة. وقد يؤدي ذلك أيضًا إلى آثار جانبية (على سبيل المثال، تشغيل رنين عند وميض ضوء حزام الأمان).
  • العرض التقديمي: يربط Presenter الحالة الجديدة بـ UIModel. ‫UIModel هو نموذج عرض يحتوي على بيانات منسَّقة خصيصًا لواجهة المستخدم (على سبيل المثال، تنسيق السرعة "120" كسلسلة "65 ميل في الساعة").
  • إنشاء التخصيص: يتم استدعاء طريقة apply في نموذج واجهة المستخدم لإنشاء مجموعة من مثيلات RenderCustomization. هذه تعليمات صريحة لتعديل تصميم Figma (مثلاً، "اضبط نص العقدة #speed على '65 ميل في الساعة'").
  • UpdatePolicy للتحسين: بعد كل عملية عرض مسبق، يتم عرض القيمة UpdatePolicy، ما يشير إلى الوقت الذي يجب فيه إجراء عملية تعديل العرض التالية. إذا لم تكن هناك أي تغييرات في الحالة في انتظار المراجعة ولم تكن أي رسوم متحركة قيد التشغيل، يشير UpdatePolicy إلى أنّه ما مِن حاجة إلى إجراء أي تعديلات أخرى على الفور. في مثل هذه الحالات، يتوقف Reducer عن إنشاء قوائم عرض جديدة، ما يمنع دورات العرض غير الضرورية ويحافظ على الموارد إلى أن يؤدي إجراء أو حدث جديد إلى تغيير.

‫1.3 عرض عملية استيعاب البيانات وتهيئة المستودع

تبدأ سلسلة المعالجة بمثيل DesignComposeDefinition. هذه هي مستندات التصميم في Figma التي تم تسلسلها بواسطة DesignCompose إلى بنية Protocol Buffers.

  • التحميل الأولي: عند بدء التشغيل، يتم تحويل التصميم الرئيسي (المحدّد بواسطة عقدته الجذرية) من DesignComposeDefinition إلى شجرة SquooshView أولية. هذه عملية تُجرى لمرة واحدة.

  • المستودع: يدير SquooshVariantRepository أشكال المكوّنات القابلة لإعادة الاستخدام وطرق العرض التي يتم تحميلها في البداية.

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

‫1.4 بطاقة التخصيص

يتم الانتقال إلى شجرة SquooshView لتطبيق حالة التطبيق الديناميكية:

  • تبديل الأشكال المختلفة: يتم تبديل مثيلات المكوّنات بأشكال مختلفة محدّدة (على سبيل المثال، تغيير رمز يمثّل وضع القيادة الحالي من الوضع الرياضي إلى الوضع الاقتصادي) استنادًا إلى منطق وقت التشغيل.

  • توسيع القائمة: يتم استبدال عنصر نموذج واحد في Figma بقائمة ديناميكية من العناصر الفرعية. يتم إنشاء معرّفات فريدة جديدة لهذه العناصر الفرعية للتحقّق من هوية ثابتة للرسوم المتحركة.

  • تجاهل النص والنمط: يتم تعديل محتوى النص (مثل قيمة السرعة) والأنماط (مثل مستوى التعتيم واللون) من الحالة الحالية.

‫1.5 دقة متغيرة

يتم حلّ رموز التصميم والمتغيرات المحدّدة في Figma أو محليًا في التطبيق.

  • الربط: يتم استبدال خصائص SquooshView التي تشير إلى متغيرات (مثل الألوان أو الأبعاد) بقيمها المحدّدة للإطار الحالي.

‫1.6 احتساب التنسيق

  • التصميم الديناميكي: تحسب DynamicLayout الموضع والحجم النهائيين (الحدود) لكل عقدة في شجرة SquooshView.

  • تنسيق النص: تستخدم TextHelper عملية تنفيذ السمة LayoutHelper لحساب مقاييس النص وتغليفه وتشكيله. يساعد ذلك في التأكّد من أنّ النص يتدفق بشكل صحيح ضمن قيوده قبل عرضه.

‫1.7 أدوات القياس

هذه خطوة متخصّصة لواجهات مستخدم السيارات.

  • MeterData: إذا كانت إحدى العُقد تتضمّن بيانات عدّاد (محدّدة في Figma)، يتم تغيير شكلها الهندسي بشكل ديناميكي استنادًا إلى meter_value (مثل سرعة المركبة).
    • الأقواس: يتم ضبط زاوية المسح.
    • عمليات التدوير: يتم احتساب عملية التدوير استنادًا إلى زاويتَي البدء والنهاية.
    • أشرطة التقدم: يتم تغيير حجم عرض المستطيل أو ارتفاعه.
    • متجهات التقدّم: يتم تعديل طول مسار المتجه.

1.8 Animation

  • المقارنة: تتم مقارنة SquooshView الحالي بـ previous_squoosh_view من PreRenderCache.

  • الاستيفاء: إذا تغيّرت الخصائص، تنشئ Squoosh أدوات استيفاء للانتقال بسلاسة بين القيم (مثل التعتيم أو التحويل) بمرور الوقت.

المرحلة 2: إنشاء الأوامر

بعد اكتمال عملية تحليل شجرة SquooshView وتحريكها، يتم تحويلها إلى تسلسل خطي من أوامر الرسم.

المكوّن الرئيسي في هذه المرحلة هو حزمة DisplayList:

  • generate_dl: تتنقّل هذه الدالة بشكل متكرّر في شجرة SquooshView.

  • الترجمة:

    • الأشكال والمسارات: يتم تحويلها إلى DisplayListEntry مع نوع DisplayListAppearance المناسب (على سبيل المثال، Rect أو Path)
    • النص: تم تحويله باستخدام TextHelper إلى إدخالات رسم نصية.
    • عمليات التحويل والمقاطع: يتم تحويلها إلى أزواج PushTransform3D وPopTransform3D أو PushClipRegion وPopClipRegion لإدارة حزمة حالات الرسم.
    • الإخفاء: تم تحويلها إلى أزواج PushMaskLayer وPopMaskLayer من أجل إنشاء الطبقات ودمجها بشكلٍ صحيح.

النتيجة النهائية هي مثيل من Vec<DisplayListEntry> يصف ما سيتم رسمه، بغض النظر عن كيفية رسمه.

2.1 النقل إلى مشّغل رسائل

بعد إنشاء DisplayList، يغلّفه Reducer في مثيل من ViewDescriptor ويرسله عبر قناة Rust MPSC (LooperMessage) إلى سلسلة looper. يكون Looper مسؤولاً عن مرحلتَي العرض والتنفيذ، ما يمنع سلسلة Reducer من حظر مسار الرسومات.

المرحلة 3: العرض

يتم تسليم DisplayList المستقل عن النظام الأساسي إلى الخلفية الخاصة بالعرض، حيث يتم تحويل الأوامر المجردة إلى تعليمات لوحدة معالجة الرسومات.

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

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

للحصول على مقدمة شاملة حول بنية Impeller، شاهِد [Introducing Impeller - Flutter's new rendering engine][impeller-video]. على الرغم من أنّ الفيديو يتناول موضوع Flutter، إلا أنّ هذه المزايا الأساسية تعزّز بشكل مباشر حزمة HAR للسيارات.

المكوّنات الرئيسية لمرحلة العرض هي:

  • ImpellerRenderer: تحويل قائمة العرض من مرحلة العرض المسبق إلى أوامر العرض في Impeller

  • واجهة برمجة تطبيقات Impeller Rust: تعمل على تضمين مكتبة Impeller لاستخدامها في Rust (الحزمتان impeller وimpeller-rs-bindgen).

  • TypographyContext: تدير تسجيل الخطوط وتشكيل النصوص.

impeller-video

‫3.1 الإعداد وإدارة مساحة العرض

  • إنشاء السياق: يبدأ برنامج العرض مثيلاً من impeller::Context باستخدام برنامج OpenGL ES الخلفي، مع تمرير دالة ردّ اتصال لحل مؤشرات دالة OpenGL ES من سياق GL للنظام الأساسي.

  • سطح FBO المغلّف: بدلاً من إنشاء نافذته الخاصة، يعرض Impeller المحتوى في عنصر مخزن مؤقت للإطارات (FBO) حالي من OpenGL توفّره المرحلة 4. يتم ذلك من خلال استدعاء Surface::create_wrapped_fbo.

‫3.2 إدارة الموارد

  • الصور: تتوافق مع التنسيقات العادية والأنسجة المضغوطة KTX2. ويتم تحميلها إلى مواد العرض في وحدة معالجة الرسومات وإدارتها من خلال بنية Resources داخلية.

  • الخطوط: يتم تحميل خطوط TrueType وOpenType وتسجيلها باستخدام TypographyContext لعرض النصوص.

  • الصور الخارجية: تتضمّن المعالجة المتخصّصة للنسيج الخارجي (على سبيل المثال، خلاصات الكاميرا وعمليات العرض ثلاثي الأبعاد الخارجية) ربط مثيلات EGLImage أو نصوص OpenGL الخارجية بكائنات Texture في Impeller من أجل العرض بدون نسخ.

‫3.3 بطاقة العرض

تنشئ حلقة render مثيلاً من DisplayList في Impeller (يجب عدم الخلط بينه وبين Vec<DisplayListEntry> الذي يتم إنشاؤه في مرحلة العرض المسبق) باستخدام DisplayListBuilder:

  1. يمحو المخزن المؤقت ويطبّق عمليات التحويل العامة لتغيير حجم الشاشة حسب عدد النقاط في البوصة وتدوير الشاشة.

  2. تكرِّر هذه الدالة عناصر الإدخال DisplayListEntry:

    • الحالة: يتم استخدام save() وrestore() لتنفيذ عمليات تحويل ودفعها وإزالتها، بالإضافة إلى مناطق القص.
    • العناصر الأساسية: يتم رسم Rect وRoundedRect باستخدام عمليات الطلاء العادية.
    • المسارات: يتم إنشاء مسارات متجهة معقّدة (بما في ذلك مثيلات Arc الديناميكية) ورسمها.
    • يتم عرض النص: Text وStyledText باستخدام TypographyContext.
    • الصور: يتم رسم الصور العادية والخارجية باستخدام draw_texture_rect.
  3. يرسل قائمة العرض التي تم إنشاؤها في Impeller إلى السطح باستخدام surface.draw_display_list()، ما يؤدي إلى إنشاء أوامر GL الأساسية.

  4. يتم استدعاء swap_buffers() في السياق الأساسي لتفعيل المرحلة 4.

المرحلة 4: العرض التقديمي

تتعامل هذه المرحلة النهائية مع أجهزة العرض لعرض الإطار الذي تم عرضه. تستخدم HAR مسار عرض مباشر قويًا على نظام التشغيل Android Automotive OS (AAOS) في المركبات المزوّدة ببرامج (SDV).

المكوّن الرئيسي في هذه المرحلة هو HarDirectRenderingContext (في حزمة har-gl-context).

4.1 Architecture

تستخدم طبقة العرض نهجًا مزدوجًا مع هدف رسم خارج الشاشة:

  1. مخزن مؤقت للرسم: هو FBO خارج الشاشة يعرض فيه Impeller المشهد.

  2. تحديد حجم المخزن المؤقت (اختياري): مخزن مؤقت إضافي اختياري لدعم ميزة &quot;التنعيم المتعدد العينات&quot; (MSAA)

    • يمكن تفعيل هذا الخيار عند الحاجة من خلال تنفيذ OpenGL ES الأساسي أو الإعدادات. في مثل هذه الحالات، يعمل كهدف وسيط لحلّ مخزن الرسومات المتعددة العينات قبل النقل السريع للبيانات (نقل كتلة البتات) إلى مخزن العرض.
  3. مخزن العرض المؤقت: مخزن مؤقت عام يستند إلى عنصر GBM، ويتوافق مع المخزن المؤقت الخلفي في سلسلة تبديل الرسومات النموذجية.

  4. المخزن المؤقت الأمامي: هو مخزن مؤقت في GBM يتم عرضه على الشاشة.

‫4.2 سلسلة التبديل

عند استدعاء swap_buffers، يتّبع HAR الخطوات التالية:

  1. يتم نقل محتويات مخزن مؤقت للرسم إلى مخزن مؤقت للعرض (مع نقل وسيط إلى مخزن مؤقت للحل، إذا كان ذلك مطلوبًا في التنفيذ).

  2. يتم استدعاء glFlush() في سياق GL، ويتم إنشاء مثيل من EGL_SYNC_NATIVE_FENCE_ANDROID لتتبُّع اكتمال وحدة معالجة الرسومات.

  3. تنشئ هذه السمة طلبًا ذريًا لإدارة الحقوق الرقمية (DRM) من أجل تبديل المخزن المؤقت للعرض إلى الشاشة. يحتوي هذا الطلب على معرّف وصف ملف (FD) لحاجز وحدة معالجة الرسومات (يُسمى الحاجز الداخلي) لمنع وحدة التحكّم في العرض من عرض مخزن العرض المؤقت قبل أن تنتهي وحدة معالجة الرسومات من الرسم.

  4. يطلب في الوقت نفسه سياجًا جديدًا من نظام إدارة الحقوق الرقمية (يُسمى السياج الخارجي)، وذلك للإشارة إلى أنّ المخزن المؤقت السابق (المخزن المؤقت الأمامي للإطار السابق) لم يعُد معروضًا على الشاشة.

  5. تنفِّذ هذه السمة الطلب الذري باستخدام العلامة غير الحظرية، ما يتيح استمرار عمل سلسلة المحادثات الرئيسية مع الحفاظ على مزامنة الأنظمة الفرعية للرسومات.

  6. يخزِّن هذا الحقل السياج الخارجي الجديد في السياق حتى يتمكّن HAR من انتظار الإشارة إليه في بداية عملية swap_buffers في الإطار اللاحق. يمنع ذلك وحدة معالجة الرسومات من الرسم في مخزن مؤقت لا يزال معروضًا.

‫4.3 إعداد الوضع المباشر

تتفاعل HAR مباشرةً مع النواة باستخدام أنظمة DRM وKernel Mode Setting (KMS) الفرعية لإعداد دقة العرض في AAOS SDV، مع تجاوز التفاعلات مع أدوات إدارة النوافذ، مثل SurfaceFlinger (في إعدادات معيّنة)، ما يتيح التحكّم الحصري والعالي الأولوية في أجهزة العرض.

‫4.4 العرض الخارجي

تتيح HAR تفويض عرض عناصر معيّنة في واجهة المستخدم (يتم تحديدها باستخدام علامات في Figma) إلى عمليات أو سلاسل خارجية. ويكون ذلك مفيدًا لدمج مشاهد ثلاثية الأبعاد معقّدة (على سبيل المثال، عرض مرئي لسيارة من محركات مثل Kanzi أو Unity) أو محتوى آخر يتطلّب سياق OpenGL مخصّصًا.

‫4.4.1 المكوّنات الرئيسية

  • HarExternalRenderContext: سياق EGL مخصّص خارج الشاشة للخدمة الخارجية
  • SurfacePool: يدير مجموعة من مخازن LocalSurface المؤقتة (Texture بالإضافة إلى EGLImage) للتخزين المؤقت المزدوج أو الثلاثي.
  • SharedSurfaceExternalImage: برنامج تضمين آمن للاستخدام المتزامن لتمرير مقابض EGLImage بين الخدمة الخارجية والعارض الرئيسي.

‫4.4.2 سير العمل

يتّبع سير العمل التسلسل التالي:

  1. تبدأ الخدمة الخارجية وتسجّل نفسها في أداة التكرار الرئيسية، مع تحديد علامات Figma التي تعرضها (على سبيل المثال، #cluster/3d-car).

  2. تنتظر الخدمة إشارات RenderStart من أداة التكرار لمواءمة عملية العرض مع إشارة VSYNC للشاشة.

  3. في الخلفية، تعرض الخدمة محتواها في إطار مخزن مؤقت للصور تقدّمه SurfacePool.

  4. تستدعي الخدمة swap_buffers في سياقها، ما يؤدي إلى تدوير المجموعة وإتاحة الإطار المكتمل كنسخة من SharedSurface.

  5. يتم تضمين SharedSurface في ExternalImage وإرساله عبر قناة Rust MPSC إلى مشّغل رسائل.

  6. يتلقّى عارض Impeller الرئيسي (المرحلة 3) الصورة الخارجية. بدلاً من نسخ بيانات البكسل، يتم ربط EGLImage الأساسي مباشرةً بمادة ورسمه كجزء من المشهد الرئيسي، ما يؤدي إلى تحقيق التركيب بدون نسخ.

‫4.5 منصات التطوير والاختبار (har-platform-linux)

لأغراض التطوير والاختبار، يمكن أن تستهدف تطبيقات HAR بيئات سطح المكتب العادية لنظام التشغيل Linux وعمليات الإعداد بدون شاشة. يتم تنفيذ هذه الأنظمة الأساسية في حزمة crates/reference/platforms/har-platform-linux.

وعلى عكس هدف AAOS SDV الإنتاجي، لا تستخدم هذه المنصات نظام direct-rendering الفرعي من har-gl-context لإخراج العرض. بدلاً من ذلك، تعتمد على حِزم Rust OpenGL العادية:

  • وضع النافذة: يستخدم winit لإدارة النوافذ وحلقات الأحداث، وglutin لإنشاء سياقات OpenGL ES والدمج مع نظام النوافذ.

  • وضع التشغيل بدون واجهة مستخدم رسومية: يستخدم حزمة har-gl-context لإنشاء سياق pbuffer خارج الشاشة مع شاشة EGL التلقائية. يتيح ذلك عرض المحتوى في مخزن مؤقت خارج الشاشة بدون الحاجة إلى نافذة مرئية أو إذن مباشر بالوصول إلى أجهزة العرض، ويُستخدم بشكل أساسي في الاختبارات الآلية أو معالجة الخلفية.