이전 회사에서 일할때 자주 듣지만, 자주 이해가 안되는 표현이 있었다.

바로 "Hitch" 라는 표현인데, "지연" 이라는 뜻으로만 어렴풋이 이해하고 있었다.

 

근데 알고보니, Hitch 라는 표현이 생각보다 많은 의미를 담고 있는거같더라...

 

이번에 강연가니까 아예 "The Anatomy of a Hitch" 라는 항목이 있을 정도였으니...
(대충 한국식으로 번역하면 Hitch 뜯어먹기 정도 되려나)


Hitch란?

간단하게 요약하면, "특정 Frame 또는 특정 Pipeline Stage 가 예상 시간보다 오래 걸려서 Frame Timing이 깨지는 현상"

더 간단하게 요약하면, "어 뭔가 지연이 생긴거같은데요" 같은 느낌인것.

 

예를들어, 60Hz 환경이라고 치면 1 Frame 당 Budget은 대략 16.67ms 정도가 된다.

 

정상적인 경우라면 많이 차이가 나봤자 1~2ms 정도로 큰 차이가 없을건데, IO 지연 등의 문제로 특정 프레임에서 갑자기 50ms / 100ms 이러고 있으면 그게 Hitch 인 것이다.

 

1. The Anatomy of the a Hitch - "Hitch 뜯어보기"

 

Hitch 를 도식화 하면 다음과 같은 느낌이다.

1. 다양한 이유에서 파생된 지연이 발생하고

2. 이것이 기본 Frame Budget를 초과해버린다

3. 이를 Hitch 라 부르고

4. Hitch 로 인해 프레임 지연, Jitter 등의 파생 문제들이 발생한다.

 

 

 

좀 더 자세히 설명하자.

(너무너무 다양한) Hitch의 원인말고, Hitch의 여파에 대해 언급하면

 

1. Dropped Frames (=Frame Miss)

- 프레임을 정해진 Budget 안에 만들지 못했다.

 

대체로 이런 경우, 새로운 프레임이 아직 생성이 되지 않아서 "이전 프레임을 더 오래 표시" -> "화면이 멈춘것처럼 보임".

혹은, 흔히 "버벅임", "끊김" 같은 느낌이 들게 된다.

 

2. Recovery (->과한 Deltatime 여파)

- Hitch로 인한 Deltatime 과부하, 그로 인한 Simulation Time Problem

 

게임 업데이트 기준은 Deltatime 이다. 대체로 Deltatime은 Frame Rate를 따라가는 편이지만(항상 같은건 아님), Hitch 등이 발생한 경우에는 Deltatime이 "길어지는 현상" 이 일어난다.

 

그럼 무슨 일이 일어나는가, 대체로 이동 계산을 "속도 x Deltatime" 을 1틱마다 움직이게 하는데, 1틱의 Deltatime이 5배로 커졌다 하면 이동도 갑자기 5배 멀리 움직이는 이른바 "순간이동 현상"이 발생하는것.

 

그래서 요즘 게이머들이 흔히 "트레이서 현상" 이라고 부르는게 바로 이거인것... 데자뷰 느껴본적 있어? x N

 

 

3. Pipeline Refill

Hitch로 인해 특정 스레드가 지연이 걸리면, 그 후속 스레드들도 죄다 지연이 걸린다.

 

그도 그럴게... 아무리 언리얼 파이프라인상 각 스레드가 병렬로 돌아가서 여러 단계의 프레임이 동시에 연산된다지만, 각 N번째 프레임은 GT -> RT -> RHI -> GPU 로 이어지는 순서가 정해져 있다. 

근데 여기서 하나 지연이 크게 걸리면? 그 후속 작업들도 전부 지연이 걸리겠지? 연대책임인 것이다

 

(조금 정확하지 않을 수 있음)

 

대충 이런 느낌.

 

한번 지연이 씨게 걸리면 그 프레임만 걸리는게 아니라 나머지도 쭉쭉 지연되는것...

그리고, 지연이 걸리면 "복구"를 해야하니까 복구를 위한 작업도 파바박 이루어지고... 그러면서 프레임 재생이 개판 오분전이 되는것이다.

 

이런 상황을 대응하기 위해 발생된 "타이밍 변동성"가 바로 Jitter 이다

 

4. Jitter?

이제 Jitter를 설명할 차례. Jitter를 번역하면 많은 뜻이 나오는데, 여기서는 간단하게 "지연현상" 의 의미로 설명하겠다.

 

Jitter는 원인이 아니라 지연의 결과고, 우리가 흔히 눈에 보게 되는 지연 현상도 "Jitter" 로 보이는것이다.

 

대략 이런 흐름인 셈.

 

밀려버린 Frame Time으로 인한 지연을 대응하기 위해 발생하는 현상... 인 것, 그래서 Hitch가 한 프레임에서만 발생했다고 Jitter도 한 프레임만 발생한다는걸 보장할 수 없는게 이런 이유인 것이다. (실제 Hitch 가 단 1프레임이라도 최악을 상정한다면 거의 1초 이상 Jitter 현상이 발생할수도 있는것이다.)

 

 

'게임 개발 > Unreal' 카테고리의 다른 글

Unreal Render Pipeline... (1)  (0) 2026.08.21
MetalFX For Unreal - 개발일지 4  (0) 2026.06.29
MetalFX For Unreal - 개발일지 3  (0) 2026.05.20
MetalFX For Unreal - 개발일지 2  (0) 2026.05.13
MetalFX For Unreal - 개발일지 1  (0) 2026.05.07

얼마전에 면접을 봤었는데, 랜더링 파이프라인에 관한 깊은 얘기가 많이 나왔었다.

 

물론 대차게 망해버렸고, "이제는 이런것도 묻는구나" 라는 생각이 들더라...

 

해서, 이왕 이렇게 알게 된거, 제대로 정리하자! 싶어서 정리 문서로 남길까 한다.

 

 


 

우선 여기서 설명하는 랜더링 파이프라인은 "보편적인 랜더링 파이프라인" 을 의미하는것이 아닌, 언리얼 시스템 플로우에 가까운 무언가 긴 하다.

 

즉 "언리얼에서는 이 프레임을 어떤 순서로 그리게 되는가" 라는 뜻.

 

