פייפליין גרפי של HAR

בדף הזה מפורט צינור הגרפיקה המלא של רכיב הרינדור בזמינות גבוהה (HAR), עם מעקב אחר זרימת הנתונים ממסמך עיצוב של Figma ועד לפיקסלים הסופיים שמוצגים במסך.

סקירה כללית

תהליך הצינור ממיר הגדרות של ממשק משתמש ברמה גבוהה לפקודות גרפיות ברמה נמוכה, ומציג אותן ביעילות בתצוגות חומרה. הצינור מיועד לאפליקציות קריטיות לבטיחות ברכב, עם דגש על עיבוד דטרמיניסטי, ניהול יעיל של מצבים ואינטראקציה חזקה עם מערכות משנה של גרפיקה בפלטפורמה, כמו Direct Rendering Manager‏ (DRM) ו-Generic Buffer Management‏ (GBM).

התהליך מחולק לארבעה שלבים עיקריים:

  1. טרום-רינדור: עיבוד של גרף הסצנה, החלת התאמות אישיות ופתרון פריסה.
  2. יצירת פקודות: המרת גרף הסצנה שנפתר לרשימת תצוגה שאינה תלויה ב-backend.
  3. רינדור: הפעלת פקודות ציור באמצעות מנוע הגרפיקה Impeller.
  4. מצגת: ניהול של מאגרי מסגרות וסנכרון עם חומרת התצוגה.

HAR Graphics Flow

איור 1. תרשים זרימה של HAR.

שלב 1: טרום-רינדור

בשלב הזה, העיצוב הסטטי ב-Figma ומצב האפליקציה הדינמי הופכים לעץ ממשק משתמש מלא בזיכרון, שמוכן לרינדור. השלב הזה פועל ב-thread ייעודי של reducer, בנפרד מלולאת התצוגה הראשית.

‫1.1 DesignCompose foundation

