Menu

Emerging Tech Topic

WebGPU: Next-Generation Graphics & Shaders

1. WebGPU Fundamentals

What is WebGPU?

WebGPU is a modern, low-level graphics API for the web that provides direct access to the GPU (Graphics Processing Unit) from web applications. It's designed to succeed WebGL, offering better performance, more flexibility, and access to modern GPU features.

Think of it as: Vulkan/DirectX 12/Metal for the web – giving web apps near-native GPU performance.

A. Why WebGPU? (The WebGL Problem)

WebGL (based on OpenGL ES) was great but has limitations:

  • Outdated architecture (designed for 2007-era hardware)

  • CPU bottleneck - too much driver overhead

  • Missing modern GPU features (compute shaders, advanced parallelism)

  • Inconsistent performance across browsers/devices

WebGPU fixes these by:

  1. Reducing CPU overhead (more work done on GPU)

  2. Exposing modern GPU capabilities

  3. Better performance (often 2-3x faster than WebGL)

  4. Cross-platform consistency


Core Concepts

1. The Rendering Pipeline

WebGPU uses an explicit, modern pipeline:

text

CPU → Command Encoding → Command Buffer → GPU Execution

Key Pipeline Stages:

  • Vertex Shader (process vertices)

  • Primitive Assembly (form triangles/lines)

  • Rasterization (convert to pixels)

  • Fragment Shader (color each pixel)

  • Output Merging (final image composition)

2. Key WebGPU Objects

Object Purpose Analogy
Adapter Represents physical GPU Graphics card
Device Logical connection to GPU Graphics driver session
Buffer Memory on GPU (vertices, uniforms) GPU RAM allocation
Texture Image data on GPU GPU image storage
Bind Group Collection of resources for shaders Parameter bundle
Pipeline Complete rendering configuration Blueprint for drawing
Command Encoder Records GPU commands Instruction recorder
Render Pass Drawing operations batch Drawing session

WebGPU Shaders: WGSL

What is WGSL?

WGSL (WebGPU Shader Language) is WebGPU's shading language. It's similar to Rust in syntax and designed for safety and performance.

Basic WGSL Shader Example

wgsl

// Vertex Shader
struct VertexInput {
    @location(0) position: vec3f,
    @location(1) color: vec3f,
};

struct VertexOutput {
    @builtin(position) position: vec4f,
    @location(0) color: vec3f,
};

@vertex
fn vertex_main(input: VertexInput) -> VertexOutput {
    var output: VertexOutput;
    output.position = vec4f(input.position, 1.0);
    output.color = input.color;
    return output;
}

// Fragment Shader
@fragment
fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
    return vec4f(input.color, 1.0);
}

WebGPU vs WebGL Comparison

Feature WebGL WebGPU
API Design State machine (global state) Explicit, object-oriented
Performance Good, but CPU-bound Excellent, GPU-bound
Compute Shaders No (except WebGL 2 compute) Yes, first-class citizen
Parallelism Limited Massive parallel draws
Memory Model Implicit Explicit, controllable
Error Handling Global error flags Promise-based, explicit
Shader Language GLSL (text-based) WGSL (compile-time safe)

Key WebGPU Features

1. Compute Shaders (Game Changer)

javascript

// WebGPU compute pipeline example
const computePipeline = device.createComputePipeline({
    compute: {
        module: computeShaderModule,
        entryPoint: 'compute_main'
    }
});

Use cases: Physics simulations, particle systems, image processing, AI inference, ray tracing.

2. Parallel Command Encoding

javascript

// Multiple command encoders can run in parallel!
const encoder1 = device.createCommandEncoder();
const encoder2 = device.createCommandEncoder();
// Both encode commands simultaneously

3. Bind Groups & Layouts

wgsl

// Define resource layout
@group(0) @binding(0) var<uniform> camera: CameraUniform;
@group(0) @binding(1) var texture: texture_2d<f32>;
@group(0) @binding(2) var sampler: sampler;

4. Render Bundles

Pre-record reusable rendering commands:

javascript

const bundleEncoder = device.createRenderBundleEncoder(descriptor);
// Record commands once...
const bundle = bundleEncoder.finish();
// Reuse many times!
renderPassEncoder.executeBundles([bundle]);

Practical WebGPU Example

javascript

// Basic WebGPU initialization
async function initWebGPU() {
    // 1. Get adapter and device
    if (!navigator.gpu) throw Error("WebGPU not supported");
    
    const adapter = await navigator.gpu.requestAdapter();
    const device = await adapter.requestDevice();
    
    // 2. Get canvas context
    const canvas = document.getElementById('canvas');
    const context = canvas.getContext('webgpu');
    
    // 3. Configure swap chain
    const format = navigator.gpu.getPreferredCanvasFormat();
    context.configure({
        device: device,
        format: format,
        alphaMode: 'premultiplied'
    });
    
    // 4. Create shader module
    const shaderModule = device.createShaderModule({
        code: wgslShaderCode
    });
    
    // 5. Create pipeline
    const pipeline = device.createRenderPipeline({
        vertex: {
            module: shaderModule,
            entryPoint: 'vertex_main'
        },
        fragment: {
            module: shaderModule,
            entryPoint: 'fragment_main',
            targets: [{ format: format }]
        },
        primitive: { topology: 'triangle-list' }
    });
    
    // 6. Render loop
    function render() {
        const commandEncoder = device.createCommandEncoder();
        const renderPass = commandEncoder.beginRenderPass({/*...*/});
        renderPass.setPipeline(pipeline);
        renderPass.draw(3, 1, 0, 0); // Draw triangle
        renderPass.end();
        
        device.queue.submit([commandEncoder.finish()]);
        requestAnimationFrame(render);
    }
    render();
}

B. WebGPU Architecture

text

┌─────────────────────────────────────────────────────────┐
│                 WebGPU Architecture                      │
├─────────────────────────────────────────────────────────┤
│ Application (JavaScript/TypeScript/WASM)                │
│   │                                                     │
│   ├─ WebGPU API                                         │
│   │   ├─ Device Management                              │
│   │   ├─ Resource Creation                              │
│   │   └─ Command Recording                              │
│   │                                                     │
│   ├─ Shaders (WGSL)                                     │
│   │   ├─ Vertex Shaders                                 │
│   │   ├─ Fragment Shaders                               │
│   │   ├─ Compute Shaders                                │
│   │   └─ Mesh/Task Shaders (future)                    │
│   │                                                     │
│   └─ Bind Groups & Pipelines                            │
│                                                     │
├─────────────────────────────────────────────────────────┤
│ Browser Implementation                                  │
│   ├─ Dawn (Chromium)                                    │
│   ├─ wgpu (Firefox)                                     │
│   └─ WebKit WebGPU (Safari)                             │
│                                                     │
├─────────────────────────────────────────────────────────┤
│ Native Backends                                         │
│   ├─ Vulkan (Windows/Linux/Android)                    │
│   ├─ Metal (macOS/iOS)                                  │
│   ├─ D3D12 (Windows)                                    │
│   └─ OpenGL (fallback)                                  │
└─────────────────────────────────────────────────────────┘

2. Core WebGPU API

A. Initialization & Setup

javascript

// 1. Check for WebGPU support
async function initWebGPU() {
    if (!navigator.gpu) {
        throw new Error("WebGPU not supported");
    }
    
    // 2. Request adapter (physical GPU)
    const adapter = await navigator.gpu.requestAdapter({
        powerPreference: 'high-performance', // or 'low-power'
        forceFallbackAdapter: false
    });
    
    if (!adapter) {
        throw new Error("No suitable GPU adapter found");
    }
    
    // 3. Request device (logical GPU connection)
    const device = await adapter.requestDevice({
        requiredFeatures: [
            'timestamp-query',
            'depth-clip-control',
            'texture-compression-bc'
        ],
        requiredLimits: {
            maxTextureDimension1D: 8192,
            maxTextureDimension2D: 8192,
            maxBindGroups: 4,
            maxUniformBuffersPerShaderStage: 12
        }
    });
    
    // Handle device loss
    device.lost.then((info) => {
        console.error(`WebGPU device lost: ${info.message}`);
        if (info.reason !== 'destroyed') {
            // Re-initialize
            initWebGPU();
        }
    });
    
    return { adapter, device };
}

