WebGPU라는 단단한 대륙 위에서, 새로운 도시들이 건설되기 시작했다. Babylon.js와 Three.js라는 두 개의 거대한 고대 도시는, 자신들의 낡은 도로(WebGL 아키텍처)를 걷어내고 WebGPU라는 새로운 고속도로를 깔며 성공적으로 현대화에 성공했다. 개발자들은 이 익숙하고 안정적인 도시들 안에서 안락함을 느끼고 있었다.
그 평화로운 지형도를 뒤흔든 것은, 황무지에서 갑자기 솟아오른 신생 도시의 등장이었다.
시작은 한 젊고 야심 찬 개발자, ‘리오’가 자신의 블로그에 올린 선언문과도 같은 글이었다.
“Rusted-GPU: WebGPU 시대를 위한, 타협 없는 첫 번째 렌더링 엔진을 공개합니다.”
리오가 공개한 ‘Rusted-GPU’는 처음부터 WebGPU만을 위해, Rust와 WebAssembly로 작성된 엔진이었다. 그에게는 지켜야 할 과거의 유산도, 하위 호환성을 위해 짊어져야 할 짐도 없었다.
그의 글은 예의 바르지 않았다. 오히려 날카롭고 도발적이었다.
“우리는 언제까지 WebGL 시대의 낡은 습관에 머물러 있어야 합니까? 기존의 대형 라이브러리들은 WebGPU를 지원한다고 말하지만, 그들의 심장부에는 여전히 ‘상태를 실시간으로 바꾸는’ 낡은 철학이 남아있습니다. 그들은 강력한 스포츠카에 마차 바퀴를 달고 있는 것과 같습니다.”
그는 구체적인 벤치마크 데이터를 첨부했다.
수천 개의 다른 재질을 가진 객체들을 렌더링하는 테스트.
Three.js는 렌더링 루프 안에서 각 재질에 맞춰 파이프라인을 동적으로 찾거나 생성하느라 미세한 지연을 보였다.
하지만 Rusted-GPU는 달랐다. 모든 가능한 파이프라인의 조합을 애플리케이션 시작 시점에, 병렬 컴퓨팅을 통해 극도로 빠르게 생성하고 해시맵에 저장해두었다. 렌더링 루프에서는 오직 해시값을 이용한 즉각적인 조회만 일어날 뿐이었다.
결과는 Rusted-GPU의 압도적인 승리였다.
이 글은 개발자 커뮤니티에 핵폭탄을 던졌다.
포럼과 소셜 미디어는 두 개의 파벌로 나뉘어 격렬한 전쟁을 시작했다.
“이것이야말로 진짜 WebGPU다! 과거의 망령에서 벗어날 때가 왔다!”
“Three.js의 거대한 생태계와 수많은 예제를 무시하는가? 성능이 전부는 아니다!”
“순수함이냐, 안정성이냐. 이것은 철학의 싸움이다.”
혼란은 W3C의 공식 깃허브 이슈 트래커에까지 번졌다.
한 개발자는 드미트리를 직접 태그하며 질문을 던졌다.
“@dmalyshau, 당신이 생각하는 ‘올바른’ WebGPU 사용법은 무엇입니까? Rusted-GPU와 같은 방식이 WebGPU가 의도한 본래의 사용법인가요? 커뮤니티에 공식적인 가이드라인을 제시해 주십시오.”
드미트리는 이 모든 상황을 무거운 마음으로 지켜보고 있었다.
이것은 그가 예상했지만, 피하고 싶었던 시나리오였다.
플랫폼이 성공하자, 그 플랫폼의 ‘정통성’을 두고 새로운 전쟁이 벌어진 것이다.
그가 만약 Rusted-GPU의 손을 들어준다면, 그것은 수년간 WebGPU 생태계에 기여해 온 Three.js 커뮤니티 전체를 부정하는 행위가 될 터였다.
반대로 Three.js의 편을 든다면, 그는 혁신을 가로막고 과거에 안주하려는 보수적인 관리자로 비칠 것이었다.
그의 입에서 나오는 모든 말은 이제, 의도와 상관없이 한쪽을 지지하고 다른 쪽을 공격하는 무기가 될 수 있었다.
그는 며칠간 침묵했다.
그는 이 논쟁에 직접 뛰어드는 대신, 더 근본적인 해결책을 고민했다.
‘나는 심판이 되어서는 안 된다. 나는 이들이 공정하게 경쟁할 수 있는 규칙과 경기장을 만들어주는 사람이다.’
그는 새로운 종류의 공식 문서를 작성하기 시작했다.
그 문서의 제목은 ‘WebGPU 성능 모범 사례(Best Practices)’였다.
문서의 내용은 특정 라이브러리를 언급하지 않았다.
대신, WebGPU 애플리케이션의 성능을 결정하는 핵심 원칙들을 상세히 기술했다.
- 파이프라인 생성 비용:
createRenderPipelineAsync
는 무거운 작업이다. 런타임에 빈번하게 호출하는 것을 피하고, 가능한 한 초기에 생성하여 캐싱하라. - Bind Group 관리: Bind Group은 가볍게 생성할 수 있지만, 너무 자주 생성하면 병목이 될 수 있다. 가능한 한 여러 드로우 콜에 걸쳐 재사용하라.
- 메모리 관리:
writeBuffer
와mapAsync
의 차이점. 데이터 업로드 빈도에 따라 어떤 방식이 더 유리한가. - 자원 상태 추적:
destroy()
를 호출하는 최적의 시점은 언제인가.
그가 작성하는 것은 어느 한쪽을 위한 변호가 아니었다.
그것은 모든 개발자들이 스스로의 코드를, 그리고 다른 라이브러리들을 객관적으로 평가할 수 있는 ‘기준점’을 제시하는 작업이었다.
문서 작성을 마치고, 드미트리는 그 링크를 논쟁이 벌어지던 깃허브 이슈에 조용히 게시했다.
그는 아무런 부연 설명도 달지 않았다.
그는 이제 깨닫고 있었다.
자신의 역할은 왕을 선택하는 것이 아니라, 현명한 시민들이 스스로 왕을 선택할 수 있도록, 가장 정확하고 공정한 법전을 집필하는 것이라는 것을. 플랫폼의 관리자로서, 그는 이제 기술을 넘어 ‘문화’를 설계해야 하는 새로운 단계에 접어들고 있었다.