צינור ה-HAR מבוסס על המערכת האקולוגית של DesignCompose.

  • מקור: ממשק המשתמש מעוצב ב-Figma ומיוצא באמצעות התוסף DesignCompose.
  • הגדרה: הפלט הוא מופע של DesignComposeDefinition, ייצוג מסודר של העיצוב (צמתים, סגנונות, וריאציות).
  • קשירת נתונים: מודל ממשק המשתמש של האפליקציה משתמש בפקודות מאקרו פרוצדורליות (לדוגמה, #[Design(node = "#speed")]) כדי לקשור באופן מפורש שדות של מבנה Rust לצמתים ספציפיים עם שם במסמך Figma. כך מצב האפליקציה יכול להגדיר אוטומטית את המאפיינים של הרכיבים החזותיים.

הרכיבים העיקריים של הבסיס הזה הם:

  • Reducer: פועל כמרכז של לולאת האירועים, מעבד פעולות ומעדכן את המצב הנוכחי. המסגרת מספקת DefaultReducer, אבל אפשר לספק יישום מותאם אישית של reducer אם צריך.
  • Presenter: מגשר בין המצב הנוכחי לבין מודל ממשק המשתמש. המאפיין Presenter מוגדר בתיבת הכלים harry של framework, ויישום לדוגמה (UIModelPresenter) מסופק בתיבת הכלים harry-app-core.
  • מודל ממשק משתמש: יוצר התאמות אישיות על סמך המצב הנוכחי. קוד המודל של ממשק המשתמש נוצר באמצעות מאקרו DesignDocument שסופק על ידי תיבת derive_customizations. דוגמה לכך אפשר לראות במבנה UIModel בתיבת harry-app-core.
  • Squoosh: מספק את מבנה הנתונים SquooshView ואת מאגר הווריאציות, שמשמשים לעיבוד ממשק המשתמש בהתאם לעיצוב. תיבת dc_bundle טוענת מסמך עיצוב מסודר מהספרייה DesignCompose וממירה אותו לעץ של מבני נתונים מסוג SquooshView כדי לשפר את הביצועים בזמן הריצה.

‫1.2 הפניה חזרה לכתובת האתר המקורית

צינור עיבוד הנתונים מבוסס על פעולות. המסגרת מציינת את Actionsסוג המנייה שמגדיר פעולות פנימיות שבהן נעשה שימוש במסגרת עצמה, אבל כולל גם וריאנט של CustomAction שמאפשר למשתמשים להגדיר פעולות נוספות ספציפיות לאפליקציה (לדוגמה, UpdateVehicleSpeed או ButtonPress).

בנוסף, ה-framework מספק את המאפיין StateAction שמפשט את ההטמעה של פעולות שמשפיעות על מצב האפליקציה, ובאופן אופציונלי יוצר תופעות לוואי שמועברות בחזרה לאפליקציה מה-reducer לצורך עיבוד. ה-enum‏ CustomActions ב-crate‏ harry-app-core מספק דוגמה מפורטת לכך.

זוהי תוכנית בסיסית של לולאת ה-reducer:

  • עיבוד הפעולה: Reducer מקבל פעולה ומעדכן את המצב הנוכחי. אלה הנתונים הגולמיים, כמו המהירות הנוכחית או נורות האזהרה שפעילות. יכול להיות שיהיו לזה גם תופעות לוואי (לדוגמה, השמעת צליל כשהאור של חגורת הבטיחות מהבהב).
  • Presentation: Presenter ממפה את המצב החדש ל-UIModel. ‫UIModel הוא מודל תצוגה שמכיל נתונים בפורמט שמתאים במיוחד לממשק המשתמש (לדוגמה, עיצוב המהירות '120' כמחרוזת '65 מייל לשעה').
  • יצירת התאמה אישית: השיטה apply של מודל ממשק המשתמש מופעלת כדי ליצור קבוצה של מופעי RenderCustomization. אלה הוראות מפורשות לשינוי העיצוב ב-Figma (לדוגמה, 'הגדרת הטקסט של הצומת #speed ל-'65 mph'').
  • UpdatePolicy לאופטימיזציה: אחרי כל מעבר של טרום-רינדור, מוחזר ערך UpdatePolicy שמציין מתי נדרש עדכון הרינדור הבא. אם אין שינויים במצב בהמתנה ואף אנימציה לא פועלת, הערך UpdatePolicy מציין שאין צורך בעדכונים נוספים באופן מיידי. במקרים כאלה, הרכיב Reducer מפסיק ליצור רשימות תצוגה חדשות, וכך נמנעים מחזורי עיבוד מיותרים ונשמרים משאבים עד שפעולה או אירוע חדשים מפעילים שינוי.

‫1.3 צפייה בהטמעה ובאתחול של המאגר

הצינור מתחיל עם מופע DesignComposeDefinition. זהו מסמך העיצוב של Figma שעבר סריאליזציה על ידי DesignCompose למבנה של מאגר אחסון לפרוטוקולים.

  • טעינה ראשונית: בהפעלה, העיצוב הראשי (שמצוין על ידי צומת הבסיס שלו) מומר מ-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 אנימציה

  • השוואה: המדד הנוכחי 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 מועברת לחלק האחורי של העיבוד, שבו פקודות מופשטות מתורגמות להוראות GPU.

ב-HAR נעשה שימוש ב-Impeller, מנוע עיבוד תמונות שנוצר במקור עבור Flutter. ‫Impeller נועד לפתור את הבעיה של תקלות בקצב הפריימים שנובעות מהידור של Shader, על ידי הידור מראש של קבוצה קטנה ויעילה של Shader בזמן הבנייה. הגישה הזו, בשילוב עם חלוקה יעילה למנות וקצה עורפי שעבר אופטימיזציה גבוהה, מאפשרת:

  • ביצועים דטרמיניסטיים: כמעט ולא מתרחשות תקלות בהידור של Shader בזמן ריצה.
  • הפעלה מהירה: מצמצמת את התקורה של האתחול.
  • טביעת רגל קטנה: יוצר גודל בינארי קומפקטי.

כדי לקבל מבוא מקיף לארכיטקטורה של Impeller, כדאי לצפות בסרטון [Introducing Impeller - Flutter's new rendering engine][impeller-video]. למרות שהסרטון עוסק ב-Flutter, היתרונות העיקריים האלה תומכים ישירות ב-HAR automotive stack.

הרכיבים העיקריים של שלב הרינדור הם:

  • ImpellerRenderer: ממיר את רשימת התצוגה משלב הטרום-רינדור לפקודות רינדור של Impeller.

  • Impeller Rust API: עוטף את ספריית Impeller לשימוש ב-Rust (הארגזים impeller ו-impeller-rs-bindgen).

  • TypographyContext: ניהול של רישום גופנים ועיצוב טקסט.

impeller-video

‫3.1 הפעלה וניהול של משטחים

  • יצירת הקשר: רכיב הרינדור מאתחל מופע של impeller::Context עם קצה עורפי של OpenGL ES, ומעביר קריאה חוזרת כדי לפתור מצביעים של פונקציות OpenGL ES מהקשר GL של הפלטפורמה.

  • משטח FBO עטוף: במקום ליצור חלון משלו, Impeller מבצע רינדור לאובייקט framebuffer‏ (FBO) של OpenGL קיים שסופק על ידי Phase 4. כדי לעשות את זה, מתקשרים אל Surface::create_wrapped_fbo.

‫3.2 ניהול משאבים

  • תמונות: תמיכה בפורמטים סטנדרטיים ובטקסטורות דחוסות בפורמט KTX2. הם מועלים לטקסטורות של ה-GPU ומנוהלים על ידי מבנה פנימי של Resources.

  • גופנים: גופני TrueType ו-OpenType נטענים ונרשמים ב-TypographyContext לצורך עיבוד טקסט.

  • תמונות חיצוניות: טיפול מיוחד בטקסטורות חיצוניות (לדוגמה, פידים של מצלמות ומעבדי תלת-ממד חיצוניים) כולל קישור של מופעי EGLImage או טקסטורות חיצוניות של OpenGL לאובייקטים של Impeller Texture לצורך עיבוד ללא העתקה.

‫3.3 Render pass

הלולאה render יוצרת מופע DisplayList של Impeller (לא להתבלבל עם Vec<DisplayListEntry> שנוצר בשלב הטרום-רינדור) באמצעות DisplayListBuilder:

  1. מנקה את המאגר ומחיל טרנספורמציות גלובליות עבור קנה מידה של DPI וסיבוב המסך.

  2. הפונקציה מבצעת איטרציה על פריטי הקלט DisplayListEntry:

    • מצב: התגים save() ו-restore() משמשים להעברה של טרנספורמציות ואזורי חיתוך.
    • פרימיטיבים: הציור של Rect ו-RoundedRect מתבצע באמצעות פעולות צביעה רגילות.
    • נתיבים: נתיבי וקטור מורכבים (כולל מופעי Arc דינמיים) נוצרים ומצוירים.
    • טקסט: Text ו-StyledText מעובדים באמצעות TypographyContext.
    • תמונות: תמונות רגילות ותמונות חיצוניות מצוירות באמצעות draw_texture_rect.
  3. שולח את רשימת התצוגה של Impeller אל ה-Surface באמצעות surface.draw_display_list(), וכך יוצר את פקודות ה-GL הבסיסיות.

  4. המודל שולח קריאה swap_buffers() להקשר הבסיסי כדי להפעיל את שלב 4.

שלב 4: מצגת

בשלב הסופי הזה מתבצעת האינטראקציה עם חומרת התצוגה כדי להציג את הפריים שעבר רינדור. ‫HAR משתמש בנתיב רנדור ישיר חזק ב-Android Automotive OS (מערכת ההפעלה של Android לרכב) Software-Defined Vehicle (רכב מוגדר תוכנה, SDV).

הרכיב העיקרי בשלב הזה הוא HarDirectRenderingContext (בתיבת har-gl-context).

‫4.1 ארכיטקטורה

שכבת ההצגה משתמשת בגישה של מאגר כפול עם יעד ציור מחוץ למסך:

  1. מאגר לציור: FBO מחוץ למסך שבו Impeller מעבד את הסצנה.

  2. מאגר נתונים זמני לפתרון בעיות (אופציונלי): מאגר נתונים זמני עזר אופציונלי לתמיכה בשיטת MSAA (מניעת קצוות מחודדים עם דגימות מרובות)

    • אפשר להפעיל את האפשרות הזו כשצריך באמצעות ההטמעה או ההגדרה הבסיסית של OpenGL ES. במקרים כאלה, הוא משמש כיעד ביניים כדי לפתור את מאגר הציור עם הדגימה המרובה לפני ההעתקה (העברת בלוק ביטים) למאגר העיבוד.
  3. מאגר רנדור: מאגר גנרי שמגובה באובייקט GBM, שמתאים למאגר האחורי בשרשרת החלפה גרפית טיפוסית.

  4. מאגר קדמי: מאגר GBM שנסרק אל התצוגה.

‫4.2 החלפת שרשרת

כשקוראים ל-swap_buffers, HAR פועל לפי השלבים הבאים:

  1. הפונקציה מעבירה את התוכן של מאגר הציור למאגר העיבוד (עם העברה ביניים למאגר הפתרון, אם נדרש על ידי ההטמעה).

  2. מתבצעת קריאה ל-glFlush() בהקשר של GL, ונוצרת מכונה של EGL_SYNC_NATIVE_FENCE_ANDROID כדי לעקוב אחרי השלמת ה-GPU.

  3. יוצר בקשה אטומית של DRM להחלפת מאגר הרינדור במסך. הבקשה הזו מכילה את ה-FD של ה-GPU fence (נקרא in fence) כדי למנוע מבקר התצוגה להציג את מאגר הרינדור לפני שה-GPU מסיים את הציור.

  4. במקביל, המערכת שולחת בקשה לגדר חדשה מ-DRM (נקראת הגדר היציאה), כדי לסמן מתי המאגר הקודם (המאגר הקדמי של הפריים הקודם) כבר לא מוצג במסך.

  5. מבצע את הבקשה האטומית באמצעות הדגל nonblocking, כדי לאפשר לשרשור הראשי להמשיך בזמן שמערכות המשנה של הגרפיקה נשארות מסונכרנות.

  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. השירות החיצוני מופעל ונרשם ב-looper הראשי, ומציין אילו תגי Figma (לדוגמה, #cluster/3d-car) הוא מעבד.

  2. השירות ממתין לאותות RenderStart מהלופר כדי ליישר את העיבוד שלו עם אות ה-VSYNC של התצוגה.

  3. מאחורי הקלעים, השירות מעבד את התוכן שלו לתוך מאגר מסגרות שסופק על ידי SurfacePool.

  4. השירות קורא ל-swap_buffers בהקשר שלו, שמסובב את המאגר ומאפשר להשתמש בפריים שהושלם כמופע של SharedSurface.

  5. SharedSurface עטוף ב-ExternalImage ונשלח דרך ערוץ Rust MPSC אל ה-looper.

  6. התמונה החיצונית מתקבלת על ידי רכיב ה-renderer הראשי של Impeller (שלב 3). במקום להעתיק נתוני פיקסלים, הוא קושר את EGLImage הבסיסי ישירות לטקסטורה ומצייר אותו כחלק מהסצנה הראשית, וכך מתבצעת קומפוזיציה ללא העתקה.

‫4.5 פלטפורמות פיתוח ובדיקה (har-platform-linux)

למטרות פיתוח ובדיקה, אפליקציות HAR יכולות להיות מיועדות לסביבות שולחן עבודה רגילות של Linux ולהגדרות headless. הפלטפורמות האלה מיושמות בתיבת crates/reference/platforms/har-platform-linux.

בניגוד ליעד SDV של AAOS בסביבת הייצור, הפלטפורמות האלה לא משתמשות במערכת המשנה direct-rendering של har-gl-context לתצוגת הפלט. במקום זאת, הם מסתמכים על תיבות סטנדרטיות של Rust OpenGL:

  • מצב חלון: נעשה שימוש ב-winit לניהול חלונות וללולאות אירועים, וב-glutin ליצירת הקשרים של OpenGL ES ולשילוב עם מערכת החלונות.

  • מצב Headless: נעשה שימוש ב-crate‏ har-gl-context כדי ליצור הקשר של pbuffer מחוץ למסך עם תצוגת EGL שמוגדרת כברירת מחדל. האפשרות הזו מאפשרת לבצע רינדור למאגר זמני מחוץ למסך בלי צורך בחלון גלוי או בגישה ישירה לחומרה של המסך. היא משמשת בעיקר לבדיקות אוטומטיות או לעיבוד בקצה העורפי.