<Unreal Insights>
**Chat GPT에게 물어본 내용도 있어 틀린 정보일 수 있음
*Stat Unit
-Frame: total time to finish each frame
-Game : C++ or Blueprint game operations
-Draw : CPU render time
-GPU : GPU render time
-숫자가 가장 큰 것이 bottleneck일 것이다
-Frame != Game + Draw + GPU time
*CPU
: 게임 로직, 애니메이션, 물리 계산 등 게임의 핵심 로직을 처리함. 여러 스레드를 통해 작업을 병렬로 처리함! CPU가 하는 병렬 처리를 “멀티 스레딩”이라고 함. 하나의 프로세스(CPU)는 멀티 코어를 가지고 있으며, 각 코어는 독립적으로 스레드를 실행한다. 여러 스레드는 동시에 실행되어 병렬 처리가 이루어진다.
파이프라인은 CPU > GPU 순으로 이루어진다. CPU에서는 Game Threads > Rendering Threads > RHI Threads 순으로 작업이 이루어진다. 순서가 있고 상호작용을 하지만 모두 독립적으로 수행됨!!!! 그래서 최종 ms != Game Thread 시간 + Render Thread 시간 + RHI 시간 + GPU 시간임!!! 각 스레드는 일을 병렬로 수행함. Worker Threads는 딱히 순서가 정해진 건 아니고 Game Threads와 Rendering Threads의 일을 분담한다.
1. Game Threads
2. Render Threads
3. RHI Thread
4. Worker Threads (Foreground Workers, Background Workers, Task Graph System, Async Loading Threads, Audio Threads, Network Threads..)
*Game Thread
: 메인 스레드임. 프레임 단위로, 동기적으로 게임의 주요 로직을 처리함. 매 프레임마다 업데이트 되며 렌더링되는 게임의 주요 루프를 관리함. 게임 로직 처리란 캐릭터 이동, 충돌, 게임 규칙 적용, 애니메이션 업데이트 등의 작업을 말함.
플레이어 입력 처리를 해서 게임 상태를 업데이트렌더링에 필요한 데이터를 준비하고 이를 Render Thread에 보냄. 매 프레임마다 렌더링 명령을 생성해서 이를 렌더 스레드에 보내는 것임.
*Rendering Thread
: Game Thread에서 받은 명령을 처리함. 받은 명령을 가지고 객체의 위치, 조명, 셰이더 등의 설정을 포함하여 얘가 찐 렌더링 명령을 생성함.
렌더링 작업을 준비함. 이 과정에서 복잡한 그래픽 연산과 최적화 작업이 수행됨.
이렇게 생성된 렌더링 명령을 큐에 저장하고, 이를 RHI Thread로 전달함.
*RHI Thread
: Render Hardware Interface Thread. Render Thread에서 받은 렌더링 명령을 처리하고 이를 GPU에 보내는 역할을 함. Game Thread와 Rendering Thread로부터 전달받은 명령을 큐에 쌓고 이를 GPU가 이해할 수 있는 저수준 명령으로 변환하는 방식임. 이 변환이 DirectX, Vulkan, OpenGL등의 그래픽 API 호출 형태로 변환하는 것! 걍 CPU가 한 명령을 GPU한테 전달해주는 애임
또, 렌더링 작업을 게임로직, 애니메이션 처리와 병렬로 처리할 수 있게 함. CPU와 GPU 사이의 작업을 분산시켜준다는 뜻임.
GPU 자원 관리도 함. 텍스처, 버퍼, 셰이더 등의 gpu 자원을 생성, 파괴, 업데이트를 해줌.
*Worker Thread
: 병렬 처리함. 최적화를 위해 주로 게임 로직과 관련 없는 작업을 병렬로 처리하는 것임.
비동기 작업을 함. 게임 주요 루프와 독립적으로 실행되는 연산을 처리함. 예를 들어 AI 계산, 경로 찾기, 물리 연산 등이 있음.
그러니깐 병렬 처리가 가능한 작업을 비동기로 수행하여 성능 최적화를 함!! 얘는 특정한 순서에 의해 실행되는 건 아니고 게임에 요구에 따라 할당되어 작업을 처리함. Game Thread나 Render Thread의 일을 분담하여 병렬로 비동기 작업을 함. GPU 작업과는 연관없음
- Foreground Workers
: 주로 게임의 주요 작업을 하는 스레드임.
실시간으로 처리해야하는 작업(렌더링, 물리 연산, 애니메이션 업데이트, AI 계산 등)을 처리
게임 플레이 중에 직접적인 영향을 미치는 작업을 담당함
- Background Workers
: 우선 순위가 낮은 작업을 처리하는 스레드임.
실시간 반응성이 덜 중요한 작업(데이터 로딩, 파일 처리, 네트워크 통신 등)을 처리함
백그라운드에서 실행되며 게임 플레이에 영향을 주지 않음
- Async Loading Thread
: 리소스를 비동기적으로 로딩하기 위해서 사용된다. 게임 플레이 중 리소스를 로딩하여 메인 스레드가 중단되지 않게 하는 것임.
*GPU
: 그래픽 렌더링 작업을 처리함. 얘도 수천 개의 코어를 이용해 병렬로 처리함. 렌더링, 셰이더 처리, 텍스처 맵핑, 메시 처리 등을 함
Draw Calls 수, 셰이더 복잡도, LOD, 텍스처 크기, 불필요한 오브젝트 컬링 모두 GPU 최적화 관련된 것들임. 컬링의 경우 cpu와 gpu 모두에게 도움이 됌
*메모리
: 메모리 사용량은 CPU와 GPU의 작업 처리애 영향을 미칠 수 있지만 별도로 고려한다고 함.
- RAM (CPU 메모리)
: CPU가 하는 일인 게임 로직, 물리 연산, 애니메이션 계산, AI 처리하는 데이터를 저장하고 읽어들임.
- VRAM (GPU 메모리)
: 모델, 텍스처, 버텍스 데이터 등을 저장하고 GPU가 이걸 데이터를 이용해서 렌더링함
*PSO (Pipeline State Object)
: 기하 도형을 그리기 위해 GPU로 전송할 경우 입력 데이터가 해석 및 렌더링되는 방식을 결정하는 다양한 하드웨어 설정이 있다. 종합적으로 이러한 설정을 PSO라고 한다. Rendering Pipeline의 상태를 제어하는 대부분의 객체를 지정하는데 사용된다.
GPU는 그래픽스 파이프라인을 거치면서 다양한 상태(state)와 설정(configuration)을 사용한다. 여기서 PSO는 그래픽스 파이프라인에서 사용되는 다양한 상태와 설정을 하나의 객체로 묶어서 관리한다. 이는 GPU가 그래픽스 파이프라인의 특정 설정을 바꿀 때, 한 번에 쉽게 변경하기 쉽게 하려고 그런거다. 예를 들어, 렌더할 때 사용하는 설정이나 파라미터는 엄청 많다. 뭐 셰이더 프로그램(VS, PS 등), 블렌드 상태(Add, Multiply 같은거), 레스터라이즈 상태(폴리곤이 어떻게 화면에 그려질 지, Depth Test나 culling 등), depth/stencil 상태(깊이 값이나 스텐실 테스트가 어떻게 처리될 지), 입력 레이아웃(vertex 데이터가 어떻게 구성되어 있는지 정의) 등이 있다.
PSO는 위와 같은 설정들을 하나로 묶어, GPU가 그래픽스 파이프라인에서 일관된 상태로 렌더링될 수 있게 함. GPU에서 특정 상태를 변경하려면 PSO를 설정(바인딩)하고, 그것을 기반으로 렌더링함.
예를 들어 스태틱메시는 블렌드 상태를 Add로 하고, 컬링은 backface culling을 하고, UI의 경우에는 블렌드 상태를 Multiply로 하고 Depth Test는 하지 않고 UV의 좌표는 이렇고 뭐 이런 렌더할 때 필요한 설정이나 파라미터를 다 정해서 만들어진 파라미터 세트(모음집?)을 PSO라고 한다. 스태틱메시 PSO, UI의 PSO 뭐 이런 식으로 그래픽스 파이프라인에서 렌더링을 위해 필요한 모든 설정과 파라미터를 하나로 묶어 관리하는 객체다. 머티리얼 파라미터 콜렉션이랑 느낌 비슷한 듯. PSO의 설정과 파라미터는 개발자가 결정한다
PSOPrecache는 미리 PSO를 준비해두는 작업이다. GPU에서 새로운 PSO가 처음 사용될 때, 그것을 컴파일하고 준비하는데 시간이 걸리면서 렌더링 성능이 순간적으로 떨어질 수 있음. 이거 방지하기 위해 PSOPrecache를 통해 PSO을 미리 로드해두는 것
*ParallelFor Task
: 언리얼엔진이 작업을 병렬로 처리하기 위해 사용되는 함수다. 이 함수는 주로 멀티스레딩을 사용하여 여러 작업을 병렬로 수행한다. ParallelFor는 주어진 작업을 여러 개의 작은 작업으로 나누고, 이 작은 작업을 여러 스레드에서 동시에 실행한다. 대규모 작업을 병렬화해서 작업 속도를 크게 향상시킬 수 있다.
*SyncPoint_Wait
: 언리얼 엔진에서 스레드 간 동기화 지점을 나타낸다. 여러 스레드가 병렬 작업을 수행한 후 다시 합쳐지는 지점, 즉 병렬로 나누어진 작업들이 다시 하나로 합쳐지기 전에 대기해야할 때 발생한다. 병렬 작업이 완료되기 전에 주 스레드나 다른 스레드가 대기하는 지점을 동기화 지점이라고 한다. CPU의 스레드 간 뿐만 아니라 GPU가 포함된 작업이라면 CPU와 GPU 간의 동기화일 수도 있다. CPU가 특정 작업을 완료하기 전에 GPU가 해당 작업을 완료할 때까지 기다리는 동작임
이 시간을 줄이려면 멀리스레드 작업을 최적화하던가, GPU 작업을 최적화하던가(DrawCall 줄이기, 셰이더 복잡도 낮추기, 자주 Flush되는지 확인하고 플러시 간격 최적회하기), 명령 버퍼 및 작업 스케줄링 관리하기
*Flush
: 일반적으로 GPU 작업을 강제적으로 완료시키고 그 결과를 기다리는 동작을 의미함. CPU가 GPU의 작업이 완료될 때까지 기다리는 것. 한 시스템에서 두 개의 처리장치가 병렬로 실행되다보니 여러 가지 동기화 문제가 발생함. 동기화는 한 처리장치가 작업을 마칠 때까지 다른 처리 장치는 놀고 있어야 하므로 성능에 좋지 않은 것임. 동기화는 최소화해야 함. 어쨋든 동기화 문제의 해결 방법 중 하나는 GPU가 명령 대기열의 명령들 중 특정 지점까지의 모든 명령을 다 처리할 때까지 CPU를 기다리게 하는 것임. 대기열의 모든 명령을 처리하는 것을 Flush(방출)이라고 함
Flush는 데이터를 강제로 비우는, 또는 처리하는 과정을 말함. Buffer 버퍼란 데이터를 일시적으로 저장하는 메모리 영역을 말함. 이 버퍼에 데이터가 채워지면, 특정 시점이나 조건에서 버퍼에 있는 데이터를 실제로 처리하거나 다른 위치로 전송해야 함. Flush란 이 버퍼에 쌓인 데이터를 한 번에 비우거나 더 이상 기다리지 않고 즉시 처리하는 과정을 말함. 또, CPU 캐시에서 데이터를 메인 메모리로 옮길 때도 flush라고 함. 캐시에 쌓인 데이터를 강제로 메인 메모리로 내보내는 것을 말함.
그래픽스 파이프라인에서 Flush는 보통 GPU 명령 버퍼와 관련이 있다. GPU는 CPU에게서 받은 명령을 Command Buffer에 저장함. 이 버퍼는 명령을 모아두는 일종의 대기열임. Flush는 이 명령 버퍼에 쌓인 명령들을 한꺼번에 GPU가 실행하도록 강제로 보내는 작업을 말한다. 보통 GPU가 명령 버퍼가 가득 차거나, 특정 명령(화면 업데이트하는 명령)을 실행해야 할 때 Flush가 발생한다.
*WaitForTasks
: 언리얼 엔진이 멀티스레드로 처리하는 작업들이 완료될 때까지 대기하는 시간을 말함.
*WaitFrame
: 프레임 동기화를 나타낸다. 예를 들어, 프레임이 시작될 때까지 또는 GPU 작업이 끝나기를 기다리는 시점이다. GPU 작업이 지연되면 CPU가 이를 기다려야 하며, 그 결과 프레임 드랍이 발생할 수 있다.
*WaitUntilTasksComplete
: 특정 작업(Task)이 모두 완료될 때까지 기다리는 시간을 의미한다. 이 역시 멀티스레딩 작업의 동기화 지점에서 발생할 수 있으며, 대기 시간이 길어지면 프레임 드랍 발생
*Count, Inclusive Time, Exclusive Time
-Count : 해당 함수 또는 이벤트가 호출된 총 횟수
-Inclusive Time : 해당 함수 또는 이벤트가 실행되는데 소요된 총 시간. 이 시간에는 해당 이벤트 또는 함수 내부에서 호출된 다른 모든 함수의 실행 시간이 포함된다. 그 안에서 호출된 모든 서브 작업의 시간이 포함된 것
-Exclusive Time : 해당 이벤트 또는 함수 자체만 실행된 시간을 의미한다. 다른 하위 작업이나 함수 호출 시간은 포함되지 않고, 오직 해당 이벤트 또는 함수 자체의 실행 시간만을 의미함
*Instanced Static Mesh(ISM)과 Hierarchical Instanced Static Mesh(HISM)
: 둘 다 BP에서의 컴포넌트이다. ISM과 HISM을 쓰려면 BP를 만들어서 컴포넌트로 추가하면 된다. ISM은 같은 스태틱 메시를 다수의 인스턴스로 렌더해준다. 똑같은 스태틱 메시를 각각 여러 개 쌓지 않고 하나로 묶을 때 쓴다. 같은 스태틱 메시를 몇 천개 복사하던지 아주 적은 양의 drawcall로 취급한다. 퍼포먼스가 좋아지지만 단점이 있다. ISM을 아주 넓은 맵에 서로 멀리 흩뿌려놓는다면 단점이 될 것이다. 왜냐면 ISM은 개별 인스턴스 별로 LOD가 세팅되는 것이 아니라 흩뿌려진 ISM들이 전부 동일한 LOD를 가지게 된다. 이런 경우에는 HISM을 이용하면 된다. 각 인스턴스 별로 서로 다른 LOD를 가질 수 있다. 그치만 5.4부터는 ISM도 개별적으로 LOD를 세팅할 수 있게 되었다. 액터들을 Level Instance로 만들면 자동으로 액터가 HISM로 그룹화된다고 한다. 그러나 왜 HISM을 항상 이용하진 않냐면 이걸 쓰면 어느 LOD를 써야할 지 계산해야되기 때문에 extra load가 발생된다. HISM을 작은 맵에서 쓰게 될 경우 오히려 ISM보다 load를 더 발생기키거나 퍼포먼스가 낮아질 수 있다. 집을 만드는 것 처럼 작은 구역의 경우 ISM, 도시처럼 넓은 맵에 흩뿌려놓을거면 HISM을 추천한다. 둘은 사실 같은 기능인데 개별 LOD 지원의 차이일 뿐이다.
ISM이라고 똑같은 메시만 조합할 경우에만 이용하는 것은 아니다. BP에서 ISM 컴포넌트 추가 후 벽돌 넣어주고, 또 다른 ISM 컴포넌트 추가 후 문 에셋 넣어주고 이런 식으로 만들면 된다. 그러나 ISM 컴포넌트 추가 후 디테일 창에서 Static Mesh를 지정해도 메시가 보이지 않는다. 왜냐면 아직 인스턴스를 추가하지 않아서 그런다. 디테일 창 아래쪽에 Instances에 +버튼을 눌러 추가해주고, 각 메시들의 transform을 조절해주면 된다.
이미 만들어놓은 세트를 ISM으로 바꾸려면 에디터에서 세트로 만들 모든 메시 선택 후 좌측 상단의 액터 > 액터 병합 > 배치(Batch) 눌러주면 된다.