בדף הזה מפורט צינור הגרפיקה המלא של רכיב הרינדור בזמינות גבוהה (HAR), עם מעקב אחר זרימת הנתונים ממסמך עיצוב של Figma ועד לפיקסלים הסופיים שמוצגים על המסך.
סקירה כללית
תהליך הצינור ממיר הגדרות של ממשק משתמש ברמה גבוהה לפקודות גרפיות ברמה נמוכה, ומציג אותן ביעילות בתצוגות חומרה. הצינור מיועד לאפליקציות קריטיות לבטיחות ברכב, עם דגש על עיבוד דטרמיניסטי, ניהול יעיל של מצבים ואינטראקציה חזקה עם מערכות משנה של גרפיקה בפלטפורמה, כמו Direct Rendering Manager (DRM) ו-Generic Buffer Management (GBM).
התהליך מחולק לארבעה שלבים עיקריים:
- טרום-רינדור: עיבוד של גרף הסצנה, החלת התאמות אישיות ופתרון פריסה.
- יצירת פקודות: המרת גרף הסצנה שנפתר לרשימת תצוגה שאינה תלויה ב-backend.
- רינדור: הפעלת פקודות ציור באמצעות מנוע הגרפיקה Impeller.
- מצגת: ניהול של מאגרי מסגרות וסנכרון עם חומרת התצוגה.
איור 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. דוגמה לכך אפשר לראות במבנהUIModelstruct בתיבתharry-app-core. - Squoosh: מספק את מבנה הנתונים
SquooshViewואת מאגר הווריאציות, שמשמשים לעיבוד ממשק המשתמש בהתאם לעיצוב. תיבתdc_bundleטוענת מסמך עיצוב מסודר מהספרייה DesignCompose וממירה אותו לעץ של מבני נתונים מסוגSquooshViewכדי לשפר את הביצועים בזמן הריצה.
1.2 הפניה חזרה לכתובת האתר המקורית
הצינור מבוסס על פעולות. המסגרת מציינת את Actionsסוג המנייה שמגדיר פעולות פנימיות שמשמשות את המסגרת עצמה, אבל כולל גם וריאנט CustomAction שמאפשר למשתמשים להגדיר פעולות נוספות ספציפיות לאפליקציה (לדוגמה, UpdateVehicleSpeed או ButtonPress).
בנוסף, המסגרת מספקת את המאפיין StateAction שמפשט את ההטמעה של פעולות שמשפיעות על מצב האפליקציה, ובאופן אופציונלי יוצר תופעות לוואי שמועברות בחזרה לאפליקציה מהפונקציה reducer לצורך עיבוד. דוגמה מפורטת לכך אפשר לראות ב-enum CustomActions ב-crate harry-app-core.
זוהי תוכנית בסיסית של לולאת ה-reducer:
- עיבוד הפעולה:
Reducerמקבל פעולה ומעדכן את המצב הנוכחי. אלה הנתונים הגולמיים, כמו המהירות הנוכחית או נורות האזהרה שפעילות. יכול להיות שיווצרו גם תופעות לוואי (לדוגמה, אות שמשמיע צלצול כשאור חגורת הבטיחות מהבהב). - Presentation:
Presentermaps the new state intoUIModel. 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 – מנוע הרינדור החדש של Flutter). למרות שהסרטון עוסק ב-Flutter, היתרונות העיקריים האלה תומכים ישירות ב-HAR automotive stack.
הרכיבים העיקריים של שלב הרינדור הם:
ImpellerRenderer: ממיר את רשימת התצוגה משלב הטרום-רינדור לפקודות רינדור של Impeller.Impeller Rust API: עוטף את ספריית Impeller לשימוש ב-Rust (תיבות הכלים
impellerו-impeller-rs-bindgen).
TypographyContext: ניהול של רישום גופנים ועיצוב טקסט.
3.1 הפעלה וניהול של משטחים
יצירת הקשר: רכיב הרינדור מאתחל מופע של
impeller::Contextעם קצה עורפי של OpenGL ES, ומעביר קריאה חוזרת כדי לפתור מצביעים של פונקציות OpenGL ES מהקשר GL של הפלטפורמה.משטח FBO עטוף: במקום ליצור חלון משלו, Impeller מבצע רינדור לאובייקט framebuffer (FBO) של OpenGL קיים שסופק על ידי שלב 4. כדי לעשות את זה, צריך להתקשר למספר
Surface::create_wrapped_fbo.
3.2 ניהול משאבים
תמונות: תמיכה בפורמטים סטנדרטיים ובטקסטורות דחוסות בפורמט KTX2. הם מועלים לטקסטורות של GPU ומנוהלים על ידי מבנה פנימי של
Resources.גופנים: גופני TrueType ו-OpenType נטענים ונרשמים ב-
TypographyContextלצורך עיבוד טקסט.תמונות חיצוניות: טיפול מיוחד בטקסטורות חיצוניות (לדוגמה, פידים של מצלמות ומעבדי תלת-ממד חיצוניים) כולל קישור של מופעי
EGLImageאו טקסטורות חיצוניות של OpenGL לאובייקטים של ImpellerTextureלצורך עיבוד ללא העתקה.
3.3 Render pass
הלולאה render יוצרת מופע DisplayList של Impeller (לא להתבלבל עם Vec<DisplayListEntry> שנוצר בשלב הטרום-רינדור) באמצעות DisplayListBuilder:
מנקה את המאגר ומחיל טרנספורמציות גלובליות עבור קנה מידה של DPI וסיבוב המסך.
הפונקציה מבצעת איטרציה על פריטי הקלט
DisplayListEntry:- מצב: התגים
save()ו-restore()משמשים להעברה של טרנספורמציות ואזורי חיתוך. - פרימיטיבים: הציור של
Rectו-RoundedRectמתבצע באמצעות פעולות צביעה רגילות. - נתיבים: נתיבי וקטור מורכבים (כולל מופעי
Arcדינמיים) נוצרים ומצוירים. - טקסט:
Textו-StyledTextמעובדים באמצעותTypographyContext. - תמונות: תמונות רגילות ותמונות חיצוניות מצוירות באמצעות
draw_texture_rect.
- מצב: התגים
שולח את רשימת התצוגה של Impeller אל ה-Surface באמצעות
surface.draw_display_list(), וכך יוצר את פקודות ה-GL הבסיסיות.מתבצעת קריאה
swap_buffers()בהקשר הבסיסי כדי להפעיל את שלב 4.
שלב 4: מצגת
בשלב הסופי הזה מתבצעת האינטראקציה עם חומרת התצוגה כדי להציג את הפריים שעבר רינדור. HAR משתמש בנתיב רנדור ישיר חזק בתוכנת Android Automotive OS (AAOS) Software-Defined Vehicle (SDV).
הרכיב העיקרי בשלב הזה הוא HarDirectRenderingContext (בתיבת har-gl-context).
4.1 ארכיטקטורה
שכבת ההצגה משתמשת בגישה של מאגר כפול עם יעד ציור מחוץ למסך:
מאגר לציור: FBO מחוץ למסך שבו Impeller מעבד את הסצנה.
מאגר נתונים זמני לפתרון בעיות (אופציונלי): מאגר נתונים זמני עזר אופציונלי לתמיכה בשיטת MSAA (מניעת קצוות מחודדים עם דגימות מרובות)
- אפשר להפעיל את האפשרות הזו כשצריך באמצעות ההטמעה או ההגדרה הבסיסית של OpenGL ES. במקרים כאלה, הוא משמש כיעד ביניים לפתרון מאגר הציור עם דגימה מרובה לפני העברה של בלוק ביטים (bit block transfer) למאגר העיבוד.
מאגר רנדור: מאגר גנרי שמגובה על ידי אובייקט GBM, שמתאים למאגר האחורי בשרשרת החלפה גרפית טיפוסית.
מאגר קדמי: מאגר GBM שנסרק אל התצוגה.
4.2 החלפת שרשרת
כשקוראים ל-swap_buffers, HAR פועל לפי השלבים הבאים:
התוכן של מאגר הציור מועבר למאגר העיבוד (עם העברה ביניים למאגר הרזולוציה, אם נדרש בהטמעה).
מתבצעת קריאה ל-
glFlush()בהקשר של GL, ונוצרת מכונה שלEGL_SYNC_NATIVE_FENCE_ANDROIDכדי לעקוב אחרי השלמת ה-GPU.יוצר בקשה אטומית ל-DRM כדי להחליף את מאגר הרינדור במסך. הבקשה הזו מכילה את ה-FD של ה-GPU fence (נקרא in fence) כדי למנוע מבקר התצוגה להציג את מאגר הרינדור לפני שה-GPU מסיים את הציור.
במקביל, נשלחת בקשה לגידור חדש מ-DRM (נקרא גידור חיצוני), כדי לסמן מתי מאגר הנתונים הזמני הקודם (מאגר הנתונים הזמני הקדמי של המסגרת הקודמת) כבר לא מוצג במסך.
מבצע את הבקשה האטומית באמצעות הדגל nonblocking, כדי לאפשר לשרשור הראשי להמשיך בזמן שמערכות המשנה של הגרפיקה נשארות מסונכרנות.
מאחסן את הגדרת הגדר החדשה בהקשר, כדי ש-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בין השירות החיצוני לבין רכיב ה-renderer הראשי.
4.4.2 תהליך עבודה
רצף הפעולות בתהליך העבודה:
השירות החיצוני מופעל ונרשם ב-looper הראשי, ומציין אילו תגי Figma (לדוגמה,
#cluster/3d-car) הוא מעבד.השירות ממתין לאותות
RenderStartמהלופר כדי ליישר את העיבוד שלו עם אות ה-VSYNC של התצוגה.מחוץ למסך, השירות מעבד את התוכן שלו לתוך מאגר מסגרות שסופק על ידי
SurfacePool.השירות קורא ל-
swap_buffersבהקשר שלו, שמסובב את המאגר והופך את הפריים שהושלם לזמין כמופע שלSharedSurface.
SharedSurfaceעטוף ב-ExternalImageונשלח דרך ערוץ Rust MPSC אל ה-looper.המעבד הראשי של 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 שמוגדרת כברירת מחדל. האפשרות הזו מאפשרת לבצע רינדור למאגר זמני מחוץ למסך בלי צורך בחלון גלוי או בגישה ישירה לחומרה של המסך. היא משמשת בעיקר לבדיקות אוטומטיות או לעיבוד בקצה העורפי.