iOS · Rendering · Performance
GPU 렌더링 · 오프스크린 · VSync Q&A
CPU/GPU 렌더링 파이프라인, 오프스크린 렌더링, VSync와 프레임 드랍 이해
1. 오프스크린 렌더링이란?
GPU가 화면에 바로 합성하지 못하고 별도 버퍼에 미리 그린 뒤 다시 합성해야 하는 경우. 컨텍스트 전환 비용이 커서 스크롤에서 특히 치명적.
정상 렌더링 (On-screen)
Layer 1 ─┐
Layer 2 ─┼─→ GPU Frame Buffer → 화면
Layer 3 ─┘
한 번에 합성
오프스크린 렌더링
Layer 1 ─→ [별도 버퍼] ─┐
├─→ Frame Buffer → 화면
Layer 2, 3 ────────────┘
2단계 (버퍼 전환 비용 발생)
왜 오프스크린이 필요한가?
마스킹 (cornerRadius + clipsToBounds):
이미지를 먼저 그리고 → 모서리를 잘라야 함 → "그린 다음 자르기" = 별도 버퍼 필요
그림자 (shadowPath 없을 때):
그림자 모양 = View 모양 기반 → View를 먼저 그려서 모양을 알아야 그림자 계산 가능 → 별도 버퍼 필요
왜 스크롤에서 치명적인가?
60fps = 16.67ms마다 새 프레임
정상: [합성] 5ms ────────── 여유 11ms ✓
오프스크린: [버퍼1] 8ms + [버퍼2] 6ms + [합성] 5ms = 19ms
↑
16.67ms 초과 = 프레임 드랍!
오프스크린 피하는 방법
| 원인 | 해결 |
|---|---|
cornerRadius + clipsToBounds | layer.shouldRasterize = true (캐싱) |
shadow (path 없음) | shadowPath 지정 |
mask | 가능하면 이미지로 대체 |
shadowPath 예시:
// ❌ 오프스크린 발생 layer.shadowOpacity = 0.5 layer.shadowRadius = 4 // ✅ 오프스크린 없음 layer.shadowOpacity = 0.5 layer.shadowRadius = 4 layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 12).cgPath
디버깅: Simulator → Debug → Color Off-screen Rendered (노란색 = 오프스크린)
2. CPU/GPU 렌더링 파이프라인
Q: GPU 렌더는 동기적으로 기다리지 않더라도 늦게 처리되면 문제인가?
A: 네. CPU가 기다리냐 아니냐 문제가 아니라, VSync 데드라인(16.67ms)을 GPU가 못 맞추면 프레임 드랍
Q: Run Loop와 GPU VSync는 별개인가?
A: 별개 프로세스지만 VSync 신호에 동기화됨. 둘 다 16.67ms 주기
전체 흐름
VSync 신호 (디스플레이 하드웨어, 매 16.67ms)
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Run Loop (CPU) │ │ Render Server (GPU) │
│ 앱 프로세스 │ │ 별도 시스템 프로세스 │
│ │ │ │
│ 1. Event 처리 │ │ │
│ 2. layoutSubviews │ │ │
│ 3. draw(_:) │ │ │
│ 4. commit ─────────│─────→│ 렌더링 & 합성 │
│ │ │ → 화면에 표시 │
└─────────────────────┘ └─────────────────────┘
순서 주의! layoutSubviews → draw → commit (commit이 마지막)
| Run Loop (CPU) | Render Server (GPU) | |
|---|---|---|
| 위치 | 앱 프로세스 | 별도 시스템 프로세스 |
| 트리거 | CADisplayLink (VSync 기반) | VSync |
| 주기 | ~16.67ms | ~16.67ms |
| 하는 일 | 레이아웃, 드로우, commit | 합성, 화면 출력 |
3. CPU/GPU 파이프라인 동작
CPU와 GPU는 파이프라인으로 동시에 다른 프레임 작업:
시간 → 0ms 16.67ms 33.33ms 50ms
│ │ │ │
▼ ▼ ▼ ▼
VSync VSync VSync VSync
CPU: [Frame 1] [Frame 2] [Frame 3]
layout layout layout
draw draw draw
commit commit commit
│ │ │
▼ ▼ ▼
GPU: ─────[Frame 1] [Frame 2] [Frame 3]
렌더링 렌더링 렌더링
│ │ │
▼ ▼ ▼
화면: ─────[Frame 1] [Frame 2] [Frame 3]
핵심: CPU와 GPU가 각각 16.67ms 안에 끝내야 60fps 유지
4. CPU가 16.67ms 초과하면?
Q: CPU 작업이 다 끝나지 못하면 다음 16.67ms에서 마무리?
A: 네, CPU는 계속 작업. 그동안 GPU는 commit이 없어서 대기
Q: 그럼 화면은?
A: 이전 프레임 반복 = 버벅임
정상:
0ms 16.67ms 33.33ms
│ │ │
CPU: [Frame 1] [Frame 2] [Frame 3]
└commit └commit
GPU: [Frame 1] [Frame 2]
화면: F1 F2 ← 매 VSync 새 프레임
CPU 느릴 때:
0ms 16.67ms 33.33ms
│ │ │
CPU: [Frame 1──────────────] [Frame 2]
아직 작업중 └commit
GPU: [할 게 없음...] [Frame 1]
화면: F0(이전) F0(반복) F1
↑ ↑
프레임 드랍! 2번 반복됨
| 상황 | CPU | GPU | 화면 |
|---|---|---|---|
| CPU 16.67ms 초과 | 계속 작업 | commit 없어서 대기 | 이전 프레임 반복 |
| commit 되면 | 다음 프레임 시작 | 이제 렌더링 | 다음 VSync에 표시 |
결론: CPU가 commit 안 하면 → GPU는 할 일이 없음 → 화면은 이전 프레임 반복 = 버벅임
핵심 요약
- 오프스크린 렌더링: 별도 버퍼에 그린 뒤 합성 → 컨텍스트 전환 비용 → 스크롤에서 치명적
- shadowPath 지정하면 오프스크린 피할 수 있음
- VSync: 16.67ms마다 화면 갱신 신호, CPU/GPU 모두 이에 맞춰 동작
- CPU/GPU 파이프라인: 각각 16.67ms 안에 끝내야 60fps
- CPU 느리면: commit 지연 → GPU 대기 → 이전 프레임 반복 = 프레임 드랍