언리얼 렌더링 시스템 분석(01) - 개요 및 기본
🌐 원문 링크: 剖析虚幻渲染体系(01)- 综述和基础 (cnblogs.com/timlly)
📅 원문 발행일: 2020-10-26 | ✍️ 저자: Timlly (00 / )💡 시리즈 분류:
Unreal Engine Rendering| 국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료
언리얼 엔진(UE)은 그래픽 렌더링과 개발 키트를 통합한 상용 엔진입니다. 수십 년간의 개발과 축적 끝에 Baiqing 경쟁에서 두각을 나타내며 실시간 렌더링 분야를 선도하는 글로벌 범용 상용 엔진이 되었습니다. 게임, 디자인, 시뮬레이션, 영화 및 TV, 교육, 의학 및 기타 산업에서 널리 사용됩니다. 게임 회사 Epic Games에서 왔으며 원래 Tim Sweeney가 이끌었습니다. 이 버전은 20년 넘게 사용되었으며 1990년대 중반 이후 여러 차례 주요 버전의 반복을 거쳤습니다.
1.1.1 언리얼 엔진 1(1995)
팀 스위니(Tim Sweeney)는 1995년 연구 개발을 주도했고, 1998년에는 3인칭 슈팅 게임인 최초의 게임인 언리얼(Unreal)을 개발해 언리얼 엔진의 일반 상용 엔진의 문을 열었습니다.
1세대 엔진으로서 다음과 같은 특징을 가지고 있습니다.
컬러 조명.
제한된 텍스처 필터링(제한된 형태의 텍스처 필터링)
충돌 감지.
장면 편집자.
소프트 렌더러(CPU가 그리기 지침을 실행한 다음 하드웨어 가속 Glide API로 이동)

언리얼 엔진 1세대 에디터의 인터페이스입니다.
1.1.2 언리얼 엔진 2(1998)
Unreal 2세대는 여전히 Tim Sweeney가 주도하고 있었습니다. 연구 개발은 1998년에 시작되었습니다. 두 번째 버전의 개발은 2002년에 완료되었으며, 해당 멀티플레이어 슈팅 게임인 America's Army와 같은 여러 게임이 개발되었습니다.
1세대와 비교하여 2세대 Unreal의 특징은 주로 다음과 같습니다.
더욱 완전한 도구 체인.
시네마틱 편집 툴체인.
입자 시스템.
DCC 골격 애니메이션 내보내기와 같은 플러그인을 지원합니다.
C++ 기반 wxWidgets 도구 상자.
Karma 물리 엔진을 기반으로 한 물리적 시뮬레이션: 봉제 인형 충돌, 강체 충돌 등

Unreal Engine 2 편집기 인터페이스.

Unreal Engine 2를 기반으로 개발된 게임 Killing Floor의 화면입니다.
1.1.3 언리얼 엔진 3(2004)
언리얼 3는 1년 반의 비공개 개발 끝에 2004년 출시됐다. 이 버전은 또한 다음을 포함하여 업계에 많은 새로운 기능을 제공합니다.
객체지향 디자인.
데이터 기반 스크립트.
물리적 시스템.
사운드 시스템.
새로운 동적 WYSIWYG 툴체인.
프로그래밍 가능한 렌더링 파이프라인.
픽셀별 빛과 그림자 계산.
감마 보정된 HDR 렌더러.
파괴 가능한 환경.
동적 소프트웨어 시뮬레이션.
그룹 역할 시뮬레이션.
실시간 GI 솔루션.

Unreal Engine 3 편집기 인터페이스.
Unreal 3에는 Gear of War, RobotBlitz, Infinity Blade, Dungeon Defenders, Batman: Arkham City, Aliens: Colonial Marines 등을 포함한 많은 게임 걸작이 있습니다.

배트맨: 아캄시티 게임 화면 개요.
1.1.4 언리얼 엔진 4(2008)
언리얼 엔진 4는 이르면 2008년에 출시됐으니 벌써 12년이 지났습니다. 20개 이상의 버전을 반복한 후 다음을 포함하되 이에 국한되지 않는 수많은 놀라운 기능이 도입되었습니다.
PBR 렌더링 파이프라인 및 지원 도구 체인.
DXR, RTX 기반의 실시간 레이 트레이싱.
청사진 시스템.
시각자료 편집자.
디퍼드 렌더링 파이프라인.
모바일 플랫폼을 위한 경량 렌더링 파이프라인.
VR 렌더링 파이프라인.
나이아가라와 같은 GPU 파티클.
더욱 사실적인 물리 시뮬레이션(파괴, 충돌, 소프트웨어 등).
더욱 완벽한 게임, 영화, TV 제작 도구 체인입니다.
더 많은 주류 플랫폼을 지원합니다.
-......

UE4 에디터 개요.

UE4.22 실시간 라이트 트레이싱 화면 개요.
UE4의 개발과 Epic Games의 기업 전략 변화로 인해 2015년 마침내 업계에 충격을 주는 결정이 내려졌습니다. 모든 사용자에게 무료이며 소스 코드가 공개되었습니다. 그때부터 누구나, 어느 기관에서나 UE의 소스 코드를 연구할 수 있으며, 이것이 이 기사 시리즈가 탄생한 이유입니다.
UE4를 기반으로 개발된 게임 명작도 점점 더 많아지고 있습니다. 대표작으로는 Gears of War 4, Deadline by Daylight, PlayerUnknown's Battlegrounds, Peace Elite, Sword Art Online, Minecraft Dungeon, Final Fantasy VII Remake, Bloodthirsty Code 등이 있습니다.

Final Fantasy 7 Remake는 사실적이고 화려하며 역동적인 그래픽을 제공합니다.
게임 산업 외에도 영화 및 TV, 시뮬레이션, 디자인, 라디오 및 TV, 과학 시각화 및 기타 산업에서도 UE를 시각적 제작 도구로 점진적으로 도입하고 해당 도구 체인을 점차 개선했습니다.

Unreal Engine 4로 렌더링된 영화 및 TV 수준의 가상 캐릭터.
1.1.5 언리얼 엔진 5(2021)
2020년 5월, Unreal은 Unreal 5의 렌더링 기능을 보여주는 비디오 "Lumen in the Land of Nanite"를 공식 출시했습니다. 이 비디오는 가상 마이크로 다각형 기하학과 실시간 전역 조명 Lumen 기술을 기반으로 하는 Nanite를 시연하여 영화와 TV 수준의 시청각 경험을 실시간 게임에 구현했습니다. 이건 게임이 아니고 영화임이 분명해요! 당시 이 영상을 보고 많은 독자들이 매료됐을 거라 생각하고, 모두를 정말 놀라게 했습니다. 이 영상을 처음 봤을 때 너무 신기해서 여러 번 봤어요.

'나나이트의 나라 루멘' 데모 영상의 한 프레임입니다.
공식 소개에 따르면 Nanite는 동일한 화면에서 수천억 개의 폴리곤을 지원합니다. 이는 모델 토폴로지 및 노멀 매핑과 같은 전통적인 예술 제작 프로세스가 더 이상 필요하지 않으며 하이 폴리 렌더링이 직접 채택된다는 것을 의미합니다. Lumen은 전문적인 레이 트레이싱 하드웨어 없이도 장면과 조명 변화에 실시간으로 반응할 수 있는 완전 동적 전역 조명 솔루션입니다. 시스템은 크고 세밀한 장면에서 무한히 바운스될 수 있는 간접 스페큘러 반사 및 디퓨즈를 렌더링할 수 있습니다.
또한 이 비디오에서는 카오스 물리 및 파괴 시스템, 나이아가라 VFX, 컨볼루션 리버브, 주변 스테레오 렌더링과 같은 새로운 기능도 선보입니다.
UE5의 출시 시간에 관해서는 공식 성명을 직접 인용하는 것이 간단합니다:
Unreal Engine 4 및 5 출시 일정
언리얼 엔진 4.25는 이미 Sony와 Microsoft의 차세대 콘솔 플랫폼을 지원합니다. Epic은 현재 Unreal Engine 4를 사용하여 차세대 게임을 개발하기 위해 콘솔 제조업체, 여러 게임 개발자 및 퍼블리셔와 긴밀히 협력하고 있습니다.
**Unreal Engine 5는 2021년 초에 프리뷰로 출시되고 2021년 말에 정식 버전으로 출시될 예정입니다.**차세대 콘솔, 현세대 콘솔, PC, Mac, iOS 및 Android 플랫폼을 지원합니다.
우리는 먼저 UE4에서 차세대 개발을 시작하고, 적절한 시기에 프로젝트를 UE5로 마이그레이션할 수 있도록 향후 호환 기능을 설계하고 있습니다.
UE4를 사용해 개발한 '포트나이트'를 차세대 콘솔 출시 후 차세대 콘솔에서 사용할 수 있도록 할 예정입니다. 자체 개발을 통해 업계 최고의 기술을 입증하기 위해 2021년 중반에 게임을 UE5로 마이그레이션할 예정입니다.
즉, UE 관계자들이 손을 놓지 않는다면 2021년에 UE5 풀버전을 출시할 것이라는 얘기다. 기다려보자.
1.2 렌더링 개요
1.2.1 언리얼 렌더링의 진화
UE의 개발 역사를 통틀어 UE는 실제로 하드웨어 기술과 소프트웨어 기술의 개발 추세를 따르고 소프트웨어와 하드웨어의 새로운 기능과 소프트웨어 엔지니어링, 운영 체제 및 기타 기술을 결합하는 데 능숙한 결과입니다. 예를 들어, 1990년대 중반 하드웨어가 개발되면서 16비트 트루 컬러 렌더링 파이프라인이 추가되었고, 1998년에는 32비트 RGBA로 늘어났습니다. 금세기 초, 하드웨어 기반 프로그래밍 가능 렌더링 API가 등장한 후 UE는 고정 파이프라인(현재 폐기됨)과 프로그래밍 가능 렌더링 파이프라인을 모두 지원했습니다. 그 후 몇 년 동안 HDR이 등장했고, 디퍼드 렌더링 파이프라인, 표면 세분화, 컴퓨팅 셰이더 등이 모두 이 패턴을 따랐습니다.

사진은 DirectX 11에 추가된 표면 테셀레이션, 계산 셰이더 등의 새로운 기능을 보여줍니다.
몇 년 전까지만 해도 CPU에 대한 무어의 법칙은 한계에 도달했고, CPU 제조업체는 전략을 바꾸고 멀티 코어를 적극적으로 개발할 수밖에 없었습니다. 그 결과, CPU 멀티코어 및 GPU 데이터 기반 병렬화가 강력하게 개발되었으며, Vulkan, DirectX12, Metal과 같은 경량의 멀티스레드 친화적인 그래픽 API가 등장했습니다. UE는 최신 CPU 멀티 코어 및 GPU 대규모 컴퓨팅 장치의 성능 이점을 최대한 활용하기 위해 복잡한 멀티 스레드 렌더링을 추가했습니다.

CPU의 코어 주파수 증가는 1970년부터 2011년까지 무어의 법칙을 유지해 왔지만, 칩 기술 개발이 정체되면서 그 이후로는 분명히 무어의 법칙 곡선을 따라잡지 못했습니다. (사진 오른쪽이 인텔 창업자 무어 본인입니다.)

2006년경 CPU 성능은 무어의 법칙 곡선에 비해 분명히 뒤처졌지만 동시에 CPU 코어 수도 증가했습니다.
UE는 많은 주류 운영 체제를 지원하고 많은 그래픽 API와 해당 셰이더를 캡슐화해야 하기 때문에 UE의 렌더링 시스템은 원래의 API를 계층별로 캡슐화하여 원래의 간단한 API를 오늘날의 복잡한 UE 시스템으로 발전시켜야 합니다. 예를 들어, 여러 그래픽 API를 교차하기 위해 사용자 수준에서 그래픽 API를 직접 호출하는 문제를 해결하기 위해 RHI 시스템이 추가되었습니다. 사용자가 머티리얼 효과를 쉽게 사용하고 편집할 수 있도록 머티리얼 템플릿과 머티리얼 편집기가 도입되었으며, 일련의 중간 레이어를 사용하여 해당 하드웨어 플랫폼에 셰이더를 컴파일했습니다. 멀티코어의 장점을 최대한 활용하기 위해 게임 스레드, 렌더링 스레드, RHI 스레드가 도입되었습니다. 스레드 액세스 충돌과 경쟁을 해결하기 위해 U로 시작하는 게임 스레드 표현이 도입되었으며 F로 시작하는 해당 표현도 있었습니다. 렌더링 스레드는 다음을 나타냅니다. 일괄 처리를 모델링하고 DrawCall 및 기타 렌더링 최적화를 줄이기 위해 동적 및 정적 렌더링 경로가 추가되고 FMeshPassProcessor, FMeshBatch, FMeshDrawCommand와 같은 개념이 추가됩니다. Vulkan 및 DirectX12와 같은 새로운 경량 최신 그래픽 API에 적응하고 이를 최대한 활용하기 위해 UE는 4.22에서 RDG(Rendering 종속성 그래프)도 도입했습니다. 등등 셀 수 없이 많습니다.

프레임 그래프(또는 RDG)는 엔진 기능 모듈과 GPU 리소스를 분리하여 구조를 보다 명확하게 만들고 메모리, 비디오 메모리, 패스 등의 목표 스케줄링 최적화를 가능하게 합니다.
지난 몇 년 동안 AI 기술은 이러한 추세와 함께 성장했으며 업계 R&D 인력에 의해 그래픽 분야에 도입되어 실시간 소음 감소 분야에서 이를 최대한 활용하고 있습니다. NVIDIA의 Turing 하드웨어 아키텍처의 Tensor Core와 Raytrace Core 통합, Microsoft의 Ray Tracing용 DirectX Raytracing 표준 API 통합과 결합하여 실시간 분야의 Ray Tracing이 마침내 봄을 맞이하고 번성했습니다. 실시간 라이트 트레이싱을 통합한 최초의 범용 상용 엔진은 UE 4.22이며, 해당 데모 비디오 "Troll"이 공개되었습니다.

