주요 내용으로 건너뛰기

성능

이 섹션에서는 내장되는 그래픽 사용자 인터페이스의 성능에 대해 알아보겠습니다.

여기에서 정의하는 고성능이란 높은 프레임 속도를 유지하면서 원하는 그래픽 효과와 애니메이션을 구현할 수 있는 것을 말합니다.

이전 섹션에서 우리는 사용자 인터페이스의 프레임 속도가 메인 루프의 영향을 얼마나 받는지 살펴보았습니다. 병렬 RGB 디스플레이가 LTDC에 연결되어 있고, 프레임버퍼가 2개라고 다시 한 번 가정하겠습니다. 기본적인 상황은 아래와 같습니다.

이중 프레임버퍼

디스플레이가 초당 60회 리프레시된다고 가정했을 때 각 리프레시 사이에 약 16ms의 시간이 존재합니다. 계산 방법은 이렇습니다. 1s / 60 = 0.01667s = 16.67ms.

TouchGFX는 프레임버퍼1의 전송이 시작되는 시간에 프레임1을 프레임버퍼2로 그리기 시작합니다. 다음 전송이 시작되기 전에 프레임1 렌더링이 끝나면 프레임버퍼 2를 전송할 수 있습니다. 16.67ms 안에 렌더링이 끝나지 않으면 프레임버퍼 1이 다시 전송되고 디스플레이가 변함없이 그대로 표시됩니다.

16.67ms를 벗어나는 메인 루프 시간

이러한 상황을 프레임 손실이라고 합니다.

수집 및 업데이트 단계에 걸리는 시간은 일반적으로 매우 적기 때문에(예: 1ms 미만), 전체 메인 루프 시간을 감안했을 때 다소 무시해도 무방합니다. 따라서 지금부터, 그리고 일반적으로 렌더링 시간을 고려할 때는 수집 단계와 업데이트 단계도 포함된 것입니다.

다수의 프레임에서 렌더링 시간이 제한 시간인 16.67ms를 초과할 경우 디스플레이의 프레임 속도는 초당 30프레임(fps)이 됩니다.

렌더링 시간이 대부분 16.67ms보다 짧더라도 일부 프레임에서 16.67ms보다 길면 평균 프레임 속도는 60fps에 가까워질 수 있습니다. 하지만 애니메이션은 사용자에게 자연스럽게 보이지 않을 수도 있습니다. 애플리케이션에 따라 일부 애니메이션 단계는 빠르고, 일부는 느리게 보일 수도 있습니다. 이러한 현상은 바람직하지 않습니다.

또한 렌더링 시간이 더 길어질 수도 있습니다. 33ms를 조금만 초과해도 세 번째 전송에서만 새로운 프레임을 준비할 수 있기 때문에 프레임 속도가 20fps로 떨어집니다.

FPS최대 렌더링 시간
6016.67ms
3033.34ms
2050.00ms
1566.67ms

위 표는 일정 프레임 속도에서 사용할 수 있는 최대 렌더링 시간(수집 및 업데이트 단계 포함)을 나타낸 것입니다.

사용자 인터페이스의 성능을 높이려면 프레임 속도를 주기적으로 확인하고 모니터링하는 것이 좋습니다. 이때 사용할 수 있는 방법은 두 가지입니다.

  • 렌더링 시간 측정
  • 손실된 프레임 계산

렌더링 시간 측정

첫째, 렌더링 시간 측정은 가장 상세한 정보를 얻을 수 있는 방법입니다. 이때는 기본적으로 프레임 전송부터 렌더링 단계가 끝날 때까지 걸리는 시간을 측정합니다. 수집 단계가 시작되면 그래픽 엔진이 GPIO 클래스 함수를 호출하고, 렌더링 단계가 끝나면 한 번 더 호출합니다. 애플리케이션은 이러한 함수를 정의한 후 측정을 수행하기 위해 이를 연동시킬 수 있습니다.

