멀티스레드 렌더링 (Multithreaded rendering)

xtozero 2,279 views 61 slides Nov 04, 2021
Slide 1
Slide 1 of 61
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61

About This Presentation

Multithreaded Rendering과 Dynamic Instancing에 대해서 다룹니다.


Slide Content

Multithreaded
Rendering
+Dynamic Instancing
1

목차
•시작하며…
•Multithreaded Rendering 왜해야할까?
•DataRace
•경합해결전략
•Code Reading
•Rendering Command 생성
•Direct3D11 DeferredContext
•Code Reading
•Dynamic Instancing
•동일한물체의정의
•DrawSnapshot
•더자세한코드를보고싶으시다면 …
•참고자료
2

시작하며…
이ppt는Multithreaded Rendering과덤으로Dynamic Instancing에대해서
다룹니다.
이프로젝트는 UE4의코드와다음동영상에서 영감을많이받았으며동영상은
한글자막도제공하므로 한번보시는걸추천합니다 .
https://youtu.be/qx1c190aGhs
3

Multithreaded Rendering 왜해야할까?
현시대에서 코어가하나만달린CPU는찾아보기힘들게되었습니다 . CPU의
발전방향이코어의개수를늘리는것으로바뀐이후로고성능의프로그램의
경우스레드를사용하는것이필수가되었습니다 .
이는렌더링도예외가아닙니다. ‘그런데렌더링은GPU의성능에영향을받는
것이아닌가?’ 라고생각하실수있을것같은데요.
여기서는스레드를사용하지않은경우왜사용률이떨어지는지를 살펴보겠습니
다.
4

Multithreaded Rendering 왜해야할까?
Direct3D11 이전의그래픽API는단일스레드에서 실행되는것에중점을두고
설계되었습니다 .
암시적인동기화지점이존재해서API를호출하고난다음GPU의처리가완료
되었을때이후의코드가실행됩니다 .
다음은단일스레드에서 동작하도록 작성된프로그램의 실행흐름을간략하게
나타낸것입니다.
5
프로그램실행흐름
N프레임CPU계산 N프레임렌더링 N+1프레임CPU계산 N+1프레임렌더링

Multithreaded Rendering 왜해야할까?
문제는이렇게순서를맞춰서진행하는방식이CPU와GPU를최대로사용하지
못한다는점입니다.
CPU 계산중에GPU는제출된명령이없기때문에아무런일도하지않고CPU
의처리를기다리게되고렌더링중에는동기화지점으로인해서CPU는GPU
의처리가끝날때까지기다리게됩니다.
6
프로그램실행흐름
N프레임CPU계산 N프레임렌더링 N+1프레임CPU계산 N+1프레임렌더링
대기 대기 대기

Multithreaded Rendering 왜해야할까?
따라서이렇게CPU와GPU가비효율적으로 사용되지않도록스레드를분리하
여렌더링과CPU 계산이동시에이뤄지도록 해야합니다.(※)
그런데이렇게스레드하나를추가하는정도로는멀티스레드라고 부르기에는
무리가있어보이는데아직여러스레드를사용하여성능을개선할수
있는부분이있습니다.
7
프로그램실행흐름
N프레임CPU계산 N+1 프레임CPU계산
N+1 프레임렌더링N 프레임렌더링
N+2 프레임CPU계산 N+3 프레임CPU계산
N+2 프레임렌더링
※물론Direct3D12, Vulkan과같은최신Graphics API의경우에는비동기가기본이되었기에상황이다릅니다.

Multithreaded Rendering 왜해야할까?
GPU가무언가를그리도록요청하기위해서GraphicsAPI를호출하면다음과
같은순서로그리기명령(Rendering command)이생성되어GPU로제출됩니
다.
8
Graphics API
호출
API의동작에
맞는그리기
명령생성
GPU에제출
GPU에제출
가능한지판단
명령버퍼에
임시보관
바로제출가능
제출불가

Multithreaded Rendering 왜해야할까?
그리기명령을생성하고이를제출하는과정은CPU를통해서처리됩니다 .
그리고이과정이CPU에서처리된다면 이부분을스레드를통해서개선할수
있습니다.
9
Graphics API
호출
API의동작에
맞는그리기
명령생성
GPU에제출
GPU에제출
가능한지판단
명령버퍼에
임시보관
바로제출가능
제출불가