에픽게임즈는 UE 4.22를 출시하면서 실시간 레이 트레이싱 지원을 발표했으며, Goodbye Kansas Studios와 공동으로 데모 비디오 "Troll"을 공개했습니다.
종합하면, UE 렌더링 시스템이 오늘날의 복잡한 상황을 제시하는 주요 이유는 다음과 같습니다.
소프트웨어 및 하드웨어 기술의 발전을 준수합니다.
객체 지향 소프트웨어 엔지니어링의 아이디어와 디자인을 충족시킵니다.
재사용성, 확장성을 향상하고 결합을 줄이기 위한 모듈식 아키텍처입니다.
크로스 플랫폼, 크로스 컴파일러, 크로스 그래픽 API.
기존 기능, 코드, 인터페이스와 호환됩니다.
렌더링 효율성, 성능 비율 및 견고성을 향상시킵니다.
GamePlay 레이어 사용자(논리 프로그래머, 아티스트, TA, 기획자 등)의 학습 및 사용 비용을 줄이기 위해 API의 기본 세부 사항을 캡슐화하고 렌더링 시스템의 세부 사항과 복잡성을 추상화합니다.
엔진의 범용성을 향상시키기 위해서는 다단계, 다개념 캡슐화가 추가되어야 합니다.
전체 그래픽 렌더링 산업의 발전 과정에서 업계 R&D 인력의 목표는 동일합니다. 즉, 제한된 하드웨어 자원을 최대한 활용하여 더욱 사실적이거나 양식화된 이미지를 더 빠르고 더 좋게 렌더링하는 것입니다. **
1.2.2 콘텐츠 범위
많은 사람들이 언리얼 렌더링을 분석하는 책(예: "코끼리는 보이지 않습니다: 언리얼 엔진 프로그래밍에 대한 간략한 분석")이나 기술 기사(예: Unreal Engine 4 렌더링 시리즈, "Fang Yanliang-Unreal 4 렌더링 시스템 아키텍처 분석" 및 많은 Zhihu 기사)를 저술했지만 저자는 그들의 기사가 UE 렌더링 시스템의 일부만을 드러낼 수 있다고 믿습니다. 적어도 나는 UE 렌더링 시스템 전체를 더 완전하게 분석할 수 있는 책이나 기사 시리즈를 아직 찾지 못했습니다. 이런 점에서 저자는 이 중요한 임무를 과감하게 떠맡지만 결국 에너지도 부족하고 실력도 한계가 있다. 실수나 누락된 부분이 있으면 정정해 주시기 바랍니다.
이 기사 시리즈는 UE의 렌더링 시스템 분석에 중점을 둡니다. 보다 구체적으로 말하면 주로 다음 UE 디렉터리의 소스 코드로 제한됩니다.
엔진\소스\런타임\렌더러코어.
엔진\소스\런타임\렌더러.
엔진\소스\런타임\RHI.
일부 RHI 모듈: D3D12RHI, OpenGLDrv, VulkanRHI 등
일부 기본 모듈: Core, CoreUObject 등
물론 필요하다면 위에 나오지 않은 코드파일도 관련되겠지만, 나중에는 구체적으로 언급하지 않겠습니다.
1.3 기본 모듈
이 섹션에서는 주로 렌더링 시스템에 일반적으로 사용되는 몇 가지 기본 지식, 개념 및 시스템을 간략하게 설명하여 익숙하지 않거나 기반이 약한 독자에게 전환 및 진입점을 제공합니다. UE 베테랑이라면 이 섹션을 건너뛰셔도 됩니다.
1.3.1 C++의 새로운 기능
이 섹션에서는 UE 및 렌더링 시스템에 자주 사용되는 새로운 C++ 기능(C++11, C++14 및 이후 버전)에 대해 간략하게 설명합니다.
1.3.1.1 람다
C++의 람다는 C++11의 고유한 기능입니다. C#, Lua 등 스크립팅 언어의 클로저 및 익명 함수와 완전히 동일하지만 사용 방식이 더 복잡하고 다양하며 네이티브 언어 고유의 스타일에 더 가깝습니다. 몇 가지 문법적 형태가 있습니다:
(1) [ captures ] <tparams>(optional)(C++20) ( params ) specifiers exception attr -> ret requires(optional)(C++20) { body }
(2) [ captures ] ( params ) -> ret { body }
(3) [ captures ] ( params ) { body }
(4) [ captures ] { body }그 중 타입 (1)은 C++20에서만 지원하는 문법으로 UE에서는 당분간 사용하지 않는다. 나머지 3개는 일반적인 형태입니다. 가장 일반적으로 사용되는 방법은 FScene::AddPrimitive와 같은 렌더링 명령을 렌더링 스레드에 푸시하는 것입니다((...)는 코드의 일부가 생략됨을 의미하며 아래와 같습니다):
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp
void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
(......)
FScene* Scene = this;
TOptional<FTransform> PreviousTransform = FMotionVectorSimulation::Get().GetPreviousTransform(Primitive);
ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
[Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
{
FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
FScopeCycleCounter Context(SceneProxy->GetStatId());
SceneProxy->SetTransform(Params.RenderMatrix, Params.WorldBounds, Params.LocalBounds, Params.AttachmentRootPosition);
// Create any RenderThreadResources required.
SceneProxy->CreateRenderThreadResources();
Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
});
}클로저를 생성할 때 현재 환경(범위)에서 변수를 캡처하는 방법에는 값(변수를 캡처 목록에 직접 넣기) 또는 참조(변수 앞에 & 기호를 붙이고 캡처 목록에 넣기) 등 여러 가지 방법이 있습니다. 위의 FScene::AddPrimitive는 Lambda 변수를 사용하여 값으로 전달됩니다. Lambda에 전달된 변수의 수명주기는 코더에 의해 보장되므로 UE는 유효하지 않은 메모리에 대한 액세스를 방지하기 위해 대부분 값별 전달 방법을 사용합니다.
자세한 지침은 C++ 공식 웹사이트의 Lambda: Lambda 표현식 지침을 참조하세요.
1.3.1.2 스마트 포인터
C++11부터 표준 라이브러리는 메모리 할당 및 추적에서 코더의 부담을 줄이기 위해 고유 포인터(unique_ptr), 공유 포인터(shared_ptr) 및 **약한 포인터(weak_ptr)**와 같은 스마트 포인터 세트를 도입했습니다.
그러나 Unreal은 이 포인터 세트를 직접 사용하지 않고 자체 세트를 구현했습니다. 위에서 언급한 세 가지 외에도 UE는 nullable이 아닌 공유 포인터와 동일하게 동작하는 공유 참조를 추가할 수도 있습니다. Unreal Objects는 게임 코드에 더 적합한 별도의 메모리 추적 시스템을 사용하므로 이러한 클래스를 UObject 시스템과 동시에 사용할 수 없습니다.
비교 및 설명은 다음과 같습니다.
이름유C++설명
공유 포인터 TSharedPtr shared_ptr 공유 포인터는 자신이 참조하는 객체를 소유하고, 해당 객체가 삭제되는 것을 무기한 방지하며, 공유 포인터나 공유 참조가 참조하지 않을 때 결국 삭제를 처리합니다. 공유 포인터는 비어 있을 수 있습니다. 즉, 어떤 객체도 참조하지 않습니다. null이 아닌 공유 포인터는 참조하는 객체에 대한 공유 참조를 생성할 수 있습니다.
고유 포인터 TUniquePtr 고유_ptr 고유 포인터는 자신이 참조하는 개체만 명시적으로 소유합니다. 특정 리소스에 대한 고유 포인터는 하나만 있으므로 고유 포인터는 소유권을 이전할 수 있지만 공유할 수는 없습니다. 고유 포인터를 복사하려고 하면 컴파일 오류가 발생합니다. 고유 포인터가 범위를 벗어나면 참조하는 객체를 자동으로 삭제합니다.
약한 포인터 TWeakPtr 약한_ptr 약한 포인터 클래스는 공유 포인터와 유사하지만 참조하는 객체를 소유하지 않으므로 수명에 영향을 주지 않습니다. 이 속성은 참조 순환을 깨기 때문에 유용하지만 약한 포인터가 언제든지 경고 없이 null이 될 수 있음을 의미하기도 합니다. 따라서 약한 포인터는 자신이 참조하는 개체에 대한 공유 포인터를 생성하여 프로그래머가 해당 개체에 안전하게 임시 액세스할 수 있도록 보장합니다.
견적 공유 TSharedRef
- 공유 참조는 공유 포인터처럼 동작합니다. 즉, 참조하는 개체를 보유합니다. 이는 null 개체의 경우 다릅니다. 공유 참조는 null이 아닌 객체로 고정되어야 합니다. 공유 포인터에는 그러한 제한이 없으므로 공유 참조는 공유 포인터로 고정적으로 변환 가능하며 공유 포인터는 유효한 개체를 고정적으로 참조합니다. 참조된 개체가 null이 아닌지 확인하거나 공유 개체의 소유권을 표시하려는 경우 공유 참조를 사용합니다.
UE는 또한 C++와 유사한 도구 인터페이스를 제공하여 스마트 포인터를 더 좋고 빠르게 구축합니다.
이름유C++설명
이것으로부터 공유 포인터를 구성하십시오 TSharedFromThis 활성화_공유_from_this AsShared 또는 SharedThis 함수를 추가하여 TSharedFromThis에서 클래스를 파생시킵니다. 이 함수를 사용하여 객체의 TSharedRef를 가져옵니다.
공유 포인터 생성 MakeShared, MakeShareable make_shared 일반 C++ 포인터 내에 공유 포인터를 만듭니다. MakeShared는 단일 메모리 블록에 새 개체 인스턴스와 참조 컨트롤러를 할당하지만 개체가 공용 생성자를 제출해야 합니다. MakeShareable은 덜 효율적이지만 개체의 생성자가 비공개인 경우에도 계속 실행할 수 있습니다. 이 작업을 사용하여 생성하지 않은 개체를 소유하고 개체 삭제 시 사용자 지정 동작을 지원합니다.
정적 변환 StaticCastSharedRef, StaticCastSharedPtr
- 일반적으로 파생 유형으로 다운캐스팅하는 데 사용되는 정적 프로젝션 유틸리티 함수입니다.
고정 변환 ConstCastSharedRef, ConstCastSharedPtr
- const 스마트 참조 또는 스마트 포인터를 각각 변경 가능한 스마트 참조 또는 스마트 포인터로 변환합니다.
UE 자체 스마트 포인터 라이브러리는 메모리 관리 액세스 및 참조 카운팅 추적과 같은 기본 기능을 제공하는 것 외에도 효율성 및 메모리 사용 측면에서 스마트 포인터의 C++ 표준 버전과 비슷합니다. 또한 스레드로부터 안전한 액세스 모드가 제공됩니다.
TSharedPtr<T, ESPMode::ThreadSafe>
TSharedRef<T, ESPMode::ThreadSafe>
TWeakPtr<T, ESPMode::ThreadSafe>
TSharedFromThis<T, ESPMode::ThreadSafe>
그러나 스레드 안전 버전은 원자 참조 카운팅에 의존하기 때문에 스레드 안전이 아닌 버전보다 성능이 약간 느리지만 동작은 일반 C++ 포인터와 일치합니다.
읽기 및 복사는 스레드로부터 안전함을 보장합니다.
쓰기와 재설정은 동기화되어야 안전합니다.
이러한 스레드 안전 스마트 포인터는 일반적으로 UE 다중 스레드 렌더링 아키텍처에서 사용됩니다.
1.3.1.3 위임
Delegate는 본질적으로 함수의 유형이자 대표자로, 지정된 멤버 함수의 선언, 참조 및 실행을 용이하게 합니다. C++ 표준 라이브러리는 위임을 구현하지 않지만, 클래스 위임의 효과는 모호한 구문을 통해 얻을 수 있습니다.
Microsoft의 내장 라이브러리는 위임 기능을 구현합니다. 마찬가지로 UE에는 위임 요구 사항과 응용 프로그램이 많기 때문에 UE도 내부적으로 일련의 위임 메커니즘을 구현합니다. UE 위임에는 세 가지 유형이 있습니다.
단일 주문
멀티캐스트 위임
이벤트
- 동적 개체
UObject
-직렬화 가능
이는 매크로 세트를 통해 선언됩니다. 일반적인 선언 형식과 해당 함수 정의는 다음과 같습니다.
매크로 선언기능 정의 또는 설명
DECLARE_DELEGATE(대리인이름) 무효 함수()
DECLARE_DELEGATE_OneParam(DelegateName, Param1Type) 무효함수(Param1)
DECLARE_DELEGATE_Params(DelegateName, Param1Type, Param2Type, ...) 무효 함수(Param1, Param2, ...)
DECLARE_DELEGATE_RetVal(RetValType, DelegateName) 함수()
DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type) 함수(Param1)
DECLARE_DELEGATE_RetVal_Params(RetValType, DelegateName, Param1Type, Param2Type, ...) 기능(Param1, Param2, ...)
DECLARE_MULTICAST_DELEGATE(_XXX) 멀티캐스트 위임 유형 생성(매개변수 사용 가능)
DECLARE_DYNAMIC_MULTICAST_DELEGATE() 동적 멀티캐스트 위임 유형 생성(매개변수 사용 가능)
선언 후에는 BindXXX 및 UnBind 인터페이스를 통해 그에 따라 기존 인터페이스를 바인딩 및 바인딩 해제할 수 있습니다. 바인딩된 위임의 경우 Execute를 호출하여 실행할 수 있습니다. 사용 예:
// 선언 타입/유형
DECLARE_DELEGATE_OneParam(FOnEndCaptureDelegate, FRHICommandListImmediate*);
// 정의 객체/오브젝트
static FOnEndCaptureDelegate GEndDelegates;
//
void RegisterCallbacks(FOnBeginCaptureDelegate InBeginDelegate, FOnEndCaptureDelegate InEndDelegate)
{
GEndDelegates = InEndDelegate;
}
// 실행(Execute)(바인딩(Bind)의 )
void EndCapture(FRHICommandListImmediate* RHICommandList)
{
if (GEndDelegates.IsBound())
{
GEndDelegates.Execute(RHICommandList);
}
}
// 바인딩(Bind)
void UnregisterCallbacks()
{
GEndDelegates.Unbind();
}UE의 위임 구현 코드는 TBaseDelegate에 있습니다:
// Engine\Source\Runtime\Core\Public\Delegates\DelegateSignatureImpl.inl
template <typename WrappedRetValType, typename... ParamTypes>
class TBaseDelegate : public FDelegateBase
{
public:
/** Type definition for return value type. */
typedef typename TUnwrapType<WrappedRetValType>::Type RetValType;
typedef RetValType TFuncType(ParamTypes...);
/** Type definition for the shared interface of delegate instance types compatible with this delegate class. */
typedef IBaseDelegateInstance<TFuncType> TDelegateInstanceInterface;
(......)
}템플릿과 다중 상속을 사용하여 구현된 소스 코드가 많이 있지만 기본적으로 객체, 함수 포인터 등도 캡슐화합니다.
1.3.1.4 코딩 표준
이 섹션에서는 UE 공식 권장 또는 필수 공통 코딩 표준 및 상식을 간략하게 설명합니다.
- 명명 규칙
이름(예: 유형 또는 변수)에 있는 각 단어의 첫 글자는 대문자로 시작해야 하며 일반적으로 단어 사이에 밑줄이 없습니다. 예: lastMouseCoordinates 또는 delta_coordinates가 아닌 Health 및 UPrimitiveComponent.
- 유형 이름 앞에는 추가 대문자를 붙여 변수 이름과 구별해야 합니다. 예를 들어 FSkin은 유형 이름이고 Skin은 FSkin의 인스턴스입니다.
템플릿 클래스에는 T라는 접두사가 붙습니다.
UObject에서 상속받는 클래스에는 U라는 접두사가 붙습니다.
AActor에서 상속받는 클래스에는 A라는 접두사가 붙습니다.
SWidget에서 상속되는 클래스에는 S라는 접두사가 붙습니다.
인터페이스 클래스(Interface)의 접두사는 I입니다.
열거형에는 E라는 접두사가 붙습니다.
부울 변수에는 b 접두사가 붙어야 합니다(예: bPendingDestruction 또는 bHasFadedIn).
대부분의 다른 클래스에는 F라는 접두사가 붙는 반면, 일부 하위 시스템에는 다른 문자가 접두사로 붙습니다.
Typedef에는 해당 유형과 일치하는 문자가 앞에 붙어야 합니다. 즉, 구조 typedef의 경우 F, Uobject typedef의 경우 U 등이 있습니다.
특수 템플릿으로 인스턴스화된 Typedef는 더 이상 템플릿이 아니므로 그에 따라 접두사를 붙여야 합니다. 예를 들면 다음과 같습니다.
typedef TArray<FMytype> FArrayOfMyTypes;C#에서는 접두사가 생략됩니다.
대부분의 경우 UnrealHeaderTool에는 올바른 접두사가 필요하므로 접두사를 추가하는 것이 중요합니다.
유형과 변수의 이름은 명사로 지정됩니다.
메소드 이름은 메소드의 효과 또는 메소드의 영향을 받지 않는 반환 값을 설명하는 동사입니다.
변수, 메소드, 클래스의 이름은 명확하고 간결하며 설명적으로 지정되어야 합니다. 이름의 범위가 클수록 잘 설명하는 이름이 더 중요해집니다. 과도한 약어를 사용하지 마세요.
부울을 반환하는 모든 함수는 IsVisible() 또는 ShouldClearBuffer()와 같은 참/거짓 쿼리를 시작해야 합니다.
프로그램(반환 값이 없는 함수)은 Object 뒤에 강력한 활용 동사를 사용해야 합니다. 메소드의 객체가 메소드가 위치한 객체인 경우는 예외입니다. 이 경우 객체는 맥락에 맞게 이해되어야 합니다. "Handle" 및 "Process"로 시작하지 마세요. 그러한 동사는 모호성을 유발할 수 있습니다.
STL 허용 목록
UE는 메모리 관리 및 효율성과 같은 이유로 일부 STL 라이브러리 사용을 피하고 자체 코드를 구현하지만 C++ 표준이 더욱 강력해지고 많은 새로운 크로스 플랫폼 효과적인 모듈이 추가됨에 따라 다음 모듈이 공식적으로 화이트리스트에 등록되었습니다(사용이 허용되며 향후 변경되지 않음).
원자
유형_특성
초기화_목록
-정규식
- 한도
-cmath
- 클래스의 선언은 구현자가 아닌 사용자의 관점에서 이루어져야 하므로 일반적으로 클래스의 공용 인터페이스 및/또는 멤버 변수가 먼저 선언되고 개인 변수가 선언됩니다.
UCLASS()
class MyClass
{
public:
UFUNCTION()
void SetName(const FString& InName);
UFUNCTION()
FString GetName() const;
private:
void ProcessName_Internal(const FString& InName);
private:
UPROPERTY()
FString Name;
};- const를 사용해 보세요. 매개변수, 변수, 상수, 함수 정의, 반환 값 등을 포함합니다.
void MyFunction(const TArray<Int32>& InArray, FThing& OutResult)
{
// 수정 InArray,그러나 수정 OutResult
}
void MyClass::MyFunction() const
{
// 코드 MyClass의 ,이면 로써 선언 추가(Add)const
}
TArray<FString> StringArray;
for (const FString& :StringArray)
{
// 루프 의 수정 StringArray
}- 코드 애플리케이션에는 명확하고 정확한 주석이 있습니다. 문서 시스템에 대한 편집기 도구 설명을 자동으로 생성하기 위해 특정 주석 형식이 제공됩니다.