// 4. Get canvas context
function getCanvasContext(canvas, device) {
    const context = canvas.getContext('webgpu');
    
    // Configure presentation
    const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
    
    context.configure({
        device: device,
        format: presentationFormat,
        alphaMode: 'premultiplied',
        usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
        colorSpace: 'srgb',
        compositingAlphaMode: 'opaque'
    });
    
    return context;
}

B. Resource Creation

javascript

// Buffer creation utilities
class WebGPUResourceManager {
    constructor(device) {
        this.device = device;
        this.buffers = new Map();
        this.textures = new Map();
        this.samplers = new Map();
    }
    
    createBuffer(label, size, usage, data = null) {
        const descriptor = {
            label: label,
            size: size,
            usage: usage,
            mappedAtCreation: data !== null
        };
        
        const buffer = this.device.createBuffer(descriptor);
        
        if (data) {
            if (data instanceof ArrayBuffer) {
                new Uint8Array(buffer.getMappedRange()).set(new Uint8Array(data));
            } else {
                new data.constructor(buffer.getMappedRange()).set(data);
            }
            buffer.unmap();
        }
        
        this.buffers.set(label, buffer);
        return buffer;
    }
    
    createTexture(label, descriptor) {
        const texture = this.device.createTexture({
            label: label,
            size: descriptor.size,
            mipLevelCount: descriptor.mipLevelCount || 1,
            sampleCount: descriptor.sampleCount || 1,
            dimension: descriptor.dimension || '2d',
            format: descriptor.format,
            usage: descriptor.usage
        });
        
        this.textures.set(label, texture);
        return texture;
    }
    
    createSampler(label, descriptor = {}) {
        const sampler = this.device.createSampler({
            label: label,
            addressModeU: descriptor.addressModeU || 'clamp-to-edge',
            addressModeV: descriptor.addressModeV || 'clamp-to-edge',
            addressModeW: descriptor.addressModeW || 'clamp-to-edge',
            magFilter: descriptor.magFilter || 'linear',
            minFilter: descriptor.minFilter || 'linear',
            mipmapFilter: descriptor.mipmapFilter || 'linear',
            lodMinClamp: descriptor.lodMinClamp || 0,
            lodMaxClamp: descriptor.lodMaxClamp || 32,
            compare: descriptor.compare,
            maxAnisotropy: descriptor.maxAnisotropy || 1
        });
        
        this.samplers.set(label, sampler);
        return sampler;
    }
    
    // Create structured buffer with proper alignment
    createUniformBuffer(label, data) {
        // WebGPU requires uniform buffer offsets to be multiples of 256
        const alignedSize = Math.ceil(data.byteLength / 256) * 256;
        return this.createBuffer(
            label,
            alignedSize,
            GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
            data
        );
    }
    
    // Create storage buffer for compute shaders
    createStorageBuffer(label, data) {
        return this.createBuffer(
            label,
            data.byteLength,
            GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
            data
        );
    }
}

3. WGSL (WebGPU Shading Language)

A. WGSL Fundamentals

WGSL = WebGPU Shading Language (replaces GLSL for WebGPU)

wgsl

// Basic WGSL structure
// ====================

// 1. Enable extensions (if needed)
enable f16;  // Enable 16-bit floats

// 2. Struct definitions
struct VertexInput {
    @location(0) position: vec3<f32>,
    @location(1) normal: vec3<f32>,
    @location(2) uv: vec2<f32>,
    @location(3) color: vec4<f32>,
};

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv: vec2<f32>,
    @location(1) color: vec4<f32>,
    @location(2) world_position: vec3<f32>,
    @location(3) world_normal: vec3<f32>,
};

// 3. Uniform buffers
struct CameraUniforms {
    view_projection: mat4x4<f32>,
    position: vec3<f32>,
    // WGSL requires explicit padding for alignment
    _padding: f32,
};

// 4. Storage buffers (read-write)
struct Particle {
    position: vec3<f32>,
    velocity: vec3<f32>,
    lifetime: f32,
    // Align to 16 bytes
    _padding: f32,
};

// 5. Group and binding annotations
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
@group(0) @binding(1) var<storage, read_write> particles: array<Particle>;
@group(1) @binding(0) var my_sampler: sampler;
@group(1) @binding(1) var my_texture: texture_2d<f32>;

// 6. Entry point functions
@vertex
fn vs_main(vertex: VertexInput) -> VertexOutput {
    var output: VertexOutput;
    
    // Transform position
    output.position = camera.view_projection * vec4<f32>(vertex.position, 1.0);
    output.world_position = vertex.position;
    
    // Pass through other data
    output.uv = vertex.uv;
    output.color = vertex.color;
    output.world_normal = vertex.normal;
    
    return output;
}

@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
    // Sample texture
    let tex_color = textureSample(my_texture, my_sampler, input.uv);
    
    // Simple lighting
    let light_dir = normalize(vec3<f32>(1.0, 1.0, 1.0));
    let normal = normalize(input.world_normal);
    let ndotl = max(dot(normal, light_dir), 0.2);
    
    // Combine
    return tex_color * input.color * vec4<f32>(vec3<f32>(ndotl), 1.0);
}

B. Advanced WGSL Features

wgsl

// 1. Compute shaders
@group(0) @binding(0) var<storage, read> input_data: array<f32>;
@group(0) @binding(1) var<storage, read_write> output_data: array<f32>;

@compute @workgroup_size(64, 1, 1)
fn cs_main(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let idx = global_id.x;
    
    if (idx < arrayLength(&input_data)) {
        // Parallel computation
        output_data[idx] = input_data[idx] * input_data[idx];
        
        // Atomic operations
        var<workgroup> shared_counter: atomic<u32>;
        atomicAdd(&shared_counter, 1u);
    }
}

// 2. Subgroup operations (wave intrinsics)
enable chromium_experimental_subgroups;

@compute @workgroup_size(64, 1, 1)
fn subgroup_example(@builtin(local_invocation_id) local_id: vec3<u32>) {
    var value: f32 = f32(local_id.x);
    
    // Subgroup reduce
    let sum = subgroupAdd(value);
    let max = subgroupMax(value);
    let min = subgroupMin(value);
    
    // Ballot operations
    let active_mask = subgroupBallot(value > 0.5);
    let first_active = subgroupBallotFindLSB(active_mask);
    
    // Broadcast
    let broadcasted = subgroupBroadcast(value, 0u);
}

// 3. Texture operations
@group(0) @binding(0) var input_tex: texture_2d<f32>;
@group(0) @binding(1) var output_tex: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(2) var depth_tex: texture_depth_2d;
@group(0) @binding(3) var cube_tex: texture_cube<f32>;
@group(0) @binding(4) var array_tex: texture_2d_array<f32>;

@compute @workgroup_size(8, 8, 1)
fn texture_operations(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let coord = vec2<i32>(global_id.xy);
    
    // Load from texture
    let pixel = textureLoad(input_tex, coord, 0);
    
    // Sample with LOD
    let uv = vec2<f32>(coord) / vec2<f32>(textureDimensions(input_tex));
    let sampled = textureSampleLevel(input_tex, my_sampler, uv, 0.0);
    
    // Sample depth
    let depth = textureSample(depth_tex, my_sampler, uv);
    
    // Write to storage texture
    textureStore(output_tex, coord, vec4<f32>(pixel.rgb * 2.0, 1.0));
}

// 4. Ray tracing helpers (software ray tracing)
struct Ray {
    origin: vec3<f32>,
    direction: vec3<f32>,
    t_min: f32,
    t_max: f32,
};

struct HitRecord {
    t: f32,
    position: vec3<f32>,
    normal: vec3<f32>,
    material_id: u32,
};

fn ray_sphere_intersect(ray: Ray, center: vec3<f32>, radius: f32) -> f32 {
    let oc = ray.origin - center;
    let a = dot(ray.direction, ray.direction);
    let b = 2.0 * dot(oc, ray.direction);
    let c = dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;
    
    if (discriminant < 0.0) {
        return -1.0;
    }
    
    return (-b - sqrt(discriminant)) / (2.0 * a);
}

