Pure Go에서 셰이더 컴파일러 구축: naga가 v0.4.0에 도달

발행: (2025년 12월 12일 오전 09:58 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Cover image for Building a Shader Compiler in Pure Go: naga Reaches v0.4.0

TL;DR

naga는 WGSL(WebGPU Shading Language)을 SPIR‑V 바이트코드로 컴파일합니다.

  • 순수 Go 17 000줄.
  • CGO 없음. 외부 의존성 없음. go build만 하면 됩니다.

v0.4.0에서는 원자 연산과 배리어를 지원하는 컴퓨트 셰이더를 지원합니다.

  • 완전한 타입 추론 시스템
  • 203개의 테스트, 약 61 % 커버리지
  • 그래픽 및 컴퓨트 워크로드에 대한 프로덕션 레디

왜 셰이더 컴파일러를 직접 만들까?

Go에서 GPU 프로그래밍을 해본 사람이라면 이런 벽에 부딪히게 됩니다: 셰이더 컴파일은 언제나 외부 도구(러스트의 naga, 구글의 glslc, NVIDIA 툴체인 등)를 필요로 합니다. 그 결과:

  • 추가 빌드 의존성
  • 플랫폼별 바이너리
  • CGO 혹은 서브프로세스 호출
  • 복잡한 배포

GoGPU 생태계는 이 마찰을 없애는 것을 목표로 합니다. 순수 Go 셰이더 컴파일러는 다음을 의미합니다:

go build ./...
# 그게 전부. cmake도, 러스트 툴체인도, DLL도 필요 없습니다.

여정: v0.1.0 → v0.4.0

v0.1.0 — 기본 구축 (~10 K LOC)

  • 140개 이상의 토큰을 인식하는 WGSL 렉서
  • 재귀 하강 파서
  • 중간 표현(IR) (표현식 33종, 문장 16종)
  • 100개 이상의 opcode를 지원하는 SPIR‑V 바이너리 라이터

결과: 정점 및 프래그먼트 셰이더를 성공적으로 컴파일.

v0.2.0 — 타입 시스템 (~2 K LOC)

핵심 난관: 타입 추론. SPIR‑V는 모든 것에 명시적인 타입을 요구하지만 WGSL은 추론을 허용합니다:

let x = 1.0;           // f32
let v = vec3(1.0);     // vec3
let n = normalize(v); // vec3, 함수 반환값으로부터 추론

모든 표현식에 걸쳐 타입을 추적하는 완전한 타입 해석 엔진을 구축했습니다.

v0.3.0 — 텍스처 (~3 K LOC)

그래픽의 핵심인 텍스처 연산을 추가했습니다:

@fragment
fn main(@location(0) uv: vec2) -> @location(0) vec4 {
    return textureSample(myTexture, mySampler, uv);
}

OpSampledImage, OpImageSampleImplicitLod 등 SPIR‑V 이미지 연산을 구현했습니다.

v0.4.0 — 컴퓨트 셰이더 (~2 K LOC)

최신 릴리스는 GPU 컴퓨트 기능을 제공합니다:

@group(0) @binding(0)
var counter: atomic;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3) {
    atomicAdd(&counter, 1u);
    workgroupBarrier();
}

주요 추가 기능

  • 스토리지 버퍼 접근 모드: read, read_write
  • 워크그룹 공유 메모리: var
  • 9가지 원자 연산: add, sub, min, max, and, or, xor, exchange, compare‑exchange
  • 3가지 배리어 종류: workgroup, storage, texture
  • 주소 연산자: 원자 포인터용 &

아키텍처

┌─────────────────────────────────────────────────────┐
│                   WGSL Source                        │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│              Lexer (140+ tokens)                     │
│         wgsl/lexer.go — ~400 LOC                     │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│         Parser (recursive descent)                   │
│        wgsl/parser.go — ~1400 LOC                    │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│              AST → IR Lowering                       │
│         wgsl/lower.go — ~1100 LOC                    │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│          Intermediate Representation                 │
│    33 expression types, 16 statement types           │
│     Type inference + deduplication                   │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│             SPIR‑V Backend                           │
│       spirv/backend.go — ~1800 LOC                  │
│       100+ opcodes, GLSL.std.450                     │
└─────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│              SPIR‑V Binary                           │
│         Vulkan‑compatible bytecode                   │
└─────────────────────────────────────────────────────┘

사용법

라이브러리로 사용

import "github.com/gogpu/naga"

func main() {
    source := `
@vertex
fn main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4 {
    return vec4(0.0, 0.0, 0.0, 1.0);
}
`
    spirv, err := naga.Compile(source)
    if err != nil {
        log.Fatal(err)
    }
    // spirv is ready for Vulkan
}

CLI 도구

go install github.com/gogpu/naga/cmd/nagac@latest

nagac shader.wgsl -o shader.spv
nagac -debug shader.wgsl -o shader.spv  # 디버그 이름 포함

경고와 함께 사용

v0.4.0에서는 사용되지 않은 변수 감지를 도입했습니다:

result, err := naga.LowerWithWarnings(ast)
for _, w := range result.Warnings {
    fmt.Printf("Warning: %s at line %d\n", w.Message, w.Span.Line)
}

_로 시작하는 변수는 의도적으로 무시됩니다(Go 스타일).

지원 기능

카테고리기능
타입f32, f64, i32, u32, bool, vec2‑4, mat2x2‑4x4, 배열, 구조체, 원자
텍스처texture_2d, texture_3d, texture_cube, sampler
셰이더@vertex, @fragment, @compute
바인딩@location, @group/@binding, @builtin
스토리지uniform, storage (read/read_write), workgroup
함수50개 이상의 내장 함수(수학, 기하, 보간, 원자, 배리어)
제어 흐름if/else, for, while, loop, break, continue

앞으로의 계획

v0.5.0

  • GLSL 백엔드 – OpenGL 호환성을 위한 GLSL 출력
  • 소스 맵 – SPIR‑V와 WGSL 사이의 디버그 정보 매핑
  • 최적화 패스 – 상수 폴딩, 죽은 코드 제거

v1.0.0

  • WGSL 사양 완전 호환
  • DirectX/Metal을 위한 HLSL/MSL 백엔드
  • 프로덕션 수준 하드닝

성능

컴파일 속도가 빠릅니다. 일반적인 셰이더는 5 ms 이하로 컴파일됩니다. 전체 테스트 스위트(203개 테스트)는 약 2 초에 실행됩니다.

Rust의 naga와 아직 벤치마크는 없지만, 목표는 원시 속도가 아니라 순수 Go 솔루션을 제공해 Go 프로젝트에 매끄럽게 통합하는 것입니다.

Back to Blog

관련 글

더 보기 »

고성능 GPGPU와 Rust 및 wgpu

컴퓨트 애플리케이션의 아키텍처 GPGPU 애플리케이션은 전통적인 렌더링 루프와 크게 다릅니다. 그래픽 컨텍스트에서 파이프라인은 c...

Go 서버에서 고성능 SQLite 읽기

워크로드 가정 이 권장 사항은 다음을 전제로 합니다: - 읽기가 쓰기보다 우세하고 쓰기는 드물거나 오프라인 - 단일 서버 프로세스가 데이터베이스를 소유함 - 다중 goroutine…