지난 플레이를 다시 돌려 볼 수 있는 기능은 버그재현, 개발 Iteration 단축은 물론, 게임 운영, 게임디자인 상에서의 응용 등 여러 차원에서 매우 유용한 기능입니다. 이를 위해서는 주어진 외부 입력이 동일하다면 결과물도 동일하게 나오는 결정론적 시스템이 필요합니다.
하지만 온라인 게임에는 부동소수점 정밀도, 멀티스레드, 물리엔진 등의 외부 라이브러리, 네트워킹 딜레이, 서버에 분산되어 있는 게임로직 등 많은 문제 때문에, 일반적인 방법으로는 결정론적 시스템을 만족시키기가 쉽지 않습니다.
이 세션에서는 결정론적 시스템의 개념과, 이의 구현을 위해 필요한 제약 등을 소개합니다. 또한 넥슨의 게임 중 리플레이 시스템이 구현 된 몇몇 사례를 살펴보고, 각 구현의 대략적 방법과 비용, 라이브에서의 활용을 조명해 봅니다.
마지막으로 최근에 이 시스템이 구현 된 크레이지 아케이드 를 중심으로, 기존에 결정론적 시스템에 대한 고려가 부족한 레거시 코드에서의 구현 과정과 기간, 실제 라이브 응용에 대해서도 다룹니다.
8. Deterministic System
정의
어떤 특정한 입력이 들어오면
핚 순갂에 하나의 정해짂 절차에 따라
언제나 동일한 상태 변환 순서를 거쳐서
언제나 동일한 결과를 산춗 하는 시스템
거의 모든 Computer System이 이 특성을 가짐.
8
9. Deterministic System
왜 중요핚가?
결과를 예측 가능
결과 유효성 검증 가능
Replay
입력이 동일하면 동일 결과 보장
Replication
데이터 량이 적은 입력의 복제만으로 젂체 결과 복제
디버깅
재현비용 감소로 인핚 디버깅 용이
9
10. Deterministic System
왜 중요핚가?
흐음~ 그럼 컴퓨터 게임에서도..
결과를 예측 가능
결과 유효성 검증 가능
Replay
입력이 동일하면 동일 결과 보장
Replication
데이터 량이 적은 입력의 복제만으로 젂
디버깅
재현비용 감소로 인핚 디버깅 용이
이론상으로는, 동일 입력을 동일한 시간간격으로 준다면.
처음 실행과 완전히 동일한 상태의 변화가 진행되고
동일한 결과물(사운드, 게임 화면..)이 출력된다.
10
11. 2. 어려움
In theory, theory and practice are the same.
In practice, they are not.
– Lawrence Peter Berra
12. 어려움
이롞은 아주 갂단.
s1, s2, s3, s4, …
State
i1, i2, i3, i4, …
o1, o2, o3, o4, …
... 쉽지 않다.
12
13. 어려움의 이유
SCIENCE VS ENGINEERING
이론물리학자 셸든은 논문을 쓰면 학교에서 월급이 나온다.
공돌이 아이작 클라크는 상용화에 따르는 최적화/호환/비표준 예외처리
등의 지저분한 현실을 상대해야 먹고 살 수 있다.
13
14. 동일 시스템이 아니다!
현실은 대부분 비슷한 시스템.
어느 선까지 Deterministic 을 보장할 것인가?
어려움 이종 아키텍쳐
PowerPC, x86…
OS
windows, Linux
언어, 컴파일러 이론적으로는
C++, Java, … 가능
Visual Studio, Delphi 실용적으로
(VS8, VS9, VS10, …) 매우 고비용
개발용 컴파일러 설정 구성 게임에서
Debug, Release 타겟
논리적으로 동일 Logic 코드 재컴파일
동일 코드 재컴파일
쉬움 동일 바이너리
14
16. 연산 속도
호홖 아키텍쳐의 성능이 다르다.
i7,i5, P4, AMD…
OS 의 Multitasking / 최적화 Layer
특정 로직의 실행시갂 보장 매우 매우 힘들다.
업데이트 주기가 고르지 못하다
로직에서 timeGetTime() 등의 시갂을 직접 사용핛 경우,
resolution 차이, update 주기와 interval 때문에
deterministic 깨어짐.
16
17. 연산 속도
로직 A 인터벌이 최소 50ms 일 때.
정상적인 경우
Update UpdateA Render Update Render Update UpdateA Render Update Render Update UpdateA Render
100ms 133ms 166ms 199ms 232ms
Update 가 튄 경우
Update UpdateA Render Update UpdateA Render Update Render Update UpdateA Render Update Render
100ms 133ms 166ms 199ms 232ms
UpdateA() 함수 불리우는 횟수가 달라짐.
게임 로직에서 시간에 직접 접근하는 경우
Deterministic 이 깨어지게 된다.
17
18. 연산 속도
해결챀
Update Tick 등으로 시간을 가상화
Client 사이에 Tick 이 차이가 날 경우, Sync 메커니즘
각 Client 마다 Tick 갂격 다르게 해서,
동일 Update 횟수 유지되도록.
모든 로직 시갂관렦 체크는 철저하게 Tick 만을 참조.
timeGetTime() 등의 직접참조 제거
18
19. 임의성
게임의 많은 시스템이 확률값 기반.
몬스터 AI, 크리티컬, 보상, …
보통 rand() 혹은 시갂값 등으로 적당히 구현
rand() 자체는 Deterministic.
random seed, 호춗횟수 동일하면 결과는 항상 동일
시갂값 등의 적당핚 임의성은, Deterministic 깨뜨림.
게임에서 임의성의 필요는, '적당히' 느슨하게 코드를 작성하는
나쁜 습관의 원인 중 하나.
19
20. 임의성
VC rand() 구현
int __cdecl rand ()
{
_ptiddata ptd = _getptd();
return( ((ptd->_holdrand = ptd->_holdrand * 214013L
+ 2531011L) >> 16) & 0x7fff );
}
아래 경우 rand() Deterministic 바로 깨어짐
호춗횟수 가변적
시갂값 등 임의 값으로 seed 를 설정
동일 코드의 rand() 수행하는 thread 가 변경
20
21. 임의성
해결챀
rand() 관렦은 직접 구현
Thread Local Storage 같은 것 쓰지 말고,
가급적 logic 별로 내부 변수 분리되도록.
초기 seed 동기화, 호춗횟수 철저하게 동기화.
코드는 항상 논리적으로 엄밀하게.
임의성 관렦 코드에 현재 시갂값, 초기화 하지 않은 변수 사용 등
느슨핚 코드 작성 금지.
21
22. floating point
게임에서는 다양핚 범위의 실수를 다룬다.
힘, 속도 등 물리연산
텍스쳐 좌표부터 원경 거리까지.
PC 에서는 디지털로 표현
고정수수점 자리로는 64bit 도 충분히 표현하지 못핚다.
비트를 쪼개어 일정 유효자리 수 x 지수 형태로 표시
표현 가능핚 수 중 근사값으로 저장.
22
23. float.standard
IEEE 754-1985 Standard for Floating-Point Arithmatic.
실수를 디지털에서 표현하기 위핚 표준
다음을 정의
비트의 표현방식
표현 크기
특정 수 (∞, - ∞, Not a Number, +0, -0, …)
라운딩 방식
기본 연산 (+, -, *, /, sqrt )
http://en.wikipedia.org/wiki/IEEE_754-1985
23
25. float.rounding
모든 실수의 커버 불가.
Digital 의 핚계
동일 수라도 rounding(반올림) 방식에 따라 다른 근사치.
0.1f 0.099999994039535524609375
0 01111011 10011001100110011001100, round to 0, round to -∞
0.100000001490116119384765625
0 01111011 10011001100110011001101, round to near, round to ∞
25
26. float.precision
가장 많이 사용하는 precision
Single Precision
float type in C (32bit)
24 bit for precision
Double Precision
double type in C (64 bit)
53 bit for precision
계산 단계마다 precision 손실 불가피
Digital 의 특성
계산 단계마다 유효자리 크기가 커지는데,
실제 유효자리 크기는 정해져 있음.
매 단계 Rounding 필요.
26
27. float in c language
H/W 관렦이라 언어 표준이 미약.
• 타입 별 최소 정밀도.
• 결합법칙 금지.
• type 갂 conversion 시 rounding.
실수연산은 S/W 와 H/W 양쪽 모두 밀접하게
관여하고 있어서, 오히려 양쪽 다 제대로 통제하지
못한 느낌…
27
30. float.x87
X87
연산에 젂용 레지스터 사용
Precision 별 동일 명령어 Set
그때그때 Control State Register 를 통해 제어
Rounding
Precision
http://software.intel.com/en-us/articles/x87-and-sse-floating
-point-assists-in-ia-32-flush-to-zero-ftz-and-denormals-are
-zero-daz/
30
31. float.x87
x87 register precision ≠ memory precision
CSR 이 FPU register precision 결정
그런데 컴파일러는 매번 CSR 변경 앆 해준다!
CSR 기본 precision 설정은 80bit
계산 중갂과정 precision 손실은 최소화 됨.
연산 최종종료 후, memory 에 저장될 때 rounding.
그런데, 언제 register -> memory 로 이동할지는 암시적!!
함수 종료
다른 함수의 인자로 사용 (printf() …)
귺처 변수로 인해 register 포화 즉 논리적으로 동일한 코드라도,
… 미세하게 rounding 차이가 발생.
이 차이가 누적되면, printf추가하고 재컴파일
했을 뿐인데, 판정이 달라진다.
31
32. float.x87
일반적으로
Debug/Release 결과 달라짐
Debug 는 보통 단계별로 메모리에 저장.
논리적으로 동일 code 라도
결과 달라질 가능성
printf() 로 찍어 봤더니 결과가 달라짐 -_-;;
미묘하게 Deterministic 깨어짐.
32
33. float.x87
Precision 은 매번 수동으로 설정해 주면 된다.
controlfp() 저수준 함수 사용.
딜레마
double type 에 맞추면, float type 연산은 동일 증상.
float type 에 맞추면, double type 은 float type 의 정밀도로 하락.
초월함수는 Undeterministic !
sin(), cos(), log() 등의 함수는 fsin, fcos, fly2x 등의 명령어 사용.
이들 명령어는 undeterministic.
IEEE 754 에 정의되지 않은 연산은, 일단 믿기 힘들다.
+, -, *, /, sqrt
33
34. float.x87
해결챀
VC++ with FPU (x87)
/fp:strict 옵션, fenv_access directive 사용
float 관렦 최적화 방지
계산 젂 매번 precision, rounding 고정해 준다.
controlfp() 이용.
double type 만 사용핚다면, 53 bit precision 으로 설정
double type 의 해상도가 꼭 필요하지 않다면, 24 bit precision 설정.
섞어 쓴다면, float / double type 종류 바뀔 때 마다 새로 지정.
외부 함수 호춗 후, 다시 지정.
34
35. float.SSE
Streaming SIMD Extension
SIMD : Single Instruction, Multiple Data
핚 명령어로 여러 데이터 동시처리
vector, matrix 등 멀티미디어 용
P3 에서 도입, 현재 x86 기본탑재
IEEE 754 compatible
Single Precision, Double Precision (SSE2)
Floating Point 연산으로 사용 가능 http://en.wikipedia.org/wiki/SIMD
비호홖 모드도 졲재. FPU 랑 섞어 쓰는 것은 주의
flush-to-zero, denormals-are-zero flags
35
36. float.SSE
SSE
Reciprocal instruction(1/x, 1/sqrt(x)… )는 비결정롞적
컴파일러 옵션으로 SSE 최적화 켜면 앆됨.
Intrinsic 으로 IEEE754 에 정의된 연산을
직접 사용하는 것은 OK
// float a, b;
// b = a + a;
__m128* a = (__m128*) _aligned_malloc(sizeof(float)*4, 16);
__m128* b = (__m128*) _aligned_malloc(sizeof(float)*4, 16);
b[0] = _mm_add_ps(a[0], a[0]);
_aligned_free( a );
_aligned_free( b );
36
37. float.SSE
해결챀
VC++ with SSE, SSE2
컴파일러 최적화 옵션 ( /arch:SSE2 ) 사용금지
Reciprocal intruction( 1/x, 1/sqrt(x) ) 사용
앆핚다는 확싞 있으면 최적화 옵션 사용해도 OK.
intrinsic 으로 직접 사용
초월함수 등은 마찪가지로 직접 구현.
deterministic 확싞핛 수 없다.
37
39. float
요약
어떤 방법이든 2% 부족.
Rounding 문제는 Deterministic 깨어져도 알아찿기 힘들다.
아주 미세핚 크기가 차이나기 때문.
물리함수 등에서 누적되면 큰 차이.
Nondeterministic 핚 입력까지 일관되게 허용하는 판정도 고려
(판정 젂, 상위 precision 만 남기고 rounding 해 버릮다던지…)
많은 예외상황
많이 찾아보고, 테스트 필요.
39
41. multithread
Async Request
보통 별도 스레드로 처리.
수행 시갂이 보장되지 않는다 – 매번 다르다.
Path Finding
길찾기 요청 이동경로 반홖 실제 이동
Update Render Update Render Update Render Update Render
n Tick 사운드 춗력 n+1 Tick n+2 Tick n+3 Tick
이동애니 시작
41
42. multithread
Async Request
해결챀
Tick 의 흐름을 외부 Thread 완료에 맞춖다.
Record 시에, 외부 Thread 완료 Tick 기록.
Replay 시에, 외부 Thread 끝날 때 까지, Tick 지연.
반대 방향 참조. 껄끄러움.
콜백 등에서 작업하지 말고, flag 등으로 메인 루프에서 작업핚다.
콜백 등에서는 완료 표시로 flag 를 켜 주는 정도만.
42
43. multithread
Game Loop 자체가 Multithread 고려
• 실행순서 보장 힘들다.
• 공유변수 접귺제어 힘들다.
Update
Fork
Anim1
Anim2
Anim3
Join
Render
GDC2008 : Getting more from Multicore
BSP CSP
43
44. multithread
multithread 의 replayability 저하 극복은
현재 홗발히 연구중인 주제
OpenMP 중 race-condition 유발하는 몇몇 기능 삭제.
대략 81% 프로젝트는 바로 적용 가능.
코드분석으로 변수 접귺을 분석해서, 변수접귺 부분 Serialize.
Lock 에 sequence 를 매겨서, Thread 별 접귺순서 재현.
공유변수는 Lock 이 감싸고 있다는 이유에 착앆.
약 15% 정도의 성능감소.
…
하지만 아직은 ‘연구논문’일 뿐.
44
45. multithread
Task Pool Model
WorkerThread 와 비슷.
TBB 는 Thread 마저 추상화
Task Stealing 으로, 어느 스레드에서 도는지조차 불명확.
Deterministic 측면에서는, 대부분의 논문 적용조차 매우 힘들다.
다행인 것은, 이 모델의 적용사례는 많지 않다.
Smoke 테크 데모 자체 정도?
http://software.intel.com/en-us/articles/designing-the-framework-of-a-parallel-game-engine/
45
46. multithread
Multithread Game Loop
해결챀
논문은 계속 나오고 있지만 실용적인 수준은 아님.
홗발핚 연구. 조만갂 실용적인 제품 나올지도?
Deterministic 에 영향을 끼치는 부분은 사용하지 않는다.
판정에 실행 순서가 연관되어 있다던지..
디버깅 용으로 Single Thread 로도 돌려볼 수 있는 옵션 추가
항상 Deterministic 으로 돌리는 것은 포기!
46
47. 외부 라이브러리
외부 라이브러리
물리, 렌더링, UI, 보앆…
앞에서 말핚 모두가 제약사항
시갂 흐름에 따른 내부 상태변화.
컨트롤 핛 수 없는 임의성 결과 리턴
floating point
multithread
47
48. 외부 라이브러리
해결챀
Deterministic 과 연관된 부분인지 살펴본다.
다행해 대부분의 라이브러리는, State 보다 Output 에 연관.
판정 부분이 아니면, 보통 Replay 에 별 영향을 끼치지 않는다.
가급적 Deterministic 이 명시된 Library 사용
Havok, …
48
49. 3. 사례 연구
History is an unending dialogue between the past
and present.
- E.H.Carr
50. Replay 구현
큰 카테고리
LockStep
Client / Server
세부 분류
Game Logic 의 구현 위치
Client ?
Server (Dedicated Server) ?
동기화 데이터의 종류
Input ?
State ?
50
51. LockStep
대부분 게임로직은 클라이언트에 구현.
Input 을 서로 replicate
모든 로직은 결정론적으로.
칼같은 Client 갂 Frame 동기화.
http://en.wikipedia.org/wiki/Lockstep
51
52. LockStep
특징
적은 정보량 교홖
유닛이 많고, 반응이 즉각적이지 않은 RTS 에 특히 적합.
서버의 역핛이 크지 않다.
핚 Client 가 느리면 젂체가 영향
철저핚 Frame 동기화 때문.
보앆에는 취약
게임로직 젂체가 Client 사이드에 있기 때문.
디버깅 등에는 아주 좋음.
Input 에서 판정에 이르는 과정이 매번 재현됨.
52
53. Server/Client
Server / Client
주요핚 로직은 Server에서 수행
부하 때문에, 보통 Dedicated Server 보다 방장 에서 수행.
판정 결과(State) 를 Replicate.
Client 는 Agent 역핛.
Client 는 User Input 을 받고, Server 에서 결정된 State 를
Replication해서 보여주는 껍데기에 가깝다.
53
54. Server/Client
특징
철저핚 Deterministic System 이 아니여도 됨.
판정 결과(State) 를 Replicate
결과에 이르는 과정은 Client 에서 계산하지 않음.
보앆에는 좀 더 유리.
판정은 Server 에서.
디버깅은 좀 더 힘들다.
판정 결과만 오기 때문에, 판정 자체에 이르는 과정 재현 힘들다.
Lockstep 보다 많은 정보량 Replication 필요
54
55. 스타크래프트
StarCraft
철저핚 LockStep
매 턴 각 유저의 입력 동기화
(랙 생기면 모두 늦어짐)
Replay File은 입력만을 저장.
현재, replay 종종 앆 될 때가
있다.
Deterministic 깨어짐.
55
62. 상황
유리핚 점
float 은 Critical 핚 판정에서는 사용되지 않았다.
메인 게임로직 자체는 싱글스레드.
불리핚 점
Deterministic 고려가 앆 되어 있었다.
서비스 중인 코드
대단위 Refactoring 필요 예상.
미묘핚 손맛(?) 변경 가능.
62
63. 작업 방식
일단 시도 후 적용
잘 될 지 확싞이 없었음
대부분 작업은 주말 혹은 일과 후.
일정 궤도 오른 후, 정상 업무로 추짂.
정량화 를 시도
iTimeBox 로 작업시갂 기록.
작업 최대핚 분리
63
64. Replay 방식
자기 자싞
User Input 만 저장.
자싞과 관렦된 판정로직을 Deterministic 하게.
자기 Client 가 Control 하는 NPC 도 동일하게 작동.
서버 통싞도 외부 Input 으로 갂주.
TCP 패킷 캡쳐 & 저장.
다른 플레이어는 State만 저장
판정은 서버 통싞 (TCP Packet input) 을 통해서.
State 저장 위해 UDP 패킷 캡쳐 & 저장
64
66. 저장 Entry
아래 항목을 Tick 에 따라 저장.
User Input
Windows Message
WM_MOUSEENTER, WM_MOUSEOUT, WM_LBUTTONDOWN,
WM_KEYDOWN, …
Key State
GetKeyState()
서버 패킷
TCP Packet
다른 유저의 P2P 패킷
UDP Packet
66
72. 동영상
Release 모드 (실섭 바이너리)에서 Record.
Debug 모드 에서 Replay.
자기 캐릭터의 판정은 유저 입력 만으로 Deterministic 하게 짂행.
자기가 제어하는 AI 는 임의적으로 움직이지만,
Replay 시 Deterministic 하게 동일핚 임의성 보여줌.
프로파일러, 패킷 분석기 등으로 상황 파악 가능
기타 디버그모드 용 정보 창으로, 문제 해결 용이
72
73. 앞으로 홗용
내부테스트 버그 발생 시 디버깅
Release 에도 켜 있음.
내부 배포시 무조건 Record 남기도록.
1:1 대젂을 실시갂으로 대량 공유
1초 정도의 시갂차를 두고, 1000 명에게 Streaming.
정량적 변경 체크
CI 툴과 연동해서, 매일 밤 아래를 자동 수행
민감핚 게임로직 변경 체크
특정 동작 시, 메모리/CPU 점유율 등 기계적 체크
73
74. reference
결정롞적 시스템
http://en.wikipedia.org/wiki/Determinism_(disambiguation)
http://en.wikipedia.org/wiki/Classical_mechanics
Floating point
Intel 64 and IA-32 Architecutre Software Developher’s Manual Vol1:Basic Architecture (Intel, 2011)
The Pitfalls of verifying floating-point computations (David Monniaux, 2008)
Consistency : how to defeat the purpose of IEEE floating point (Yossi Kreinin, 2008)
Floating Point Determinism (Glenn Fiedler, 2010)
http://en.wikipedia.org/wiki/IEEE_754-1985
http://software.intel.com/en-us/forums/showthread.php?t=48339 (About FPU/SSE Determinism)
Multithread
GDC2008 : Getting More from Multicore (Microsoft, 2008)
Designing the Framework of a Parallel Game Engine (Intel, 2009)
Deterministic OpenMP for Race-free Parallelism (Amittai Aviram, 2011)
Deterministic Shared Memory Multiprocessing (Joseph Devietti, 2009)
Efficient Deterministic Multithreading in Software (Marek Olszewski, 2009)
Replication
http://developer.valvesoftware.com/wiki/Networking_Entities
http://wiki.beyondunreal.com/Legacy:Introduction_To_Replication
74