C. WGSL Math Library

wgsl

// Custom math functions (WGSL's stdlib is limited)
const PI: f32 = 3.141592653589793;
const EPSILON: f32 = 0.000001;

fn rotate_2d(point: vec2<f32>, angle: f32) -> vec2<f32> {
    let s = sin(angle);
    let c = cos(angle);
    return vec2<f32>(
        point.x * c - point.y * s,
        point.x * s + point.y * c
    );
}

fn look_at_matrix(eye: vec3<f32>, target: vec3<f32>, up: vec3<f32>) -> mat4x4<f32> {
    let z = normalize(target - eye);
    let x = normalize(cross(up, z));
    let y = cross(z, x);
    
    return mat4x4<f32>(
        vec4<f32>(x, 0.0),
        vec4<f32>(y, 0.0),
        vec4<f32>(z, 0.0),
        vec4<f32>(eye, 1.0)
    );
}

fn quat_from_axis_angle(axis: vec3<f32>, angle: f32) -> vec4<f32> {
    let half_angle = angle * 0.5;
    let s = sin(half_angle);
    return vec4<f32>(
        axis.x * s,
        axis.y * s,
        axis.z * s,
        cos(half_angle)
    );
}

fn quat_mul(q1: vec4<f32>, q2: vec4<f32>) -> vec4<f32> {
    return vec4<f32>(
        q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
        q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
        q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w,
        q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z
    );
}

4. Render Pipeline & Graphics

A. Complete Render Pipeline Setup

javascript

class WebGPURenderer {
    constructor(canvas, device) {
        this.canvas = canvas;
        this.device = device;
        this.context = canvas.getContext('webgpu');
        this.format = navigator.gpu.getPreferredCanvasFormat();
        
        this.configureCanvas();
        this.createDepthTexture();
        this.createRenderPipeline();
    }
    
    configureCanvas() {
        this.context.configure({
            device: this.device,
            format: this.format,
            alphaMode: 'premultiplied',
            usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
        });
    }
    
    createDepthTexture() {
        this.depthTexture = this.device.createTexture({
            size: [this.canvas.width, this.canvas.height, 1],
            format: 'depth24plus-stencil8',
            usage: GPUTextureUsage.RENDER_ATTACHMENT
        });
    }
    
    async createRenderPipeline(shaderSource, layout) {
        // Compile shader module
        const shaderModule = this.device.createShaderModule({
            code: shaderSource
        });
        
        // Create pipeline layout
        const pipelineLayout = this.device.createPipelineLayout({
            bindGroupLayouts: layout.bindGroupLayouts
        });
        
        // Create render pipeline
        this.pipeline = this.device.createRenderPipeline({
            layout: pipelineLayout,
            
            vertex: {
                module: shaderModule,
                entryPoint: 'vs_main',
                buffers: layout.vertexBuffers
            },
            
            fragment: {
                module: shaderModule,
                entryPoint: 'fs_main',
                targets: [{
                    format: this.format,
                    blend: {
                        color: {
                            srcFactor: 'src-alpha',
                            dstFactor: 'one-minus-src-alpha',
                            operation: 'add'
                        },
                        alpha: {
                            srcFactor: 'one',
                            dstFactor: 'one-minus-src-alpha',
                            operation: 'add'
                        }
                    }
                }]
            },
            
            primitive: {
                topology: 'triangle-list',
                cullMode: 'back',
                frontFace: 'ccw'
            },
            
            depthStencil: {
                format: 'depth24plus-stencil8',
                depthWriteEnabled: true,
                depthCompare: 'less',
                stencilFront: {
                    compare: 'always',
                    failOp: 'keep',
                    depthFailOp: 'keep',
                    passOp: 'keep'
                },
                stencilBack: {
                    compare: 'always',
                    failOp: 'keep',
                    depthFailOp: 'keep',
                    passOp: 'keep'
                }
            },
            
            multisample: {
                count: 4  // MSAA
            }
        });
    }
    
    beginRenderPass(commandEncoder, clearColor = [0, 0, 0, 1]) {
        const textureView = this.context.getCurrentTexture().createView();
        
        const renderPassDescriptor = {
            colorAttachments: [{
                view: textureView,
                resolveTarget: textureView,  // For MSAA
                clearValue: clearColor,
                loadOp: 'clear',
                storeOp: 'store'
            }],
            depthStencilAttachment: {
                view: this.depthTexture.createView(),
                depthClearValue: 1.0,
                depthLoadOp: 'clear',
                depthStoreOp: 'store',
                stencilClearValue: 0,
                stencilLoadOp: 'clear',
                stencilStoreOp: 'store'
            }
        };
        
        return commandEncoder.beginRenderPass(renderPassDescriptor);
    }
    
    render(frameData) {
        // Start command recording
        const commandEncoder = this.device.createCommandEncoder();
        
        // Begin render pass
        const pass = this.beginRenderPass(commandEncoder);
        
        // Set pipeline
        pass.setPipeline(this.pipeline);
        
        // Set bind groups
        for (let i = 0; i < frameData.bindGroups.length; i++) {
            pass.setBindGroup(i, frameData.bindGroups[i]);
        }
        
        // Set vertex buffers
        for (let i = 0; i < frameData.vertexBuffers.length; i++) {
            pass.setVertexBuffer(i, frameData.vertexBuffers[i].buffer, 
                                frameData.vertexBuffers[i].offset);
        }
        
        // Set index buffer
        if (frameData.indexBuffer) {
            pass.setIndexBuffer(frameData.indexBuffer.buffer, 
                               frameData.indexBuffer.format,
                               frameData.indexBuffer.offset);
        }
        
        // Draw
        if (frameData.indexBuffer) {
            pass.drawIndexed(frameData.indexCount, frameData.instanceCount,
                           0, 0, 0);
        } else {
            pass.draw(frameData.vertexCount, frameData.instanceCount,
                    0, 0);
        }
        
        // End pass and submit
        pass.end();
        
        const commandBuffer = commandEncoder.finish();
        this.device.queue.submit([commandBuffer]);
    }
}

B. Advanced Rendering Techniques

javascript

// 1. Deferred Rendering Setup
class DeferredRenderer {
    constructor(device, width, height) {
        this.device = device;
        this.createGBuffers(width, height);
        this.createLightingPipeline();
    }
    
    createGBuffers(width, height) {
        // G-Buffer textures
        this.gBufferTextures = {
            albedo: this.device.createTexture({
                size: [width, height],
                format: 'rgba8unorm',
                usage: GPUTextureUsage.RENDER_ATTACHMENT | 
                       GPUTextureUsage.TEXTURE_BINDING
            }),
            normal: this.device.createTexture({
                size: [width, height],
                format: 'rgba16float',
                usage: GPUTextureUsage.RENDER_ATTACHMENT | 
                       GPUTextureUsage.TEXTURE_BINDING
            }),
            position: this.device.createTexture({
                size: [width, height],
                format: 'rgba32float',
                usage: GPUTextureUsage.RENDER_ATTACHMENT | 
                       GPUTextureUsage.TEXTURE_BINDING
            }),
            depth: this.device.createTexture({
                size: [width, height],
                format: 'depth24plus',
                usage: GPUTextureUsage.RENDER_ATTACHMENT
            })
        };
    }
    
    // 2. Shadow Mapping
    createShadowMap(size = 2048) {
        this.shadowMap = this.device.createTexture({
            size: [size, size, 1],
            format: 'depth32float',
            usage: GPUTextureUsage.RENDER_ATTACHMENT | 
                   GPUTextureUsage.TEXTURE_BINDING,
            sampleCount: 1
        });
        
        this.shadowSampler = this.device.createSampler({
            compare: 'less',
            magFilter: 'linear',
            minFilter: 'linear',
            mipmapFilter: 'linear',
            maxAnisotropy: 1
        });
    }
    