일단, 이미지부터 한장 보자.

 

(살짝 정확하지 않을 수 있음에 주의)

 

이번 면접때 크게 당한 부분이자, 마침 다음날 언리얼 페스트에서 들은 가장 중요한 내용같아서 따로 빼둔다.

 

요약하면, "각 프레임을 그리기 위한 일들은 같은 시점에 일어나지않다" / "각 프레임을 그리기 위한 일은 병행 처리한다"

이렇게 2개인것 같다.

 

실제로 N번째 프레임을 화면에 그리고 있을때, (진행 속도에 따라) Game Thread 는 N+2 프레임을 준비하고 있고 Render Thread는 N+1~N+2, RHI Thread는 N+1을 준비하는 느낌.

 

대체로 디스플레이를 N으로 두면 (Display+GPU를 하나로 보는 편인데)

RHI는 N+1

Render Thread는 N+1~N+2

Game Thread는 N+2 정도가 나오는 편이다.

 

그리고, 각 프레임 진행의 간격을 맞추기 위해 중간중간 Wait가 들어가는데, 이게 들어가지 않으면 "내 화면에서 보이는건 N번째인데 실제로 입력이 들어가는건 N+3~N+4" 이 되어버릴수도 있는 처참한 상황이 되어버린다.

극단적으로, 이게 60 FPS 환경이라면 N+3 지연 -> 약 64ms 이상 지연이라는 소리가 되어버린다. (1초당 프레임 수가 줄어들면 줄어들수록 지연은 더 길어지고...)

 

그리고 각 프레임의 진행 간격을 최소화 시키면 그게 Low Latency 가 되는거고...

 

1. 랜더링 파이프라인의 구조

언리얼 랜더링 파이프라인은 (나누는 방식에 따라)

Game Thread

Render Thread

RHI Thread

GPU Task Thread

Display

로 나눌 수 있다.

 

Game Thread는 말 그대로 Game 관련 이벤트 등이 발생하는 스레드, World 구성 및 Tick, Input 이 발생하는 "우리가 일반적으로 알고 있는 게임 스레드".

Render Thread는 Game World를 렌더링 가능한 Scene으로 해석하고, 이번 프레임의 Rendering Operations를 구성하는 Renderer Frontend. 즉, "이제 뭘 그릴까요?" 하는 부분인 셈.

Render Thread에서는 Scene representation, Visibility&Culling, View, Mesh drawing, Material/shader selection, RDG Pass와 Resource dependency 구성, RHI command 생성 등의 작업을 수행한다.

RHI Thread는 Render Thread에서 생성된 RHI Command를 처리하고, 플랫폼별 Graphics API(DX12/Vulkan/Metal)에 대응하는 backend 작업을 수행한다.

Epic 가이드에서도 Render Thread를 frontend, RHI Thread를 backend execution/translation 단계로 설명한다. 

CPU 단의 랜더링에서는 Render Thread - Front / RHI Thread - Back 인 셈.

 

Render Thread 와 RHI Thread가 분리되어있는 핵심 이유는 "Render frontend 작업 + Graphics backend 작업" 을 병행 처리하기 위함 인 것이다.

 

2. 스레드 간 데이터 전송

 

이렇게 다양한 스레드가 있는데, 각 스레드별로 데이터 전송은 어떻게 하는가.

내가 이걸 면접때 "락 걸고 처리..." 라고 얘기했는데, 대체로 틀린 표현이다. (아예 틀린건 아님. 락을 걸고 주고받을수도 있긴 하니까)

 

락을 걸고 처리를 할 수 있으나 안 하는 이유 - 락은 공짜냐?

락을 걸고 작업하는순간 (상황에 따라) 작업의 비용이 너무너무너무너무 비싸져서, 언리얼에선 대체로 각각의 데이터를 별개로 처리하는 방식을 택한다.

 

Game Thread : UPrimitiveComponent
       │
       │ Render representation 생성
      ▼
Render Thread : FPrimitiveSceneProxy

이런 식인것. Shared mutable state를 최소화하고, 각 Thread가 자기 representation을 소유한 채 command/message를 통해 상태 변화를 전달하여 처리.

 

이러는 이유는 "각 스레드가 병행 처리 될 수 있음" 이 가장 큰 이슈이다. 만일 데이터를 공유한다 치자. Game Thread N 에서 존재하던 오브젝트 A가 N+1 에 완전히 사라진다고 할때, 아직 Render Thread N에서 A 에 접근하려는 순간 바로 잘못된 포인터 접근으로 크래시가 나버릴것이다.

 

모든 시스템을 thread-safe shared object로 만드는 대신, 각 실행 영역에 ownership을 부여하고 필요한 데이터를 command/task/message 형태로 넘긴다.


이게 핵심인것.

 

그래서 이런 데이터들을 (예시로) RDG Pass에서는 다음과 같이 처리한다.

 

SomeGTFunction()
{
    Data = ...;

    ENQUEUE_RENDER_COMMAND(UpdateSomething)(
        [CopiedData = Data](FRHICommandListImmediate& RHICmdList)
        {
            // 나중에 Render Thread에서 수행
        });
}

 

ENQUEUE_RENDER_COMMAND 라는걸 호출, 필요한 명령 / 데이터 전송을 호출하는 셈이다.

 

 

MetalFX For Unreal - 개발일지 5 : 사실 MetalFX 업스케일러는 2개에요


 

두개지요

부제가 모든걸 설명하는 이번 회고입니다.

 

사실 최근 작업의 하이라이트는 이번 회고에 포함된 0.3까지가 아니라 그 이후~오늘까지 진행한 작업들이 굵직굵직해서 거기를 얘기해야할거같은데

 

일단 그 전에 정리할건 하고 가야죠

 

 

 

그래서 무슨 일이 있었나요?

1. 업스케일러가 두개라구요?

 

사실 업스케일러는 크게 2개가 있습니다.

우리가 흔히 아는 "Temporal Upscaling", 시간적 업스케일링이라 부르는 최신 기술.

 

그리고, 그 전에 쓰이던 "Spatial Upscaling", 공간적 업스케일링이라 부르는 기존 업스케일링 기술

 

