주요 내용으로 건너뛰기

비트맵 캐싱

이 섹션에서는 TouchGFX의 비트맵 캐시에 대해 알아보겠습니다. 비트맵 캐시는 애플리케이션에서 비트맵을 저장(또는 캐싱)할 때 사용하는 전용 RAM 버퍼입니다. 비트맵이 캐싱되면 TouchGFX가 비트맵을 드로잉할 때 자동으로 RAM 캐시를 픽셀 소스로 사용합니다.

비트맵 캐싱은 여러 경우에 유용하게 사용될 수 있습니다. RAM에서 데이터를 읽어오면 플래시에서 읽어오는 것보다 대체로 속도가 더 빠릅니다(비선형 메모리 액세스를 사용하기 때문에 특히 Texturemapper를 사용할 때 더욱 그렇습니다). 따라서 RAM으로 캐싱하면 UI의 성능을 높일 수 있습니다. 하지만 내장 플래시에서 외장 RAM으로 캐싱하면 오히려 성능이 떨어질 수 있습니다. 또한 RAM으로 캐싱하면 UI를 표시하는 동시에 플래시를 다른 용도(예: 로그 파일)로 사용할 수 있는데, 이는 비트맵을 RAM에서 읽어오기 때문입니다(일부 경우 플래시에 데이터를 쓰려면 비 메모리 매핑이 필요합니다). 그 밖에 비트맵 픽셀에 대한 수정이 필요해서 비트맵을 수정 가능한 메모리에 저장해야 할 때에도 유용할 수 있습니다.

TouchGFX는 성능상의 이유로 외장 플래시에 저장된 그래픽 데이터에 드라이버 계층을 통하지 않고 (포인터를 통해) 직접 액세스할 수 있어야 합니다. 이 말은 TouchGFX는 비 메모리 매핑 플래시(SD-카드와 같은 Non-memory mapped flash)에서 직접 렌더링하지 못한다는 것을 의미합니다. 비트맵 캐시는 전원이 켜져 있을 때 비트맵 데이터의 일부 또는 전부를 RAM에 캐싱하는 메커니즘을 통해 이러한 제약을 해결합니다. 따라서 비트맵 캐싱은 비트맵을 USB 디스크나 SD-카드 같이 속도가 느린 외장 저장 장치에 저장해야 할 때 유용합니다.

비트맵 캐시 설정

비트맵 캐싱 기능을 사용하려면 먼저 비트맵 캐시 구성 정보를 TouchGFX에 제공해야 합니다. 그런 다음 (일부 경우) 하드웨어에 따라 외장 저장 장치에서 데이터를 읽어올 수 있는 BlockCopy 함수의 구현체를 제공해야 합니다.

비트맵 캐시 구성

비트맵 캐시 구성은 버퍼를 가리키는 포인터와 버퍼 크기로 구성됩니다. Bitmap::setCache를 호출해서 이 두 값을 TouchGFX에 제공해야 합니다. 이 함수 호출은 일반적으로 FrontendApplication.cpp 파일에서 찾을 수 있습니다.

FrontendApplication.cpp (extract)
#include <gui/common/FrontendApplication.hpp>
#include <touchgfx/Bitmap.hpp>

FrontendApplication::FrontendApplication(Model& m, FrontendHeap& heap)
: FrontendApplicationBase(m, heap)
{
// Place cache start address in SDRAM at address 0xC0008000;
uint16_t* const cacheStartAddr = (uint16_t*)0xC0008000;
const uint32_t cacheSize = 0x300000; //3 MB, as example
Bitmap::setCache(cacheStartAddr, cacheSize);
}

위의 예제에서는 외장 메모리의 3MB 버퍼가 비트맵 캐시로 TouchGFX에 전달됩니다. 주소는 애플리케이션 프로그래머가 선택합니다. 다음 예제에서는 배열을 선언한 후 배열의 주소와 크기만 전달합니다. 배열의 구체적인 위치는 링커 스크립트에 따라 달라집니다. 이 메소드는 내장 RAM에서 (작은) 비트맵 캐시를 생성할 때 주로 사용됩니다.

