Quy trình đồ hoạ HAR

Trang này trình bày chi tiết quy trình đồ hoạ hoàn chỉnh của trình kết xuất có độ khả dụng cao (HAR), theo dõi luồng dữ liệu từ tài liệu thiết kế Figma đến các pixel cuối cùng hiển thị trên màn hình.

Tổng quan

Quy trình này chuyển đổi các định nghĩa giao diện người dùng cấp cao thành các lệnh đồ hoạ cấp thấp và trình bày các lệnh đó một cách hiệu quả trên màn hình phần cứng. Quy trình này được thiết kế cho các ứng dụng quan trọng về an toàn cho ô tô, nhấn mạnh vào việc kết xuất mang tính xác định, quản lý trạng thái hiệu quả và tương tác mạnh mẽ với các hệ thống con đồ hoạ của nền tảng, chẳng hạn như Trình quản lý kết xuất trực tiếp (DRM) và Trình quản lý vùng đệm chung (GBM).

Quy trình này có thể được chia thành 4 giai đoạn chính:

  1. Kết xuất trước: Xử lý biểu đồ cảnh, áp dụng các tuỳ chỉnh và phân giải bố cục.
  2. Tạo lệnh: Chuyển đổi biểu đồ cảnh đã phân giải thành danh sách hiển thị không phụ thuộc vào phần phụ trợ.
  3. Kết xuất: Thực thi các lệnh vẽ bằng công cụ đồ hoạ Impeller.
  4. Trình bày: Quản lý các khung đệm và đồng bộ hoá với phần cứng hiển thị.

Luồng đồ hoạ HAR

Hình 1. Luồng đồ hoạ HAR.

Giai đoạn 1: Kết xuất trước

Giai đoạn này chuyển đổi thiết kế Figma tĩnh và trạng thái ứng dụng động thành một cây giao diện người dùng đã phân giải hoàn toàn trong bộ nhớ, sẵn sàng cho quá trình kết xuất. Giai đoạn này chạy trên một luồng bộ giảm tải chuyên dụng, tách biệt với vòng lặp hiển thị chính.

1.1 Nền tảng DesignCompose