측정은 다음과 같이 두 가지 방식으로 이루어집니다.

  • 오실로스코프와 같은 외부 시간 측정 장치 사용: 오실로스코프를 사용해 측정하려면 애플리케이션이 GPIO 인터페이스에서 set(GPIO_ID) 메소드와 clear(GPIO_ID) 메소드를 구현해야 합니다. 그러면 오실로스코프가 출력이 높을 때도 시간 경과에 따른 렌더링 시간을 측정할 수 있습니다.
  • 내부 타이머 사용: sysTick과 같은 내부 타이머를 사용하는 방법이 있습니다. GPIO::set(RENDER_TIME)가 호출되면 애플리케이션이 타이머 값을 변수로 저장할 수 있습니다. 이후 clear가 호출되면 애플리케이션이 타이머를 다시 읽고 이전 값을 감산하여 렌더링 시간을 구합니다. 측정 해상도는 타이머의 속도에 따라 결정됩니다. 또한 애플리케이션은 어떻게든 렌더링 시간을 표시해야 합니다. 이때 한 가지 방법은 값을 전역 변수로 저장하여 스크린에서 TextArea에 표시하는 것입니다. 또한 디버거를 사용해 값을 확인하는 방법도 있습니다.

손실된 프레임 계산

그래픽 엔진은 마지막 수집-업데이트-렌더링 단계에서 수행된 전송 횟수를 계산합니다. 애플리케이션은 이 값을 확인하여 프레임 손실로 인해 프레임 속도가 떨어졌는지 여부를 쉽게 알 수 있습니다.

계산은 다음과 같이 HAL 클래스에서 가능합니다.

void handleTickEvent() {
tickCounter += 1;
if (HAL::getInstance()->getLCDRefreshCount() > 1) {
//Alert programmer somehow
...
}
}

손실된 프레임 보상

프레임 손실이 발생하여 애니메이션 1개의 프레임 속도가 떨어졌다면 어느 정도까지는 보상할 수 있습니다. 방법은 다음 중 한 가지입니다.

  • 끝까지 기다립니다 - 애니메이션을 계속 실행합니다. 애니메이션 시간이 길어질 뿐만 아니라 자주 끊길 수도 있습니다.
  • 일부 프레임을 건너뜁니다 - 프레임을 건너뛰어 전체 애니메이션이 예상보다 오래 걸리지 않게 설정합니다.

TouchGFX에서는 프레임이 손실되면 일부 프레임을 자동으로 건너뛰도록 지정할 수 있습니다. 그 방법은 실제 프레임마다 애니메이션을 한 번 이상 실행하면 됩니다. 이는 렌더링 시간이 불규칙할 때 애니메이션을 보다 자연스럽게 실행하는 데 효과적입니다.

HAL.hpp
void setFrameRateCompensation(bool enabled)

렌더링 시간에 영향을 미치는 요인

렌더링 시간에 영향을 미치는 요인은 업데이트되는 부분의 크기, 계층화 사용, 위젯의 복잡성, 렌더링 시 하드웨어 지원 등 다양합니다.

업데이트해야 하는 스크린의 영역

렌더링 시간은 일반적으로 업데이트할 픽셀의 수에 비례합니다. 애니메이션의 렌더링 시간이 너무 길면 애니메이션 영역을 줄이는 것이 좋습니다. 예를 들어 회전하는 이미지가 있는데 성능이 떨어진다면 이미지의 크기를 줄여서 성능을 개선할 수 있습니다.

이미지의 크기가 줄면 렌더링 시간도 짧아집니다.

단, 그래픽 엔진은 애플리케이션에서 무효화한 영역을 다시 그리기를 합니다. 따라서 리프레시가 실제로 필요한 영역만 무효화하는 것이 중요합니다.

무효화 영역이 커질수록 렌더링 시간도 길어집니다.

그래픽의 계층 수

일반 애플리케이션에서 그래픽은 여러 가지 요소가 서로 스택을 이루어 구성됩니다. 따라서 한 가지 요소 업데이트하면 일반적으로 모든 요소를 다시 그려야 합니다.