    // 3. Post-processing Chain
    createPostProcessingChain(width, height) {
        this.postProcessTextures = [
            this.createRenderTexture(width, height, 'rgba16float'),
            this.createRenderTexture(width, height, 'rgba16float')
        ];
        
        this.postProcessPipelines = {
            bloom: this.createBloomPipeline(),
            tonemap: this.createTonemapPipeline(),
            fxaa: this.createFXAAPipeline(),
            ssao: this.createSSAOPipeline()
        };
    }
    
    createBloomPipeline() {
        // Bloom uses compute shaders for blur
        const blurShader = `
            @group(0) @binding(0) var input_tex: texture_2d<f32>;
            @group(0) @binding(1) var output_tex: texture_storage_2d<rgba16float, write>;
            @group(0) @binding(2) var<uniform> params: BloomParams;
            
            struct BloomParams {
                direction: vec2<f32>,
                radius: f32,
                threshold: f32,
            };
            
            @compute @workgroup_size(8, 8, 1)
            fn blur(@builtin(global_invocation_id) global_id: vec3<u32>) {
                let coord = vec2<i32>(global_id.xy);
                let tex_size = textureDimensions(input_tex);
                let uv = vec2<f32>(coord) / vec2<f32>(tex_size);
                
                // Gaussian blur
                var sum = vec4<f32>(0.0);
                var weight_sum = 0.0;
                
                for (var i = -5; i <= 5; i++) {
                    let offset = params.direction * f32(i) * params.radius;
                    let sample_uv = uv + offset / vec2<f32>(tex_size);
                    let sample_color = textureSampleLevel(input_tex, default_sampler, sample_uv, 0.0);
                    
                    // Gaussian weight
                    let weight = exp(-f32(i * i) / (2.0 * 2.5 * 2.5));
                    sum += sample_color * weight;
                    weight_sum += weight;
                }
                
                textureStore(output_tex, coord, sum / weight_sum);
            }
        `;
        
        return this.device.createComputePipeline({
            layout: 'auto',
            compute: {
                module: this.device.createShaderModule({ code: blurShader }),
                entryPoint: 'blur'
            }
        });
    }
}

5. Compute Shaders & GPGPU

A. Basic Compute Pipeline

javascript

class WebGPUCompute {
    constructor(device) {
        this.device = device;
        this.timestampQuerySets = new Map();
    }
    
    async createComputePipeline(shaderSource, bindGroupLayouts) {
        const shaderModule = this.device.createShaderModule({
            code: shaderSource
        });
        
        const pipelineLayout = this.device.createPipelineLayout({
            bindGroupLayouts: bindGroupLayouts
        });
        
        return this.device.createComputePipeline({
            layout: pipelineLayout,
            compute: {
                module: shaderModule,
                entryPoint: 'main',
                constants: {}  // Specialization constants
            }
        });
    }
    
    createTimestampQuerySet(label, count = 2) {
        const querySet = this.device.createQuerySet({
            type: 'timestamp',
            count: count
        });
        
        this.timestampQuerySets.set(label, querySet);
        
        // Create buffer to store results
        const resultBuffer = this.device.createBuffer({
            size: count * 8,  // 8 bytes per timestamp (uint64)
            usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
        });
        
        return { querySet, resultBuffer };
    }
    
    async dispatchCompute(pipeline, bindGroups, workgroups, label = 'compute') {
        const commandEncoder = this.device.createCommandEncoder();
        
        // Begin timestamp query
        const timestamp = this.createTimestampQuerySet(`${label}_timestamp`, 2);
        
        const pass = commandEncoder.beginComputePass({
            timestampWrites: {
                querySet: timestamp.querySet,
                beginningOfPassWriteIndex: 0,
                endOfPassWriteIndex: 1
            }
        });
        
        pass.setPipeline(pipeline);
        
        for (let i = 0; i < bindGroups.length; i++) {
            pass.setBindGroup(i, bindGroups[i]);
        }
        
        pass.dispatchWorkgroups(workgroups[0], workgroups[1], workgroups[2]);
        pass.end();
        
        // Resolve timestamps
        commandEncoder.resolveQuerySet(
            timestamp.querySet,
            0,
            2,
            timestamp.resultBuffer,
            0
        );
        
        const commandBuffer = commandEncoder.finish();
        this.device.queue.submit([commandBuffer]);
        
        // Read back timestamps
        await this.device.queue.onSubmittedWorkDone();
        const timestamps = await this.readTimestampResults(timestamp.resultBuffer);
        
        const duration = (timestamps[1] - timestamps[0]) / 1000000; // Convert to ms
        console.log(`${label} took ${duration.toFixed(3)}ms`);
        
        return duration;
    }
    
    async readTimestampResults(buffer) {
        const readbackBuffer = this.device.createBuffer({
            size: buffer.size,
            usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
        });
        
        const commandEncoder = this.device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(buffer, 0, readbackBuffer, 0, buffer.size);
        
        const commandBuffer = commandEncoder.finish();
        this.device.queue.submit([commandBuffer]);
        
        await readbackBuffer.mapAsync(GPUMapMode.READ);
        const array = new BigUint64Array(readbackBuffer.getMappedRange());
        const results = Array.from(array);
        readbackBuffer.unmap();
        
        return results;
    }
}

B. Practical Compute Applications

javascript

// 1. Particle System Compute Shader
const particleComputeShader = `
    struct Particle {
        position: vec3<f32>,
        velocity: vec3<f32>,
        color: vec4<f32>,
        lifetime: f32,
        size: f32,
    };

    struct SimulationParams {
        delta_time: f32,
        gravity: vec3<f32>,
        emitter_position: vec3<f32>,
        emitter_radius: f32,
        particle_count: u32,
        time: f32,
    };

    @group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
    @group(0) @binding(1) var<uniform> params: SimulationParams;

    @compute @workgroup_size(256, 1, 1)
    fn update_particles(@builtin(global_invocation_id) global_id: vec3<u32>) {
        let idx = global_id.x;
        
        if (idx >= params.particle_count) {
            return;
        }
        
        var particle = particles[idx];
        
        // Apply gravity
        particle.velocity += params.gravity * params.delta_time;
        
        // Update position
        particle.position += particle.velocity * params.delta_time;
        
        // Update lifetime
        particle.lifetime -= params.delta_time;
        
        // Respawn dead particles
        if (particle.lifetime <= 0.0) {
            // Random position in emitter sphere
            let angle1 = f32(idx) * 137.5 * 3.14159 / 180.0;
            let angle2 = f32(idx) * 42.0 * 3.14159 / 180.0;
            let radius = params.emitter_radius * sqrt(f32(idx % 100) / 100.0);
            
            particle.position = params.emitter_position + vec3<f32>(
                radius * cos(angle1) * sin(angle2),
                radius * sin(angle1) * sin(angle2),
                radius * cos(angle2)
            );
            
            // Random velocity
            particle.velocity = vec3<f32>(
                (f32(idx % 10) - 5.0) * 0.1,
                2.0 + f32(idx % 5) * 0.1,
                (f32(idx % 10) - 5.0) * 0.1
            );
            
            // Random color
            particle.color = vec4<f32>(
                0.5 + 0.5 * sin(params.time + f32(idx) * 0.1),
                0.5 + 0.5 * sin(params.time + f32(idx) * 0.2 + 2.0),
                0.5 + 0.5 * sin(params.time + f32(idx) * 0.3 + 4.0),
                1.0
            );
            
            particle.lifetime = 3.0 + f32(idx % 10) * 0.2;
            particle.size = 0.1 + f32(idx % 5) * 0.02;
        }
        
        particles[idx] = particle;
    }
`;