C++ 구성 요소가 변수에 주석을 추가한 후 해당 설명은 UE 컴파일 시스템에 의해 캡처되어 편집기 프롬프트에 적용됩니다.
- C++ 새로운 구문
nullptr은 이전 NULL을 대체합니다.
static_assert(정적 어설션)
재정의 및 최종
auto 키워드를 사용하지 마십시오.
새로운 순회 구문
TMap<FString, int32> MyMap;
// Old style
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// New style
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}- 새로운 유형의 열거형
// Old enum
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// New enum
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}의미론을 이동합니다. 모든 UE 내장 컨테이너는 이동 의미 체계를 지원하고 C++의 std::move 대신 MoveTemp를 사용합니다.
클래스 멤버 변수의 초기값입니다.
UCLASS()
class UMyClass : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
float Width = 11.5f;
UPROPERTY()
FString Name = TEXT("Earl Grey");
};- 타사 라이브러리 특정 형식입니다.
// @third party code - BEGIN PhysX
#include <physx.h>
// @third party code - END PhysX
// @third party code - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
// Used to set the thread name in the debugger
...
//@third party code - END MSDN SetThreadName더 자세한 코딩 사양은 UE 공식 문서: Coding Standard를 참조하세요.
1.3.2 컨테이너
Unreal Engine 자체는 기본 컨테이너 및 알고리즘 라이브러리 세트를 구현하며 STL 표준 라이브러리를 사용하지 않습니다. 그러나 그 중 일부는 아래 표와 같이 STL과 일대일 대응을 찾을 수 있습니다.
컨테이너 이름UE4STL분석
배열 TArray 벡터 연속 배열은 요소를 추가, 삭제, 정렬할 수 있으며 STL의 벡터보다 더 강력하고 편리합니다. 어레이를 추가하면 필요에 따라 메모리가 재할당되고 특정 전략에 따라 어레이 길이가 늘어납니다(자세한 내용은 아래 성장 전략 참조).
튜플 T튜플 튜플 데이터 세트를 저장합니다. 시공 후에는 길이를 변경할 수 없으며 요소 유형이 다를 수 있습니다.
연결된 목록 T목록 앞으로_목록 단방향 연결 목록, 작업 및 기본 구현은 stl과 유사합니다.
이중 연결 리스트 TDoubleLinkedList 목록 이중 연결 목록의 작업 및 기본 구현은 stl과 유사합니다.
매핑 테이블 티맵 지도 키-값 일대일 매핑 테이블을 정렬하고 하단에 TSet로 구현하며 키-값 쌍 배열 세트를 저장합니다.
다중 값 매핑 테이블 TMultiMap 순서가 없는_지도 키-다중 값 매핑 테이블, 순서, 기본 구현은 기본적으로 TMap과 동일하지만 요소를 추가할 때 기존 값이 삭제되지 않습니다. 차이점은 stl의 unordered_map이 순서가 없고 저장 및 인덱싱을 위해 해시 테이블을 사용한다는 것입니다.
정렬된 매핑 테이블 TSortedMap 지도 키-값 일대일 매핑 테이블, 정렬됨, 하단 레이어는 키별로 정렬된 TArray로 구현되고 키-값 쌍 배열 세트를 저장합니다. TMap에 비해 메모리를 절반 정도 차지하지만, 요소를 추가하고 삭제하는 복잡도는 O(n), 검색 복잡도는 O(Log n)입니다.
컬렉션 TSet 세트 키 모음과 키는 겹칠 수 없습니다. 하단 레이어는 TSparseArray를 사용하여 구현되며 요소는 버킷에 저장됩니다. 버킷의 수는 요소의 크기에 따라 달라지며 저장된 요소를 연결하기 위해 해시 값이 사용됩니다.
해시 테이블 F해시테이블 해시_맵 종종 다른 배열을 색인화하는 데 사용됩니다. 다른 Hash 함수에 따라 지정된 ID의 Hash 값을 구한 후 다른 배열의 요소를 저장하고 검색합니다.
큐 TQueue 대기열 잠금이 없는 연결 목록을 사용하여 구현된 제한되지 않은 비침해 대기열입니다. MPSC(다중 생산자-단일 소비자) 및 SPSC(단일 생산자-단일 소비자)의 두 가지 모드를 지원합니다. 두 모드 모두 스레드로부터 안전합니다. 여러 스레드 간의 데이터 전송 및 액세스에 일반적으로 사용됩니다.
순환 대기열 TCircularQueue
- SPSC(단일 생산자-단일 소비자) 모드에서 스레드로부터 안전한 순환 배열(TCircularBuffer)을 사용하여 구현된 잠금 없는 순환 큐, 선입선출.
루프 배열 TCircularBuffer
- 아래쪽 레이어는 경계가 없는 TArray를 사용하여 구현됩니다. 용량은 생성 시 지정해야 하며, 나중에 용량을 변경할 수 없습니다.
문자열 FString 문자열 내용과 크기를 동적으로 변경할 수 있는 문자열로, stl의 문자열과 유사하지만 더 완전한 기능을 제공합니다. 맨 아래 레이어는 TArray를 사용하여 구현됩니다. 또한 최적화된 버전인 FText 및 FName도 있습니다.
위의 UE와 STL 간의 대응은 제공된 호출 인터페이스(사용자)의 관점에서만 고려되었지만 실제 기본 구현 메커니즘은 매우 다를 수 있으므로 이 점을 참고하시기 바랍니다. 예를 들어, TArray의 요소 크기 증가 전략(일부 매크로 및 분기 단순화)을 자세히 살펴보겠습니다.
// Array.h
void TArray::ResizeGrow(SizeType OldNum)
{
ArrayMax = AllocatorInstance.CalculateSlackGrow(ArrayNum, ArrayMax, sizeof(ElementType));
AllocatorInstance.ResizeAllocation(OldNum, ArrayMax, sizeof(ElementType));
}
// ContainerAllocationPolicies.h
SizeType CalculateSlackGrow(SizeType NumElements, SizeType NumAllocatedElements, SIZE_T NumBytesPerElement) const
{
return DefaultCalculateSlackGrow(NumElements, NumAllocatedElements, NumBytesPerElement, true, Alignment);
}
template <typename SizeType>
SizeType DefaultCalculateSlackGrow(SizeType NumElements, SizeType NumAllocatedElements, SIZE_T BytesPerElement, bool bAllowQuantize, uint32 Alignment = DEFAULT_ALIGNMENT)
{
const SIZE_T FirstGrow = 4;
const SIZE_T ConstantGrow = 16;
SizeType Retval;
checkSlow(NumElements > NumAllocatedElements && NumElements > 0);
SIZE_T Grow = FirstGrow; // this is the amount for the first alloc
if (NumAllocatedElements || SIZE_T(NumElements) > Grow)
{
// Allocate slack for the array proportional to its size.
Grow = SIZE_T(NumElements) + 3 * SIZE_T(NumElements) / 8 + ConstantGrow;
}
if (bAllowQuantize)
{
Retval = (SizeType)(FMemory::QuantizeSize(Grow * BytesPerElement, Alignment) / BytesPerElement);
}
else
{
Retval = (SizeType)Grow;
}
// NumElements and MaxElements are stored in 32 bit signed integers so we must be careful not to overflow here.
if (NumElements > Retval)
{
Retval = TNumericLimits<SizeType>::Max();
}
return Retval;
}위에서 우리는 TArray 메모리 길이의 성장 전략을 볼 수 있습니다: 처음 할당되면 최소 4 요소만큼 증가합니다; 나중에 새 요소 크기에 따라 16만큼 비례적으로 고정적으로 커집니다. 그런 다음 8의 배수로 조정되고 메모리가 정렬됩니다. 메모리 성장 전략이 STL의 벡터와 상당히 다르다는 것을 알 수 있습니다.
위의 내용은 일반적으로 사용되는 UE 컨테이너의 일부 목록입니다. 수십 개의 UE 컨테이너가 있습니다. 전체 목록은 컨테이너에 있습니다.
1.3.3 수학 라이브러리
언리얼 엔진은 수학 라이브러리 세트를 구현하며, 코드는 Engine\Source\Runtime\Core\Public\Math 디렉터리에 있습니다. 일반적으로 사용되는 유형과 구문 분석은 다음과 같습니다.
유형이름분석
F박스 경계 상자 축 평행 3차원 경계 상자는 경계 몸체, 충돌 몸체, 가시성 결정 등에 자주 사용됩니다.
FBoxSphereBounds 구형 큐브 경계 상자 여기에는 구와 축 평행 큐브 경계 상자 데이터가 포함되어 있으며 각각은 서로 다른 목적으로 사용됩니다. 예를 들어 구는 장면 횡단 가속 구조에 사용되고 큐브는 충돌 감지 등에 사용됩니다. 대부분의 눈에 보이는 객체에 대한 경계 상자 유형입니다.
F색상 감마 공간 색상 감마 공간에 있고 선형 공간의 FLinearColor에서 변환할 수 있는 RGBA8888 4개 채널의 색상 값을 저장합니다.
FLinearColor 선형 공간 색상 RGBA 4채널의 색상값을 저장합니다. 각 채널의 정밀도는 32비트 부동 소수점 값입니다. 이는 선형 공간에 있으며 감마 공간의 FColor에서 변환될 수 있습니다.
FCapsuleShape 캡슐 본체 두 개의 원과 원통의 데이터가 저장됩니다. 두 개의 원은 원통의 양쪽 끝에 위치하여 캡슐을 형성합니다. 물리적 충돌 캡슐에 자주 사용됩니다.
FInterp곡선 보간 곡선 템플릿 클래스는 일련의 키 프레임을 저장하고 외부 곡선 조작을 용이하게 하기 위해 보간 및 파생과 같은 인터페이스를 제공합니다.
F매트릭스 4x4 매트릭스 회전, 배율 조정, 평행 이동, 기타 강체 변환 및 전단과 같은 비강체 변환과 같은 공간 변환을 저장하는 데 사용되는 16개의 부동 소수점 값을 포함합니다.
FMatrix2x2 2x2 행렬 2D 공간 변환을 위한 2x2 매트릭스가 포함되어 있습니다.
FQuat 쿼터니언 회전축, 회전각과 연관된 쿼터니언의 4차원 데이터가 저장됩니다. 회전 및 회전 보간과 같은 작업에 일반적으로 사용됩니다.
FPlane 비행기 점과 추가 W 값으로 설명되는 3차원 공간의 평면입니다.
프레이 레이 점과 벡터를 사용하여 3차원 공간의 광선을 설명합니다.
FRotationMatrix 회전 행렬 이동이 없는 회전 행렬은 이동이 있는 회전 행렬 FRotationTranslationMatrix에서 상속됩니다.
F로테이터 스피너 Pitch, Yaw 및 Roll로 설명되는 회전 구조를 제공합니다. 이는 인간 관점의 회전 설명 방법과 더욱 일치하며 로직 레이어를 통해 객체(예: 카메라)의 회전을 제어할 수 있습니다.
FSphere 구체 점과 반지름으로 설명되는 3차원 구입니다.
F수학 수학 도구 상자 크로스 플랫폼, 정밀도 호환 수학 상수 정의 및 유틸리티 함수 모음입니다.
F벡터 3D 벡터 설명 지점으로도 사용할 수 있는 각 차원의 부동 소수점 값을 갖는 3차원 공간의 벡터입니다.
FVector2D 2D 벡터 2차원 벡터는 2D 점을 설명할 수도 있습니다.
F벡터4 4D 벡터 XYZW의 4차원 벡터를 저장하며 동차 좌표, 투영 변환 등에 사용할 수 있습니다.
위에 나열된 일반적인 유형 외에도 UE 수학 라이브러리는 다양한 정밀도의 부동 소수점 수, 난수, 경계, 저차 시퀀스, 장면 관리 노드, 보조 클래스 및 기본 유형에서 파생된 도구 상자 등과 같은 모듈도 제공합니다. 수학 라이브러리의 전체 목록은 UE 소스 코드 또는 공식 문서인 Unreal Engine Math를 참조하세요.
UE가 다양한 매크로 지원 해당 버전을 정의할 수 있는 여러 가지 최적화된 벡터 SIMD 명령 버전을 제공한다는 점은 언급할 가치가 있습니다.
// Engine\Source\Runtime\Core\Public\Math\VectorRegister.h
// Platform specific vector intrinsics include.
#if WITH_DIRECTXMATH
#define SIMD_ALIGNMENT (16)
#include "Math/UnrealMathDirectX.h"
#elif PLATFORM_ENABLE_VECTORINTRINSICS
#define SIMD_ALIGNMENT (16)
#include "Math/UnrealMathSSE.h"
#elif PLATFORM_ENABLE_VECTORINTRINSICS_NEON
#define SIMD_ALIGNMENT (16)
#include "Math/UnrealMathNeon.h"
#else
#define SIMD_ALIGNMENT (4)
#include "Math/UnrealMathFPU.h"
#endif위 코드에서 볼 수 있듯이 UE는 DirectX 내장 라이브러리, Arm Neon 명령어, SSE 명령어, FPU 및 기타 버전을 지원합니다.
네온은 Arm이 디자인했습니다. Arm Cortex-A 및 Cortex-R 시리즈 프로세서에 적합한 단일 명령 다중 데이터(SIMD) 아키텍처 확장 기술 세트입니다.
SSE(Stream SIMD Extensions)는 Intel에서 설계했습니다. 컴퓨터 칩 Pentium3에 처음 도입된 명령어 세트는 MMX를 따르는 확장 명령어 세트이며 x86 및 x64 아키텍처에 적합합니다. 현재 SSE2, SSE3, SSSE3, SSE4와 같은 명령어 세트가 이미 존재합니다.
FPU(부동 소수점 단위)는 CPU 코어의 하드웨어 구조의 일부를 구성하는 부동 소수점 계산 단위입니다. 부동 소수점 연산과 벡터 연산을 처리하는 CPU의 핵심 장치입니다.
1.3.4 좌표 공간
UE는 왼손 좌표계를 사용합니다(DirectX와 동일하지만 OpenGL은 오른손 좌표계를 사용합니다). 기본 레벨(새로 생성된 장면) 보기에서 Z축은 위쪽, Y축은 왼쪽, X축은 시선 뒤쪽을 향합니다. 그러나 CameraActor를 장면으로 드래그하면 카메라의 기본 뷰는 Z축이 위쪽을 향하고 Y는 오른쪽을 향하고 X는 뷰의 앞쪽을 향하는 것입니다. UE 좌표계의 기본 보기는 다른 많은 엔진과 다릅니다. 처음 접하시는 분들에게는 다소 낯설 수도 있지만, 오랫동안 사용하시면 불편함을 느끼지 못할 것입니다.

UE 카메라 뷰의 기본 좌표계 방향은 그림과 같습니다.
UE의 좌표 공간은 기본적으로 3D 렌더링 파이프라인의 변환과 동일하지만 몇 가지 독특한 개념도 있습니다. 세부사항은 다음과 같습니다:
UE 좌표 공간 중국어 이름 별칭 분석하다
탄젠트 탄젠트 공간
- 직교(보간 후 바이어스됨)는 왼손잡이 또는 오른손잡이일 수 있습니다. TangentToLocal은 회전만 포함하고 위치 변환 정보는 포함하지 않으므로 OrthoNormal입니다(전치 행렬도 역행렬임).
로컬 지역 공간 ObjectSpace(객체 공간) 직교 - 왼쪽-오른쪽 또는 오른쪽일 수 있습니다(삼각형 자르기와 관련되어 조정이 필요함을 의미). LocalToWorld에는 회전, 크기 조정 및 변환과 같은 정보가 포함되어 있습니다. 애니메이션, 바람 방향 등과 같은 시뮬레이션의 경우 크기 조정이 음수일 수 있습니다.
세계 세계 공간
- WorldToView 행렬에는 크기 조정이 아닌 회전 및 이동만 포함됩니다.
번역된 세계 번역이 있는 세계 공간
- TranslatedWorld=World+PreViewTranslation, PreViewTranslation은 카메라 위치의 반대 위치이며 TranslatedWorld는 카메라 변환 정보가 포함되지 않은 World 매트릭스와 동일합니다. 이는 BasePass, 뼈 스키닝, 입자 효과, 머리카락, 노이즈 감소 및 기타 계산에 널리 사용됩니다.
보기 시야 공간 카메라공간 뷰 공간은 카메라의 가까운 클리핑 평면의 중심을 원점으로 하는 좌표 공간입니다. ViewToClip 행렬에는 x,y 배율이 포함되어 있지만 변환은 포함되어 있지 않습니다. 깊이 값 z는 크기 조정 및 변환이 가능하며, 종종 균일한 투영 공간에 투영 행렬 변환을 적용합니다.
클립 클리핑 공간 HomogeniousCoordinates(균일 좌표), PostProjectionSpace(투영 후 공간), ProjectionSpace(투영 공간) 원근 투영 행렬이 적용되면 동종 클리핑 공간으로 변환될 수 있습니다. 클립 공간의 W는 뷰 공간의 Z와 동일합니다.
화면 화면 공간 NormalizedDeviceCoordinates(정규화된 장치 좌표) Clip 공간의 좌표(xyz를 w 구성요소로 나눈 값)에 원근 분할을 적용한 후 화면 공간의 좌표를 얻을 수 있습니다. 화면 공간의 가로 좌표는 왼쪽에서 오른쪽으로 [-1, 1] 값을 취하고, 세로 좌표는 아래에서 위로 [-1, 1] 값을 취하고, 깊이는 가까운 곳에서 먼 곳으로 [0, 1] 값을 갖습니다(그러나 OpenGL RHI의 깊이는 [-1, 1] 값을 갖습니다).
뷰포트 뷰포트 공간 뷰포트 좌표, 창 좌표 화면 좌표를 창의 픽셀 좌표에 매핑합니다. 가로 좌표는 왼쪽에서 오른쪽으로 [0, 너비-1] 값을 취하고, 세로 좌표는 위에서 아래로 [0, 높이-1] 값을 갖습니다(화면 공간의 세로 좌표는 아래에서 위로 증가한다는 점에 유의하세요).
UE의 C++ 인터페이스나 셰이더 변수에는 한 공간에서 다른 공간으로의 광범위한 변환이 있습니다. 이들의 이름은 X To Y입니다(위 표에서 X와 Y는 모두 공백 명사입니다). 일반적인 것들은 다음과 같습니다:
-LocalToWorld
-LocalToView
-TangentToWorld
TangentToView
월드투스크린
-WorldToLocal
-WorldToTangent
-......
탄젠트 공간은 로컬 공간(모델 공간)과 다릅니다. 각 버텍스(또는 픽셀)의 법선과 탄젠트를 축으로 하여 직교 좌표 공간을 구성합니다.

모델 정점의 탄젠트 공간의 개략도. 각 정점에는 고유한 탄젠트 공간이 있습니다.

정점으로부터 직교 탄젠트 공간의 3개 축(탄젠트 T, 부접선 B, 노멀 N)을 구성하기 위한 일반적인 공식입니다.
이미 로컬 공간이 있는데 탄젠트 공간이 필요한 이유는 무엇입니까?
이는 탄젠트 공간의 역할에서 답할 수 있다. 요약하면 주요 내용은 다음과 같습니다.
다양한 애니메이션을 지원합니다. 여기에는 스킨이 적용된 골격 애니메이션, 프로그래밍된 애니메이션, 버텍스 애니메이션, UV 애니메이션 등이 포함됩니다. 모델이 애니메이션 작업을 수행하므로 법선이 변경됩니다. 탄젠트 공간에서 법선이 실시간으로 수정되지 않으면 잘못된 조명 결과가 생성됩니다.
**조명의 탄젠트 공간 계산을 지원합니다.**광원 방향 L과 시선 V를 탄젠트 공간으로 변환하고 일반 샘플링에서 직접 얻은 노멀 N을 추가하기만 하면 조명 계산을 수행하고 올바른 조명 결과를 얻을 수 있습니다.
노멀맵은 재사용이 가능합니다. 탄젠트 공간의 노멀 맵은 상대적인 노멀 정보를 기록하므로, 전혀 다른 메시 모델에 노멀 맵을 적용하더라도 비교적 합리적인 라이팅 결과를 얻을 수 있습니다. 동일한 모델은 노멀 맵을 여러 번 재사용할 수 있으며, 다른 모델도 동일한 노멀 맵을 재사용할 수 있습니다. 예를 들어 큐브 모델의 경우 6개 면 모두에 하나의 맵만 사용할 수 있습니다.
압축 가능. 탄젠트 공간에서 노멀 맵의 노멀 Z 방향은 항상 Z 축의 양의 방향을 향하므로 노멀 맵은 Z 방향을 파생시키기 위해 XY 방향만 저장하면 됩니다.
위에서는 노멀 텍스처의 압축에 대해 언급했는데, 그런데 UE Shader 레이어에 널리 존재하는 단위 벡터의 압축도 비교적 비슷합니다.
Zina H. Cigolle 등은 2014년 초에 독립 단위 벡터에 대한 효율적인 표현에 대한 설문조사 논문을 발표했습니다. 이 논문에서는 3차원 단위 벡터를 2차원 단위로 압축하는 방법을 제안했습니다. 압축 과정은 아래 그림과 같이 먼저 단위 구(Sphere)를 팔면체(Octahedron)로 매핑한 다음 이를 2차원 정육면체(Square)로 투영하는 것입니다.

