10. SIMD 정말 4배 빠른가요? // C 버젼 for(size_ti=0; i<count;++i) { b[i] = a[i] + a[i]; } -> 실행 시간 49.267 ms // Compiler Intrinsic 버젼 for(size_ti=0; i<count/4;++i) { b4[i] = a4[i] + a4[i]; } -> 실행 시간 47.927 ms
20. _mm_stream_ps() // C 버젼 for(size_ti=0; i<count;++i) { b[i] = a[i] + a[i]; } -> 실행 시간 49.267 ms // a+a stream 버젼 for(size_ti=0; i<count/4;++i) { _mm_stream_ps((float*)(b4+i), _mm_add_ps(a4[i], a4[i])); } -> 실행 시간 30.114 ms
21. CPU _mm_stream_ps() 의 작동 Excution Unit L1 Cache L2 Cache WC Buffer Memory BUS Memory
25. 같은 시간에 더 많은 일을 합시다!! float Read + Write 시간 : 2.896 ns __m128 Read + Write 시간 : 11.214 ns __m128 Read + Stream 시간 : 6.977 ns
26. SSE 프로그래밍 메모리 접근 시간이 길어지고 연산시간이 짧아짐에 따라 더 많은 계산을 할 수 있다. 요즘 CPU는 Out-of-Order 로 인해 대부분 비동기 실행을 한다. 적극 이용하자. 병렬화와 병목 문제는 GPGPU 연산에도 동일하게 적용된다. 미래를 대비하자.!!
28. SSE 를 사용한 CPU Skinning Vertex : 1024 * 1024 Bone : 200 4 weight per vertex + normal + tangent SSE 컴파일 옵션이 켜진 C, SSE최적화 스키닝 없는 C 루프 복사, SSE 루프 복사, memcpy()
37. Scaleform과 SSE Flash 파일을 3D 가속을 받으며 실행 가능하도록 만들어진 라이브러리 Direct3D/OpenGL 및 다양한 렌더링 라이브러리 지원 현재 프로젝트의 UI 제작에 사용 209개 파일 65147 Line 의 Acton Script 와 DXT5 79MB UI 이미지
38. Scaleform 3.1 의 문제점 복잡한 swf들을 다수 사용할 경우 CPU 사용률이 상당히 높다. 높은 자유도가 GPU에 최적화 되기 어려운 UI 를 만들게 한다. GRendererD3D9 은예제 코드에 가깝고 개발시 H/W 특성이 고려되지 않았다.
40. GFxQueue의 Batch 합치기 기능 Batch 합치기를 하기 위해 Vertex 를 Queue 에 넣을때 Transform (TnL) 을 미리 처리 Render State, Texture State 를 체크해서 중복된 렌더링 재설정을 방지 Scene 에서 벗어난 Shape 들안그리는 기능 추가 CPU로 대체된 VertexShader는 삭제, Pixel Shader도 Batch 합치기를 위해 수정
43. GFx Renderer 코멘트 GRenderD3D9 코드가 구리다. 프로그래머라면 찬찬히 분석한다음 여러군데 손을 봐두자. UI 아티스트는 GPU 최적화에신경쓰지 않는다. 초기 단게부터 적절한 레이아웃과 컴포넌트를 설계해두자. GFxExport에서 DXTn포맷을 무조건 2의 배수로 Resize 해버려 저장하는 경우가 있다. GFxExport에서 Texture Atlas 기능을 쓰는 것도 최적화에 큰 도움이 된다.
이 그림은 일반적인 프로그래밍을 할때 32비트, 64비트 프로그래밍을 할때 사용하는 레지스터들입니다.왼쪽에 있는 RAX 부터 R15까지가 범용 레지스터로 RSP 까지의 흰색 부분이 32비트 범용 레지스터이고 그것을 R15 까지 확장한 것이 64비트 범용 레지스터입니다.가운데 있는 것이 64비트 크기의 FPU/MMX 겸용으로 사용되는 레지스터입니다. FPU는 이것을 80비트까지 확장해서 쓰기도 하는데 64비트 프로그래밍 시에는 이 부분을 사용하지 않도록 권장하고 있습니다.그리고 마지막 SSE 용 128 비트 레지스터인 XMM 레지스터 입니다. 0번부터 7번까지는 32비트에서 사용하고 64비트에서는 15번까지 전부 사용할 수 있습니다.크기로 비교해보면 32비트 모드일때나 64비트 모드일때나 XMM 레지스터 크기가 훨씬 크다는 것을 알 수 있습니다.그만큼 더 많은 일을 시킬 수 있다는 뜻이죠.
XMM 레지스터가 이렇게 거대해진 이유는 SIMD 연산을 하기 위해서 입니다.SIMD 는 Single Instruction Multi Data 의 약자라고 했는데 이 그림을 보면 쉽게 이해하실 수 있을겁니다.왼쪽에 있는 것이 일반적인 연산이라면 오른쪽 같이 4개를 한꺼번에 처리하는 것이 SIMD 처리입니다.SIMD는 하나의 명령으로 여러개의 데이터를 동시에 처리하기 때문에 다량의 데이터를 처리해야할때 매우 효율적입니다.
이렇게 빠른 작동을 할 수 있는 것은 Write Combining 버퍼라는 것 때문입니다.처음 소개된 것은 PCI 버스 시절인데 버스에 접근 할때는 캐쉬를 끄고 접근하기 때문에 접근 속도가 매우 느려지는데그것을 보완하기 위해서 메모리 컨트롤러단에 몇십 바이트 크기의 쓰기 버퍼를 만들어 놓고 캐쉬를 통하지 않는 쓰기 작업을 도와주도록 한 것입니다.이것은 그래픽 카드에 많이 쓰였고 옛날 Direct Draw 프로그래밍 시절 Surface 를 읽어오는 것에 비해 쓰는 것이 월등히 빨랐던 것도 WC 버퍼 덕분입니다._mm_stream_ps() 명령은 이 WC 버퍼를 통해 데이터 쓰기 작업을 도와줍니다.
SSE 프로그래밍을 사용한 예제를 세가지 준비했습니다.모든 예제들은 실제 상용화된 프로젝트나 내부 툴 코드에 사용된 경험이 있는 것들이고 더 자세한 정보가 필요하신 분들은 개별적으로 접촉하시면 상세한 정보를 나눠드릴 수 있습니다.
마지막으로 Scaleform최적화 부분입니다.사실 Scaleform최적화는 별도 세션으로 다루고 싶었는데 준비 시간 문제도 있었고저는 스케일폼보다 SSE 최적화쪽이 훨씬 익숙하기 때문에 아직 준비가 덜 되었다고생각되어서 이렇게 이 밑에 붙였습니다.스케일폼은 게임내에서 매크로 미디어 플래쉬 파일을 구동할 수 있게 해주는 라이브러리입니다. 요즘 게임 UI 개발에 많이 사용하고 있고 저희 팀도 꽤 일찍 도입해서 아주 많은부분을 GFx에 의지하고 있습니다.참고로 저희 프로젝트에서는 SWF 파일이 209개이고 액션 스크립트만 65000라인, 그리고배경 이미지를 제외한 UI 컴포넌트에 쓰인 이미지만 압축텍스쳐로 79MB에 달합니다.
Scaleform을 최적화 하게된 이유는 저희 프로젝트에서 아주 많은 부분을이 라이브러리에 의존하고 있으며 덕분에 상당히 많은 부하가 GFx에걸리고 있기 때문입니다. 최적화 이전에 부하가 심할 경우 15ms 정도가 꾸준히GFx에 의해서 사용되었고 GFx하나만으로 프레임 레이트가 팍팍 떨어지는문제를 발생시키고 있었습니다.어쨌거나 GFx라이브러리는 방대하고 코드 핵심 부분은 고치기 어려운 만큼소극적인 방법으로 최적화를 진행했고 여러가지 방법을 동원해서 게임에 부하를획기적으로 줄일 수 있도록 부하를 분산할 수 잇었습니다.이자리를 빌어 GFx최적화 아이디어를 주신 전 네오위즈 최의종 팀장님께 감사의말씀을 드립니다.
GFx의 최대 문제점은 Display 를 호출한 시점에 여러가지 CPU 연산을 수행하고 그 다음 3D 렌더링이 진행된다는 점입니다. CPU 부하가 적은 프로젝트에서는 큰부하가 되지 않지만 CPU 부하가 심할 경우 이러한 부하는 매우 부담스럽습니다.저희 프로젝트의 경우 최종적으로 전체 swf파일을 그리는 Display() 를 호출했을때5~15ms 까지의 시간이 걸렸었습니다. 여기서 아주 많은 UI 컴포넌트들을 그리는데개중의 많은 시간은 SWF 파일들을 돌아다니며 애니메이션을 처리하고 좌표를 정하는등의 연산 작업이었습니다.게다가 문제는 이 부하는 게임에서 전투가 격해지고 캐릭터들이 화면에 많이 나올때 더더욱 심해졌다는 점입니다. 15ms 정도라면 상당히 긴 시간이라서 GFx렌더링 만 하더라도 60fps 를 겨우 그릴 수 있는 상태였습니다.저희 팀에서는 이 문제를 GRenderer를 멀티쓰레드화 시키는 것으로 해결하였습니다. GFx라이브리 자체는 매우 크고 복잡하기 때문에 쉽게멀티쓰레드화 시키지 힘들지만 다행히도 Display() 함수를 호출한 다음은GFx라이브러리 외부와의 호출이나 데이터 접근이 존재하지 않고 단지화면 출력을 위한 Direct3D 호출만이 이뤄지고 있었습니다.따라서 Display() 함수 자체를 다른 쓰레드로 옮기고 화면에 렌더링하고자하는 데이터들을 전부 큐에 넣은 다음 게임 화면 렌더링이 끝나면 큐에들어있는 데이터를 한번에 그리도록 하였습니다. 이것은 DirectX 11 에 추가된 큐와 매우 흡사한 버젼입니다.
이 큐는 단순히 Direct3D함수 호출만을 큐하는 것이 아니라 미리 전처리 할 수 있는 것들을처리해두는 기능을 추가하였습니다. 가장 간단한 것으로 RenderState가 중복되어 설정되는것을 막아주고 동일한 텍스쳐와 쉐이더를 사용하는 오브젝트들을 한꺼번에 그릴 수 있도록배치 처리를 해주었습니다. 또한 UI 제작 아티스트 분들이 UI를 감추기 위해 컴포넌트들을 화면 밖으로 살짝 옮겨놓는 경우가 있는데 이런 것들에 대한 호출도 실행하지 않도록 하였죠.GFx는 버텍스 쉐이더에서 행렬을 써서 UI 의 좌표를 지정했는데 배치 합치기와 클립핑 작업을하기 위해서 버텍스 쉐이더 코드를 전부 CPU 단으로 옮겼습니다. 이 코드들은 SSE 로 재작성되었고 몇몇 픽셀 쉐이더 상수들도 버텍스 데이터를 통해 전달할 수 있도록 바뀌었습니다.
이 코드가 버텍스 쉐이더를 대체한 CPU 코드의 일부입니다. SSE 코딩을 하기 앞서먼저 C 코드로 잘 작동하는 코드를 작성한 다음 SSE 로 옮겼습니다.GFx최적화를 하며 애먹었던게 구형 그래픽 카드인데 프로젝트 스펙상 쉐이더 모델2.0을 지원해야 했고 라데온 9600 과 그 것을 승계한 x1600 같은 그래픽 카드는 Free Vertex Format 을 제대로 지원하지 않앗기 때문에 포맷에 맞춰쳐 Vertex 구조체를 사용해야하는 문제가 발생했습니다. (덕분에 패치 한번 깨먹었습니다. )속도를 위해 16바이트 정렬된 Vertex 를 사용하였었는데 쉐이더 모델 2.0을 위해하드웨어에 맞게 Vertex 정보를 사용하였고 덕분에 _mm_storeu_ps() 같은 명령을쓰게 되었죠.
이 그래프는 큐를 이용함으로써 배치 합치기의 결과를 보여주는 그래프 입니다.왼쪽부터 로그인 화면, 로비 화면, 상점 화면, 대기실, 로딩, 게임 화면, 게임내 특수 UI 화면등을 순서대로 띄웠을 때이고 그래프는 각각 화면에서 Draw Call횟수를 나타내고 있습니다.아주 만족스러운건 아니지만 경우에 따라서 1/3 ~ 1/4 정도 Draw Call 횟수가줄어들었으며 큐를 사용함에 따라 CPU 부하가 전부 다른 쓰레드로 감춰진 관계로 2 Core 이상 있는 머신에서는 프레임 레이트가 대폭 향상 되었습니다.참고로 Call횟수가 기대보다 줄어들지 못한 이유는 UI 설계상 이유도 있는데가량 캐릭터 머리위에 떠있는 이름의 경우 아이콘-이름-아이콘-이름이 차례대로 있는 컴포넌트를 그릴 경우 아이콘들이 같은 텍스쳐에 있더라도 큐는상태가 변경됨에 따라 4번의 Draw Call 별도로 호출해줘야 한다는 점 입니다.특히 폰트 텍스쳐가 다른 아이콘이나 UI 들과 별도로 있는 관계로 그리는 순서에 매우 민감하게 반응합니다. 또한 컴포넌트들을 어떻게 조합하냐에 따라한번에 주루룩 그려주는 경우도 있을 수 있고 전부 별도로 그리는 경우도 있기 때문에 디자인 단계에서 최적화를 고려하지 않으면 코드만으로는개선에 한계가 있습니다.
스케일 폼에 대해 결론을 내자면..일단 스케일 폼에 따라온 GRenderD3D9 코드는 매우 구립니다. 매니지드 텍스쳐도제대로 지원하지 못하고 메모리 관리자도 너무 많은 메모리를 사용합니다. 시간이있다면 차분히 이곳저곳을 분석해서 손을 대줘야 합니다.그리고 UI 아티스트들이 작업하는 초기부터 여러가지 신경써서 작업할 수 있도록해야 합니다. 나중에 손을 대면 늦고 최적화가 힘든 만큼 텍스쳐를 어떻게 쓰고있는지 그리는 순서를 어떻게 하는게 좋은지 미리 미리 알려주고 작업 결과물을틈틈히 체크해 주어야 합니다.GFXExport라는 툴이 있는데 이 툴도 문제가 좀 많습니다. 저희 프로젝트는 압축텍스쳐를 사용하는데 여기 사용된 텍스쳐 압축 라이브러리가 최적화가 꺼져 있다던지 압축을 사용할 경우 무조건 이미지를 2의 배수로 resize 한다던지 문제가 꽤많습니다.그리고 배치 합치기 작업을 계획한다면 GFxExport옵션을 적절히 줘서 텍스쳐아틀라스를 활성화 해주는게 좋습니다. 이게 안되어 있으면 텍스쳐가 바뀌면서모아 그리는게 매우 힘들어지니까요.참고로 멀티쓰레드를 써서 GFx가 소모하는 시간은 전체 렌더링에서 1/4~1/3 정도로 대폭 줄었습니다. DirectX 호출 시간은 줄일 수 없지만 나머지 CPU 작업부분들은 전부 줄일 수 있었습니다.다시한번 아이디어를 주신 최의종 팀장님께 감사의 말을 전하고 저는 이만 발표를 마치겠습니다.