기존 업스케일링 기법은 현재 픽셀 데이터를 기반으로 화질 상승을 이루어주는 기법으로, 종종 보이는 "이미지를 2배로 키워주는 기능" 과 같은 일반 이미지 업스케일러를 게임 랜더링환경에 맞게 발전시킨 버전으로 볼 수 있다.

다만 이는 큰 문제가 있는데, 기존 업스케일링 기법인 공간적 업스케일링은 "현재 프레임"만 활용하다보니, 가볍긴 하나 성능도 떨어지고 빠르게 변하는 환경에서는 품질이 떨어질 수 있다는 문제가 있다.

 

해서 나온게 시간적 업스케일링인 "Temporal Upscaling". 물론, 여기서 얘기할건 Temporal이 아니라 Spatial 이긴 하다.

주로 공개된 게임용 Spatial Upscaler는 Nvidia 계열로 NIS (Nvidia Image Scaling), AMD 계열로 FSR 1.0 2개를 꼽는 편.

 

물론 둘다 최신 Temporal / AI 기반 Neural Upscaling으로 가면 얘기가 많이 다르긴 합니다.

 

 

 

여튼 그래서, 최신 업스케일링 기법들을 대충 요약하면... 요런 느낌이긴 한데요.

 

 

여기에 빠져있는, 지금 작업중인 "MetalFX"의 경우에는 내부적으로 Spatial, Temporal을 둘다 지원합니다.

다만, 지원 타겟이 달라요.

 

Spatial은 "MetalFX가 탑재만 가능하면 대부분 실행 가능" 이나, Temporal은 Spatial보다는 좀 더 높은 사양을 요구합니다.
테스트 결과로는 iPhone 15 (A17칩) 전후로 유용하게 사용가능한것으로 판단했는데, 실제로는 좀 다른거 같더라구요.

 

여튼, MetalFX Temporal Upscaling을 쓰지 못하는 경우에도 Spatial이라도 사용가능하도록, (언리얼의 경우라면, 기존 모바일 업스케일러인 내장 FSR1을 대체할 수 있도록) 추가 작업을 진행했습니다.

 

 

2. 그래서 뭘 했어요?

사실, 아직 Spatial Upscaler를 완성한건 아니에요. 우선 Temporal Upscaler를 완성시키는게 목표라 공간적 업스케일러는 조금 후순위긴 했어요.

 

솔직히, 성능 좋은걸 먼저해놔야 "오 때깔좋네" 하지... 보기좋은게 먹기도 좋다고... 그쵸?

 

다만, 작업하다 보니 문득 든 생각.

 

"어 이거 지금 분리 안해두면 Spatial Upscaler 작업할때 두번 작업할거같은데"

 

'어 이거 큰일날거같은데'

그래서 0.22 태그 달아서 기존 작업 상태 확정지은 김에 바로 분리작업 진행했습니다.

 

기존에는 MetalFX 업스케일러에 직접 명령을 내리는 UpscalerCore라는 클래스가 TemporalUpscaler 전용이었거든요.

 

신규 구조로는 UpscalerCore를 상속받아서 Temporal / Spatial Upscaler 2개의 클래스가 존재하도록 변경했습니다.

 

공통적으로 쓰는 유틸 함수 같은건 Core 부모에 넣고, 각 업스케일러 타입별로 자체 기능들을 담는 느낌인거죠.

 

 

 

그래서 사실상 해당 작업을 위주로 진행했습니다. 작업 자체는 별거 안한거 같은데, 생각보다 시간이 오래 걸렸습니다. 도중에 면접도 좀 볼 일 있었고 떨어졌지만 이런저런 일들이 있었네요.

 

3. 겸사겸사 확인한 일

그리고, Mac에서도 Metal Debug HUD를 열 수 있더라구요? 빌드 세팅을 변경해서 해당 설정을 띄운채로 테스트결과를 확보해서 첫번째 벤치마크 결과를 띄웠습니다.

 

대충 요런 느낌

https://github.com/LocketGoma/MetalFX/blob/main/Docs/PerformanceValidation.md

 

1차 벤치마크는 요기.

 

 

이게 최신 데이터 (24일까지 작업한 내용)을 토대로는 조금 안 맞는 부분이 있어서, 다음 벤치마크는 좀 더 디테일한 데이터를 가져올거 같긴 해요.

 

그리고... 이거 작업하면서 발견한 재밌는 이슈가 있는데, 이건 다음 문서 (지금까지 쓴 회고글 중에서 제일 분량 크고 데이터도 많을 예정) 에서 적도록 하겠습니다.

 

아마 바로 이어갈거 같아요.

 

 

안녕하세요. 곰입니다.

 

결론부터 얘기하면 아직도 백수입니다

안타까운 청년...

생각보다 면접이 잘 안잡히네요... 업계가 많이 춥긴 한가봅니다.

 

아직 이직처도 안 정해진 상태에서 실업급여도 1달정도밖에 안 남아서 좀 초조한 상태가 됐는데

 

그렇다고 제가 할 수 있는게 있나요 하던 공부나 계속 해야지...

 

 

 

그나마 다행인건 개인 프로젝트인 MetalFX 플러그인 작업이 순항중이고, 다이어트도 지난 회고대비 -10kg 빠져서 잘 되고 있다는점.....?

 

하반기는 좀 좋은일만 있으면 좋겠다 싶었는데 아직은 좋은소식이 없네요 습.

 

MetalFX 개발일지나 써야지

'신변잡기 > 일상' 카테고리의 다른 글

근황  (0) 2026.04.07
12월부터 26년 계획  (0) 2025.12.01
트위터 이런날은 첨보네  (0) 2025.03.11
To do.  (0) 2024.07.22
묵혀뒀던 프로젝트들 좀 살려야겠어요  (0) 2024.06.11

MetalFX For Unreal 개발일지 4 : 이제 슬슬 상용 프로젝트에도 쓸 수 있지 않을까?

 


https://locketgoma.tistory.com/106

 

MetalFX For Unreal - 개발일지 3

MetalFX For Unreal 개발일지 3 : 그 플러그인은 어쩌다 얼레벌레를 벗어나게 되었는가 뭐, 보신분이 있으실진 모르겠는데, 개발일지 2를 올린 뒤 드디어 "0.1 릴리즈" 를 올렸습니다. https://github.com/Locke