// 2. Physics Simulation
const physicsComputeShader = `
    struct RigidBody {
        position: vec3<f32>,
        velocity: vec3<f32>,
        rotation: vec4<f32>,  // Quaternion
        angular_velocity: vec3<f32>,
        mass: f32,
        inertia: mat3x3<f32>,
    };

    struct Collision {
        body_a: u32,
        body_b: u32,
        point: vec3<f32>,
        normal: vec3<f32>,
        depth: f32,
    };

    @group(0) @binding(0) var<storage, read_write> bodies: array<RigidBody>;
    @group(0) @binding(1) var<storage, read_write> collisions: array<Collision>;
    @group(0) @binding(2) var<uniform> params: PhysicsParams;

    @compute @workgroup_size(256, 1, 1)
    fn integrate(@builtin(global_invocation_id) global_id: vec3<u32>) {
        let idx = global_id.x;
        
        if (idx >= arrayLength(&bodies)) {
            return;
        }
        
        var body = bodies[idx];
        
        // Linear integration
        body.velocity += params.gravity * params.delta_time;
        body.position += body.velocity * params.delta_time;
        
        // Angular integration
        let delta_rotation = quat_from_angular_velocity(body.angular_velocity, params.delta_time);
        body.rotation = normalize(quat_mul(delta_rotation, body.rotation));
        
        // Apply damping
        body.velocity *= 0.99;
        body.angular_velocity *= 0.99;
        
        bodies[idx] = body;
    }

    @compute @workgroup_size(256, 256, 1)
    fn detect_collisions(
        @builtin(global_invocation_id) global_id: vec3<u32>,
        @builtin(local_invocation_id) local_id: vec3<u32>
    ) {
        let idx_a = global_id.x;
        let idx_b = global_id.y;
        
        if (idx_a >= idx_b) {
            return;
        }
        
        var body_a = bodies[idx_a];
        var body_b = bodies[idx_b];
        
        // Sphere-sphere collision
        let delta = body_b.position - body_a.position;
        let distance = length(delta);
        let min_distance = 1.0;  // Sphere radius
        
        if (distance < min_distance * 2.0) {
            let collision_idx = atomicAdd(&collision_count, 1);
            
            collisions[collision_idx] = Collision(
                idx_a,
                idx_b,
                (body_a.position + body_b.position) * 0.5,
                normalize(delta),
                min_distance * 2.0 - distance
            );
        }
    }
`;

// 3. Image Processing / Computer Vision
const imageProcessingShader = `
    @group(0) @binding(0) var input_tex: texture_2d<f32>;
    @group(0) @binding(1) var output_tex: texture_storage_2d<rgba8unorm, write>;
    @group(0) @binding(2) var<uniform> kernel: mat3x3<f32>;

    @compute @workgroup_size(16, 16, 1)
    fn convolve(@builtin(global_invocation_id) global_id: vec3<u32>) {
        let coord = vec2<i32>(global_id.xy);
        let tex_size = textureDimensions(input_tex);
        
        var result = vec4<f32>(0.0);
        
        // Apply convolution kernel
        for (var y: i32 = -1; y <= 1; y++) {
            for (var x: i32 = -1; x <= 1; x++) {
                let sample_coord = vec2<i32>(coord.x + x, coord.y + y);
                let sample_color = textureLoad(input_tex, sample_coord, 0);
                let kernel_value = kernel[x + 1][y + 1];
                
                result += sample_color * kernel_value;
            }
        }
        
        // Clamp to valid range
        result = clamp(result, vec4<f32>(0.0), vec4<f32>(1.0));
        
        textureStore(output_tex, coord, result);
    }

    // Edge detection (Sobel operator)
    fn sobel_edge_detection(coord: vec2<i32>) -> f32 {
        let gx = mat3x3<f32>(
            -1.0, 0.0, 1.0,
            -2.0, 0.0, 2.0,
            -1.0, 0.0, 1.0
        );
        
        let gy = mat3x3<f32>(
            -1.0, -2.0, -1.0,
            0.0, 0.0, 0.0,
            1.0, 2.0, 1.0
        );
        
        var gradient_x = vec3<f32>(0.0);
        var gradient_y = vec3<f32>(0.0);
        
        for (var y: i32 = -1; y <= 1; y++) {
            for (var x: i32 = -1; x <= 1; x++) {
                let sample_coord = vec2<i32>(coord.x + x, coord.y + y);
                let sample_color = textureLoad(input_tex, sample_coord, 0).rgb;
                
                gradient_x += sample_color * gx[x + 1][y + 1];
                gradient_y += sample_color * gy[x + 1][y + 1];
            }
        }
        
        let magnitude = length(vec2<f32>(length(gradient_x), length(gradient_y)));
        return magnitude;
    }
`;

6. Advanced Graphics Techniques

A. Ray Tracing in WebGPU (Software)

javascript

// Software Ray Tracer using Compute Shaders
class WebGPURayTracer {
    constructor(device, width, height) {
        this.device = device;
        this.width = width;
        this.height = height;
        
        this.createRayTracingPipeline();
        this.createAccelerationStructure();
    }
    
    createRayTracingPipeline() {
        const rayTracingShader = `
            struct Ray {
                origin: vec3<f32>,
                direction: vec3<f32>,
                t_min: f32,
                t_max: f32,
            };
            
            struct HitRecord {
                t: f32,
                position: vec3<f32>,
                normal: vec3<f32>,
                material: u32,
            };
            
            struct Sphere {
                center: vec3<f32>,
                radius: f32,
                material: u32,
            };
            
            struct Camera {
                position: vec3<f32>,
                lower_left_corner: vec3<f32>,
                horizontal: vec3<f32>,
                vertical: vec3<f32>,
            };
            
            @group(0) @binding(0) var<uniform> camera: Camera;
            @group(0) @binding(1) var<storage> spheres: array<Sphere>;
            @group(0) @binding(2) var output_tex: texture_storage_2d<rgba8unorm, write>;
            
            fn ray_sphere_intersect(ray: Ray, sphere: Sphere) -> f32 {
                let oc = ray.origin - sphere.center;
                let a = dot(ray.direction, ray.direction);
                let b = 2.0 * dot(oc, ray.direction);
                let c = dot(oc, oc) - sphere.radius * sphere.radius;
                let discriminant = b * b - 4.0 * a * c;
                
                if (discriminant < 0.0) {
                    return -1.0;
                }
                
                let sqrt_disc = sqrt(discriminant);
                let t1 = (-b - sqrt_disc) / (2.0 * a);
                let t2 = (-b + sqrt_disc) / (2.0 * a);
                
                if (t1 > ray.t_min && t1 < ray.t_max) {
                    return t1;
                }
                if (t2 > ray.t_min && t2 < ray.t_max) {
                    return t2;
                }
                
                return -1.0;
            }
            
            fn trace_ray(ray: Ray) -> vec3<f32> {
                var closest_t = ray.t_max;
                var hit_material = 0u;
                var hit_normal = vec3<f32>(0.0);
                
                // Test against all spheres
                for (var i = 0u; i < arrayLength(&spheres); i++) {
                    let sphere = spheres[i];
                    let t = ray_sphere_intersect(ray, sphere);
                    
                    if (t > 0.0 && t < closest_t) {
                        closest_t = t;
                        hit_material = sphere.material;
                        hit_normal = normalize((ray.origin + ray.direction * t) - sphere.center);
                    }
                }
                
                if (closest_t < ray.t_max) {
                    // Simple shading
                    let light_dir = normalize(vec3<f32>(1.0, 1.0, 1.0));
                    let ndotl = max(dot(hit_normal, light_dir), 0.2);
                    
                    // Material colors
                    var color = vec3<f32>(0.0);
                    if (hit_material == 0u) {
                        color = vec3<f32>(0.8, 0.3, 0.3);  // Red
                    } else if (hit_material == 1u) {
                        color = vec3<f32>(0.3, 0.8, 0.3);  // Green
                    } else {
                        color = vec3<f32>(0.3, 0.3, 0.8);  // Blue
                    }
                    
                    return color * ndotl;
                }
                
                // Sky color
                let t = 0.5 * (ray.direction.y + 1.0);
                return mix(vec3<f32>(1.0), vec3<f32>(0.5, 0.7, 1.0), t);
            }
            
            @compute @workgroup_size(8, 8, 1)
            fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
                let coord = vec2<i32>(global_id.xy);
                let uv = vec2<f32>(coord) / vec2<f32>(textureDimensions(output_tex));
                
                // Create ray from camera
                let ray = Ray(
                    camera.position,
                    camera.lower_left_corner + 
                        uv.x * camera.horizontal + 
                        uv.y * camera.vertical - 
                        camera.position,
                    0.001,
                    1000.0
                );
                
                // Trace ray
                let color = trace_ray(ray);
                
                // Apply simple tone mapping
                let mapped = color / (color + vec3<f32>(1.0));
                
                textureStore(output_tex, coord, vec4<f32>(mapped, 1.0));
            }
        `;
        
        // Create compute pipeline for ray tracing
        this.rayTracingPipeline = this.device.createComputePipeline({
            layout: 'auto',
            compute: {
                module: this.device.createShaderModule({ code: rayTracingShader }),
                entryPoint: 'main'
            }
        });
    }
    