Multithreaded Rendering 왜해야할까?
Multithreaded Rendering은그리기명령을생성하는부분을여러스레드를통
해서빠르게생성하고이를GPU에제출하는것입니다.
10출처: https://software.intel.com/content/www/us/en/develop/articles/understanding-directx-multithreaded-rendering-performance-by-experiments.html

Multithreaded Rendering 왜해야할까?
정리하자면 Multithreaded Rendering은CPU와GPU의사용률을최대로하여
높은성능을얻으려는방법입니다 .
하지만스레드를사용한다면 그에따른대가가따릅니다.
이제멀티스레드에서 렌더링을하기위해처리해야하는이슈에대해서알아보
도록하겠습니다 .
11

Data Race
데이터경합은멀티스레드프로그래밍에서 피할수없는이슈입니다 .
MultithreadedRendering시스레드간의경합에더해GPU와의경합도
발생합니다 .
따라서이런데이터경합이발생하지않도록전략을세워야합니다.
우선렌더링상황에서일어날수있는데이터경합의예시를몇가지생각해보겠
습니다.
12

Data Race
첫번째는스레드간데이터경합입니다 .다음과같이게임로직과렌더링이
별도의스레드에서 이뤄지고있는상황을생각해보도록 하겠습니다 .
그리고여기에는화면에그려질수있는게임오브젝트A가있습니다.
13
Game thread Rendering thread
AA A

Data Race
게임스레드는A에대한게임로직을수행하고렌더링스레드에서는 A를화면에
그립니다.
즉A는두개의스레드가공유하고있는자원입니다 .
14
Game thread Rendering thread
AA A
위치갱신등의게임로직적용
위치와같은데이터참조

Data Race
만약게임로직에따라서A라는오브젝트가 삭제되는데렌더링스레드가A를
참조하고있는상황이라면 게임스레드가A를바로삭제하는것은문제가됩니다.
따라서렌더링스레드가A를참조하지않을때까지A의삭제는유보돼야합니다.
15
Game thread Rendering thread
AA A
모종의이유로A가삭제
삭제된데이터에접근할가능성

Data Race
두번째는GPU와의경합입니다 . GPU에서A를그리기위한셰이더코드가
실행되고있는경우를생각해보겠습니다 .
GPU는그래픽카드메모리로전송된물체의위치나재질을참조하여물체를
어디에어떻게그려야할지를결정합니다 .
16
CPU GPU
물체위치
카메라위치
재질
ETC
A A

Data Race
이런상황에서CPU가A의상태를업데이트하고 이결과를그래픽카드로메모
리로전송하면A를그리고있는도중에참조하고있던데이터의값이변경될수
있습니다.
17
CPU GPU
물체위치
카메라위치
재질
ETC
A A
물리적용등으로위치가
갱신되어이를적용
GPU에서참조하고있는위치
값도갱신되어버림

Data Race
경합해결전략
스레드간의데이터경합그리고CPU와GPU간의데이터경합을해결하기
위한전략은게임의세상을2가지로나누는것입니다.
게임의세상을게임스레드를위한World와렌더링스레드를위한Scene으로
나눕니다.
18
Game
World Scene

Data Race
경합해결전략
Scene은World의복제본인데렌더링에관련된데이터에만 복사해온렌더링을
위한세상입니다 .
그리고게임스레드는World만을수정하고렌더링스레드는Scene만을수정
하도록엄격하게제한합니다 .
19
Game
World Scene
복제
A
B
C
A
B
C

Data Race
경합해결전략
이는프로그래머가 신경을써야할부분이기때문에실수를줄이기위해서다음
과같은도구가사용될수있습니다.
다음은특정스레드에서만 수행되야하는코드가해당스레드에서 실행되는지
검사하여이런제약을준수할수있도록하는예시입니다 .
20

Data Race
경합해결전략
다음과같이게임스레드가월드의A를삭제해도Scene에는영향이없기
때문에삭제된오브젝트에 접근해서문제가발생하는경우를방지할수있습니
다.
그럼Scene의A는어떻게삭제해야할까요?
21
Game
World Scene
복제
A
B
C
A
B
C