locketgoma.tistory.com

 

개발일지 3을 올린지 대충 1달이 지났습니다.

 

그동안 어찌어찌 Resolution 버그도 고쳤고, 기타 안정화 작업을 꽤 많이 수행했습니다.

 

그래서 테스트 환경에서는 이제 안터지더라구요! 

 

그리고 이제, 이 글 이후로 대망의 ver 0.2가 올라갈 예정입니다.

 

드디어 정량화된 수치가 나왔거든요.

 

그래서 무슨 일이 있었나요?

1. 일그러지는거 뭐였는데요?

 

3편에서는 텍스쳐 터지고, resolution 일그러짐 문제가 있었다고 얘기했었죠?

 

그중에, res 문제가 좀 많이 골치아팠는데... 이건 좀 희안하게 고쳤습니다.

 

사실대로 얘기하자면... Codex 도움을 좀 받았었습니다.

 

다만, 원인 추측 자체는 직접 하긴 했는데... 역시나 결론은 "인풋사이즈가 두개다" 이었습니다.

 

정확히는, MetalFX descriptor가 쓰는 입력 texture 크기실제로 화면에 유효한 content rect 크기가 다를 수 있다.

라는 거였죠.

즉, 할당된 "텍스쳐 크기"가 실제 유효 영역보다 더 클 수 있다는것을 놓친것이 가장 큰 문제였습니다.

이거 진짜에요?

 

 

그래서 보완을 어떻게 했느냐... 보완 자체는 별거 없었습니다. 그냥...

실제 렌더 사이즈가 RDG Allocation 크기보다 작으면 실제 렌더 사이즈를 사용한다.

 

즉 descriptor 입력 크기를 texture allocation으로 잡더라도, allocation이 content보다 크면 content 크기로 내려서 MetalFX가 잘못된 입력 스케일을 보지 않도록 보완, 일그러짐을 잡았습니다.

 

이 작업이 완료된 이후, 드디어 MetalFX On / Off 를 반복하더라도 "화면 출력 크기가 변하는 버그" 가 사라졌습니다.

 

2. 방치해둔 Motion Vector / Jitter 임시 처리

Temporal Upscaler에 필요한 데이터 중, Motion Vector (or Velocity Texture) 라는것이 있습니다.

간단히 설명하면, "이전 프레임과 현재 프레임간의 이동" 을 기록한 데이터인데, 지금 작업중인 업스케일러에 Temporal 이라는 표현에서도 알 수 있다시피. "시간적 변화" 의 정보가 필요합니다.

근데 이걸 처음에는 MetalFX를 위한 Motion Vector가 제대로 준비 안 된채로 사용하다보니 업스케일러의 최종 결과물이 불안정해서 "퍼포먼스 우선"(= 스크린 퍼센트를 떨굴수록...) 상황에서 화면이 달달달 떨리는 문제가 있었습니다.

(진짜 이런 느낌이었음)

문제를 확인해보니... Motion Vector 데이터가 미완성인 상태로 적용하려고 하다보니 떨림이 발생하는것이었고, 이 사항은 우선 Motion Vector Scale을 0으로 밀어버리는(= 영향을 주지 않게) 방법 으로 임시대응하긴 했습니다.

 

 

추가로, Jitter라는 데이터도 있는데, 이 값 역시 MetalFX Temporal Upscaler가 이전 프레임과 현재 프레임을 정렬하는 데 필요한 값. 이 데이터는 TemporalUpscaler Input에 포함되는데, 이걸 MetalFX용으로 보정할 수 있는 로직을 추가, 적당히 필요한 조건대로 컨버팅 한 뒤 Jitter를 쓸 수 있는 상황 (=Upscaler Encode가 가능할때)에 전달해서 작업 처리를 했습니다.

 

여튼 이렇게, 큰 벽을 하나 더 넘었습니다.

 

 

 

3. 진짜 최종 모바일 테스트.ipa

그리고 이제, 이게 "진짜 제대로 돌아가는지" 봐야하는 시간.

다만, MetalFX는 (나름) 최신 기술이다보니 Apple 기기중에서도 꽤 최근 기기들에서 잘 돌아갑니다.

그리고 제가 가지고 있는 개발용 아이폰은....... iPhone XR.......... 꽤 옛날 폰이죠.

 

의미있는 테스트를 위해서는 iPhone 15 이상이 필요한데, 이걸 지금 상황에서 구할수는 없으니, 주변에서 구할수 있는 "그나마 최신 iOS 기기"를 하나 구했습니다.

 

그리고 돌려보려고 하니... 어라. 모바일 패스에서 문제가?

라고 하기엔 이미 작업했던거라 반영 안된거만 고치고 빠르게 진행했습니다.

 

그 결과...

 

 

 

드디어... 드디어 Metal Debug HUD에 MetalFX가 잡혔습니다!!!!

 

히히 신난다

지금까지의 노력이 헛되지 않았다는 증거가 드디어 나왔습니다...

 

수치는 50%로 테스트한거라 정상적으로 나온게 맞구요.

 

 

4. 그리고 기타등등 안정화 작업, 폴리싱 등등

이제 테스트 할 항목이 많아지다보니, 테스트를 위한 Debug Display Log를 찍기 시작했습니다.

 

이제 실시간 설명이 필요한 시간

특히 업스케일러는 "스케일을 줄인다" 라는 작업이 수반되는 편이다 보니, 실제 input rect가 어떤지를 꼭 알아야 하더라구요.

 

해당 데이터는 꽤 중요한 사항으로 생각되어서, 출력에 넣었습니다.

 


이제 "iOS에서도 확인은 가능하다" 라는 단계에 도달해서, 슬슬 상용 프로젝트에서도 쓸 수 있지 않을까? 라는 생각이 들기 시작했습니다.

 

근데... 일단 지금 회사 나와있는 마당에 재 취업을 해야 다시 써먹든지 하죠...

 

제가 이 플러그인을 가지고 취업을 할 수 있을까요?

 

일단... 아직 남은게 많으니까 작업은 계속됩니다.

MetalFX For Unreal 개발일지 3 : 그 플러그인은 어쩌다 얼레벌레를 벗어나게 되었는가

 


 