    createAccelerationStructure() {
        // For software ray tracing, we use uniform/storage buffers
        // Hardware acceleration requires GPU vendor extensions
    }
}

B. GPU-Driven Rendering

javascript

// GPU-driven rendering with indirect draws
class GPUDrivenRenderer {
    constructor(device) {
        this.device = device;
        this.createIndirectBuffers();
        this.createCullingPipeline();
    }
    
    createIndirectBuffers() {
        // Indirect draw buffer
        this.indirectDrawBuffer = this.device.createBuffer({
            size: 5 * 4 * 1000,  // 1000 draw calls * 5 parameters * 4 bytes
            usage: GPUBufferUsage.INDIRECT | 
                   GPUBufferUsage.STORAGE | 
                   GPUBufferUsage.COPY_DST
        });
        
        // Instance count buffer
        this.instanceCountBuffer = this.device.createBuffer({
            size: 4 * 1000,  // 1000 draw calls * 4 bytes
            usage: GPUBufferUsage.STORAGE | 
                   GPUBufferUsage.COPY_SRC |
                   GPUBufferUsage.COPY_DST
        });
        
        // Instance data buffer
        this.instanceDataBuffer = this.device.createBuffer({
            size: 64 * 10000,  // 10,000 instances * 64 bytes each
            usage: GPUBufferUsage.STORAGE | 
                   GPUBufferUsage.VERTEX
        });
    }
    
    createCullingPipeline() {
        const cullingShader = `
            struct DrawCommand {
                vertexCount: u32,
                instanceCount: u32,
                firstVertex: u32,
                firstInstance: u32,
            };
            
            struct Instance {
                model_matrix: mat4x4<f32>,
                bounding_sphere: vec4<f32>,  // xyz = center, w = radius
            };
            
            struct Frustum {
                planes: array<vec4<f32>, 6>,
            };
            
            @group(0) @binding(0) var<uniform> frustum: Frustum;
            @group(0) @binding(1) var<storage, read> instances: array<Instance>;
            @group(0) @binding(2) var<storage, read_write> draw_commands: array<DrawCommand>;
            @group(0) @binding(3) var<storage, read_write> visible_instances: array<u32>;
            
            fn sphere_in_frustum(sphere: vec4<f32>) -> bool {
                for (var i = 0u; i < 6u; i++) {
                    let plane = frustum.planes[i];
                    let distance = dot(plane.xyz, sphere.xyz) + plane.w;
                    if (distance < -sphere.w) {
                        return false;
                    }
                }
                return true;
            }
            
            @compute @workgroup_size(256, 1, 1)
            fn cull_instances(@builtin(global_invocation_id) global_id: vec3<u32>) {
                let instance_id = global_id.x;
                
                if (instance_id >= arrayLength(&instances)) {
                    return;
                }
                
                let instance = instances[instance_id];
                
                if (sphere_in_frustum(instance.bounding_sphere)) {
                    // Instance is visible
                    let visible_idx = atomicAdd(&visible_count, 1u);
                    visible_instances[visible_idx] = instance_id;
                    
                    // Update draw command
                    atomicAdd(&draw_commands[0].instanceCount, 1u);
                }
            }
        `;
        
        this.cullingPipeline = this.device.createComputePipeline({
            layout: 'auto',
            compute: {
                module: this.device.createShaderModule({ code: cullingShader }),
                entryPoint: 'cull_instances'
            }
        });
    }
    
    renderIndirect(pipeline, bindGroups) {
        const commandEncoder = this.device.createCommandEncoder();
        
        // First, run culling compute pass
        const cullPass = commandEncoder.beginComputePass();
        cullPass.setPipeline(this.cullingPipeline);
        cullPass.setBindGroup(0, this.cullingBindGroup);
        cullPass.dispatchWorkgroups(Math.ceil(this.instanceCount / 256), 1, 1);
        cullPass.end();
        
        // Then, render using indirect draw
        const renderPass = commandEncoder.beginRenderPass({
            colorAttachments: [/* ... */]
        });
        
        renderPass.setPipeline(pipeline);
        
        for (let i = 0; i < bindGroups.length; i++) {
            renderPass.setBindGroup(i, bindGroups[i]);
        }
        
        // Set instance data buffer as vertex buffer
        renderPass.setVertexBuffer(1, this.instanceDataBuffer);
        
        // Indirect draw
        renderPass.drawIndirect(this.indirectDrawBuffer, 0);
        
        renderPass.end();
        
        this.device.queue.submit([commandEncoder.finish()]);
    }
}
7. Performance Optimization
A. Memory Management & Buffer Recycling
javascript

class WebGPUBufferPool {
    constructor(device) {
        this.device = device;
        this.pools = new Map();  // Map<size, Buffer[]>
        this.allocated = new Set();
    }
    
    allocate(size, usage, label = 'buffer') {
        // Try to find existing buffer in pool
        const poolKey = `${size}_${usage}`;
        
        if (!this.pools.has(poolKey)) {
            this.pools.set(poolKey, []);
        }
        
        const pool = this.pools.get(poolKey);
        
        if (pool.length > 0) {
            const buffer = pool.pop();
            this.allocated.add(buffer);
            return buffer;
        }
        
        // Create new buffer
        const buffer = this.device.createBuffer({
            label: `${label}_${size}`,
            size: size,
            usage: usage,
            mappedAtCreation: false
        });
        
        this.allocated.add(buffer);
        return buffer;
    }
    
    release(buffer) {
        if (this.allocated.has(buffer)) {
            this.allocated.delete(buffer);
            
            // Determine pool key from buffer properties
            const poolKey = `${buffer.size}_${buffer.usage}`;
            
            if (!this.pools.has(poolKey)) {
                this.pools.set(poolKey, []);
            }
            
            this.pools.get(poolKey).push(buffer);
        }
    }
    
    // Frame-based allocation tracking
    beginFrame() {
        this.frameAllocations = new Set();
    }
    
    allocateFrame(size, usage, label) {
        const buffer = this.allocate(size, usage, label);
        this.frameAllocations.add(buffer);
        return buffer;
    }
    
    endFrame() {
        // Release all buffers allocated this frame
        for (const buffer of this.frameAllocations) {
            this.release(buffer);
        }
        this.frameAllocations.clear();
    }
}

// Usage:
const bufferPool = new WebGPUBufferPool(device);

// Each frame
bufferPool.beginFrame();

const frameUniforms = bufferPool.allocateFrame(256, GPUBufferUsage.UNIFORM, 'frame_uniforms');
const tempStorage = bufferPool.allocateFrame(1024, GPUBufferUsage.STORAGE, 'temp');

// ... use buffers ...

bufferPool.endFrame();  // Automatically releases frame buffers

B. Pipeline Caching

javascript

class WebGPUPipelineCache {
    constructor(device) {
        this.device = device;
        this.cache = new Map();
        this.asyncCreation = new Map();  // For async pipeline creation
    }
    
    async getOrCreatePipeline(key, createFunc) {
        // Check cache
        if (this.cache.has(key)) {
            return this.cache.get(key);
        }
        
        // Check if already being created
        if (this.asyncCreation.has(key)) {
            return await this.asyncCreation.get(key);
        }
        
        // Create new pipeline (can be async)
        const pipelinePromise = createFunc();
        this.asyncCreation.set(key, pipelinePromise);
        
        try {
            const pipeline = await pipelinePromise;
            this.cache.set(key, pipeline);
            this.asyncCreation.delete(key);
            return pipeline;
        } catch (error) {
            this.asyncCreation.delete(key);
            throw error;
        }
    }
    
