6. 자주 등장하는 용어
M2 : 마비노기2 클라이언트 프로그램
스레드Thread , 싱글 스레드Single Thread, 멀티 스레드Multithread
조인Join : 동기화
리졸브Resolve : 커맨드 큐Command Queue 실행
병렬화Parallelization
작업Task
로직Logic : 게임 로직 코드
스레드 안전성Thread Safety
코어Core
디바이스Device : Direct3D 디바이스
8. 개발 방향
매우 보수적이고 안전한 스레딩 모델을 택함
싱글 스레드 구조로 시작한 오래된 프로젝트.
프로젝트 시작 시 상용엔진을 사용하다가 제거하고 직접 제작했다.
9. 구조 개요
기능 수준 병렬화Functional Parallelization
로직 -> 렌더 -> D3D의 파이프라인
일부 작업들은 데이터 병렬화Data Parallelization
애니메이션, 파티클, …
두 개의 백그라운드 작업 스레드
작업 우선 순위 차이
평균 3개의 코어를 사용, 일부 구간에서 모든 코어 사용
10. 압니다, Task Parallelism
하면 좋지요… 그러나…
경험 있는 프로그래머가 많이 필요.
작업 분할Task Partitioning 은 어렵다.
디버깅이 어렵다.
물론 익숙해지면 쉽겠지만…
미들웨어 통합은 어떻게?
업계 표준 작업 관리 라이브러리가 필요.
11. 두 가지가 없다
메인 스레드가 없다.
프로세스가 생성될 때 만들어진 기본 스레드라는 의미만 존재..
전담Dedicated 스레드가 없다.
대신, 스레드 타입이 있다: 로직, 애니메이션, 렌더, 백그라운드
예외: 디바이스 스레드(윈도 및 D3D 디바이스가 생성된 스레드)
12. 스레드 관리
Intel TBB(Threading Building Blocks) 사용.
TBB가 아닌 OS 스레드를 사용하는 영역
• 백그라운드 작업
디스크 IO
비동기 처리
• 전담 스레드
미들웨어 내부에서 생성하는 작업 스레드
네트워크 IO 작업 스레드
시스템 로더(부팅)
13. 스레드 통신
메시지(커맨드) 큐를 사용한 단방향 통신
로직 -> 렌더 -> 디바이스
Single Reader, Multiple Writer
역방향 통신은 조인 구간에서 처리.
로직과 디바이스 스레드는 서로 통신하지 않는다.
디바이스 스레드는 메시지를 받기만 한다.
14. 렌더링은 로직보다 1프레임 늦게 간다
로직은 렌더링 객체에 직접 접근할 수 없다.
프록시Proxy를 통해서 간접적으로 접근한다.
조인 구간에서 직접 접근할 수 있다.
로직에서 처리한 렌더링 작업은 그 다음 프레임에서 실행된다.
프록시 객체마다 더블 버퍼링되는 커맨드 큐를 갖고 있다.
최소 1프레임, 최대 2프레임의 지연이 발생.
1(로직->렌더) + 1(입력 지연. 있거나 없거나 함)
60fps 기준으로 18~36ms
사람이 인지할 수 있는 지연 시간은 평균적으로 100ms이라고 함
• 프로게이머, 굇수 제외
15. 렌더 – 디바이스 스레드 통신
두 가지 모드가 있다.
싱글 버퍼링(기본값)
더블 버퍼링
싱글 버퍼링(푸시 버퍼Push Buffer) 모드
잠시 버퍼링한 후에 디바이스 커맨드 버퍼로 전송.
더블 버퍼링 모드
커맨드를 쌓아둔 후에 조인할 때 디바이스 커맨드 버퍼와 교체.
즉, 모든 명령어는 다음 프레임에 실행된다.
16. 두 가지 방식의 특징
싱글 버퍼링은 약간 느리다.
렌더 스레드에서 최초의 명령을 보내기까지 수 ms 걸린다.
이 시간 동안 디바이스 스레드가 공회전한다.
더블 버퍼링은 결과가 1프레임 지연된다.
60fps 이상에서는 거의 느껴지지 않는다.
17. 런타임에 모드를 선택
일명, 스마트 버퍼링
60fps 이상에서는 더블 버퍼링한다.
그 미만에서는 싱글 버퍼링.
18. 자원의 스레드 안전성
대부분의 자원은 특정 스레드에서만 사용되므로 안전하다.
생성 후에 그 내용을 변경할 수 없다.
내용을 변경하고 싶다면 사본을 생성한다.
동시에 사용하는 자원은 불변Immutable 타입으로 제한.
1/3
19. 자원의 스레드 안전성 2/3
생성된 스레드와 파괴된 스레드가 다를 수 있다.
참조 카운터로 자원의 수명을 관리한다.
D3D 자원이 좋은 예.
특정 스레드에서 파괴되어야 하는 자원에 주의하자.
24. 스레드 어피니티Affinity
Windows API를 호출하는 작업
D3D를 사용하는 작업
어떤 작업은 반드시 정해진 스레드에서 실행되어야 한다.
다음 작업들은 디바이스 스레드에서 실행된다.
Debug Console
Processing
Input Devices
Processing
D3DQueue
Resolve
D3DDevice
Validation
나머지 작업들은 어피니티가 없다.
25. TBB와 스레드 타입
디바이스, 백그라운드 작업은 직접 생성한 스레드에서 실행.
좀 더 세밀한 제어가 필요하기 때문.
나머지 작업들은 모두 TBB 스케줄러로 실행한다.
31. 2/4로직Logic
렌더링 명령은 프록시 객체를 통해서 처리
실제 객체는 렌더 스레드 안에 있다.
프록시 객체는 큐에 명령을 쌓아두기만 한다.
명령 실행은 에서.
더블버퍼링된다: 조인 구간에서 버퍼 교체.
RenderQueue
Resolve
프록시 커맨드 큐는 객체마다 존재
최대 병렬화를 위해서.
큐에 들어온 순서대로 실행되지 않기 때문에 비결정론적nondeterministic이다.
34. 물리Physics
로직 스레드 타입이다.
ThreadType::Physics 이런 거 없다.
오직 로직 스레드에서만 물리를 처리한다.
렌더 스레드는 물리를 모른다.
레이 캐스팅은 예외
• raycastClosestShape() 등의 몇 가지 PhysX 함수들은 스레드에 안전.
• 길찾기, 캐릭터 IK(Inverse Kinematics), 파티클 등에서 사용.
35. 1/3애니메이션Animation
캐릭터 단위로 병렬 처리한다.
tbb::parallel_for()를 쓰면 간단하지만…
작업 스레드 수를 제어하고 싶어서 간단하게 직접 구현.
캐릭터 목록이 담긴 배열의 주소를
각 태스크가 경쟁적으로 증가시킨다.
36. 2/3애니메이션Animation
캐릭터 사이의 의존성 분석이 필요
서로 참조하고 있는 캐릭터들을 동시에 처리하면,
race가 발생하거나 크래시될 수 있다.
의존성 있는 캐릭터들은 하나의 태스크에서 순서대로 처리한다.
41. 컬링Cull
일반적인 CPU 시야 컬링 View Frustum Culling 수행.
GPU로 가려진 물체 컬링Occlusion Culling 수행
Hierarchical Occlusion Map 기반.
자세한 설명은 생략.
커서 선택Picking 처리도 이 구간에서 수행된다.
46. 디바이스 큐 리졸브D3DQueue
Resolve
렌더 스레드가 전송한 명령어들을 실행한다.
실제로 D3D를 사용하는 구간이다.
프레임 버퍼 크기 변경 처리
윈도 크기 변경 등으로 프레임 버퍼 크기가 변경되면,
명령어가 생성된 시점의 버퍼 크기와 실제 크기가 다르다.
그냥 억지로 렌더링한 다음에 프레임 버퍼를 검게 칠한다.
47. 1/2
Join 조인
각 작업의 상태를 동기화하는 단계로,
이 구간을 가급적 빨리 실행해야 성능에 유리하다.
스레드에 안전한 구간
각종 코드들의 축제가 벌어진다.
현재 이 코드는 남아 있지 않다…
48. 2/2
Join 조인
커맨드의 실행 순서에 주의!!
스레드에 안전하다고 커맨드 큐 없이 바로 상태를 조작하면,
실행 순서가 역전된다.
이를 방지하기 위해서 조인 구간에서 프록시의 본체에 접근할 경우,
커맨드 큐를 먼저 실행한다.
• 의도하지 않은 커맨드 큐 실행이 발생할 수 있다.
52. 스레드 타입 검사
다른 스레드에서 실행되는 코드에 접근할 수 없도록 설계를 잘 해야 한다.
그래도 어떻게든 용케 방법을 찾아서 호출하더라…
작업 시작 시 자기 타입을 TLSThread Local Storage에 기록하고,
주요 함수마다(…) 매크로 함수로 스레드 타입을 검사.
매우 위험하고 실수의 여지가 많다.
55. 스레딩 옵션 변경
최소한 시작 옵션으로 지정할 수 있어야 한다.
런타임에 변경할 수 있으면 스레딩 성능 비교 및 디버깅에 유용.
여러 개의 클라이언트를 실행할 경우, 싱글 스레드 모드가 편하다.
56. 커맨드 생성 위치 추적
커맨드가 생성되는 시점과 실행 시점이 다르다.
커맨드 실행 코드에 브레이크 포인트를 걸어봤자 얻는 것이 별로 없다.
커맨드를 추가하는 위치가 더 중요하다.
커맨드에 ID를 붙이고, 특정 ID를 생성하는 위치를 찾는다.
자원 생성 커맨드는 유니크 id를 부여하기 쉽다.
일반 커맨드는 시퀀스 넘버를 붙이기 때문에 버그의 재현 법이 중요.
62. 캐시 파일 생성
개발 클라이언트 전용 기능.
백그라운드 스레드에서 실행한다.
어셋 저장소Repository에는 무압축 원본 텍스쳐만 올리고,
런타임에 텍스쳐를 가공한다.
매번 변환하면 너무 느리므로 캐시를 만들어둔다.
Cache
Generation
디스크립터, 캐시가 없다면 이 단계에서 바로 생성한다.
77. 스레드에 안전하지 않다!
최신 버전에서는 아래 내용이 다 쓸데없을 지도…
진짜 예전 버전을 쓰고 있다(1.7.X)!!!!
스트링 캐시 테이블 등이 그냥 생 전역변수.
그래서 싱글 스레드로 처리
중요한 캐릭터만 표정을 사용하므로 아직까지는 별 문제 없었다.
런칭 후에 왠지 폭탄이 터질 것 같다…
81. 문서화도 잘 되어있고 아무튼 좋다.
일반적으로 스레드에 안전하지 않지만,
레이 캐스팅 같은 일부 함수들은 안전하다.
82. 버전 4.X를 사용 중.
3.X는 스레드에 안전하지 않다.
sdk 렌더러 소스를 싹 고쳐서 모든 디바이스 접근을 커맨드 버퍼링 했었다.
(경험있고 유능한) 프로그래머라면 2~3일 걸린다.
웬만하면 최신 버전을 사는 것이 정신 건강에 좋다.
4.X는 스레드에 안전하다.
모든 렌더링 명령이 내부 커맨드 큐에 쌓인다
업데이트와 렌더링을 다른 스레드에서 실행할 수 있다.
따로 해줄 것이 거의 없다.
83. 버전 6.X 사용 중
버전 5.X
디바이스 스레드에서 실행했기 때문에,
모든 sdk 접근을 콜백으로 해야 해서 코드가 매우 복잡했다.
버전 6.X
sdk의 렌더러 API를 재구현해서 렌더 스레드에 붙였다.
스레드에 안전한 것 같다.
• 통합한지 얼마 되지 않아서 확신이 없다.
85. M2 클라이언트 스레딩 아키텍쳐
이 상황인 분들에게 조금이라도 도움이 되길 바랍니다.
오래된 프로젝트 + 자체 엔진 + 멀티 스레딩 구현
이 발표 내용 정도만 구현해도 쿼드코어까지 잘 지원한다.
게임 로직 병렬화를 잘 연구하면 진정한 태스크 병렬화도 가능할 것이다.
멀티 스레딩, 어렵지 않다.
87. “Magic and Technology: Migrating from One to Many Cores in Shadowrun”. Gamefest2007
“Multicore Programming Two Years Later”. Gamefest2007
“Memory Models: Foundational Knowledge for Concurrent Code”. Gamefest2008
“What’s in a Frame: Latency, Tearing, Jitter, and Frame Rate on Xbox 360”. Gamefest2011
“Scaling Your Game to n Cores: A Deep Dive on Tasking”. Gamefest2011
“Getting More From Multicore”. GDC2008
“Optimizing DirectX on Multi-core architectures”. GDC2008
“Optimizing Game Architectures with Intel® Threading Builing Blocks”. GDC2008
“The Future of Programming for Multi-core with the Intel C++ Compilers”. GDC2008
“Comparative Analysis of Game Parallelization”. GDC2008
“Threading QUAKE 4 & Enemy Territory QUAKE Wars”. GDC2008
“Optimizing Game Architectures with Intel Threading Building Blocks”. GDC2009
“Task-based-Multithreading – How to Program for 100 cores”. GDC2010
“Firaxis' Civilization V: A Case Study in Scalable Game Performance”. GDC2010
“Don’t Dread Threads”. GDC2010
“Streaming Massive Environments. From Zero to 200MPH”. GDC2010
“DirectX11 Rendering In Battlefield3”. GDC2011
“Hotspots, FLOPS, and uOps:To-The-Metal CPU Optimization”. GDC2011
“Multi-Core Memory Management Technology in Mortal Kombat”. GDC2011
“Terrain In Battlefield 3: A Modern, complete and scalable system”. GDC2012
Joe Waters.
Ian Lewis.
Herb Sutter.
David Cook.
Steve Smith, Leigh Davies.
Ian Lewis.
Leigh Davies.
Brad Werth.
Ganesh Rao.
Dmitry Eremin.
Anu Kalra, Jan Paul van Waveren.
Brad Werth.
Ron Fosner.
Dan Baker, Yannis Minadakis.
Orion Granatir, Omar Rodriguez.
Chris Tector.
Johan Andersson.
Stan Melax, Deppak Vembar.
Adisak Pochanayon.
Mattias Widmark.