캐시(Cacheable) 컨테이너를 통한 성능 개선
이 섹션에서는 일부 애니메이션 시나리오에서 RAM을 사용해 재사용이 가능한 이미지를 저장하여 성능을 개선하는 방법에 대해 알아보겠습니다.
애플리케이션(Image, TextArea 등)에서 드래그나 애니메이션을 통해 위젯 위치를 조정할 경우 TouchGFX가 모든 프레임마다 새로운 위치에서 해당 위젯을 다시 그리기를 해야 할 뿐만 아니라, 조정 이전에 배경에서 위젯으로 가려졌던 부분까지 다시 그리기를 해야 하는 경우가 많습니다.
Texture Mapper 위젯과 Shapes, 그리고 투명하고 큰 이미지까지 이러한 위젯들은 계산이 복잡할 뿐만 아니라 하드웨어 가속없이 렌더링되기 때문에 MCU에서 효율적으로 렌더링하기가 어렵습니다. 결과적으로 스크린을 다시 그리기를 하느라 시간(수 밀리초)이 걸리며 애플리케이션의 성능에도 영향을 미칩니다.
따라서 비용이 많이 드는 다시 그리기 작업을 하지 않고, 캐시어블 컨테이너를 사용해 계산이 복잡한 요소가 포함된 애니메이션을 가속하는 방법에 대해 알아보겠습니다. 이 글에서 측정은 STM32F429Discovery 보드를 사용해 이루어지지만 캐시 컨테이너 기법은 일반적으로 다른 하드웨어 플랫폼에도 적용됩니다. 그 밖에 비트맵 캐시를 생성하려면 사용 가능한 RAM이 필요합니다.
Further reading
성능에 미치는 영향
MCU를 사용해 계산이 복잡한 위젯의 위치를 변경하면 성능에 영향을 미치므로 여러 단계로 이루어진 애니메이션은 각 프레임마다 렌더링 시간이 오래 걸려 속도가 느려지게 됩니다. 그렇다고 애니메이션을 시간적으로 더 빨리 끝나도록 프로그래밍하게 되면 각 단계의 크기가 커져서 사용자에게 애니메이션이 부드럽게 보이지 않습니다.
다음은 STM32F429-DISCO 보드(240x320)에서 실행하는 예시입니다. 여기서 전체 화면 컨테이너는 수직 방향으로 움직이는 반면 유사한 컨테이너는 하단에서 움직입니다.
아래 동영상에서 ToggleButton이 캐시 컨테이너를 활성화와 비활성화 사이에서 전환합니다. 이때 확연한 성능 차이를 알 수 있습니다.
움직이는 두 컨테이너는 각각 배경 Box, a TextArea, and a Texture Mapper로 구성되어 있습니다. 특히 Texture Mapper는 이중 선형 렌더링 알고리즘을 사용하고 전역 알파 값으로 174를 사용하도록 구성되어 있어 그리기를 위해 많은 작업량을 필요로 합니다. STM32F429-DISCO 보드에서 전체 화면일 때 렌더링 시간은 약 100ms입니다.
테스트 애플리케이션
서로 관련이 있는 두 요소를 이동시키기 위해 두 요소는 masterContainer
라는 상위 컨테이너에 포함됩니다. 이에 따라 상위 컨테이너의 세로 길이는 각 하위 컨테이너의 2배가 되어 240 x 640 (2*320)
이 됩니다. TouchGFX Designer에서 컨테이너를 위치 변경 애니메이터로 선언하면 애플리케이션 틱(application ticks)을 수신하여 성능을 측정하는 동안 애니메이션을 수행할 수 있습니다.
container1
이라는 이름의 상부 컨테이너는 x=0, y=0 위치에 배치됩니다. container2
라는 이름의 하부 컨테이너는 상위 컨테이너인 masterContainer
에서 container1 바로 아래 x=0, y=320 위치에 배치됩니다.
container1
과 container2
가 masterContainer
에 배치되므로 masterContainer
의 위치를 변경하면 두 요소의 위치도 바뀌게 됩니다. 예를 들어 masterContainer
의 위치를 x=0, y=-320으로 변경하면 container1
은 보이지 않지만 container2
는 완전히 보이게 됩니다. 이러한 두 상태 사이에서도 TouchGFX Designer의 상호작용 기능을 사용하면 애니메이션을 생성할 수 있습니다.
아래 코드는 masterContainer
가 아래로 내려간 경우 위로, 그리고 위로 올라간 경우 아래로 위치를 변경합니다. 이해를 돕기 위해 코드를 뷰의 handleClickEvent
이벤트 핸들러에 삽입했습니다. 따라서 사용자가 화면 어느 곳을(ToggleButton 아래) 터치하더라도 코드가 실행됩니다.
Screen1View.cpp
void Screen1View::handleClickEvent(const ClickEvent& evt)
{
//Forward event to base View (for the ToggleButton to work)
View::handleClickEvent(evt);
//If touch is released and y > 50 (below the ToggleButton), move masterContainer
if (evt.getType() == ClickEvent::RELEASED && evt.getY() > 50)
{
const int endPosition = masterContainer.getY() >= 0 ? -320 : 0;
masterContainer.startMoveAnimation(masterContainer.getX(), endPosition,
20 /* ticks */,
EasingEquations::cubicEaseInOut,
EasingEquations::cubicEaseInOut);
}
}
복잡한 컨테이너를 다시 그리기 하는 성능
앞서 언급했듯이, MCU가 각 애니메이션 단계마다 복잡하고 시간도 오래 걸리는 Texture Mapper를 다시 그리기를 해야 한다면 한 프레임마다 소요되는 렌더링 시간은 약 100ms가 됩니다. 결국 초당 10 프레임(fps)의 그리기 해야 하는 셈입니다. 전체 애니메이션이 20프레임이므로 약 2초가 걸립니다.
STM32F429-DISCO 평가 키트에서는 렌더링 시간이 GPIO G14를 통해 디지털 신호로 제공됩니다. VSYNC 신호는 G13을 통해 제공됩니다. GPIO 구성은 GPIO.cpp
파일에서 설정합니다.
다음은 masterContainer
를 위로 올렸을 때 애플리케이션의 VSYNC와 RENDER_TIME을 측정하는 이미지입니다.
첫 번째 신호는 렌더링 시간입니다(active low). 위치 변경 애니메이션에서 첫 번째 프레임의 렌더링 시간이 99.29ms인 것을 알 수 있습니다.
아래 신호는 VSYNC입니다. 이 신호는 모든 프레임에서 픽셀이 디스플레이로 클럭 아웃될 때 high에서 low로 전환됩니다. 위 측정에서 단일 프레임의 그리기를 수행 하려면 디스플레이에서 7 프레임만큼의 시간이 걸린다는 것을 알 수 있습니다. 다음 프레임 렌더링이 8번째 VSYNC 신호에서 시작되기 때문입니다. 렌더링 과정에서 (다른 프레임버퍼에서) 이전에 그리기가 완료된 프레임이 디스플레이에 반복적으로 표시됩니다.
캐싱을 통한 성능 개선
컨테이너 랜더링을 메모리에 캐싱하면 위의 위치 변경 애니메이션의 성능을 개선할 수 있습니다. 그러면 MCU를 사용해 복잡한 위젯의 다시 그리기를 할 필요 없이(DMA를 사용해) 해당 메모리에 저장된 픽셀을 프레임버퍼로 가져올 수 있습니다. MCU만 사용해도 애플리케이션에서 초당 60 프레임을 얻을 수 있지만 더 중요한 작업을 수행하지 않고 동일한 계산을 반복할 경우 연산 작업이 많아져 부하가 증가하게 됩니다(MCU 부하가 100%에 이를 수 있음).
이제는 컨테이너를 다시 렌더링할 필요 없이 컨테이너의 "in-memory-image"를 스크린에서 다양한 위치에 표시할 수 있습니다.
먼저 TouchGFX Designer에서 두 컨테이너인 container1
과 container2
의 Cacheable 속성을 선택하여 캐싱을 활성화합니다:
그런 다음 RAM에서 두 컨테이너를 캐싱할 2개의 동적 비트맵을 생성합니다.
비트맵 캐시가 저장되는 RAM의 주소를 결정합니다. 아래 예제에서는 프레임버퍼 바로 뒤에 있는 SDRAM(STM32F429에서 0xd0000000 주소로 시작)에 저장했습니다.
Windows 시뮬레이터의 경우에는 캐시가 전역 변수로 할당됩니다.
Screen1View.hpp
#ifdef SIMULATOR
uint32_t sdramBuffer[8*1024*1024/4];
uint16_t* sdram = (uint16_t*)sdramBuffer;
#else
uint16_t* sdram = (uint16_t*)(0xd0000000 + 320*240*2*2);
#endif
비트맵 캐시를 초기화한 후 캐싱에 필요한 2개의 동적 비트맵을 생성합니다.
Screen1View.cpp
//Create bitmap cache and two dynamic bitmap for caching, each bitmap is 150Kb
Bitmap::setCache(sdram, 320*1024, 2); //320Kb cache
dynamicBitmap1 = Bitmap::dynamicBitmapCreate(240, 320, Bitmap::RGB565);
dynamicBitmap2 = Bitmap::dynamicBitmapCreate(240, 320, Bitmap::RGB565);
동적 비트맵을 컨테이너에 할당한 후 캐싱 모드로 설정합니다.
Screen1View.cpp
//Assign the bitmaps to the Cacheable Containers
container1.setCacheBitmap(dynamicBitmap1);
container2.setCacheBitmap(dynamicBitmap2);
//Enable caching
container1.enableCachedMode(true);
container2.enableCachedMode(true);
//Finally update the cached bitmaps
container1.updateCache();
container2.updateCache();
Container::updateCache()
를 호출하면 컨테이너 2개가 각 비트맵으로 렌더링됩니다. 이제 컨테이너를 업데이트해야 할 때마다 이 메소드를 호출합니다. 단, 개발자가 애플리케이션 코드에서 처리해야 합니다.
container1
과 container2
에서 캐싱을 활성화한 후 성능을 측정하면 렌더링 시간이 99ms에서 5ms로 20배까지 개선됩니다. 따라서 초당 60 프레임으로 손쉽게 렌더링하여 전체 애니메이션을 20 프레임 내에 완료할 수 있습니다.
결론
피사체 계산이 복잡하여 애니메이션 단계마다 변경이 어려운 경우에 캐시 컨테이너를 DynamicBitmap과 함께 사용하여 애니메이션(잦은 위치 변경) 처리하면 렌더링 시간을 크게 단축할 수 있습니다. 캐시를 업데이트해야 하는 경우에도(시간 업데이트 시 시계의 숫자판 등) 애플리케이션에서 애니메이션을 제어하는 일정 시점에 캐시의 내용을 다시 계산할 수 있습니다.