뭐, 보신분이 있으실진 모르겠는데, 개발일지 2를 올린 뒤 드디어 "0.1 릴리즈" 를 올렸습니다.

 

https://github.com/LocketGoma/MetalFX/releases/tag/0.11

 

Release MetalFX Alpha 0.11 (Edited) · LocketGoma/MetalFX

MetalFX For Unreal Plugin Alpha Release v0.1 Verified Features MetalFX integration Confirmed MetalFX Temporal Upscaler startup Packaging is supported, and testing in packaged builds is possible W...

github.com

 

이 글 기준으로는 0.11이네요.

 

이전 개발일지 (2편) 이후로 뭔가 많이 많이 수정하고, 안정화도 좀 해서

"오 이제 좀 돌릴만한데?" 싶어서 바로 릴리즈를 올린건데... 뭐. 안터지는거같으니 된거 아닐까요?

 

 

 

 

그래서 뭘 했나요?

2편에서는 "Motion Texture" 펑크 문제가 제일 큰 문제였습니다.

 

Command Buffer 문제는 사실 1편쯤에도 어느정도 해결된거였고, 1편쯤에서는 "일단 붙여보자", 2편쯤에는 "어 이거 될거같은데" 같은 느낌이었죠.

 

근데 하다보니... 여전히 몇가지 문제가 더 나왔습니다.

 

1. 텍스쳐가 자꾸 터지는데요?

네. 텍스쳐가 자꾸 터졌습니다.

 

말 그대로, 텍스쳐 객체가 "터져요"

 

와 텍스쳐가 모였더니 터져요

MetalFX도 결국 랜더링 파이프라인을 건드는 무언가다 보니, 랜더링을 위한 데이터들을 가지고 있어야합니다.

그 중, 가장 중요하고 필수적인것이 바로 "텍스쳐" 들인데... 얘들도 결국 데이터란 말이죠?

언리얼, C++ 이니까 당연히 C++ 스타일로 "객체"로 저장되어 "포인터"로 접근하게 될건데, 지금 내가 접근한 텍스쳐가 언제까지 유효할지 알 수가 없는겁니다.

 

그래서 언제는 1프레임 진행하다 터지고, 언제는 3초정도 있다가 터지고... 난리도 아니더라구요.

 

 

 

그으으으래서 우에엥 DLSS에몽~ 하면서 DLSS 로직을 찾아봤습니다.

 

RHICmdList.EnqueueLambda(
	[LocalNGXRHIExtensions, DLSSArguments, DLSSState](FRHICommandListImmediate& Cmd) mutable
	{
		const uint32 FeatureCreationNode = CVarNGXDLSSFeatureCreationNode.GetValueOnRenderThread();
		const uint32 FeatureVisibilityMask = CVarNGXDLSSFeatureVisibilityMask.GetValueOnRenderThread();

		DLSSArguments.GPUNode = FeatureCreationNode == -1 ? Cmd.GetGPUMask().ToIndex() : FMath::Clamp(FeatureCreationNode, 0u, GNumExplicitGPUsForRendering - 1);
		DLSSArguments.GPUVisibility = FeatureVisibilityMask == -1 ? Cmd.GetGPUMask().GetNative() : (Cmd.GetGPUMask().All().GetNative() & FeatureVisibilityMask) ;
		LocalNGXRHIExtensions->ExecuteDLSS(Cmd, DLSSArguments, DLSSState);
	});

 

CommandList에 "EnqueueLambda" 라는게 있더라구요?

 

그으러니까... 작업 스텝을 요약하면, "현재 활성화 된 커맨드버퍼 리스트에 작업을 넣어두고, 차례가 오면 예약된 람다함수를 수행한다" 로 흘러가는거였습니다.

 

이러면 중간에 끼워넣을 필요도, 지금 커맨드버퍼가 마지막인지 검사할 필요도 없어지는거죠. 물론! 텍스쳐의 수명 문제는 밑에서 언급했다시피 여전히 발목을 잡았지만

 

텍스쳐 문제도 결국엔 이쪽에서 해결되는게 몇개 있었습니다. 가장 큰 건 "실행 시점과 텍스쳐 전달 시점의 일치" 가 확보되었다는게 중요한 지점인거 같아요.

 

좀 더 정확히 설명하면 ::
EnqueueLambda를 쓰면 RDG pass 구성 시점이 아니라 실제 RHI command 실행 단계에서 로직을 태울 수 있었습니다. 덕분에 current Metal command buffer를 잡는 문제는 많이 정리됐습니다. 다만 이게 모든 리소스 수명 문제를 자동으로 해결해주는 건 아니고, RDG Texture에서 RHI/Metal Texture를 꺼내는 작업은 AddPass 본문에서 처리하고, EnqueueLambda 내부에서는 이미 준비된 native texture와 command buffer만 쓰도록 역할을 분리해야 했습니다.

 

덕분에, "MetalFX Encode"가 정상적으로 실행되고, 리소스 Release까지 도달했습니다!

 

드디어... 긴 터널의 끝이 보이는구나...

 

물론! 이걸로 끝이 난건 아니었습니다.

 

2. 사용한 포인터는 제자리에

이게 결국 포인터로 관리되잖아요?

그러면 포인터가 유효할때만 써야하는데, 위에서 언급한것처럼 랜덤하게 와장창 터지는 문제가 있었습니다.

특히, 1번에서 EnqueueLambda로 꽤 많은 문제가 해결된 이후 발견된 문제인데, MetalFX 로직을 벗어나면서 텍스쳐 릴리즈될때 터지는 문제가 발견되었습니다.

 

갑자기 텍스쳐가 또 왜 터지냐? 이건 지금 부제를 보면 알겠지만... 잘못된 포인터 접근으로 터지는거였습니다.

 

간단히 요약하면,

1. 텍스쳐를 받아옴

2. 텍스쳐를 변환함

3. 텍스쳐를 Encode로 옮김

4. 텍스쳐를 사용함

5. 텍스쳐를 해제함

 