감압 과정은 정반대입니다. UE의 셰이더 코드는 압축 및 압축 해제의 특정 프로세스를 명확하게 기록합니다.
// Engine\Shaders\Private\DeferredShadingCommon.ush
// 압축/패킹(Packing): 로부터 3의 벡터 변환(Transform/Convert)까지 , 반환(Return)2의 결과 .
float2 UnitVectorToOctahedron( float3 N )
{
N.xy /= dot( 1, abs(N) ); // 변환(Transform/Convert)로
if( N.z <= 0 )
{
N.xy = ( 1 - abs(N.yx) ) * ( N.xy >= 0 ? float2(1,1) : float2(-1,-1) );
}
return N.xy;
}
// 압축해제/언패킹(Unpacking): 로부터 2의 벡터 변환(Transform/Convert)까지 3의 벡터 .
float3 OctahedronToUnitVector( float2 Oct )
{
float3 N = float3( Oct, 1 - dot( 1, abs(Oct) ) );
if( N.z < 0 )
{
N.xy = ( 1 - abs(N.yx) ) * ( N.xy >= 0 ? float2(1,1) : float2(-1,-1) );
}
return normalize(N);
}위의 압축된 벡터는 단위 길이가 필요하므로 입사광의 방향, 시선, 노멀 등의 벡터만 압축할 수 있습니다. 색상, 빛의 강도 등 길이 정보가 포함된 벡터는 정확하게 압축할 수 없습니다.
또한 UE는 반팔각형 인코딩 및 디코딩도 지원합니다.
// Engine\Shaders\Private\DeferredShadingCommon.ush
// 3벡터 압축/패킹(Packing)의 2벡터
float2 UnitVectorToHemiOctahedron( float3 N )
{
N.xy /= dot( 1, abs(N) );
return float2( N.x + N.y, N.x - N.y );
}
// 의 2벡터 압축해제/언패킹(Unpacking)3벡터
float3 HemiOctahedronToUnitVector( float2 Oct )
{
Oct = float2( Oct.x + Oct.y, Oct.x - Oct.y ) * 0.5;
float3 N = float3( Oct, 1 - dot( 1, abs(Oct) ) );
return normalize(N);
}1.3.5 기본 매크로 정의
다양한 플랫폼과 컴파일러의 다양한 옵션 간의 차이점과 호환되기 위해 UE는 주로 Definitions.h 및 Build.h 파일에 집중된 다양한 매크로 정의를 정의합니다.
// Engine\Intermediate\Build\Win64\UE4Editor\Development\Launch\Definitions.h
#define IS_PROGRAM 0
#define UE_EDITOR 1
#define ENABLE_PGO_PROFILE 0
#define USE_VORBIS_FOR_STREAMING 1
#define USE_XMA2_FOR_STREAMING 1
#define WITH_DEV_AUTOMATION_TESTS 1
#define WITH_PERF_AUTOMATION_TESTS 1
#define UNICODE 1
#define _UNICODE 1
#define __UNREAL__ 1
#define IS_MONOLITHIC 0
#define WITH_ENGINE 1
#define WITH_UNREAL_DEVELOPER_TOOLS 1
#define WITH_APPLICATION_CORE 1
#define WITH_COREUOBJECT 1
#define USE_STATS_WITHOUT_ENGINE 0
#define WITH_PLUGIN_SUPPORT 0
#define WITH_ACCESSIBILITY 1
#define WITH_PERFCOUNTERS 1
#define USE_LOGGING_IN_SHIPPING 0
#define WITH_LOGGING_TO_MEMORY 0
#define USE_CACHE_FREED_OS_ALLOCS 1
#define USE_CHECKS_IN_SHIPPING 0
#define WITH_EDITOR 1
#define WITH_SERVER_CODE 1
#define WITH_PUSH_MODEL 0
#define WITH_CEF3 1
#define WITH_LIVE_CODING 1
#define WITH_XGE_CONTROLLER 1
#define UBT_MODULE_MANIFEST "UE4Editor.modules"
#define UBT_MODULE_MANIFEST_DEBUGGAME "UE4Editor-Win64-DebugGame.modules"
#define UBT_COMPILED_PLATFORM Win64
#define UBT_COMPILED_TARGET Editor
#define UE_APP_NAME "UE4Editor"
#define NDIS_MINIPORT_MAJOR_VERSION 0
#define WIN32 1
#define _WIN32_WINNT 0x0601
#define WINVER 0x0601
#define PLATFORM_WINDOWS 1
#define PLATFORM_MICROSOFT 1
#define OVERRIDE_PLATFORM_HEADER_NAME Windows
#define RHI_RAYTRACING 1
#define NDEBUG 1
#define UE_BUILD_DEVELOPMENT 1
#define UE_IS_ENGINE_MODULE 1
#define WITH_LAUNCHERCHECK 0
#define UE_BUILD_DEVELOPMENT_WITH_DEBUGGAME 0
#define UE_ENABLE_ICU 1
#define WITH_VS_PERF_PROFILER 0
#define WITH_DIRECTXMATH 0
#define WITH_MALLOC_STOMP 1
#define CORE_API DLLIMPORT
#define TRACELOG_API DLLIMPORT
#define COREUOBJECT_API DLLIMPORT
#define INCLUDE_CHAOS 0
#define WITH_PHYSX 1
#define WITH_CHAOS 0
#define WITH_CHAOS_CLOTHING 0
#define WITH_CHAOS_NEEDS_TO_BE_FIXED 0
#define PHYSICS_INTERFACE_PHYSX 1
#define WITH_APEX 1
#define WITH_APEX_CLOTHING 1
#define WITH_CLOTH_COLLISION_DETECTION 1
#define WITH_PHYSX_COOKING 1
#define WITH_NVCLOTH 1
#define WITH_CUSTOM_SQ_STRUCTURE 0
#define WITH_IMMEDIATE_PHYSX 0
#define GPUPARTICLE_LOCAL_VF_ONLY 0
#define ENGINE_API DLLIMPORT
#define NETCORE_API DLLIMPORT
#define APPLICATIONCORE_API DLLIMPORT
#define DDPI_EXTRA_SHADERPLATFORMS SP_XXX=32,
#define DDPI_SHADER_PLATFORM_NAME_MAP { TEXT("XXX"), SP_XXX },
#define RHI_API DLLIMPORT
#define JSON_API DLLIMPORT
#define WITH_FREETYPE 1
#define SLATECORE_API DLLIMPORT
#define INPUTCORE_API DLLIMPORT
#define SLATE_API DLLIMPORT
#define WITH_UNREALPNG 1
#define WITH_UNREALJPEG 1
#define WITH_UNREALEXR 1
#define IMAGEWRAPPER_API DLLIMPORT
#define MESSAGING_API DLLIMPORT
#define MESSAGINGCOMMON_API DLLIMPORT
#define RENDERCORE_API DLLIMPORT
#define ANALYTICSET_API DLLIMPORT
#define ANALYTICS_API DLLIMPORT
#define SOCKETS_PACKAGE 1
#define SOCKETS_API DLLIMPORT
#define ASSETREGISTRY_API DLLIMPORT
#define ENGINEMESSAGES_API DLLIMPORT
#define ENGINESETTINGS_API DLLIMPORT
#define SYNTHBENCHMARK_API DLLIMPORT
#define RENDERER_API DLLIMPORT
#define GAMEPLAYTAGS_API DLLIMPORT
#define PACKETHANDLER_API DLLIMPORT
#define RELIABILITYHANDLERCOMPONENT_API DLLIMPORT
#define AUDIOPLATFORMCONFIGURATION_API DLLIMPORT
#define MESHDESCRIPTION_API DLLIMPORT
#define STATICMESHDESCRIPTION_API DLLIMPORT
#define PAKFILE_API DLLIMPORT
#define RSA_API DLLIMPORT
#define NETWORKREPLAYSTREAMING_API DLLIMPORT
// Engine\Source\Runtime\Core\Public\Misc\Build.h
#ifndef UE_BUILD_DEBUG
#define UE_BUILD_DEBUG 0
#endif
#ifndef UE_BUILD_DEVELOPMENT
#define UE_BUILD_DEVELOPMENT 0
#endif
#ifndef UE_BUILD_TEST
#define UE_BUILD_TEST 0
#endif
#ifndef UE_BUILD_SHIPPING
#define UE_BUILD_SHIPPING 0
#endif
#ifndef UE_GAME
#define UE_GAME 0
#endif
#ifndef UE_EDITOR
#define UE_EDITOR 0
#endif
#ifndef UE_BUILD_SHIPPING_WITH_EDITOR
#define UE_BUILD_SHIPPING_WITH_EDITOR 0
#endif
#ifndef UE_BUILD_DOCS
#define UE_BUILD_DOCS 0
#endif
(......)일반적인 기본 매크로와 설명은 다음과 같습니다.
매크로 이름분석기본값
UE_EDITOR 현재 프로그램이 편집기인가요? 가장 많이 사용되는 프로그램입니다. 1
엔진 있음 엔진을 활성화할지 여부에 관계없이 SDK는 기본 API만 제공하며 많은 모듈이 제대로 작동하지 않습니다. 1
WITH_EDITOR UE_EDITOR와 유사하게 에디터를 활성화할지 여부입니다. 1
WIN32 win32비트 프로그램인지 여부. 1
PLATFORM_WINDOWS 운영 플랫폼이 Windows인지 여부. 1
UE_BUILD_DEBUG 디버그 빌드 모드. 0
UE_BUILD_개발 개발자 빌드 모드. 1
UE_BUILD_SHIPPING 빌드 모드를 해제합니다. 0
UE_GAME 게임 제작 모드. 0
UE_EDITOR 에디터 빌드 모드. 0
UE_BUILD_DEVELOPMENT_WITH_DEBUGGAME 게임 디버깅이 포함된 개발자 빌드 모드. 0
UE_BUILD_SHIPPING_WITH_EDITOR 에디터로 빌드 모드를 해제하세요. 0
UE_BUILD_DOCS 문서 구성 모드. 0
RHI_RAYTRACING 레이 트레이싱 활성화 여부 1
1.4 엔진 모듈
이 섹션에서는 UE에 익숙하지 않은 독자들이 전반적인 이해를 갖고 렌더링 모듈에 더 잘 들어갈 수 있도록 UE의 기본 시스템과 개념을 살펴보겠습니다.
1.4.1 객체, 액터, ActorComponent
UObject는 UE의 모든 객체 유형에 대한 기본 클래스입니다. UObjectBaseUtility에서 상속되고, UObjectBase에서 상속됩니다. 메타데이터, 리플렉션 생성, GC 가비지 수집, 직렬화, 부분 편집기 정보, 객체 생성 및 소멸, 이벤트 콜백 등과 같은 기능을 제공합니다. 서브클래스의 특정 유형은 UClass 설명에 따라 결정됩니다. 이들의 상속 관계는 다음과 같습니다.

AActor는 UE 시스템에서 가장 중요하고 중요한 개념이자 유형입니다. 이는 UObject에서 상속되며 게임 레벨에 배치할 수 있는 모든 객체의 기본 클래스입니다. Unity 엔진의 GameObject와 동일합니다. 이는 네트워크 동기화(Replication), 객체 생성 및 파괴, 프레임 업데이트(Tick), 구성요소 작업, Actor 중첩 작업, 변환 등과 같은 기능을 제공합니다. AActor 객체는 AActor 객체를 중첩할 수 있으며 다음 인터페이스에서 지원됩니다.
// Engine\Source\Runtime\Engine\Classes\GameFramework\Actor.h
void AttachToActor(AActor* ParentActor, ... );
void AttachToComponent(USceneComponent* Parent, ... );위의 두 인터페이스는 실제로 동일합니다. 실제로 AActor::AttachToActor의 구현 코드는 RootComponent::AttachToComponent 인터페이스도 호출하기 때문입니다.
// Engine\Source\Runtime\Engine\Private\Actor.cpp
void AActor::AttachToActor(AActor* ParentActor, const FAttachmentTransformRules& AttachmentRules, FName SocketName)
{
if (RootComponent && ParentActor)
{
USceneComponent* ParentDefaultAttachComponent = ParentActor->GetDefaultAttachComponent();
if (ParentDefaultAttachComponent)
{
RootComponent->AttachToComponent(ParentDefaultAttachComponent, AttachmentRules, SocketName);
}
}
}즉, Actor 자체에는 중첩 기능이 없지만 일대일 관계를 갖는 RootSceneComponent를 통해 구현할 수 있습니다.
Actor에서 상속되는 일반적인 하위 클래스는 다음과 같습니다.
ASkeletalMeshActor: 뼈 스키닝을 사용하여 동적 모델을 렌더링하는 데 사용되는 스키닝된 뼈대 본체.
AStaticMeshActor: 정적 모델.
ACameraActor: 카메라 객체.
APlayerCameraManager: 카메라 관리자는 현재 세계의 모든 카메라(ACameraActor) 인스턴스를 관리합니다.
ALight: 점 광원(APointLight), 방향성 조명(ADirectionalLight), 스포트라이트(ASpotLight), 직사각형 조명(ARectLight) 및 기타 유형에서 파생된 조명 개체입니다.
AReflectionCapture: 환경 맵의 오프라인 생성을 위한 반사 캡처 프로그램입니다.
AController: 캐릭터 컨트롤러. AAIController 및 APlayerController와 같은 하위 클래스도 아래에서 파생됩니다.
APawn: AI를 사용하여 동적 캐릭터나 개체를 설명합니다. 그 서브클래스에는 ACharacter, ADefaultPawn, AWheeledVehicle 등이 포함됩니다.
AMaterialInstanceActor: 머티리얼 인스턴스 바디.
ALightmassPortal: 오프라인 전역 조명 효율성 및 효과를 가속화하고 개선하는 데 사용되는 전역 조명 포털입니다.
AInfo: 구성 정보 클래스의 기본 클래스입니다. 여기에서 상속된 일반적인 하위 클래스에는 AWorldSettings, AGameModeBase, AAtmosphericFog, ASkyAtmosphere, ASkyLight 등이 포함됩니다.
......
위에는 AActor의 일부 서브클래스만 나열되어 있습니다. 그 중 일부는 레벨에 배치할 수 있지만 일부는 레벨에 직접 배치할 수 없다는 것을 알 수 있습니다. 상속 시스템의 일부는 다음과 같습니다.

UActorComponent는 UObject와 IInterface_AssetUserData 인터페이스를 상속합니다. 이는 모든 구성 요소 유형의 기본 클래스이며 AActor 인스턴스에 하위 노드로 추가될 수 있습니다. 액터는 일련의 구성 요소를 포함하는 컨테이너로 간주할 수 있다는 것이 보다 직관적으로 말할 수 있습니다. 액터의 기능적 특징과 프로퍼티는 주로 액터에 부착된 컴포넌트에 따라 결정됩니다.
일반적으로 사용되는 주요 UActorComponent 하위 구성 요소 유형은 다음과 같습니다.
USceneComponent: SceneComponent는 변환을 소유한 ActorComponent입니다. 변환은 위치, 회전 및 배율로 정의되는 장면의 위치입니다. SceneComponent는 계층적 방식으로 서로 연결될 수 있습니다. 액터의 위치, 회전, 스케일은 계층구조 루트에 있는 SceneComponent에서 가져옵니다.
UPrimitiveComponent: SceneComponent에서 상속되며 모든 표시 가능한(메시 또는 입자 시스템과 같이 렌더링 가능한) 객체의 기본 클래스입니다. 또한 물리, 충돌, 광 채널 및 기타 기능을 제공합니다.
UMeshComponent: 렌더링 가능한 모든 삼각형 메시 컬렉션(정적 모델, 동적 모델, 절차적으로 생성된 모델)의 기본 클래스인 UPrimitiveComponent에서 상속됩니다.
UStaticMeshComponent: UMeshComponent에서 상속되며 정적 메쉬의 형상이며 UStaticMesh 인스턴스를 만드는 데 자주 사용됩니다.
USkinnedMeshComponent: 스킨드 메시 렌더링을 지원하고 메시, 뼈대 리소스, 메시 LOD와 같은 인터페이스를 제공하는 구성 요소인 UMeshComponent에서 상속됩니다.
USkeletalMeshComponent: USkinnedMeshComponent에서 상속되며 일반적으로 애니메이션이 포함된 USkeletalMesh 리소스 인스턴스를 만드는 데 사용됩니다.
이들의 상속 관계는 다음과 같습니다.

레벨에 배치할 수 있는 모든 액터에는 루트 구성요소(장면 구성요소 유형)가 있으며, 이는 장면 구성요소의 하위 클래스일 수 있습니다. Scene 컴포넌트는 월드 내 액터의 위치, 각도, 스케일링을 지정하며 이러한 속성은 액터의 모든 하위 객체에 영향을 미칩니다.
빈 액터에도 가장 간단한 씬 구성요소인 "기본 씬 루트" 객체가 있습니다. 에디터 작업 단계에서 액터에 새 씬 컴포넌트를 배치하면 액터의 기본 씬 루트 객체가 교체됩니다.

Actor, RootComponent, SceneComponent, ActorComponent 계층적 중첩 다이어그램.
1.4.2 레벨, 월드, WorldContext, 엔진
ULevel은 UE의 레벨입니다. 이는 장면의 객체 모음이며 보이는 객체(예: 메시, 조명, 특수 효과 등)와 보이지 않는 객체(예: 볼륨, 청사진, 레벨 구성, 탐색 데이터 등)를 포함한 일련의 액터를 저장합니다.
UWorld는 장면을 실제로 표현하는 ULevel의 컨테이너입니다. 그 이유는 콘텐츠를 표시하려면 ULevel이 UWorld에 배치되어야 하기 때문입니다. 각 UWorld 인스턴스는 기본 레벨(영구 레벨)을 포함해야 하며 여러 스트리밍 레벨도 포함할 수 있습니다(스트리밍 레벨, 선택 사항, 필수는 아님, 요청 시 동적으로 로드 및 언로드 가능). 레벨 정보 외에도 UWorld는 Scene, GameInstance, AISystem, FXSystem, NavigationSystem, PhysicScene, TimerManager 및 기타 정보도 저장합니다. 다음과 같은 유형이 있습니다.
// Engine\Source\Runtime\Engine\Classes\Engine\EngineTypes.h
namespace EWorldType
{
enum Type
{
None,
Game,
Editor,
PIE,
EditorPreview,
GamePreview,
GameRPC,
Inactive
};
}일반적인 WorldType에는 Game, Editor, Editor Playback(PIE) 및 미리 보기 모드(EditorPreview, GamePreview) 등이 포함됩니다. 우리가 일반적으로 사용하는 편집기의 장면은 실제로 Editor 유형의 World입니다.
FWorldContext는 엔진 수준에서 Level을 처리하기 위한 장치 컨텍스트로, UEEngine이 World 관련 정보를 관리하고 기록하는 데 도움이 됩니다. 내부 클래스에 사용되며 논리 계층에서 직접 조작하면 안 됩니다. 저장되는 데이터에는 World 유형, ContextHandle, GameInstance, GameViewport 및 기타 정보가 포함됩니다.
UEngine은 많은 내부 시스템과 리소스를 제어하고 관리하며 UGameEngine과 UEditorEngine에서 파생됩니다. 싱글톤 전역 변수입니다.
// Engine\Source\Runtime\Engine\Classes\Engine\Engine.h
/** Global engine pointer. Can be 0 so don't use without checking. */
extern ENGINE_API class UEngine* GEngine;프로그램 시작 시 FEngineLoop::PreInitPostStartupScreen에 값이 생성되고 할당됩니다.
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
int32 FEngineLoop::PreInitPostStartupScreen(const TCHAR* CmdLine)
{
(......)
if ( GEngine == nullptr )
{
#if WITH_EDITOR
if ( GIsEditor )
{
FString EditorEngineClassName;
GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("EditorEngine"), EditorEngineClassName, GEngineIni);
UClass* EditorEngineClass = StaticLoadClass( UEditorEngine::StaticClass(), nullptr, *EditorEngineClassName);
// 생성(Create)인스턴스
GEngine = GEditor = NewObject<UEditorEngine>(GetTransientPackage(), EditorEngineClass);
(......)
}
else
#endif
{
FString GameEngineClassName;
GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("GameEngine"), GameEngineClassName, GEngineIni);
UClass* EngineClass = StaticLoadClass( UEngine::StaticClass(), nullptr, *GameEngineClassName);
// 생성(Create)인스턴스
GEngine = NewObject<UEngine>(GetTransientPackage(), EngineClass);
(......)
}
}
(......)
return 0;
}위에서 볼 수 있듯이, 에디터 모드인지 여부에 따라 UEditorEngine 또는 UGameEngine의 인스턴스가 생성된 다음 전역 변수 GEngine에 할당됩니다. GEngine은 다른 곳의 코드를 통해 직접 액세스할 수 있습니다.
ULevel, UWorld, FWorldContext, UEEngine 사이의 상속, 종속성, 참조 관계는 아래 그림과 같습니다:

1.4.3 메모리 할당
UE의 메모리 할당 시스템은 크고 복잡하다고 설명할 수 있다. 제공되는 기능은 다음과 같이 요약됩니다.
시스템 플랫폼 간의 차이점을 캡슐화하고 통일된 인터페이스를 제공합니다.
특정 규칙에 따라 효율적으로 메모리를 생성하고 회수하여 메모리 운영 효율성을 효과적으로 향상시킬 수 있습니다.
귀하의 필요에 맞게 다양한 메모리 할당 방식을 지원합니다.
다양한 시나리오에 대처할 수 있도록 다양한 호출 방법을 지원합니다.
다중 스레드로부터 안전한 메모리 작업을 지원합니다.
TLS(스레드 로컬 캐시)를 부분적으로 지원합니다.
GPU 메모리의 통합 관리를 지원합니다.
메모리 디버깅 및 통계정보를 제공합니다.
확장성이 좋다.
그렇다면 UE는 위의 목표를 어떻게 달성합니까? 그 비밀은 아래에서 공개하겠습니다.
1.4.3.1 메모리 할당의 기본
나중에 메모리 할당 방식을 더 잘 설명하기 위해 이 섹션에서는 먼저 관련된 기본 개념을 설명합니다.
- FFreeMem
할당 가능한 작은 메모리 정보 레코드는 FMallocBinned에서 다음과 같이 정의됩니다.
struct FMallocBinned::FFreeMem
{
FFreeMem* Next; // 메모리 또는 . 에 의해 메모리 (pool), .
uint32 NumFreeBlocks; // 할당(Allocate)의 연속 메모리 개수 , 로 1.
uint32 Padding; // 구조체 16정렬 의 .
};- F풀정보
일반적으로 사용되는 메모리 할당자에서 메모리 풀은 시스템 운영 메모리의 오버헤드를 줄이기 위해 일반적으로 더 큰 메모리를 먼저 할당한 다음 큰 메모리를 여러 개의 작은 블록으로 나눕니다(UE의 작은 메모리 블록은 크기가 동일합니다).
먼저 큰 메모리 조각을 할당한 다음 이를 여러 개의 작은 조각으로 잘라야 하는 이유는 무엇입니까?
"게임 엔진 아키텍처" 5장의 메모리 관리 장에서 심층적이고 명확한 답을 얻을 수 있습니다. 그 이유는 다음과 같이 요약됩니다.
메모리 할당자는 일반적으로 힙에서 작동하며 프로세스가 상대적으로 느립니다. 직접 적용할 경우 모든 크기의 할당 요청을 처리해야 하는 범용 장치이므로 운영 체제에 상당한 관리 오버헤드가 발생합니다.
대부분의 운영 체제에서 시스템 메모리 작업을 호출하면 사용자 모드에서 커널 모드로 전환되고 요청을 처리한 후 사용자 모드로 전환됩니다. 이러한 상태 사이를 전환하는 데는 많은 시간이 소요됩니다.

Windows 운영체제의 사용자 모드와 코어 모드 간 통신의 개략도. 이들 간의 통신에는 여러 계층의 드라이버가 필요하다는 것을 알 수 있습니다.
이 할당 방법은 메모리 할당 효율성을 효과적으로 향상시키고 모든 메모리 작업(GC, 최적화, 조각 모음, 적시 해제 등)을 전역적으로 관리할 수 있습니다. 그러나 일정 비율의 메모리 공간이 불가피하게 낭비되고, 순간적인 IO가 증가하고, 메모리 조각화(정기적으로 구성될 수 있음)가 형성되는 등의 부작용도 있습니다.
FPoolInfo는 FMallocBinned에서 다음과 같이 정의됩니다.
struct FMallocBinned::FPoolInfo
{
uint16 Taken; // 할당(Allocate)의 메모리 개수 .
uint16 TableIndex; // 의 MemSizeToPoolTable.
uint32 AllocSize; // 할당(Allocate)의 메모리 크기 .
FFreeMem* FirstMem; // 만약 는 , 메모리 을(를) 활용하여 의 메모리 ; 만약 , 에 의해 할당(Allocate)의 메모리 .
FPoolInfo* Next; // 개 메모리 .
FPoolInfo** PrevLink; // 개 메모리 .
};메모리 풀의 메모리 블록(Blocks)은 크기가 동일하므로 메모리 풀의 메모리 분배 다이어그램은 다음과 같습니다.