대표적으로 배경 이미지와 프레임, 그리고 일부 텍스트가 그렇습니다.

그래픽 요소 계층화

이 사용자 인터페이스는 투명 프레임이 보이는 이미지 위젯 위에 TextArea 위젯을 배치한 것입니다. 두 위젯 모두 배경 이미지 위에 있습니다.

TouchGFX Designer의 그래픽 요소 계층화

이 해결책은 애플리케이션에서 자주 사용됩니다. 또한 유연성이 높아서 매우 쉬운 해결책이기도 합니다. 예를 들어 런타임에서 프레임 변경이 가능할 뿐만 아니라 배경에서 프레임과 텍스트를 이동시키는 것도 가능합니다.

텍스트가 런타임에서 업데이트되어 다시 그려야 할 경우에는 그래픽 엔진도 배경과 프레임, 그리고 이어서 새로운 텍스트까지 다시 그려야 합니다. 이에 따라 텍스트를 렌더링하는 시간도 증가하기 때문에 렌더링 시간에 문제가 발생합니다.

무효화 영역에 계층이 많을수록 렌더링 시간도 길어집니다.

픽셀 렌더링의 복잡성

픽셀이라고 해서 프레임버퍼로 렌더링하는 것이 다 어려운 것은 아닙니다. 어떤 렌더링이 되었든지 그래픽 엔진이 해당하는 픽셀을 프레임버퍼에 작성해야 합니다. 하지만 작성할 픽셀을 계산하는 드는 손실은 각기 다릅니다.

예를 들어 Box Widget에 사용되는 고정된 색상은 가장 낮은 손실이 드는데, 그 이유는 픽셀을 한 번만 계산하면 모든 픽셀에 재사용할 수 있기 때문입니다. 즉, Box들을 많이 사용하여 매우 높은 성능을 얻는 셈입니다. 하지만 사용자 인터페이스의 품질이 높지 않을 때는 권장하지 않는 방법입니다.

이미지는 픽셀 계산에 드는 손실이 두 번째로 낮은데, 이는 픽셀이 언제든지 사용할 수 있는 형식으로 비트맵에 저장되기 때문입니다. 픽셀을 계산하여 프레임버퍼에 작성하려면 정확한 비트맵 위치에서 색상 값을 가져와야 합니다.

텍스트는 문자 하나마다 실제로 작은 이미지로 표현되기 때문에 손실이 이미지와 맞먹습니다. 실제로 손실이 더 높습니다. 작은 이미지가 너무 많아서 “시작-정지” 손실이 크게 늘어나기 때문입니다. 각 문자의 위치를 계산하는 경우에도 그렇습니다. 텍스트는 최대한 충실하게 보일 수 있도록 투명도를 적용하여 작은 이미지로 표현됩니다. 투명도에 대한 내용은 아래를 참조하십시오.

회전하거나 확대/축소된 이미지는 비용이 더 높습니다. 마찬가지로 비트맵에서 픽셀 값을 가져오지만 계산 시간이 더 오래 걸리는데, 그 이유는 그래픽 엔진이 확대/축소와 회전까지 고려해야 하기 때문입니다.

원형과 같은 기하학적 요소는 손실이 훨씬 더 높습니다. 이때는 비트맵에서 픽셀 색상을 가져오지 못해서 원형 형상과 원형의 각 픽셀 색상까지 모두 계산해야 하기 때문입니다.

투명도가 적용되면 요소를 그리는데 드는 손실이 증가합니다. 일부 픽셀이 불투명하지 않은 요소는 투명합니다. 이 경우 그래픽 엔진이 투명 요소 뒤에 있는 요소를 먼저 그려야 하기 때문에 “프레임 내 텍스트” 섹션에서도 보았듯이 그리는 손실이 증가하게 됩니다. 그런 다음 그래픽 엔진이 배경 픽셀과 투명 요소의 픽셀을 결합하여 그 결과를 프레임버퍼에 작성해야 합니다. 이러한 계산 과정은 단순히 이미 계산된 픽셀을 작성하는 것보다 훨씬 더 많은 시간이 소요됩니다.

