📱 Erkannter Endgerättyp ⛱️ Tag und Nacht. Verbraucht keinen oder einen 🍪.
guest
Login 🍪 1 Anzahl Ihrer gespeicherten Kekse, führt zur Keksdose

DRAFT

WebGPU für Kategorientheoretiker

Hier sammel ich meine Dokumentation für WebGPU. WebGPU erlaubt das Verschalten beliebiger Morphismen direkt auf der GPU was in etwa einem Orchester mit ~1000 immer korrekt spielenden Instrumenten gleichkommt.

Leider ist die Dokumentation häufig auf 3D ausgerichtet, da Compute-Shader erst später dazukamen. Um optimal zu programmieren ist wichtig zu verstehen welcher Speicher auf der GPU genutzt wird, wann Schnittstellen zur CPU genutzt werden und natürlich wie man von einem Shader zum andern Daten übergibt.

Ich dokumentiere daher immer vom CPU-Space zum GPU-Space hin, ab dem Fragment-Shader erfolgt die Ausgabe an den Bildschirm.

Im Compute-Shader kann man noch Daten zum CPU-Space zurückgeben (<->) hier kann der Output also auch wieder unser Programm sein, danach ist alles mehr oder weniger unidirektional (->) und der Output ist der 2D-Clipspace des Ausgabefensters auf dem Bildschirm.

Glossar

🖼️ - Canvas-HTML5-Element, nutzen wir für die Größenkorrektur der Ausgabe

const – The declaration is of a compile time constant.

attribute – Global variables that may change per vertex, that are passed from the OpenGL application to vertex shaders. This qualifier can only be used in vertex shaders. For the shader this is a read-only variable. See Attribute section.

@builtin(vertex_index) <-- Attribute

uniform – Global variables that may change per primitive [...], that are passed from the OpenGL application to the shaders. This qualifier can be used in both vertex and fragment shaders. For the shaders this is a read-only variable. See Uniform section.

varying – used for interpolated data between a vertex shader and a fragment shader. Available for writing in the vertex shader, and read-only in a fragment shader. See Varying section.

Voraussetzungen

Ich kürze hier viel ab, wir prüfen z.B. nicht ob der Client WebGPU unterstützt und streichen alle Promises weg um ein virtuelles GPU-Device zu erhalten:

🖼️ Canvas

const canvas = document.getElementById('webgpuscribble') as HTMLCanvasElement ?? document.createElement('canvas') as HTMLCanvasElement;
const canvasContainerDiv = canvas.parentElement!;

canvas.width = canvasContainerDiv.clientWidth
canvas.height = canvasContainerDiv.clientHeight

GPU-Device

Wir arbeiten mit mehreren Pipelines, aber immer nur einem device. Ja es ist machbar mehrere virtuelle Devices zu erhalten, das macht die Dinge leider nicht sauberer, sondern nur langsamer. Schneller und noch sauber ist mehrere Pipelines auf einem Device (wir setzen dort die Separation nach Sub-Kategorie an)

adapter represents the GPU itself whereas device represents an instance of the API on that GPU.

Von der CPU zur GPU: Bind-Groups

Uniforms, storage buffers, textures --> bind group

let und var in WGSL

let - immutable

var - mutable

Built-ins

Bekommen wir von der GPU gesetzt in Vertex-Shadern,

🎯 Vertex Shader Inputs

Variable Type Meaning
@builtin(vertex_index) u32 Auto-generated index per vertex. Use when not using vertex buffers or procedural geometry.
@builtin(instance_index) u32 Current instance (used in instanced rendering) — think of this like a "loop index" for multiple draws of the same geometry.

🌀 Vertex Shader Outputs

Variable Type Meaning
vs @builtin(position) vec4<f32> Final clip-space position — required for rasterization.

🔬 Also:

Der Vertex Shader läuft n Mal → 1× pro Vertex.

Der Rasterizer interpoliert Outputs der Vertex Shader → z. B. Farben, UVs, Positionen.

Der Fragment Shader wird nur für gültige Fragmente (Pixel im Dreieck) ausgeführt.

Kein expliziter Index nötig — @builtin(frag_coord) sagt dir, wo du gerade bist.

Schleifen brauchst du nur, wenn du selbst mehrere Samples, Lichtquellen, oder ähnliches berechnen willst. Nicht um über Pixel zu iterieren — das macht die GPU schon für dich.

Der Fragment shader wird vom Clipspace aus PRO primitive aufgerufen und loopt dann über frag_coord:

🎨 Fragment Shader Inputs

