WebGPU와 WebGL2
이 페이지에서

WebGPU와 WebGL2

호스트가 TypeShade 모듈을 쓰는 방법은 세 가지입니다. 모듈을 컴파일해 셰이더 텍스트를 자신의 WebGPU나 WebGL2 코드에 넘기면, 디바이스와 파이프라인, 바인드 그룹, 버퍼, 텍스처는 그 코드가 소유합니다. Vite 플러그인으로 모듈을 가져와 진입점을 부르면, 호출에 필요한 그 객체들은 TypeShade의 런타임이 만듭니다. 컴파일된 프로그램을 프로그램 런타임에 불러와 자신의 프레임에서 그릴 수도 있습니다. 이때 그 객체들은 런타임이 만들고, 렌더 상태와 프레임 루프는 호스트가 맡습니다. 어느 쪽이 무엇을 소유하는지 알면 첫 TypeShade 프로그램에 필요한 것은 거의 다 아는 셈입니다.

소유 관계

WebGPU 애플리케이션이 만드는 객체마다 한 행이며, 호스트가 모듈을 컴파일해 자신의 코드에 연결하는 경우를 보여 줍니다.

객체애플리케이션이 하는 일TypeShade가 보태는 것
디바이스어댑터와 GPUDevice를 요청하고 페이지가 살아 있는 동안 들고 있습니다.없습니다. 컴파일된 모듈은 WebGPU 객체를 건드리지 않습니다.
파이프라인렌더 파이프라인이나 컴퓨트 파이프라인을 만들고 스테이지마다 진입점 이름을 지정합니다.모듈의 셰이더 텍스트와, 그 안의 진입점 이름을 모두 제공합니다.
바인드 그룹 레이아웃바인딩마다 그룹, 번호, 종류, 그리고 그 바인딩을 보는 스테이지를 적습니다.reflect()가 컴파일된 모듈에서 같은 내용을 읽어 돌려줍니다.
버퍼버퍼를 할당하고 바이트를 씁니다.유니폼 구조체의 필드마다 오프셋과 크기와 타입을 알려 줍니다.
텍스처와 샘플러둘을 만들어 바인드 그룹에 넣습니다.셰이더가 선언한 바인딩과, 거기에서 기대하는 타입을 알려 줍니다.

모듈 가져오기

호스트는 typeshade/vite 플러그인으로 .shade.ts를 자신의 TypeScript에 가져와, 모듈이 내보내는 것을 부를 수도 있습니다. 헬퍼는 CPU에서 실행됩니다. @compute 진입점은 await entry(bindings, workgroups)로 부르고, 전체 화면 @fragment 진입점은 entry(canvas, bindings)로 WebGPU, WebGL2, CPU 순서로 그립니다. 이때 위 표에서 애플리케이션이 하던 일은 런타임이 맡습니다. 첫 호출에서 디바이스를 요청해 이후 호출과 함께 쓰고, 진입점마다 파이프라인을 만들며, 호출이 넘긴 객체로 바인딩을 채우고, 컴퓨트 진입점이 쓴 값을 다시 읽어 옵니다.

프로그램 불러오기

엔진이나 렌더러처럼 프레임을 직접 그리는 호스트는 컴파일된 프로그램을 프로그램 런타임 typeshade/runtime에 불러와 실행합니다. 프로그램은 매니페스트라는 객체 하나로 전달되며, 여기에 셰이더 코드와 바인딩, 진입점이 모두 들어 있습니다. packModule(compile(source).module)이 이 객체를 돌려주고, 호스트에서 모듈을 가져오면 기본 내보내기로 받습니다. 평범한 JSON이어서 빌드가 디스크에 그대로 쓸 수도 있습니다.

import { createRuntime } from 'typeshade/runtime'
import scene from './scene.shade.ts' // the manifest
const rt = await createRuntime({ device })
const format = navigator.gpu.getPreferredCanvasFormat()
context.configure({ device, format })
const draw = await rt.load(scene).render({ targets: [format] })
const frame = rt.frame()
frame.pass({ color: [context] }, (pass) => {
pass.draw(draw, { u: { time } }, { count: 3 })
})
await frame.submit()

이때 위 표에서 애플리케이션이 하던 일은 대부분 런타임이 맡습니다. createRuntime({ device })에 애플리케이션의 GPUDevice를 넘기면 그 디바이스를 쓰되 파괴하지는 않고, 넘기지 않으면 프로그램에 필요한 기능을 갖춘 디바이스를 직접 요청합니다. 파이프라인과 바인드 그룹은 매니페스트에 적힌 레이아웃대로 만듭니다. 그리기나 디스패치가 이름으로 넘긴 값으로 바인딩을 채우고, 버퍼와 텍스처, 샘플러도 런타임이 만듭니다. 애플리케이션은 컴파일러가 알 수 없는 것을 정합니다. 파이프라인별 타깃과 깊이, 토폴로지, 그리고 프레임을 언제 그릴지가 여기에 속합니다. 애플리케이션이 직접 만든 GPUBuffer, GPUTexture, GPUSampler는 그대로 바인딩되고, 프레임의 encoder와 패스의 raw 인코더에는 애플리케이션 자신의 명령을 기록할 수 있습니다.

