Quy trình đồ hoạ HAR

Trang này trình bày chi tiết toàn bộ quy trình đồ hoạ của trình kết xuất có tính sẵn sà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 hiệu quả các lệnh đó 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 khả năng kết xuất có tính xác định, khả năng quản lý trạng thái hiệu quả và khả năng tương tác mạnh mẽ với các hệ thống con đồ hoạ 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 chế độ 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ị độc lập với 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ý vùng đệm khung 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ế tĩnh của Figma và trạng thái ứng dụng động thành một cây giao diện người dùng hoàn toàn được phân giải trong bộ nhớ, sẵn sàng để kết xuất. Giai đoạn này chạy trên một luồng giảm 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: Đầu ra là một thực thể của DesignComposeDefinition, một bản trình bày được chuyển đổi tuần tự 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 phần tử trực quan.

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

  • Reducer: Đóng vai trò là vòng lặp sự kiện trung tâm, xử lý các thao tác 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 trình giảm tuỳ chỉnh nếu cần.
  • Presenter: 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ằng 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 chế độ 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 cách sử dụ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 chuyển đổi tuần tự sẽ được vùng chứa dc_bundle tải từ thư viện DesignCompose và chuyển đổi thành một cây gồm các cấu trúc SquooshView để có hiệu suất thời gian chạy hiệu quả.

1.2 Vòng lặp giảm tốc

Quy trình này được điều khiển bằng các thao tác. Khung này chỉ định kiểu liệt kê Actions xác định các thao tác 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 thao tác 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 thao tác ảnh hưởng đến trạng thái ứng dụng và tuỳ ý tạo ra các hiệu ứng phụ, sau đó được truyền trở lại ứng dụng từ bộ giảm để 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à dàn ý cơ bản của vòng lặp giảm:

  • 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 đèn báo (đèn cảnh báo) nào đang bật. Điều này cũng có thể tạo ra các hiệu ứng phụ (ví dụ: tín hiệu phát ra tiếng chuông khi đèn báo 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 khung hiển thị, lưu giữ dữ liệu được định dạng riêng 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 chế độ 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à những 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 mph"").
  • UpdatePolicy để tối ưu hoá: Sau mỗi lần truyền trước, một giá trị UpdatePolicy sẽ được trả về, cho biết thời điểm cần có bả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ó ảnh động nào đang chạy, thì 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, Reducer 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 thao tác hoặc sự kiện mới kích hoạt thay đổi.

1.3 Xem quá trình khởi tạo kho lưu trữ và nhập

Quy trình 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 một cấu trúc vùng đệm giao thức.

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

  • Kho lưu trữ: SquooshVariantRepository quản lý các biến thể của thành phần có thể dùng lại và các khung hiển thị đượ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 khung hiển thị bổ sung (những khung hiển thị 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 Thẻ tuỳ chỉnh

Cây SquooshView được duyệt qua để á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ừ chế độ thể thao sang chế độ 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 một danh sách động gồm các phần tử con. Các mã nhận dạng duy nhất mới được tạo cho những thành phần con này để xác minh danh tính ổn định cho các ảnh động.

  • Văn bản và kiểu ghi đè: 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 (chẳng hạn như màu sắc hoặc kích thước) sẽ được thay thế bằng các giá trị cụ thể cho khung hình 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 một cách triển khai đặc điểm LayoutHelper để tính toán các chỉ số, cách xuống dòng và hình dạng của văn bản. Điều này giúp xác minh rằng văn bản hiển thị chính xác trong các ràng buộc của văn bản trước khi kết xuất.

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

Đây là một bước chuyên biệt dành 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).
    • 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 một hình chữ nhật được điều chỉnh tỷ lệ.
    • Vectơ tiến trình: Độ dài của đường dẫn vectơ được điều chỉnh.

1.8 Ảnh động

  • 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 hiệu ứng động 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 đệ quy duyệt qua cây SquooshView.

  • Bản dịch:

    • Hình dạng và đường dẫn: Được chuyển đổi thành DisplayListEntry bằng 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 nhập vẽ văn bản.
    • Biến đổi và đoạn 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ả what (cần vẽ), độc lập với how (cách vẽ).

2.1 Chuyển sang hiệu ứng lặp âm

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

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

DisplayList độc lập với 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 của 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 nhóm nhỏ chương trình đổ bóng hiệu quả tại thời điểm tạo bản dựng. Phương pháp này, kết hợp với tính năng phân lô hiệu quả và một phần phụ trợ được tối ưu hoá cao, mang lại:

  • Hiệu suất xác định: Gần như loại bỏ các trục trặc khi 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 động.
  • Kích thước nhỏ: Tạo ra kích thước nhị phân nhỏ gọn.

