WONMOCK LEE / GRAPHICS ENGINEER
프로젝트 01 · 개발 진행 중

커스텀 C++ 게임 엔진

World · Render · RHI · Asset, 4개 Thread로 게임 로직과 렌더링을 나눈 Unreal 스타일 구조로 짬. 본업의 OpenGL ES 지도 렌더러 바깥에서 해보고 싶었던 걸 여기 다 담았고, 위 메뉴의 네 시스템이 이 엔진 위에 얹혀 있음.

01 / INTRO

핵심 내용 & 전체 구조

전체 구조 — Thread별 책임과 데이터 흐름검은 칸 = 다음 Thread로 넘기는 지점
엔진 Thread 구조와 데이터 흐름 Main, World, Render, RHI, Asset Thread가 각자 맡은 단계를 처리하고, World에서 Render로는 RenderCommandList, Render에서 RHI로는 RHICommandList로 넘기며 각 경계는 FrameGate가 최대 3프레임까지 앞서도록 제한함. Asset Thread는 파싱 후 GPU 업로드 명령을 RHITaskExecutor로 넘김. RHI는 RHISystem과 RHIContext를 거쳐 OpenGL 또는 Vulkan 백엔드를 호출함. MAIN Thread WORLD Thread RENDER Thread RHI Thread ASSET Thread +4 Engine::Run 메시지 펌프 · Worker 시작 Win32Window WndProc · 리사이즈 · 종료 Win32InputHandler 키 · 마우스 이벤트 큐 InputStorage lock-free 3중 버퍼 입력 스냅샷 (매 프레임) World::Update 입력 → System 실행 System Graph Kahn 위상정렬로 실행 순서 Scene::Flush 추가 · 삭제 · 갱신 3단계 Commander Proxy 스냅샷 → 람다 기록 FrameGate ≤ 3 RenderCommandList 람다 · LinearArena ×2 핑퐁 RenderCommandList ExecuteAll — 람다 실행 RenderScene SceneProxy · MeshBatch MeshProcessor → MeshDrawCommand RenderGraph Pass 5종 · ECS::Graph 상속 FrameGate ≤ 3 RHICommandList CRTP RHICommand<T> 기록 RHIFrameExecutor CommandList 링 ×3 (SPSC) RHITaskExecutor Asset 업로드 큐 RHISystem 상태 캐시 · 지연 해제 Transition · 상수 Ring Buffer RHIContext 백엔드 가상 인터페이스 엔진은 이 아래를 모름 GLContext OpenGL 4.6 · WGL VulkanContext Vulkan 1.1 · 진행 중 AssetSystem 로드 요청 큐 AssetWorker 유휴 TaskWorker에 배정 TaskWorker ×4 OBJ · MTL · PNG · GLSL 파싱 AssetCommander GPU 업로드 명령 기록 업로드 Task (RHI Thread에서만 실행)

지그재그로 읽으면 됨 — Main(←) → World(→) → Render(←) → RHI(→). World → Render, Render → RHI 경계마다 FrameGate가 있어서 앞 Thread가 뒤 Thread보다 최대 3프레임까지만 앞서 나감. 그래픽스 API를 부르는 건 RHI Thread 하나뿐이고, Asset Thread도 GPU 업로드는 직접 하지 않고 명령만 기록해서 넘김.

왜 이 구조: OpenGL Context는 Thread 종속적이라 GPU 호출을 한 Thread에 몰아야 함 — World 시뮬레이션이 그 GPU 대기에 발목 잡히지 않도록, Unreal 스타일로 World/Render/RHI/Asset을 4개 Thread로 분리.
렌더 타입을 참조하지 않는 World
ProxyComponent가 IsDirty()만 뒤집으면, Scene이 감지해서 Commander를 통해 스냅샷을 내보냄. World 코드 어디에도 렌더러 타입이 등장하지 않음.
그래픽스 API 호출은 RHI 전담
OpenGL Context는 Thread 종속성이 있어서, 모든 GPU 호출은 Command 객체(실측 41개)로 감싸고 RHIWorker만 실행함.
힙 할당이 거의 없는 RenderCommandList
두 개의 LinearArena가 핑퐁 방식으로 동작 — World Thread가 하나에 람다를 쓰는 동안 Render Thread가 다른 하나를 비움.
100만 Instance 최적화의 진짜 병목"무조건 실행되는 삽입"
Dirty 추적용 HashSet::Insert가 프레임당 200만 번 무조건 호출되던 것을 if (!IsDirty()) 가드 하나로 5.4 → 22 FPS(Release)까지 끌어올림.

특징

코드로 지켜지는 World-Render 경계
렌더 타입을 아예 모르는 구조라, 렌더러를 통째로 교체해도(OpenGL→Vulkan) World/ECS 코드는 한 줄도 안 건드림 — 지금 Vulkan 백엔드를 별도 아키텍처 없이 기존 RHICommand/RHIWorker 위에 그대로 얹을 수 있는 것도 이 경계 덕분임.
vtable 없는 Command Dispatch41개 Command를 함수 Pointer 하나로
CRTP 기반 RHICommand<T>라 실행 시점에 가상 함수 호출 Overhead가 없고, OpenGL Context는 RHIWorker Thread 하나로만 격리해서 Thread 안전성을 구조적으로 확보함.
숫자로 남은 실측 근거
TransformPass(GPU Compute)로 50만 Instance, Hash Storm 가드로 5.4→22 FPS — 숫자로 뒷받침된 최적화 히스토리가 실제 Profiler 스크린샷과 함께 남아 있음.

주요 내용

라이트 하나짜리 데모에선 안 드러나는 결함
LightingPass Blending 버그가 대표적 — 실사용 시나리오(여러 라이트 동시)로 테스트하지 않으면 놓치는 종류의 문제임.
여러 군데 남은 설계 초기 흔적
WorldCommandList(사실상 미사용), CullingPass·OverlayPass(미구현)처럼 구조는 있는데 내용이 없는 부분이 있어서, 코드만 보고 "이게 지금 쓰이는 경로인지" 판단하기 어려운 지점이 있음.
아직 절반인 GC 통합과 Vulkan
Memory Library는 이미 완성됐지만 엔진에 완전히 통합되지는 않았고(기능 현황 "진행 중"), Vulkan은 리소스 계층(메모리 · Buffer · Texture · Shader)까지 동작하고 Pipeline · RenderPass · Draw 경로는 아직 비어있음 — 그 전에 RHI 공용 계층부터 재설계하는 중.
02 / RESULTS

결과물

성능 최적화 여정100만 Instance 병목 추적기

이슈 #25 한 건에서 한 달(2026-04-22 ~ 2026-05-23)에 걸쳐 실제로 있었던 일임 — Profiler로 병목을 찾고, 고치고, 중간에 리팩토링으로 오히려 성능이 떨어지기도 하고, 다시 고치고, 다음 병목을 찾는 과정을 그대로 옮김.