FrontendApplication.cpp (extract)
#include <gui/common/FrontendApplication.hpp>
#include <touchgfx/Bitmap.hpp>

// Define an array for the bitmap cache
uint16_t cache[128*1024]; //256 KB cache

FrontendApplication::FrontendApplication(Model& m, FrontendHeap& heap)
: FrontendApplicationBase(m, heap)
{
Bitmap::setCache(cache, sizeof(cache));
}

TouchGFX Generator로 비트맵 캐시 활성화하기

CubeMX와 TouchGFX Generator를 사용하는 경우, TouchGFXHAL.cpp에서도 비트맵 캐시를 활성화 및 구성할 수 있습니다.

TouchGFXHAL.cpp (extract)
void TouchGFXHAL::initialize()
{
/* Initialize TouchGFX Engine */
TouchGFXGeneratedHAL::initialize();

uint16_t* cacheStartAddr = (uint16_t*)0xC0008000;
uint32_t cacheSize = 0x300000; //3 MB, as example

touchgfx::Bitmap::setCache(cache, sizeof(cache));
}

캐시를 다시 사용하고 싶다면 새로운 캐시를 설정하기 전에 이전 캐시를 삭제해야 합니다. 이때는 touchgfx::Bitmap::removeCache()를 호출하면 삭제가 가능합니다. 애플리케이션에서 한 번만 설정한 경우에는 캐시를 삭제할 필요가 없습니다.

모든 비트맵을 캐싱해야 한다면 당연히 캐시의 크기도 비트맵 데이터를 모두 저장할 수 있을 만큼 커야 합니다. 하지만 기록에 사용할 수 있는 메모리 용량(8바이트 x 애플리케이션의 비트맵 수)은 많지 않습니다. 따라서 실제로 원시 픽셀 데이터에 필요한 용량보다 약간 더 많은 메모리를 할당해야 합니다. 이 용량은 애플리케이션의 비트맵 수에 따라 다르지만, 보통 몇 킬로바이트의 메모리만 추가로 사용해도 충분합니다.

플래시에서 캐시로 데이터를 복사하는 BlockCopy

비트맵을 캐싱하면 TouchGFX가 HAL 클래스의 BlockCopy 함수를 사용해 픽셀을 원본 위치에서 비트맵 캐시로 복사합니다.

비트맵을 주소 지정이 가능한 일반 플래시(내장 플래시 또는 QSPI-플래시와 같은 메모리 매핑 외장 플래시)에 저장하는 경우에는 아무 작업도 할 필요가 없습니다. 그 이유는 내장된 구현체만으로도 충분하기 때문입니다.

반면, 비트맵을 파일 시스템이나 비 메모리 매핑 플래시와 같이 주소 지정이 불가능한 플래시에 저장하는 경우에는 표준 복사 메소드만으로는 부족하기 때문에 특정 플래시 저장 장치에서 데이터를 읽어올 수 있는 업데이트 버전을 제공해야 합니다.

자세한 내용은 비 메모리 매핑 플래시를 사용해 이미지 저장하기 섹션을 참조하십시오.

캐시 연산

비트맵 캐싱 연산은 모두 Bitmap 클래스에 있습니다.