    // Create key from pipeline descriptor
    createPipelineKey(descriptor) {
        // Create hash from relevant pipeline properties
        const keyParts = [];
        
        // Vertex state
        if (descriptor.vertex) {
            keyParts.push(`vs:${descriptor.vertex.entryPoint}`);
        }
        
        // Fragment state
        if (descriptor.fragment) {
            keyParts.push(`fs:${descriptor.fragment.entryPoint}`);
            if (descriptor.fragment.targets) {
                keyParts.push(`fmt:${descriptor.fragment.targets[0].format}`);
            }
        }
        
        // Primitive state
        if (descriptor.primitive) {
            keyParts.push(`topo:${descriptor.primitive.topology}`);
        }
        
        // Depth-stencil state
        if (descriptor.depthStencil) {
            keyParts.push(`depth:${descriptor.depthStencil.format}`);
        }
        
        return keyParts.join('|');
    }
}

// Usage:
const pipelineCache = new WebGPUPipelineCache(device);

const pipeline = await pipelineCache.getOrCreatePipeline(
    'opaque_material',
    () => device.createRenderPipeline({
        // ... pipeline descriptor ...
    })
);

C. Multi-Threaded Command Recording

javascript

class ParallelCommandRecorder {
    constructor(device, numWorkers = 4) {
        this.device = device;
        this.workers = [];
        this.commandEncoders = [];
        
        // Create command encoders for each thread
        for (let i = 0; i < numWorkers; i++) {
            this.commandEncoders.push(device.createCommandEncoder());
        }
    }
    
    // Record commands in parallel
    async recordInParallel(recordFuncs) {
        const promises = recordFuncs.map(async (recordFunc, index) => {
            const encoder = this.commandEncoders[index % this.commandEncoders.length];
            return await recordFunc(encoder);
        });
        
        await Promise.all(promises);
        
        // Finish all encoders
        const commandBuffers = this.commandEncoders.map(encoder => encoder.finish());
        
        // Reset encoders for next frame
        this.commandEncoders = this.commandEncoders.map(() => 
            this.device.createCommandEncoder()
        );
        
        return commandBuffers;
    }
}

// Usage:
const recorder = new ParallelCommandRecorder(device, 4);

const commandBuffers = await recorder.recordInParallel([
    // Worker 1: Shadow passes
    (encoder) => {
        const pass = encoder.beginRenderPass(shadowPassDescriptor);
        // ... draw shadow casters ...
        pass.end();
    },
    
    // Worker 2: G-Buffer pass
    (encoder) => {
        const pass = encoder.beginRenderPass(gBufferPassDescriptor);
        // ... draw opaque geometry ...
        pass.end();
    },
    
    // Worker 3: Lighting pass
    (encoder) => {
        const pass = encoder.beginComputePass();
        // ... compute lighting ...
        pass.end();
    },
    
    // Worker 4: Post-processing
    (encoder) => {
        const pass = encoder.beginComputePass();
        // ... apply post effects ...
        pass.end();
    }
]);

// Submit all command buffers
device.queue.submit(commandBuffers);

8. Debugging & Profiling

A. Debug Utilities

javascript

class WebGPUDebug {
    static enableDebugLayer(device) {
        // Create error scopes for better error reporting
        device.pushErrorScope('validation');
        device.pushErrorScope('out-of-memory');
        device.pushErrorScope('internal');
        
        // Check for errors
        setInterval(async () => {
            const validationError = await device.popErrorScope();
            const oomError = await device.popErrorScope();
            const internalError = await device.popErrorScope();
            
            if (validationError) {
                console.error('WebGPU Validation Error:', validationError);
            }
            if (oomError) {
                console.error('WebGPU OOM Error:', oomError);
            }
            if (internalError) {
                console.error('WebGPU Internal Error:', internalError);
            }
            
            // Push new scopes
            device.pushErrorScope('validation');
            device.pushErrorScope('out-of-memory');
            device.pushErrorScope('internal');
        }, 1000);
    }
    
    static createDebugBuffer(device, data, label = 'debug') {
        const buffer = device.createBuffer({
            label: label,
            size: data.byteLength,
            usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
        });
        
        device.queue.writeBuffer(buffer, 0, data);
        
        // Async readback for debugging
        setTimeout(async () => {
            await buffer.mapAsync(GPUMapMode.READ);
            const mappedData = new data.constructor(buffer.getMappedRange());
            console.log(`${label}:`, mappedData);
            buffer.unmap();
        }, 100);
        
        return buffer;
    }
    
    static createDebugTexture(device, width, height, format = 'rgba8unorm') {
        const texture = device.createTexture({
            label: 'debug_texture',
            size: [width, height],
            format: format,
            usage: GPUTextureUsage.COPY_SRC | 
                   GPUTextureUsage.RENDER_ATTACHMENT
        });
        
        return texture;
    }
    
    static async captureFrame(device, context, frameNumber = 0) {
        const texture = context.getCurrentTexture();
        const canvas = document.createElement('canvas');
        canvas.width = texture.width;
        canvas.height = texture.height;
        
        const ctx = canvas.getContext('2d');
        
        // Read texture data
        const buffer = await this.readTextureToBuffer(device, texture);
        
        // Convert to ImageData
        const imageData = new ImageData(
            new Uint8ClampedArray(buffer),
            texture.width,
            texture.height
        );
        
        ctx.putImageData(imageData, 0, 0);
        
        // Download
        const link = document.createElement('a');
        link.download = `frame_${frameNumber}.png`;
        link.href = canvas.toDataURL('image/png');
        link.click();
    }
    
    static async readTextureToBuffer(device, texture) {
        const bytesPerRow = Math.ceil(texture.width * 4 / 256) * 256;
        const bufferSize = bytesPerRow * texture.height;
        
        const buffer = device.createBuffer({
            size: bufferSize,
            usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
        });
        
        const commandEncoder = device.createCommandEncoder();
        commandEncoder.copyTextureToBuffer(
            { texture: texture },
            { buffer: buffer, bytesPerRow: bytesPerRow },
            [texture.width, texture.height]
        );
        
        device.queue.submit([commandEncoder.finish()]);
        
        await buffer.mapAsync(GPUMapMode.READ);
        const data = new Uint8Array(buffer.getMappedRange());
        buffer.unmap();
        
        return data;
    }
}

9. Real-World Applications

A. Scientific Visualization

javascript

class VolumeRenderer {
    constructor(device, volumeData) {
        this.device = device;
        this.volumeData = volumeData;
        
        this.createVolumeTexture();
        this.createRaycastingPipeline();
    }
    
    createVolumeTexture() {
        // Create 3D texture for volume data
        this.volumeTexture = this.device.createTexture({
            dimension: '3d',
            size: [
                this.volumeData.width,
                this.volumeData.height,
                this.volumeData.depth
            ],
            format: 'r8unorm',  // For CT/MRI data
            usage: GPUTextureUsage.TEXTURE_BINDING | 
                   GPUTextureUsage.COPY_DST
        });
        
        // Upload volume data
        this.device.queue.writeTexture(
            { texture: this.volumeTexture },
            this.volumeData.buffer,
            { bytesPerRow: this.volumeData.width, rowsPerImage: this.volumeData.height },
            [this.volumeData.width, this.volumeData.height, this.volumeData.depth]
        );
    }
    