Data Race
경합해결전략
A는게임로직에따라서삭제되었습니다 . World는게임스레드에서 수정할수
있으니삭제가가능했지만 Scene은렌더링스레드에의해서만수정되야하기
때문에게임로직에서이를삭제할수없습니다.
그러므로스레드접근제한을준수하기위해서게임스레드는렌더링스레드가
A를삭제하도록 요청해야합니다.
22
Game
World Scene
복제
A
B
C
A
B
C

Data Race
경합해결전략
스레드에대한요청은스레드의전용큐를통해서이뤄집니다 .
A가삭제될경우게임스레드는A에대한삭제요청을렌더링스레드큐에집어
넣고렌더링스레드는적절한때에큐의요청을처리하게됩니다.
23
Game
World Scene
복제
A
B
C
A
B
C
A를제거할것
Rendering Thread 전용Queue

Data Race
경합해결전략
실제코드를통해서렌더링스레드에오브젝트의 삭제를요청하는예시를
보겠습니다 .
24
렌더링스레드에서 RemovePrimitiveSceneInfo() 함수를호출하도록 요청

Data Race
경합해결전략
EnqueueRenderTask() 함수는렌더링스레드에태스크를제출하는함수이며
렌더링스레드에서 호출한경우에는해당태스크를바로실행합니다 .
25
렌더링스레드에서 호출한경우바로실행
전용스레드큐에접근

Data Race
경합해결전략
전용큐는각스레드당하나로제한하였는데 이는다른스레드의요청이순서를
지켜실행돼야하기때문입니다 .
‘A 물체의위치업데이트-> 게임장면그리기-> A 물체의삭제’
와같은요청이순서가보장되지않아
‘A 물체의삭제-> A 물체의위치업데이트-> 게임장면그리기’
와같은순서로실행되면의도하지않은동작이기때문입니다 .
26

Data Race
경합해결전략
이러한방법은게임로직과렌더링을마치클라이언트 서버모델과유사하게
다루게합니다.
27
Client Internet Server
Game
Logic
Queue Rendering

Data Race
Code Reading
이제게임물체가추가될때실행되는코드를보면서지금까지이야기한내용을
정리하도록 하겠습니다 .
새로생성된게임물체는World 클래스의SpawnObject() 함수를통해서게임
세상에추가됩니다 .
28

Data Race
Code Reading
World 클래스는게임스레드에서 참조할수있는게임물체인CGameObject
객체들을보관하고있으며World와쌍을이루는렌더링스레드전용세상인
Scene을참조하고있습니다.
29

Data Race
Code Reading
Scene 클래스는World 와유사하게렌더링스레드에서 참조할수있는게임
물체인PrimitiveSceneInfo 객체를보관하고있으며이객체는게임스레드의
요청에의해Scene에추가되거나 삭제됩니다 .
30

Data Race
Code Reading
게임스레드는렌더링할필요가있는경우에SpawnObject() 함수에서오브젝
트를초기화할때필요한에셋이모두갖춰졌는지 판단하여렌더링스레드에
PrimitiveSceneInfo 객체의추가를요청합니다 .
코드흐름은다음과같습니다.
31

Data Race
Code Reading
32

Rendering Command 생성
그리기작업은때때로순서가중요한경우가있습니다.
예를들면반투명물체와같이카메라에먼순서부터물체를그려야하는경우
(Z Sorting)로순서를보장하기위해서어떻게병렬화를하는것이좋을지고려
해야합니다.
33
출처: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-10-transparency/
50% 투명도를가진두물체를서로
다른순서로그렸을때최종색상이
달라지는것을확인할수있음.

Rendering Command 생성
여기서는전형적인Fork-join모델을사용하여그려야할전체리스트를작업
스레드의개수로나눠처리하였습니다 .
34
출처: https://en.wikipedia.org/wiki/Fork–join_model

Rendering Command 생성
깊이렌더링이나 그림자맵렌더링과같이Z 버퍼를사용할수있는상황에서는
그리기순서가그리중요하지않기때문에다른병렬화전략을취할수있습니다.
예를들면Join시모든태스크의완료를기다리지않고완료된태스크부터 GPU
에명령을제출할수도있습니다.
현재코드는모든스레드를기다리도록 구현되어있지만모든상황에알맞은
방법은아니라는점을언급하고싶습니다.
35

