HAR 그래픽 파이프라인

이 페이지에서는 고가용성 렌더러 (HAR)의 전체 그래픽 파이프라인을 자세히 설명하여 Figma 디자인 문서에서 화면에 표시되는 최종 픽셀까지 데이터 흐름을 추적합니다.

개요

파이프라인은 상위 수준 UI 정의를 하위 수준 그래픽 명령어로 변환하고 하드웨어 디스플레이에 효율적으로 표시합니다. 이 파이프라인은 자동차 안전 필수 앱을 위해 설계되었으며 결정적 렌더링, 효율적인 상태 관리, 플랫폼 그래픽 하위 시스템(예: 직접 렌더링 관리자(DRM), 일반 버퍼 관리(GBM))과의 강력한 상호작용을 강조합니다.

파이프라인은 네 가지 주요 단계로 나눌 수 있습니다.

  1. 사전 렌더링: 장면 그래프를 처리하고, 맞춤설정을 적용하고, 레이아웃을 해결합니다.
  2. 명령어 생성: 해결된 장면 그래프를 백엔드에 구애받지 않는 표시 목록으로 변환합니다.
  3. 렌더링: Impeller 그래픽 엔진을 사용하여 그리기 명령어를 실행합니다.
  4. 프레젠테이션: 프레임 버퍼 관리 및 디스플레이 하드웨어와의 동기화

HAR 그래픽 흐름

그림 1. HAR 그래픽 흐름

1단계: 사전 렌더링

이 단계에서는 정적 Figma 디자인과 동적 앱 상태를 렌더링 준비가 완료된 완전한 인메모리 UI 트리로 변환합니다. 이 단계는 기본 디스플레이 루프와 별도의 전용 리듀서 스레드에서 실행됩니다.

1.1 DesignCompose 기초