    createRaycastingPipeline() {
        const raycastShader = `
            @group(0) @binding(0) var volume_tex: texture_3d<f32>;
            @group(0) @binding(1) var volume_sampler: sampler;
            @group(0) @binding(2) var tf_texture: texture_2d<f32>;  // Transfer function
            @group(0) @binding(3) var<uniform> camera: Camera;
            
            @fragment
            fn main(@location(0) world_pos: vec3<f32>) -> @location(0) vec4<f32> {
                let ray_origin = camera.position;
                let ray_dir = normalize(world_pos - ray_origin);
                
                // Ray march through volume
                var t = 0.0;
                var color = vec4<f32>(0.0);
                
                for (var i = 0u; i < 256u; i++) {
                    let sample_pos = ray_origin + ray_dir * t;
                    let tex_coord = (sample_pos + vec3<f32>(0.5)) / vec3<f32>(textureDimensions(volume_tex));
                    
                    // Early exit if outside volume
                    if (any(tex_coord < vec3<f32>(0.0)) || any(tex_coord > vec3<f32>(1.0))) {
                        break;
                    }
                    
                    // Sample volume
                    let density = textureSample(volume_tex, volume_sampler, tex_coord).r;
                    
                    // Apply transfer function
                    let tf_color = textureSample(tf_texture, default_sampler, vec2<f32>(density, 0.5));
                    
                    // Front-to-back compositing
                    color = color + tf_color * tf_color.a * (1.0 - color.a);
                    
                    // Early ray termination
                    if (color.a > 0.99) {
                        break;
                    }
                    
                    // Step size based on density gradient
                    let step_size = 0.01;  // Adaptive step size
                    t += step_size;
                }
                
                return color;
            }
        `;
        
        // Create pipeline for volume rendering
    }
}

B. Machine Learning Inference

javascript

class WebGPUML {
    constructor(device) {
        this.device = device;
        this.createMLPipelines();
    }
    
    createMLPipelines() {
        // Convolution pipeline
        this.convPipeline = this.createConvPipeline();
        
        // Matrix multiplication pipeline (for fully connected layers)
        this.matmulPipeline = this.createMatmulPipeline();
        
        // Activation functions pipeline
        this.activationPipeline = this.createActivationPipeline();
    }
    
    createMatmulPipeline() {
        const matmulShader = `
            @group(0) @binding(0) var<storage, read> A: array<f32>;
            @group(0) @binding(1) var<storage, read> B: array<f32>;
            @group(0) @binding(2) var<storage, read_write> C: array<f32>;
            @group(0) @binding(3) var<uniform> params: MatMulParams;
            
            struct MatMulParams {
                M: u32,
                N: u32,
                K: u32,
            };
            
            @compute @workgroup_size(16, 16, 1)
            fn matmul(@builtin(global_invocation_id) global_id: vec3<u32>) {
                let row = global_id.x;
                let col = global_id.y;
                
                if (row >= params.M || col >= params.N) {
                    return;
                }
                
                var sum = 0.0;
                
                for (var k = 0u; k < params.K; k++) {
                    let a_idx = row * params.K + k;
                    let b_idx = k * params.N + col;
                    sum += A[a_idx] * B[b_idx];
                }
                
                let c_idx = row * params.N + col;
                C[c_idx] = sum;
            }
        `;
        
        return this.device.createComputePipeline({
            layout: 'auto',
            compute: {
                module: this.device.createShaderModule({ code: matmulShader }),
                entryPoint: 'matmul'
            }
        });
    }
    
    async runInference(model, input) {
        // Upload input data
        const inputBuffer = this.device.createBuffer({
            size: input.byteLength,
            usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
        });
        
        this.device.queue.writeBuffer(inputBuffer, 0, input);
        
        // Run layers sequentially
        let currentBuffer = inputBuffer;
        
        for (const layer of model.layers) {
            if (layer.type === 'conv') {
                currentBuffer = await this.runConv(layer, currentBuffer);
            } else if (layer.type === 'matmul') {
                currentBuffer = await this.runMatmul(layer, currentBuffer);
            } else if (layer.type === 'activation') {
                currentBuffer = await this.runActivation(layer, currentBuffer);
            }
        }
        
        // Read back results
        const outputBuffer = await this.readBuffer(currentBuffer);
        return outputBuffer;
    }
}

10. Future Directions & Standards

A. Upcoming WebGPU Features

javascript

// 1. Mesh Shaders (proposed)
const meshShaderExample = `
    @mesh @workgroup_size(128) 
    fn mesh_main() {
        // Generate mesh dynamically on GPU
        // Bypass traditional vertex fetch & vertex shader stages
    }
    
    @task @workgroup_size(64)
    fn task_main() {
        // Cull and LOD selection before mesh generation
    }
`;

// 2. Ray Tracing (proposed extensions)
const rayTracingShader = `
    @ray_generation
    fn rg_main() {
        // Hardware-accelerated ray tracing
    }
    
    @intersection
    fn intersection_main() {
        // Custom intersection tests
    }
    
    @any_hit
    fn any_hit_main() {
        // Any-hit shader
    }
    
    @closest_hit
    fn closest_hit_main() {
        // Closest-hit shader
    }
    
    @miss
    fn miss_main() {
        // Miss shader
    }
`;

// 3. Shader Linking & Separate Compilation
const shaderLibraries = `
    // Declare external functions
    fn external_lighting(normal: vec3<f32>) -> vec3<f32>;
    
    // Library can be compiled separately and linked
`;

B. Cross-Platform Considerations

javascript

class WebGPUFeatureDetector {
    static async detectFeatures(adapter) {
        const features = [];
        const limits = adapter.limits;
        
        // Check for specific features
        const requiredFeatures = [
            'timestamp-query',
            'depth-clip-control',
            'texture-compression-bc',
            'indirect-first-instance',
            'shader-f16',
            'rg11b10ufloat-renderable',
            'bgra8unorm-storage',
            'float32-filterable'
        ];
        
        for (const feature of requiredFeatures) {
            if (adapter.features.has(feature)) {
                features.push(feature);
            }
        }
        
        // Platform-specific optimizations
        const platform = navigator.platform;
        
        if (platform.includes('Mac')) {
            // Metal-specific optimizations
            features.push('metal-specific');
        } else if (platform.includes('Win')) {
            // D3D12-specific optimizations
            features.push('d3d12-specific');
        } else {
            // Vulkan-specific optimizations
            features.push('vulkan-specific');
        }
        
        return {
            features: features,
            limits: limits,
            adapterInfo: await adapter.requestAdapterInfo()
        };
    }
    
    static getOptimalSettings(adapterInfo) {
        const settings = {
            msaa: 4,
            anisotropy: 16,
            textureFormat: 'rgba8unorm',
            depthFormat: 'depth24plus-stencil8'
        };
        
        // Adjust based on GPU
        const gpu = adapterInfo.device || '';
        
        if (gpu.includes('Apple')) {
            // Apple Silicon optimizations
            settings.textureFormat = 'bgra8unorm';  // Better on Apple
            settings.msaa = 2;  // Memory conservative
        } else if (gpu.includes('NVIDIA')) {
            // NVIDIA optimizations
            settings.anisotropy = 8;  // Conservative for mobile NVIDIA
        } else if (gpu.includes('AMD')) {
            // AMD optimizations
            settings.msaa = 2;  // AMD tends to have smaller memory
        } else if (gpu.includes('Intel')) {
            // Intel optimizations
            settings.msaa = 1;  // Integrated graphics memory limited
        }
        
        return settings;
    }
}

Short Summary

WebGPU represents a quantum leap in web graphics and compute capabilities:

Key Takeaways:

  1. Performance: 5-10x faster than WebGL for complex scenes

  2. Modern Features: Compute shaders, storage textures, explicit control

  3. Cross-Platform: Vulkan/Metal/D3D12 backends ensure wide compatibility

  4. Future-Proof: Designed for modern GPU architectures

Best Practices:

  • Use buffer pools for memory management

  • Pipeline caching for reduced stutter

  • Timestamp queries for performance profiling

  • Feature detection for cross-platform compatibility

  • Async pipeline creation for smooth loading

When to Use WebGPU:

  • ✅ Complex 3D applications (games, CAD)

  • ✅ Scientific visualization (volume rendering, simulations)

  • ✅ Machine learning inference on GPU

  • ✅ Real-time image/video processing

  • ✅ High-performance data visualization

When WebGL Might Still Be Better:

  • ❌ Simple 2D graphics

  • ❌ Maximum browser compatibility required

  • ❌ Quick prototypes where WebGPU complexity isn't needed

WebGPU is production-ready in Chrome 113+, Firefox Nightly, and Safari Technology Preview. As adoption grows, it will become the standard for high-performance web graphics, enabling previously impossible applications to run directly in the browser.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?