박스, 이미지, 회전 이미지, 원형 위는 불투명 요소이고, 아래는 투명 요소입니다.

투명 요소는 항상 계층이 하나 더 있습니다. 하지만 불투명 픽셀이 다른 불투명 픽셀 위에 겹친다고 해서 반드시 계층 수가 늘어나는 것은 아닙니다. 그래픽 엔진은 다른 불투명 픽셀로 겹치는 픽셀은 그리지 않으려고 하는데, 이는 소중한 시간을 낭비할 수 있기 때문입니다.

무효화 영역에 손실이 높은 요소가 많을수록 렌더링 시간이 길어집니다.

앞에서도 얘기했지만 렌더링 시간이 길어지는 이유는 오직 무효화 영역에 포함되는 요소에서 비롯됩니다. 무효화 영역 외부의 요소는 렌더링 시간에 영향을 미치지 않습니다.

UI 구성요소와 성능에 대한 자세한 내용은 여기에서 확인하십시오.

렌더링을 지원하는 하드웨어

일부 STM32 마이크로컨트롤러에는 Chrom-ART(또는 DMA2D)라고 하는 그래픽 가속기가 포함되어 있는데, 이 가속기가 렌더링 시간을 줄일 수 있습니다. 가속기가 마이크로컨트롤러 코어와 함께 실행되기 때문에 가속기가 그래픽을 렌더링하는 동안 마이크로컨트롤러는 다른 작업을 자유롭게 실행합니다.

Chrom-ART는 주로 이미지와 텍스트에 유용합니다. 가능한 경우 그래픽 엔진에서 자동으로 사용됩니다.

렌더링 시간을 고려해야 하는 경우

렌더링 시간이 항상 중요한 것은 아닙니다. 프레임 속도가 눈에 띄게 낮을 때 렌더링 시간에 신경 써야 합니다. 일반적으로 일부 스크린 영역에서 애니메이션(회전하는 아이콘 등)을 실행하거나, 혹은 스크린에서 임의 요소를 움직이거나 밀어서 움직일 때가 여기에 해당합니다. 또한 업데이트 주파수가 낮으면 사용자에게 자연스럽게 표시되지 않고 단계적으로 표시됩니다. 이러한 경우에도 렌더링 시간을 확인해야 합니다.

그 밖에 전체 스크린을 새로운 스크린으로 변경하는 경우에는 프레임 속도가 크게 떨어지더라도 보통은 사용자의 눈에 띄지 않습니다. 그 이유는 사용자가 렌더링 시작 시점이 아닌, 종료 시점에만 인지할 수 있기 때문입니다.

위의 두 가지 규칙은 예를 들어 움직이는 애니메이션 요소에서는 계층을 적게 사용하고 복잡한 요소와 여러 계층은 사용을 자제해야 한다는 것을 의미합니다. 다른 스크린 영역에서는 이렇게 해도 문제가 되지 않을 수 있습니다.

아날로그 시계와 스크롤 목록

위 예를 보면 왼쪽에 아날로그 시계가 있습니다. 시계 바늘 3개가 작고 긴 이미지들을 따라 회전하면서 렌더링됩니다. 이때는 시계 바늘이 계속 움직이는 것이 아니기 때문에 일반적으로 문제가 없습니다. 하지만 스크린에서 시계의 위치를 옮긴다면 모든 프레임에서 다시 그리기를 하여 문제가 될 수 있습니다. 회전하는 이미지를 그릴려면 시간이 오래 걸리기 때문입니다.

오른쪽에 스크롤 리스트가 있습니다. 사용자가 숫자 목록을 위아래로 움직일 수 있기 때문에 사용자 인터페이스에서 반응하려면 프레임 속도가 높아야 합니다. 따라서 이때는 스크롤 리스트 요소의 렌더링 시간을 고려하거나 스크롤 리스트의 크기를 줄여야 합니다.