HAR 파이프라인은 DesignCompose 생태계를 기반으로 빌드됩니다.

  • 소스: UI는 Figma에서 디자인되고 DesignCompose 플러그인을 사용하여 내보내집니다.
  • 정의: 출력은 디자인 (노드, 스타일, 변형)의 직렬화된 표현인 DesignComposeDefinition의 인스턴스입니다.
  • 데이터 바인딩: 앱의 UI 모델은 절차적 매크로 (예: #[Design(node = "#speed")])를 사용하여 Rust 구조체 필드를 Figma 문서의 특정 명명된 노드에 명시적으로 바인딩합니다. 이렇게 하면 앱 상태가 시각적 요소의 속성을 자동으로 제어할 수 있습니다.

이 기반의 주요 구성요소는 다음과 같습니다.

  • 리듀서: 중앙 이벤트 루프 역할을 하며 작업을 처리하고 현재 상태를 업데이트합니다. 프레임워크는 DefaultReducer를 제공하지만 필요한 경우 맞춤 리듀서 구현을 제공할 수 있습니다.
  • 프레젠터: 현재 상태를 UI 모델에 연결합니다. Presenter 트레이트는 harry 프레임워크 크레이트로 지정되며 참조 구현 (UIModelPresenter)은 harry-app-core 크레이트에서 제공됩니다.
  • UI 모델: 현재 상태를 기반으로 맞춤설정을 생성합니다. UI 모델 코드는 derive_customizations 크레이트에서 제공하는 DesignDocument 매크로를 사용하여 생성됩니다. harry-app-core 크레이트의 UIModel 구조체는 이 예시를 제공합니다.
  • Squoosh: 디자인에 따라 UI를 렌더링하는 데 사용되는 SquooshView 데이터 구조와 변형 저장소를 제공합니다. 직렬화된 설계 문서는 DesignCompose 라이브러리에서 dc_bundle 크레이트에 의해 로드되고 효율적인 런타임 성능을 위해 SquooshView 구조체 트리로 변환됩니다.

1.2 리듀서 루프

파이프라인은 작업에 의해 실행됩니다. 프레임워크는 프레임워크 자체에서 사용하는 내부 작업을 정의하는 Actions 열거형 유형을 지정하지만 사용자가 추가 앱별 작업 (예: UpdateVehicleSpeed 또는 ButtonPress)을 정의할 수 있는 CustomAction 변형도 포함합니다.

프레임워크는 앱 상태에 영향을 미치는 작업의 구현을 간소화하고 선택적으로 처리하기 위해 리듀서에서 앱으로 다시 전달되는 부작용을 생성하는 StateAction 특성도 제공합니다. harry-app-core 크레이트의 CustomActions enum은 이에 관한 자세한 예를 제공합니다.

리듀서 루프의 기본 개요는 다음과 같습니다.

  • 작업 처리: Reducer가 작업을 수신하고 현재 상태를 업데이트합니다. 현재 속도 또는 활성 상태인 경고등과 같은 원시 데이터입니다. 이로 인해 부작용이 발생할 수도 있습니다(예: 안전벨트 표시등이 깜박일 때 신호에서 차임벨이 재생됨).
  • 프레젠테이션: Presenter는 새 상태를 UIModel에 매핑합니다. UIModel는 UI용으로 특별히 형식이 지정된 데이터를 보유하는 뷰 모델입니다(예: '120' 속도를 '65mph' 문자열로 형식 지정).
  • 맞춤설정 생성: UI 모델의 apply 메서드가 호출되어 RenderCustomization 인스턴스 집합을 생성합니다. Figma 디자인을 수정하기 위한 명시적 지침입니다 (예: '노드 #speed의 텍스트를 '65mph'로 설정').
  • 최적화를 위한 UpdatePolicy: 각 사전 렌더링 패스 후 다음 렌더링 업데이트가 필요한 시기를 나타내는 UpdatePolicy 값이 반환됩니다. 대기 중인 상태 변경사항이 없고 실행 중인 애니메이션이 없으면 UpdatePolicy는 추가 업데이트가 즉시 필요하지 않음을 나타냅니다. 이러한 경우 리듀서는 새 디스플레이 목록 생성을 중단하여 새 작업이나 이벤트가 변경을 트리거할 때까지 불필요한 렌더링 주기를 방지하고 리소스를 절약합니다.

1.3 수집 및 저장소 초기화 보기

파이프라인은 DesignComposeDefinition 인스턴스로 시작합니다. DesignCompose에 의해 프로토콜 버퍼 구조로 직렬화된 Figma 디자인 문서입니다.

  • 초기 로드: 시작 시 루트 노드로 지정된 기본 디자인이 DesignComposeDefinition에서 초기 SquooshView 트리로 변환됩니다. 이는 일회성 프로세스입니다.

  • 저장소: SquooshVariantRepository는 재사용 가능한 구성요소 변형과 초기 로드된 뷰를 관리합니다.

  • 지연 로드: 시작 시간과 메모리 사용량을 최소화하기 위해 추가 뷰(초기 루트 노드 트리의 일부가 아닌 뷰)는 렌더링 로직에서 명시적으로 참조되고 필요할 때만 문서에서 지연 로드됩니다 (예: 목록 맞춤설정 중).

1.4 맞춤설정 패스

SquooshView 트리는 동적 앱 상태를 적용하기 위해 순회됩니다.

  • 변형 교체: 런타임 로직에 따라 구성요소 인스턴스가 특정 변형으로 교체됩니다(예: 현재 드라이브 모드를 나타내는 아이콘을 스포츠에서 에코로 변경).

  • 목록 확장: Figma의 단일 템플릿 항목이 동적 하위 요소 목록으로 대체됩니다. 애니메이션의 안정적인 ID를 확인하기 위해 이러한 하위 요소에 대해 새로운 고유 ID가 생성됩니다.

  • 텍스트 및 스타일 재정의: 텍스트 콘텐츠 (예: 속도 값)와 스타일 (예: 불투명도, 색상)이 현재 상태에서 업데이트됩니다.

1.5 변수 확인

Figma 또는 앱의 로컬에 정의된 디자인 토큰과 변수가 확인됩니다.

  • 바인딩: 변수 (예: 색상 또는 크기)를 참조하는 SquooshView 속성은 현재 프레임의 구체적인 값으로 대체됩니다.

1.6 레이아웃 계산

  • 동적 레이아웃: DynamicLayoutSquooshView 트리에 있는 모든 노드의 최종 위치와 크기(경계)를 계산합니다.

  • 텍스트 레이아웃: TextHelperLayoutHelper 특성의 구현을 사용하여 텍스트 측정항목, 줄 바꿈, 모양을 계산합니다. 이렇게 하면 렌더링 전에 텍스트가 제약 조건 내에서 올바르게 흐르는지 확인할 수 있습니다.

1.7 다이얼 및 게이지

자동차 UI를 위한 특수 단계입니다.

  • MeterData: 노드에 미터 데이터 (Figma에 정의됨)가 있는 경우 기하 도형이 meter_value (예: 차량 속도)에 따라 동적으로 변경됩니다.
    • 호: 스윕 각도가 조정됩니다.
    • 회전: 회전 변환은 시작 각도와 종료 각도를 기준으로 계산됩니다.
    • 진행률 표시줄: 직사각형의 너비 또는 높이가 조정됩니다.
    • 진행 벡터: 벡터 경로의 길이가 조정됩니다.

1.8 애니메이션

  • 차이 비교: 현재 SquooshViewPreRenderCacheprevious_squoosh_view과 비교됩니다.

  • 보간: 속성이 변경된 경우 Squoosh는 시간에 따라 값을 부드럽게 전환하기 위해 인터폴레이터를 만듭니다 (예: 불투명도 또는 변환).

2단계: 명령어 생성

SquooshView 트리가 완전히 해결되고 애니메이션 처리된 후에는 그리기 명령어의 선형 시퀀스로 변환됩니다.

이 단계의 핵심 구성요소는 DisplayList 크레이트입니다.

  • generate_dl: 이 함수는 SquooshView 트리를 재귀적으로 순회합니다.

  • 번역:

    • 모양 및 경로: 적절한 DisplayListAppearance 변형 (예: Rect 또는 Path)을 사용하여 DisplayListEntry로 변환됩니다.
    • 텍스트: TextHelper를 사용하여 텍스트 그리기 항목으로 변환됩니다.
    • 변환 및 클립: 그리기 상태 스택을 관리하기 위해 PushTransform3DPopTransform3D 또는 PushClipRegionPopClipRegion 쌍으로 변환되었습니다.
    • 마스크: 레이어를 올바르게 만들고 혼합하기 위해 PushMaskLayerPopMaskLayer 쌍으로 변환했습니다.

최종 결과는 그리는 방법과 관계없이 그릴 내용을 설명하는 Vec<DisplayListEntry>의 인스턴스입니다.

2.1 루퍼로 핸드오프

DisplayList가 생성되면 리듀서가 ViewDescriptor 인스턴스로 래핑하고 Rust MPSC 채널 (LooperMessage)을 통해 루퍼 스레드로 전송합니다. Looper는 렌더링 및 표시 단계를 담당하므로 리듀서 스레드가 그래픽 파이프라인을 차단하지 않습니다.

3단계: 렌더링

플랫폼에 구애받지 않는 DisplayList는 렌더링 백엔드에 전달되며, 여기서 추상 명령어가 GPU 명령어로 변환됩니다.

HAR은 원래 Flutter용으로 빌드된 렌더링 엔진인 Impeller를 사용합니다. Impeller는 빌드 시에 작고 효율적인 셰이더 세트를 사전 컴파일하여 셰이더 컴파일로 인한 프레임 속도 글리치 문제를 해결하도록 설계되었습니다. 이 접근 방식은 효과적인 일괄 처리 및 고도로 최적화된 백엔드와 결합되어 다음을 제공합니다.

  • 결정적 성능: 런타임 셰이더 컴파일 결함을 사실상 제거합니다.
  • 빠른 시작: 초기화 오버헤드를 줄입니다.
  • 작은 설치 공간: 간결한 바이너리 크기를 생성합니다.

Impeller 아키텍처에 대한 자세한 소개는 Introducing Impeller - Flutter's new rendering engine(Impeller 소개 - Flutter의 새로운 렌더링 엔진)을 참고하세요. 동영상에서는 Flutter에 대해 설명하지만 이러한 핵심 이점은 HAR 자동차 스택을 직접 지원합니다.

렌더링 단계의 주요 구성요소는 다음과 같습니다.

  • ImpellerRenderer: 사전 렌더링 단계의 표시 목록을 임펠러 렌더링 명령어로 변환합니다.

  • Impeller Rust API: Rust에서 사용할 수 있도록 Impeller 라이브러리를 래핑합니다(impellerimpeller-rs-bindgen 크레이트).

  • TypographyContext: 글꼴 등록 및 텍스트 모양을 관리합니다.

3.1 초기화 및 화면 관리

  • 컨텍스트 생성: 렌더러는 OpenGL ES 백엔드로 impeller::Context 인스턴스를 초기화하여 플랫폼의 GL 컨텍스트에서 OpenGL ES 함수 포인터를 확인하는 콜백을 전달합니다.

  • 래핑된 FBO 표면: Impeller는 자체 창을 만드는 대신 4단계에서 제공하는 기존 OpenGL 프레임버퍼 객체 (FBO)로 렌더링합니다. Surface::create_wrapped_fbo을 호출하면 됩니다.

3.2 리소스 관리

  • 이미지: 표준 형식과 KTX2 압축 텍스처를 지원합니다. 이러한 값은 GPU 텍스처에 업로드되고 내부 Resources 구조체로 관리됩니다.

  • 글꼴: TrueType 및 OpenType 글꼴이 로드되고 텍스트 렌더링을 위해 TypographyContext에 등록됩니다.

  • 외부 이미지: 외부 텍스처 (예: 카메라 피드 및 외부 3D 렌더러)의 특수 처리는 제로 카피 렌더링을 위해 EGLImage 인스턴스 또는 외부 OpenGL 텍스처를 임펠러 Texture 객체에 바인딩하는 것을 포함합니다.

3.3 렌더링 패스

render 루프는 DisplayListBuilder를 사용하여 Impeller DisplayList 인스턴스를 생성합니다 (사전 렌더링 단계에서 생성된 Vec<DisplayListEntry>와 혼동하지 마세요).

  1. 버퍼를 지우고 DPI 조정 및 디스플레이 회전을 위한 전역 변환을 적용합니다.

  2. 입력 DisplayListEntry 항목을 반복합니다.

    • 상태: save()restore()는 변환과 클립 영역을 푸시하고 팝하는 데 사용됩니다.
    • 기본 요소: RectRoundedRect는 표준 페인트 작업을 사용하여 그려집니다.
    • 경로: 복잡한 벡터 경로 (동적 Arc 인스턴스 포함)가 빌드되고 그려집니다.
    • 텍스트: TextStyledTextTypographyContext를 사용하여 렌더링됩니다.
    • 이미지: 표준 및 외부 이미지는 draw_texture_rect를 사용하여 그려집니다.
  3. surface.draw_display_list()를 사용하여 빌드된 Impeller 디스플레이 목록을 서페이스에 제출하여 기본 GL 명령어를 생성합니다.

  4. 기본 컨텍스트에서 swap_buffers()를 호출하여 4단계를 트리거합니다.

4단계: 프레젠테이션

이 최종 단계에서는 렌더링된 프레임을 표시하기 위해 디스플레이 하드웨어와의 상호작용을 처리합니다. HAR은 Android Automotive OS (AAOS) 소프트웨어 정의 차량 (SDV)에서 강력한 직접 렌더링 경로를 사용합니다.

이 단계의 핵심 구성요소는 HarDirectRenderingContext (har-gl-context 크레이트)입니다.

4.1 아키텍처

프레젠테이션 레이어는 오프스크린 그리기 타겟을 사용하여 이중 버퍼링 접근 방식을 사용합니다.

  1. 그리기 버퍼: Impeller가 장면을 렌더링하는 오프스크린 FBO입니다.

  2. 해결 버퍼 (선택사항): 멀티샘플 앤티앨리어싱 (MSAA)을 지원하는 선택적 보조 버퍼

    • 이는 기본 OpenGL ES 구현이나 구성에 의해 필요한 경우 사용 설정할 수 있습니다. 이 경우 렌더링 버퍼로 블리팅 (비트 블록 전송)하기 전에 멀티샘플링된 그리기 버퍼를 해결하는 중간 타겟 역할을 합니다.
  3. 렌더링 버퍼: 일반적인 그래픽 스왑 체인의 백 버퍼에 해당하는 GBM 객체로 지원되는 일반 버퍼입니다.

  4. 전면 버퍼: 디스플레이로 스캔 아웃되는 GBM 버퍼입니다.

4.2 스왑 체인

swap_buffers가 호출되면 HAR은 다음 단계를 따릅니다.

  1. 그리기 버퍼의 콘텐츠를 렌더링 버퍼로 블리트합니다 (구현에서 필요한 경우 해결 버퍼로 중간 블리트 사용).

  2. GL 컨텍스트에서 glFlush()를 호출하고 GPU 완료를 추적하는 EGL_SYNC_NATIVE_FENCE_ANDROID 인스턴스를 만듭니다.

  3. 렌더링 버퍼를 화면으로 스왑하는 DRM 원자 요청을 빌드합니다. 이 요청에는 GPU가 그리기를 완료하기 전에 디스플레이 컨트롤러가 렌더링 버퍼를 표시하지 못하도록 하는 GPU 펜스 FD (in fence라고 함)가 포함되어 있습니다.

  4. 이전 버퍼 (이전 프레임의 전면 버퍼)가 더 이상 화면에 표시되지 않는 시점을 알리기 위해 DRM에서 새 펜스 (아웃 펜스라고 함)를 동시에 요청합니다.

  5. 그래픽 하위 시스템이 동기화된 상태로 유지되는 동안 기본 스레드가 계속될 수 있도록 차단되지 않는 플래그를 사용하여 원자 요청을 커밋합니다.

  6. HAR이 후속 프레임에서 swap_buffers 프로세스가 시작될 때 신호가 전송되기를 기다릴 수 있도록 컨텍스트에 새 out fence를 저장합니다. 이렇게 하면 GPU가 아직 표시되고 있는 버퍼에 그리는 것을 방지할 수 있습니다.

4.3 직접 모드 설정

HAR은 DRM 및 커널 모드 설정(KMS) 하위 시스템을 사용하여 커널과 직접 상호작용하여 디스플레이 해상도 AAOS SDV를 구성하고 SurfaceFlinger와 같은 창 관리자와의 상호작용을 우회하여 (특정 구성에서) 디스플레이 하드웨어를 독점적이고 우선순위가 높은 방식으로 제어할 수 있습니다.

4.4 외부 렌더링

HAR은 특정 UI 요소 (Figma의 태그로 식별됨)의 렌더링을 외부 프로세스 또는 스레드에 위임하는 것을 지원합니다. 이는 복잡한 3D 장면 (예: 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. SharedSurfaceExternalImage로 래핑되고 Rust MPSC 채널을 통해 루퍼로 전송됩니다.

  6. 기본 Impeller 렌더러 (3단계)가 외부 이미지를 수신합니다. 픽셀 데이터를 복사하는 대신 기본 EGLImage를 텍스처에 직접 바인딩하고 기본 장면의 일부로 그려 제로 카피 컴포지션을 달성합니다.

4.5 개발 및 테스트 플랫폼 (har-platform-linux)

개발 및 테스트 목적으로 HAR 앱은 표준 Linux 데스크톱 환경과 헤드리스 설정을 타겟팅할 수 있습니다. 이러한 플랫폼은 crates/reference/platforms/har-platform-linux 크레이트에서 구현됩니다.

프로덕션 AAOS SDV 타겟과 달리 이러한 플랫폼은 디스플레이 출력에 har-gl-contextdirect-rendering 하위 시스템을 사용하지 않습니다. 대신 표준 Rust OpenGL 크레이트를 사용합니다.

  • 창 모드: 창 관리 및 이벤트 루프에는 winit를 사용하고 OpenGL ES 컨텍스트 생성 및 창 지정 시스템과의 통합에는 glutin를 사용합니다.

  • 헤드리스 모드: har-gl-context 크레이트를 사용하여 기본 EGL 디스플레이로 오프스크린 pbuffer 컨텍스트를 만듭니다. 이를 통해 표시되는 창이나 직접 디스플레이 하드웨어 액세스 없이 오프스크린 버퍼에 렌더링할 수 있으며, 주로 자동화된 테스트나 백엔드 처리에 사용됩니다.