메인 루프
이 섹션에서는 TouchGFX 그래픽 엔진의 작동 방식, 특히 메인 루프에 대해 자세히 알아보겠습니다. 이전에도 언급했지만 그래픽 엔진의 주된 역할은 애플리케이션의 그래픽(UI 모델)을 프레임버퍼에서 렌더링하는 것입니다. 이러한 렌더링 프로세스가 반복되면서 디스플레이에 새로운 프레임이 생성됩니다.
그래픽 엔진은 디스플레이 터치나 버튼 누름과 같은 외부 이벤트를 수집합니다. 이렇게 수집된 이벤트는 필터링을 거쳐 애플리케이션으로 전송됩니다. 애플리케이션은 전송된 이벤트를 사용해 UI 모델을 업데이트합니다. 예를 들면 사용자가 버튼 위로 화면을 터치할 때 버튼이 누름 상태로 바뀌고, 나중에 사용자가 화면을 더는 터치하지 않을 때 다시 해제 상태로 바뀌는 것이 여기에 해당합니다.
마지막으로 그래픽 엔진이 업데이트된 모델을 프레임버퍼로 렌더링합니다. 이러한 프로세스가 계속 반복됩니다.
프레임 렌더링이 끝나고 프레임버퍼가 디스플레이로 전송되면 사용자가 그래픽을 볼 수 있습니다. 디스플레이의 그래픽이 깨지는 글리치(glitch) 현상을 방지하려면 디스플레이로의 전송이 디스플레이와 동기화되어야 합니다. 최소 주기에 따라 전송이 주기적으로 수행되어야 하는 디스플레이도 있고 디스플레이에서 신호를 보내면 반드시 전송이 일어나야 하는 디스플레이도 있습니다.
그래픽 엔진은 하드웨어 추상화 계층에서 “go” 신호를 보낼 때까지 기다려서 이러한 동기화를 구현합니다. 하드웨어 추상화 계층에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
TouchGFX 그래픽 엔진 내부의 메인 루프를 의사 코드로 보면 다음과 비슷한 모습입니다.
while(true) {
collect(); // Collect events from outside
update(); // Update the application ui model
render(); // Render new updated graphics to the framebuffer
wait(); // Wait for 'go' from display
}
실제 구현체에서는 코드가 더 많으며, 위의 의사 코드는 엔진의 주요 부품에 대한 이해를 돕기 위한 것일 뿐입니다.
이제 위의 4단계에 대해 더 자세히 설명하겠습니다.
수집
수집 단계에서는 그래픽 엔진이 외부 환경의 이벤트를 수집합니다. 주로 터치 이벤트와 버튼이 수집됩니다.
TouchGFX는 수집된 이벤트를 샘플링하여 애플리케이션으로 전송합니다. 원시 터치 이벤트는 다음과 같이 좀 더 구체적인 터치 이벤트로 변환됩니다.
- 클릭: 사용자가 디스플레이를 손가락으로 눌렀거나 떼었습니다.
- 드래그: 사용자가 디스플레이를 터치한 상태에서 손가락을 움직였습니다.
- 제스처: 사용자가 손가락을 한 방향으로 빠르게 움직였다가 떼었습니다. 이러한 제스처를 스와이프라고 하며, 그래픽 엔진에서 인식합니다.
위의 이벤트는 현재 활성화된 UI 요소(위젯 등)로 전송됩니다.
이때 엔진이 실행(tick) 이벤트도 전송합니다. 이 이벤트는 새로운 프레임(또는 시간 순서에 따른 단계)을 의미하며, 다른 외부 입력이 없다는 가정 하에 항상 전송됩니다. 전송된 이벤트는 애플리케이션에서 애니메이션, 또는 특정 시간 경과 후 일시 정지 화면으로 변경하는 것과 같은 시간 기반 액션을 실행하는 데 사용됩니다.
업데이트
업데이트 단계에서는 그래픽 엔진이 애플리케이션과 함께 수집된 이벤트를 반영하여 UI를 업데이트합니다. 그래픽 엔진은 현재 활성화된 스크린을 인식하여 이벤트를 해당 객체로 전달합니다.
기본적인 원리는 이렇습니다. 먼저 엔진이 애플리케이션(UI 모델의 스크린 또는 위젯 객체)에게 이벤트 정보를 알립니다. 그러면 애플리케이션이 디스플레이에서 특정 요소를 다시 그리기를 요청합니다. 이때 애플리케이션은 이벤트에 대한 응답으로 바로 그리기 작업을 하지 않고 위젯 속성을 변경하여 다시 그리기 작업을 하도록 요청합니다.
예를 들어 클릭 이벤트가 발생하면 그래픽 엔진이 스크린 객체의 장면 모델에서 이벤트를 수신할 위젯을 찾습니다. 이미지나 TextArea와 같은 위젯은 클릭 이벤트를 수신하려고 하지 않습니다. 이러한 위젯은 이벤트 핸들러가 비어있기 때문에 아무 일도 일어나지 않습니다.
그 밖에 버튼과 같은 위젯은 클릭(누름 또는 해제) 이벤트에 반응합니다. 버튼 위젯은 버튼을 눌렀을 때 상태를 변경하여 다른 이미지를 표시하고, 터치를 해제하면 상태를 다시 복원합니다.
버튼과 같은 위젯의 상태가 바뀌면 프레임버퍼에서도 다시 그려야 합니다. 또한 위젯은 이벤트에 대한 응답으로 이러한 상태 변경을 그래픽 엔진에게 다시 알려야 합니다. 그래픽 엔진은 수집된 이벤트를 토대로 위젯을 직접 다시 그리지 않습니다. 위젯이 내부 상태(버튼의 경우 그릴려고 하는 이미지)를 추적하여 그래픽 엔진에게 디스플레이에서 위젯에 해당하는 부분(직사각형)을 다시 그리기 작업을 하도록 요청합니다.
애플리케이션 역시 이벤트에 반응할 수 있습니다. 일반적으로 다음 중 한 가지 방법으로 반응합니다.
- TouchGFX Designer의 위젯에 대한 인터랙션을 구성합니다. 예를 들어 버튼을 눌렀을 때 다른 위젯이 표시되도록 인터랙션을 구성할 수 있습니다. 이러한 인터랙션은 버튼 위젯이 상태가 바뀌어 그래픽 엔진에게 다시 그리기 작업을 하라고 요청했을 때 실행됩니다. 인터랙션을 사용해 다른(보이지 않는) 위젯을 표시할 경우 애플리케이션도 그래픽 엔진에게 다시 그리기 작업을 하도록 요청해야 합니다.
- 스크린에서 이벤트에 반응합니다. 스크린 자체에서도 이벤트에 반응할 수 있습니다. 스크린 클래스에서는 이벤트 핸들러가 가상 함수입니다. 이러한 함수들은 애플리케이션의 스크린에서 다시 구현될 수 있습니다. 예를 들어 위젯에 터치가 있든 없든 상관없이 사용자가 스크린을 터치할 때마다 액션을 실행하는 데 사용할 수 있습니다.
스크린 클래스는 다음과 같은 이벤트 핸들러를 가지고 있습니다. 이벤트 핸들러는 해당하는 외부 이벤트가 수집되었을 때 그래픽 엔진에서 호출됩니다.
framework/include/touchgfx/Screen.hpp
virtual void handleClickEvent(const ClickEvent& evt);
virtual void handleDragEvent(const DragEvent& evt);
virtual void handleGestureEvent(const GestureEvent& evt);
virtual void handleTickEvent();
virtual void handleKeyEvent(uint8_t key);
위의 이벤트 핸들러에는 C++ 코드를 무엇이든 삽입할 수 있습니다. 일반적으로 애플리케이션은 일부 위젯의 상태를 업데이트하거나, 애플리케이션 전용 함수(비즈니스 로직)를 호출합니다.
시간 기반 업데이트
어떤 프레임이든 handleTickEvent 이벤트 핸들러가 호출됩니다. 애플리케이션이 시간을 기반으로 사용자 인터페이스를 업데이트할 수 있는 것도 이러한 핸들러가 호출되기 때문입니다. 한 예로, 10초가 지났을 때 위젯이 서서히 사라지는 경우를 들 수 있습니다. 초당 60프레임이라고 가정했을 때 코드는 다음과 같은 모습이 될 수 있습니다.
void handleTickEvent() {
tickCounter += 1;
if (tickCounter == 600) {
myWidget.startFadeAnimation(0, 20); // Fade to 0 = invisible in 20 frames
}
}
Model 클래스에서는 그래픽 엔진이 이벤트 핸들러도 호출합니다. 이 이벤트 핸들러는 일반적으로 메시지 대기열을 확인하거나 GPIO를 샘플링하는 등 반복적인 액션을 실행하는 데 사용됩니다.
void Model::tick() {
bool b = sampleGPIO_Input1(); // Sample polled IO
if (b) {
...
}
}
다시 그리기 요청
위의 버튼 예시에서도 얘기했듯이 위젯은 상태가 바뀌면 다시 그리기 작업을 하도록 요청해야 합니다. 이러한 메커니즘을 일컬어 무효화 영역이라고 합니다.
예를 들어 버튼 상태가 해제에서 누름으로 바뀌어 다시 그리기를 해야 하는 경우에는 버튼 위젯이 차지하는 영역이 무효화 영역입니다. 그래픽 엔진은 프레임에 필요하여 요청을 받는 무효화 영역 목록을 보관합니다. 이벤트(터치, 버튼, 실행)가 수집될 때마다 무효화 영역이 1개 이상 발생할 수 있기 때문에 모든 프레임에 다수의 무효화 영역이 존재할 수 있습니다.
스크린 클래스의 이벤트 핸들러도 영역을 다시 그리기 작업을 하도록 요청할 수 있습니다. 아래 예에서는 박스에서 invalidate 메소드를 호출하여 프레임 10 이후 박스 위젯 box1의 색상을 변경하고 다시 그리기 작업을 하도록 요청합니다.
void handleTickEvent() {
tickCounter += 1;
if (tickCounter == 10) {
box1.setColor(Color::getColorFromRGB(0xFF, 0x00, 0x00)); // Set color to red
box1.invalidate(); // Request redraw
}
}
위의 예에서 그래픽 엔진은 handleTickEvent 핸들러를 모든 프레임에서 호출합니다. 이후 10번째 프레임에서 애플리케이션 코드가 box1의 영역을 다시 그리기 작업을 하도록 요청합니다. 그러면 그래픽 엔진이 요청에 대한 응답으로 box1 위젯에 저장된 색상을 사용해 프레임버퍼에서 해당 영역을 다시 그리기를 합니다.
아래 사용자 인터페이스를 보면 버튼 위젯이 있고, 배경 이미지 상단에 박스 위젯이 있습니다. 이때 버튼에 대한 인터랙션을 삽입하여 버튼을 눌렀을 때 박스 색상이 바뀌도록 설정하면 사용자가 버튼을 클릭했을 때 발생하는 무효화 영역은 2개(빨간색 표시)가 됩니다.
박스 영역이 무효화되면서 프레임버퍼에 새로운 색상이 그려집니다. 또한 버튼도 무효화되어 다시 해제 상태가 그려지게 됩니다.
렌더링
앞에서 설명한 것처럼 업데이트 단계를 마치면 다시 그려야 할 영역, 즉 무효화 영역의 목록이 발생합니다. 렌더링 단계에서는 기본적으로 이러한 목록을 확인하고 해당 영역을 차지하는 위젯을 프레임버퍼에서 그리기 작업을 수행합니다.
렌더링 단계는 그래픽 엔진에서 자동으로 처리합니다. 애플리케이션이 이미 장면 모델(UI의 위젯)을 정의하여 일부 영역을 무효화했기 때문에 나머지는 엔진에서 처리합니다.
그래픽 엔진은 무효화 영역을 하나씩 처리합니다. 각 영역마다 엔진이 장면 모델을 스캔하여 일부분이든 전체든 상관없이 해당 영역이 차지하고 있는 위젯의 목록을 수집합니다.
그래픽 엔진이 이렇게 수집된 위젯 목록을 고려하여 해당 위젯에 대한 draw 메소드를 호출합니다. 그리기 작업 순서는 배경의 위젯부터 시작하여 가장 앞에 있는 위젯으로 끝납니다.
위젯의 draw 메소드는 프레임버퍼로 그릴 때 색상 등 위젯의 상태를 사용합니다. 위젯을 그리는 데 필요한 정보는 모두 업데이트 단계에서 위젯에 저장되어야 합니다. 그렇지 않으면 렌더링 단계에서 이러한 정보를 사용할 수 없습니다.
수신 대기
TouchGFX 그래픽 엔진은 다음 프레임을 업데이트 및 렌더링할 때까지 신호를 기다립니다. 프레임을 최대한 빠르게 연속해서 렌더링하지 않고 프레임 사이에서 기다리는 데는 두 가지 이유가 있습니다.
렌더링은 디스플레이와 동기화됩니다. 위에서도 언급했지만 일부 디스플레이에서는 프레임버퍼를 반복해서 전송해야 합니다. 이러한 전송이 진행 중일 때 임의로 프레임버퍼로 렌더링하는 것은 바람직하지 않습니다. 그래픽 엔진이 전송 후 렌더링이 시작될 때까지 잠시 신호를 기다리는 이유도 여기에 있습니다. 프레임버퍼를 전송해야 할 때는 다른 디스플레이들이 마이크로컨트롤러에 신호를 전송합니다. 그래픽 엔진이 기다리는 신호도 바로 이 신호입니다.
프레임은 고정된 속도로 렌더링됩니다. 프레임을 고정된 속도로 렌더링하면 애플리케이션에게 유용할 때가 많은데, 이는 특정 시간 동안 지속되는 애니메이션을 보다 쉽게 만들 수 있기 때문입니다. 예를 들어 60Hz 디스플레이라면 2초 분량의 애니메이션은 120 프레임 후에 마치도록 프로그래밍되어야 합니다.
그래픽 엔진이 신호를 기다리는 시간은 일반적으로 애플리케이션에서 우선순위가 낮은 프로세스에서 사용됩니다. 우선순위가 낮은 프로세스라고 해도 일정 시점에 이르면 결국 실행되어야 하기 때문에 이러한 방식으로 대기 시간을 무의미하게 소비하지는 않습니다.
프레임버퍼 처리
앞에서도 얘기했지만 그래픽 엔진은 프레임버퍼를 업데이트하기 전에 먼저 디스플레이와 동기화됩니다. 또한 프레임버퍼로 렌더링을 마친 이후에는 디스플레이에 업데이트된 프레임버퍼가 표시되는지 확인해야 합니다.
프레임버퍼가 2개일 때
가장 간단한 구성에서는 2개의 프레임버퍼를 사용할 수 있습니다. 그래픽 엔진은 프레임버퍼 2개를 번갈아 사용합니다. 프레임을 프레임버퍼로 그리기 작업을 하는 동안 나머지 프레임버퍼가 디스플레이로 전송되어 표시됩니다.
위 그림에서는 병렬 RGB 디스플레이가 LTDC 컨트롤러에 연결되어 있다고 가정합니다. 즉, 이 경우 모든 프레임에서 프레임버퍼를 디스플레이로 전송해야 합니다. 프레임버퍼가 2개이므로 그래픽 엔진은 프레임 버퍼 1개를 전송하는 동안 나머지 프레임버퍼로 프레임을 그릴 수 있습니다. 매우 효과적이므로 가능하다면 이러한 기법을 사용하는 것이 좋습니다.
그래픽 엔진이 모든 프레임에서 그리기 작업을 하기 때문에 위 그림의 모든 프레임에서 새로운 프레임버퍼를 전송합니다.
하지만 애플리케이션이 아무것도 업데이트하지 않는 프레임도 있습니다. 이때는 마찬가지로 아무것도 렌더링 되지 않습니다. 따라서 이후 프레임에서도 동일한 프레임버퍼가 다시 전송됩니다.
애플리케이션이 프레임 2에서 아무것도 그리지 않기 때문에 그래픽 엔진이 프레임 3에서도 프레임버퍼 2를 다시 전송합니다.
일반적인 병렬 RGB 디스플레이는 화면 리프레시 비율이 약 60Hz입니다. 마이크로컨트롤러는 이러한 업데이트 주파수를 유지 관리해야 합니다. 이러한 업데이트 주파수는 전송이 다시 시작하기 전까지 새로운 프레임을 렌더링할 수 있는 시간이 16ms라는 것을 의미합니다. 새 프레임을 렌더링할 수 있는 시간이 16ms를 초과하는 경우도 있습니다. 이때는 그래픽 엔진이 이전과 동일한 프레임을 다시 전송합니다.
프레임1을 렌더링하는 데 16ms 넘게 걸리기 때문에 이전에 프레임버퍼1로 렌더링된 프레임 0이 재전송됩니다. 프레임버퍼2의 새 프레임은 프레임 3에서 전송됩니다. 프레임버퍼를 2개 사용할 경우에는 렌더링 시간이 길어질 수 있습니다. 새 프레임을 사용할 수 있을 때까지 이전 프레임이 재전송되기 때문입니다.
프레임버퍼가 1개일 때
일부 시스템에는 프레임버퍼를 1개만 사용할 수 있는 메모리가 탑재되어 있습니다. 이때는 병렬 RGB 디스플레이를 가지고 있더라도 어쩔 수 없이 모든 프레임에서 프레임버퍼1을 전송해야 합니다.
이렇게 되면 그래픽 엔진이 디스플레이로 동시에 전송하는 것과 동일한 프레임버퍼로 그리기 작업을 해야 하기 때문에 문제가 될 수 있습니다. 주의를 기울이지 않으면 디스플레이가 이전 프레임과 새로운 프레임이 뒤섞인 프레임을 표시할 위험이 매우 큽니다.
이때 한 가지 해결책으로는 전송이 완료될 때까지 그리기 작업을 멈추었다가 전송이 다시 시작되기 이전 시간대에만 그리기 작업을 하는 방법이 있습니다. 하지만 이렇게 하면 전체 프레임 시간에서 전송이 차지하는 시간이 매우 크기 때문에 드로잉할 수 있는 시간이 매우 부족해집니다. 그 밖에도 다음 전송이 시작될 때 그리기 작업을 마치지 못하면 불완전한 프레임(티어링)이 발생할 수 있는 문제가 있습니다.
더 가능성 있는 해결책도 있습니다. 프레임버퍼의 전송량을 미리 추적하여 렌더링을 프레임버퍼에서 해당하는 부분으로 제한하는 방법입니다. 따라서 전송이 진행되면서 렌더링 알고리즘에 따라 사용할 수 있는 프레임버퍼가 점차 늘어나게 됩니다.
그래픽 엔진에는 프로그래머가 정확하게 그리기 작업을 하는 데 도움이 될 수 있는 알고리즘들이 포함되어 있습니다.
애플리케이션은 아래와 같이 모든 프레임에서 프레임버퍼를 업데이트하고 렌더링합니다.
프레임에서 아무것도 업데이트되지 않으면 프레임버퍼가 그대로 재전송됩니다.
렌더링 시간이 16ms보다 길면 재전송이 다시 시작되어도 렌더링이 끝나지 않습니다.
이러한 상황에서는 그래픽 엔진이 전송되는 부분이 완전하게 렌더링 되는지 확인해야 합니다. 그렇지 않으면 디스플레이가 불완전한 프레임버퍼를 표시하기 때문입니다.
다음 섹션에서는 각 위젯에 따른 렌더링 시간에 대해 알아보겠습니다. 다음 섹션은 프로그래머가 고성능 애플리케이션을 개발하는 데 도움이 될 것입니다.