Successfully reported this slideshow.

전형규, M2 클라이언트 스레딩 아키텍쳐, NDC2013

19

Share

Loading in …3
×
1 of 88
1 of 88

전형규, M2 클라이언트 스레딩 아키텍쳐, NDC2013

19

Share

  1. 1. M2 클라이언트 스레딩 아키텍쳐 넥슨 코리아 N스퀘어개발본부 1실 GTR팀 전형규 henjeon@nexon.co.kr 2013.04.24
  2. 2. 발표자 소개 넥슨 10년차 프로그래머 현재 GTR팀 팀장 참여 프로젝트 마비노기 XBOX360 마비노기 마비노기 2 주요 관심사 Computer Graphics Real-Time Rendering
  3. 3. 발표 내용 M2 클라이언트의 멀티 스레딩 구조를 설명 스레딩 사례 소개 미들웨어 통합 가이드 다루지 않는 것 서버 스레딩 멀티 스레드 API 사용법 각종 잠금 장치 사용법 락프리(Lock-Free) 알고리즘
  4. 4. M2 소개 M2 엔진 이름 사용 중인 미들웨어들
  5. 5. 자주 등장하는 용어 M2 : 마비노기2 클라이언트 프로그램 스레드Thread , 싱글 스레드Single Thread, 멀티 스레드Multithread 조인Join : 동기화 리졸브Resolve : 커맨드 큐Command Queue 실행 병렬화Parallelization 작업Task 로직Logic : 게임 로직 코드 스레드 안전성Thread Safety 코어Core 디바이스Device : Direct3D 디바이스
  6. 6. 개요 Overview
  7. 7. 개발 방향 매우 보수적이고 안전한 스레딩 모델을 택함 싱글 스레드 구조로 시작한 오래된 프로젝트. 프로젝트 시작 시 상용엔진을 사용하다가 제거하고 직접 제작했다.
  8. 8. 구조 개요 기능 수준 병렬화Functional Parallelization 로직 -> 렌더 -> D3D의 파이프라인 일부 작업들은 데이터 병렬화Data Parallelization 애니메이션, 파티클, … 두 개의 백그라운드 작업 스레드 작업 우선 순위 차이 평균 3개의 코어를 사용, 일부 구간에서 모든 코어 사용
  9. 9. 압니다, Task Parallelism 하면 좋지요… 그러나… 경험 있는 프로그래머가 많이 필요. 작업 분할Task Partitioning 은 어렵다. 디버깅이 어렵다. 물론 익숙해지면 쉽겠지만… 미들웨어 통합은 어떻게? 업계 표준 작업 관리 라이브러리가 필요.
  10. 10. 두 가지가 없다 메인 스레드가 없다. 프로세스가 생성될 때 만들어진 기본 스레드라는 의미만 존재.. 전담Dedicated 스레드가 없다. 대신, 스레드 타입이 있다: 로직, 애니메이션, 렌더, 백그라운드 예외: 디바이스 스레드(윈도 및 D3D 디바이스가 생성된 스레드)
  11. 11. 스레드 관리 Intel TBB(Threading Building Blocks) 사용. TBB가 아닌 OS 스레드를 사용하는 영역 • 백그라운드 작업 디스크 IO 비동기 처리 • 전담 스레드 미들웨어 내부에서 생성하는 작업 스레드 네트워크 IO 작업 스레드 시스템 로더(부팅)
  12. 12. 스레드 통신 메시지(커맨드) 큐를 사용한 단방향 통신 로직 -> 렌더 -> 디바이스 Single Reader, Multiple Writer 역방향 통신은 조인 구간에서 처리. 로직과 디바이스 스레드는 서로 통신하지 않는다. 디바이스 스레드는 메시지를 받기만 한다.
  13. 13. 렌더링은 로직보다 1프레임 늦게 간다 로직은 렌더링 객체에 직접 접근할 수 없다. 프록시Proxy를 통해서 간접적으로 접근한다. 조인 구간에서 직접 접근할 수 있다. 로직에서 처리한 렌더링 작업은 그 다음 프레임에서 실행된다. 프록시 객체마다 더블 버퍼링되는 커맨드 큐를 갖고 있다. 최소 1프레임, 최대 2프레임의 지연이 발생. 1(로직->렌더) + 1(입력 지연. 있거나 없거나 함) 60fps 기준으로 18~36ms 사람이 인지할 수 있는 지연 시간은 평균적으로 100ms이라고 함 • 프로게이머, 굇수 제외
  14. 14. 렌더 – 디바이스 스레드 통신 두 가지 모드가 있다. 싱글 버퍼링(기본값) 더블 버퍼링 싱글 버퍼링(푸시 버퍼Push Buffer) 모드 잠시 버퍼링한 후에 디바이스 커맨드 버퍼로 전송. 더블 버퍼링 모드 커맨드를 쌓아둔 후에 조인할 때 디바이스 커맨드 버퍼와 교체. 즉, 모든 명령어는 다음 프레임에 실행된다.
  15. 15. 두 가지 방식의 특징 싱글 버퍼링은 약간 느리다. 렌더 스레드에서 최초의 명령을 보내기까지 수 ms 걸린다. 이 시간 동안 디바이스 스레드가 공회전한다. 더블 버퍼링은 결과가 1프레임 지연된다. 60fps 이상에서는 거의 느껴지지 않는다.
  16. 16. 런타임에 모드를 선택 일명, 스마트 버퍼링 60fps 이상에서는 더블 버퍼링한다. 그 미만에서는 싱글 버퍼링.
  17. 17. 자원의 스레드 안전성 대부분의 자원은 특정 스레드에서만 사용되므로 안전하다. 생성 후에 그 내용을 변경할 수 없다. 내용을 변경하고 싶다면 사본을 생성한다. 동시에 사용하는 자원은 불변Immutable 타입으로 제한. 1/3
  18. 18. 자원의 스레드 안전성 2/3 생성된 스레드와 파괴된 스레드가 다를 수 있다. 참조 카운터로 자원의 수명을 관리한다. D3D 자원이 좋은 예. 특정 스레드에서 파괴되어야 하는 자원에 주의하자.
  19. 19. 실행 중인 스레드가 디바이스 스레드인지 조사 자원의 스레드 안전성 3/3
  20. 20. 세부 사항 Implementation Details
  21. 21. Frame N Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Animation Join Logic Physics RenderQueue Resolve Cull Particle Particle Frame N+1 Render Background Worker #2 Background Worker #1 D3DDevice Validation M2 클라이언트 태스크 다이어그램(2013/04/24)
  22. 22. 작업의 스레드 타입 ThreadType::Simulation ThreadType::Animation ThreadType::Render ThreadType::Background 모든 태스크는 다음 타입 중 하나 ‘이상’을 가진다. 로직 = Simulation 애니메이션 Simulation | Animation 렌더 = Render 백그라운드 = Background 조인 = 전부 다(ThreadType::DontCare) 타입 예:
  23. 23. 스레드 어피니티Affinity Windows API를 호출하는 작업 D3D를 사용하는 작업 어떤 작업은 반드시 정해진 스레드에서 실행되어야 한다. 다음 작업들은 디바이스 스레드에서 실행된다. Debug Console Processing Input Devices Processing D3DQueue Resolve D3DDevice Validation 나머지 작업들은 어피니티가 없다.
  24. 24. TBB와 스레드 타입 디바이스, 백그라운드 작업은 직접 생성한 스레드에서 실행. 좀 더 세밀한 제어가 필요하기 때문. 나머지 작업들은 모두 TBB 스케줄러로 실행한다.
  25. 25. D3DDevice Validation 디바이스 검사 D3D 디바이스 로스트, 창 크기 변경 등의 이벤트를 처리한다. 자주 발생하는 이벤트가 아니므로 싱글 스레드로 처리한다.
  26. 26. Debug Console Processing 디버그 콘솔 처리 개발 전용 기능들을 처리한다. 싱글 스레드로 실행한다. 빠르고 쉽게 만들 수 있다. 디버깅하기 쉽다. 배포 버전에서 제거되므로 유저 경험에 영향을 주지 않는다.
  27. 27. 입력장치 처리Input Devices Processing Windows API의 스레딩 문제를 피하기 위해서, 싱글 스레드로 실행. 수행 시간이 1ms 안쪽이므로 성능에 영향이 없다.
  28. 28. Frame N Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Animation Join Logic Physics RenderQueue Resolve Cull Particle Particle Frame N+1 Render Background Worker #2 Background Worker #1 D3DDevice Validation M2 클라이언트 태스크 다이어그램
  29. 29. 1/4로직Logic 모든 게임 로직은 이 태스크에서 실행된다. 게임 상태 갱신 네트워킹 AI 사운드 UI
  30. 30. 2/4로직Logic 렌더링 명령은 프록시 객체를 통해서 처리 실제 객체는 렌더 스레드 안에 있다. 프록시 객체는 큐에 명령을 쌓아두기만 한다. 명령 실행은 에서. 더블버퍼링된다: 조인 구간에서 버퍼 교체. RenderQueue Resolve 프록시 커맨드 큐는 객체마다 존재 최대 병렬화를 위해서. 큐에 들어온 순서대로 실행되지 않기 때문에 비결정론적nondeterministic이다.
  31. 31. 3/4로직Logic 프록시 객체는 단순히 메시지만 전달Delegation 실제 수행 전달
  32. 32. 4/4로직Logic 상태 질의state query는 금지. 어쩔 수 없이 필요한 경우에는… 조인 구간에서 동기화 하거나 프록시에 동일하게 구현
  33. 33. 물리Physics 로직 스레드 타입이다. ThreadType::Physics 이런 거 없다. 오직 로직 스레드에서만 물리를 처리한다. 렌더 스레드는 물리를 모른다. 레이 캐스팅은 예외 • raycastClosestShape() 등의 몇 가지 PhysX 함수들은 스레드에 안전. • 길찾기, 캐릭터 IK(Inverse Kinematics), 파티클 등에서 사용.
  34. 34. 1/3애니메이션Animation 캐릭터 단위로 병렬 처리한다. tbb::parallel_for()를 쓰면 간단하지만… 작업 스레드 수를 제어하고 싶어서 간단하게 직접 구현. 캐릭터 목록이 담긴 배열의 주소를 각 태스크가 경쟁적으로 증가시킨다.
  35. 35. 2/3애니메이션Animation 캐릭터 사이의 의존성 분석이 필요 서로 참조하고 있는 캐릭터들을 동시에 처리하면, race가 발생하거나 크래시될 수 있다. 의존성 있는 캐릭터들은 하나의 태스크에서 순서대로 처리한다.
  36. 36. 3/3 Animation 코드 예: 애니메이션
  37. 37. Frame N Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Animation Join Logic Physics RenderQueue Resolve Cull Particle Particle Frame N+1 Render Background Worker #2 Background Worker #1 D3DDevice Validation M2 클라이언트 태스크 다이어그램
  38. 38. 로직 스레드에서 전송한 커맨드를 실행한다. 렌더링 큐 리졸브RenderQueue Resolve
  39. 39. 코드 예:
  40. 40. 컬링Cull 일반적인 CPU 시야 컬링 View Frustum Culling 수행. GPU로 가려진 물체 컬링Occlusion Culling 수행 Hierarchical Occlusion Map 기반. 자세한 설명은 생략. 커서 선택Picking 처리도 이 구간에서 수행된다.
  41. 41. 파티클Particle 애니메이션과 동일하게 병렬화했다. 로직과 밀접한 관계가 있는 파티클 타입은 에서 처리한다.Logic 예: 동전 효과
  42. 42. 1/3렌더링Render 장면 업데이트, 렌더링 파이프라인 실행. 디바이스 커맨드 큐로 명령어들을 전송한다. 코드 예:
  43. 43. 2/3렌더링Render 미룰 수 있는 작업은 최대한 디바이스 스레드로 미룬다. 게을러서 병목이 아니라서 지켜보고 있는 중. 병렬화할 여지가 많다
  44. 44. 3/3렌더링Render D3D 디바이스 자원에 직접 접근하고 싶을 때 콜백Callback을 사용. 코드 예:
  45. 45. 디바이스 큐 리졸브D3DQueue Resolve 렌더 스레드가 전송한 명령어들을 실행한다. 실제로 D3D를 사용하는 구간이다. 프레임 버퍼 크기 변경 처리 윈도 크기 변경 등으로 프레임 버퍼 크기가 변경되면, 명령어가 생성된 시점의 버퍼 크기와 실제 크기가 다르다. 그냥 억지로 렌더링한 다음에 프레임 버퍼를 검게 칠한다.
  46. 46. 1/2 Join 조인 각 작업의 상태를 동기화하는 단계로, 이 구간을 가급적 빨리 실행해야 성능에 유리하다. 스레드에 안전한 구간 각종 코드들의 축제가 벌어진다. 현재 이 코드는 남아 있지 않다…
  47. 47. 2/2 Join 조인 커맨드의 실행 순서에 주의!! 스레드에 안전하다고 커맨드 큐 없이 바로 상태를 조작하면, 실행 순서가 역전된다. 이를 방지하기 위해서 조인 구간에서 프록시의 본체에 접근할 경우, 커맨드 큐를 먼저 실행한다. • 의도하지 않은 커맨드 큐 실행이 발생할 수 있다.
  48. 48. Background Worker 백그라운드 작업 비동기Asynchronous 처리, 파일 IO 작업. 두 작업 스레드가 있다 낮은 우선순위 작업 • 리소스 로딩, 파일 IO • 이미지 업로드 높은 우선순위 작업 • 캐릭터 커스터마이징
  49. 49. 정리 Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Join Logic Physics RenderQueue Resolve Cull Particle ParticleRender Background Worker #2 Background Worker #1 D3DDevice Validation Animation 가로축이 시간, 세로 축이 스레드 이게 더 보기 쉽잖아…
  50. 50. 디버깅 Debugging Tips
  51. 51. 스레드 타입 검사 다른 스레드에서 실행되는 코드에 접근할 수 없도록 설계를 잘 해야 한다. 그래도 어떻게든 용케 방법을 찾아서 호출하더라… 작업 시작 시 자기 타입을 TLSThread Local Storage에 기록하고, 주요 함수마다(…) 매크로 함수로 스레드 타입을 검사. 매우 위험하고 실수의 여지가 많다.
  52. 52. 스레드 프로파일링 스레드 작업 막대 그래프 1/2
  53. 53. 스레드 프로파일링 타이밍 그래프 2/2
  54. 54. 스레딩 옵션 변경 최소한 시작 옵션으로 지정할 수 있어야 한다. 런타임에 변경할 수 있으면 스레딩 성능 비교 및 디버깅에 유용. 여러 개의 클라이언트를 실행할 경우, 싱글 스레드 모드가 편하다.
  55. 55. 커맨드 생성 위치 추적 커맨드가 생성되는 시점과 실행 시점이 다르다. 커맨드 실행 코드에 브레이크 포인트를 걸어봤자 얻는 것이 별로 없다. 커맨드를 추가하는 위치가 더 중요하다. 커맨드에 ID를 붙이고, 특정 ID를 생성하는 위치를 찾는다. 자원 생성 커맨드는 유니크 id를 부여하기 쉽다. 일반 커맨드는 시퀀스 넘버를 붙이기 때문에 버그의 재현 법이 중요.
  56. 56. 코드 예:
  57. 57. 스레딩 사례 소개 Case Study
  58. 58. 렌더링 리소스 로딩 필연적으로 비동기 작업이다. 보통, 렌더 스레드가 리소스를 요청하지만, 렌더 스레드는 디바이스 자원을 생성할 수 없다. 여러 스레드가 참여하는 파이프라인 구조가 필요하다. 텍스쳐 로딩 파이프라인 구현 사례 소개.
  59. 59. 텍스쳐 로딩 과정 개요 Texture Descriptor Loading Texture Requested Cache Generation File Loading Device Texture Creation Texture Locking memcpy() Texture Unlocking Finalization Rendering!
  60. 60. 디스크립터 로딩Texture Descriptor Loading 텍스쳐에 대한 정보가 담긴 작은 파일을 바로 로딩한다. 텍스쳐 크기, 형식 등은 요청 즉시 반환한다. 32*32 썸네일Thumbnail도 같이 로딩해서 로딩 완료 전까지 사용한다. 렌더 스레드에서 실행.
  61. 61. 캐시 파일 생성 개발 클라이언트 전용 기능. 백그라운드 스레드에서 실행한다. 어셋 저장소Repository에는 무압축 원본 텍스쳐만 올리고, 런타임에 텍스쳐를 가공한다. 매번 변환하면 너무 느리므로 캐시를 만들어둔다. Cache Generation 디스크립터, 캐시가 없다면 이 단계에서 바로 생성한다.
  62. 62. 파일 로딩 백그라운드 스레드에서 실행. File Loading 텍스쳐 파일을 열고 원하는 밉맵을 메모리로 복사한다.
  63. 63. 디바이스 텍스쳐 생성 디바이스 스레드에서 실행. D3D 텍스쳐 객체를 생성한다. Device Texture Creation
  64. 64. 텍스쳐 락 걸기 디바이스 스레드에서 실행. 텍스쳐에 락을 걸고 밉맵 복사를 준비한다. Texture Locking
  65. 65. 이미지 복사 백그라운드 스레드에서 실행. 락을 걸고 받아 둔 주소에 파일 내용을 복사한다. memcpy()
  66. 66. 텍스쳐 락 풀기 디바이스 스레드에서 실행. 언락한다. 텍스쳐를 사용할 준비를 거의 다 했다. Texture Unlocking
  67. 67. 마무리 조인 구간에서 실행. Finalization 각종 자원 해제 및 뒷정리 단계. 파이프라인이 중간에 취소되어도 이 단계는 무조건 실행된다.
  68. 68. 렌더링 리소스 로딩 - 정리 렌더링 리소스 로딩은 여러 스레드를 통과하는 복잡한 작업이다. 스레드 어피니티를 고려하는 파이프라인 구현이 필요. 찾아봐도 용도에 딱 맞는 구현이 없다. 만들기 어렵지 않아서 다행이다.
  69. 69. 종료 처리 1/6 게임 루프 전환, 종료 시 커맨드 큐의 정리는 언제나 골치 아프다. 히스토리 채널. Vikings. 2013 대충 종료하면 발할라에 갈 수 없다.
  70. 70. 종료 처리 2/6 실행할 것이냐 그냥 버릴 것이냐 쌓인 커맨드의 처리 문제 그냥 버린다 / 바로 실행한다 / 큐에 넣는다 종료 처리 중에 발생하는 커맨드 커맨드 내부 객체가 산 넘고 물 건너서 커맨드 큐 참조까지 들고 있는 경우. 순환 참조
  71. 71. 종료 처리 3/6 있으면 제보 바람… 우리는 무식하게 Case by Case로 처리했다. 은탄환은 역시 없다.
  72. 72. 종료 처리 4/6 종료 중에 추가되는 커맨드는 바로 실행.
  73. 73. 종료 처리 5/6 종료 시 특별한(…) 조인 처리 종료 시 조인을 두 번 한다! 코드 보고 나도 놀람.
  74. 74. 종료 처리 6/6 종료시 명시적으로 순환 참조를 끊는다.
  75. 75. 미들웨어 통합 사례 Middleware Integration
  76. 76. 스레드에 안전하지 않다! 최신 버전에서는 아래 내용이 다 쓸데없을 지도… 진짜 예전 버전을 쓰고 있다(1.7.X)!!!! 스트링 캐시 테이블 등이 그냥 생 전역변수. 그래서 싱글 스레드로 처리 중요한 캐릭터만 표정을 사용하므로 아직까지는 별 문제 없었다. 런칭 후에 왠지 폭탄이 터질 것 같다…
  77. 77. 인스턴스 단위로 스레드에 안전하다. 병렬 처리에 문제 없다 버전 4.X 사용 중.
  78. 78. 버전 7.X 사용 중. (구) Kynapse World 단위로 스레드 안전성을 보장한다. 역시 별 문제 없다.
  79. 79. 사용하는 스레드가 제한적이라서 별 문제가 없다.
  80. 80. 문서화도 잘 되어있고 아무튼 좋다. 일반적으로 스레드에 안전하지 않지만, 레이 캐스팅 같은 일부 함수들은 안전하다.
  81. 81. 버전 4.X를 사용 중. 3.X는 스레드에 안전하지 않다. sdk 렌더러 소스를 싹 고쳐서 모든 디바이스 접근을 커맨드 버퍼링 했었다. (경험있고 유능한) 프로그래머라면 2~3일 걸린다. 웬만하면 최신 버전을 사는 것이 정신 건강에 좋다. 4.X는 스레드에 안전하다. 모든 렌더링 명령이 내부 커맨드 큐에 쌓인다  업데이트와 렌더링을 다른 스레드에서 실행할 수 있다. 따로 해줄 것이 거의 없다.
  82. 82. 버전 6.X 사용 중 버전 5.X 디바이스 스레드에서 실행했기 때문에, 모든 sdk 접근을 콜백으로 해야 해서 코드가 매우 복잡했다. 버전 6.X sdk의 렌더러 API를 재구현해서 렌더 스레드에 붙였다. 스레드에 안전한 것 같다. • 통합한지 얼마 되지 않아서 확신이 없다.
  83. 83. 정리 Summary
  84. 84. M2 클라이언트 스레딩 아키텍쳐 이 상황인 분들에게 조금이라도 도움이 되길 바랍니다. 오래된 프로젝트 + 자체 엔진 + 멀티 스레딩 구현 이 발표 내용 정도만 구현해도 쿼드코어까지 잘 지원한다. 게임 로직 병렬화를 잘 연구하면 진정한 태스크 병렬화도 가능할 것이다. 멀티 스레딩, 어렵지 않다.
  85. 85. 참고자료 References
  86. 86. “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.
  87. 87. QnA 발표자료는 devcat.nexon.com/publication.html
  1. 1. M2 클라이언트 스레딩 아키텍쳐 넥슨 코리아 N스퀘어개발본부 1실 GTR팀 전형규 henjeon@nexon.co.kr 2013.04.24
  2. 2. 발표자 소개 넥슨 10년차 프로그래머 현재 GTR팀 팀장 참여 프로젝트 마비노기 XBOX360 마비노기 마비노기 2 주요 관심사 Computer Graphics Real-Time Rendering
  3. 3. 발표 내용 M2 클라이언트의 멀티 스레딩 구조를 설명 스레딩 사례 소개 미들웨어 통합 가이드 다루지 않는 것 서버 스레딩 멀티 스레드 API 사용법 각종 잠금 장치 사용법 락프리(Lock-Free) 알고리즘
  4. 4. M2 소개 M2 엔진 이름 사용 중인 미들웨어들
  5. 5. 자주 등장하는 용어 M2 : 마비노기2 클라이언트 프로그램 스레드Thread , 싱글 스레드Single Thread, 멀티 스레드Multithread 조인Join : 동기화 리졸브Resolve : 커맨드 큐Command Queue 실행 병렬화Parallelization 작업Task 로직Logic : 게임 로직 코드 스레드 안전성Thread Safety 코어Core 디바이스Device : Direct3D 디바이스
  6. 6. 개요 Overview
  7. 7. 개발 방향 매우 보수적이고 안전한 스레딩 모델을 택함 싱글 스레드 구조로 시작한 오래된 프로젝트. 프로젝트 시작 시 상용엔진을 사용하다가 제거하고 직접 제작했다.
  8. 8. 구조 개요 기능 수준 병렬화Functional Parallelization 로직 -> 렌더 -> D3D의 파이프라인 일부 작업들은 데이터 병렬화Data Parallelization 애니메이션, 파티클, … 두 개의 백그라운드 작업 스레드 작업 우선 순위 차이 평균 3개의 코어를 사용, 일부 구간에서 모든 코어 사용
  9. 9. 압니다, Task Parallelism 하면 좋지요… 그러나… 경험 있는 프로그래머가 많이 필요. 작업 분할Task Partitioning 은 어렵다. 디버깅이 어렵다. 물론 익숙해지면 쉽겠지만… 미들웨어 통합은 어떻게? 업계 표준 작업 관리 라이브러리가 필요.
  10. 10. 두 가지가 없다 메인 스레드가 없다. 프로세스가 생성될 때 만들어진 기본 스레드라는 의미만 존재.. 전담Dedicated 스레드가 없다. 대신, 스레드 타입이 있다: 로직, 애니메이션, 렌더, 백그라운드 예외: 디바이스 스레드(윈도 및 D3D 디바이스가 생성된 스레드)
  11. 11. 스레드 관리 Intel TBB(Threading Building Blocks) 사용. TBB가 아닌 OS 스레드를 사용하는 영역 • 백그라운드 작업 디스크 IO 비동기 처리 • 전담 스레드 미들웨어 내부에서 생성하는 작업 스레드 네트워크 IO 작업 스레드 시스템 로더(부팅)
  12. 12. 스레드 통신 메시지(커맨드) 큐를 사용한 단방향 통신 로직 -> 렌더 -> 디바이스 Single Reader, Multiple Writer 역방향 통신은 조인 구간에서 처리. 로직과 디바이스 스레드는 서로 통신하지 않는다. 디바이스 스레드는 메시지를 받기만 한다.
  13. 13. 렌더링은 로직보다 1프레임 늦게 간다 로직은 렌더링 객체에 직접 접근할 수 없다. 프록시Proxy를 통해서 간접적으로 접근한다. 조인 구간에서 직접 접근할 수 있다. 로직에서 처리한 렌더링 작업은 그 다음 프레임에서 실행된다. 프록시 객체마다 더블 버퍼링되는 커맨드 큐를 갖고 있다. 최소 1프레임, 최대 2프레임의 지연이 발생. 1(로직->렌더) + 1(입력 지연. 있거나 없거나 함) 60fps 기준으로 18~36ms 사람이 인지할 수 있는 지연 시간은 평균적으로 100ms이라고 함 • 프로게이머, 굇수 제외
  14. 14. 렌더 – 디바이스 스레드 통신 두 가지 모드가 있다. 싱글 버퍼링(기본값) 더블 버퍼링 싱글 버퍼링(푸시 버퍼Push Buffer) 모드 잠시 버퍼링한 후에 디바이스 커맨드 버퍼로 전송. 더블 버퍼링 모드 커맨드를 쌓아둔 후에 조인할 때 디바이스 커맨드 버퍼와 교체. 즉, 모든 명령어는 다음 프레임에 실행된다.
  15. 15. 두 가지 방식의 특징 싱글 버퍼링은 약간 느리다. 렌더 스레드에서 최초의 명령을 보내기까지 수 ms 걸린다. 이 시간 동안 디바이스 스레드가 공회전한다. 더블 버퍼링은 결과가 1프레임 지연된다. 60fps 이상에서는 거의 느껴지지 않는다.
  16. 16. 런타임에 모드를 선택 일명, 스마트 버퍼링 60fps 이상에서는 더블 버퍼링한다. 그 미만에서는 싱글 버퍼링.
  17. 17. 자원의 스레드 안전성 대부분의 자원은 특정 스레드에서만 사용되므로 안전하다. 생성 후에 그 내용을 변경할 수 없다. 내용을 변경하고 싶다면 사본을 생성한다. 동시에 사용하는 자원은 불변Immutable 타입으로 제한. 1/3
  18. 18. 자원의 스레드 안전성 2/3 생성된 스레드와 파괴된 스레드가 다를 수 있다. 참조 카운터로 자원의 수명을 관리한다. D3D 자원이 좋은 예. 특정 스레드에서 파괴되어야 하는 자원에 주의하자.
  19. 19. 실행 중인 스레드가 디바이스 스레드인지 조사 자원의 스레드 안전성 3/3
  20. 20. 세부 사항 Implementation Details
  21. 21. Frame N Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Animation Join Logic Physics RenderQueue Resolve Cull Particle Particle Frame N+1 Render Background Worker #2 Background Worker #1 D3DDevice Validation M2 클라이언트 태스크 다이어그램(2013/04/24)
  22. 22. 작업의 스레드 타입 ThreadType::Simulation ThreadType::Animation ThreadType::Render ThreadType::Background 모든 태스크는 다음 타입 중 하나 ‘이상’을 가진다. 로직 = Simulation 애니메이션 Simulation | Animation 렌더 = Render 백그라운드 = Background 조인 = 전부 다(ThreadType::DontCare) 타입 예:
  23. 23. 스레드 어피니티Affinity Windows API를 호출하는 작업 D3D를 사용하는 작업 어떤 작업은 반드시 정해진 스레드에서 실행되어야 한다. 다음 작업들은 디바이스 스레드에서 실행된다. Debug Console Processing Input Devices Processing D3DQueue Resolve D3DDevice Validation 나머지 작업들은 어피니티가 없다.
  24. 24. TBB와 스레드 타입 디바이스, 백그라운드 작업은 직접 생성한 스레드에서 실행. 좀 더 세밀한 제어가 필요하기 때문. 나머지 작업들은 모두 TBB 스케줄러로 실행한다.
  25. 25. D3DDevice Validation 디바이스 검사 D3D 디바이스 로스트, 창 크기 변경 등의 이벤트를 처리한다. 자주 발생하는 이벤트가 아니므로 싱글 스레드로 처리한다.
  26. 26. Debug Console Processing 디버그 콘솔 처리 개발 전용 기능들을 처리한다. 싱글 스레드로 실행한다. 빠르고 쉽게 만들 수 있다. 디버깅하기 쉽다. 배포 버전에서 제거되므로 유저 경험에 영향을 주지 않는다.
  27. 27. 입력장치 처리Input Devices Processing Windows API의 스레딩 문제를 피하기 위해서, 싱글 스레드로 실행. 수행 시간이 1ms 안쪽이므로 성능에 영향이 없다.
  28. 28. Frame N Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Animation Join Logic Physics RenderQueue Resolve Cull Particle Particle Frame N+1 Render Background Worker #2 Background Worker #1 D3DDevice Validation M2 클라이언트 태스크 다이어그램
  29. 29. 1/4로직Logic 모든 게임 로직은 이 태스크에서 실행된다. 게임 상태 갱신 네트워킹 AI 사운드 UI
  30. 30. 2/4로직Logic 렌더링 명령은 프록시 객체를 통해서 처리 실제 객체는 렌더 스레드 안에 있다. 프록시 객체는 큐에 명령을 쌓아두기만 한다. 명령 실행은 에서. 더블버퍼링된다: 조인 구간에서 버퍼 교체. RenderQueue Resolve 프록시 커맨드 큐는 객체마다 존재 최대 병렬화를 위해서. 큐에 들어온 순서대로 실행되지 않기 때문에 비결정론적nondeterministic이다.
  31. 31. 3/4로직Logic 프록시 객체는 단순히 메시지만 전달Delegation 실제 수행 전달
  32. 32. 4/4로직Logic 상태 질의state query는 금지. 어쩔 수 없이 필요한 경우에는… 조인 구간에서 동기화 하거나 프록시에 동일하게 구현
  33. 33. 물리Physics 로직 스레드 타입이다. ThreadType::Physics 이런 거 없다. 오직 로직 스레드에서만 물리를 처리한다. 렌더 스레드는 물리를 모른다. 레이 캐스팅은 예외 • raycastClosestShape() 등의 몇 가지 PhysX 함수들은 스레드에 안전. • 길찾기, 캐릭터 IK(Inverse Kinematics), 파티클 등에서 사용.
  34. 34. 1/3애니메이션Animation 캐릭터 단위로 병렬 처리한다. tbb::parallel_for()를 쓰면 간단하지만… 작업 스레드 수를 제어하고 싶어서 간단하게 직접 구현. 캐릭터 목록이 담긴 배열의 주소를 각 태스크가 경쟁적으로 증가시킨다.
  35. 35. 2/3애니메이션Animation 캐릭터 사이의 의존성 분석이 필요 서로 참조하고 있는 캐릭터들을 동시에 처리하면, race가 발생하거나 크래시될 수 있다. 의존성 있는 캐릭터들은 하나의 태스크에서 순서대로 처리한다.
  36. 36. 3/3 Animation 코드 예: 애니메이션
  37. 37. Frame N Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Animation Join Logic Physics RenderQueue Resolve Cull Particle Particle Frame N+1 Render Background Worker #2 Background Worker #1 D3DDevice Validation M2 클라이언트 태스크 다이어그램
  38. 38. 로직 스레드에서 전송한 커맨드를 실행한다. 렌더링 큐 리졸브RenderQueue Resolve
  39. 39. 코드 예:
  40. 40. 컬링Cull 일반적인 CPU 시야 컬링 View Frustum Culling 수행. GPU로 가려진 물체 컬링Occlusion Culling 수행 Hierarchical Occlusion Map 기반. 자세한 설명은 생략. 커서 선택Picking 처리도 이 구간에서 수행된다.
  41. 41. 파티클Particle 애니메이션과 동일하게 병렬화했다. 로직과 밀접한 관계가 있는 파티클 타입은 에서 처리한다.Logic 예: 동전 효과
  42. 42. 1/3렌더링Render 장면 업데이트, 렌더링 파이프라인 실행. 디바이스 커맨드 큐로 명령어들을 전송한다. 코드 예:
  43. 43. 2/3렌더링Render 미룰 수 있는 작업은 최대한 디바이스 스레드로 미룬다. 게을러서 병목이 아니라서 지켜보고 있는 중. 병렬화할 여지가 많다
  44. 44. 3/3렌더링Render D3D 디바이스 자원에 직접 접근하고 싶을 때 콜백Callback을 사용. 코드 예:
  45. 45. 디바이스 큐 리졸브D3DQueue Resolve 렌더 스레드가 전송한 명령어들을 실행한다. 실제로 D3D를 사용하는 구간이다. 프레임 버퍼 크기 변경 처리 윈도 크기 변경 등으로 프레임 버퍼 크기가 변경되면, 명령어가 생성된 시점의 버퍼 크기와 실제 크기가 다르다. 그냥 억지로 렌더링한 다음에 프레임 버퍼를 검게 칠한다.
  46. 46. 1/2 Join 조인 각 작업의 상태를 동기화하는 단계로, 이 구간을 가급적 빨리 실행해야 성능에 유리하다. 스레드에 안전한 구간 각종 코드들의 축제가 벌어진다. 현재 이 코드는 남아 있지 않다…
  47. 47. 2/2 Join 조인 커맨드의 실행 순서에 주의!! 스레드에 안전하다고 커맨드 큐 없이 바로 상태를 조작하면, 실행 순서가 역전된다. 이를 방지하기 위해서 조인 구간에서 프록시의 본체에 접근할 경우, 커맨드 큐를 먼저 실행한다. • 의도하지 않은 커맨드 큐 실행이 발생할 수 있다.
  48. 48. Background Worker 백그라운드 작업 비동기Asynchronous 처리, 파일 IO 작업. 두 작업 스레드가 있다 낮은 우선순위 작업 • 리소스 로딩, 파일 IO • 이미지 업로드 높은 우선순위 작업 • 캐릭터 커스터마이징
  49. 49. 정리 Debug Console Processing Input Devices Processing D3DQueue Resolve Animation Join Logic Physics RenderQueue Resolve Cull Particle ParticleRender Background Worker #2 Background Worker #1 D3DDevice Validation Animation 가로축이 시간, 세로 축이 스레드 이게 더 보기 쉽잖아…
  50. 50. 디버깅 Debugging Tips
  51. 51. 스레드 타입 검사 다른 스레드에서 실행되는 코드에 접근할 수 없도록 설계를 잘 해야 한다. 그래도 어떻게든 용케 방법을 찾아서 호출하더라… 작업 시작 시 자기 타입을 TLSThread Local Storage에 기록하고, 주요 함수마다(…) 매크로 함수로 스레드 타입을 검사. 매우 위험하고 실수의 여지가 많다.
  52. 52. 스레드 프로파일링 스레드 작업 막대 그래프 1/2
  53. 53. 스레드 프로파일링 타이밍 그래프 2/2
  54. 54. 스레딩 옵션 변경 최소한 시작 옵션으로 지정할 수 있어야 한다. 런타임에 변경할 수 있으면 스레딩 성능 비교 및 디버깅에 유용. 여러 개의 클라이언트를 실행할 경우, 싱글 스레드 모드가 편하다.
  55. 55. 커맨드 생성 위치 추적 커맨드가 생성되는 시점과 실행 시점이 다르다. 커맨드 실행 코드에 브레이크 포인트를 걸어봤자 얻는 것이 별로 없다. 커맨드를 추가하는 위치가 더 중요하다. 커맨드에 ID를 붙이고, 특정 ID를 생성하는 위치를 찾는다. 자원 생성 커맨드는 유니크 id를 부여하기 쉽다. 일반 커맨드는 시퀀스 넘버를 붙이기 때문에 버그의 재현 법이 중요.
  56. 56. 코드 예:
  57. 57. 스레딩 사례 소개 Case Study
  58. 58. 렌더링 리소스 로딩 필연적으로 비동기 작업이다. 보통, 렌더 스레드가 리소스를 요청하지만, 렌더 스레드는 디바이스 자원을 생성할 수 없다. 여러 스레드가 참여하는 파이프라인 구조가 필요하다. 텍스쳐 로딩 파이프라인 구현 사례 소개.
  59. 59. 텍스쳐 로딩 과정 개요 Texture Descriptor Loading Texture Requested Cache Generation File Loading Device Texture Creation Texture Locking memcpy() Texture Unlocking Finalization Rendering!
  60. 60. 디스크립터 로딩Texture Descriptor Loading 텍스쳐에 대한 정보가 담긴 작은 파일을 바로 로딩한다. 텍스쳐 크기, 형식 등은 요청 즉시 반환한다. 32*32 썸네일Thumbnail도 같이 로딩해서 로딩 완료 전까지 사용한다. 렌더 스레드에서 실행.
  61. 61. 캐시 파일 생성 개발 클라이언트 전용 기능. 백그라운드 스레드에서 실행한다. 어셋 저장소Repository에는 무압축 원본 텍스쳐만 올리고, 런타임에 텍스쳐를 가공한다. 매번 변환하면 너무 느리므로 캐시를 만들어둔다. Cache Generation 디스크립터, 캐시가 없다면 이 단계에서 바로 생성한다.
  62. 62. 파일 로딩 백그라운드 스레드에서 실행. File Loading 텍스쳐 파일을 열고 원하는 밉맵을 메모리로 복사한다.
  63. 63. 디바이스 텍스쳐 생성 디바이스 스레드에서 실행. D3D 텍스쳐 객체를 생성한다. Device Texture Creation
  64. 64. 텍스쳐 락 걸기 디바이스 스레드에서 실행. 텍스쳐에 락을 걸고 밉맵 복사를 준비한다. Texture Locking
  65. 65. 이미지 복사 백그라운드 스레드에서 실행. 락을 걸고 받아 둔 주소에 파일 내용을 복사한다. memcpy()
  66. 66. 텍스쳐 락 풀기 디바이스 스레드에서 실행. 언락한다. 텍스쳐를 사용할 준비를 거의 다 했다. Texture Unlocking
  67. 67. 마무리 조인 구간에서 실행. Finalization 각종 자원 해제 및 뒷정리 단계. 파이프라인이 중간에 취소되어도 이 단계는 무조건 실행된다.
  68. 68. 렌더링 리소스 로딩 - 정리 렌더링 리소스 로딩은 여러 스레드를 통과하는 복잡한 작업이다. 스레드 어피니티를 고려하는 파이프라인 구현이 필요. 찾아봐도 용도에 딱 맞는 구현이 없다. 만들기 어렵지 않아서 다행이다.
  69. 69. 종료 처리 1/6 게임 루프 전환, 종료 시 커맨드 큐의 정리는 언제나 골치 아프다. 히스토리 채널. Vikings. 2013 대충 종료하면 발할라에 갈 수 없다.
  70. 70. 종료 처리 2/6 실행할 것이냐 그냥 버릴 것이냐 쌓인 커맨드의 처리 문제 그냥 버린다 / 바로 실행한다 / 큐에 넣는다 종료 처리 중에 발생하는 커맨드 커맨드 내부 객체가 산 넘고 물 건너서 커맨드 큐 참조까지 들고 있는 경우. 순환 참조
  71. 71. 종료 처리 3/6 있으면 제보 바람… 우리는 무식하게 Case by Case로 처리했다. 은탄환은 역시 없다.
  72. 72. 종료 처리 4/6 종료 중에 추가되는 커맨드는 바로 실행.
  73. 73. 종료 처리 5/6 종료 시 특별한(…) 조인 처리 종료 시 조인을 두 번 한다! 코드 보고 나도 놀람.
  74. 74. 종료 처리 6/6 종료시 명시적으로 순환 참조를 끊는다.
  75. 75. 미들웨어 통합 사례 Middleware Integration
  76. 76. 스레드에 안전하지 않다! 최신 버전에서는 아래 내용이 다 쓸데없을 지도… 진짜 예전 버전을 쓰고 있다(1.7.X)!!!! 스트링 캐시 테이블 등이 그냥 생 전역변수. 그래서 싱글 스레드로 처리 중요한 캐릭터만 표정을 사용하므로 아직까지는 별 문제 없었다. 런칭 후에 왠지 폭탄이 터질 것 같다…
  77. 77. 인스턴스 단위로 스레드에 안전하다. 병렬 처리에 문제 없다 버전 4.X 사용 중.
  78. 78. 버전 7.X 사용 중. (구) Kynapse World 단위로 스레드 안전성을 보장한다. 역시 별 문제 없다.
  79. 79. 사용하는 스레드가 제한적이라서 별 문제가 없다.
  80. 80. 문서화도 잘 되어있고 아무튼 좋다. 일반적으로 스레드에 안전하지 않지만, 레이 캐스팅 같은 일부 함수들은 안전하다.
  81. 81. 버전 4.X를 사용 중. 3.X는 스레드에 안전하지 않다. sdk 렌더러 소스를 싹 고쳐서 모든 디바이스 접근을 커맨드 버퍼링 했었다. (경험있고 유능한) 프로그래머라면 2~3일 걸린다. 웬만하면 최신 버전을 사는 것이 정신 건강에 좋다. 4.X는 스레드에 안전하다. 모든 렌더링 명령이 내부 커맨드 큐에 쌓인다  업데이트와 렌더링을 다른 스레드에서 실행할 수 있다. 따로 해줄 것이 거의 없다.
  82. 82. 버전 6.X 사용 중 버전 5.X 디바이스 스레드에서 실행했기 때문에, 모든 sdk 접근을 콜백으로 해야 해서 코드가 매우 복잡했다. 버전 6.X sdk의 렌더러 API를 재구현해서 렌더 스레드에 붙였다. 스레드에 안전한 것 같다. • 통합한지 얼마 되지 않아서 확신이 없다.
  83. 83. 정리 Summary
  84. 84. M2 클라이언트 스레딩 아키텍쳐 이 상황인 분들에게 조금이라도 도움이 되길 바랍니다. 오래된 프로젝트 + 자체 엔진 + 멀티 스레딩 구현 이 발표 내용 정도만 구현해도 쿼드코어까지 잘 지원한다. 게임 로직 병렬화를 잘 연구하면 진정한 태스크 병렬화도 가능할 것이다. 멀티 스레딩, 어렵지 않다.
  85. 85. 참고자료 References
  86. 86. “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.
  87. 87. QnA 발표자료는 devcat.nexon.com/publication.html

More Related Content

Slideshows for you

Viewers also liked

More from devCAT Studio, NEXON

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

×