Bitmap 캐싱 메소드설명
bool Bitmap::cache(BitmapId id)이 메소드는 비트맵을 캐싱합니다. 비트맵은 캐시에서 미사용 메모리가 충분한 경우에만 캐싱됩니다. 비트맵이 캐싱되면 true를 반환합니다. 이미 캐싱된 비트맵을 캐싱하면 아무 효과도 일어나지 않습니다.
bool Bitmap::cacheReplaceBitmap(BitmapId out, BitmapId in)이 메소드는 캐시에 저장된 비트맵을 다른 비트맵으로 대체합니다. 단, 대체할 비트맵이 이미 캐싱되어 있는 상태에서 크기(바이트)가 동일해야만 가능합니다.
This method replaces a bitmap (out) in the cache with another bitmap (in).이 메소드는 캐시에서 비트맵을 삭제합니다. 삭제된 비트맵에서 사용했던 메모리는 이후 다른 비트맵을 캐싱하는 데 사용됩니다.
void Bitmap::clearCache()이 메소드는 캐싱된 비트맵을 캐시에서 모두 삭제합니다.
void Bitmap::cacheAll()이 메소드는 모든 비트맵을 캐싱합니다. 단 캐시에 할당된 RAM 용량이 전체 비트맵 크기보다 작으면 이 메소드를 사용할 수 없습니다.

캐시 전략

비트맵 캐시에 할당되는 RAM 용량이 총 비트맵 크기보다 작으면 시작 시 모든 비트맵을 캐싱할 수 없습니다. 이때는 첫 번째 스크린에 필요한 비트맵만 캐싱하는 등 다른 방법을 선택할 수 있습니다. 스크린을 변경할 경우에는 캐싱된 비트맵을 일부 또는 모두 삭제한 후 다음 스크린에 필요한 비트맵을 캐싱할 수 있습니다. 이 내용은 다음 섹션에서 예를 들어 설명하겠습니다.

스크린 기준 비트맵 캐싱

애플리케이션 사용자 인터페이스는 View들의 세트로 구성되어 있습니다. 각 View 마다 비트맵을 사용할 가능성이 높습니다. 이러한 경우에 단순한 캐싱 전략은 View::setupScreen 메소드를 사용해 View에서 사용할 비트맵을 모두 캐싱하고, View::tearDownScreen 메소드를 사용해 캐시를 소거하는 것입니다.

Screen1View.cpp (extract)
void Screen1View::setupScreen()
{
//ensure background is cached
Bitmap::cache(BITMAP_SCREEN2_ID);
//cache some icons
Bitmap::cache(BITMAP_ICON10_ID);
Bitmap::cache(BITMAP_ICON11_ID);
Bitmap::cache(BITMAP_ICON12_ID);
}

void Screen1View::tearDownScreen()
{
//Remove all bitmaps from the cache
Bitmap::clearCache();
}

캐시에 할당되는 메모리 요건으로는 스크린에서 사용할 비트맵의 크기와 비트맵의 주요 용도입니다. 이 메소드는 다음과 같은 단점이 있습니다. 예를 들어 두 개의 View 모두 비트맵을 사용할 경우 첫 번째 View에서 나갈 때 해당 비트맵이 캐시에서 삭제되고, 두 번째 View로 전환하면 다시 캐싱됩니다.

이때는 Bitmap::cacheRemoveBitmap을 사용해 비트맵을 선별적으로 언캐싱하여 이러한 오버헤드를 줄일 수 있습니다. cacheRemoveBitmap에서는 캐시 메모리가 파편화된다는 단점이 있습니다.

그 밖에 UI를 변경하면(버튼 추가 등) 캐싱 코드를 업데이트하여 새 비트맵을 추가해야 하는 단점이 있습니다.

캐시에서 배경 비트맵 대체하기

애플리케이션에 소형 비트맵(아이콘 등) 세트와 전체 화면 “배경” 비트맵이 포함되어 있으면 다음과 같이 다른 전략을 고려할 수 있습니다.

먼저 첫 번째 스크린을 시작하기 전에 작은 비트맵을 모두 캐싱합니다. 이때는 FrontendApplication 생성자가 적합합니다. 또한 첫 번째 스크린에 사용할 배경 비트맵도 캐싱합니다.

