3P by GN⁺ | ★ favorite | 댓글 1개
  • OpenGL 경험이 있던 개발자가 Vulkan을 처음 배워 약 3개월 만에 두 개의 작은 게임 데모와 재사용 가능한 EDBR 엔진을 만들었음
  • 처음부터 범용 엔진을 설계하지 않고 작은 게임을 먼저 만든 뒤 필요한 부분만 엔진화해 과도한 설계와 bike-shedding을 줄임
  • 엔진은 19k LoC 규모이며 그래픽 코드 6.7k LoC, Vulkan 경량 추상화 2k LoC로 구성되고 컴퓨트 스키닝, CSM, PBR 셰이딩, MSAA, 포스트 FX, UI 렌더링을 포함함
  • vk-bootstrap, Vulkan Memory Allocator, volk, VK_KHR_dynamic_rendering, 푸시 상수, 버퍼 디바이스 주소, bindless descriptor로 Vulkan 보일러플레이트와 descriptor set 사용을 줄임
  • Vulkan은 전역 상태 제거, 더 나은 검증 오류, RenderDoc 셰이더 디버깅, GPU·OS 간 일관성을 제공했지만 명시적 동기화는 여전히 직접 다뤄야 함

3개월 동안 만든 Vulkan 기반 EDBR 엔진

  • EDBR(Elias Daler’s Bikeshed Engine)는 Vulkan 학습 프로젝트에서 시작해 이후 프로젝트에 재사용할 수 있는 작은 엔진으로 발전함
  • 엔진과 게임 코드는 GitHub 저장소에 공개되어 있음
  • 작성 시점 코드 규모는 다음과 같음
    • 엔진 자체: 19k LoC
      • 그래픽 관련 코드: 6.7k LoC
      • Vulkan 경량 추상화: 2k LoC
    • 3D 고양이 게임: 4.6k LoC
    • 2D 플랫폼 게임: 1.2k LoC
  • 입력 처리와 오디오 시스템 등 일부 비그래픽 코드는 이전 엔진에서 가져왔고, 그래픽과 여러 핵심 시스템은 새로 작성함
  • Vulkan을 기존 OpenGL 추상화에 끼워 넣기보다 새로 작성하는 편이 더 적합했다고 평가함

그래픽 프로그래밍에서 Vulkan까지의 학습 순서

  • 그래픽 프로그래밍을 처음 시작한다면 Vulkan보다 OpenGL로 시작하는 편이 복잡도에 압도되지 않기 쉬움
  • 최소 목표로는 텍스처가 적용된 모델을 화면에 표시하고 간단한 Blinn-Phong 조명을 구현하는 수준을 권함
  • 기본 shadow mapping은 장면을 다른 시점과 다른 렌더 타깃으로 렌더링하고, depth texture를 샘플링하는 방법을 익히는 데 도움 됨
  • OpenGL 학습 자료로 다음을 추천함
  • Vulkan 학습에는 vkguide가 가장 도움이 되었고, 처음 시작한다면 전체를 따라가되 간단한 게임에는 “GPU driven rendering” 수준의 복잡도가 당장 필요하지 않을 수 있음
  • Vulkan Lecture Series by TU Wien는 Vulkan 기본기를 다루며, 특히 동기화 강의가 도움이 됨
  • 첫 달 학습 결과로 다음 기능을 구현함
    • glTF 모델 로딩
    • 컴퓨트 스키닝
    • frustum culling
    • shadow mapping과 cascaded shadow maps

Vulkan을 선택한 이유와 WebGPU와의 비교

  • 목표는 Windows와 Linux 중심의 데스크톱용 작은 3D 게임이었고, 오픈소스 기술과 개방 표준을 선호해 OpenGL과 Vulkan 사이에서 선택함
  • OpenGL은 작은 게임에는 충분하지만 새 버전이 나올 가능성이 낮고, macOS에서는 deprecated 상태라 미래가 불확실하다고 봄
  • WebGPU도 일부 학습했지만 다음 제약이 있었음
    • 아직 안정적이지 않고 튜토리얼과 예제가 많지 않음
    • WGSL 문법이 GLSL보다 덜 마음에 들었음
    • 데스크톱에서는 DirectX, Vulkan, Metal 위의 래퍼에 가까워 플랫폼별 RenderDoc 캡처가 달라지고 WebGPU 호출과 네이티브 API 호출이 1:1로 대응하지 않음
    • bindless textures와 push constants가 없음
  • WebGPU에는 분명한 장점도 있음
    • OpenGL/WebGL보다 검증 오류가 좋고 전역 상태가 없음
    • Vulkan과 비슷한 부분이 있어 Vulkan 학습 전에 도움이 됨
    • Vulkan보다 화면에 무언가를 띄우는 데 필요한 보일러플레이트가 적음
    • 명시적 동기화를 직접 다루지 않아도 됨
    • 브라우저에서 게임을 실행할 수 있음