프로그램의 override 값은 이름으로 지정합니다. 그릴 때는 RenderState.constants, 디스패치할 때는 Program.compute()의 constants 옵션으로 넘깁니다. Frame.submit()은 기록한 진입점마다 콘솔 줄 수와 버퍼 공간이 부족해 기록하지 못한 호출 수를 돌려줍니다. 콘솔 sink를 쓰는 호스트는 이 수치로 누락된 호출을 알릴 수 있습니다.

빌드 뒤에 콘솔 기록을 켜는 호스트는 typeshade/emit의 repack을 createRuntime({ emit: repack })으로 넘길 수 있습니다. 이때 매니페스트는 packModule(module, { ir: true })로 만들어 이식 가능한 IR을 담아야 합니다. 로드 시점 이미터는 매니페스트에 저장된 emit 옵션을 쓰며, TypeScript 프런트엔드를 포함하지 않습니다.

프로그램 런타임은 WebGPU에서만 동작합니다. WebGL2에서 그리는 호스트는 모듈을 컴파일하거나, 모듈을 가져와 진입점을 부릅니다.

리플렉션

reflect()는 컴파일된 모듈을 읽어 그 바인딩을 돌려줍니다. 셰이더가 선언한 그룹과 번호, 주소 공간, 셰이더에 필요한 접근 권한이 들어 있고, 유니폼 구조체라면 std140과 std430 레이아웃에 따른 필드별 오프셋과 크기도 함께 있습니다. 호스트는 그 목록으로 바인드 그룹 레이아웃 항목을 만들고, 그 오프셋대로 유니폼 버퍼를 채웁니다. 셰이더를 컴파일할 때 쓰인 수치를 호스트가 그대로 쓰므로 양쪽이 어긋나지 않습니다.

const shader = emitModule(paintModule)
const layout = reflect(paintModule)
for (const group of layout.bindGroups) {
for (const entry of group.entries) {
// entry.group, entry.binding, entry.space, entry.access, entry.resourceKind
}
}
for (const struct of layout.uniforms) {
for (const field of struct.fields) {
// field.name, field.type, field.offset, field.size
}
}

셰이더에서 필드 이름을 바꾸면 다음 빌드에서 리플렉션이 따라 바뀌고, 리플렉션을 읽는 호스트 코드도 함께 따라옵니다.

런타임

호스트가 컴파일하는 모듈은 브라우저에서 TypeShade의 어떤 코드도 필요로 하지 않습니다. 컴파일러는 셰이더 텍스트가 만들어지는 곳에서 돌아갑니다. 빌드, 테스트, 그리고 언어 서비스를 거친 편집기가 그런 곳입니다. 브라우저에 도달하는 것은 생성된 셰이더 소스와, 애플리케이션이 원래 쓰던 호스트 코드뿐입니다. 호스트가 가져오는 모듈은 typeshade/runtime을 번들에 싣습니다. 호출은 이 코드에서 실행되고, 컴파일러는 번들에 들어가지 않습니다. Vite 플러그인이 번들을 만들 때 모듈을 컴파일하기 때문입니다. 호출은 브라우저가 가진 첫 tier(호출이 실행되는 백엔드)에서 실행되며, 순서는 WebGPU, WebGL2, CPU입니다. configure({ prefer })는 compute 진입점과 커널 함수가 시도할 tier의 순서를 정하고, Resident는 호출과 호출 사이에 배열을 GPU에 둡니다. 프로그램 런타임에 프로그램을 불러오는 호스트도 같은 typeshade/runtime과 매니페스트만 싣고, 컴파일러는 싣지 않습니다. TypeShade가 설치하는 런타임 의존성은 1개, compile()과 언어 서비스가 소스를 읽을 때 쓰는 TypeScript뿐입니다.

WebGL2에서 달라지는 것

같은 소스가 WebGL2용으로도 컴파일되고, 호스트 쪽 모습은 달라집니다.

  • 바인드 그룹이 없습니다. 유니폼 블록은 링크된 프로그램의 바인딩 지점에 묶이고 샘플러는 유니폼 위치로 설정하므로, 호스트는 같은 리플렉션을 다른 모양으로 씁니다.
  • 컴퓨트 스테이지가 없습니다. @compute 진입점이 있는 모듈은 WGSL을 생성하고 GLSL ES 3.00 생성은 거부합니다.
  • 정밀도는 소스의 몫입니다. 생성된 GLSL ES 3.00 프로그램은 선언들 위에 기본 정밀도를 적어 두며, WGSL에는 그럴 필요가 없습니다.
  • GPU 기능은 호스트가 컨텍스트에 확장을 요청해서 켭니다. 지시문이 있는 확장이라면 생성된 소스도 그 확장을 선언합니다. 컴파일러가 이 두 몫을 어떻게 나누는지는 컴파일러 내부가 설명합니다.

더 읽을 자료

이 경로의 다음 글은 WGSL과 GLSL입니다. 소스 하나가 두 타깃에서 무엇으로 컴파일되는지 그대로 보여 줍니다.

이 페이지 편집 문제 보고