콘텐츠를 무효화하여 성능 최적화

일반적으로 전체 위젯이 무효화되지만, 그래픽 엔진은 전체 위젯 대신에 위젯의 콘텐츠만 무효화할 수 있습니다. 무효화할 영역을 줄이면 렌더링 시간이 눈에 띄게 단축될 것입니다. 렌더링 시간의 단축은 다음과 같은 요소에 달려 있습니다.

  • 전체 위젯의 크기와 비교해 위젯 콘텐츠로 겹치는 영역의 크기.
  • 해당 위젯으로 일부 또는 전체가 겹치는 배경 위젯(들).

아래 그림들은 TextArea 위젯을 예로 사용해 콘텐츠를 무효화하는 방법을 보여줍니다. 그림 1은 위젯의 전체 영역을 보여줍니다. 그림 2는 TextArea::invalidate()를 사용할 때 무효화된 영역을 보여줍니다. 그림 3은 TextArea::invalidateContent()를 사용할 때 무효화된 영역을 보여줍니다.

그림 1. 전체 스크린 너비에 걸쳐 있는 TextArea

그림 2. TextArea::invalidate() 사용 시 무효화된 영역(빨간색)

그림 3. TextArea::invalidateContent() 사용 시 무효화된 영역(녹색)

TextArea::invalidateContent() 사용 예제

위젯이 다른 위젯과 겹치는 경우, TextArea::invalidate()를 사용해 전체 TextArea가 무효화가 되면 이러한 다른 위젯들을 다시 그려야 합니다. 대신에 TextArea::invalidateContent()를 사용하면 위젯을 불필요하게 무효화하고 다시 그리는 위험을 최소화할 수 있습니다. 예를 들어 Circle, Gauge 같이 값비싼 위젯에서는 특히 그렇습니다.

아래 그림은 TextArea::invalidateContent()를 사용해 배경 위젯(이미지 - ST 로고)의 무효화를 방지하는 방법을 보여줍니다. TextArea::invalidate()를 사용했다면 배경 위젯이 무효화되어 다시 그리게 되었을 것입니다.

TextArea::invalidateContent() 사용 예제

좋은 성능을 달성하는 팁

이 섹션을 마치면서 좋은 성능을 얻기 위한 팁을 간략히 알려드리겠습니다.

  • 변화가 없는 요소를 다시 그리지 마십시오. 디스플레이에서 불필요한 요소를 무효화하는 실수를 저질러서는 안 됩니다. 이는 아무런 이점도 없이 성능만 떨어지게 되는 셈입니다.
  • 품질과 속도의 균형점을 찾으십시오. 요소의 복잡성을 줄이면 성능이 개선될 수 있습니다. 대부분의 경우 복잡성과 성능의 균형을 유지하는 것이 관건입니다.
  • 하드웨어 기능을 이용하십시오. 하드웨어 가속 기능(Chrom-ART)을 지원하는 마이크로컨트롤러가 그렇지 않은 마이크로컨트롤러보다 성능이 더 높습니다. 따라서 Chrom-ART가 포함된 마이크로컨트롤러를 사용하는 것이 좋습니다.
  • 계산된 그래픽을 이미지로 변경하십시오. 계산된 원은 원 이미지보다 성능이 느립니다. 일반적으로 이미지는 대부분의 정적 요소를 대신할 수 있습니다.
  • 디스플레이의 리프레시 비율을 조정하십시오. 이 섹션의 서두에서 얘기했다시피 리프레시 비율은 렌더링 시간의 최대 한계입니다. 렌더링 시간이 리프레시 비율을 초과하면 프레임 속도가 떨어지게 됩니다. 렌더링 시간이 리프레시 비율을 약간 초과한다면 디스플레이의 리프레시 비율을 예를 들어 55Hz(18.2ms에 해당)로 낮춰 프레임 속도를 높게 유지하는 것이 좋습니다.