프레임 렌더링 흐름

  • 한 프레임은 여러 단계로 나뉘며, 각 단계는 파이프라인 또는 패스 형태로 구현됨
  • 스키닝 단계에서는 skeletal animation이 있는 모델을 compute shader로 처리함
    • 입력은 스키닝되지 않은 mesh와 joint matrices
    • 출력은 이후 렌더링 단계에서 사용할 vertex buffer
    • 이후 단계에서는 정적 mesh와 skinned mesh를 비슷하게 다룰 수 있음
  • CSM 단계에서는 4096x4096 depth texture와 3개 slice를 사용해 cascaded shadow mapping을 수행함
  • geometry + shading 단계에서는 모델을 그리고 shadow map과 light 정보를 사용해 셰이딩함
    • PBR 모델은 Physically Based Rendering in Filament에 설명된 방식과 거의 같음
    • fragment shader는 한 draw call에서 해당 mesh에 영향을 주는 모든 light 계산을 수행함
  • 모든 것은 multi-sampled texture에 그려진 뒤 resolve됨
  • depth resolve는 fragment shader로 수동 처리함
    • multi-sample depth texture의 모든 fragment를 순회해 최소값을 non-MS depth texture에 기록함
  • post FX 단계는 현재 depth fog만 적용하며, 이후 tone mapping과 bloom도 이 단계에서 처리할 계획임
  • UI 단계에서는 dialogue UI를 그리고 한 draw call로 처리함

Vulkan 보일러플레이트를 줄이는 라이브러리

  • vk-bootstrap은 physical device 선택, swapchain 생성 등 Vulkan 초기화 보일러플레이트를 줄임
    • 전체 Vulkan 함수 래퍼가 아니라 초기화 단계에 주로 영향을 줌
  • Vulkan Memory Allocator는 Vulkan 메모리 할당을 직접 다루지 않아도 되게 해 줌
  • volk는 extension function loading을 단순화함
    • 예를 들어 vkSetDebugUtilsObjectNameEXT 같은 extension 함수를 직접 포인터로 저장하지 않고 사용할 수 있음
  • GfxDevice 클래스는 자주 쓰는 Vulkan 기능과 객체를 묶음
    • Vulkan context 초기화
    • swapchain 생성과 관리
    • beginFrameendFrame
    • image 생성과 texture 로딩
    • buffer 생성
    • bindless descriptor set 관리
  • GfxDevice.cpp는 작성 시점 714줄이며, VkDevice, VkQueue, VmaAllocator 등을 여러 곳에 전달하는 대신 하나의 객체를 넘기는 방식이 편리함

셰이더 빌드와 descriptor set 회피 전략

  • 셰이더 언어는 OpenGL 경험 때문에 GLSL을 선택함
  • 셰이더는 런타임이 아니라 빌드 단계에서 SPIR-V로 컴파일함
    • 런타임 shader 로딩 코드가 단순해짐
    • 런타임 shader compiler 의존성이 없음
    • shader 오류를 빌드 단계에서 발견함
  • glslc는 CMake의 DEPFILE을 지정할 수 있어 shader include 변경 시 관련 파일을 자동 재컴파일할 수 있음
  • Vulkan에서는 uniform을 descriptor set으로 묶어야 해서 OpenGL보다 데이터 전달이 복잡해짐
  • 구현에서는 descriptor set 사용을 크게 줄임
    • bindless texture와 sampler를 위한 전역 descriptor set 하나만 사용함
    • 나머지는 대부분 push constants로 전달함
    • buffer device address를 사용해 buffer 주소를 push constants로 넘김

pipeline class와 dynamic rendering

  • 렌더링 단계는 PostFXPipeline 같은 pipeline class로 분리함
  • 각 pipeline은 보통 다음 역할을 가짐
    • init: shader 로딩, VkPipeline, VkPipelineLayout 초기화
    • cleanup: pipeline과 layout 정리
    • draw: 매 프레임 필요한 입력을 받아 draw call 수행
  • drawvkCmdBeginRenderingvkCmdEndRendering 사이에서 호출된다고 가정함
  • render pass가 어떤 texture에 렌더링하는지는 pipeline 내부에서 신경 쓰지 않고, 호출자가 render target을 결정함
  • VK_KHR_dynamic_rendering을 전체적으로 사용하고 Vulkan render pass와 subpass는 쓰지 않음
    • tile-based GPU에서는 render pass와 subpass가 더 효율적이라는 이야기를 들었지만, 현재 모바일 지원은 고려하지 않음
    • dynamic rendering이 구현을 훨씬 쉽게 만듦

PVP, BDA, bindless descriptor 활용

  • 모든 mesh에 하나의 vertex 타입을 사용함
  • programmable vertex pulling을 사용하면 OpenGL의 VAO나 Vulkan의 VkVertexInputBindingDescription, VkVertexInputAttributeDescription 같은 vertex format 정의를 피할 수 있음
  • buffer device address를 사용하면 vertex buffer를 descriptor set에 바인딩하지 않고 buffer 주소를 push constants로 전달할 수 있음
  • push constants와 buffer에는 scalar layout을 사용함
    • std430보다 정렬 처리가 쉬워 C++ 구조체와 거의 비슷하게 다룰 수 있음
    • C++ 구조체의 padding 멤버 필요성이 줄어듦
  • bindless descriptor는 큰 descriptor set에 texture와 sampler 배열을 두는 방식으로 사용함
    • 새 texture가 로드되면 textures 배열에 넣고 해당 index를 bindless texture id로 사용함
    • shader에는 texture id를 push constants로 전달함
  • sampler는 image와 분리해 common sampler를 startup 때 만들고 samplers 배열에 넣음
  • material buffer에도 bindless texture id를 사용함
    • material ID만 push constants로 전달하고 fragment shader에서 material buffer를 조회함
    • bulky descriptor set 없이 material당 정수 하나로 texture 접근이 가능함
  • bindless texture 관련 참고 자료로 Vulkan Bindless Texture를 추천함