- FPoolTable
메모리 풀 테이블은 이중 연결 리스트를 사용하여 메모리 풀 세트를 저장합니다. 메모리 풀 테이블의 메모리 풀에 할당 가능한 메모리 블록이 부족할 경우 메모리 풀을 다시 생성하여 이중 연결 리스트에 추가합니다.
FPoolTable은 FMallocBinned에서 다음과 같이 정의됩니다.
struct FMallocBinned::FPoolTable
{
FPoolInfo* FirstPool; // 메모리 , 는 테이블 의 테이블 .
FPoolInfo* ExhaustedPool; // (할당(Allocate)의 메모리 )의 메모리 테이블
uint32 BlockSize; // 메모리 크기
};FPoolTable의 데이터 구조 다이어그램:

- 풀해시버킷
메모리 풀 해시 버킷은 메모리 주소로 해시된 키에 해당하는 모든 메모리 풀을 저장하는 데 사용됩니다. PoolHashBucket은 FMallocBinned에 다음과 같이 정의됩니다.
struct FMallocBinned::PoolHashBucket
{
UPTRINT Key; //
FPoolInfo* FirstPool; // 메모리
PoolHashBucket* Prev; // 개 메모리
PoolHashBucket* Next; // 개 메모리
};데이터 구조 다이어그램은 다음과 같습니다.

- 메모리 크기
UE의 메모리 크기에는 메모리 풀 크기(PoolSize), 메모리 페이지 크기(PageSize) 및 메모리 블록(BlockSize)을 포함한 많은 매개변수가 포함됩니다. 실제 크기는 할당자, 시스템 플랫폼, 메모리 정렬 및 호출자와 관련이 있습니다. 다음은 FMallocBinned에서 정의한 일부 메모리 관련 변수의 크기입니다.
#if PLATFORM_IOS
#define PLAT_PAGE_SIZE_LIMIT 16384
#define PLAT_BINNED_ALLOC_POOLSIZE 16384
#define PLAT_SMALL_BLOCK_POOL_SIZE 256
#else
#define PLAT_PAGE_SIZE_LIMIT 65536
#define PLAT_BINNED_ALLOC_POOLSIZE 65536
#define PLAT_SMALL_BLOCK_POOL_SIZE 0
#endifIOS 플랫폼에서 메모리 페이지 및 메모리 풀 크기의 상한은 16k이고 박스형 메모리 블록 크기는 256바이트임을 알 수 있습니다. 다른 플랫폼에서는 메모리 페이지 및 메모리 풀 크기의 상한이 64k이고 박스형 메모리 블록 크기는 0바이트입니다.
1.4.3.2 메모리 할당자
FMalloc은 UE의 모든 메모리 할당 및 해제 작업을 제어하는 UE 메모리 할당자의 핵심 클래스입니다. 그러나 이는 FUseSystemMallocForNew 및 FExec에서 상속되는 가상 기본 클래스입니다. 또한 다양한 메모리 할당 체계 및 전략에 해당하는 여러 하위 클래스가 있습니다. FMalloc의 주요 상속 관계는 다음과 같습니다.

위 그림은 FMalloc의 일부 하위 클래스만 보여주며, 다른 디버깅 및 보조 클래스는 이 그림에 없습니다. FMalloc 상속 시스템의 주요 클래스에 대한 분석은 다음과 같습니다.
- FUseSystemMallocForNew
FUseSystemMallocForNew는 new 및 delete 키워드에 대한 연산자 지원을 제공하고 FMalloc은 FUseSystemMallocForNew를 상속합니다. 이는 FMalloc의 모든 하위 클래스가 C++의 new 및 delete 키워드에 대한 메모리 작업을 지원한다는 의미입니다.
- FMallocAnsi
표준 할당자는 메모리 캐시 및 할당 전략 관리 없이 C의 malloc 및 free 작업을 직접 호출합니다.
- FMallocBinned
표준(기존) 박싱 관리 방법은 메모리 풀 테이블(FPoolTable), 페이지 메모리 풀 테이블(FPagePoolTable), 메모리 풀 해시 버킷(PoolHashBucket)을 활성화합니다. UE의 기본 메모리 할당 방식이자, 모든 플랫폼을 지원하는 메모리 할당 방식이기도 합니다. 그 핵심 정의는 다음과 같습니다.
// Engine\Source\Runtime\Core\Public\HAL\MallocBinned.h
class FMallocBinned : public FMalloc
{
private:
enum { POOL_COUNT = 42 };
enum { EXTENDED_PAGE_POOL_ALLOCATION_COUNT = 2 };
enum { MAX_POOLED_ALLOCATION_SIZE = 32768+1 };
(......)
FPoolTable PoolTable[POOL_COUNT]; // 의 메모리 테이블 리스트 , 개 메모리 의 Block해상도/크기 는 의 .
FPoolTable OsTable; // 에 의해 할당(Allocate)의 메모리 의 메모리 테이블 . 을(를) 활용하여 .
FPoolTable PagePoolTable[EXTENDED_PAGE_POOL_ALLOCATION_COUNT]; // 메모리 (메모리 )의 메모리 테이블 .
FPoolTable* MemSizeToPoolTable[MAX_POOLED_ALLOCATION_SIZE+EXTENDED_PAGE_POOL_ALLOCATION_COUNT]; // 에 따라 해상도/크기 의 메모리 테이블 , PoolTable 및 PagePoolTable.
PoolHashBucket* HashBuckets; // 메모리
PoolHashBucket* HashBucketFreeList; // 할당(Allocate)의 메모리
uint32 PageSize; // 메모리 크기
(......)
};후속 메모리 할당 메커니즘을 더 잘 이해하기 위해 먼저 메모리 할당자의 초기화 코드를 분석해 보겠습니다.
// Engine\Source\Runtime\Core\Private\HAL\MallocBinned.cpp
FMallocBinned::FMallocBinned(uint32 InPageSize, uint64 AddressLimit)
{
(......)
// 의 해상도/크기 로 8k(IOS) 또는 32k(IOS플랫폼).
BinnedSizeLimit = Private::PAGE_SIZE_LIMIT/2;
(......)
// 초기화(Initialize)메모리 의 메모리 1, 기본 동작 시, 의 BlockSize로 12k(IOS) 또는 48k(IOS플랫폼).
PagePoolTable[0].FirstPool = nullptr;
PagePoolTable[0].ExhaustedPool = nullptr;
PagePoolTable[0].BlockSize = PageSize == Private::PAGE_SIZE_LIMIT ? BinnedSizeLimit+(BinnedSizeLimit/2) : 0;
// 초기화(Initialize)메모리 의 메모리 2, 기본 동작 시, 의 BlockSize로 24k(IOS) 또는 96k(IOS플랫폼).
PagePoolTable[1].FirstPool = nullptr;
PagePoolTable[1].ExhaustedPool = nullptr;
PagePoolTable[1].BlockSize = PageSize == Private::PAGE_SIZE_LIMIT ? PageSize+BinnedSizeLimit : 0;
// 을(를) 활용하여 생성(Create)BlockSize의 배열 , 개 이면 : 1. 는 메모리 해상도/크기 의 (), 메모리 ; 2. 필수: 16정렬 .
static const uint32 BlockSizes[POOL_COUNT] =
{
8, 16, 32, 48, 64, 80, 96, 112,
128, 160, 192, 224, 256, 288, 320, 384,
448, 512, 576, 640, 704, 768, 896, 1024,
1168, 1360, 1632, 2048, 2336, 2720, 3264, 4096,
4672, 5456, 6544, 8192, 9360, 10912, 13104, 16384,
21840, 32768
};
// 생성(Create)메모리 의 메모리 테이블 , 에 따라 BlockSizes초기화(Initialize)BlockSize
for( uint32 i = 0; i < POOL_COUNT; i++ )
{
PoolTable[i].FirstPool = nullptr;
PoolTable[i].ExhaustedPool = nullptr;
PoolTable[i].BlockSize = BlockSizes[i];
#if STATS
PoolTable[i].MinRequest = PoolTable[i].BlockSize;
#endif
}
// 초기화(Initialize)MemSizeToPoolTable, 크기 의 메모리 테이블 PoolTable.
for( uint32 i=0; i<MAX_POOLED_ALLOCATION_SIZE; i++ )
{
uint32 Index = 0;
while( PoolTable[Index].BlockSize < i )
{
++Index;
}
checkSlow(Index < POOL_COUNT);
MemSizeToPoolTable[i] = &PoolTable[Index];
}
// 메모리 의 메모리 테이블 추가(Add)까지 MemSizeToPoolTable배열 의 .
MemSizeToPoolTable[BinnedSizeLimit] = &PagePoolTable[0];
MemSizeToPoolTable[BinnedSizeLimit+1] = &PagePoolTable[1];
check(MAX_POOLED_ALLOCATION_SIZE - 1 == PoolTable[POOL_COUNT - 1].BlockSize);
}MemSizeToPoolTable, PoolTable, PagePoolTable 간의 관계와 메모리 분포를 보다 명확하고 직관적으로 설명하기 위해 저자는 특별히 다음과 같은 개략도를 그렸습니다.

