본문으로 건너뛰기

© 2026 Molayo

HN요약2026. 05. 20. 02:02

Godot 4.3에서 GPU 동기화(Synchronization)가 대폭 업그레이드됩니다

요약

Godot 4.3에서는 렌더링 성능 향상과 GPU 병렬화를 위해 엔진 최하위 수준에서 방향성 비순환 그래프(DAG)를 자동으로 구축하는 기능이 도입됩니다. 이를 통해 GPU 파티클 시스템의 성능이 크게 개선되며, 포스트 프로세싱 장면에서는 약 5%~15%의 프레임 타임 감소 효과를 기대할 수 있습니다.

핵심 포인트

  • 렌더링 중 방향성 비순환 그래프(DAG) 자동 구축 기능 도입
  • GPU 파티클 시스템 및 포스트 프로세싱 성능 대폭 향상
  • AMD Polaris 등 특정 하드웨어에서의 그래픽 아티팩트 문제 해결
  • 기존 RenderingDevice API 사용자를 위한 하위 호환성 유지 및 API 간소화
  • 렌더러 개발 난이도 하락 및 엔진 유지보수 효율성 증대

Godot 4의 도입 이후, RenderingDevice는 Forward+ 및 Mobile 렌더러(Renderer)의 중추 역할을 해왔습니다. 사용자가 게임 엔진에서 원하는 모든 기능을 충족할 만큼 유연하면서도 사용하기 쉬운 API를 만드는 것은 매우 어려운 작업입니다. Vulkan이나 Direct3D 12와 같은 API가 이전 세대와 비교하여 요구하는 높은 수준의 세부 사항은 그 자체로 여러 개의 블로그 포스트를 작성할 가치가 있을 정도입니다. 이 글에서는 Godot 내부에서 해결된 문제들에 집중하기 위해 주제를 가능한 한 간결하게 유지하고자 합니다.