Để biết thông tin giới thiệu đầy đủ về cấu trúc của Impeller, hãy xem video Giới thiệu Impeller – Công cụ kết xuất mới của Flutter. 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 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.

  • API Rust của Impeller: Bao bọc thư viện Impeller để sử dụng trong Rust (các crate impellerimpeller-rs-bindgen).

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

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

  • Tạo ngữ cảnh: Trình kết xuất khởi tạo một phiên bản impeller::Context bằng một phần phụ trợ OpenGL ES, truyền một lệnh gọi lại để phân giải con trỏ hàm OpenGL ES từ ngữ cảnh GL của nền tảng.

  • Bề mặt FBO được bao bọc: Thay vì tạo cửa sổ riêng, Impeller sẽ kết xuất vào một đối tượng framebuffer OpenGL (FBO) hiện có do Giai đoạn 4 cung cấp. Bạn có thể thực hiện việc này 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 chuẩn và kết cấu nén KTX2. Các đối tượng này được tải lên hoạ tiết GPU và do một cấu trúc Resources nội bộ quản lý.

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

  • Hình ảnh bên ngoài: Việc xử lý chuyên biệt cho các kết cấu 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 các kết cấu 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 Lượt kết xuất

Vòng lặp render tạo một thực thể DisplayList 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 cách sử dụng DisplayListBuilder:

  1. Xoá vùng đệm và áp dụng các phép biến đổi chung để đ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 cũng như vùng cắt.
    • Nguyên hàm: RectRoundedRect được vẽ bằng các thao tác vẽ chuẩn.
    • Đường dẫn: Các đường dẫn vectơ phức tạp (bao gồm cả các thực thể Arc động) được tạo và vẽ.
    • Văn bản: TextStyledText được kết xuất bằng TypographyContext.
    • Hình ảnh: Hình ảnh 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 nền tảng bằng cách sử dụ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ý hoạt động tương tác với phần cứng hiển thị để trình chiếu khung hình đã kết xuất. HAR sử dụng một đườ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) Android Automotive OS (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 đệm đôi với mục tiêu vẽ ngoài màn hình:

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

  2. Giải quyết vùng đệm (không bắt buộc): Vùng đệm phụ không bắt buộc để hỗ trợ chế độ khử răng cưa nhiều mẫu (MSAA)

    • Bạn có thể bật chế độ 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, nó đóng vai trò là mục tiêu trung gian để phân giải vùng đệm vẽ được lấy mẫu nhiều lần trước khi blitting (truyền khối bit) đến vùng đệm kết xuất.
  3. Vùng đệm kết xuất: Vùng đệm chung được hỗ trợ bởi một đối tượng GBM, tương ứng với vùng đệm sau trong chuỗi hoán đổi đồ hoạ thông thường.

  4. Vùng đệm trước: Vùng đệ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 vùng đệm vẽ vào vùng đệm kết xuất (với một blit trung gian vào vùng đệm phân giải, nếu cần theo cách triển khai).

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

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

  4. Đồng thời yêu cầu một hàng rào mới từ DRM (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 hình trước) không còn trên màn hình.

  5. Xác nhận yêu cầu không thể phân chia bằ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 hàng rào mới trong bối 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 hình tiếp theo. Điều này ngăn GPU vẽ vào một vùng đệm vẫn đang được hiển thị.

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

HAR tương tác trực tiếp với nhân bằng cách sử dụng các hệ thống con DRM và Kernel Mode Setting (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 màn hình.

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

HAR hỗ trợ uỷ quyền việc 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 rất hữu ích khi tích hợp các cảnh 3D phức tạp (ví dụ: hình ảnh trực quan về ô tô từ các công cụ như Kanzi hoặc Unity) hoặc nội dung khác yêu cầu một ngữ cảnh OpenGL chuyên dụng.

4.4.1 Các thành phần chính

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

4.4.2 Quy trình công việc

Quy trình công việc diễn ra theo trình tự sau:

  1. Dịch vụ bên ngoài sẽ khởi động và tự đăng ký với trình vòng lặp chính, xác định thẻ Figma nào (ví dụ: #cluster/3d-car) mà dịch vụ đó hiển thị.

  2. Dịch vụ này đợi tín hiệu RenderStart từ trình lặp để căn chỉnh hoạt động 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ụ sẽ kết xuất nội dung vào một vùng đệm khung hình 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ụ, thao tác này sẽ xoay vùng chứa và cung cấp khung hình đã hoàn tất dưới dạng một thực thể của SharedSurface.

  5. SharedSurface được bao bọc trong ExternalImage và gửi qua kênh MPSC Rust đến looper.

  6. Trình kết xuất Impeller chính (Giai đoạn 3) nhận hình ảnh bên ngoài. Thay vì sao chép dữ liệu pixel, nó liên kết EGLImage cơ bản trực tiếp 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)

Đối với mục đích phát triển và kiểm thử, các ứng dụng HAR có thể nhắm đến môi trường máy tính Linux tiêu chuẩn và các chế độ thiết lập không cần 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 sản xuất, 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 OpenGL Rust 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 tạo cửa sổ.

  • Chế độ không đầu: Sử dụng thùng har-gl-context để tạo một bối cảnh pbuffer ngoài màn hình bằng màn hình EGL mặc định. Điều này cho phép kết xuất vào một vùng đệ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 cho kiểm thử tự động hoặc xử lý phụ trợ.