매 프레임 업로드되는 동적 데이터

  • 매 프레임 CPU에서 GPU로 넣어야 하는 데이터는 큰 배열을 미리 할당하고 매 프레임 index 0부터 채우는 방식을 사용함
  • 예로 모든 joint matrix를 하나의 큰 mat4 배열에 저장하고, skinned mesh마다 시작 index를 push constants로 전달함
  • 두 가지 방식이 있음
    • GPU에 N개 buffer를 두고 frame-in-flight마다 교체함
    • GPU에는 하나의 buffer만 두고 CPU 측 staging buffer를 N개 둠
  • 대부분의 경우 첫 번째 방식이 권장됨
    • GPU 메모리를 더 쓰지만 수동 동기화가 필요 없음
  • GPU 메모리를 아껴야 한다면 두 번째 방식이 유용할 수 있음
    • 두 방식 사이의 눈에 띄는 성능 차이는 발견하지 못했지만, 매 프레임 매우 큰 데이터를 업로드하면 차이가 생길 수 있음

cleanup과 동기화

  • vkguide의 deletion queue 패턴은 작성자의 엔진에서는 큰 효용이 없었음
    • 매 프레임 새 Vulkan 객체를 할당하거나 파괴하지 않기 때문임
  • C++ destructor 기반 cleanup도 편하지 않았음
    • wrapper class, move constructor, move assignment가 필요해 복잡도가 늘어남
    • mid-frame에 사용 중인 객체를 wrapper 파괴로 실수로 제거할 위험이 있음
  • 현재는 cleanup 함수를 명시적으로 호출해 Vulkan 객체를 한곳에서 정리함
    • 호출을 잊기 쉽지만, 종료 시 Vulkan validation error와 VMA assert가 누락된 cleanup을 알려줌
  • Vulkan synchronization은 어렵고 명시적으로 관리해야 함
    • OpenGL과 WebGPU는 texture나 buffer 읽기에 필요한 동기화를 대신 처리함
    • Vulkan에서는 data race를 피하기 위해 barrier를 직접 넣어야 함
  • 현재는 draw를 pass와 pipeline으로 나누고 그 사이에 barrier를 수동 삽입함
    • 예를 들어 compute shader의 skinning pass가 vertex data를 쓴 뒤 shadow mapping pass가 이를 읽기 전에 barrier를 넣음
  • render graph로 자동화할 수 있지만 현재는 수동 동기화에 만족함
  • vkconfig의 synchronization validation layer가 동기화 오류를 찾는 데 도움 됨

sprite, skinning, game/renderer 분리

  • bindless texture를 사용하면 많은 sprite를 vertex buffer 없이 한 draw call로 그리기 쉬움
  • sprite vertex shader는 gl_VertexIndex로 quad의 vertex coordinate와 UV를 생성함
  • 모든 sprite draw call은 SpriteDrawBufferSpriteDrawCommand로 모아짐
    • transform
    • UV 범위
    • color
    • texture ID
    • shader ID
  • 실제 draw call은 vkCmdDraw(cmd, 6, spriteDrawCommands.size(), 0, 0) 형태임
    • sprite당 6개 vertex
    • sprite 수만큼 instance
  • sprite renderer는 1만 개 sprite를 315마이크로초에 그릴 수 있음
  • 컴퓨트 스키닝은 skeletal animation이 있는 mesh에서 입력 vertex와 joint matrices를 받아 skinned vertex buffer를 생성함
    • 동일한 mesh를 가진 고양이 3마리도 서로 다른 animation을 가질 수 있음
    • output vertex buffer는 mesh instance마다 필요함
  • game logic과 renderer는 draw command로 분리됨
    • game logic은 entt를 사용함
    • renderer는 entity나 game object를 알지 않고 light, scene parameter, mesh draw command만 다룸
  • MeshDrawCommand에는 meshId, transform matrix, bounding sphere, skinned mesh 포인터, joint matrix 시작 index, shadow casting 여부가 포함됨

scene loading, UI, Dear ImGui

  • level editor는 직접 만들지 않고 Blender를 사용하고 glTF로 export함
    • level editor를 직접 작성하면 몇 달 또는 몇 년이 걸릴 수 있어 시간을 절약함
  • node 이름으로 prefab 생성과 physics shape를 지정함
    • 예: Interact.Sphere.Diary에서 첫 점 앞의 Interact는 prefab 이름
    • Sphere는 physics system에서 sphere physics body를 만들 때 사용함
    • Capsule, Box도 사용할 수 있고, 없으면 mesh vertex로 physics shape를 생성함
  • 복잡한 model은 level glTF에 직접 넣지 않고 Empty->Arrows object로 배치한 뒤 Cat.NearStore 같은 이름을 붙임
    • Cat prefab을 생성하고 NearStore tag를 붙임
  • prefab은 JSON으로 작성하며 외부 glTF와 movement, physics 정보를 포함할 수 있음
  • UI 시스템은 Roblox UI API에서 영감을 받음
    • origin
    • relative size
    • relative position
    • offsetPosition, offsetSize
    • fixed size
    • label/image content 기반 size
  • UI element size를 재귀적으로 계산한 뒤 position을 계산하고, parent부터 children 순서로 그림
  • Dear ImGui는 개발·디버그 도구에 사용함
  • sRGB framebuffer에서 Dear ImGui가 잘못 보이는 문제가 있어 자체 Dear ImGui backend를 작성함
    • rendering 부분만 작성하고, input event 처리와 clipboard 등 logic/OS interaction은 기본 Dear ImGui SDL backend가 처리함
  • 자체 backend의 장점은 다음과 같음
    • bindless texture id를 지원해 ImGui::Image(bindlessTextureId, ...)로 image를 그릴 수 있음
    • linear image와 non-linear image를 format 전달로 올바르게 그릴 수 있음
    • 엔진의 다른 Vulkan 코드와 같은 방식으로 초기화하고 다룰 수 있음