Quy trình HAR được xây dựng dựa trên hệ sinh thái DesignCompose.

  • Nguồn: Giao diện người dùng được thiết kế trong Figma và xuất bằng trình bổ trợ DesignCompose.
  • Định nghĩa: Kết quả là một thực thể của DesignComposeDefinition, một biểu diễn được tuần tự hoá của thiết kế (các nút, kiểu, biến thể).
  • Liên kết dữ liệu: Mô hình giao diện người dùng của ứng dụng sử dụng các macro theo quy trình (ví dụ: #[Design(node = "#speed")]) để liên kết rõ ràng các trường cấu trúc Rust với các nút được đặt tên cụ thể trong tài liệu Figma. Điều này cho phép trạng thái ứng dụng tự động điều khiển các thuộc tính của các phần tử trực quan.

Các thành phần chính của nền tảng này là:

  • Bộ giảm tải: Hoạt động như vòng lặp sự kiện trung tâm, xử lý các hành động và cập nhật trạng thái hiện tại. Khung này cung cấp DefaultReducer, nhưng bạn có thể cung cấp một cách triển khai bộ giảm tải tuỳ chỉnh nếu cần.
  • Trình trình bày: Kết nối trạng thái hiện tại với mô hình giao diện người dùng. Đặc điểm Presenter được chỉ định bởi thùng khung harry và một cách triển khai tham chiếu (UIModelPresenter) được cung cấp trong thùng harry-app-core.
  • Mô hình giao diện người dùng: Tạo các tuỳ chỉnh dựa trên trạng thái hiện tại. Mã mô hình giao diện người dùng được tạo bằng macro DesignDocument do thùng derive_customizations cung cấp. Cấu trúc UIModel trong thùng harry-app-core cung cấp một ví dụ về điều này.
  • Squoosh: Cung cấp cấu trúc dữ liệu SquooshView và kho lưu trữ biến thể, dùng để kết xuất giao diện người dùng theo thiết kế. Một tài liệu thiết kế được tuần tự hoá sẽ được thùng dc_bundle tải từ thư viện DesignCompose và chuyển đổi thành một cây cấu trúc SquooshView để có hiệu suất thời gian chạy hiệu quả.

1.2 Vòng lặp bộ giảm tải

Quy trình này được điều khiển bởi các hành động. Khung này chỉ định loại được liệt kê Actions xác định các hành động nội bộ do chính khung này sử dụng, nhưng cũng bao gồm một biến thể CustomAction cho phép người dùng xác định các hành động bổ sung dành riêng cho ứng dụng (ví dụ: UpdateVehicleSpeed hoặc ButtonPress).

Khung này cũng cung cấp đặc điểm StateAction giúp đơn giản hoá việc triển khai các hành động ảnh hưởng đến trạng thái ứng dụng và tuỳ ý tạo các tác dụng phụ, sau đó được chuyển lại cho ứng dụng từ bộ giảm tải để xử lý. Enum CustomActions trong thùng harry-app-core cung cấp một ví dụ chi tiết về điều này.

Đây là phác thảo cơ bản của vòng lặp bộ giảm tải:

  • Xử lý hành động: Reducer nhận một hành động và cập nhật trạng thái hiện tại. Đây là dữ liệu thô, chẳng hạn như tốc độ hiện tại hoặc các chỉ báo (đèn cảnh báo) đang hoạt động. Điều này cũng có thể tạo ra các tác dụng phụ (ví dụ: một tín hiệu phát ra tiếng chuông khi đèn thắt dây an toàn nhấp nháy).
  • Trình bày: Presenter ánh xạ trạng thái mới vào UIModel. UIModel là một mô hình xem, lưu giữ dữ liệu được định dạng cụ thể cho giao diện người dùng (ví dụ: định dạng tốc độ "120" thành chuỗi "65 dặm/giờ").
  • Tạo tuỳ chỉnh: Phương thức apply của mô hình giao diện người dùng được gọi để tạo một tập hợp các thực thể RenderCustomization. Đây là các hướng dẫn rõ ràng để sửa đổi thiết kế Figma (ví dụ: "Đặt văn bản của nút #speed thành '65 dặm/giờ'").
  • UpdatePolicy để tối ưu hoá: Sau mỗi lần kết xuất trước, một giá trị UpdatePolicy sẽ được trả về, cho biết thời điểm cần cập nhật kết xuất tiếp theo. Nếu không có thay đổi trạng thái nào đang chờ xử lý và không có hoạt ảnh nào đang chạy, UpdatePolicy sẽ báo hiệu rằng không cần cập nhật thêm ngay lập tức. Trong những trường hợp như vậy, Bộ giảm tải sẽ ngừng tạo danh sách hiển thị mới, ngăn chặn các chu kỳ kết xuất không cần thiết và tiết kiệm tài nguyên cho đến khi một hành động hoặc sự kiện mới kích hoạt thay đổi.

1.3 Nhập chế độ xem và khởi chạy kho lưu trữ

Quy trình này bắt đầu bằng một thực thể DesignComposeDefinition. Đây là tài liệu thiết kế Figma được DesignCompose chuyển đổi tuần tự thành cấu trúc vùng đệm giao thức.

  • Tải ban đầu: Khi khởi động, thiết kế chính (được chỉ định bởi nút gốc) sẽ được chuyển đổi từ DesignComposeDefinition thành cây SquooshView ban đầu. Đây là quy trình một lần.

  • Kho lưu trữ: SquooshVariantRepository quản lý các biến thể thành phần có thể dùng lại và các chế độ xem được tải ban đầu.

  • Tải từng phần: Để giảm thiểu thời gian khởi động và mức sử dụng bộ nhớ, các chế độ xem bổ sung (những chế độ xem không thuộc cây nút gốc ban đầu) sẽ được tải từng phần từ tài liệu chỉ khi chúng được tham chiếu rõ ràng và cần thiết cho logic kết xuất (ví dụ: trong quá trình tuỳ chỉnh danh sách).

1.4 Tuỳ chỉnh

Cây SquooshView được duyệt để áp dụng trạng thái ứng dụng động:

  • Hoán đổi biến thể: Các thực thể thành phần được hoán đổi với các biến thể cụ thể (ví dụ: thay đổi biểu tượng đại diện cho chế độ lái hiện tại từ thể thao sang tiết kiệm) dựa trên logic thời gian chạy.

  • Mở rộng danh sách: Một mục mẫu duy nhất trong Figma được thay thế bằng danh sách con động. Mã nhận dạng duy nhất mới được tạo cho các phần tử con này để xác minh danh tính ổn định cho hoạt ảnh.

  • Ghi đè văn bản và kiểu: Nội dung văn bản (ví dụ: giá trị tốc độ) và kiểu (ví dụ: độ mờ, màu sắc) được cập nhật từ trạng thái hiện tại.

1.5 Độ phân giải biến thiên

Các mã thông báo và biến thiết kế được xác định trong Figma hoặc cục bộ trong ứng dụng sẽ được phân giải.

  • Liên kết: Các thuộc tính SquooshView tham chiếu đến các biến (như màu sắc hoặc kích thước) được thay thế bằng các giá trị cụ thể cho khung hiện tại.

1.6 Tính toán bố cục

  • Bố cục động: DynamicLayout tính toán vị trí và kích thước cuối cùng (ranh giới) của mọi nút trong cây SquooshView.

  • Bố cục văn bản: TextHelper sử dụng cách triển khai đặc điểm LayoutHelper để tính toán các chỉ số văn bản, ngắt dòng và định hình. Điều này giúp xác minh rằng văn bản được hiển thị đúng trong các ràng buộc trước khi kết xuất.

1.7 Mặt số và đồng hồ đo

Đây là bước chuyên biệt cho giao diện người dùng ô tô.

  • MeterData: Nếu một nút có dữ liệu đồng hồ đo (được xác định trong Figma), thì hình học của nút đó sẽ được thay đổi linh hoạt dựa trên meter_value (ví dụ: tốc độ xe).
    • Vòng cung: Góc quét được điều chỉnh.
    • Xoay: Biến đổi xoay được tính dựa trên góc bắt đầu và góc kết thúc.
    • Thanh tiến trình: Chiều rộng hoặc chiều cao của hình chữ nhật được điều chỉnh theo tỷ lệ.
    • Véc tơ tiến trình: Chiều dài của đường dẫn véc tơ được điều chỉnh.

1.8 Hoạt ảnh

  • So sánh: SquooshView hiện tại được so sánh với previous_squoosh_view từ PreRenderCache.

  • Nội suy: Nếu các thuộc tính đã thay đổi, Squoosh sẽ tạo các bộ nội suy để chuyển đổi các giá trị (ví dụ: độ mờ hoặc biến đổi) một cách mượt mà theo thời gian.

Giai đoạn 2: Tạo lệnh

Sau khi cây SquooshView được phân giải và tạo hoạt ảnh hoàn toàn, cây này sẽ được chuyển đổi thành một chuỗi lệnh vẽ tuyến tính.

Thành phần chính của giai đoạn này là thùng DisplayList:

  • generate_dl: Hàm này duyệt đệ quy cây SquooshView.

  • Bản dịch:

    • Hình dạng và đường dẫn: Chuyển đổi thành DisplayListEntry với biến thể DisplayListAppearance thích hợp (ví dụ: Rect hoặc Path)
    • Văn bản: Chuyển đổi bằng TextHelper thành các mục vẽ văn bản.
    • Biến đổi và cắt: Chuyển đổi thành các cặp PushTransform3DPopTransform3D hoặc PushClipRegionPopClipRegion để quản lý ngăn xếp trạng thái vẽ.
    • Tạo mặt nạ: Chuyển đổi thành các cặp PushMaskLayerPopMaskLayer để tạo và kết hợp các lớp một cách chính xác.

Kết quả cuối cùng là một thực thể của Vec<DisplayListEntry> mô tả nội dung cần vẽ, độc lập với cách vẽ.

2.1 Chuyển giao cho trình lặp

Sau khi DisplayList được tạo, Bộ giảm tải sẽ gói danh sách này trong một thực thể của ViewDescriptor và gửi danh sách đó qua kênh Rust MPSC (LooperMessage) đến luồng trình lặp. Looper chịu trách nhiệm về các giai đoạn kết xuất và hiển thị, ngăn luồng Bộ giảm tải chặn quy trình đồ hoạ.

Giai đoạn 3: Kết xuất

DisplayList không phụ thuộc vào nền tảng được chuyển giao cho phần phụ trợ kết xuất, trong đó các lệnh trừu tượng được dịch thành hướng dẫn GPU.

HAR sử dụng Impeller, một công cụ kết xuất ban đầu được xây dựng cho Flutter. Impeller được thiết kế để giải quyết vấn đề về lỗi tốc độ khung hình do quá trình biên dịch chương trình đổ bóng bằng cách biên dịch trước một tập hợp nhỏ và hiệu quả các chương trình đổ bóng tại thời gian xây dựng. Phương pháp này, kết hợp với việc phân lô hiệu quả và phần phụ trợ được tối ưu hoá cao, mang lại:

  • Hiệu suất mang tính xác định: Hầu như loại bỏ các lỗi biên dịch chương trình đổ bóng trong thời gian chạy.
  • Khởi động nhanh: Giảm chi phí khởi chạy.
  • Dung lượng nhỏ: Tạo ra kích thước tệp nhị phân nhỏ gọn.

Để biết thông tin chi tiết về kiến trúc của Impeller, hãy xem [Giới thiệu về Impeller – công cụ kết xuất mới của Flutter][impeller-video]. Mặc dù video này thảo luận về Flutter, nhưng những lợi ích cốt lõi này sẽ trực tiếp hỗ trợ ngăn xếp ô tô HAR.

Các thành phần chính của giai đoạn kết xuất là:

  • ImpellerRenderer: Chuyển đổi danh sách hiển thị từ giai đoạn kết xuất trước thành các lệnh kết xuất Impeller.

  • Impeller Rust API: Gói thư viện Impeller để sử dụng trong Rust (các thùng impellerimpeller-rs-bindgen).

  • TypographyContext: Quản lý việc đăng ký phông chữ và định hình văn bản.

impeller-video

3.1 Khởi chạy và quản lý bề mặt

  • Tạo ngữ cảnh: Trình kết xuất khởi chạy một thực thể của impeller::Context với phần phụ trợ OpenGL ES, chuyển một lệnh gọi lại để phân giải các con trỏ hàm OpenGL ES từ ngữ cảnh GL của nền tảng.

  • Bề mặt FBO được gói: Thay vì tạo cửa sổ riêng, Impeller sẽ kết xuất vào một đối tượng khung đệm OpenGL (FBO) hiện có do Giai đoạn 4 cung cấp. Điều này được thực hiện bằng cách gọi Surface::create_wrapped_fbo.

3.2 Quản lý tài nguyên

  • Hình ảnh: Hỗ trợ các định dạng tiêu chuẩn và hoạ tiết nén KTX2. Các định dạng này được tải lên hoạ tiết GPU và được quản lý bởi cấu trúc Resources nội bộ.

  • Phông chữ: Phông chữ TrueType và OpenType được tải và đăng ký bằng TypographyContext để kết xuất văn bản.

  • Hình ảnh bên ngoài: Việc xử lý chuyên biệt cho các hoạ tiết bên ngoài (ví dụ: nguồn cấp dữ liệu camera và trình kết xuất 3D bên ngoài) liên quan đến việc liên kết các thực thể EGLImage hoặc hoạ tiết OpenGL bên ngoài với các đối tượng Texture của Impeller để kết xuất không sao chép.

3.3 Kết xuất

Vòng lặp render tạo một thực thể DisplayList của Impeller (không nhầm lẫn với Vec<DisplayListEntry> do giai đoạn kết xuất trước tạo) bằng DisplayListBuilder:

  1. Xoá bộ đệm và áp dụng các biến đổi chung cho việc điều chỉnh tỷ lệ DPI và xoay màn hình.

  2. Lặp lại các mục DisplayListEntry đầu vào:

    • Trạng thái: save()restore() được dùng để đẩy và bật các biến đổi và vùng cắt.
    • Nguyên thuỷ: RectRoundedRect được vẽ bằng các thao tác vẽ tiêu chuẩn.
    • Đường dẫn: Các đường dẫn véc tơ phức tạp (bao gồm cả các thực thể Arc động) được xây dựng và vẽ.
    • Văn bản: TextStyledText được kết xuất bằng TypographyContext.
    • Hình ảnh: Hình ảnh tiêu chuẩn và hình ảnh bên ngoài được vẽ bằng draw_texture_rect.
  3. Gửi danh sách hiển thị Impeller đã tạo đến bề mặt bằng surface.draw_display_list(), tạo các lệnh GL cơ bản.

  4. Gọi swap_buffers() trên ngữ cảnh cơ bản để kích hoạt Giai đoạn 4.

Giai đoạn 4: Trình bày

Giai đoạn cuối cùng này xử lý tương tác với phần cứng hiển thị để hiển thị khung đã kết xuất. HAR sử dụng đường dẫn kết xuất trực tiếp mạnh mẽ trên Xe được xác định bằng phần mềm (SDV) của Hệ điều hành ô tô Android (AAOS).

Thành phần chính của giai đoạn này là HarDirectRenderingContext (trong thùng har-gl-context).

4.1 Kiến trúc

Lớp trình bày sử dụng phương pháp bộ đệm kép với mục tiêu vẽ ngoài màn hình:

  1. Bộ đệm vẽ: FBO ngoài màn hình nơi Impeller kết xuất cảnh.

  2. Bộ đệm phân giải (không bắt buộc): Bộ đệm phụ trợ không bắt buộc để hỗ trợ tính năng khử răng cưa nhiều mẫu (MSAA)

    • Bạn có thể bật tính năng này khi cần bằng cách triển khai hoặc định cấu hình OpenGL ES cơ bản. Trong những trường hợp như vậy, tính năng này đóng vai trò là mục tiêu trung gian để phân giải bộ đệm vẽ nhiều mẫu trước khi blitting (chuyển khối bit) sang bộ đệm kết xuất.
  3. Bộ đệm kết xuất: Bộ đệm chung được hỗ trợ bởi đối tượng GBM, tương ứng với bộ đệm sau trong chuỗi hoán đổi đồ hoạ thông thường.

  4. Bộ đệm trước: Bộ đệm GBM được quét ra màn hình.

4.2 Chuỗi hoán đổi

Khi swap_buffers được gọi, HAR sẽ làm theo các bước sau:

  1. Blit nội dung của bộ đệm vẽ vào bộ đệm kết xuất (với một blit trung gian vào bộ đệm phân giải, nếu cần theo cách triển khai).

  2. Gọi glFlush() trên ngữ cảnh GL và tạo một thực thể của EGL_SYNC_NATIVE_FENCE_ANDROID để theo dõi quá trình hoàn tất của GPU.

  3. Tạo yêu cầu nguyên tử DRM để hoán đổi bộ đệm kết xuất sang màn hình. Yêu cầu này chứa FD hàng rào GPU (được gọi là hàng rào trong) để ngăn bộ điều khiển màn hình hiển thị bộ đệm kết xuất trước khi GPU vẽ xong.

  4. Đồng thời yêu cầu một hàng rào mới từ DRM (được gọi là hàng rào ngoài) để báo hiệu thời điểm bộ đệm trước đó (bộ đệm trước cho khung trước) không còn trên màn hình.

  5. Cam kết yêu cầu nguyên tử bằng cách sử dụng cờ không chặn để cho phép luồng chính tiếp tục trong khi các hệ thống con đồ hoạ vẫn được đồng bộ hoá.

  6. Lưu trữ hàng rào ngoài mới trong ngữ cảnh để HAR có thể đợi hàng rào này được báo hiệu khi bắt đầu quy trình swap_buffers trên khung tiếp theo. Điều này ngăn GPU vẽ vào bộ đệm vẫn đang được hiển thị.

4.3 Cài đặt chế độ trực tiếp

HAR tương tác trực tiếp với kernel bằng cách sử dụng các hệ thống con DRM và Cài đặt chế độ kernel (KMS) để định cấu hình độ phân giải màn hình AAOS SDV, bỏ qua các tương tác với trình quản lý cửa sổ như SurfaceFlinger (trong các cấu hình cụ thể), cho phép kiểm soát độc quyền và có mức độ ưu tiên cao đối với phần cứng hiển thị.

4.4 Kết xuất bên ngoài

HAR hỗ trợ uỷ quyền kết xuất các phần tử giao diện người dùng cụ thể (được xác định bằng thẻ trong Figma) cho các quy trình hoặc luồng bên ngoài. Điều này hữu ích cho việc tích hợp các cảnh 3D phức tạp (ví dụ: hình ảnh trực quan về xe tự lái từ các công cụ như Kanzi hoặc Unity) hoặc nội dung khác yêu cầu ngữ cảnh OpenGL chuyên dụng.

4.4.1 Những hành động chính

  • HarExternalRenderContext: Ngữ cảnh EGL ngoài màn hình chuyên dụng cho dịch vụ bên ngoài.
  • SurfacePool: Quản lý một tập hợp các bộ đệm LocalSurface (Texture cộng với EGLImage) để tạo bộ đệm kép hoặc bộ đệm ba.
  • SharedSurfaceExternalImage: Trình bao bọc an toàn cho luồng để chuyển các trình xử lý EGLImage giữa dịch vụ bên ngoài và trình kết xuất chính.

4.4.2 Quy trình làm việc

Quy trình làm việc tuân theo trình tự sau:

  1. Dịch vụ bên ngoài bắt đầu và tự đăng ký với trình lặp chính, xác định các thẻ Figma (ví dụ: #cluster/3d-car) mà dịch vụ này kết xuất.

  2. Dịch vụ này đợi các tín hiệu RenderStart từ trình lặp để căn chỉnh quá trình kết xuất với tín hiệu VSYNC của màn hình.

  3. Ngoài màn hình, dịch vụ này kết xuất nội dung vào một khung đệm do SurfacePool cung cấp.

  4. Dịch vụ này gọi swap_buffers trên ngữ cảnh của dịch vụ, xoay vùng chứa và cung cấp khung hoàn chỉnh dưới dạng một thực thể của SharedSurface.

  5. SharedSurface được gói trong ExternalImage và gửi qua kênh Rust MPSC đến vòng lặp.

  6. Trình kết xuất Impeller chính (Giai đoạn 3) nhận hình ảnh từ bên ngoài. Thay vì sao chép dữ liệu pixel, trình kết xuất này sẽ liên kết trực tiếp EGLImage cơ bản với một hoạ tiết và vẽ hoạ tiết đó như một phần của cảnh chính, đạt được thành phần không sao chép.

4.5 Nền tảng phát triển và kiểm thử (har-platform-linux)

Với mục đích phát triển và kiểm thử, các ứng dụng HAR có thể nhắm đến các môi trường máy tính tiêu chuẩn của Linux và các thiết lập không có màn hình. Các nền tảng này được triển khai trong thùng crates/reference/platforms/har-platform-linux.

Không giống như mục tiêu SDV AAOS chính thức, các nền tảng này không sử dụng hệ thống con direct-rendering của har-gl-context cho đầu ra hiển thị. Thay vào đó, chúng dựa vào các thùng Rust OpenGL tiêu chuẩn:

  • Chế độ cửa sổ: Sử dụng winit để quản lý cửa sổ và vòng lặp sự kiện, đồng thời sử dụng glutin để tạo ngữ cảnh OpenGL ES và tích hợp với hệ thống cửa sổ.

  • Chế độ không có màn hình: Sử dụng thùng har-gl-context để tạo ngữ cảnh pbuffer ngoài màn hình với màn hình EGL mặc định. Điều này cho phép kết xuất vào bộ đệm ngoài màn hình mà không cần cửa sổ hiển thị hoặc quyền truy cập trực tiếp vào phần cứng hiển thị, chủ yếu được dùng để kiểm thử tự động hoặc xử lý phụ trợ.