여기서 문제가 되는 지점이 바로 3번이었습니다. UE Texture를 Metal Texture로 바꾸고, 이걸 관리하고자 관리용 구조체를 만들었는데, C++ 로직상으로는, 동일한 클래스의 다른 객체로 데이터를 "옮김" 또한 "생성" 이란 말이죠? (정확히는 대입 연산자 / 복사 생성자 얘기지만 아무튼) 

 

그러다보니, "새로운 데이터를 생성하는 시점"에 잘못된 재활용을 막기위해 추가된 "강제 릴리즈 로직"이 여기서 사고를 치는겁니다...

 

아직 안썼는데!

 

저 아직 해제될때가 아닌거같은데요...

 

해서 어떻게 해결했느냐... 뭐, 별거 없습니다. 이건 AI 도움을 좀 받았는데, "현재 커맨드버퍼가 사라질때 정리를 하자" 라는 아이디어로 로직을 구성해서, 해당 텍스쳐가 진짜 사용되었을때 (혹은 현재 텍스쳐가 사용되지 않고 완전히 다음 프레임이 재생되어야할때) 소멸하도록 처리를 했습니다.

대충 deferred release 한 셈이죠. 네.

 

짜잔 문제 해결!

 

덤으로, 이건 복사/이동 생성 관련 정책이다보니, 생성자/릴리즈 플래그등을 정리해서 문제 재발을 방지했습니다.

 

 

 

3. 그리고 안정화를 위한 기타등등

그리고 추가로 몇가지를 좀 더 했습니다.

 

첫번째로, 임의의 포멧으로 자동생성되던 Output Texture의 포멧을 "Input Scene Color Texture" 포멧으로 일치. (실제로는 다른걸 써야할수도 있지만 "일단은" 그렇게 적용했습니다.)

 

두번째로, 이게... 엔진에 물려있는 코드다보니, 일부 로직이 "엔진 초기화 시점" 에 기동이 되는겁니다.

내가 원하는건 "게임이 켜질때" 인데, 게임 실행이 아니라 "패키징" 중에도 Module Init이 되버리는 바람에... Init 시점에 넣어둔 로직이 와장창 터지는 사고가.. 있었습니다.

물론 지금은 Commandlet / Cook 경로에서는 안 돌고, "실행중"(=Editor/Game 상태) 에만 돌도록 보완했구요.

 

뭐 그 외에 사소한 실수 엔진코드로 복사되어야하는 코드 자체는 업데이트해놓고 Git Repo에는 업데이트를 안했다던가 들이 몇개 있었는데, 적절하게 고치고 드디어 릴리즈를! 했습니다!

 


 

진짜... 진짜 많이 힘들었지만, 그래도 거의 3개월간 고생한 끝에 릴리즈를 띄우니까 기분이 좋더라구요.

 

이제는 남은 문제들을 해결하러 갈 시간인거 같습니다.

 

 

 

다음 이야기를 기대해주세요! (미정)

 

 

 

P.S : 남아있는 문제들

1. 스케일이 미묘하게 틀린 문제

2. Spatial Upscaler

3. 옵션 확충, "품질 선택기능"

4. 안정화... 기타등등

 

아직 갈길이 멉니다.

'게임 개발 > Unreal' 카테고리의 다른 글

Unreal Render Pipeline... (1)  (0) 2026.08.21
MetalFX For Unreal - 개발일지 4  (0) 2026.06.29
MetalFX For Unreal - 개발일지 2  (0) 2026.05.13
MetalFX For Unreal - 개발일지 1  (0) 2026.05.07
언리얼 포인터 관련 정리  (0) 2026.04.04

네 본격적인 개발일지입니다.

 

저번 1차 글에서 좀 많은 일들이 있었어요.

 

일단! 핵심만 얘기하면 이제 MetalFX 인코딩은 됩니다!

 

그다음에 메모리 해제하다 터져서 그렇지...

 


내용 정리는 AI에게 좀 맡기고, 그거 참고해서 쓰고 있습니다.

 

아래 정보는 틀린게 많을 수 있습니다. 참고만 해주시고, 틀린 정보는 언제든 제보 바랍니다.

 

1. 그래서 뭐했음?

 

일단. MetalFX를 비롯한 서드파티 업스케일러에는 필요한 사항들이 몇개 있습니다.

 

우선 RHI 정보 등을 담고 있는 CommandBuffer, 총 4종의 텍스쳐 (Scene Color, Depth, Velocity (이전씬과의 픽셀 이동 차이 등을 보여주는걸로 알고 있어요), Output), 그리고... 이번에 놓쳤던 "텍스쳐 포멧" 이란 정보가...

 

 

CommandBuffer 작업기

DLSS 구조를 참고해서 보면, 업스케일러 코어가 필요로 하는 CommandBuffer 정보는 랜더링 파이프라인의 AddPass 로직을 따라서 제공을 해줘야 합니다.

(실제로 보면, Postprocess 로직에서 서드파티 업스케일러 분기가 따로 있긴한데 이건 너무 딥한 내용이라 생략)

근데, 회사에서 작업할때까지만 해도 CommandBuffer를 제대로 핸들링을 못해서 어거지로 테스트해보고자

 

현재 작동중인 RHI 정보에서 Command Queue 생성 -> Commnad Queue에서 Command Buffer 생성 -> 생성된 Command Buffer 사용

 

이라는 좀 어처구니 없는 (= 실제 언리얼 상 랜더링 파이프라인과는 별개인) 로직으로 작동하고 있었습니다.

 

이 작업은 딱 한가지. "MetalFX 코어에 인코딩 정보가 넘어가긴 하느냐" 를 확인 할 수는 있었습니다.

실제로도 회사 게임에서 테스트할때는 (비록 해상도가 망가지는 문제가 있긴 했어도) 랜더링 & 출력이 잘 되긴 했었거든요.

 

근데 딱 보면 눈치채셨을수도 있는데, 이건 "정상적인 랜더링 파이프라인이 아닙니다". 즉 언젠가는 고쳐야 할 로직이었던거죠.

 

개선은 다음과 같이

1. MetalCpp / MetalRHI 로직에서 언리얼 커맨드버퍼 -> Metal 커맨드 버퍼 변환 로직을 찾음

2. 해당 방식을 Cpp Wrapper / Objective-C++ 네이티브로 분리해서 작업

 

