Android NativeActivity에서 프레임레이트 제어 구현

NativeActivity 기반 앱의 화면 주사율 관리

Android NDK를 활용한 그래픽 애플리케이션 개발 시 화면 갱신 주기를 정밀하게 제어하는 것은 성능 최적화의 핵심입니다. 특히 게임 엔진이나 미디어 플레이어처럼 일정한 프레임 간격이 중요한 경우, setFrameRate API를 활용한 명시적인 주사율 설정이 필요합니다.

구현 단계

단계작업 내용
1NDK 프로젝트 구성 및 NativeActivity 초기화
2ANativeWindow를 통한 프레임레이트 설정
3정밀한 타이밍 제어가 포함된 렌더링 루프 구현
4SurfaceFlinger 동기화 및 성능 검증

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 백프레스: ASurfaceTransaction API로 추가 동기화 가능

태그: Android NDK NativeActivity ANativeWindow Frame Rate Control SurfaceFlinger

7월 3일 03:28에 게시됨