NativeActivity 기반 앱의 화면 주사율 관리
Android NDK를 활용한 그래픽 애플리케이션 개발 시 화면 갱신 주기를 정밀하게 제어하는 것은 성능 최적화의 핵심입니다. 특히 게임 엔진이나 미디어 플레이어처럼 일정한 프레임 간격이 중요한 경우, setFrameRate API를 활용한 명시적인 주사율 설정이 필요합니다.
구현 단계
| 단계 | 작업 내용 |
|---|---|
| 1 | NDK 프로젝트 구성 및 NativeActivity 초기화 |
| 2 | ANativeWindow를 통한 프레임레이트 설정 |
| 3 | 정밀한 타이밍 제어가 포함된 렌더링 루프 구현 |
| 4 | SurfaceFlinger 동기화 및 성능 검증 |
1단계: 프로젝트 설정
AndroidManifest.xml에서 하드웨어 가속과 네이티브 라이브러리를 선언합니다:
<manifest>
<application android:hasCode="false">
<activity android:name="android.app.NativeActivity"
android:exported="true">
<meta-data android:name="android.app.lib_name"
android:value="game_engine" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
2단계: 프레임레이트 설정 API 활용
Android 11(API 30)부터 제공되는 ANativeWindow_setFrameRate를 사용하여 디스플레이와의 동기화를 설정합니다:
#include <android/native_window.h>
#include <android/native_activity.h>
#include <android/log.h>
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "DisplaySync", __VA_ARGS__))
typedef enum {
REFRESH_RATE_DEFAULT = 0,
REFRESH_RATE_60HZ = 60,
REFRESH_RATE_90HZ = 90,
REFRESH_RATE_120HZ = 120
} DisplayRefreshRate;
bool configureDisplaySync(ANativeActivity* nativeActivity, DisplayRefreshRate desiredRate) {
if (!nativeActivity) {
LOGI("Invalid activity handle");
return false;
}
ANativeWindow* displaySurface = ANativeActivity_getWindow(nativeActivity);
if (!displaySurface) {
LOGI("Surface acquisition failed");
return false;
}
// ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT:
// 시스템이 자동으로 최적의 모드를 선택
int compatibility = ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT;
int changeBehavior = ANATIVEWINDOW_CHANGE_FRAME_RATE_ALWAYS;
int result = ANativeWindow_setFrameRate(displaySurface,
(float)desiredRate,
compatibility,
changeBehavior);
if (result == 0) {
LOGI("Display synchronized to %d Hz", desiredRate);
return true;
}
LOGI("Frame rate configuration failed: %d", result);
return false;
}
3단계: 고정 간격 렌더링 루프
CPU 기반 스핀 대기가 아닌 하이브리드 방식으로 정밀한 프레임 타이밍을 구현합니다:
#include <chrono>
#include <thread>
#include <atomic>
struct FramePacer {
std::atomic<bool> running{true};
std::chrono::nanoseconds targetInterval;
std::chrono::steady_clock::time_point previousTimestamp;
};
void executeRenderPipeline(ANativeActivity* activity, int fpsTarget) {
FramePacer pacer;
pacer.targetInterval = std::chrono::nanoseconds(1000000000LL / fpsTarget);
pacer.previousTimestamp = std::chrono::steady_clock::now();
// 초기 디스플레이 설정
configureDisplaySync(activity, static_cast<DisplayRefreshRate>(fpsTarget));
while (pacer.running.load()) {
auto currentFrameStart = std::chrono::steady_clock::now();
// === 렌더링 작업 수행 ===
processInputEvents();
updateSimulationState();
submitDrawCommands();
// =======================
// 다음 프레임 시작까지 남은 시간 계산
auto elapsedWork = currentFrameStart - pacer.previousTimestamp;
auto sleepDuration = pacer.targetInterval - elapsedWork;
if (sleepDuration > std::chrono::milliseconds(2)) {
// 짧은 수면 + 스핀 대기로 정밀도 확보
std::this_thread::sleep_for(sleepDuration - std::chrono::milliseconds(1));
}
// 바쁜 대기로 마이크로초 단위 정밀도 보정
while (std::chrono::steady_clock::now() - pacer.previousTimestamp < pacer.targetInterval) {
std::this_thread::yield();
}
pacer.previousTimestamp = currentFrameStart;
}
}
4단계: 진입점 및 생명주기 통합
#include <android/native_app_glue/android_native_app_glue.h>
void handleAppLifecycle(struct android_app* application, int32_t command) {
static ANativeActivity* cachedActivity = nullptr;
switch (command) {
case APP_CMD_INIT_WINDOW:
cachedActivity = application->activity;
// 표면 준비 완료, 렌더링 시작 가능
break;
case APP_CMD_TERM_WINDOW:
// 표면 파괴, 렌더링 중지
break;
case APP_CMD_GAINED_FOCUS:
if (cachedActivity) {
configureDisplaySync(cachedActivity, REFRESH_RATE_120HZ);
}
break;
case APP_CMD_LOST_FOCUS:
// 백그라운드 전환 시 30Hz로 절전 모드
if (cachedActivity) {
configureDisplaySync(cachedActivity, REFRESH_RATE_DEFAULT);
}
break;
}
}
void android_main(struct android_app* state) {
state->onAppCmd = handleAppLifecycle;
// 메시지 폴링과 렌더링 병행
int pollResult;
int pollEvents;
struct android_poll_source* pollSource;
while (true) {
pollResult = ALooper_pollOnce(0, nullptr, &pollEvents,
(void**)&pollSource);
if (pollResult == ALOOPER_POLL_CALLBACK && pollSource) {
pollSource->process(state, pollSource);
}
if (state->destroyRequested) {
break;
}
// 실제 렌더링은 별도 스레드에서 수행하거나
// 여기서 조건부 실행
if (state->window) {
executeRenderPipeline(state->activity, 60);
}
}
}
고려사항
- 가변 주사율 디스플레이: 120Hz/90Hz 패널에서
ANativeWindow_getRefreshRate로 실제 지원 주사율을 확인 - 배터리 최적화:
ANATIVEWINDOW_CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS로 전환 시 깜빡임 방지 - SurfaceFlinger 백프레스:
ASurfaceTransactionAPI로 추가 동기화 가능