Rendering Command 생성
Direct3D11 Deferred Context
실제코드를보기전에Direct3D11의Deferred Context에대한한가지특이점
을언급하고자 합니다.
GPU에명령을즉시제출하는Immediate Context는일종의상태머신과같아
렌더링파이프라인의 상태를바꾸는명령( RSSetState, OMSetBlendState 등)
을통해상태가변경되면해당상태가계속유지되었습니다 .
예를들어깊이테스트를끄도록했다면다시깊이테스트를키는명령을제출하
기전까지해당상태가유지되어다음그리기에도 영향을미칩니다.
36

Rendering Command 생성
Direct3D11 Deferred Context
하지만Deferred Context의경우는생성시Immediate Context의파이프라인
상태와상관없는기본상태로생성되고Immediate Context에제출해도파이프
라인상태를변경시키지 않습니다.
잠시다음질문을생각해보시기바랍니다.
Q. 이전그리기에서 Immediate Context를통해뷰포트를설정한다음에
Deferred Context를통해기록된그리기명령을Immediate Context에제출했
을때해당명령들은Immediate Context에설정된뷰포트의영향을받을까요?
37

Rendering Command 생성
Direct3D11 Deferred Context
A. 영향을받지않습니다. 그리고Deferred Context에뷰포트를설정하는명령
을기록하지않았다면Deferred Context는기본설정으로생성되기때문에
정상적으로 렌더링이이뤄지지않습니다.
그럼다음과같은경우는어떨까요?
Q. Deferred Context 2개D1, D2에각각명령을기록하고D1 -> D2의순서로
Immediate Context에제출하였습니다 . D1의파이프라인 상태는D2에영향을
미칠까요?
38

Rendering Command 생성
Direct3D11 Deferred Context
A. 영향을미치지않습니다. D2에기록된명령들은D2의상태에만영향을받습
니다.
최종적으로 정리해보면 Deferred Context에명령을기록할때는Viewport,
Scissor rectangle, Render Target View 와같은상태를매번설정해줘야
합니다. 다음과같이일부설정을누락하면…
39

Rendering Command 생성
Direct3D11 Deferred Context
좌측그림과같이비정상적인 화면을얻게됩니다.
40

Rendering Command 생성
Code Reading
이제관련코드를보도록하겠습니다 . 그리기명령을병렬로제출하는
ParallelCommitDrawSnapshot() 함수입니다 .
41

Rendering Command 생성
Code Reading
42

Rendering Command 생성
Code Reading
실제로명령을기록하는CommitDrawSnapshotTask는이렇습니다 .
43

Rendering Command 생성
Code Reading
싱글스레드로제출한경우와2개의스레드를통해서명령을제출하는데 걸린
시간의비교표입니다 .
44
스레드1개 스레드2개
16ms 7ms
13ms 7ms
14ms 5ms
12ms 6ms
11ms 4ms
7ms 5ms
5ms 4ms
6ms 6ms
9ms 6ms
6ms 5ms
11ms 5ms
7ms 5ms
5ms 6ms
평균: 9.38461ms 평균: 5.46154ms
-사양-
CPU:Intel(R) Core(TM) i5-
4200U CPU @ 1.60GHz
2.30 GHz ( 2코어4스레드)
GPU:Intel(R)HD Graphics
Family
RAM : 8 GB
Visual Studio 2017 64 bit빌드

Dynamic Instancing
인스턴싱(Instancing)은동일한물체여러개를하나의드로우콜로한번에그리
는방식을말합니다. 다음장면의구들도한번의드로우콜로그렸습니다 .
45

Dynamic Instancing
GPU가일을하기위해서는CPU로부터의명령이필요한데명령을
제출하는데 비교적많은시간이걸립니다.
46
Graphics API
호출
API의동작에
맞는그리기
명령생성
GPU에제출
GPU에제출
가능한지판단
명령버퍼에
임시보관
바로제출가능
제출불가

Dynamic Instancing
여러물체를그리는상황을간단하게표현해보면 다음과같이오버헤드
가매드로우콜마다발생합니다 .
인스턴싱은 동일한물체들을한번에그려드로우콜마다발생하는오버
헤드를줄이게됩니다.
47
overhead
dpcall