FrontendApplication::FrontendApplication(Model& m, FrontendHeap& heap)
: touchgfx::MVPApplication(),
transitionCallback(),
frontendHeap(heap),
model(m)
{
//cache some icons
Bitmap::cache(BITMAP_ICON10_ID);
Bitmap::cache(BITMAP_ICON11_ID);
Bitmap::cache(BITMAP_ICON12_ID);

//cache first background
Bitmap::cache(BITMAP_SCREEN1_ID);
backgroundBitmapCached = BITMAP_SCREEN1_ID; //remember ID in a variable
}

아래 View::setupScreen 메소드에서 캐싱된 배경 비트맵을 필요한 비트맵으로 대체합니다.

Screen1View::setupScreen()
{
//ensure background is cached
Bitmap::cacheReplaceBitmap(backgroundBitmapCached, BITMAP_SCREEN1_ID);
backgroundBitmapCached = BITMAP_SCREEN1_ID; //remember new ID of cached bitmap
}
void Screen1View::tearDownScreen()
{
//nothing cache related
}

이러한 전략을 사용할 때는 캐싱된 비트맵의 크기와 배경 비트맵에 따라 캐시에 할당되는 메모리가 결정됩니다. 이전 방법에 비해 View의 코드가 짧기 때문에 코드가 단순해집니다. 캐시에서 추가하거나 삭제할 비트맵이 줄어들기 때문에 성능도 향상됩니다.

메모리 파편화 현상도 없기 때문에 cacheReplaceBitmap 연산이 cacheRemoveBitmap 메소드보다 많이 사용됩니다.

캐시 메모리 관리

비트맵 캐싱을 최대한 활용하려면 캐시의 내부 연산 방식을 이해해야 합니다.

캐시는 스택 형태로 구현됩니다. 이전에 캐싱된 비트맵에 이어 새로운 비트맵이 캐싱됩니다. 비트맵이 캐시에서 삭제되면 해당 비트맵에서 사용했던 메모리는 "free"로 표시되지만, 삭제된 비트맵이 스택 최상단에 도달해야만 해당 메모리를 즉시 사용할 수 있습니다. 따라서 비트맵이 캐시 "중간"에 있는 경우에는 다음 번에 Bitmap::cache를 호출할 때 압축 연산이 실행되어 해당 메모리를 회수합니다. 캐시에 “미사용 메모리”가 있을 때 Bitmap::cache를 호출하지 않는다면 이렇게 “손실이 큰” 방법을 피할 수 있습니다.

아래 그림은 이러한 원리를 나타낸 것입니다.

  1. 캐싱하면 이전에 할당된 비트맵 위에 할당됩니다.

메모리의 비트맵 할당 순서

  1. 삭제하여 미사용 메모리라고 표시됩니다.

캐싱된 비트맵 삭제 후 캐시에 남은 미사용 메모리

  1. 다음 비트맵을 할당하면 캐시가 압축되면서 최상단에 할당됩니다.

비트맵 캐싱 이전에 캐시에서 미사용 메모리를 회수함

  1. 최상단의 (마지막에 할당한) 비트맵을 삭제하면 바로 아래에 있던 여유 메모리와 함께 해당 메모리가 바로 회수됩니다.

최상단 비트맵 캐시 삭제

이때는 다음 캐시 연산에서 압축이 수행되지 않습니다.

아래 애니메이션은 이러한 코드가 실행되는 전체 순서를 나타낸 것입니다.

Bitmap::cache(BITMAP_BITMAP1_ID);
Bitmap::cache(BITMAP_BITMAP2_ID);
Bitmap::cache(BITMAP_BITMAP3_ID);
...
Bitmap::cacheRemoveBitmap(BITMAP_BITMAP2_ID);
...
Bitmap::cache(BITMAP_BITMAP4_ID);
...
Bitmap::cacheRemoveBitmap(BITMAP_BITMAP3_ID);
Bitmap::cacheRemoveBitmap(BITMAP_BITMAP4_ID);

비트맵 캐싱 및 캐싱 해제