새로운 렌더러 개발의 난이도를 낮추고 GPU가 작업을 더 효과적으로 병렬화(Parallelize)할 수 있도록 하는 것을 목표로, 엔진의 최하위 수준에서 렌더링 중 방향성 비순환 그래프 (DAG, Directed Acyclic Graph)를 자동으로 구축하는 기능이 도입되었습니다. 이 변경 사항은 이미 Godot의 main 개발 브랜치에 병합되었으며, 4.3 릴리스의 일부가 될 예정입니다. 그래프의 도입은 성능 향상과 더불어 조사하기 매우 어려웠던 문제들에 대한 다양한 버그 수정을 가져올 것입니다. 예를 들어, 그래프가 병합된 후, 해당 문제에 대한 특정 수정 작업을 수행하지 않았음에도 불구하고 SSAO와 함께 사용하는 MSAA가 AMD Polaris에서 더 이상 아티팩트(Artifacts)를 발생시키지 않는다는 것이 발견되었습니다 (#61415).

사용자 측면에서 예상되는 변경 사항은 전혀 없습니다. 만약 여러분이 RenderingDevice를 사용하는 코드를 작성한 소수의 사람 중 한 명이라 하더라도 걱정할 필요는 없습니다. API가 약간 변경되었지만, 모든 메서드가 이전보다 더 적은 정보를 요구할 뿐입니다. 여러분이 체감할 성능 향상의 정도는 프로젝트의 내용에 따라 크게 달라질 것입니다. 예를 들어, GPU 파티클 시스템(GPU particle systems)은 엄청난 개선을 보게 될 것이며, 포스트 프로세싱(Post-processing)을 사용하는 보다 표준적인 장면들은 약 5%에서 15% 사이의 프레임 타임(Frametime) 감소를 경험하게 될 것입니다. 게다가, Godot 개발자들은 가까운 미래에 렌더러의 성능을 개선하는 데 훨씬 더 수월해질 것입니다.

이것이 어떻게 달성되었는지에 대한 세부 사항에 관심이 있다면 계속 읽어주시기 바랍니다.

배경 (Background)

내려진 기술적 결정들을 심층적으로 분석하기 위해서는 문제 영역 (problem space)을 이해하는 것이 매우 중요합니다. 기존 API 위에 수천 줄의 코드가 작성된 범용 엔진의 기존 코드베이스를 다루는 것은 어떤 종류의 변경이 허용되는지에 대해 많은 제약을 가합니다. 설계 초기 단계에서 도입된 결함은 대규모 프로젝트의 개발에 장기적인 영향을 미칠 수 있습니다. RenderingDevice 추상화(abstraction)를 위해 내려진 몇몇 핵심 결정들, 즉 Vulkan과의 결합 및 그로 인해 나머지 코드베이스에 미친 부정적인 결과들이 바로 그러한 사례였습니다. 다행인 점은 처음부터 다시 시작하기에 결코 늦지 않았다는 것입니다. reduz는 2023년 말에 이 재설계(redesign)를 위한 계획을 세웠으며, 팀은 이를 현실로 만들기 위한 작업에 착수했습니다.

Vulkan

RenderingDevice가 왜 Godot 4를 위해 지금과 같은 방식으로 설계되었는지, 그리고 그 과정에서 어떤 어려움에 직면했는지 파헤치기 전에 반드시 이해해야 할 몇 가지 Vulkan 개념이 있습니다.

커맨드 버퍼 (Command buffers)

GPU를 위한 작업을 기록(recording)하고 실행을 위해 제출(submitting)하는 것은 Vulkan에서 매우 명시적인 작업입니다. 커맨드 버퍼 (Command buffers, 또는 D3D12의 command lists)는 나중에 GPU에서 실행될 모든 기록된 작업이 저장되는 객체입니다. 이 버퍼들은 사용자가 원하는 만큼 커질 수 있으며, 커맨드 큐 (command queue)에 실행을 위해 제출되기 전까지는 실행되지 않습니다. 이는 본질적으로 여러 소스, 심지어 여러 스레드(thread)로부터 작업을 기록하고, 적절한 동기화 (synchronization)를 통해 준비가 완료되면 GPU에 작업을 제출하는 것이 가능하다는 것을 의미합니다.

렌더 패스 (Render passes)

Vulkan으로 무언가를 그리기 위해서는 "렌더 패스 (render pass)"라는 형태로 사전에 많은 정보가 필요합니다. 렌더 패스는 GPU가 타겟으로 사용할 텍스처에 대한 참조, 해당 텍스처의 초기 및 최종 상태 (텍스처 메모리 레이아웃 (texture memory layouts)), 그리고 그 외 훨씬 더 많은 정보를 포함하는 객체입니다. 이는 명령 기록 (command recording) 중에 렌더 타겟 (render target)을 변경할 수 있고 사전에 객체를 생성할 필요가 없는 OpenGL이나 심지어 D3D12와 같은 이전 API들과는 극명하게 대조됩니다. 렌더 패스가 활성화되어 있는 동안에는 커맨드 버퍼 (command buffer)에 기록할 수 있는 작업에 대해서도 다양한 제한 사항이 존재합니다. 모든 유형의 작업을 다시 사용할 수 있게 하려면 렌더 패스가 반드시 종료되어야 합니다.

배리어 (Barriers)

이전 API와 비교했을 때 가장 큰 변화는 명령 간의 동기화 (synchronization)를 더 이상 드라이버가 자동으로 추론하지 않는다는 점입니다. 대신 프로그래머가 Vulkan을 사용하여 수동으로 구현해야 합니다. 이 주제에 대해 더 자세히 알고 싶다면, Vulkan 동기화에 관한 Hans-Kristian의 블로그 포스트와 공식 Vulkan 문서를 읽어보시는 것을 강력히 추천합니다. 본 기사를 짧게 유지하기 위해, 아래에 매우 기본적인 설명을 제공합니다.

커맨드 버퍼 (command buffer) 내에 기록된 명령들의 실행 순서는 제출된 순서대로 완료된다는 보장이 없습니다. GPU는 작업을 최대한 빠르게 완료하기 위해 최적이라고 판단되는 임의의 순서로 이 명령들을 재정렬할 수 있습니다. 이를 보완하기 위해 프로그래머는 커맨드 버퍼 내에 **동기화 장벽 (synchronization barriers)**을 수동으로 삽입해야 하며, 이를 통해 **전 (before)**과 **후 (after)**의 범위를 지정함으로써 어떤 명령이 완료되어야 하거나 시작되어야 하는지를 상세하게 명시할 수 있습니다. 이 범위에는 실행 단계 (예: 드로잉 (drawing), 컴퓨트 (compute), 전송 (transfer)), 액세스 유형 (예: 읽기 또는 쓰기), 그리고 영향을 받는 메모리 영역 (전역 (globally), 버퍼 (buffers) 또는 텍스처 (textures))과 같은 여러 개념이 포함됩니다. 게다가, 장벽은 텍스처를 한 메모리 레이아웃 (memory layout)에서 다른 레이아웃으로 전환할 수 있는 기능도 갖추고 있는데, 이는 서로 다른 명령에서 텍스처를 최적으로 사용하기 위해 필요한 요구 사항입니다.

장벽은 동기화 범위 (synchronization scope)를 지정함으로써 명령 간의 의존성 (dependency)을 설정할 수 있습니다.

초보자들은 이 메커니즘을 이해하는 데 어려움을 겪을 수 있지만, 전문가나 하드웨어 제조사조차 실수로부터 자유롭지 못합니다. 동기화 (synchronization) 모드에서 Vulkan 검증 레이어 (Vulkan Validation Layers)를 실행해 보면, 대부분의 Godot 프로젝트나 심지어 시장에 출시된 상용 게임에서도 여러 가지 문제점이 드러날 것입니다. 이러한 문제들은 하드웨어 제조사나 하드웨어 속도에 따라 일관되게 나타나지 않기 때문에 이해하기 가장 까다로운 문제 중 하나입니다. 이러한 문제들을 제거하는 것이 비순환 그래프 (acyclic graph) 도입의 주요 목표 중 하나였기 때문에, 동기화 오류가 남아 있지 않도록 이러한 검증 도구들을 광범위하게 사용하여 확인 작업을 거쳤습니다.

RenderingDevice

RenderingDevice는 Godot의 Forward+Mobile 렌더러(renderer)가 구축된 추상화 계층입니다. 이는 Vulkan이나 D3D12만큼 세밀하지는 않지만, OpenGL만큼 "상태 유지(stateful)"적이지도 않은 방식으로 GPU에 대해 합리적인 수준의 제어를 제공하는 인터페이스를 노출합니다. 더 많은 렌더링 상태(rendering state)를 사전에 정의해야 하며, 이전 명령에서 다음 명령으로 상태가 누출(leaking)되어 오류를 발생시킬 가능성이 현저히 줄어듭니다.

이 클래스에서 노출하는 명령들은 리소스 생성, 복사, 클리어(clearing), 그리기(drawing), 컴퓨트 작업 디스패치(dispatching compute work) 등 렌더링 API에서 통상적으로 기대하는 것들입니다. 하지만 주목할 점은 렌더링 기하 구조(geometry)와 컴퓨트 패스(compute passes)가 모두 어떻게 구성되는가 하는 점입니다. Godot에는 이미 "드로우 리스트(draw lists)"와 "컴퓨트 리스트(compute lists)"라는 개념이 있습니다. 이는 본질적으로 렌더 패스(render pass) 또는 컴퓨트 패스(compute pass) 내부에서 수행할 작업을 설명하는 명령들의 배치(batches)입니다. 리스트와 패스 사이의 일대일 상관관계는 매우 중요한데, 이는 자동 그래프가 생성해야 하는 노드의 수를 크게 줄여주기 때문입니다.

드로우 리스트와 컴퓨트 리스트는 렌더링 파이프라인(rendering pipeline)에서 단일 작업 단위로 간주됩니다.

RenderingDevice가 Vulkan이 도입한 많은 어려움을 숨기려고 시도했지만, Godot 4를 위한 렌더링 코드를 작성하는 것을 예상보다 어렵게 만든 몇 가지 문제들이 빠져나갔습니다.

드로우 리스트 액션 (Draw list actions)

드로우 리스트가 렌더 패스에 자연스럽게 매핑되었던 것처럼, 이와 연관된 텍스처(textures)에 대해 시작 및 종료 액션(start and end action)을 지정해야 한다는 사실도 마찬가지였습니다. 이러한 액션은 텍스처의 내용을 로드하거나, 폐기(discarding)하거나, 특정 색상으로 클리어(clearing)하는 등의 작업을 포함합니다. 각 렌더 패스에서 최적의 옵션을 선택함으로써 얻을 수 있는 성능 이점은 매우 큽니다. 하지만 몇 가지 내부 설계 결정으로 인해, CONTINUE를 도입하는 과정에서...

Vulkan에는 실제로 존재하지 않는 동작이 수행되었습니다. 이 동작은 본질적으로 "이전 드로우 리스트 (draw list)가 호환되며 텍스처를 다른 상태로 전환할 필요가 없다"는 것을 의미했습니다. 수많은 선택적 (optional) 포스트 프로세싱 (post-processing) 효과와 드로잉 레이어 (drawing layers)를 제공해야 하는 Godot와 같은 엔진에게, 이는 코드 내에 따라가기 어렵고 실수하기 쉬운 수많은 분기문(branching)을 만들어내는 *유지보수의 악몽 (maintenance nightmare)*이 되었습니다.

드로우 리스트 저장 텍스처 (Draw list storage textures)

디바이스는 드로우 리스트의 결과가 나중에 컴퓨트 패스 (compute pass)에서 사용될지 여부를 알 수 없기 때문에, 렌더 패스 (render pass)가 끝날 때 어떤 텍스처를 "저장 (storage)" 메모리 레이아웃 (memory layout)으로 전환해야 하는지 지정해야 했습니다. *저장 (Storage)*은 컴퓨트 패스에서 텍스처를 직접 수정하는 데 필요한 레이아웃입니다. Godot 외부자의 관점에서는 이것이 왜 API의 일부여야 하는지 명확한 이유가 없었지만, 구현 내용을 분석해 보면 이것이 Vulkan에서 요구하는 배리어 전환 (barrier transitions)을 처리하기 위해 도입되었음이 분명했습니다. 결과적으로, 이는 텍스처를 효율적으로 처리하기 위해 해당 텍스처가 나중에 컴퓨트 패스에서 사용될지 여부를 추적해야 하는 책임을 프로그래머에게 전가했습니다.

드로우 및 컴퓨트 리스트 중첩 (Draw and compute list overlap)

드로우 리스트 (Draw lists)와 컴퓨트 리스트 (compute lists)는 실행 중에 서로 "중첩 (overlap)"될 수 있는지 여부를 지정할 수 있었습니다. 이는 프로그래머가 렌더링 (render) 작업과 컴퓨트 (compute) 작업이 서로 의존적이지 않아 실제로 병렬 (parallel)로 실행될 수 있는지 여부를 정확히 이해하고 있는지에 따라 달라지는 매우 모호한 인자였습니다. 이를 검증할 실질적인 방법이 없었기 때문에, 이 플래그 (flag)는 기본 동작에서 강제하던 일부 동기화 (synchronization) 단계를 단순히 건너뛰는 방식으로 "일단 켜보고 잘 되길 바라는" 의미에 불과했습니다. 다시 한번 말하지만, 이 기능을 사용하면 병렬 작업이 제출되고 있는지 여부를 추적해야 하는 코드베이스상의 문제들이 발생했습니다. GI (Global Illumination)와 같은 컴포넌트가 이 최적화 (optimization)를 활용할 수 있었지만, 이 기능의 사용 여부는 사용자가 씬 (scene)에서 무엇을 활성화했는지에 따라 달라졌습니다.

배리어 마스크 (Barrier mask)

RenderingDevice가 배리어 (barriers)에 대한 명시적인 접근을 제공하지는 않지만, 많은 메서드 (methods)에서 명령이 파이프라인 (pipeline)의 어느 단계와 동기화되어야 하는지를 정의하기 위해 비트 마스크 (bit mask)를 지정할 수 있도록 허용했습니다. 하지만 이는 기존 렌더링 코드에서 가장 오용된 마스크 중 하나였을 것입니다. 왜냐하면 대부분의 메서드에서 그 구현이 일관되지 않았기 때문입니다. 한 사례에서는 비트 마스크가 전혀 아닌 변수가 실수로 비트 마스크로 캐스팅 (casted)되어 렌더링 메서드에 전달되기도 했습니다.

기본적으로 대부분의 명령은 이후에 오는 모든 작업이 현재 명령이 완료된 후에 실행되어야 함을 지정합니다. 예상대로, 이로 인해 Godot는 이후에 무엇이 요청될지 알 수 없기 때문에 "만약을 대비하여" 극도로 많은 배리어 (barriers)를 발행하게 됩니다. Godot의 전략은 기본적으로 RenderingDevice를 사용하는 프로그래머가 배리어 마스크 (barrier mask)를 명시하지 않는 한, 현재 명령이 완료되기 전에 미래의 작업이 실행되지 않도록 보장하기 위해 배리어를 발행하는 것으로 요약됩니다. 다시 말하지만, 이는 개발자가 다음에 올 모든 것에 대해 정확한 지식을 가지고 있어야 함을 의미하며, 결과적으로 또 다른 유지보수 문제를 야기했습니다. 따라서 이 기능은 거의 사용되지 않고 기본 동작이 선호되었으며, 이는 성능 저하로 이어졌습니다.

지나고 나면 보이는 것 (Hindsight is 20/20)

여기서 제기된 문제들이 Godot 4의 계획 단계에서는 그렇게 나쁘게 들리지 않았을 수도 있지만, 그 위에 더 많은 렌더링 코드가 작성됨에 따라 해결하기가 분명히 점점 더 어려워졌습니다. 엔진의 많은 부분을 리팩터링 (refactoring)하는 것과 더불어 드라이버 레벨 (driver-level)의 작업을 작성하는 임무를 맡는 것은 매우 어려운 작업이며, 특히 새로운 API가 훨씬 더 가파른 학습 곡선을 가질 때는 더욱 그렇습니다. 많은 프레임워크와 엔진들이 Vulkan에서 예상되는 새로운 작업량을 처리하기 위해 자신들의 솔루션을 반복적으로 개선해 왔으며, 이제 Godot도 이 문제를 다시 해결해야 할 때가 되었습니다.

솔루션 (Solution)

준비 (Preparation)

AI 자동 생성 콘텐츠

본 콘텐츠는 HN Game Dev의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0