으로, 의외로 간단하게 해결했습니다. 최신 엔진이라 그런건지, 아니면 그땐 못찾았던건지 금방 나와서 

"습... 좀 일찍 알아챘으면 좋았을걸" 싶었습니다.

 

그녀석은 사천왕중 최약체였을 뿐이야!

 

다만, 여기서 문제가 끝이 아니었습니다. 여기서 끝났으면 내가 이런 긴 회고글을 쓰고있을리가 없지

커맨드 버퍼라는건, 대략... 랜더링을 위한 커맨드를 모아둔 저장공간이란 셈인데, AddPass 로직에선 이 커맨드버퍼를 모아둔 "CmdList" 형태로 전달받습니다.

 

근데, 몰랐던 사실인데 업스케일러는 "랜더링 파이프라인의 마지막 커맨드 버퍼" 를 사용합니다.

오죽하면 함수중에 IsBottomOfPipe() 라는 로직으로 마지막인지 검사를 할까요.

 

또한, 랜더링 파이프라인의 시점에 따라, CmdList에서 뽑아둔 "Context" 가 할당해제되어있는 문제도 있었습니다.

 

그러니까... 실제로 해결해야했던 문제는

 

1. 실제 랜더링 파이프라인에서 쓰는 커맨드 버퍼 리스트를 가져와서

2. 그중의 마지막 커맨드 버퍼가

3. 유효한 커맨드 버퍼이면서

4. 가장 마지막 커맨드 버퍼여야한다

 

라는 4연타를 맞아버렸습니다.

 

돌겠네 진짜

까지 겪고 머리아파서 끙끙대다가, 잘까... 하려던 와중, 문득 머리속에 뭔가 지나치는 생각.

"근데 이거 DLSS 는 어떻게 해결함?"

 

해서 DLSS에서 해당 로직을 구현한걸 봤더니 글쎄, CmdList에 존재하는 EnqueueLambda 함수를 호출해서 해결 해버리는거 있죠.

 

그러니까... 그냥 커맨드버퍼 리스트에다가 마지막으로 집어넣으면 = 그게 마지막 아님? 같은 느낌인거지

 

오... 싶어서 해당 로직을 참고해서 바꿨더니, 2,3,4 문제가 한번에 싹 해결된겁니다!

 

옳다구나 해서 해당 로직을 세이브했습니다. 이렇게 커맨드 버퍼 문제 해결!

 

 

하기전에... 이런 문제가 있었습니다.

 

가깝지만 먼 그대... MetalRHI 모듈...

자 일단, 위의 커맨드 버퍼 얘기에서 하나 짚고 갈 것이 있습니다.

 

"MetalCpp / MetalRHI 로직에서 언리얼 커맨드버퍼 -> Metal 커맨드 버퍼 변환 로직을 찾음" 라는 얘기. 기억나시나요?

 

이 말은 즉슨, 해당 로직은 MetalCpp / MetalRHI 에 포함된 로직인겁니다.

 

그런데 아실분이 있을지 모르겠는데, MetalCpp는 에픽에서 제공해준 로직이 아닙니다.

 

애플에서 만든 로직이죠. 그 말은 즉, 언리얼의 생태계와는 따로 노는 로직이라는 셈입니다.

 

이게 무슨소리냐.

 

"언리얼 엔진의 업데이트와는 전혀 별개의 업데이트 주기를 가질 수 있다" 라는 문제가 있죠.

 

이게 생각보다 큰 문제인게...

 

언리얼 커맨드 버퍼 -> Metal 커맨드 버퍼 변환 로직은 MetalRHI에서 사용하는 로직입니다.

그런데 이 친구. 외부로 쓸 수 있게 공개가 안되어있어요.

 

정확히는, 언리얼 구조상 외부 모듈은 전용 dllexport 매크로를 선언해줘서 사용해야하는데, 변환 로직엔 그게 전혀 안 붙어있는겁니다.

 

그리고 앞에서 얘기한 문제. 얘는 언리얼 생태계와는 별개의 물건이다.

 

그렇단 말은 즉슨, MetalFX 플러그인을 위해 해당 코드를 수정하더라도, 언젠가 덮어씌워질 수 있다는 거죠. (= 유지보수성 감소)

 

그래서 조금 고민해봤는데, 재밌는 아이디어가 나왔습니다.

 

"그냥 MetalRHI 유틸리티 클래스 추가해버리죠?"

 

MetalRHI_Api인가 하는 매크로가 이미 존재하고 있으니 매크로 사용 자체는 문제가 없고, MetalRHI와 같은 모듈에 포함되어있으면서 해당 매크로를 쓰고, MetalRHI Public 함수에 접근 가능한 유틸클래스가 있으면? 되는거 아님?

 

라는 생각으로 추가. 정말 다행스럽게도 문제없이 한번에 해결됐습니다.

 

겸사겸사 이런 엔진 레벨 코드 수정할 사항들이 좀 있어서 내부 유틸리티로 코드 업데이트하는 스크립트도 추가하긴 했습니다.

 

 

 

절대 아무것도 믿지마라... (Motion Texture 문제)

 

이렇게 커맨드 버퍼도 연결했습니다. 그리고 텍스쳐를 붙여서 MetalFX Encode를 하려고 하니까... 이런 경고가 뜨는겁니다.

 

Motion and Color should have the same width for MetalFX use

 

넵. Scene Color Texture와 Motion (=Velocity) Texture 는 같은 사이즈여야한다는거죠.

 

근데 이상하게도, 무슨 짓을 하더라도 Motion Texture가 사이즈가 1x1 으로 리턴되는 겁니다?

 

여기서 거의 반나절 이상을 썼는데, 진짜 별 짓을 다 해봤습니다 PostProcess 로직을 되짚기도 하고, 변환 로직을 검사해보고 수정하고 기타등등...

 

근데, 해답은 의외로 가까운곳에 있었습니다.

 

Motion Tex 관련 정보를 찾다가, DLSS 에서는 이 Motion Texture 를 어떻게 쓰나? 해서 봤더니... 내부 로직에 이런 주석이 떡하니 박혀있었습니다.

 