사용한 라이브러리와 Vulkan 전환 효과

  • 물리는 Jolt Physics를 사용함
    • collision resolution과 basic character movement에 주로 사용함
    • JPH::CharacterVirtual이 기본 character movement를 잘 처리함
  • ECS는 entt를 사용함
    • 직접 만든 ECS 대신 외부 라이브러리를 사용해 유지할 코드를 줄임
  • 오디오는 openal-soft, libogg, libvorbis를 사용함
  • profiling은 Tracy를 사용함
    • 어떤 코드가 실제로 얼마나 적은 시간을 쓰는지 확인해 불필요한 bike-shedding을 피하는 데 도움 됨
  • Vulkan으로 전환해 얻은 점은 다음과 같음
    • OpenGL의 전역 상태가 없어져 추상화가 쉬워짐
    • OpenGL식 shader.bind()나 state tracker, magic RAII가 덜 필요함
    • validation error가 OpenGL보다 더 풍부함
    • RenderDoc에서 vertex shader와 fragment shader를 직접 디버깅할 수 있음
    • GPU와 OS 간 동작 차이가 OpenGL보다 덜 두드러짐
    • 향후 Slang, Shady 같은 다른 shading language를 탐색할 수 있음
    • 그래픽 파이프라인의 각 측면을 더 많이 제어할 수 있음

앞으로 할 작업

  • 앞으로 계획한 작업은 다음과 같음
    • sign-distance field font 지원
    • 많은 image 로딩과 mipmap 병렬 생성
    • bloom
    • volumetric fog
    • animation blending
    • render graph
    • ambient occlusion
    • 게임 완성
  • Vulkan 학습은 어려웠지만 예상만큼 어렵지는 않았고, 그래픽 프로그래밍과 modern API를 더 깊게 이해하는 계기가 됨

댓글과 토론