1) 3만 Instance 회전scalar GLM 행렬 곱에 발목 잡힌 Render Thread
Flame Graph에서 SceneProxy::UpdateRotation → UpdateTransform → glm::detail::mul4x4가 압도적 1위. GLM이 SIMD 없이 컴파일돼서 mat4×mat4가 전부 스칼라 루프로 도는 게 원인이었음.
Flame Graph — glm::detail::mul4x4 병목
Flame Graph — glm::detail::mul4x4가 Render Thread 예산을 대부분 잡아먹음
RHI Worker는 병목이 아님을 확인한 프로파일 스크린샷
프로파일링으로 배제 — RHIWorker 자체는 원인이 아니었음을 확인
2) 리팩토링 중 오히려 후퇴ISM 도입 과정의 10배 성능 저하
Unreal의 FInstancedStaticMeshSceneProxy를 참고해 InstancedStaticMeshProxy 개념을 도입하던 중, 이전 엔진이 100만 개 @ 20 FPS(Release)를 버텼던 반면 리팩토링 직후 버전은 10만 개에서야 20 FPS로 오히려 후퇴함 — 리팩토링이 항상 개선을 보장하지 않는다는 걸 Profiler가 바로 알려준 사례임.
ISM 리팩토링 직후, 10만 큐브에서 20fps로 후퇴한 프로파일 스크린샷
회귀 확인 — ISM 리팩토링 직후, 10만 개에서 이미 20 FPS까지 하락
3) TransformPass로 회복 + 그 이상TRS 행렬 계산을 GPU Compute Shader로 이전
기존에는 MeshBatch 안에서 Instance마다 CPU로 Translation×Rotation×Scale 행렬을 만들었는데, 이제 local_size_x = 64 Compute Shader가 RawTransform SSBO(위치·쿼터니언·스케일)를 읽어 Transform SSBO(mat4)로 계산함. 결과: Debug 10만 개 안정 20 FPS(리팩토링 전에는 3만 개에서도 10 FPS 밑으로 떨어졌음), Release 50만 개 ~15 FPS.
4) 다음 병목 확인Proxy 등록 시 HashTable::Rehash 임시 배열의 힙 할당
행렬 계산 병목이 풀리자 Profiler가 다음으로 HashTable::Rehash 내부의 임시 DynamicArray 생성을 새 할당 Hotspot으로 지목함. 다음 단계로 대량 삽입 전에 예상 용량만큼 Reserve를 미리 호출해서 Hot Path에서 Rehash 자체를 없애는 방향을 이슈에 남겨둠.
TransformPass 도입 이후 HashTable::Rehash가 새 병목으로 나타난 프로파일 스크린샷
다음 타겟 — HashTable::Rehash가 새로운 할당 Hotspot으로 떠오름
GPU compute TRS, 50만 큐브, 릴리즈 모드
GPU Compute TRS — 50만 큐브, Release 100만 큐브 원본(112MB) →
Deferred Rendering 파이프라인 데모
Deferred Rendering — Geometry → Lighting, 전체 파이프라인
섀도우 시머링 수정, 텍셀 스냅, 카메라 이동 중
섀도우 시머링 수정 — 텍셀 스냅, 카메라 이동 중
Hash Storm병목은 "무조건 실행되는 삽입"
100만 Instance에서 프레임당 200만 번 호출되던 HashSet::Insert를 if (!IsDirty()) 가드로 프레임당 1회로 축소: 5.4 → 22 FPS (Release).
03 / HISTORY

개발 히스토리