Dynamic Instancing
다이나믹인스턴싱은 장면에추가된물체를자동으로분류해서동일한
물체가여러개있는경우자동으로인스턴싱을 통해물체를그리는방
식으로‘시작하며…’ 챕터에서소개한‘Refactoring the Mesh Drawing
Pipeline for Unreal Engine 4.22’ 동영상에서 소개된용어입니다 .
Auto Instancing이라고도하는것같습니다.
이제부터는 인스턴싱을 자동으로지원하는환경을위해서어떤작업이
필요했는지 살펴보겠습니다 .
48

Dynamic Instancing
동일한물체의정의
우선어디까지를동일한물체로취급할것인지에대한정의가필요합니
다. 아래스크린샷처럼같은모양의구들도서로다른위치에그려야
하기때문에한번에그려지는물체에도서로다른부분이존재합니다 .
49

Dynamic Instancing
동일한물체의정의
물체에따라서다를수있는부분은대표적으로 물체의위치가있을수
있고본애니메이션이 필요한메시라면본의행렬값등이있겠습니다 .
이와같이동일한물체간에어떤값이서로다를수있는지는경우에
따라다르게규정할수있습니다.
현재프로그램에는 고정된모양의스태틱메시만존재하는데 위치,크기,
회전변환을제외하고모든값이( 재질, 메시모양등)같아야동일한
물체로취급하고있습니다.
50

Dynamic Instancing
동일한물체의정의
물체간서로다른정보는인스턴싱중에참조할수있도록미리그래픽
메모리에전송해야합니다.
이정보는렌더링스레드에서 Scene에물체를추가할때나관련데이터
변경시그래픽메모리로업로드하고 셰이더코드에서는 인풋어셈블러
를통해인스턴스데이터로전달된인덱스값을통해서접근하도록 합니
다.
51

Dynamic Instancing
동일한물체의정의
52

Dynamic Instancing
DrawSnapshot
동일한물체의기준을정했다면이제물체를분류하기위한모든정보를
모아야합니다.
DrawSnapshot은분류를위한클래스로어떤물체를그릴때의파이프
라인상태에대한스냅샷입니다 .
53
메모리리소스(버퍼, 텍스쳐등…)
Input
Assembler
Vertex
Shader
Hull
Shader
Domain
Shader
Geometry
Shader
Pixel
Shader
Tessellation
Output
Merger
Rasterizer
그리기에필요한파이프라인 상태

Dynamic Instancing
DrawSnapshot
코드에서는 다음과같습니다.
DrawSnapshot만있으면이를통해서언제든그리기명령으로변환할
수있습니다.
54

Dynamic Instancing
DrawSnapshot
CommitDrawSnapshot 함수가DrawSnapshot을그리기명령으로
변환합니다 .
55

Dynamic Instancing
DrawSnapshot
이제동일물체끼리분류하는작업만이남았습니다 . 이것은
DrawSnapshot을정렬하는것으로간단하게해결할수있습니다.
다만DrawSnapshot은모든정보를담고있기때문에클래스의크기가
매우큽니다. 64bit에서는기본크기만520Byte에달합니다. 그리고
셰이더에설정될모든리소스의참조는셰이더에따라가변적일수있기
때문에더늘어날수있습니다.
따라서DrawSnapshot자체를정렬중에비교하는것은좋지않습니다.
56

Dynamic Instancing
DrawSnapshot
이를해결하기위해서DrawSnapshot에아이디를부여하였습니다 .
아이디는다음과같은해시자료구조를 통해서부여합니다 .
57

Dynamic Instancing
DrawSnapshot
실제DrawSnapshot 정렬코드는 다음과같습니다.
DrawSnapshot을정렬하고나면이제동일한종류끼리병합합니다 .
이는비교후인스턴스개수를늘리기만하면됩니다.
58

Dynamic Instancing
DrawSnapshot
인스턴싱으로 그릴때는해당인스턴스개수만큼배열을이동하면서
그리기명령으로변환하면됩니다.
59

더자세한코드를보고싶으시다면 ...
•https://github.com/xtozero/SSR/tree/multi-thread
60

참고자료
•UE4 4.24
61