Variable Type Meaning
fs @builtin(position) vec4<f32> Interpolated position from vertex shader (clip space → screen space).
@builtin(front_facing) bool True if current triangle is front-facing (useful for lighting or backface culling).
@builtin(sample_index) u32 Used in MSAA contexts.
@builtin(sample_mask) u32 MSAA mask if you're doing advanced sampling.
@builtin(frag_coord) vec4<f32> Screen-space position, including depth (z) — very useful for screen effects.

the fragment shader output is always location(0) vec4f as this is the canvas texture - location(n) here references the target in

fragment: {
    module: shaderModule,
    targets: [
        {format: presentationFormat}, <-- location(0)
    ],
},

💻 Compute Shader Inputs

Variable Type Meaning
@builtin(local_invocation_id) vec3<u32> Thread ID inside a workgroup.
@builtin(global_invocation_id) vec3<u32> Global thread ID (across all workgroups).
@builtin(workgroup_id) vec3<u32> ID of the current workgroup.
@builtin(num_workgroups) vec3<u32> Total workgroups dispatched.
@builtin(local_invocation_index) u32 Linear index inside the workgroup.

WebGL and WebGPU fundamentals writeup

attributes, a way to specify data pulled from buffers and fed to each iteration of a vertex shader.

uniforms, a way to specify values shared by all iterations of a shader function.

varyings, a way to pass data from a vertex shader to a fragment shader and interpolate between values computed by the vertex shader when rasterizing via a fragment shader.

textures and samplers, ways to provide 2D or 3D data and sample it (filter multiple pixels into a single value).

[unnamed] ways to render to textures.

bunch of settings for how pixels are blended, how the depth buffer and stencil buffers work, etc…

Locations

In WebGPU gibt es keine named indices. WebGL würde erlauben:

function likeWebGL(inputs) {
    const {position, texcoords, normal, color} = inputs;
    ...
}

const inputs = {};
inputs.normal = normal;
inputs.color = color;
inputs.position = position;

WebGPU kennt hier keine Klassen/Instanzen, nur arrays

function likeWebGPU(inputs) {
    const [position, texcoords, normal, color] = inputs;
    ...
}

const inputs = [];
inputs[0] = position;
inputs[2] = normal;
inputs[3] = color;
likeWebGPU(inputs);

Wir müssen hier daher die @location (index) für jedes attribut kennen:

const shaderSrc = `
struct VSUniforms {
    worldViewProjection: mat4x4f,
    worldInverseTranspose: ma bt4x4f,
};

@group(0) binding(0) var<uniform> vsUniforms: VSUniforms;

struct MyVSInput {
    @location(0) position: vec4f,
    @location(1) normal: vec3f,
    @location(2) texcoord: vec2f,
};

struct MyVSOutput {
@builtin(position) position: vec4f,
@location(0) normal: vec3f,
@location(1) texcoord: vec2f,
};

@vertex
fn myVSMain(v: MyVSInput) -> MyVSOutput {
var vsOut: MyVSOutput;
vsOut.position = vsUniforms.worldViewProjection * v.position;
vsOut.normal = (vsUniforms.worldInverseTranspose * vec4f(v.normal, 0.0)).xyz;
vsOut.texcoord = v.texcoord;
return vsOut;
}

Multiple Render Targets(???)

WebGPU you have to do much of that yourself. If you want a depth buffer you create that yourself (with or without a stencil buffer). If you want anti-aliasing, you create your own multisample textures and resolve them into the canvas texture.

But, because of that, unlike WebGL, you can use one WebGPU device to render to multiple canvases. 🎉🤩

Complete Pipeline example

const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    buffers: [
      // position
      {
        arrayStride: 3 * 4, // 3 floats, 4 bytes each
        attributes: [
          {shaderLocation: 0, offset: 0, format: 'float32x3'},
        ],
      },
      // normals
      {
        arrayStride: 3 * 4, // 3 floats, 4 bytes each
        attributes: [
          {shaderLocation: 1, offset: 0, format: 'float32x3'},
        ],
      },
      // texcoords
      {
        arrayStride: 2 * 4, // 2 floats, 4 bytes each
        attributes: [
          {shaderLocation: 2, offset: 0, format: 'float32x2',},
        ],
      },
    ],
  },
  fragment: {
    module: shaderModule,
    targets: [
      {format: presentationFormat},
    ],
  },
  primitive: {
    topology: 'triangle-list',
    cullMode: 'back',
  },
  depthStencil: {
    depthWriteEnabled: true,
    depthCompare: 'less',
    format: 'depth24plus',
  },
  ...(canvasInfo.sampleCount > 1 && {
      multisample: {
        count: canvasInfo.sampleCount,
      },
  }),
});

Current PTF

Hintergrund ändern. Verbraucht keinen oder einen 🍪.

Verknüpften Viewport öffnen

🎮 Steuerung
Dokumentation 🕹️

Content Nodes Amount

Diligence / PTF Amount

FPS

Vertex-Count