커밋 기록을 불러오는 중…

  1. 1기DirectX 11 엔진2023.03 — 2023.05
    에디터부터 만들다 메모리 문제에서 정지
  2. 2023.03.23
    Engine첫 커밋DX11 CoreEngine + ImGui 레이어
    파일 · 프로퍼티 브라우저 레이어와 프로젝트 저장/불러오기까지 구현.
  3. 2023.04.21
    EngineECS 구조 도입Entity · ComponentManager · NodeManager
  4. 2023.05.13
    EngineComponentManager 재할당→ Pointer 무효화
    vector가 늘어날 때마다 Component Pointer가 전부 무효화되는 문제를 겪음 — 6일 뒤 "memory pool" 아이디어 커밋으로 이어지고, Memory · Log · ECS를 독립 저장소로 떼어내는 계기가 됨.
  5. 2기OpenGL로 재시작4-thread 뼈대2025.12 — 2026.03
    라이브러리 재작성 후 엔진을 처음부터 조립
  6. 2025.12.18
    EngineDX11 코드 전부 제거OpenGL + Win32/WGL로 재시작
    라이브러리 6개를 git submodule로 조립하는 지금의 구조가 여기서 시작됨 (PR #1).
  7. 2025.12.31
    EngineFramework 뼈대Engine · Worker · InputStorage, World/Render Worker 분리
  8. 2026.01.14
    Engine첫 화면WGLContext + GLSystem + RHICommandList로 사각형 출력
  9. 2026.01.27
    EngineRHI / Proxy 기반 아키텍처
    파일 45개가 한 번에 바뀐 736602c — World가 렌더 타입을 직접 안 건드리는 지금의 경계가 이 시점부터 시작됨.
  10. 2026.02.01
    EngineAsset 모듈 + GLSystem 본구현 + RHIWorker
    Loader · Parser · Worker로 나뉜 Asset 모듈, 버퍼 · 셰이더 · 텍스처를 다루는 GLSystem 709줄, RHI 명령을 따로 실행하는 RHIWorker가 한 번에 들어옴.
  11. 2026.02.21
    EngineWGL Context 소유권을 RHI Thread로 이전→ 멀티스레드 첫 렌더링
    OpenGL Context가 Thread 종속이라 생기던 문제 — Context를 RHI Thread가 소유하게 바꾸고 나서야 단색 배경이 멀티스레드에서 처음 그려짐.
  12. 2026.02.22
    EngineRHICommandExecutor 분리RHIFrameExecutor(프레임 링) / RHITaskExecutor(업로드)
  13. 2기Deferred · 성능 · Lighting · Shadow2026.04 — 2026.06
    엔진 커밋이 가장 많았던 석 달
  14. 2026.04.04
    EngineFrameGate 도입동기화와 데이터 보관의 분리
    FrameContext 하나가 Thread 동기화와 프레임 데이터 보관을 같이 맡던 구조를 정리 — 동기화는 FrameGate로, 데이터는 RenderCommand/Proxy로 쪼갬.
  15. 2026.04.17
    EngineMeshBatch Instancing + Normal Mapping (Stanford Dragon)
  16. 2026.05.21
    EnginePipeLine → RenderPass 아키텍처 전면 교체Deferred Rendering 완성
    전역 싱글턴 3개(GlobalResource · GlobalSampler · GlobalPipeLine)를 GlobalRenderer 하나로 합치고, eResourceSlot과 셰이더 바인딩 이름 규칙(u/t/o 접두사)을 이때 처음 세움. 3개 커밋으로 지금의 Renderer 구조가 만들어짐 (PR #28).
  17. 2026.05.24
    Engine100만 Instance 5.4 → 22 FPS (Release)
    이슈 #25를 한 달 추적한 결과 — TRS 계산을 GPU Compute(TransformPass)로 옮기고, 프레임당 200만 번 돌던 HashSet 삽입을 Dirty 가드로 정리. 과정은 결과물 섹션에 그대로 있음.
  18. 2026.06.01
    EngineMulti-proxy Dirty Flag 경합→ Proxy별 세대 카운터
  19. 2026.06.02
    EnginePhong Point · Spot LightGBuffer UInt 비트 패킹
  20. 2026.06.27
    EngineDirectional Shadow 안정화시머링 · acne · NaN 정리
    Slope Bias + PCF(06.21) → Light Space 텍셀 스냅으로 시머링 제거(06.25) → RPDB + Normal Offset + NaN-safe 분모로 grazing 각도 깜빡임 해결(06.27).
  21. 2기Vulkan 백엔드 · RHI 재설계2026.06 말 — 현재
  22. 2026.07.15
    EngineVulkan 프레임 루프 동작
    06.30 Instance · Device 선택 → 07.09 Surface · Swapchain → 07.11 Command Buffer → 07.15 Semaphore · Fence로 acquire → submit → present.
  23. 2026.07.22
    EngineVulkanMemory4MB 블록 + TLSFArena 서브 할당
    Container에 하루 전(07.21) 들어간 TLSFArena를 그대로 상속. 08.07에 블록을 묶는 키를 memoryType 인덱스 → VkMemoryPropertyFlags로 바꿈 — 서로 다른 인덱스가 같은 속성을 가질 수 있어서.
  24. 2026.08.08
    Engine코드 리뷰로 찾은 엔진 버그 8건 수정
    08.02에 등록한 #53~#60을 한 번에 수정 (PR #61) — GL 프로그램 leak, 실패한 텍스처가 "준비됨"으로 표시되던 문제 등. 남은 1건은 버그 섹션의 #52.
  25. 2026.09.03
    EngineVulkan Buffer · Texture 파이프라인
    생성 · 초기화 · 갱신 · 리사이즈로 책임을 나누고 Staging Buffer 풀과 Buffer → Image 복사를 추가. RHI의 Set/Unset은 이름 대신 binding 번호 기반으로 통일하고, Vulkan에 대응 연산이 없는 UnsetConstant는 인터페이스에서 뺌.
  26. 2026.10.14
    EngineHLSL 전환 시도→ GLSL 단일 소스로 복귀
    DXC는 블록 밖 상수를 $Globals UBO로 묶고, glslang의 Vulkan 타깃은 블록 밖 uniform을 거부함 — 한 소스로 GL · Vulkan을 같이 돌리려면 어차피 이름 있는 UBO 블록이 필요해서 GLSL로 되돌림. 공용 정의는 Common.glsl 하나, GLSLParser가 #include를 펼침.
  27. 2026.10.18
    EngineRHI 공용 계층 재설계
    RHISystem을 진입점(namespace)으로, 백엔드를 RHIContext(GL / Vulkan)로 분리. 셰이더 리플렉션을 합치는 RHIStage, 블록 기반 상수 Ring Buffer 도입 — update/vulkan-rhi 브랜치에서 진행 중. 구조는 Vulkan 절 그림 참고.
04 / THREADING

Threading & 프레임 동기화

프레임 파이프라이닝 — 같은 시점에 Thread마다 다른 프레임예시
프레임 파이프라이닝 World Thread가 프레임 F3을 처리하고 나면 다음 시점에 Render Thread가, 그 다음 시점에 RHI Thread가 F3을 처리함. 같은 시점에 World는 F5, Render는 F4, RHI는 F3을 동시에 처리함. 경계마다 FrameGate가 최대 3프레임까지만 앞서도록 제한함. 시간 → WORLD Thread F1 F2 F3 F4 F5 F6 F7 F8 RENDER Thread 대기 F1 F2 F3 F4 F5 F6 F7 RHI Thread 대기 대기 F1 F2 F3 F4 F5 F6 updateGate · FrameGate(3) renderGate · FrameGate(3) 같은 시점: F5 · F4 · F3
FrameGate = 조건 변수 두 개 + atomic 프레임 차이
Producer의 Submit()은 차이가 한도(3) 이상일 때만, Consumer의 Acquire()는 차이가 0일 때만 대기함 — 평소엔 atomic 값만 읽고 지나가서 락을 잡지 않음. World → Render(updateGate), Render → RHI(renderGate) 두 경계에 하나씩 있음.
RHICommandList 3개를 링으로 재사용하는 RHIFrameExecutor
recordIndex는 Render Thread만, beginIndex는 RHI Thread만 씀 — 두 Thread가 같이 보는 건 atomic endIndex 하나뿐이고, acquire/release 순서로 기록 내용이 보이는 시점을 보장함. Mutex 없는 SPSC 구조.
종료 순서깨우기 → 멈추기 → 놓기
Shutdown에서 두 Gate에 NotifyAll()을 먼저 걸어 대기 중인 Thread를 깨운 뒤 World → Asset → Render → RHI 순서로 Worker를 멈추고, RHICommandList를 쓰는 Worker가 전부 멈춘 다음에야 RHI Context를 놓음.
Sequence Diagram — World → Renderer → RHI, 프레임 1회
World Renderer RHI Sequence Diagram
Sequence Diagram — Asset 파이프라인, RHI로 들어가는 또 다른 경로
Asset 파이프라인 Sequence Diagram
05 / MODULES

네 개의 Layer — 상세 내용 & Diagram

WorldContext

Entity, Component, 그리고 밖으로 나가는 단방향 다리임. Scene::Flush()는 추가/삭제/갱신 3단계를 순서대로 도는데, 각 단계가 끝날 때마다 대상 HashSet을 비움.

WorldContext Class Diagram

ProxyComponent는 렌더러와 직접 대화하지 않음 — IsDirty()만 뒤집으면 Scene이 알아채고 Commander를 통해 스냅샷을 내보냄. Commander가 캡처하는 람다는 [renderView](Renderer*, RefPtr<RHICommandList>){ renderer->SetView(renderView); } 처럼 값 캡처 위주라 힙 할당이 거의 없음.

Renderer

RenderScene과 RenderGraph를 소유함 — 렌더링 쪽 객체는 전부 이 둘 중 하나를 거침.

RenderGraph — Pass 순서와 리소스 흐름검은 칸 = Compute Pass
RenderGraph의 Pass 순서와 리소스 흐름 TransformPass(Compute)가 Instance별 행렬을 Transform SSBO에 계산하면 ShadowPass와 GeometryPass가 읽음. ShadowPass는 ShadowMap을, GeometryPass는 GBuffer(Normal RGBA16F, Albedo RGBA8, Phong RGBA16UI, Depth D24S8)를 쓰고, LightingPass가 둘 다 읽어 라이트별로 셰이딩함. DebugPass는 GBuffer를 시각화함. Transform SSBO Instance별 mat4 — GPU에서 계산, Shadow · Geometry가 읽음 ShadowMap · Depth GBuffer Normal · RGBA16F Albedo · RGBA8 Phong · RGBA16UI Depth · D24S8 COMPUTE PASS TransformPass TRS → mat4 · 64개씩 병렬 GRAPHIC PASS ShadowPass Directional · 텍셀 스냅 GRAPHIC PASS GeometryPass GBuffer 채움 · Instancing GRAPHIC PASS LightingPass 라이트당 풀스크린 Quad GRAPHIC PASS DebugPass GBuffer · Frustum 시각화

RenderGraph는 ECS::Graph<RenderPass>를 상속 — ECS가 System 실행 순서를 정할 때 쓰는 위상정렬을 그대로 재사용함. 지금은 SetDependency로 한 줄 체인을 걸어둔 상태라 분기는 없음.

Renderer Class Diagram
(mesh, material) 조합 하나당 MeshDrawCommand 하나→ Draw Call 하나
GeometryPass가 vertex layout + transform SSBO + 머티리얼을 바인딩하고, Batch 안의 모든 Instance를 인덱스 Draw 한 번으로 그림.
RenderPass의 두 추상 기반ComputePass · GraphicPass
ComputePass는 Compute Shader Dispatch만 담당하고, GraphicPass는 clear/depth/blend/ rasterizer 같은 Pipeline 상태 전체를 소유함.
RenderGraph의 실체Graph가 아닌 고정 체인
Transform→Shadow→Geometry→Lighting→Debug 순서로 SetDependency가 미리 걸려 있어서, "Graph" 구조인데 실제 파이프라인은 분기 없는 직선임.
1차원 고정 TransformPass Dispatch64 Instance당 Workgroup 1개
groupX = ceil(instanceCount / 64), Y/Z는 항상 1. transform.cs.glsl의 local_size_x = 64와 맞물려 있는데, 생성자에서 넘기는 ComputePass(64, 64, 64) 그룹 크기는 아무 데서도 읽히지 않는 죽은 인자임 — Dispatch 시점에 로컬 상수 64를 다시 씀.
CullingPassStub 상태, 배선은 완료
Draw()는 상태 체크만 하고 항상 true, GetResourceState()도 항상 eReady로 하드코딩. 다만 카메라/Indirect Draw Buffer/가시성 Buffer/AABB Buffer를 바인딩하는 SetCommand()는 이미 존재함. 짝을 이루는 culling.cs.glsl은 이름과 달리 Compute가 아니라 Vertex Shader 내용이라, Culling은 Shader·Dispatch·Pass 로직 전부 미구현 상태임.
완전히 빈 파일인 OverlayPass
OverlayPass.h/.cpp 둘 다 0바이트 — Class 선언조차 없는, 시작조차 안 된 자리표시자임.

GeometryPass가 한 프레임에 이 3개 채널을 GBuffer로 나눠서 씀 — LightingPass가 이 값들만 갖고 라이트별로 다시 계산. Position 채널은 실제로 저장되는 렌더 타깃이 아니라, Depth에서 그 자리에서 재구성한 디버그 시각화(이슈 #30 참고).

GBuffer albedo 채널
Albedo — 표면 고유색
GBuffer normal 채널
Normal — 라이팅 계산용 표면 법선
GBuffer world-position 채널
Position — Depth에서 재구성

RHI

그래픽스 API를 직접 호출할 수 있는 유일한 Layer임. Command Class는 41개(초기화 11 · 갱신 3 · 리사이즈 2 · 제거 7 · Set 8 · Unset 7 · 프레임/Dispatch 3)이고, 전부 CRTP로 만든 RHICommand<T> 라 실행 시점엔 vtable이 아니라 함수 Pointer 하나로 Dispatch됨. 아래 다이어그램 3개는 master 기준(GLSystem이 RHISystem을 구현하던 구조)이고, 2026.10 재설계 후 구조는 Vulkan 절에 있음.

RHI Class Diagram
OpenGL Context는 Thread 종속성이 있어서, 다른 Thread에서 호출하면 정의되지 않은 동작이 됨. 모든 GPU 호출은 Command 객체로 감싸고 RHIWorker만 실행함.

리소스 삭제는 RHISystem이 즉시 지우지 않고, 7종 HashSet에 모아뒀다가 GLSystem::FlushPending()이 Flush 시점에 한 번에 처리함 — Thread 경계를 넘나드는 glDelete* 호출을 구조적으로 막는 장치.

Class 다이어그램 — GLSystem이 RHISystem을 구현하는 방식
GLSystem RHISystem Class 다이어그램
Sequence 다이어그램 — 지연 삭제(FlushPending)
FlushPending Sequence 다이어그램

Vulkan 백엔드 & RHI 재설계 진행 중

Vulkan을 붙이면서 OpenGL에 맞춰져 있던 RHI 공용 계층을 Explicit API 기준으로 다시 짜는 중. 새 아키텍처를 따로 세운 게 아니라 RHICommand<T> 기록 · RHIWorker 실행 모델은 그대로 두고, 그 아래 (상태 · 바인딩 · 상수 · 백엔드 경계)를 바꾸는 작업임. 그림은 update/vulkan-rhi 브랜치 기준(2026.10).

RHI 계층 — 2026.10 재설계 후점선 = 진행 중
RHI 계층 구조 (2026.10 재설계 기준) Renderer가 RHICommandList에 명령을 기록하면 RHIFrameExecutor의 3개짜리 링을 거쳐 RHI Thread가 실행함. RHISystem은 상태 캐시, 지연 해제, Transition을 맡는 진입점이고, RHIContext 가상 인터페이스 아래에 GLContext와 VulkanContext가 있음. VulkanContext는 4MB 블록을 TLSFArena로 서브 할당하는 VulkanMemory를 가짐. RENDER THREAD ↑ RHI THREAD ↓ Renderer · RenderPass SetBuffer · SetPipeLine · Draw / Dispatch 호출 RHICommandList RHICommand<T> 기록 — CRTP, 함수 Pointer 하나로 Dispatch 설계 — 리소스가 자기 배리어 상태를 들고, 기록 시점에 요구 상태를 모아 RenderPass · Dispatch 직전 일괄 전환 (구현 중) RHIFrameExecutor CommandList 링 ×3 — Render가 기록, RHI가 실행 RHISystem (namespace) 중복 Set 생략 캐시 · 지연 해제(PendingPool) · Transition 2026.10 — GLSystem / VulkanSystem 상속 구조를 진입점(RHISystem) + 백엔드(RHIContext)로 분리 RHIContext (virtual) Create · Initialize · Update · Set · Transition · Draw 상수는 낱개 Uniform 대신 이름 있는 UBO 블록 + 프레임 Ring Buffer 슬라이스 (바뀐 경우만 기록) GLContext OpenGL 4.6 · WGL VulkanContext Vulkan 1.1 · Device · Surface · Swapchain VulkanMemory 4MB VkDeviceMemory 블록 + TLSFArena 서브 할당 완료 — 메모리 · Buffer · Texture · Sampler · Shader (GLSL → SPIR-V, SPIRV-Reflect로 바인딩 검증) 진행 중 — Pipeline · RenderPass · Draw / Dispatch
동작하는 범위초기화 · 프레임 루프 · 리소스 계층
Instance/Device 선택, Surface/Swapchain, Primary/Secondary Command Buffer 분리(RHI Thread용 vs Worker별), Semaphore 2개 + Fence 1개로 acquire → submit → present, OUT_OF_DATE/SUBOPTIMAL 리사이즈 처리. 그 위에 Buffer · Texture 생성과 Staging 업로드, Buffer → Image 복사, Barrier, Sampler, VertexLayout, Shader(GLSL → SPIR-V 컴파일 후 SPIRV-Reflect로 입력 location과 바인딩 검증)까지 동작함.
진행 중인 범위Pipeline · RenderPass · Draw / Dispatch
생성 함수 자리는 있지만 아직 비어 있음. 이걸 잇기 전에 공용 계층부터 바꾸는 중 — "슬롯에 꽂으면 끝"인 GL식 바인딩 모델로는 Vulkan의 Barrier와 Descriptor를 표현할 수 없어서.
VulkanMemory4MB 블록 + 블록 내부 TLSFArena 서브 할당
메모리 속성(VkMemoryPropertyFlags)별로 고정 4MB VkDeviceMemory 블록을 필요할 때 만들고 비면 반환 — Resource마다 VkDeviceMemory를 따로 할당하는 흔한 Anti-pattern을 피함. 블록 내부 서브 할당은 Container Library의 TLSF 구현을 그대로 상속해서 씀.
상수 전달 방식 변경낱개 Uniform → 이름 있는 UBO 블록 + 프레임 Ring Buffer
CPU 쪽 사본과 비교해 바뀐 경우에만 Ring Buffer 슬라이스를 받아 기록하고, frameDiff보다 오래 안 쓴 블록은 회수함. 셰이더는 Common.glsl 한 파일에 BINDING_* 번호와 공용 구조체를 모아서 GL · Vulkan이 같은 소스를 씀.

Asset

3단계, 3개의 서로 다른 Thread — Parse가 메인 루프를 절대 막지 않고, Upload는 RHIWorker 밖에서 절대 실행되지 않음.

Asset Class Diagram
eNone --TaskWorker.Parse()--> eLoaded --AssetCommander.Load() GPU 업로드--> (상태 전이 없음)
Parse = CPU 바운드, TaskWorker 풀(유휴 Worker가 있을 때만 — 없으면 AssetSystem Queue에 다시 넣어 다음 패스로 미룸)
Load  = GPU 업로드(AssetCommander), RHIWorker 전용
※ eAssetState는 eNone/eError/eLoaded/eExpried(오타)/eDirty뿐 — "완료(eReady)" 상태는 없고,
  준비 여부는 texture/buffer/shader Pointer가 non-null인지로 판단

설계가 바뀌어온 과정(RHI/Proxy 경계, FrameGate, RenderPass 전환, Vulkan)은 개발 히스토리에 커밋 날짜와 함께 정리해둠.

06 / FINDINGS

실제 코드에서 발견한 것들

이 섹션은 Docs/Design_*.md 요약이 아니라, 2026.08 코드 리뷰 때 Engine/Private 과 Engine/Public의 실제 구현을 다시 읽으면서 확인한 내용임. 문서와 다른 부분, 그리고 흥미로운 설계 판단 몇 가지를 그대로 남겨둠.

LightingPass의 Blend 미설정라이트가 여럿이면 마지막 라이트만 남는 문제
라이트 하나당 풀스크린 Quad Draw 한 번(Directional/Point/Spot Shader 분리)인데, GetPipeLine()이 채우는 RHIPipeLineCreateDesc에 blend 필드가 끝까지 비어 있음. 로컬로 RHIBlendState blendState; blendState.enable = false;를 선언은 하지만 어디에도 대입·전달되지 않는 죽은 코드임 — 이 구조가 성립하려면 Blend를 꺼야 하는 게 아니라 라이트별로 누적되도록 Additive Blending을 켜야 함. 라이트가 하나뿐인 데모에서는 드러나지 않는 버그임.
RenderGraph::Execute()의 조기 중단Pass 하나만 준비 안 돼도 이후 Pass 전부 생략
정렬된 순서를 돌다가 어떤 Pass의 GetResourceState() != eReady 이거나 Draw()가 false를 반환하면 그 자리에서 루프를 break — 문제 있는 Pass 하나만 건너뛰는 게 아니라 이후 Pass 전부가 이번 프레임에 안 그려짐.
사실상 미사용인 WorldCommandList
struct WorldCommand {}; 정도만 있고 WorldContext::Prepare() 는 그냥 // TODO. World→Renderer 트래픽은 전부 Commander/RenderCommandList로만 흐르고 있어서, 이 Class는 설계 초기 흔적으로 보임.
RHICommand 실제 개수 41개
기존 설계 문서의 "45개 이상"이라는 서술은 실측과 다름 — 초기화 11 · 갱신 3 · 리사이즈 2 · 제거 7 · Set 8 · Unset 7 · 프레임/Dispatch 3.
RenderCommandList::ExecuteAll()의 UB 패턴수정됨 · #57
Task를 실행한 뒤 명시적으로 execute->~RenderTask()를 호출하고, 바로 다음 줄에서 execute->next를 읽었음. next가 trivial한 raw pointer라 실제로는 문제없이 동작했지만, 소멸자 호출 후 멤버를 읽는 건 엄밀히는 정의되지 않은 동작임 → 소멸자 호출 전에 next를 먼저 읽도록 바꿈.
항상 병렬은 아닌 Asset 파싱기회적(opportunistic) 분배
유휴 TaskWorker가 있을 때만 별도 Thread로 넘기고, 없으면 AssetSystem::AddTask()로 다시 Queue에 넣어 다음 패스로 미룸 — 동기적으로 직접 파싱하지 않고, "병렬 파싱"이 항상 보장되는 것도 아님.
초기화 중 AssetWorker 이중 생성수정됨 · #53
Engine::InitAsset()이 AssetWorker를 하나 만든 직후, InitWorker()가 SetTaskThread(4) 설정으로 또 하나를 만들어 m_assetWorker를 덮어씀 — 첫 번째 Instance는 조용히 버려졌음 → InitAsset()을 없애고 InitWorker() 한 곳에서만 만들도록 정리.
WM_CLOSE 처리와 실제 종료 경로의 불일치수정됨 · #54
WndProc의 switch 문에 case 사이 break가 없어서, WM_CLOSE에서 설정한 m_status = eClosed가 같은 호출 안에서 곧바로 다시 eActive로 덮어써졌음. 실제 종료는 상태 체크가 아니라 close Callback → Engine::Shutdown() → m_window 리셋 → Run() 루프의 null 체크로 빠져나가는 경로로 이루어지고 있었음 → WM_CLOSE · WM_SIZE를 각자 case로 분리.
InputStorage의 실체더블 Buffer가 아닌 Lock-free Triple Buffer
m_InputData[3] — 메인 Thread가 쓰는 m_CurrData, 핸드오프용 atomic<InputData*> m_PrevData, WorldWorker가 읽는 m_LogicData. 256개 키코드 전체 상태를 4비트(Down/Press/Release/Repeat)로 패킹한 128바이트 배열로 저장함.
TaskWorker "풀"의 실체AssetWorker 전용, 로드밸런싱 없는 선형 탐색
TaskWorker 자체는 작업 하나짜리 Worker일 뿐 — "풀"은 AssetWorker가 가진 DynamicArray<RefPtr<TaskWorker>>이고, Thread 개수 4는 Engine.cpp에 하드코딩. 분배는 라운드로빈이 아니라 첫 번째로 유휴 상태인 Worker를 선형 탐색. Win32InputHandler의 입력 Queue도 Backpressure 없이 MAX_QUEUE_SIZE를 넘으면 그냥 통째로 비워 밀린 입력을 버림.
제네릭 Task<Args...>의 두 가지 사용 방식
Task<Args...>는 std::function<void(Args...)> 얇은 Wrapper일 뿐인데, Asset/TaskWorker 쪽은 DefaultTask = Task<>로 인자 없이 Closure에 전부 캡처해서 쓰고, RenderTask : Task<Renderer*, RefPtr<RHICommandList>> 는 실제 인자를 받으며 커스텀 Allocator 위에 Intrusive List Node로 placement-new됨 — "제네릭" Template이 사실상 한쪽 전용 소비자를 위해 존재하는 셈임.
"완료(eReady)" 상태가 없는 Asset 상태 머신
실제 eAssetState는 eNone / eError / eLoaded / eExpried(오타) / eDirty 뿐임. GPU 업로드(AssetCommander::Load) 이후 별도 상태 전이가 없어서, 준비 여부는 상태값이 아니라 texture/buffer/shader Pointer가 non-null인지로 다른 코드가 추론함.
RHIDrawIndexDesc / RHIDispatchDesc의 태그 재사용수정됨 · #55
eResourceType은 9개 값(Buffer/Texture/Sampler/Shader/PipeLine/ Layout/Target/Const/Pass)이 따로 있는데, RHIDrawIndexDesc는 eLayout을, RHIDispatchDesc는 ePipeLine을 그대로 재사용했음 — Draw Command와 VertexLayout이 desc.type 값만으로는 구분되지 않았음 → eGraphics / eCompute 태그를 새로 추가.
PNGParser의 componentType 불일치수정됨 · #56
stbi_load_from_memory는 항상 8비트 데이터를 반환하는데, componentType은 소스에 따라 eFloat/ eUInt로 설정됐음. sRGB 판별도 IHDR 바로 다음 청크 하나만 검사했음 → 16비트 PNG는 stbi_load_16 경로로 분기하고 componentType을 실제 비트 수에 맞춤, sRGB 판별은 IHDR ~ IDAT 사이 청크를 전부 검사하도록 바꿈.
07 / STATUS

기능 현황

기능상태
ECS / Memory(GC) / Reflection / Containers완료
World · Commander · Scene · 4-thread Worker 시스템완료
RHI Layer(OpenGL) · Command List · 트리플 버퍼링완료
Proxy · MeshBatch Instancing · RenderGraph완료
TransformPass(GPU Compute TRS)완료
GeometryPass · LightingPass(Directional / Point / Spot)완료
GBuffer Phong(UInt Texture 지원)완료
Asset 시스템(3단계 비동기 파이프라인)완료
Directional 섀도우 매핑완료
CullingPassStub — pass-through
GC 전체 통합진행 중
Cascade 섀도우 맵예정
Spot / Point 라이트 섀도우예정
Vulkan 백엔드 — 프레임 루프 · 메모리 · Buffer · Texture · Shader 구현, Pipeline · Draw 남음진행 중
RHI 공용 계층 재설계 — RHIContext · Transition · 상수 Ring Buffer진행 중
물리 엔진예정
08 / ISSUES

해결한 이슈

섀도우 acne / NaN / 깜빡임 해결

증상
카메라 회전 중 간헐적으로 물체 전체가 검게 표시됨. RenderDoc 단일 프레임 캡처에서는 정상.
원인
grazing 각도에서 RPDB depth-bias의 분모(det)가 0에 가까워지며 NaN이 발생, FragColor까지 그대로 전파됨.
해결
NaN을 사후에 가리려 하지 않고 발생 자체를 차단(safeDet = mix(EPSILON, det, step(EPSILON, abs(det)))) — 곱셈이나 mix로는 NaN을 지울 수 없기 때문. grazing 컷오프도 맨 끝에서 한 번만 곱하도록 이동(dFdx 앞에서 분기하면 GLSL에서 undefined behavior).
섀도우 acne 디버그 캡처
Acne — depth-bias 부족으로 표면에 줄무늬
NaN으로 물체 전체가 검게 나오는 디버그 캡처
NaN 전파 — 원인 규명 전 증상
grazing 각도 디버그 캡처
Grazing 각도 — det가 0에 가까워지는 지점

Multi-proxy dirty flag race 해결

증상
같은 Transform을 공유하는 라이트와 메시를 가진 Entity가, 첫 프레임 이후 둘 중 하나의 업데이트를 조용히 멈춤.
원인
Hash Storm 최적화의 if (!IsDirty()) OnUpdate() 가드가 공유 Component에 있던 dirty 플래그를 지워버림 — 먼저 처리된 Proxy가 둘 다의 플래그를 지운 셈.
해결
Proxy별 dirty 세대(generation) 카운터 도입: Component는 변경 시 세대 번호만 증가시키고, 각 Proxy가 자신이 마지막으로 본 세대를 독립적으로 추적.

Point Light 입사 방향 아티팩트 미해결

증상
표면 아래에 위치한 라이트가, 라이트 볼륨 경계 부근에서 오히려 표면을 더 밝게 만듦.
원인
Two-sided 라이팅 보정의 max(dot(normal, rayDir), 0.0)가 이 경우 0으로 클램프되어, 입사 방향 보정이 아예 걸리지 않음.
개선 방향
보정을 fragment→light가 아니라 light→fragment 방향으로 계산. 섀도우 맵이 도입되면 이 케이스는 자연스럽게 가려지기도 함.
09 / NOTES

기술 노트

- 왜: 1920×1080에서 Position GBuffer(RGBA32F)만 프레임당 ~31MB. Frostbite/UE4/5/CryEngine 전부 Position 채널을 버리고 Depth에서 역산함.
- 어떻게: World → View → Clip → (÷w) → NDC → Depth 순서를 거꾸로 밟아 복원. 핵심은 Position이 동차좌표에서 항상 w=1이라는 불변식.
- 사이트 GBuffer 스크린샷의 "Position" 채널도 실제로 저장되는 렌더 타깃이 아니라, 디버그 시각화 Pass가 Depth로부터 그 자리에서 재구성한 것임.
- 쿼터니언 샌드위치 곱 q·v·q*이 3×3 회전 행렬과 같다는 걸 직접 손으로 증명 — 곱 전개 → Rodrigues 형태 변환 → Vector 연산을 행렬로 변환 → 쿼터니언 성분 대입, 4단계로 진행.
- Library(GLM 등)가 이미 해주는 변환을 그대로 쓰지 않고, 왜 그 공식이 맞는지 끝까지 손으로 따라간 기록임.
- GLSL reflect(I,N) = I - 2·dot(N,I)·N가 교과서 공식과 부호만 다르다는 것, 그리고 faceforward로 normal을 뒤집어도 반사 Vector 자체는 바뀌지 않는다는 걸 정리(NdotL 부호에만 영향).
- 라이트가 표면 안쪽에 있는 two-sided 케이스 3가지 방법을 비교: abs(NdotL)(specular 방향이 틀어짐) → faceforward(diffuse만 고침) → incidentDir 보정(권장 — 입사 방향 자체를 표면 앞쪽으로 재계산). 이 세 번째 방법이 실제로 채택된 방향이고, engine.html의 "Point Light 입사 방향 아티팩트" 이슈와 이어짐.
- 감쇠식은 1/(d²+1)로 d→0 발산을 막고, range 기반 falloff를 곱해 range 밖에서 매끄럽게 0이 되도록 함.
- 2단계 섀도우 매핑(라이트 시점 Depth 렌더 → 라이팅 Pass에서 비교) 기본기부터, 텍셀 스냅으로 시머링을 잡는 방법, depth bias가 Slope Bias에서 RPDB + Normal Offset으로 진화한 과정까지 한 이슈에 정리.
- 여기서 정리한 NaN/acne/grazing 원인 분석이 실제로 site의 "섀도우 acne / NaN / 깜빡임" 이슈 해결로 이어짐.
10 / BUGS

버그

실제 버그 11건 — 9건 수정 완료 클릭해서 펼치기
#52 · Renderer/LightingPass.cpp:64-65, 160-185 라이트가 여러 개면 마지막 라이트만 남는 Blending 버그 버그 · 미수정
- 증상: 라이트 하나당 풀스크린 Quad를 한 번씩 그리는 구조라, 라이트가 여러 개면 화면엔 가장 마지막에 그려진 라이트만 남음(누적이 안 됨).
- 원인: GetPipeLine()이 채우는 RHIPipeLineCreateDesc의 blend 필드가 끝까지 비어 있음. 로컬로 RHIBlendState blendState; blendState.enable = false;를 선언은 하지만 어디에도 대입·전달되지 않는 죽은 코드임.
- 개선 방향: Additive Blending(ONE, ONE)을 desc.blend에 연결해야 함. 라이트가 하나뿐인 데모/스크린샷에서는 드러나지 않아서 지금까지 남아있었던 것으로 보임.
#53 · Framework/Application.cpp, Engine.cpp 초기화 중 AssetWorker 이중 생성첫 번째 Instance가 버려지는 문제 해결
- Engine::InitAsset()이 AssetWorker를 하나 만든 직후, InitWorker()가 SetTaskThread(4) 설정으로 또 하나를 만들어 m_assetWorker를 덮어씀. 두 번째 Instance가 정상 동작해서 당장 문제는 없지만, 첫 번째가 만든 Thread/Resource가 조용히 새는 초기화 순서 버그임.
- 수정(2026.08.08): InitAsset()을 없애고 InitWorker()만 AssetWorker를 만들도록 정리. (PR #61)
#54 · Platform/Win32/Win32Window.cpp — WndProc WM_CLOSE 종료 상태의 즉시 덮어쓰기같은 WndProc 호출 안에서 eActive로 복귀 해결
- WndProc의 switch 문에 case 사이 break가 없어서, WM_CLOSE에서 설정한 m_status = eClosed가 곧바로 eActive로 되돌아감.
- 실제 종료는 이 상태값이 아니라 close Callback → Engine::Shutdown() → m_window 리셋 → Run() 루프의 null 체크로 빠져나가는 별개 경로로 이루어짐. 종료 자체는 되지만, GetStatus()를 신뢰하는 코드가 있다면 오작동함.
- 수정(2026.08.08): WM_CLOSE · WM_SIZE를 각자 case로 분리해서, 상태를 정한 뒤 바로 HandleMessage()로 넘김. (PR #61)
#55 · RHI/RHIDescriptions.h:397, 410 RHIDrawIndexDesc / RHIDispatchDesc의 타입 태그 재사용 해결
- eResourceType은 9개 값이 따로 있는데, RHIDrawIndexDesc는 자기 것 대신 eLayout을, RHIDispatchDesc는 ePipeLine을 그대로 재사용함. 지금은 이 필드로 타입을 분기하는 코드가 없어서 조용히 묻혀 있음.
- 수정(2026.08.08): eResourceType에 eGraphics / eCompute를 추가하고 두 Desc의 태그를 교체. (PR #61)
#56 · Asset/PNGParser.cpp:66-73 PNGParser componentType과 실제 데이터 타입의 불일치 해결
- stbi_load_from_memory는 항상 8비트를 반환하는데, componentType은 eFloat/eUInt로 설정됨 — 둘 다 실제 eUByte와 맞지 않음. sRGB 판별도 IHDR 바로 다음 청크 하나만 검사해서 오탐/누락 여지가 있음(코드 안에 TODO로 남아 있음).
- 수정(2026.08.08): stbi_is_16_bit_from_memory로 분기해 16비트는 stbi_load_16_from_memory를 쓰고, componentType · 픽셀 포맷을 실제 비트 수에 맞춤. sRGB는 IHDR ~ IDAT 사이 청크를 전부 검사. (PR #61)
#57 · Renderer/RenderCommandList.cpp — ExecuteAll() 소멸자 호출 후 멤버를 읽는 기술적 UB 패턴 해결
- Task를 실행한 뒤 execute->~RenderTask()를 호출하고 바로 다음 줄에서 execute->next를 읽음. next가 trivial한 raw pointer라 지금은 문제없이 동작하지만, 엄밀히는 정의되지 않은 동작이라 공격적인 최적화나 sanitizer 환경에서 깨질 수 있음.
- 수정(2026.08.08): 소멸자를 부르기 전에 next를 먼저 읽도록 순서를 바꿈. (PR #61)
RHI/Vulkan/VulkanSystem.cpp:602-666 Vulkan InitializeBuffer가 편집 중인 상태로 남아 컴파일이 안 되던 문제 해결
- vkCreateBuffer()를 인자 없이 호출하고, 끝에 매달린 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; 구문이 그대로 남아 있음.
- VulkanMemory::CreateBlock도 memoryType과 blockSize 인자가 호출부에서 뒤바뀐 것으로 보였음 — Vulkan 백엔드가 한창 작업 중이라 있을 수 있는 상태였음.
- 수정(2026.09.03): Buffer 경로를 생성 · 초기화 · 갱신 · 리사이즈로 나눠 완성 — Vulkan 절 참고.
#58 · RHI/OpenGL/GLSystem.cpp:3519-3526 FlushPipeLine의 뒤집힌 조건GL 프로그램 영구 leak 해결
- if (!glPipeLine || glPipeLine->GetID() == GL_NONE) 분기 안에서 glPipeLine->GetID()를 바로 호출함 — glPipeLine이 null이면 그 자리에서 크래시임.
- 조건 자체도 다른 모든 Flush* 함수와 반대임: ID가 이미 GL_NONE인(할 게 없는) 경우에만 glDeleteProgram을 부르고, 실제로 지워야 할 유효한 프로그램은 절대 안 지워짐 — 파이프라인을 만들 때마다 GL 프로그램 객체가 영구적으로 leak됨.
- 수정(2026.08.08): 참조 수가 1보다 크면 pending에 그대로 두고, pending 목록이 마지막 참조일 때만 glDeleteProgram을 부르도록 조건을 바로잡음. (PR #61)
#59 · RHI/OpenGL/GLSystem.cpp:639-650 InitializeTexture 실패 경로의 return 누락삭제된 Texture가 "준비됨"으로 표시되는 문제 해결
- 실패 시 Texture를 삭제하고 eError 상태를 설정한 뒤에도 return이 없어서, 바로 다음 줄의 glTexture->SetID(textureID)/SetState(eReady)가 그대로 실행됨 — 방금 삭제한(더 이상 유효하지 않은) GL Texture 이름을 "준비됨" 상태로 표시해버림.
- 수정(2026.08.08): 실패 경로에서 SetID(GL_NONE) · SetState(eError)를 명시하고 성공/실패 분기를 분리 — 실패한 Texture가 eReady가 되는 경로를 없앰. (PR #61)
RHI/OpenGL/GLSystem.cpp — RenderPass 전체 GL 백엔드 RenderPass 미구현 미구현
- InitializeRenderPass/SetRenderPass/UnsetRenderPass가 전부 빈 함수이고, GLRenderPass엔 GL 오브젝트 자체가 없음. FlushRenderPass/RemoveRenderPass도 없어서 m_pendingRenderPasses가 선언만 되어 있고 아무도 채우거나 비우지 않음 — RenderPass RefPtr이 있다면 그것도 영구 leak임.
#60 · World/Component.cpp:192-201 InstancedTransformComponent::UpdateRotation의 회전 미반영 해결
- 새로 들어온 rotation 인자와 현재 값을 비교하려고 eulerAngles를 뽑아놓고는, 실제 대입은 transform.rotation = glm::quat(eulerAngles) — 새 값이 아니라 방금 분해한 기존 값으로 다시 씀. 복붙하다 인자를 놓친 것으로 보이는 명백한 버그로, 같은 파일의 TransformComponent::UpdateRotation과 SceneProxy::UpdateRotation은 둘 다 맞게 새 값을 대입함. 이 오버로드는 지금 Camera 경로에서는 안 쓰이지만, Instancing 메시의 개별 회전 갱신 경로에서 조용히 no-op이 됨.
- 수정(2026.08.08): transform.rotation = glm::quat(rotation)으로 새 값을 대입. (PR #61)
버그는 아니지만 — 미구현 / 죽은 코드 4건 클릭해서 펼치기
Renderer/RenderPass/CullingPass.cpp, culling.cs.glsl CullingPass는 Resource 배선만 되어 있는 Stub 미구현
- Draw()는 상태 체크만 하고 항상 true 반환, GetResourceState()도 항상 eReady로 하드코딩. 카메라/Indirect Draw Buffer/ 가시성 Buffer/AABB Buffer를 바인딩하는 SetCommand()는 이미 있어서, 나중에 실제 Culling 로직만 끼워넣으면 됨. culling.cs.glsl은 이름과 달리 실제로는 Compute가 아니라 Vertex Shader 내용임.
Renderer/RenderPass/OverlayPass.h, .cpp OverlayPass는 완전히 빈 파일 미구현
- 두 파일 다 0바이트, Class 선언조차 없음. 시작조차 안 한 자리표시자임.
World/WorldContext.cpp — WorldCommand WorldCommandList는 사실상 미사용 흔적 코드
- struct WorldCommand {}; 정도만 있고 WorldContext::Prepare()는 그냥 // TODO임. World→Renderer 트래픽은 전부 Commander/RenderCommandList로만 흐르고 있어서, 설계 초기의 흔적으로 보임.
Asset/AssetTypes.h:47-54 Asset 상태 enum"완료(eReady)" 상태 부재 · 오타(eExpried) 설계 노트
- 실제 eAssetState는 eNone / eError / eLoaded / eExpried(오타, Expired가 맞음) / eDirty뿐임. GPU 업로드 이후 별도 상태 전이가 없어서, 준비 여부는 상태값이 아니라 texture/buffer/shader Pointer가 non-null인지로 판단함.

GitHub에 지금 열려 있는 이슈 (실시간)

불러오는 중…