2024년에 CPU Rasterizer를 바닥부터 다시 짜보는 이유 (feat. Modern C++)


“바퀴를 다시 발명하지 마라”는 조언이 틀린 이유

주니어 엔지니어들에게 우리는 항상 말한다. “이미 있는 라이브러리를 써라”, “바퀴를 다시 발명하지 마라”. 생산성 측면에서 백번 맞는 말이다. 하지만 엔지니어링의 깊이(Depth) 를 논할 때는 이야기가 달라진다. 블랙박스로 감춰진 ‘마법’을 이해하려면, 가끔은 그 바퀴를 직접 깎아봐야 한다.

오늘 소개할 포스트는 이런 엔지니어의 근원적인 호기심을 자극하는 프로젝트다. 바로 Modern C++(C++20)로 처음부터 작성하는 CPU Rasterizer 다. OpenGL이나 Vulkan이 해주는 일을 CPU로 직접 흉내 내며 그 내부를 파헤치는 과정이다. 솔직히 말해서, 요즘처럼 GPU가 비싸고 귀한 시대에 ‘Software Rendering’이라는 단어만 들어도 묘한 향수를 느끼는 건 나뿐만이 아닐 것이다.

Modern C++로 그래픽스 파이프라인 구축하기

이 프로젝트의 저자는 단순히 픽셀을 찍는 것을 넘어, 고정 파이프라인(Fixed-function pipeline)을 제대로 구현하는 것을 목표로 한다. 흥미로운 점은 구닥다리 C 스타일이 아니라, C++20 표준을 적극적으로 활용한다는 점이다.

1. SDL2와 CMake, 그리고 추상화

저자는 윈도우 생성과 픽셀 버퍼 관리를 위해 SDL2를 선택했다. WinAPI나 Xlib을 직접 건드리는 건 정신건강에 해롭기 때문에 아주 합리적인 선택이다. 여기서 눈여겨볼 점은 SDL_Surface를 다루는 방식이다. 보통 튜토리얼들은 void* 캐스팅을 남발하며 코드를 엉망으로 만들기 십상인데, 저자는 시작부터 image_view라는 깔끔한 구조체를 정의하고 들어간다.

namespace rasterizer
{
    struct image_view
    {
        color4ub * pixels = nullptr;
        std::uint32_t width = 0;
        std::uint32_t height = 0;
    };
}

이런 사소한 구조화가 프로젝트가 커졌을 때의 유지보수성을 결정한다. std::fill_n을 사용해 버퍼를 클리어하는 방식이나, namespace를 활용해 API를 설계하는 접근 방식은 모던 C++ 개발자라면 고개를 끄덕일 만한 대목이다.

2. 성능 측정: 3.7ms의 의미

초기 구현에서 저자는 1920x1080 해상도 기준으로 프레임당 약 3.7ms 가 소요된다고 밝혔다. 여기서 재미있는 분석이 나온다.

  • Clear: 0.3ms (메모리 셋팅)
  • Blit: 3.3ms (CPU 버퍼 -> 윈도우 서피스 복사)

아직 삼각형 하나 그리지 않았는데 벌써 3ms를 태웠다. SDL_BlitSurface가 OS 윈도우와 동기화하고 픽셀을 복사하는 비용이 만만치 않다는 뜻이다. 이건 CPU 렌더러가 가질 수밖에 없는 태생적 한계지만, 역설적으로 GPU가 얼마나 많은 일을 대신해주고 있었는지 깨닫게 해준다. 270 FPS면 나쁘지 않지만, 본격적인 셰이딩이 들어가면 이 수치는 처참하게 떨어질 것이다. 그 과정을 지켜보는 게 이 프로젝트의 묘미다.

Hacker News의 반응: 표절 논란과 고인물들의 등판

이 글이 올라온 Hacker News 스레드는 기술적 토론보다 더 흥미로운 논쟁으로 뜨거웠다. 특히 유명한 그래픽스 입문서인 Computer Graphics from Scratch의 저자 Gabriel Gambetta가 직접 등판해 “내 책의 구조와 너무 비슷해서 소름 끼친다”며 표절 의혹을 제기했다.

  • 논란: 챕터 구성, 전개 방식, 심지어 ‘Why’ 섹션까지 비슷하다는 지적.
  • 반박: 블로그 저자는 “그 책을 읽은 적이 없다”고 부인했고, 제3자들이 코드를 분석한 결과 구현 방식(Pseudocode vs Modern C++)과 수학적 접근이 완전히 다르다는 점을 들어 저자를 변호했다.

개인적으로는 그래픽스 튜토리얼의 흐름(점 -> 선 -> 삼각형 -> 래스터라이제이션)은 대부분 수렴 진화(Convergent Evolution)한다고 본다. 바퀴를 만드는 순서는 누구에게나 비슷할 수밖에 없으니까.

더 가치 있는 댓글은 최적화 에 관한 고인물들의 조언이었다.

  • Triangle Rasterization: 한 유저는 “삼각형 래스터라이제이션은 쉽지만, 픽셀 겹침(Overlapping)이나 틈(Gap) 없이 인접한 삼각형을 렌더링하는 건 악몽”이라고 경고했다. 이는 30년 전 16-bit 시절부터 내려오는 난제다.
  • PS3 SPU 최적화: 또 다른 유저는 과거 PS3 SPU에서 구현했던 코드를 공유하며, x.dx + y.dy + C = 0 형태의 하프-라인 방정식(Half-line equation)을 이용한 최적화 기법을 소개했다. 이런 게 진짜 돈 주고도 못 사는 현장의 지혜다.

Verdict: 엔지니어라면 한 번쯤은

이 프로젝트는 상용 게임을 만들기 위한 것이 아니다. 하지만 Graphics API가 블랙박스처럼 느껴지는 엔지니어 라면, 주말에 시간을 내어 따라 해볼 가치가 충분하다.

현대의 고수준 API 뒤에 숨겨진 픽셀 단위의 처리를 이해하고 나면, 셰이더 코드를 짤 때나 렌더링 파이프라인을 최적화할 때 시야가 달라진다. “GPU가 알아서 해주겠지”라는 안일한 생각에서 벗어나, 데이터가 어떻게 흘러가는지 뼈저리게 느낄 수 있기 때문이다.

결론: 3D 그래픽스를 제대로 이해하고 싶다면, 남이 만든 엔진을 쓰기 전에 딱 한 번만이라도 직접 픽셀을 찍어보자. 그 고통스러운 과정이 당신을 ‘코더’에서 ‘엔지니어’로 만들어줄 것이다.