// we use InSceneDepthTexture here and not InVelocityTexture since the latter can be a 1x1 black texture check(InVelocityTexture->Desc.Extent == FIntPoint(1, 1) || InVelocityTexture->Desc.Extent == InSceneDepthTexture->Desc.Extent);

 

대충 요약하면... "종종 VelocityTexture 가 1x1 짜리 텍스쳐로 들어오니까 차라리 새로 만드는게 나을거같음." 라는 의미가 담긴 주석이었습니다.

 

네. "애초에 언리얼 랜더링 파이프라인에서 제공되는 Motion Texture 가 정상이 아닐 때가 있다." 란 거죠

 

보고도 믿기지 않는 소리

 

근데 생각을 해보니까, Motion Texture는 화면이 움직이거나 할때 생성이 될건데, 안 움직이면... 굳이 만들 필요가 없잖아요? 그래서 그런지 사실 그럴리가 없겠지만 효율성을 위해서 특정 상황에서는 1x1 텍스쳐만으로 해결이 될테니... 그때도 전체 로직을 쓸 필요는 없을테니까...

그래서 이런 로직이 생긴게 아닐까...? 라는 추측만 있었고... 이번에도 저 멀리 미국에 있을 DLSS 개발자분들에게 마음속으로 감사인사를 올리며 로직을 참고했습니다...

 

잘 되더라구요...

 

 

 

내 반나절.... 내 시간 돌려내라 에픽놈들아...

 

 

하여간. 그 외에도 여러 일들이 있었는데, 지금 이 글을 쓰고 있는 시점에서는 "MetalFX Encode 성공" 이 되는 상황입니다.

네, 전체 파이프라인이 멀쩡하단건 아닌데, Encode 이후 정리되는 로직을 제외하고는 전부 공정이 잘 돌아간단 뜻이에요.

 

 

 

드디어 이 험난한 MetalFX 플러그인 릴리즈를 향해... 진짜 얼마 안남은거 같습니다.

 

진짜 빠르게 진도 뽑으면 이번달 안에 0.1 릴리즈는 할 수 있을거 같아요.

 

어차피 이 플러그인은 단시간에 안정화 하긴 힘들거 같지만, 최소한 연구목적으로는 충분히 기능할거 같습니다.

 

다음에는... 0.1 찍고 오도록 하죠!

 

 

최근 날백수가 되버리고, 개인 프로젝트를 2개를 운영하고 있습니다.

 

하나는 실제 게임 출시를 위한 게임 프로젝트 하나, 그리고 다른 하나가... 지금 쓰고 있는 MetalFX 플러그인.

 

https://github.com/LocketGoma/MetalFX

 

GitHub - LocketGoma/MetalFX: MetalFx For Unreal (in progress)

MetalFx For Unreal (in progress). Contribute to LocketGoma/MetalFX development by creating an account on GitHub.

github.com

 

이 친구인데요.

 

만들게 된 계기는 뭐... 어쩌면 뻔하게도, 직전 회사에서 쓰려고 열심히 만들고 있었는데 완성을 못시키고 회사를 나오게 되어서

 

"팀장님 저 혹시 이거 플러그인 만들던거 완성시키고 싶은데 외부 공개 가능할까요"
- "ㅇㅇ 그러던가. 나가면 안되는 회사 자료 그런거 없지?"
"넵"
- "그래 잘해봐라"

 

 

거의 이런 느낌으로 (...) 코드 반출을 허락받고 개선 작업에 들어가고 있습니다.

 

원래 쓰던 엔진 버전에서 벗어나, (작업 당시 기준) 최신 버전인 5.7.4를 기반으로 작업중이에요.

 

지금 생각보다 진척도가 많이 오른거 같아서, 정리 차원에서 개발 일지 써보고 있습니다.

 


그래서 뭘 만들었느냐.

 

간단히 요약하면 다음과 같습니다.

 

Apple MetalFX를 이용, Unreal Engine에서 "기술 실증" 테스트 및 정상실행이 가능한 수준의 플러그인 작업

 

 

양대 업스케일러인 DLSS / FSR과 다르게, MetalFX는 Firebase 같은 애들마냥 그냥 SDK로만 공개되어있다보니 Unreal... 뿐만 아니라 그냥 일반 C++ / C# 기반의 대부분의 환경에서 제대로 적용이 안됩니다.

 

그나마, 언리얼 5.4 부터였나? MetalCPP 라는 Metal RHI를 C++로 Wrapping 한 SDK를 UE 엔진 소스코드에서 제공을.... 해주긴 해서 조금 접근성이 낮아... 지긴 했는데... 낮아져봤자 에베레스트에서 K2 정도 된 느낌이지 이게 장난치나

 

여튼, 플러그인을 작업하다보니 결국에는 Spatial / Temporal Upscaler Unreal Engine Wrapping 버전 코드를 직접 작업해야한다는 충격적인 사실에 도달해버렸습니다.

 

그러니까... 아무리 SDK만 덩그러니 있다고 쳐도, DLSS / FSR급의 복잡한 업스케일링 코드를... 적어도 흉내는 낼 수 있어야 한다는겁니다.

 

제가 그걸 해야 한다구요? 진짜루요?

 

어쩌겠습니까 해야지....

 

뭐, 여러 시행착오 끝에 꽤 많이 작업하긴 했습니다.

 

회사에서 작업할때 결국 해결 못했던 Command Buffer 문제도 해결했고, 어찌되었든 지금 개발 환경인 Mac M5 Pro 기반에서 작동은 합니다.

 

네. 작동은 한다구요. 멀쩡하다곤 안했습니다.......

 

그치만, 매 초 최소 60번 반복되는 로직을 수정해놨는데 안 터진것만 해도 저는 대단한거라 생각해요. 배워보시는 분은 아시겠지만 랜더링 파이프라인 직접 건드는건 보통 일이 아니거든요...

 

좀 많이...

 

많이... 많이 힘들다고....

일단 뭐, 당장은 0.1 알파 릴리즈 조차도 힘든 단계기야 하지만, 거의 다 온거 같아서 정리 및 푸념 겸 하나 써봤습니다.

 

다음 글에는 AI 도움 좀 받아서 지금까지 작업한거 정리해서 올려봐야겠어요.

 

히히 빌드 돌리러 가야지

 

ㅠㅠ

+ Recent posts