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:
-
Reducing CPU overhead (more work done on GPU)
-
Exposing modern GPU capabilities
-
Better performance (often 2-3x faster than WebGL)
-
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:
-
Performance: 5-10x faster than WebGL for complex scenes
-
Modern Features: Compute shaders, storage textures, explicit control
-
Cross-Platform: Vulkan/Metal/D3D12 backends ensure wide compatibility
-
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!