Android Swappy FPS 고착으로 느린 세션 비율이 높아진 문제 해결
- 환경: Unity 6.x, Android (모든 버전), Samsung Galaxy (모든 One UI 버전)
- 해결 방법: FPS 값을 Swappy 호환 값으로 변경
문제 요약
섹션 제목: “문제 요약”Google Play Console의 Android Vitals 렌더링 지표에서 앱의 느린 세션 비율이 동종 앱 중앙값보다 높게 나타났다.
- Samsung Game Booster의 터치 방지 기능을 쓴 뒤 FPS가 30에서 복구되지 않는다.
- FPS가 30과 45 사이를 오가며 불안정하다.
- Android Vitals에서 Android 16 (SDK 36) 기기의 Slow Session 비율이 급증했다.
영향 범위
섹션 제목: “영향 범위”| 환경 | 영향 | 비고 |
|---|---|---|
| 모든 Android 버전 | 잠재적 영향 | Swappy 사용 시 비호환 FPS를 설정하면 발생 |
| 모든 One UI 버전 | 잠재적 영향 | Game Booster 터치 방지 기능과 충돌 |
| Unity Optimized Frame Pacing 사용 프로젝트 | 해당 | Swappy 라이브러리 자동 활성화 |
조사 과정
섹션 제목: “조사 과정”느린 세션으로 잡힌 지표의 특징(모두 삼성 갤럭시 기기, Android 16 기기에 몰림)을 바탕으로 AI 분석으로 가설을 세우고, 의심스러운 것부터 하나씩 테스트해 소거했다.
| 가설 | 확인 방법 | 결과 |
|---|---|---|
| 1. Optimized Frame Pacing(Swappy) 자체의 문제 | 옵션을 끈 빌드로 테스트 | 재현되지 않음. 다만 Swappy를 끄는 것은 렌더링 품질을 희생하는 타협이라 해결책이 아니다. |
| 2. Adaptive Performance 대신 만든 Custom AdaptiveFPS 로직의 문제 | 배터리 절약 모드에서 동작 관찰 | 절약 모드는 앱을 Pause하지 않아 Reset이 호출되지 않지만, 5초 주기 검사가 FPS 상태를 정상으로 관리했다. 정상 동작 |
| 3. Graphics API(Vulkan, OpenGLES3) 호환성 문제 | 사용 중인 API 확인 | 이미 Vulkan을 쓰지 않음. 기각 |
| 4. OS가 앱을 게임으로 인식하지 못하는 문제 | Game Booster에 앱을 게임으로 직접 등록 | 같은 문제 재현. 가능성 낮음 |
| 5. Unity와 Android 버전 호환성 문제 | 다음 배포용 Unity 6000.3.4f1로 빌드 | 같은 문제 재현. 이 과정에서 Unity 이슈 트래커의 60Hz 기기에서 45Hz 목표 프레임을 설정할 수 없는 이슈를 찾았다. |
가설 1(Frame Pacing이 관여)과 가설 5(45 FPS 설정 불가)의 단서로 코드를 다시 보니, Custom AdaptiveFPS 로직이 Quality.Mid에서 45 FPS를 요청하고 있었다.
원인 분석
섹션 제목: “원인 분석”1. Android Swappy (Frame Pacing Library)란?
섹션 제목: “1. Android Swappy (Frame Pacing Library)란?”Unity의 Optimized Frame Pacing 옵션을 켜면 Android에서 Swappy 라이브러리를 쓴다.
Player Settings → Resolution and Presentation → Optimized Frame Pacing ✅Swappy는 Google Android Game Development Kit(AGDK)의 일부로, 프레임 타이밍을 최적화해 화면 출력을 부드럽게 만든다.
Optimized Frame Pacing ON vs OFF 비교
섹션 제목: “Optimized Frame Pacing ON vs OFF 비교”| 항목 | ON (Swappy 활성화) | OFF (Swappy 비활성화) |
|---|---|---|
| FPS 제어 방식 | Swappy가 Refresh Rate 약수로 강제 변환 | Unity 기본 프레임 제한 (VSync는 항상 ON) |
| 45 FPS 설정 시 | 60Hz→30fps, 120Hz→40fps로 변환 | 45fps 그대로 시도 (프레임 타이밍 불균일) |
| 프레임 일관성 | 프레임 간격 균일 | 프레임 간격 불균일 가능 |
| 배터리 효율 | 불필요한 화면 갱신 방지 | 불필요한 화면 갱신 발생 |
| FPS 고착 문제 | 발생 가능 | 발생하지 않음 |
OFF로 설정하면 왜 재현되지 않는가
섹션 제목: “OFF로 설정하면 왜 재현되지 않는가”Optimized Frame Pacing OFF 시:
- Swappy 라이브러리가 비활성화된다.
Application.targetFrameRate값이 Android 기본 VSync 메커니즘으로 처리된다.- Swappy의 Divisor 알고리즘이 적용되지 않아 FPS 강제 변환이 일어나지 않는다.
- Game Booster가 Refresh Rate를 바꿔도 Swappy의 swap interval 고착이 없다.
그래도 OFF를 권장하지 않는 이유:
- 프레임 타이밍이 불균일해져 화면 끊김(jittering)이 생길 수 있다.
- 배터리 소모가 늘어난다 (120Hz 디스플레이에서 60fps 렌더링 시 불필요한 갱신).
2. Swappy의 Divisor 알고리즘
섹션 제목: “2. Swappy의 Divisor 알고리즘”Swappy는 Refresh Rate의 정수 약수인 FPS만 허용한다.
허용 FPS = Refresh Rate / 정수| Refresh Rate | 허용 FPS (정수 약수) |
|---|---|
| 60Hz | 60, 30, 20, 15, 12, 10… |
| 120Hz | 120, 60, 40, 30, 24, 20… |
3. 문제의 45 FPS
섹션 제목: “3. 문제의 45 FPS”기존 코드는 Mid Quality를 45 FPS로 설정했다.
public int GetFrameRate(Quality quality){ if (quality == Quality.Mid) return 45; // ❌ 문제! else if (quality >= Quality.High) return 60; return 30;}45 FPS는 Swappy 비호환이다.
60Hz / 45 = 1.33... (정수 아님!) ❌120Hz / 45 = 2.67... (정수 아님!) ❌Swappy는 비호환 FPS 요청을 받으면 가장 가까운 유효 약수로 자동 변환한다.
| Refresh Rate | 요청 FPS | Swappy 변환 결과 | 이유 |
|---|---|---|---|
| 60Hz | 45 FPS | 30 FPS | 가장 가까운 약수 (60/2) |
| 120Hz | 45 FPS | 40 FPS | 가장 가까운 약수 (120/3) |
One UI 5, 120Hz 기기에서 테스트한 결과는 다음과 같다.
targetFrameRate = 45로 설정하면 실제 FPS가 40 FPS로 측정된다.- Swappy가 120Hz에서 유효한 약수인 40 FPS(120/3=40)로 자동 변환했기 때문이다.
- 60Hz 기기에서는 같은 설정이 30 FPS로 변환된다(60/2=30).
4. Samsung One UI 7+ 변경사항
섹션 제목: “4. Samsung One UI 7+ 변경사항”One UI 6 이하
- Game Booster에 48Hz 옵션이 있다.
- 45 FPS가 48Hz 모드에서 부분적으로 동작한다.
One UI 7 (Android 15) / One UI 8 (Android 16)
- 48Hz 옵션이 제거됐다.
- 60Hz 또는 120Hz만 선택할 수 있다.
- 45 FPS는 60Hz와 120Hz 어느 쪽의 정수 약수도 아니다.
5. Game Booster 터치 방지 시나리오
섹션 제목: “5. Game Booster 터치 방지 시나리오”1. 정상 상태: 60Hz, 60 FPS (Divisor = 1) ↓2. 터치 방지 활성화: Game Center가 절전 모드 적용 ↓3. 시스템이 30Hz/30 FPS로 강제 변경 ↓4. Swappy 내부 swap interval = 33.3ms (30 FPS) ↓5. 터치 방지 해제: 시스템은 60Hz로 복귀 ↓6. 하지만 Swappy의 swap interval은 33.3ms로 "고착" ↓7. targetFrameRate = 60 설정해도 실제 30 FPS 유지 ❌해결 방법
섹션 제목: “해결 방법”FPS 값을 Swappy 호환 값으로 변경
섹션 제목: “FPS 값을 Swappy 호환 값으로 변경”변경 전 (문제)
// High: 60, Mid: 45, Low: 30public int GetFrameRate(Quality quality){ if (quality == Quality.Mid) return 45; // ❌ Swappy 비호환 else if (quality >= Quality.High) return 60; return 30;}변경 후 (해결)
// High: 60, Mid: 30, Low: 20public int GetFrameRate(Quality quality){ if (quality == Quality.Mid) return 30; // ✅ 60Hz/2, 120Hz/4 else if (quality >= Quality.High) return 60; // ✅ 60Hz/1, 120Hz/2 return 20; // ✅ 60Hz/3, 120Hz/6}Swappy 호환 FPS 검증표
섹션 제목: “Swappy 호환 FPS 검증표”| Quality | FPS | 60Hz Divisor | 120Hz Divisor | 결과 |
|---|---|---|---|---|
| High | 60 | 60/60 = 1 | 120/60 = 2 | 호환 |
| Mid | 30 | 60/30 = 2 | 120/30 = 4 | 호환 |
| Low | 20 | 60/20 = 3 | 120/20 = 6 | 호환 |
테스트 환경
섹션 제목: “테스트 환경”- One UI 5 (Android 13): 문제 해결 확인
- One UI 8 (Android 16, Galaxy S25): 문제 해결 확인
- 터치 방지 해제 후 FPS가 정상 복구된다.
참고 자료
섹션 제목: “참고 자료”공식 문서
섹션 제목: “공식 문서”- Android Frame Pacing Library (Swappy)
- Unity Optimized Frame Pacing
- Android Refresh Rate Optimization
Samsung 관련
섹션 제목: “Samsung 관련”알려진 이슈
섹션 제목: “알려진 이슈”- Swappy는 정수 약수 FPS만 허용한다
- 45, 50 같은 비표준 FPS는 Swappy가 가장 가까운 약수로 바꿔, 요청한 값과 실제 FPS가 달라진다.
- One UI 7+에서 48Hz 옵션이 제거됐다
- One UI 6 이하의 48Hz 모드에서 부분적으로 동작하던 45 FPS 설정이 One UI 7 이상에서는 맞는 모드가 없다.
- 디바이스 Refresh Rate를 반드시 고려한다
- 60Hz와 120Hz 모두에서 정수 약수가 되는 FPS를 고른다.
- Android Vitals를 모니터링한다
- 특정 OS 버전에서 급증하는 문제는 플랫폼 변경을 의심한다.
체크리스트
섹션 제목: “체크리스트”프로젝트의 Swappy 호환성을 확인한다.
-
Optimized Frame Pacing을 사용 중인가? - 모든 FPS 설정값이 60Hz/120Hz의 정수 약수인가?
- 45, 50 같은 비표준 FPS를 쓰고 있지 않은가?
- Android 16 (One UI 8) 기기에서 테스트했는가?
- Game Booster 터치 방지 시나리오를 테스트했는가?