Hacker News 의견들
  • 미니멀리즘은 매우 효과적임
    나는 반대로 갔고, 그 때문에 큰 고통을 겪고 있음. Rust로 메타버스 클라이언트를 만들고 있는데, 지금도 다른 화면에서 아바타가 거대한 스팀펑크 도시를 트램으로 지나가는 장면을 보여주는 중임. 새 프리릴리스를 내기 전에 12시간씩 돌려봄
    Vulkan 위에 WGPU와 Rend3를 얹었고, Rend3는 메시, 2D 텍스처, 객체를 만들면 객체가 메시와 텍스처를 참조하고 화면에 나타나는 깔끔한 API를 제공함. Rust의 참조 카운팅이 연결을 맞춰줘서 쓰기는 직관적임
    하지만 계층이 많아지면 문제가 생김. WGPU는 웹 브라우저, Vulkan, Metal, DX11(최근 제거됨), DX12, Android, OpenGL까지 지원하려 하므로 큰 개발팀이 필요하고 변경이 어려움. WGPU API 자체도 대체로 Vulkan 같아서 GPU 메모리 할당과 동기화를 직접 신경 써야 함
    WGPU에는 최소공통분모 문제가 있음. 어떤 플랫폼은 특정 기능을 지원하지 못하고, Vulkan이 지원하는 여러 스레드의 GPU 메모리 동시 갱신을 WGPU는 간섭 없이 처리하지 못함. 큰 세계를 다루는 게임이나 클라이언트에는 프레임률을 죽이지 않고 콘텐츠를 GPU로 넣는 이 기능이 필요함. 플랫폼마다 동시성 제약이 달라 잠금 충돌로 성능이 크게 떨어질 수도 있음
    Rend3는 동기화와 할당을 처리하는 적당한 접착 코드 역할을 하려 했지만, 특히 동기화를 일반적으로 풀기는 어려움. 절두체 컬링은 큰 성능 이득이지만, 가려짐 컬링은 계산 비용 때문에 손해였음. 반투명 처리도 깊이 정렬이 필요해서 골치 아픔. 창이 많은 세계를 다루기 때문에 밖을 보고 안을 들여다볼 수 있는 반투명 객체가 꼭 필요함
    Rust 3D 스택 쪽 사람들은 내가 3년 동안 스택을 고치라고 계속 압박해서 짜증이 난 듯함. 모두 자원봉사이고, Vulkan은 돈과 사용자 기반이 있어 유지됨. Rend3는 최근 창작자가 포기해서 이제 내가 안으로 들어가 고쳐야 함. WGPU로 복잡한 것을 만드는 사람은 드물고, 대부분 Flash로도 가능했을 2D 게임이나 단순한 정적 3D 장면임. 상업 프로젝트는 계속 Unity나 UE5를 씀
    Vulkan으로 직접 가도 동기화, 할당, 절두체 컬링, 반투명 처리를 직접 작성해야 하니 큰 전환임
    덧붙여 Vulkan과 Metal 위 래퍼인 Vulkano도 최소공통분모 문제가 있음. Vulkan과 Metal은 둘 다 GPU 자산의 동시 갱신을 지원하지만, Vulkano는 허용하지 않음. 물론 Apple은 또 다르게 처리함

    • WGPU는 WebGPU를 사용하고, 내가 알기로 아직 어떤 브라우저도 스레드를 지원하지 않음
      https://gpuweb.github.io/gpuweb/explainer/#multithreading
      https://github.com/gpuweb/gpuweb/issues/354
      OpenGL도 스레드를 지원한 적이 없으니 OpenGL을 쓰는 것은 그럴 수 없음
    • 앞부분은 맞지만, 뒷부분은 아님. 할당과 동기화는 자동으로 처리됨
    • Metal이 Vulkan보다 오래됐으니, 엄밀히는 Vulkan이 다르게 하는 쪽임
    • WGPU가 여러 스레드에서 GPU 메모리를 간섭 없이 갱신하지 못한다는 점은 좋은 경험을 만들기 위해 꼭 제대로 알고 싶던 핵심임. WGPU가 확장으로라도 이 기능을 추가할 방법을 찾으면 좋겠음
    • 예전에 URLO에 올린 글도 매우 흥미롭게 읽었음: https://users.rust-lang.org/t/game-dev-in-rust-some-notes-on...
  • 1년 조금 넘게 전에 Vulkan을 배워보려 했지만 다시는 건드리고 싶지 않음. OpenGL을 폐기하고 단순한 일도 터무니없이 어렵게 만드는 것으로 대체하는 게 정말 불편함. 예를 들어 회전하는 큐브 하나에도 코드가 수백 줄 필요함
    OpenGL도 쉽지는 않았지만, 보통 사람이 비교적 짧은 시간 안에 기초를 배울 수는 있었음. 큰 서점에서 그래픽 프로그래밍 입문서를 사서 오후 하루이틀이면 기본 렌더링을 띄울 수 있었음. Vulkan이 어떤 면에서는 더 낫겠지만 빠르게 배우길 기대하기는 현실적이지 않음
    최신 Intel/ARM/AMD 칩이 나오면서 C나 C++을 못 쓰고 “고수준 언어 지원을 중단하니 이제 어셈블리만 쓰라, 제어권이 많아져서 더 빠르다”라고 한다고 상상해보면, 당연히 말도 안 된다고 불릴 것임

    • OpenGL은 내가 알기로 macOS에서만 폐기됐고, 앞으로도 오랫동안 존재할 것임
      Vulkan은 고수준 그래픽 API라기보다 “GPU API”에 가깝다고 자주 말함. 그렇게 보면 복잡성은 놀랍지 않고, 도메인 자체가 어려움
      최신 CPU에서 고수준 언어를 없애고 어셈블리만 쓰라는 비유보다는 단일 스레드 C/C++와 다중 스레드 C/C++의 차이에 더 가까움. 복잡도가 크게 늘고 모르면 터지거나 성능이 더 나빠지지만, 앞으로 가는 실용적인 길이기도 함
      OpenGL은 대체로 Vulkan 위에 구현할 수 있음. OpenGL 표준이 더 이상 활발히 개발되지 않는 건 아쉽지만, 영원한 것은 없음
    • 회전하는 큐브 하나가 수백 줄이라는 건 큰 의미가 없음. 두 번째 큐브나 다른 도형을 추가한다고 줄 수가 두 배가 되지는 않음
      문제는 OpenGL이 더 이상 현재 하드웨어와 잘 맞지 않아 순진한 방식으로 쓰면 성능이 매우 비효율적이라는 데 있음. OpenGL에서도 드라이버 부담을 없애는 기법까지 가면 Vulkan이 그렇게까지 더 어렵지는 않음
      현재 Intel/ARM/AMD 칩도 C나 C++을 직접 지원하지 않음. 이미 어셈블리를 쓰거나, C/C++에서 변환해주는 서드파티 도구를 쓰고 있음. Vulkan의 목표도 GPU에 대한 저수준 표준 인터페이스를 제공하고, 그 위에 더 쓰기 좋은 추상화를 얹도록 하는 것임
    • Vulkan은 OpenGL 같은 라이브러리를 작성하기 위한 대상임. 장점은 라이브러리 수준의 많은 코드가 불투명하고 버그 많은 장치 드라이버에서 사용자 공간 라이브러리로 이동한다는 데 있음
    • OpenGL을 원한다면 ANGLE을 쓰면 됨
      https://github.com/google/angle
      여러 휴대폰은 이제 Vulkan 드라이버 위의 유일한 OpenGL 지원으로 ANGLE을 탑재함
      비교적 쉽고 이식성 있는 현대적인 API를 원한다면 Rust의 wgpu나 C++의 dawn을 통한 WebGPU를 쓰면 됨
  • Vulkan은 훌륭하다고 생각하지만, 목적은 고급 GPU 기능을 최대한 활용하는 데 있음. 고급 GPU 기능을 쓸 때 OpenGL보다 더 나은 성능도 낼 수 있음
    고급 렌더링 기법을 목표로 하지 않는다면 보통은 OpenGL이 권장 경로라고 느낌
    지금도 2D, 로우폴리, PS1풍 그래픽 게임이 많고, 이런 게임은 Vulkan을 쓸 필요가 없음
    Vulkan은 AAA 게임 산업이 렌더링 품질과 외형 쪽으로 기울어졌다는 예시임. AAA 스튜디오는 아주 고급 엔진과 콘텐츠로 예산을 정당화하지만, 플레이어들이 지쳐서 그래픽보다 게임플레이를 원한다는 걸 깨닫고 2D/로우폴리 게임 시장이 커지고 있음
    게임 개발자라면 렌더링 품질보다 게임플레이와 기능에 집중하고 싶을 것임

    • Vulkan은 시각적 충실도에 관심이 없어도 OpenGL보다 장점이 있음
      전역 상태가 없고, 런타임에 쓸 GPU를 선택할 수 있으며, OpenGL의 오류 처리는 끔찍함. 검증 계층이 있고, 공식 아트워크도 멋지고, 문서도 훌륭함. 두 번째 스레드에서 비동기로 GPU에 데이터를 올리는 것도 더 합리적으로 가능하고, 메시 셰이더나 RTX 같은 고급 GPU 기능도 있음
    • 여기서 동인은 게임 개발의 재정적 현실에 더 가까워 보임. 고충실도 그래픽은 엄청나게 비싸고, 작은 게임 스튜디오는 현실적인 일정과 예산으로 만들기 어려움
      소비자가 AAA급 그래픽의 인디 게임을 거부할까? 그렇지는 않을 것임. 그런 게임이 적은 이유는 재정적으로 성립하기 어렵고, 더 양식화되고 낮은 충실도의 그래픽을 받아들이는 시장이 충분히 크기 때문임
    • 중간 지점은 WebGPU임. Vulkan보다 훨씬 덜 장황하고 브라우저를 포함해 어디서든 실행된다는 보장이 있음
      동시에 WebGL에서는 쓸 수 없는 계산 셰이더 같은 현대적 기능에 접근할 수 있고, OpenGL처럼 같은 일을 하는 여러 방식으로 쌓인 레거시도 많지 않음. 주된 장점은 새롭다는 것이지만, 그래서 튜토리얼이 훨씬 적고 이건 꽤 심각한 단점임
    • OpenGL은 저물고 있고, Metal에 신경 쓰는 사람은 거의 없으며, 자발적으로 Vulkan을 쓰는 사람은 주말에 커널 작성에서 쉬고 나온 수염 난 코드 마법사들뿐임
      프레임워크와 엔진은 끝도 없이 많지만 대부분 미완성이고, 모두 코드 구조에 대해 강한 의견을 가짐. “콜백을 담는 장면 트리를 만들어라. 아니, 엔티티와 컴포넌트 클래스와 객체를 작성해라. 잠깐, 이제는 모두 즉시 모드이고 함수형이며 무상태다” 같은 식임
      여기에 플랫폼 난장판까지 얹힘. 모바일 게임의 푸시 알림, 앱 내 구매, Xcode를 통한 필수 서명까지 합치면 완전히 엉망임. Unity가 시장 점유율을 가진 이유가 있고, 그게 훌륭한 소프트웨어라서가 아님. 화려한 웹뷰 이상인 크로스플랫폼은 여전히 엄청난 고통이기 때문임
    • 성능만의 문제가 아님. OpenGL에서 벗어나는 이유 중 하나는 고수준 기능이 드라이버 구현이나 GPU마다 일관되지 않다는 점이라고 봄
      GPU/드라이버별 코드를 계속 써야 한다면 추상화 계층의 의미가 없음. 저수준 드라이버와 고수준 라이브러리를 분리하는 편이 더 낫다
  • 이 글에는 좋은 조언이 많음. 특히 “지금 필요하지 않으면 구현하지 말라”가 눈에 띄었음
    이건 경력 몇 년은 있지만 아직 성장 중인 주니어 프로그래머들과 늘 싸우는 부분임. 이들은 “모범 사례”와 유행하는 화려한 새 도구에 집착하는 경우가 많지만, 풀어야 할 문제에서 출발해 그 문제를 해결하는 데 필요한 최소한에 집중하는 데 어려움을 겪음

    • 이건 1인 팀 맥락임. 다음 조언을 보면 분명해짐: “게임/엔진의 어느 부분이든 나중에 다시 작성할 수 있다는 걸 기억하라”
      중대형 조직에서는 보통 그렇지 않음. 대개 다음 일로 넘어가고 다시 들여다볼 시간이 거의 없음. 안타깝지만, 그래서 처음부터 제대로 만들고 버그나 부작용을 만들 여지를 줄여야 함
      너무 많은 코드베이스에서 새 기능을 서두르다 지뢰밭 같은 코드가 되었고, 작은 변경 하나에도 모든 기능을 수동으로 확인하고 전체 애플리케이션 맥락을 머리에 넣어야 했음
    • 과잉 설계는 계속 골칫거리임. 결과적으로 사실상 이득 없이 복잡도만 많이 늘어남
      내 경험상 나중에 새 요구사항이 들어오면, 이전에 만든 범용 해법은 완전히 부적절해서 어차피 다시 만들어야 함. 눈앞의 문제를 풀어야지 알 수 없는 미래 문제를 풀면 안 됨
    • YAGNI도 모범 사례라고 알려주면 됨 :D
    • 이 원칙을 엄격히 지키면 업무가 대부분 여러 API와 서비스들을 붙이고, 이미 작성된 소프트웨어를 회사 필요에 맞게 조정하는 일로 돌아갈 수 있음
      그래서 사람들이 가능한 모든 기회에 여기저기 무언가를 직접 작성하려는 것도 놀랍지 않음. 이런 디지털 배관공 같은 허드렛일은 사람을 지치게 하니, 제정신을 조금 더 오래 유지하려면 가끔은 더 즐거운 일을 끼워 넣고 싶어짐
    • “지금 필요하지 않으면 구현하지 말라”는 시작할 때 유용한 조언임. 다만 전문가가 깨도 되는 규칙이기도 함
  • 사이트가 접속 폭주로 죽은 듯해서 캐시를 남김: https://web.archive.org/web/20240606103630/https://edw.is/le...

  • 훌륭한 글임. 과학 데이터 시각화 엔진을 만들려고 Vulkan을 직접 배웠음: https://datoviz.org/ 아직 꽤 실험적이고 곧 새 버전을 낼 예정임
    이전에 OpenGL 지식은 있었지만 Vulkan 학습은 정말 어려웠음. 5년 전에는 학습 자료도 그리 좋지 않았음. 그래도 도전했고 무척 재미있었음
    수십 가지 추상화의 역할을 이해하는 데 몇 달이 걸렸고, 그 과정에서 Vulkan을 조금 덜 고통스럽게 쓰기 위한 작은 래퍼도 작성함: https://datoviz.org/api/vklite/
    이 래퍼는 과학 시각화에 가장 필요한 기능 일부만 지원함

  • Vulkan으로 OpenGL보다 좋은 성능을 내는 건 간단하지 않음. Vulkan 드라이버에는 OpenGL 드라이버가 렌더링 파이프라인과 렌더 타깃 설정을 대신 해주던 약 2만 줄가량의 코드가 빠져 있음
    그 코드는 이미 OpenGL 드라이버 안에 있고, 업계 최고의 사람들이 20년 넘게 최적화해온 것임
    그래서 OpenGL이 기본으로 제공하던 기능과 동등한 것을 Vulkan 위에 순진하게 조립한다고 마법처럼 좋은 성능이 나오지 않음. 더 많은 작업이 필요하고, 올바른 펜스와 동기화 원시 도구를 넣는 등 진짜 문제가 쌓이기 시작함
    실제로 무엇을 하는지 알고, 좋은 병렬성과 올바른 동기화로 렌더링을 실행할 수 있을 때에야 Vulkan의 성능 이점을 꿈꿀 수 있음
    취미 개발자인 나는 단순함 때문에 OpenGL ES3를 씀. 나에게는 이미 충분히 좋고, 성가신 Vulkan 정점 디스크립터 디스크립터 디스크립터를 작성하는 것보다 더 급한 일이 많음
    참고로 내 엔진은 여기 있음: https://github.com/ensisoft/detonator

    • DirectX 12와 DirectX 11도 같은 얘기를 들었음. 어떤 책에서는 자신이 뭘 하는지 모르면 DirectX 12가 DirectX 11보다 성능이 더 나쁠 가능성이 높다고 했음
    • 나에게 가장 큰 부분은 셰이더 컴파일러임. OpenGL은 내장돼 있지만 Vulkan은 또 다른 의존성을 끌어와야 함
      Vulkan이 이제 바인드리스 텍스처를 허용한다고 들었으니 디스크립터 난장판은 예전보다 덜 끔찍할 듯함
      Vulkan은 매력적이지만, 지불하고 싶지 않은 높은 초기 비용이 있음
    • Vulkan을 대상으로 하되 더 높은 추상화 수준에서 쓸 수 있도록 OpenGL-to-Vulkan 계층을 라이브러리로 만들 수 없을까?
      그러고 나서 점진적으로 그 라이브러리를 사용 사례에 맞게 최적화한 루틴으로 교체하면 됨
  • Vulkan 자료가 더 늘어나는 건 좋지만, 이것도 화면에 무언가 띄우는 모든 Vulkan 자료에서 봤던 같은 문제를 겪음
    모두 단순한 경우를 보여주기도 전에 Vulkan 위에 또 다른 추상화 계층을 도입함. 늘 vk-bootstrap, volk, vma나 다른 라이브러리를 쓰라고 함
    메모리 관리를 수동으로 하는 예제를 보여주는 자료가 어디 하나라도 있는지 모르겠음. vma를 쓰거나 명세서를 직접 파라는 선택지밖에 없는 듯함. Vulkan SDK 자체 말고는 아무 라이브러리도 추가하지 않는 가장 기본 예제를 요구하는 게 그렇게 과한 일인가?

    • 게임 개발에서는 보통 큰 메모리 덩어리를 앞에서 할당하고 그 안에서 범프 할당자를 쓰는 관행이 있음
      대부분의 게임에는 대략 세 가지 수명이 있음: 영구/시작 시점, 레벨 단위, 프레임 단위
      이 수명들은 중첩되므로 하나의 스택 할당자로 꽤 멀리 갈 수 있음. 프레임이나 레벨이 끝나면 시작 위치로 되돌리면 됨
      더 복잡한 패턴도 있지만 이 방식만으로도 꽤 유용하고, CPU와 GPU 양쪽에서 쓸 수 있음
    • Vulkan은 처음부터 극도로 저수준 API로 설계됐고, OpenGL/DX11 수준까지 끌어올리려면 라이브러리가 필요하다는 생각이 깔려 있었음
      그런 점에서 위에 라이브러리를 광범위하게 쓰는 건 매우 정상적임. 요즘 소프트웨어를 시스템 호출에 직접 대고 작성하지 않는 것과 같음
    • 맞음, 그건 너무 많은 요구임. 문서의 “공식” 예제조차 그렇게 하지 않음. Vulkan 명세를 읽는 건 기술적 헛소리를 견디는 훈련에 가까움
      어떤 사람의 Vulkan 초기화 지침을 Khronos Group 저장소의 코드와 대조하고, Vulkan 1.3 명세를 읽다가, 뭔가를 하려면 명세를 순서대로가 아니라 뒤섞어 읽어야 한다는 걸 깨닫는 순간 실패했다는 게 분명해짐
      그들은 실패했음. 다른 기준으로 봐도 나쁜 작업임. 다만 한 번 해두고 대부분 잊어버릴 수 있어서 전문가들이 크게 불평하지 않을 뿐임
      이 스레드의 다른 댓글에 명세의 장과 절을 주석으로 달아둔 소스 코드 일부를 남겼음. SDL 등과 함께 쓸 수 있는 일반 구현임
      작성 시점 기준 표준 접근은 공식 Vulkan SDK에 포함된 VMA와 Volk를 쓰는 것임. 이것만으로도 현재 상태를 충분히 말해줌
  • 몇 년 동안 간헐적으로 Vulkan을 배우려 했음. 예전에는 OpenGL ES 2와 3을 꽤 잘 알았음
    어려웠던 것 중 하나는 샘플이 아니라 실제 엔진에서 어떻게 써야 하는지 이해하는 것이었음. 많은 샘플은 딱 필요한 만큼만 할당하거나, 부족할 일이 없도록 수백 개를 할당함
    DirectX를 배울 때는 Microsoft의 MiniEngine이 도움이 됐음. 너무 복잡하지 않으면서 디스크립터 할당을 관리하는 DescriptorAllocator 같은 것이 있었기 때문임. Vulkan에도 비슷한 것이 있는지 궁금함
    또 하나 어려운 점은 머티리얼, 메시, 렌더링 순서 같은 좋은 추상화를 어떻게 만들지 아는 것임. 튜토리얼을 넘어가기 위해 공부할 만한 좋은 엔진이나 프레임워크가 있을까?

    • Vulkan은 DirectX 12와 꽤 비슷해서 일부 개념은 그대로 옮겨짐. 메모리 할당에는 VMA라는 라이브러리를 쓰면 도움이 됨. 표준이 세월 동안 쌓아온 몇 가지 성가신 예외 상황을 처리해주고 꽤 강력함
      디스크립터 세트 할당에서는 나에게는 한 가지 패턴만 말이 됨. 풀은 수명이 짧고 많이 생긴다고 가정해야 함. 현재 풀에서 할당이 실패하면 새 풀을 만들고, 직접 디스크립터 개수 카운터를 유지하지 않는 방식임. 표준은 단순 카운팅과 다른 온갖 풀 동작을 허용함. 그 풀을 참조하는 마지막 명령 버퍼가 끝난 뒤 오래된 풀을 버리면 됨
      파이프라인 배리어와 이미지 레이아웃은 정말 골치 아픔. 모든 리소스의 마지막 사용과 마지막 형식을 추적하고 필요한 배리어를 추가하는 계층으로 추상화하는 게 좋음. 복잡해질 수 있지만, 선택적 패스가 있거나 패스 순서를 바꿀 수 있는 등 더 복잡한 상황이 생기면 그만한 가치가 있음
      메시, 머티리얼, 렌더링 순서는 HN 댓글 하나로 요약하기 어렵고, 렌더링 알고리즘 선택에 크게 좌우됨. 아주 일반화된 해법을 제대로 만드는 데 드는 엄청난 노력은 그만한 가치가 없다고 봄
    • 실제 엔진을 보려면 vkQuake 같은 것이 좋은 참고자료임: https://github.com/Novum/vkQuake
  • Vulkan 1.3에서 “Hello, Triangle!”을 작성하려면 무엇이 필요한지 궁금한 가벼운 독자라면 이걸 보면 됨: https://github.com/Planimeter/game-engine-3d/blob/main/src/g...

    • 맞음. 그래도 vk-bootstrap을 쓰면 코드 600줄로 조금 나음: https://github.com/charles-lunarg/vk-bootstrap/blob/main/exa...
      Vulkan 초기화와 기본 스왑체인 관리는 매우 장황하지만, 한 번 해보고 나중에 파이프라인 생성과 관리를 위한 편한 추상화를 만들면 훨씬 나아짐