티어링
고친 사람 github-actions[bot]
티어링은 한 화면에 앞 장면과 다음 장면이 위아래로 섞여 보이는 고장입니다. 모니터가 화면을 칠하는 도중에 그림이 바뀌면 화면이 가로로 찢어진 것처럼 보입니다. 저장장치 분야에서는 데이터를 자주 쓰는 것과 덜 쓰는 것으로 갈라 다른 저장장치에 두는 일도 티어링이라 부릅니다. 이 편은 화면 쪽 뜻을 다룹니다.
쉽고 빠른 이해
티어링은 화면이 가로로 찢어져 보이는 고장입니다. 게임에서 시점을 옆으로 빠르게 돌리면 화면 중간에 가로선이 생깁니다. 선 위의 물체와 선 아래의 물체가 옆으로 어긋나 보입니다.
이 고장이 문제인 까닭은 움직임이 매끄럽게 안 보이기 때문입니다. 영상이나 스크롤처럼 부드럽게 흘러야 하는 화면에서 특히 눈에 거슬립니다.
어떻게 생기나:
- 모니터는 그림을 한 번에 바꾸지 않고 윗줄부터 한 줄씩 칠합니다
- 모니터가 화면 중간쯤을 칠할 때 프로그램이 새 장면을 내보냅니다
- 남은 아랫줄은 새 장면으로 칠해져서 경계에 가로선이 남습니다
막는 법과 그 대가 — 그림을 바꾸는 때를 모니터가 맨 위로 돌아가는 순간까지 미루면 찢김이 사라집니다. 대신 그만큼 기다려야 해서 화면 반응이 늦어집니다.
상세
이 절은 모니터가 화면을 칠하는 방식에서 출발합니다. 그 칠하기와 그림이 바뀌는 순간이 어긋날 때 무엇이 보이는지, 어떤 조건이 겹쳐야 재현되는지를 차례로 봅니다.
모니터가 화면을 칠하는 방식
모니터는 화면 전체를 한 번에 바꾸지 않습니다. 맨 윗줄부터 한 줄씩, 한 줄 안에서는 왼쪽에서 오른쪽으로 픽셀 색을 새로 칠합니다. 마지막 줄까지 칠하면 맨 위로 돌아가 다시 시작합니다.
칠할 색은 프레임버퍼에서 옵니다. 프레임버퍼는 화면의 픽셀마다 색 값을 하나씩 담아 둔 메모리입니다. 프로그램이 그린 그림은 이 메모리에 놓여야 화면에 나옵니다.
프레임버퍼를 읽어 모니터로 보내는 일은 디스플레이 컨트롤러라는 회로가 맡습니다. 프로그램은 메모리에 그림을 그리기만 합니다. 그 그림을 모니터 박자에 맞춰 내보내는 것은 이 회로의 일입니다.
스캔아웃(scanout)은 이 회로가 메모리를 윗줄부터 한 줄씩 읽어 내보내는 일입니다. 모니터가 윗줄부터 칠하는 것은 스캔아웃이 윗줄부터 읽기 때문입니다.
모니터가 1초에 화면 전체를 몇 번 새로 칠하는지를 주사율(refresh rate)이라고 합니다. 단위는 Hz(hertz, 헤르츠)입니다. 60 Hz 모니터는 1초에 60번 칠하므로 한 번 칠하는 데 60분의 1초가 걸립니다.
마지막 줄을 칠한 뒤 맨 윗줄로 돌아가기 전에는 아무 줄도 칠하지 않는 짧은 틈이 있습니다. 이 틈을 수직 귀선 구간(vertical blanking interval)이라고 부릅니다. 이 틈에 그림이 바뀌면 다음 칠하기는 처음부터 끝까지 새 그림 하나만 읽습니다.
칠하는 도중에 그림이 바뀔 때
프로그램은 모니터와 다른 박자로 돕니다. 한 장면을 다 그리는 대로 화면에 내보냅니다. 한 장면을 그리는 데 걸리는 시간은 장면마다 달라서 모니터 박자와 맞으라는 법이 없습니다.
모니터가 화면 중간쯤을 칠하고 있을 때 프레임버퍼가 새 장면으로 바뀌었다고 해 봅시다. 이미 칠한 위쪽 줄에는 앞 장면이 남습니다. 아직 안 칠한 아래쪽 줄은 새 장면으로 칠해집니다.
sequenceDiagram
participant 프로그램
participant 버퍼 as 프레임버퍼
participant 컨트롤러 as 디스플레이 컨트롤러
participant 모니터
컨트롤러->>버퍼: 윗줄부터 읽는다
컨트롤러->>모니터: 윗줄은 앞 장면을 보낸다
Note over 컨트롤러,모니터: 칠하기가 화면 중간까지 왔다
프로그램->>버퍼: 새 장면으로 바꾼다
컨트롤러->>버퍼: 이어서 아랫줄을 읽는다
컨트롤러->>모니터: 아랫줄은 새 장면을 보낸다
Note over 모니터: 위는 앞 장면 · 아래는 새 장면
그림에서 컨트롤러는 프레임버퍼가 바뀌어도 멈추지 않고 이어서 읽습니다. 그래서 한 번 칠한 화면 안에 두 장면이 위아래로 붙습니다. 모니터가 받은 것은 어느 장면도 아닌 섞인 화면입니다.
가로로 찢기는 까닭
찢긴 선은 언제나 가로로 납니다. 모니터가 한 줄을 다 칠한 다음에 아랫줄로 내려가기 때문입니다. 그림이 바뀐 순간 칠하던 줄 언저리가 두 장면의 경계가 됩니다.
경계가 눈에 띄려면 두 장면이 달라야 합니다. 멈춘 화면이면 앞 장면과 새 장면이 같아서 바뀌어도 보이는 것이 없습니다. 물체가 옆으로 움직이면 선 위의 물체와 선 아래의 물체가 옆으로 어긋나 경계가 드러납니다.
그래서 화면이 옆으로 빠르게 흐를수록 티어링이 잘 보입니다. 게임에서 시점을 좌우로 돌리는 장면이나 영상에서 카메라가 옆으로 훑는 장면이 그렇습니다. 세로로 선 기둥이나 글자의 세로획이 선을 경계로 뚝 끊겨 보입니다. 한 물체가 둘로 갈라져 보이므로 움직임이 매끄럽게 이어지지 않습니다.
한 화면에 생기는 찢긴 선의 수
찢긴 선의 수는 모니터가 한 번 칠하는 동안 그림이 몇 번 바뀌었느냐로 정해집니다. 경우를 나누면 아래와 같습니다.
| 한 번 칠하는 동안 그림이 바뀐 횟수 | 보이는 화면 |
|---|---|
| 0번 | 앞 화면이 한 번 더 보인다. 찢김 없음 |
| 1번 · 수직 귀선 구간 안에서 | 새 장면 한 장. 찢김 없음 |
| 1번 · 칠하는 도중에 | 찢긴 선 하나 |
| 여러 번 | 찢긴 선 여럿. 여러 장면이 띠처럼 쌓인다 |
프레임률은 프로그램이 1초에 새 그림을 몇 장 내보내는지를 세는 값입니다. 프레임률이 주사율보다 높으면 표의 넷째 줄이 잦아집니다. 60 Hz 모니터에 1초에 150장을 내보내면 한 번 칠하는 동안 그림이 두세 번씩 바뀝니다.
프레임률이 주사율보다 낮아도 찢김은 생깁니다. 바뀌는 순간이 칠하는 도중이기만 하면 셋째 줄이 됩니다. 프레임률이 낮으면 한 번 칠하는 동안 그림이 안 바뀌는 첫째 줄이 섞여 나올 뿐입니다.
찢긴 선의 높이는 장마다 달라집니다. 그림이 바뀌는 순간이 모니터 박자에 묶여 있지 않아서 어느 줄을 칠할 때 바뀔지가 매번 다르기 때문입니다. 그래서 선이 화면 위아래로 오르내리며 보이기도 합니다.
재현되는 조건
넷이 다 서면 티어링이 보입니다.
- 모니터가 프레임버퍼를 윗줄부터 차례로 읽어 칠합니다
- 프로그램이 화면에 나갈 그림을 모니터 박자와 상관없이 바꿉니다
- 바뀌는 순간이 수직 귀선 구간이 아니라 칠하는 도중입니다
- 앞 장면과 새 장면이 다릅니다. 움직이는 것이 있습니다
둘째 조건이 서는 흔한 모양은 두 가지입니다. 하나는 모니터가 읽는 프레임버퍼 한 장에 프로그램이 곧바로 그리는 경우입니다. 그리다 만 그림이 윗부분만 새 장면인 채로 읽혀 나갑니다.
다른 하나는 프레임버퍼를 두 장 쓰는 경우입니다. 더블 버퍼링(double buffering)은 프레임버퍼를 두 장 두는 방식입니다. 모니터가 한 장을 읽는 동안 프로그램은 다른 한 장에 그립니다.
다 그리면 두 장의 역할을 맞바꿉니다. 이 맞바꿈을 스왑이라고 부릅니다. 내용을 복사하지 않고 모니터가 읽을 쪽만 바꾸므로 순식간에 끝납니다.
더블 버퍼링은 그리다 만 그림이 보이는 것을 막습니다. 그래도 스왑이 칠하는 도중에 일어나면 똑같이 찢어집니다. 위쪽은 스왑 전에 읽던 프레임버퍼에서, 아래쪽은 새로 맞바꾼 프레임버퍼에서 읽히기 때문입니다.
셋째 조건이 빠지면 찢김이 아예 안 생깁니다. 스왑을 수직 귀선 구간에 맞추는 일을 수직 동기화라고 부릅니다. 넷째 조건이 빠지면 찢김은 일어나도 눈에 안 보입니다.
백엔드 코드에서 같은 모양으로 터지는 고장
읽는 쪽이 값을 한 번에 다 못 읽는 때가 있습니다. 그 도중에 쓰는 쪽이 값을 고치면 티어링이 생깁니다. 모니터는 프레임버퍼를 60분의 1초에 걸쳐 나눠 읽습니다. 그 사이에 프로그램이 내용을 바꾸면 반은 옛것, 반은 새것인 화면을 읽습니다.
백엔드 코드에도 같은 모양의 고장이 있습니다. 자바에서 volatile 이 아닌 long 필드가 그런 예입니다. 32비트 JVM(Java Virtual Machine, 자바 가상 머신)은 이 64비트 값을 32비트씩 두 번에 나눠 쓸 수 있습니다. 한 스레드가 앞 절반만 고친 사이에 다른 스레드가 읽으면 옛값도 새값도 아닌 수가 나옵니다.
torn read는 이렇게 절반씩 다른 값을 읽어 버리는 고장의 이름입니다. 티어링과 같은 영어 동사 tear(찢다)에서 왔습니다.
막는 길도 수직 동기화와 닮았습니다. 수직 동기화는 바뀌는 도중의 화면을 모니터가 못 읽게 합니다. 앞의 long 필드는 volatile 을 붙이면 한 번에 쓰이고 한 번에 읽힙니다. 도중 상태 없이 한 번에 일어나는 이 성질이 원자성입니다.
막는 수단과 치르는 값
티어링을 막으려면 셋째 조건을 깨야 합니다. 길은 둘입니다. 그림을 바꾸는 순간을 모니터가 안 칠하는 틈으로 옮깁니다. 아니면 모니터가 칠하는 때를 그림에 맞춥니다.
이름 있는 수단은 셋입니다. 수직 동기화와 트리플 버퍼링은 첫째 길이고, 가변 주사율은 둘째 길입니다.
| 수단 | 하는 일 | 치르는 값 |
|---|---|---|
| 수직 동기화 | 스왑을 다음 수직 귀선 구간까지 미룬다 | 기다린 만큼 화면이 늦게 바뀐다. 한 장이 조금 늦으면 다음 틈까지 한 번 더 기다려 움직임이 끊긴다 |
| 트리플 버퍼링 | 스왑은 수직 동기화처럼 수직 귀선 구간에 맞춘다. 기다리는 동안 셋째 프레임버퍼에 다음 장면을 그려 두어 수직 동기화의 끊김을 줄인다 | 메모리를 한 장 더 쓴다 |
| 가변 주사율 | 모니터가 그림이 도착할 때마다 새로 칠한다 | 모니터와 그래픽 장치가 이 기능을 갖춰야 한다 |
표의 첫 줄에서 늦게 바뀌는 시간은 사용자 쪽에서 반응이 굼뜬 것으로 느껴집니다. 버튼을 누른 뒤 그 결과가 화면에 보이기까지의 시간을 입력 지연(input lag)이라고 부릅니다. 수직 동기화는 기다린 만큼 이 시간을 늘립니다.
같은 줄의 끊김에도 이름이 있습니다. 스터터링(stuttering)은 화면에 새 그림이 나오는 간격이 고르지 않아 움직임이 툭툭 끊겨 보이는 결함입니다. 한 장을 그리는 시간이 한 번 칠하는 시간과 비슷해서 그 위아래를 오갈 때 잘 생깁니다.
신경 써야 하는 화면과 아닌 화면
티어링은 모니터에 그림을 계속 내보내는 프로그램에서만 생깁니다. 게임, 영상 재생, 스크롤과 애니메이션이 있는 화면이 그렇습니다.
서버에서 이미지나 동영상 파일을 만드는 일에는 해당하지 않습니다. 파일에는 완성된 장면만 차례로 적힙니다. 그 파일을 칠하는 모니터가 없습니다.
모니터로 내보내는 프로그램이라도 무엇을 먼저 막을지는 고르는 문제입니다. 영상 재생처럼 반응 속도가 덜 중요한 화면은 수직 동기화를 켭니다. 지연을 조금 감수해도 찢긴 선이 안 보이는 쪽이 낫기 때문입니다.
몇 밀리초의 반응 차이가 조작감을 가르는 게임은 지연을 먼저 줄입니다. 찢김을 감수하고 수직 동기화를 끄기도 합니다. 모니터와 그래픽 장치가 받쳐 주면 가변 주사율로 찢김과 지연을 함께 줄입니다.
관련 항목
티어링이 일어나는 화면 출력 장치와 신호
모니터 · 주사율 · 스캔아웃 · 디스플레이 컨트롤러 · 수직 귀선 구간 · 프레임버퍼 · 픽셀 · GPU
티어링을 막는 버퍼와 동기화 수단
더블 버퍼링 · 버퍼 스왑 · 프런트 버퍼 · 백 버퍼 · 스왑 체인 · 수직 동기화 · 트리플 버퍼링 · 가변 주사율 · Adaptive-Sync · FreeSync · G-Sync
티어링을 막을 때 대신 생기는 화면 결함
입력 지연 · 스터터링 · 프레임 드롭 · 프레임 페이싱
티어링이 생기는 그리기 흐름과 속도 지표
그래픽스 · 렌더링 · 게임 루프 · 그래픽스 파이프라인 · 프레임률 · 프레임 타임
티어링과 같은 모양의 동시성 고장과 방어 수단
torn read · torn write · 경쟁 상태 · 데이터 경쟁 · 원자성 · 락
티어링과 소리가 같은 저장 계층 용어
스토리지 티어링 · 계층형 스토리지 · 핫 데이터 · 콜드 스토리지
다른 이름: tearing · screen tearing · 화면 찢김 · 화면 찢어짐 · 스크린 티어링