FMallocBinned가 할당한 메모리의 주요 코드와 분석은 다음과 같습니다.
// Engine\Source\Runtime\Core\Private\HAL\MallocBinned.cpp
void* FMallocBinned::Malloc(SIZE_T Size, uint32 Alignment)
{
(......)
// 처리(Process)메모리 정렬 , 에 따라 메모리 정렬 조정(Adjust)Size
if (Alignment == DEFAULT_ALIGNMENT)
{
// 기본값(Default)의 메모리 정렬 는 16
Alignment = Private::DEFAULT_BINNED_ALLOCATOR_ALIGNMENT;
}
Alignment = FMath::Max<uint32>(Alignment, Private::DEFAULT_BINNED_ALLOCATOR_ALIGNMENT);
SIZE_T SpareBytesCount = FMath::Min<SIZE_T>(Private::DEFAULT_BINNED_ALLOCATOR_ALIGNMENT, Size);
Size = FMath::Max<SIZE_T>(PoolTable[0].BlockSize, Size + (Alignment - SpareBytesCount));
(......)
FFreeMem* Free = nullptr;
bool bUsePools = true; // 기본 활성화메모리
(......)
if (bUsePools)
{
// 만약 할당(Allocate)의 해상도/크기 BinnedSizeLimit(32k), 는 메모리 , MemSizeToPoolTable의 FPoolTable 내에서 .
if( Size < BinnedSizeLimit)
{
// Allocate from pool.
FPoolTable* Table = MemSizeToPoolTable[Size];
#ifdef USE_FINE_GRAIN_LOCKS
FScopeLock TableLock(&Table->CriticalSection);
#endif
checkSlow(Size <= Table->BlockSize);
Private::TrackStats(Table, (uint32)Size);
FPoolInfo* Pool = Table->FirstPool;
if( !Pool )
{
Pool = Private::AllocatePoolMemory(*this, Table, Private::BINNED_ALLOC_POOL_SIZE/*PageSize*/, Size);
}
Free = Private::AllocateBlockFromPool(*this, Table, Pool, Alignment);
}
// 만약 할당(Allocate)의 해상도/크기 BinnedSizeLimit(32k) 및 PagePoolTable[0].BlockSize(48k), 또는 PageSize(64k) 및 PagePoolTable[1].BlockSize(96k), 에 의해 PagePoolTable메모리 테이블 내에서 .
else if ( ((Size >= BinnedSizeLimit && Size <= PagePoolTable[0].BlockSize) ||
(Size > PageSize && Size <= PagePoolTable[1].BlockSize)))
{
// Bucket in a pool of 3*PageSize or 6*PageSize
uint32 BinType = Size < PageSize ? 0 : 1;
uint32 PageCount = 3*BinType + 3;
FPoolTable* Table = &PagePoolTable[BinType];
#ifdef USE_FINE_GRAIN_LOCKS
FScopeLock TableLock(&Table->CriticalSection);
#endif
checkSlow(Size <= Table->BlockSize);
Private::TrackStats(Table, (uint32)Size);
FPoolInfo* Pool = Table->FirstPool;
if( !Pool )
{
Pool = Private::AllocatePoolMemory(*this, Table, PageCount*PageSize, BinnedSizeLimit+BinType);
}
Free = Private::AllocateBlockFromPool(*this, Table, Pool, Alignment);
}
// 메모리 해상도/크기 , 에 의해 할당(Allocate)메모리 , HashBuckets테이블 내에서 .
else
{
// Use OS for large allocations.
UPTRINT AlignedSize = Align(Size,PageSize);
SIZE_T ActualPoolSize; //TODO: use this to reduce waste?
Free = (FFreeMem*)Private::OSAlloc(*this, AlignedSize, ActualPoolSize);
if( !Free )
{
Private::OutOfMemory(AlignedSize);
}
void* AlignedFree = Align(Free, Alignment);
// Create indirect.
FPoolInfo* Pool;
{
#ifdef USE_FINE_GRAIN_LOCKS
FScopeLock PoolInfoLock(&AccessGuard);
#endif
Pool = Private::GetPoolInfo(*this, (UPTRINT)Free);
if ((UPTRINT)Free != ((UPTRINT)AlignedFree & ~((UPTRINT)PageSize - 1)))
{
// Mark the FPoolInfo for AlignedFree to jump back to the FPoolInfo for ptr.
for (UPTRINT i = (UPTRINT)PageSize, Offset = 0; i < AlignedSize; i += PageSize, ++Offset)
{
FPoolInfo* TrailingPool = Private::GetPoolInfo(*this, ((UPTRINT)Free) + i);
check(TrailingPool);
//Set trailing pools to point back to first pool
TrailingPool->SetAllocationSizes(0, 0, Offset, BinnedOSTableIndex);
}
}
}
Free = (FFreeMem*)AlignedFree;
Pool->SetAllocationSizes(Size, AlignedSize, BinnedOSTableIndex, BinnedOSTableIndex);
(......)
}
}
return Free;
}정리하면, 비 IOS 플랫폼과 기본 페이지 크기(64k)의 경우 FMallocBinned의 할당 전략을 간략하게 설명하면 다음과 같다.
할당할 메모리의 크기는 (0, 32k)이며 MemSizeToPoolTable의 PoolTable을 이용하여 할당 및 저장된다.
할당할 메모리의 크기는 [32k, 48K] 또는 [64k, 96k]이며, PagePoolTable의 PoolTable을 이용하여 할당하고 저장한다.
할당할 다른 메모리의 크기는 시스템에서 직접 할당하여 HashBuckets에 배치합니다.
UE가 박싱 대신 시스템에 직접 (48k, 64k) 크기의 메모리를 할당해야 하는 이유는 무엇입니까?
FMallocBinned의 메모리 풀은 균등하게 나누어져 있기 때문에 (48k, 64k) 사이의 메모리를 박싱 방식으로 할당한다면 64k 메모리 풀에 넣어야 하며, 이는 (0, 16k) 사이의 메모리 낭비를 초래하게 됩니다. 즉, 최악의 경우 이 범위의 각 메모리 할당은 16k의 메모리를 낭비하게 되며, 메모리 낭비 비율은 무려 33.33%에 이릅니다. 이는 고성능을 중시하는 UE 공식팀에서는 당연히 허용되지 않는 일이다. 이 전략은 두 가지 악 중 더 적은 것을 선택하고 장단점을 비교하는 데 기반을 두고 있습니다.
물론 여기에는 최적화의 여지가 있습니다. 즉, (48k, 64k) 사이의 메모리를 더 작은 블록으로 조립할 수 있습니다. 예를 들어, BlockSize가 2k인 메모리 풀에 50k의 메모리를 넣어 25개 블록을 차지할 수 있습니다(그러나 이로 인해 메모리 풀과 메모리 풀 테이블의 관리 복잡성도 증가합니다).
아래에서 언급하는 FMallocBinned와 FMallocBinned2, FMallocBinned3는 실제로 큰 메모리를 미리 할당한 후, 그 큰 메모리에 적절한 작은 메모리 블록을 할당합니다. 이러한 방법은 메모리 할당 효율성을 향상시킬 수 있지만 순간적인 IO 압력이 증가하고 메모리 낭비가 불가피하게 발생합니다.
FMallocBinned의 메모리 낭비는 주로 다음 사항에 반영됩니다.
새로 할당된 메모리 풀을 즉시 완전히 활용하지 못하는 경우가 많아 특정 프로그램이 중복되는 경우가 있습니다.
메모리 정렬 및 크기 정렬로 인해 연속적인 크기의 많은 메모리 블록이 동일한 크기의 메모리 풀 테이블에 상향 매핑됩니다(예: 크기가 [9, 16]인 메모리 블록은 BlockSize가 16인 메모리 풀 테이블에 매핑됨). 이 역시 일정 비율의 메모리 낭비로 이어집니다.
할당자의 메모리 풀 테이블, 메모리 풀, 해시 버킷, 메모리 블록 및 기타 정보에 의해 생성된 추가 메모리를 유지합니다.
- FMallocBinned2
새로운 박스형 메모리 할당 방법의 소스 코드 분석을 통해 FMallocBinned2가 FMallocBinned보다 간단하다는 것을 알 수 있습니다. 해당 할당자와 전략은 작은 메모리, 정렬 크기 및 스레드 캐시 활성화 여부(기본적으로 활성화됨)에 따라 선택됩니다.
- FMallocBinned3
64비트 시스템에서만 사용할 수 있는 새로운 박스형 메모리 할당 방법입니다. 구현은 FMallocBinned2와 유사하며 스레드 캐싱을 지원합니다.
- FMallocTBB
FMallocTBB는 타사 메모리 할당자 TBB에서 Scalable_allocator 할당자를 채택합니다. Scalable_allocator가 제공하는 인터페이스는 다음과 같습니다.
// Engine\Source\ThirdParty\IntelTBB\IntelTBB-2019u8\include\tbb\scalable_allocator.h
void * __TBB_EXPORTED_FUNC scalable_malloc (size_t size);
void __TBB_EXPORTED_FUNC scalable_free (void* ptr);
void * __TBB_EXPORTED_FUNC scalable_realloc (void* ptr, size_t size);
void * __TBB_EXPORTED_FUNC scalable_calloc (size_t nobj, size_t size);
int __TBB_EXPORTED_FUNC scalable_posix_memalign (void** memptr, size_t alignment, size_t size);
void * __TBB_EXPORTED_FUNC scalable_aligned_malloc (size_t size, size_t alignment);
void * __TBB_EXPORTED_FUNC scalable_aligned_realloc (void* ptr, size_t size, size_t alignment);
void __TBB_EXPORTED_FUNC scalable_aligned_free (void* ptr);
size_t __TBB_EXPORTED_FUNC scalable_msize (void* ptr);FMallocTBB는 위의 visible_aligned_malloc 인터페이스를 사용하여 메모리 작업을 구현합니다. 할당 코드는 다음과 같습니다.
// Engine\Source\Runtime\Core\Private\HAL\MallocTBB.cpp
void* FMallocTBB::TryMalloc( SIZE_T Size, uint32 Alignment )
{
(......)
void* NewPtr = nullptr;
if( Alignment != DEFAULT_ALIGNMENT )
{
Alignment = FMath::Max(Size >= 16 ? (uint32)16 : (uint32)8, Alignment);
NewPtr = scalable_aligned_malloc( Size, Alignment );
}
else
{
// Fulfill the promise of DEFAULT_ALIGNMENT, which aligns 16-byte or larger structures to 16 bytes,
// while TBB aligns to 8 by default.
NewPtr = scalable_aligned_malloc( Size, Size >= 16 ? (uint32)16 : (uint32)8);
}
(......)
return NewPtr;
}TBB(Threading Building Blocks)는 Intel에서 개발하고 SDK를 제공합니다. 그 특징은 다음과 같습니다:
tbb_allocator, Scalable_allocator, Cache_aligned_allocator의 3가지 할당 방식을 제공합니다.
병렬 알고리즘 및 데이터 구조.
작업 기반 메모리 스케줄러.
멀티스레딩에 친화적이며, 동시에 메모리를 동작시킬 수 있는 멀티 스레드를 지원합니다. Scalable_allocator는 동일한 메모리 풀에 메모리를 할당하지 않으므로 다중 스레드 경쟁으로 인한 소비를 피할 수 있습니다.
캐시 처리 효율이 다른 방법보다 높습니다. 캐시_aligned_allocator는 캐시 정렬을 통해 잘못된 공유 문제를 해결합니다.
기타 메모리 할당자
위에서 일반적으로 사용되는 기본 메모리 할당자 외에도 UE에는 FMallocDebug(메모리 디버깅), FMallocStomp(불법 메모리 작업 디버깅), FMallocJemalloc(멀티스레딩에서 메모리 할당 관리에 적합), GPU 메모리 관련 할당(FMallocBinnedGPU) 등이 함께 제공됩니다. 이러한 메모리 할당 방법은 매우 특별하므로 여기서는 자세히 설명하지 않습니다. 관심 있는 독자는 스스로 소스 코드를 연구할 수 있습니다.
1.4.3.3 메모리 동작 모드
이전 섹션에서는 메모리 할당 방법과 전략 기술에 대해 설명했고, 이어서 메모리 사용 방법에 대해 이야기하겠습니다. 호출자의 경우 메모리를 작동하는 방법은 다음과 같습니다.
- GMalloc: GMalloc은 UE 시작 시작 시 FPlatformMemory를 통해 생성되는 전역 메모리 할당자입니다.
// Engine\Source\Runtime\Core\Private\HAL\UnrealMemory.cpp
static int FMemory_GCreateMalloc_ThreadUnsafe()
{
(......)
GMalloc = FPlatformMemory::BaseAllocator();
(......)
}FPlatformMemory는 다양한 운영 체제의 다양한 유형에 해당합니다. 예를 들어 Windows 시스템에서는 실제로 FWindowsPlatformMemory입니다.
// Engine\Source\Runtime\Core\Public\Windows\WindowsPlatformMemory.h
struct CORE_API FWindowsPlatformMemory : public FGenericPlatformMemory
{
(......)
static class FMalloc* BaseAllocator();
(......)
};
typedef FWindowsPlatformMemory FPlatformMemory;위 코드에서 볼 수 있듯이 GMalloc은 실제로 FMalloc의 인스턴스입니다. 다양한 운영 체제에서 FPlatformMemory는 다양한 FMalloc 하위 클래스를 생성하여 다양한 메모리 할당 전략을 적용하는 데 사용됩니다. FWindowsPlatformMemory::BaseAllocator의 코드를 분석해 보겠습니다.
// Engine\Source\Runtime\Core\Private\Windows\WindowsPlatformMemory.cpp
FMalloc* FWindowsPlatformMemory::BaseAllocator()
{
#if ENABLE_WIN_ALLOC_TRACKING
// This allows tracking of allocations that don't happen within the engine's wrappers.
// This actually won't be compiled unless bDebugBuildsActuallyUseDebugCRT is set in the
// build configuration for UBT.
_CrtSetAllocHook(WindowsAllocHook);
#endif // ENABLE_WIN_ALLOC_TRACKING
// 에 따라 정의 의 메모리 할당(Allocate)
if (FORCE_ANSI_ALLOCATOR) //-V517
{
AllocatorToUse = EMemoryAllocatorToUse::Ansi;
}
else if ((WITH_EDITORONLY_DATA || IS_PROGRAM) && TBB_ALLOCATOR_ALLOWED) //-V517
{
AllocatorToUse = EMemoryAllocatorToUse::TBB;
}
#if PLATFORM_64BITS
else if ((WITH_EDITORONLY_DATA || IS_PROGRAM) && MIMALLOC_ALLOCATOR_ALLOWED) //-V517
{
AllocatorToUse = EMemoryAllocatorToUse::Mimalloc;
}
else if (USE_MALLOC_BINNED3)
{
AllocatorToUse = EMemoryAllocatorToUse::Binned3;
}
#endif
else if (USE_MALLOC_BINNED2)
{
AllocatorToUse = EMemoryAllocatorToUse::Binned2;
}
else
{
AllocatorToUse = EMemoryAllocatorToUse::Binned;
}
#if !UE_BUILD_SHIPPING
// If not shipping, allow overriding with command line options, this happens very early so we need to use windows functions
const TCHAR* CommandLine = ::GetCommandLineW();
// 에 따라 조정(Adjust)메모리 할당(Allocate)
if (FCString::Stristr(CommandLine, TEXT("-ansimalloc")))
{
AllocatorToUse = EMemoryAllocatorToUse::Ansi;
}
#if TBB_ALLOCATOR_ALLOWED
else if (FCString::Stristr(CommandLine, TEXT("-tbbmalloc")))
{
AllocatorToUse = EMemoryAllocatorToUse::TBB;
}
#endif
#if MIMALLOC_ALLOCATOR_ALLOWED
else if (FCString::Stristr(CommandLine, TEXT("-mimalloc")))
{
AllocatorToUse = EMemoryAllocatorToUse::Mimalloc;
}
#endif
#if PLATFORM_64BITS
else if (FCString::Stristr(CommandLine, TEXT("-binnedmalloc3")))
{
AllocatorToUse = EMemoryAllocatorToUse::Binned3;
}
#endif
else if (FCString::Stristr(CommandLine, TEXT("-binnedmalloc2")))
{
AllocatorToUse = EMemoryAllocatorToUse::Binned2;
}
else if (FCString::Stristr(CommandLine, TEXT("-binnedmalloc")))
{
AllocatorToUse = EMemoryAllocatorToUse::Binned;
}
#if WITH_MALLOC_STOMP
else if (FCString::Stristr(CommandLine, TEXT("-stompmalloc")))
{
AllocatorToUse = EMemoryAllocatorToUse::Stomp;
}
#endif // WITH_MALLOC_STOMP
#endif // !UE_BUILD_SHIPPING
// 에 따라 의 타입/유형 생성(Create)FMalloc의 객체/오브젝트 。
switch (AllocatorToUse)
{
case EMemoryAllocatorToUse::Ansi:
return new FMallocAnsi();
#if WITH_MALLOC_STOMP
case EMemoryAllocatorToUse::Stomp:
return new FMallocStomp();
#endif
#if TBB_ALLOCATOR_ALLOWED
case EMemoryAllocatorToUse::TBB:
return new FMallocTBB();
#endif
#if MIMALLOC_ALLOCATOR_ALLOWED && PLATFORM_SUPPORTS_MIMALLOC
case EMemoryAllocatorToUse::Mimalloc:
return new FMallocMimalloc();
#endif
case EMemoryAllocatorToUse::Binned2:
return new FMallocBinned2();
#if PLATFORM_64BITS
case EMemoryAllocatorToUse::Binned3:
return new FMallocBinned3();
#endif
default: // intentional fall-through
case EMemoryAllocatorToUse::Binned:
return new FMallocBinned((uint32)(GetConstants().BinnedPageSize&MAX_uint32), (uint64)MAX_uint32 + 1);
}
}GMalloc은 FMalloc의 서브클래스를 통해 메모리를 동작시키는 것을 볼 수 있다. 다음 표에는 다양한 운영 체제의 지원 및 기본 메모리 할당 방법이 나와 있습니다.
운영 체제 지원되는 메모리 할당 방법 기본 메모리 할당 방법
창 Ansi, Binned, Binned2, Binned3, TBB, 스톰프, Mimalloc 비닝됨
안드로이드 비닝, 비닝2, 비닝3 비닝됨
애플(IOS, 맥) 안시, Binned, Binned2, Binned3 비닝됨
유닉스 Ansi, Binned, Binned2, Binned3, 스톰프, Jemalloc 비닝됨
홀로렌즈 안시, 빈드, TBB 비닝됨
- FMemory: FMemory는 UE의 정적 도구 클래스입니다. 이는 메모리 작동을 위한 다양한 정적 메서드를 제공합니다. 일반적인 API는 다음과 같습니다.
// Engine\Source\Runtime\Core\Public\HAL\UnrealMemory.h
struct CORE_API FMemory
{
// 호출(Call)c의 메모리 할당(Allocate) 및 해제(Release)인터페이스 .
static void* SystemMalloc(SIZE_T Size);
static void SystemFree(void* Ptr);
// GMalloc객체/오브젝트 메모리
static void* Malloc(SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT);
static void* Realloc(void* Original, SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT);
static void Free(void* Original);
static void* MallocZeroed(SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT);
// 메모리 인터페이스
static void* Memmove( void* Dest, const void* Src, SIZE_T Count );
static int32 Memcmp( const void* Buf1, const void* Buf2, SIZE_T Count );
static void* Memset(void* Dest, uint8 Char, SIZE_T Count);
static void* Memzero(void* Dest, SIZE_T Count);
static void* Memcpy(void* Dest, const void* Src, SIZE_T Count);
static void* BigBlockMemcpy(void* Dest, const void* Src, SIZE_T Count);
static void* StreamingMemcpy(void* Dest, const void* Src, SIZE_T Count);
static void Memswap( void* Ptr1, void* Ptr2, SIZE_T Size );
(......)
};위 코드에서 볼 수 있듯이 FMemory는 GMalloc 및 C 스타일 메모리 작업을 모두 지원합니다.
- new/delete 연산자: new 및 delete 연산자를 오버로드하는 일부 클래스를 제외하고 다른 전역 new 및 delete 연산자는 다음 선언을 사용합니다.
// Engine\Source\Runtime\Core\Public\Modules\Boilerplate\ModuleBoilerplate.h
#define REPLACEMENT_OPERATOR_NEW_AND_DELETE \
OPERATOR_NEW_MSVC_PRAGMA void* operator new ( size_t Size ) OPERATOR_NEW_THROW_SPEC { return FMemory::Malloc( Size ); } \
OPERATOR_NEW_MSVC_PRAGMA void* operator new[]( size_t Size ) OPERATOR_NEW_THROW_SPEC { return FMemory::Malloc( Size ); } \
OPERATOR_NEW_MSVC_PRAGMA void* operator new ( size_t Size, const std::nothrow_t& ) OPERATOR_NEW_NOTHROW_SPEC { return FMemory::Malloc( Size ); } \
OPERATOR_NEW_MSVC_PRAGMA void* operator new[]( size_t Size, const std::nothrow_t& ) OPERATOR_NEW_NOTHROW_SPEC { return FMemory::Malloc( Size ); } \
void operator delete ( void* Ptr ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete[]( void* Ptr ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete ( void* Ptr, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete[]( void* Ptr, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete ( void* Ptr, size_t Size ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete[]( void* Ptr, size_t Size ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete ( void* Ptr, size_t Size, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } \
void operator delete[]( void* Ptr, size_t Size, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); }전역 메모리 연산자도 FMemory를 호출하여 메모리 작업을 완료한다는 것을 소스 코드에서 볼 수 있습니다.
- 특정 API: 위의 세 가지 메모리 작동 방법 외에도 UE는 특정 메모리를 생성하고 파괴하기 위한 다양한 인터페이스를 제공합니다. 일반적으로 다음과 같이 쌍으로 나타납니다.
struct FPooledVirtualMemoryAllocator
{
void* Allocate(SIZE_T Size);
void Free(void* Ptr, SIZE_T Size);
};
class CORE_API FAnsiAllocator
{
class CORE_API ForAnyElementType
{
void ResizeAllocation(SizeType PreviousNumElements, SizeType NumElements, SIZE_T NumBytesPerElement);
};
};
class FVirtualAllocator
{
void* AllocateVirtualPages(uint32 NumPages, size_t AlignmentForCheck);
void FreeVirtual(void* Ptr, uint32 NumPages);
};
class RENDERER_API FVirtualTextureAllocator
{
uint32 Alloc(FAllocatedVirtualTexture* VT );
void Free(FAllocatedVirtualTexture* VT );
};
template<SIZE_T RequiredAlignment> class TMemoryPool
{
void* Allocate(SIZE_T Size);
void Free(void *Ptr, SIZE_T Size);
};호출자의 관점에서 볼 때 대부분의 경우 new/delete 연산자와 FMemory 메서드를 사용하여 메모리를 작동하며 시스템 메모리를 직접 적용하는 경우는 거의 없습니다.
1.4.4 가비지 컬렉션
Garbage Collection의 약자는 GC(Garbage Collection)입니다. 특정 전략으로 유효하지 않은 자원을 재활용하거나 재사용하는 메커니즘입니다. 게임 엔진, 가상 머신, 운영 체제 등에 일반적으로 사용됩니다.
1.4.4.1 GC 알고리즘 목록
"가비지 수집 알고리즘 및 구현"이라는 책에서 언급된 GC 알고리즘은 다음과 같습니다.
- **마크-스윕.**즉, 마크 정리 알고리즘은 알고리즘이 두 단계로 구분됩니다.
첫 번째 단계는 마크 단계입니다. 이 프로세스는 루트의 활성 개체 목록을 순회하고 모든 활성 개체가 가리키는 힙 개체를 TRUE로 표시하는 것입니다.

두 번째 단계는 스윕(Sweep) 단계입니다. 이 프로세스는 힙 목록을 순회하고 FALSE로 표시된 모든 개체를 할당 가능한 힙에 해제하고 다음에 표시 동작이 실행될 수 있도록 활성 개체의 표시를 재설정하는 것입니다.

- 비밥. 전체 이름은 Big Bag Of Pages입니다. 그 접근 방식은 관리를 위해 비슷한 크기의 객체를 고정 크기 블록으로 구성하는 것인데, 이는 UE의 FMallocBinned 할당자의 전략과 정확히 동일합니다.

**보수적 GC.**보수적 GC는 포인터와 비포인터를 식별할 수 없다는 특징이 있습니다. GC 수준에서는 변수가 메모리 값을 기준으로 포인터인지 여부를 판단하는 것이 불가능하기 때문에 이를 확인하는 방법이 많아지고 특정 비용이 필요합니다. 그 반대가 바로 Exact GC인데, 태그를 통해 포인터인지를 명확하게 식별할 수 있습니다.
세대 GC. 세대별 가비지 수집 방식은 객체에 연령 개념을 도입하고, 쓰레기가 되기 쉬운 객체를 우선적으로 재활용하여 가비지 수집의 효율성을 향상시킵니다.
증분 GC. 증분 가비지 수집은 가비지 수집을 점진적으로 진행하여 mutator의 최대 일시 중지 시간을 제어하는 방법입니다.

증분 가비지 수집 다이어그램.
- 참조 카운팅 이미믹스. RC Immix 알고리즘의 약어는 병합 참조 GC 알고리즘입니다. 목적은 GC 처리량 향상이라는 목적을 달성하기 위해 몇 가지 전략을 통해 참조 카운팅 동작을 개선하는 것입니다.
UE의 GC 알고리즘은 주로 Mark-Sweep(mark-clean 알고리즘)을 기반으로 하며 UObject 객체를 정리하는 데 사용됩니다. Mark-Sweep 알고리즘과 마찬가지로 UE에도 Root라는 개념이 있습니다. 객체(속성 및 정적 변수 포함)가 GC에 의해 정리되는 것을 방지하려면 UObject의 AddToRoot 인터페이스를 사용하면 됩니다.
1.4.4.2 UE의 GC
UE의 GC 모듈의 주요 구현 코드와 분석은 다음과 같습니다.
// Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp
void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
// GC, GC스레드(Thread)
AcquireGCLock();
// 실행(Execute)GC
CollectGarbageInternal(KeepFlags, bPerformFullPurge);
// 해제(Release)GC, 로써 스레드(Thread)
ReleaseGCLock();
}
// 실행(Execute)GC。KeepFlags:의 UObject플래그 ,bPerformFullPurge:는 비활성화(Disable)갱신(Update)
void CollectGarbageInternal(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
(......)
{
FGCScopeLock GCLock;
// 회 의 , 또는 회 , 호출(Call)GC의 .
if (GObjIncrementalPurgeIsInProgress || GObjPurgeIsRequired)
{
IncrementalPurgeGarbage(false);
FMemory::Trim();
}
// This can happen if someone disables clusters from the console (gc.CreateGCClusters)
if (!GCreateGCClusters && GUObjectClusters.GetNumAllocatedClusters())
{
GUObjectClusters.DissolveClusters(true);
}
(......)
// Fall back to single threaded GC if processor count is 1 or parallel GC is disabled
// or detailed per class gc stats are enabled (not thread safe)
// Temporarily forcing single-threaded GC in the editor until Modify() can be safely removed from HandleObjectReference.
const bool bForceSingleThreadedGC = ShouldForceSingleThreadedGC();
// Run with GC clustering code enabled only if clustering is enabled and there's actual allocated clusters
const bool bWithClusters = !!GCreateGCClusters && GUObjectClusters.GetNumAllocatedClusters();
{
const double StartTime = FPlatformTime::Seconds();
FRealtimeGC TagUsedRealtimeGC;
// 실행(Execute)(즉, 플래그 )
TagUsedRealtimeGC.PerformReachabilityAnalysis(KeepFlags, bForceSingleThreadedGC, bWithClusters);
UE_LOG(LogGarbage, Log, TEXT("%f ms for GC"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
// Reconstruct clusters if needed
if (GUObjectClusters.ClustersNeedDissolving())
{
const double StartTime = FPlatformTime::Seconds();
GUObjectClusters.DissolveClusters();
UE_LOG(LogGarbage, Log, TEXT("%f ms for dissolving GC clusters"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
// Fire post-reachability analysis hooks
FCoreUObjectDelegates::PostReachabilityAnalysis.Broadcast();
{
FGCArrayPool::Get().ClearWeakReferences(bPerformFullPurge);
// 의
GatherUnreachableObjects(bForceSingleThreadedGC);
if (bPerformFullPurge || !GIncrementalBeginDestroyEnabled)
{
// 로부터 테이블 내에서
UnhashUnreachableObjects(/**bUseTimeLimit = */ false);
FScopedCBDProfile::DumpProfile();
}
}
// Set flag to indicate that we are relying on a purge to be performed.
GObjPurgeIsRequired = true;
//
if (bPerformFullPurge || GIsEditor)
{
IncrementalPurgeGarbage(false);
}
// UObject테이블
if (bPerformFullPurge)
{
ShrinkUObjectHashTables();
}
// Destroy all pending delete linkers
DeleteLoaders();
// 해제(Release)메모리 .
FMemory::Trim();
}
// Route callbacks to verify GC assumptions
FCoreUObjectDelegates::GetPostGarbageCollect().Broadcast();
STAT_ADD_CUSTOMMESSAGE_NAME( STAT_NamedMarker, TEXT( "GarbageCollection - End" ) );
}마킹 단계는 FRealtimeGC::PerformReachabilityAnalytic 인터페이스에 의해 완료됩니다.
// Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp
class FRealtimeGC : public FGarbageCollectionTracer
{
void PerformReachabilityAnalysis(EObjectFlags KeepFlags, bool bForceSingleThreaded, bool bWithClusters)
{
(......)
/** Growing array of objects that require serialization */
FGCArrayStruct* ArrayStruct = FGCArrayPool::Get().GetArrayStructFromPool();
TArray<UObject*>& ObjectsToSerialize = ArrayStruct->ObjectsToSerialize;
// 리셋/초기화(Reset)개수 .
GObjectCountDuringLastMarkPhase.Reset();
// Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
{
ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
}
{
const double StartTime = FPlatformTime::Seconds();
// 을(를) 활용하여 플래그 의 함수 .
(this->*MarkObjectsFunctions[GetGCFunctionIndex(!bForceSingleThreaded, bWithClusters)])(ObjectsToSerialize, KeepFlags);
UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Mark Phase (%d Objects To Serialize"), (FPlatformTime::Seconds() - StartTime) * 1000, ObjectsToSerialize.Num());
}
{
const double StartTime = FPlatformTime::Seconds();
// 실행(Execute)의 .
PerformReachabilityAnalysisOnObjects(ArrayStruct, bForceSingleThreaded, bWithClusters);
UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Reachability Analysis"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
// Allowing external systems to add object roots. This can't be done through AddReferencedObjects
// because it may require tracing objects (via FGarbageCollectionTracer) multiple times
FCoreUObjectDelegates::TraceExternalRootsForReachabilityAnalysis.Broadcast(*this, KeepFlags, bForceSingleThreaded);
FGCArrayPool::Get().ReturnToPool(ArrayStruct);
#if UE_BUILD_DEBUG
FGCArrayPool::Get().CheckLeaks();
#endif
}
};위에서 언급한 MarkObjectsFunctions 및 PerformReachabilityAnalyticOnObjects는 실제로 병렬(Parallel) 및 클러스터(Cluster) 처리를 지원하는 결합된 템플릿 함수입니다.
// Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp
class FRealtimeGC : public FGarbageCollectionTracer
{
// 선언
MarkObjectsFn MarkObjectsFunctions[4];
ReachabilityAnalysisFn ReachabilityAnalysisFunctions[4];
// 초기화(Initialize)
FRealtimeGC()
{
MarkObjectsFunctions[GetGCFunctionIndex(false, false)] = &FRealtimeGC::MarkObjectsAsUnreachable<false, false>;
MarkObjectsFunctions[GetGCFunctionIndex(true, false)] = &FRealtimeGC::MarkObjectsAsUnreachable<true, false>;
MarkObjectsFunctions[GetGCFunctionIndex(false, true)] = &FRealtimeGC::MarkObjectsAsUnreachable<false, true>;
MarkObjectsFunctions[GetGCFunctionIndex(true, true)] = &FRealtimeGC::MarkObjectsAsUnreachable<true, true>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(false, false)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<false, false>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(true, false)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<true, false>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(false, true)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<false, true>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(true, true)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<true, true>;
}
};UE의 GC는 다음과 같은 특징을 가지고 있음을 소스 코드에서 볼 수 있습니다.
- 주요 알고리즘은 Mark-Sweep입니다. 그러나 두 단계만 있는 기존 Mark-Sweep 알고리즘과 달리 UE의 GC는 세 단계로 구성됩니다.
도달 가능한 객체를 색인화합니다.
청소할 물건을 모으십시오.
2단계에서 수집한 개체를 정리합니다.
게임 스레드에서 UObject를 정리합니다.
스레드로부터 안전하며 멀티스레드 병렬(Parallel) 및 클러스터(Cluster) 처리를 지원하여 처리량을 향상시킵니다.
편집기 모드에서 강제로 수행되는 전체 정리를 지원합니다. 또한 GC 처리 스레드가 너무 오랫동안 정체되는 것을 방지하기 위해 증분 정리를 지원합니다.
표시된 특정 개체를 정리하지 않도록 지정할 수 있습니다.
실제로 UE의 GC 메커니즘과 원리는 위에서 설명한 것보다 훨씬 더 복잡합니다. 다만, 지면과 주제의 제약으로 인해 자세히 소개하지는 않겠습니다. 관심 있는 분은 UE 소스 코드를 연구하거나 참고 자료를 찾아보세요.
1.4.5 메모리 장벽
메모리 배리어는 멤바, 메모리 펜스 또는 펜스 명령이라고도 합니다. 이는 순서가 잘못된 메모리 액세스 문제와 CPU 버퍼 데이터의 비동기화 문제를 해결하는 것으로 보입니다.
컴파일이나 런타임 중에 메모리 순서가 잘못된 문제가 발생할 수 있습니다. 컴파일 시 순서가 잘못된 문제는 명령어 순서가 변경되는 컴파일러 최적화로 인해 발생합니다. 런타임 비순차적 문제는 다중 처리 및 다중 스레드에 의한 비순차적 액세스로 인해 발생하는 경우가 많습니다.
1.4.5.1 컴파일 타임 메모리 장벽
예를 들어 컴파일 타임 메모리 재정렬의 경우 다음 C++ 코드를 가정합니다.
sum = a + b + c;
print(sum);컴파일러에 의해 컴파일된 후 어셈블리 명령 시퀀스는 다음 세 가지 중 하나가 될 수 있습니다.
// 1
sum = a + b;
sum = sum + c;
// 2
sum = b + c;
sum = a + sum;
// 3
sum = a + c;
sum = sum + b;위의 상황은 결과에 영향을 미치지 않는 것처럼 보이지만 다음 코드의 경우 다른 결과가 생성됩니다.
sum = a + b + sum;
print(sum);컴파일된 품질은 다음과 같습니다.
// 1
sum = a + b;
sum = sum + sum;
// 2
sum = b + sum;
sum = a + sum;
// 3
sum = a + sum;
sum = sum + b;분명히, 조립 지침을 컴파일한 후에는 세 가지 경우에서 서로 다른 결과를 얻을 수 있습니다! !
컴파일 중에 순서가 잘못된 문제를 방지하려면 다음과 같이 명령어 사이에 메모리 장벽을 명시적으로 추가해야 합니다.
sum = a + b;
__COMPILE_MEMORY_BARRIER__;
sum = sum + c;위의 __COMPILE_MEMORY_BARRIER__은 컴파일러마다 다르게 구현됩니다. 일부 컴파일러 구현은 다음과 같습니다.
// C11 / C++11
atomic_signal_fence(memory_order_acq_rel);
// Microsoft Visual C++
_ReadWriteBarrier();
// GCC
__sync_synchronize();
// GNU
asm volatile("" ::: "memory");
__asm__ __volatile__ ("" ::: "memory");
// Intel ICC
__memory_barrier();또한 다양한 유형의 장벽을 다른 작업(예: 로드, 저장, 원자 증가, 원자 비교 및 스왑)에 결합하는 결합 장벽이 있으므로 이전 또는 이후에 추가 메모리 장벽을 추가할 필요가 없습니다. 조합 장벽이 CPU 아키텍처와 관련되어 있다는 점은 언급할 가치가 있습니다. 이는 다양한 CPU 아키텍처에 대한 다양한 명령어로 컴파일되며 하드웨어 메모리 주문 보장에 의존합니다.
1.4.5.2 런타임 메모리 장벽
위에서는 컴파일 타임의 메모리 장애 문제를 설명하고, 런타임 시의 메모리 장애 문제는 아래에서 설명하겠습니다.
초기 프로세서는 순차 프로세서였습니다. 이러한 종류의 프로세서에 컴파일 시간 순서 문제가 없다면 처리 순서가 프로그래머가 작성한 코드 순서와 일치하는지 확인할 수 있습니다.
최신 멀티코어 프로세서 시대에는 비순차적인 프로세서가 많이 있습니다. 프로세서가 실제로 명령을 실행하는 순서는 프로그래머가 작성한 순서가 아니라 사용 가능한 입력 데이터에 따라 결정됩니다. 이전에 요청한 모든 명령어의 실행 결과가 레지스터 파일에 기록된 후에만 명령어 실행 결과가 레지스터 파일에 기록됩니다(실행 결과가 순서대로 실행되도록 재정렬됨).
비순차적 다중 프로세서 아키텍처에서 런타임 메모리 장벽 메커니즘이 없으면 예상치 못한 실행 결과가 많이 발생합니다. 여기에 구체적인 예가 있습니다.
메모리 변수 x와 f가 있고 그 값이 0으로 초기화되어 프로세서 #1과 프로세서 #2가 모두 이에 액세스할 수 있으며 프로세서의 실행 명령은 다음과 같다고 가정합니다.
프로세서 #1:
while (f == 0);
print(x);프로세서 #2:
x = 42;
f = 1;이러한 상황 중 하나는 프로세서 #1이 x 값 42를 출력할 것으로 예상하는 것입니다. 그러나 이는 사실이 아닙니다. 프로세서 #2가 순서대로 실행되지 않을 수 있으므로 f = 1은 x = 42 이전에 실행될 수 있으며 프로세서 #1의 출력 값은 42가 아닌 0입니다. 마찬가지로 프로세서 #1은 x 값을 먼저 출력한 다음 while 문을 실행하여 예상치 못한 결과를 얻을 수도 있습니다. 잘못된 실행으로 인한 예기치 않은 결과를 방지하기 위해 두 프로세서 명령어 사이에 런타임 메모리 장벽을 추가할 수 있습니다.
프로세서 #1:
while (f == 0);
_RUNTIME_MEMORY_BARRIAR_; // 추가(Add)메모리 배리어 , f의 로드/읽기(Read)까지 처리(Process)의 , 실행(Execute)print(x)
print(x);프로세서 #2:
x = 42;
_RUNTIME_MEMORY_BARRIAR_; // 추가(Add)메모리 배리어 , x처리(Process), 실행(Execute)f=1
f = 1;위의 _RUNTIME_MEMORY_BARRIAR_는 런타임 메모리 장벽을 나타냅니다. 실제로는 서로 다른 하드웨어 아키텍처에서 서로 다른 구현을 갖고 있으며 이에 대해서는 나중에 자세히 설명하겠습니다.
하드웨어 수준에는 L1, L2, L3 및 기타 수준의 캐시, 저장소 버퍼, 멀티 코어 및 멀티 스레딩이 있습니다. 메모리를 순서대로 유지하기 위해 많은 상태(예: MESI)와 메시지 전달(MESI 메시지)이 정의됩니다. 이들 사이에는 10개 이상의 결합된 대화형 상태가 있으며 이는 CPU 하드웨어 아키텍처와 관련되어 있습니다. 분명히 프로그래머가 이러한 상태에 직접 노출되어 조작한다면 재앙이 될 것입니다.
MESI 프로토콜은 무효화 기반 캐시 일관성 프로토콜이며 다시 쓰기 캐싱을 지원하는 데 가장 일반적으로 사용되는 프로토콜입니다. 멀티 코어 CPU에서 캐시와 메인 메모리의 동기화에 일반적으로 사용됩니다.
MESI 프로토콜의 기본 상태: M수정됨, 배타적, Shared, Invalid.
MESI 프로토콜 메시지: 읽기, 읽기 응답, 무효화, 승인 무효화, 읽기 무효화, 쓰기 저장.
MESI 프로토콜의 기본 상태 전환은 다음과 같습니다.

각 기본 상태는 서로 다른 의미에 해당하지만 여기서는 자세히 설명하지 않겠습니다.
MESI와 유사한 프로토콜에는 Coherence 프로토콜, MSI 프로토콜, MOSI 프로토콜, MOESI 프로토콜, MESIF 프로토콜, MERSI 프로토콜 등이 포함됩니다.
MESI에 대한 자세한 내용은 다음을 참조하세요.
MESI 프로토콜
메모리 장벽: 소프트웨어 해커를 위한 하드웨어 관점
따라서 Doug Lea와 같은 똑똑한 사람들은 이러한 상태 및 메시지 전달 메커니즘을 단순화하고 특정 유형의 메모리 순서를 방지하기 위해 명명된 일반적으로 사용되는 4가지 조합 장벽으로 결합했습니다. CPU마다 특정 지침이 있습니다. 이 네 가지 명령어는 비록 완전히 일치하지는 않지만 실제 CPU의 명령어와 더 잘 일치할 수 있습니다. 대부분의 경우 실제 CPU 명령어는 특정 효과를 달성하기 위해 여러 유형을 조합한 것입니다.
Read Barrier(Load Barrier) 및 **Write Barrier(Store Barrier)**가 탄생했습니다. 명령이 실행되기 전에 로드 장벽을 삽입하면 캐시의 데이터가 무효화되고 데이터가 주 메모리에서 강제로 다시 로드될 수 있습니다. 명령 뒤에 저장 장벽이 삽입되면 캐시의 최신 데이터가 주 메모리에 기록되어 다른 프로세서 스레드에서 볼 수 있습니다.
Load Barrier와 Store Barrier를 배열하고 결합한 후 4가지 유형의 명령어를 구성할 수 있습니다.
- LoadLoad: 재정렬로 인해 발생하는 장벽 전후 읽기 작업의 순서가 뒤바뀌는 문제를 방지할 수 있습니다.

LoadLoad 배리어를 추가한 후 CPU가 순서대로 액세스하지 않더라도 LoadLoad 배리어 전후로 점프하지 않습니다.
적용 예:
if (IsValid) // 로드 검사/감지(Detect)IsValid
{
LOADLOAD_FENCE(); // LoadLoad배리어 개 로드 의 정렬 ,로드 Value 및 로드/읽기(Read)로드/읽기(Read)의 데이터 ,IsValid 및 로드/읽기(Read)의 데이터 로드/읽기(Read)。
return Value; // 로드 Value
}- StoreStore: 배리어 전후의 쓰기 작업 장애로 인한 재정렬을 방지할 수 있습니다.

적용 예:
Value = x; // 기록/쓰기(Write)Value
STORESTORE_FENCE(); // StoreStore배리어 개 기록/쓰기(Write)의 정렬 ,IsValid 및 기록/쓰기(Write)실행(Execute),Value의 기록/쓰기(Write)처리(Process)。
IsValid = 1; // 기록/쓰기(Write)IsValid- LoadStore: 장벽 이전의 로드 작업과 장벽 이후의 스토리지 작업 재정렬을 방지할 수 있습니다. 적용 예:
if (IsValid) // 로드 검사/감지(Detect)IsValid
{
LOADSTORE_FENCE(); // LoadStore배리어 로드 및 기록/쓰기(Write)의 정렬 ,Value 및 기록/쓰기(Write),IsValid로드/읽기(Read)의 데이터 로드/읽기(Read)。
Value = x; // 기록/쓰기(Write)Value
}- StoreLoad: 장벽 이전의 쓰기 작업과 장벽 이후 로드 작업의 재정렬을 방지할 수 있습니다. 대부분의 CPU 아키텍처에서 이는 다른 세 가지 메모리 배리어의 기능을 갖는 범용 배리어이지만 가장 비용이 많이 들기도 합니다. 적용 예:
Value = x; // 기록/쓰기(Write)Value
STORELOAD_FENCE(); // IsValid 및 로드/읽기(Read)실행(Execute),Value의 기록/쓰기(Write)처리(Process)。
if (IsValid) // 로드 검사/감지(Detect)IsValid
{
return 1;
}SMP(Symmetric Multiprocessing) 마이크로 아키텍처에서는 메모리 액세스 일관성 모델 분류에 따라 다음과 같이 나눌 수 있습니다.
순차적 일관성: 순차적 일관성, 모든 읽기 및 쓰기 작업이 순차적입니다.
완화된 일관성: 느슨한 일관성(또는 부분 일관성으로 이해됨), 로드 후 로드, 저장 후 저장, 로드 후 저장 및 저장 후 로드로 인해 재정렬이 발생합니다.
약한 일관성: 약한 일관성, 명시적인 메모리 장벽이 없는 한 모든 읽기 및 쓰기 작업으로 인해 재정렬이 발생할 수 있습니다.
다음 그림은 여러 상태의 일부 공통 CPU 아키텍처 쌍을 재정렬한 표입니다.

런타임 메모리 장벽은 다양한 하드웨어 아키텍처에서 서로 다르게 구현됩니다. 일반적인 아키텍처의 구현은 다음과 같습니다.
// x86, x86-64
lfence (asm), void _mm_lfence(void) // 배리어
sfence (asm), void _mm_sfence(void) // 배리어
mfence (asm), void _mm_mfence(void) // 배리어
// ARMv7
dmb (asm) // Data Memory Barrier, 데이터 메모리 배리어
dsb (asm) // Data Synchronization Barrier, 데이터 동기화 배리어
isb (asm) // Instruction Synchronization Barrier, 동기화 배리어
// POWER
dcs (asm)
// PowerPC
sync (asm)
// MIPS
sync (asm)
// Itanium
mf (asm)메모리 장벽은 광범위한 주제입니다. 공간 및 주제 제한으로 인해 해당 기술과 메커니즘을 완전히 설명하는 것은 불가능하지만 다음과 같은 몇 가지 확장된 기사를 권장할 수 있습니다.
메모리 순서
메모리 장벽은 소스 제어 작업과 같습니다.
하드웨어 수준에서 메모리 장벽 이해
1.4.5.3 UE 메모리 장벽
UE의 메모리 배리어는 FGenericPlatformMisc 및 그 서브클래스에 캡슐화되어 있습니다. 일반적인 운영 체제의 구현은 다음과 같습니다.
struct FGenericPlatformMisc
{
(......)
/**
* Enforces strict memory load/store ordering across the memory barrier call.
*/
static void MemoryBarrier();
(......)
};
// Windows
struct FWindowsPlatformMisc : public FGenericPlatformMisc
{
(......)
static void MemoryBarrier()
{
_mm_sfence();
}
(......)
};
#if WINDOWS_USE_FEATURE_PLATFORMMISC_CLASS
typedef FWindowsPlatformMisc FPlatformMisc;
#endif
// Android
struct FAndroidMisc : public FGenericPlatformMisc
{
(......)
static void MemoryBarrier()
{
__sync_synchronize();
}
(......)
};
#if !PLATFORM_LUMIN
typedef FAndroidMisc FPlatformMisc;
#endif
// Apple
struct FApplePlatformMisc : public FGenericPlatformMisc
{
(......)
static void MemoryBarrier()
{
__sync_synchronize();
}
(......)
};
// Linux
struct FLinuxPlatformMisc : public FGenericPlatformMisc
{
(......)
static void MemoryBarrier()
{
__sync_synchronize();
}
(......)
};
#if !PLATFORM_LUMIN
typedef FLinuxPlatformMisc FPlatformMisc;
#endifx86 아키텍처 명령어를 사용하는 Windows를 제외하고 다른 시스템에서는 GCC의 메모리 장벽 명령어를 사용합니다. 이상한 점은 Windows에는 런타임 메모리 장벽이 있는 반면 다른 플랫폼에는 컴파일 타임 메모리 장벽이 있는 것 같습니다. 저자는 처음에 이에 대해 혼란스러워했지만 참조 메모리 순서에서 답을 찾았습니다.
하드웨어 메모리 배리어에 대한 컴파일러 지원
일부 컴파일러는 하드웨어 메모리 장벽 명령을 내보내는 내장 기능을 지원합니다.
GCC 버전 4.4.0 이상에는 __sync_synchronize가 있습니다.
C11 및 C++11부터omic_thread_fence() 명령이 추가되었습니다.
Microsoft Visual C++ 컴파일러에는 MemoryBarrier()가 있습니다.
Sun Studio Compiler Suite에는 __machine_r_barrier, __machine_w_barrier 및 __machine_rw_barrier가 있습니다.
즉, 일부 컴파일러의 컴파일 타임 메모리 장벽은 GCC 컴파일러의 __sync_synchronize를 포함한 하드웨어(런타임) 메모리 장벽도 트리거합니다.
UE의 시스템 플랫폼 다형성 캡슐화를 통해 호출자는 그것이 어떤 시스템인지 주의를 기울일 필요가 없습니다. 크로스 플랫폼 런타임 메모리 장벽은 아무 생각 없이 FPlatformMisc::MemoryBarrier()를 호출하여 코드에 추가할 수 있습니다. 샘플 코드는 다음과 같습니다.
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
void RenderingThreadMain( FEvent* TaskGraphBoundSyncEvent )
{
LLM_SCOPE(ELLMTag::RenderingThreadMemory);
ENamedThreads::Type RenderThread = ENamedThreads::Type(ENamedThreads::ActualRenderingThread);
ENamedThreads::SetRenderThread(RenderThread);
ENamedThreads::SetRenderThread_Local(ENamedThreads::Type(ENamedThreads::ActualRenderingThread_Local));
FTaskGraphInterface::Get().AttachToThread(RenderThread);
// 추가(Add)메모리 배리어
FPlatformMisc::MemoryBarrier();
// Inform main thread that the render thread has been attached to the taskgraph and is ready to receive tasks
if( TaskGraphBoundSyncEvent != NULL )
{
TaskGraphBoundSyncEvent->Trigger();
}
// set the thread back to real time mode
FPlatformProcess::SetRealTimeMode();
#if STATS
if (FThreadStats::WillEverCollectData())
{
FThreadStats::ExplicitFlush(); // flush the stats and set update the scope so we don't flush again until a frame update, this helps prevent fragmentation
}
#endif
FCoreDelegates::PostRenderingThreadCreated.Broadcast();
check(GIsThreadedRendering);
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(RenderThread);
// 추가(Add)메모리 배리어
FPlatformMisc::MemoryBarrier();
check(!GIsThreadedRendering);
FCoreDelegates::PreRenderingThreadDestroyed.Broadcast();
#if STATS
if (FThreadStats::WillEverCollectData())
{
FThreadStats::ExplicitFlush(); // Another explicit flush to clean up the ScopeCount established above for any stats lingering since the last frame
}
#endif
ENamedThreads::SetRenderThread(ENamedThreads::GameThread);
ENamedThreads::SetRenderThread_Local(ENamedThreads::GameThread_Local);
// 추가(Add)메모리 배리어
FPlatformMisc::MemoryBarrier();
}UE는 런타임 메모리 장벽을 직접 캡슐화하여 사용하지만 컴파일 타임 메모리 장벽은 캡슐화하지 않는 것을 볼 수 있습니다.
UE는 시스템의 메모리 장벽 외에도 그래픽 API 계층의 메모리 장벽을 캡슐화하고 사용합니다.
// Direct3D / Metal
FPlatformMisc::MemoryBarrier();
// OpenGL
glMemoryBarrier(Barriers);
// Vulkan
typedef struct VkMemoryBarrier {
(......)
} VkMemoryBarrier;
typedef struct VkBufferMemoryBarrier {
(......)
} VkBufferMemoryBarrier;
typedef struct VkImageMemoryBarrier {
(......)
} VkImageMemoryBarrier;1.4.6 엔진 시동 프로세스
Windows와 같은 운영 체제에 대한 프로그래밍을 배운 독자는 각 응용 프로그램마다 운영 체제마다 다른 입구가 있다는 것을 알아야 합니다. 예를 들어 Windows의 프로그램 시작은 WinMain이고 Linux의 경우 Main입니다. 다음은 분석 과정으로 Windows PC 플랫폼 진입을 사용합니다. 시작 코드는 다음과 같습니다.
// Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp
int32 WINAPI WinMain( _In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char*, _In_ int32 nCmdShow )
{
TRACE_BOOKMARK(TEXT("WinMain.Enter"));
SetupWindowsEnvironment();
int32 ErrorLevel = 0;
hInstance = hInInstance;
const TCHAR* CmdLine = ::GetCommandLineW();
// 처리(Process)
if ( ProcessCommandLine() )
{
CmdLine = *GSavedCommandLine;
}
if ( FParse::Param( CmdLine, TEXT("unattended") ) )
{
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
}
(......)
// 에 따라 는 처리(Process) 및 단계 , 의 ,그러나 는 GuardedMain함수 .
#if UE_BUILD_DEBUG
if( true && !GAlwaysReportCrash )
#else
if( bNoExceptionHandler || (FPlatformMisc::IsDebuggerPresent() && !GAlwaysReportCrash ))
#endif
{
// GuardedMain
ErrorLevel = GuardedMain( CmdLine );
}
else
{
(......)
{
GIsGuarded = 1;
// GuardedMain
ErrorLevel = GuardedMainWrapper( CmdLine );
GIsGuarded = 0;
}
(......)
}
// 탈출/종료(Exit)
FEngineLoop::AppExit();
(......)
return ErrorLevel;
}위의 주요 분기는 결국 GuardedMian 인터페이스로 들어갑니다. 코드(발췌)는 다음과 같습니다.
// Engine\Source\Runtime\Launch\Private\Launch.cpp
int32 GuardedMain( const TCHAR* CmdLine )
{
(......)
// 호출(Call)EngineExit
struct EngineLoopCleanupGuard
{
~EngineLoopCleanupGuard()
{
EngineExit();
}
} CleanupGuard;
(......)
// 초기화(Initialize)
int32 ErrorLevel = EnginePreInit( CmdLine );
if ( ErrorLevel != 0 || IsEngineExitRequested() )
{
return ErrorLevel;
}
{
(......)
#if WITH_EDITOR
if (GIsEditor)
{
// 초기화(Initialize)
ErrorLevel = EditorInit(GEngineLoop);
}
else
#endif
{
// ()초기화(Initialize)
ErrorLevel = EngineInit();
}
}
(......)
while( !IsEngineExitRequested() )
{
// 갱신(Update)
EngineTick();
}
#if WITH_EDITOR
if( GIsEditor )
{
// 탈출/종료(Exit)
EditorExit();
}
#endif
return ErrorLevel;
}이 로직은 주로 엔진 사전 초기화(EnginePreInit), 엔진 초기화(EngineInit), 엔진 프레임 업데이트(EngineTick), 엔진 종료(EngineExit)의 네 단계로 구성되어 있음을 어렵지 않게 볼 수 있습니다.
1.4.6.1 엔진 사전 초기화
UE 엔진 사전 초기화는 주로 시작 페이지에서 많은 초기화 및 기본 핵심 관련 모듈을 수행합니다.

주요 코드는 다음과 같습니다.
// Engine\Source\Runtime\Launch\Private\Launch.cpp
int32 EnginePreInit( const TCHAR* CmdLine )
{
// 호출(Call)GEngineLoop초기화(Initialize).
int32 ErrorLevel = GEngineLoop.PreInit( CmdLine );
return( ErrorLevel );
}
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
int32 FEngineLoop::PreInit(const TCHAR* CmdLine)
{
// 의 개
const int32 rv1 = PreInitPreStartupScreen(CmdLine);
if (rv1 != 0)
{
PreInitContext.Cleanup();
return rv1;
}
const int32 rv2 = PreInitPostStartupScreen(CmdLine);
if (rv2 != 0)
{
PreInitContext.Cleanup();
return rv2;
}
return 0;
}사전 초기화 단계에서는 랜덤 시드를 초기화하고, CoreUObject 모듈을 로드하고, FTaskGraphInterface 모듈을 시작하고 현재 게임 스레드를 여기에 연결한 다음, LoadPreInitModules에 의해 완료되는 UE의 기본 핵심 모듈(엔진, 렌더러, SlateRHIRenderer, 랜드스케이프, TextureCompressor 등) 중 일부를 로드합니다.
void FEngineLoop::LoadPreInitModules()
{
#if WITH_ENGINE
FModuleManager::Get().LoadModule(TEXT("Engine"));
FModuleManager::Get().LoadModule(TEXT("Renderer"));
FModuleManager::Get().LoadModule(TEXT("AnimGraphRuntime"));
FPlatformApplicationMisc::LoadPreInitModules();
#if !UE_SERVER
if (!IsRunningDedicatedServer() )
{
if (!GUsingNullRHI)
{
// This needs to be loaded before InitializeShaderTypes is called
FModuleManager::Get().LoadModuleChecked<ISlateRHIRendererModule>("SlateRHIRenderer");
}
}
#endif
FModuleManager::Get().LoadModule(TEXT("Landscape"));
FModuleManager::Get().LoadModule(TEXT("RenderCore"));
#if WITH_EDITORONLY_DATA
FModuleManager::Get().LoadModule(TEXT("TextureCompressor"));
#endif
#endif // WITH_ENGINE
#if (WITH_EDITOR && !(UE_BUILD_SHIPPING || UE_BUILD_TEST))
FModuleManager::Get().LoadModule(TEXT("AudioEditor"));
FModuleManager::Get().LoadModule(TEXT("AnimationModifiers"));
#endif
}후속 처리는 로그 구성, 진행 정보 로드, 메모리 할당자의 TLS(스레드 로컬 범위) 캐시, 일부 전역 상태 설정, 작업 디렉터리 처리, 일부 기본 핵심 모듈(FModuleManager, IFileManager, FPlatformFileManager 등) 초기화입니다. 또 다른 중요한 점은 게임 스레드를 처리하고, 현재 WinMain을 실행 중인 스레드를 게임 스레드(메인 스레드)로 설정하고 스레드 ID를 기록하는 것입니다. 이 코드는 다음과 같습니다:
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
(......)
GGameThreadId = FPlatformTLS::GetCurrentThreadId();
GIsGameThreadIdInitialized = true;
FPlatformProcess::SetThreadAffinityMask(FPlatformAffinity::GetMainGameMask());
FPlatformProcess::SetupGameThread();
(......)
}그런 다음 셰이더 소스 코드 디렉터리 매핑을 설정하고, 네트워크 토큰(토큰)을 처리하고, 일부 기본 모듈(FCsvProfiler, AppLifetimeEventCapture, FTracingProfiler) 및 앱을 초기화한 다음, 플랫폼이 멀티스레딩을 지원하는지 여부에 따라 스레드 풀과 지정된 수의 스레드를 생성합니다.
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
(......)
if (FPlatformProcess::SupportsMultithreading())
{
{
TRACE_THREAD_GROUP_SCOPE("IOThreadPool");
SCOPED_BOOT_TIMING("GIOThreadPool->Create");
GIOThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 2;
}
verify(GIOThreadPool->Create(NumThreadsInThreadPool, 96 * 1024, TPri_AboveNormal));
}
}
(......)
}그런 다음 UGameUserSettings, 확장성, 렌더링 스레드(활성화된 경우), FConfigCacheIni, FPlatformMemory, 게임 물리학, RHI, RenderUtils, FShaderCodeLibrary, ShaderHashCache를 초기화하거나 처리합니다.
사전 초기화의 후반 단계에서 엔진은 SlateRenderer, IProjectManager, IInstallBundleManager, MoviePlayer, PIE 미리 보기 장치 및 엔진 기본 재질과 같은 모듈을 처리합니다.
1.4.6.2 엔진 초기화
엔진 초기화는 편집기와 비편집기의 두 가지 모드로 구분됩니다. 편집자가 아닌 사람은 FEngineLoop::Init를 실행하고, 편집자는 EditorInit+FEngineLoop::Init를 실행합니다. 여기서는 편집자가 아닌 사람이 실행한 초기화 로직만 분석합니다.
엔진 초기화 프로세스는 FEngineLoop::Init에 의해 완료됩니다. 주요 프로세스는 다음과 같습니다.
구성 파일에 따라 해당 게임 엔진 인스턴스를 생성하고 GEngine에 저장합니다. GEngine 인스턴스는 나중에 광범위하게 사용될 것입니다.
멀티스레딩 지원 여부에 따라 EngineService 인스턴스를 생성해야 하는지 여부를 결정합니다.
GEngine->Start()를 실행합니다.
Media, AutomationWorker, AutomationController, ProfilerClient, SequenceRecorder, SequenceRecorderSections 모듈을 로드합니다.
스레드 하트비트 FThreadHeartBeat를 켭니다.
외부 프로파일러 FExternalProfiler를 등록합니다.
1.4.6.3 엔진 프레임 업데이트
엔진 초기화 프로세스는 FEngineLoop::Tick에 의해 완료됩니다. 주요 프로세스는 다음과 같습니다.
스레드 및 스레드 후크 하트비트를 켭니다.
렌더링 모듈이 모든 프레임을 업데이트할 수 있는 객체(FTickableObjectRenderThread 인스턴스)를 업데이트합니다.
분석기(FExternalProfiler) 프레임 동기화.
콘솔의 콜백 인터페이스를 실행합니다.
플러시 렌더링 명령(FlushRenderingCommands). 별도의 렌더링 스레드가 활성화되지 않은 경우 게임 스레드에서 렌더링 지침이 실행된 다음 ImmediateFlush가 호출되어 그리기를 위해 명령 대기열이 제출되었는지 확인합니다. 렌더 펜스(FRenderCommandFence)가 끝에 추가됩니다.
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
void FlushRenderingCommands(bool bFlushDeferredDeletes)
{
(......)
if (!GIsThreadedRendering
&& !FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::GameThread)
&& !FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::GameThread_Local))
{
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread_Local);
}
ENQUEUE_RENDER_COMMAND(FlushPendingDeleteRHIResourcesCmd)(
[bFlushDeferredDeletes](FRHICommandListImmediate& RHICmdList)
{
RHICmdList.ImmediateFlush(
bFlushDeferredDeletes ?
EImmediateFlushType::FlushRHIThreadFlushResourcesFlushDeferredDeletes :
EImmediateFlushType::FlushRHIThreadFlushResources);
});
AdvanceFrameRenderPrerequisite();
FPendingCleanupObjects* PendingCleanupObjects = GetPendingCleanupObjects();
FRenderCommandFence Fence;
Fence.BeginFence();
Fence.Wait();
(......)
}OnBeginFrame 이벤트를 트리거합니다.
스레드 로그를 새로 고칩니다.
GEngine을 사용하여 시간을 새로 고치고 최대 프레임 속도를 처리하세요.
모든 WorlContext의 현재 World를 탐색하고 World에 있는 장면의 PrimitiveSceneInfo를 업데이트합니다. 여기에 코드를 직접 게시하면 이해하기가 더 쉬울 수 있습니다.
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
UWorld* CurrentWorld = Context.World();
if (CurrentWorld)
{
FSceneInterface* Scene = CurrentWorld->Scene;
ENQUEUE_RENDER_COMMAND(UpdateScenePrimitives)(
[Scene](FRHICommandListImmediate& RHICmdList)
{
Scene->UpdateAllPrimitiveSceneInfos(RHICmdList);
});
}
}RHI 프레임 시작을 처리합니다.
모든 장면에 대해 StartFrame을 호출합니다.
성능 분석 및 통계를 처리합니다.
렌더링 스레드에 대한 프레임별 작업을 처리합니다.
월드 스케일 스케일링(WorldToMetersScale)을 처리합니다.
활성 플랫폼에 대한 설명서를 업데이트합니다.
슬레이트 모듈 입력을 처리합니다.
GEngine의 Tick 이벤트입니다. 이는 메인 프레임 업데이트이며 여기에서 많은 로직이 처리됩니다. 다음은 UGameEngine::Tick의 주요 프로세스입니다:
시간 간격이 충분하면 로그를 새로 고치십시오.
닫힌 게임 뷰(뷰포트)를 정리합니다.
하위 시스템을 업데이트합니다.
FEngineAnalytics 및 FStudioAnalytics 모듈이 업데이트되었습니다.
(Chaos가 활성화된 경우) ChaosModule을 업데이트합니다.
WorldTravel의 프레임 업데이트를 처리합니다.
모든 월드 프레임 업데이트를 처리합니다.
스카이라이트 구성 요소(USkyLightComponent) 및 반사 공 구성 요소(UReflectionCaptureComponent)가 업데이트되었습니다. 이는 이 두 구성 요소가 특별하고 하드 코드가 필요함을 보여줍니다.
플레이어 개체(ULocalPlayer)를 처리합니다.
레벨 스트리밍 로딩을 처리합니다.
업데이트 가능한 모든 개체를 업데이트합니다. 여기서 업데이트된 것은 FTickableGameObject입니다.
GameViewport가 업데이트되었습니다.
창 모드에서 창을 처리합니다.
뷰포트를 그립니다.
IStreamingManager 및 FAudioDeviceManager 모듈이 업데이트되었습니다.
GRenderingRealtimeClock, GRenderTargetPool 및 FRDGBuider와 같은 렌더링 관련 모듈이 업데이트되었습니다.
GShaderCompilingManager의 비동기 컴파일 결과를 처리합니다.
GDistanceFieldAsyncQueue(거리 필드 비동기 대기열)의 비동기 작업을 처리합니다.
Slate 관련 작업 로직을 병렬로 처리합니다.
복제된 속성(ReplicatedProperties)을 처리합니다.
ConcurrentTask에 저장된 병렬 작업을 처리하려면 FTaskGraphInterface를 사용하세요.
렌더링 대기열에서 뛰어난 렌더링 작업을 기다리고 있습니다. 어쩌면 내 이해가 정확하지 않을 수도 있으므로 코드를 게시하겠습니다.
ENQUEUE_RENDER_COMMAND(WaitForOutstandingTasksOnly_for_DelaySceneRenderCompletion)(
[](FRHICommandList& RHICmdList)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_DelaySceneRenderCompletion_TaskWait);
FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::WaitForOutstandingTasksOnly);
});AutomationWorker 모듈이 업데이트되었습니다.
RHI 모듈을 업데이트합니다.
프레임 수(GFrameCounter) 및 총 프레임 업데이트 시간(TotalTickTime)을 처리합니다.
다음 프레임에서 정리해야 할 개체를 수집합니다.
프레임 종료 동기화 이벤트(FFrameEndSync)를 처리합니다.
Ticker, FThreadManager 및 GEngine에 대한 TickDeferredCommands가 업데이트되었습니다.
OnEndFrame은 게임 스레드에서 트리거됩니다.
EndFrame 이벤트는 렌더링 모듈에서 트리거됩니다.
1.4.6.4 엔진 종료
엔진이 종료될 때, 에디터 모드가 아닌 경우 ErrorLevel 값이 직접 반환됩니다. 편집기 모드에 있으면 EditorExit 로직이 실행됩니다. 주요 작업은 로그를 저장하고 각 엔진 모듈을 닫고 해제하는 것입니다.
// Engine\Source\Editor\UnrealEd\Private\UnrealEdGlobals.cpp
void EditorExit()
{
TRACE_CPUPROFILER_EVENT_SCOPE(EditorExit);
GLevelEditorModeTools().SetDefaultMode(FBuiltinEditorModes::EM_Default);
GLevelEditorModeTools().DeactivateAllModes(); // this also activates the default mode
// Save out any config settings for the editor so they don't get lost
GEditor->SaveConfig();
GLevelEditorModeTools().SaveConfig();
// Clean up the actor folders singleton
FActorFolders::Cleanup();
// Save out default file directories
FEditorDirectories::Get().SaveLastDirectories();
// Allow the game thread to finish processing any latent tasks.
// Some editor functions may queue tasks that need to be run before the editor is finished.
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
// Cleanup the misc editor
FUnrealEdMisc::Get().OnExit();
if( GLogConsole )
{
GLogConsole->Show( false );
}
delete GDebugToolExec;
GDebugToolExec = NULL;
}특별 지침
모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.
이 시리즈의 기사는 저자가 직접 작성한 것이며 블로그에만 게시됩니다. 이 기사의 링크를 공유하는 것은 환영하지만 무단 복제는 허용되지 않습니다!
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
📚 참고 자료 및 공식 링크 (References)
- Unreal Engine 4 Sources
- Unreal Engine 4 Documentation
- Rendering and Graphics
- Materials
- Graphics Programming
- Unreal Engine
- Unreal Engine 4 Rendering
- 언리얼 엔진(Unreal Engine)
- 📖 언리얼 엔진의 초현실적 휴먼 렌더링 기술 분석 1부 - 개요 및 피부 렌더링
- 📖 레이 트레이싱 기술과 UE4 구현 살펴보기
- 📖 GPU 하드웨어 아키텍처 및 작동 메커니즘에 대한 심층적인 이해
- 언리얼 엔진(Unreal Engine)5
- 『-언리얼 엔진(Unreal Engine)개요 분석』
- 『-언리얼 엔진 4 (UE4)렌더링아키텍처분석 및 고찰』
- UE4 Render System Sheet
- The End of Moore’s Law and the Return of Cleverness
- FrameGraph Extensible Rendering Architecture in Frostbite
- Unity개발 언리얼 엔진(Unreal Engine)4가이드
- Unreal Engine Math
- Neon
- Streaming SIMD Extensions
- Floating-point unit
- Inside UE4
- Exploring in UE4
- c++ lambda
- ?
- Survey of Efficient Representations for Independent Unit Vectors
- UE4(1)
- UE4소스 코드 심층 분석:MallocBinned
- UE4
- ——
- 『 알고리즘 및 구현』
- 『게임 엔진 아키텍처 (Game Engine Architecture)』
- Memory stomp allocator for Unreal Engine 4
- Intel® Threading Building Blocks
- 언리얼 엔진 4 (UE4)
- Memory ordering
- Memory barrier
- Memory barrier
- Memory Barrier
- Using JDK 9 Memory Order Modes
- Memory Barriers Are Like Source Control Operations
- Memory Barriers: a Hardware View for Software Hackers
- MESI protocol
- User mode and kernel mode
- UE4
- UE4 ()GC Cluster
- 언리얼 엔진 4 (UE4)심층 분석