device.createTexture() requires a size (width, height, and depthOrArrayLayers), a format and a usage; dimension ('1d', '2d' or '3d'), mipLevelCount, sampleCount and viewFormats are optional. The usage flags are fixed at creation and checked at every use:
| Flag | Value | Allows |
|---|---|---|
| COPY_SRC | 1 | Copies out of the texture |
| COPY_DST | 2 | writeTexture(), copies in, copyExternalImageToTexture() |
| TEXTURE_BINDING | 4 | Sampling or textureLoad() in a shader |
| STORAGE_BINDING | 8 | textureStore() from shaders (Storage Textures) |
| RENDER_ATTACHMENT | 16 | Color or depth attachment; also external-image uploads |
| TRANSIENT_ATTACHMENT | 32 | Attachment that never leaves the pass (with RENDER_ATTACHMENT only) |
The format fixes texel layout and what the texture can do. rgba8unorm is the workhorse: it filters, renders, blends and is a storage format. rgba8unorm-srgb renders and filters but is not a storage format; r32float is not filterable without the float32-filterable feature; bgra8unorm needs bgra8unorm-storage for storage; depth formats come from Depth and Multisampling. Default device limits are lower than the adapter's: this GTX 1650 reports a maxTextureDimension2D of 16384 and maxTextureArrayLayers of 2048, but a default device allows 8192 and 256, and a 9000-pixel texture failed until the higher limit was put in requiredLimits. A full mip chain has floor(log2(max(width, height))) + 1 levels: 9 for a 256 x 384 cover, and 10 failed ("exceeds the maximum (9) for its size").
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="340"></canvas>
<canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
const code = /* wgsl */ `
@group(0) @binding(0) var ramp: texture_1d<f32>; // dimension '1d'
@group(0) @binding(1) var cover: texture_2d<f32>; // dimension '2d'
@group(0) @binding(2) var cube: texture_3d<f32>; // dimension '3d'
@group(0) @binding(3) var smp: sampler;
@group(0) @binding(4) var<uniform> time: f32;
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f, @location(1) @interpolate(flat) which: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
let rects = array(vec4f(-0.95, 0.55, 1.9, 0.15), vec4f(-0.95, -0.78, 0.8, 1.0), vec4f(0.1, -0.78, 0.85, 1.0));
let r = rects[i];
return Out(vec4f(r.xy + q * r.zw, 0, 1), vec2f(q.x, 1 - q.y), i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let a = textureSample(ramp, smp, in.uv.x); // one coordinate
let b = textureSample(cover, smp, in.uv); // two
let c = textureSample(cube, smp, vec3f(in.uv, fract(time * 0.2))); // three: a moving slice
switch in.which { case 0u: { return a; } case 1u: { return b; } default: { return c; } }
}`;
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const T = GPUTextureUsage;
const usage = T.TEXTURE_BINDING | T.COPY_DST;
// 1D: a 256-texel price gradient. 2D: a little cover. 3D: a 16^3 colour cube.
const ramp = device.createTexture({ dimension: '1d', size: [256], format: 'rgba8unorm', usage });
device.queue.writeTexture({ texture: ramp }, new Uint8Array(256 * 4).map((_, k) => [k >> 2, 102, 191 - (k >> 3), 255][k % 4]), { bytesPerRow: 1024 }, [256]);
const cover = device.createTexture({ size: [8, 12], format: 'rgba8unorm', usage });
const coverPixels = new Uint8Array(8 * 12 * 4);
for (let y = 0; y < 12; y++) for (let x = 0; x < 8; x++) {
const band = y === 4 || y === 5, spine = x === 0;
coverPixels.set(band ? [245, 230, 200, 255] : spine ? [12, 70, 140, 255] : [20, 102, 191, 255], (y * 8 + x) * 4);
}
device.queue.writeTexture({ texture: cover }, coverPixels, { bytesPerRow: 32 }, [8, 12]);
const cube = device.createTexture({ dimension: '3d', size: [16, 16, 16], format: 'rgba8unorm', usage });
const voxels = new Uint8Array(16 ** 3 * 4);
for (let z = 0; z < 16; z++) for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++)
voxels.set([x * 17, y * 17, z * 17, 255], ((z * 16 + y) * 16 + x) * 4);
device.queue.writeTexture({ texture: cube }, voxels, { bytesPerRow: 64, rowsPerImage: 16 }, [16, 16, 16]);
// Rules, checked in error scopes: storage on an -srgb format, and one mip level too many.
const check = async (fn) => { device.pushErrorScope('validation'); try { fn(); } catch (e) { return e.message; } const e = await device.popErrorScope(); return e ? e.message.split('\n')[0] : 'ok'; };
const srgbStorage = await check(() => device.createTexture({ size: [4, 4], format: 'rgba8unorm-srgb', usage: T.STORAGE_BINDING }));
const tooManyMips = await check(() => device.createTexture({ size: [256, 384], mipLevelCount: 10, format: 'rgba8unorm', usage }));
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module }, fragment: { module, targets: [{ format }] } });
const time = device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
{ binding: 0, resource: ramp.createView() }, { binding: 1, resource: cover.createView() },
{ binding: 2, resource: cube.createView() }, { binding: 3, resource: device.createSampler() }, // nearest: texels stay crisp
{ binding: 4, resource: { buffer: time } }] });
ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
ink.fillText("'1d', 256 x 1, rgba8unorm", 18, 22);
ink.fillText("'2d', 8 x 12", 18, 110);
ink.fillText("'3d', 16 x 16 x 16: a moving slice", 336, 110);
ink.font = '11px ui-monospace, monospace'; ink.fillStyle = '#8a2b2b';
ink.fillText(`rgba8unorm-srgb + STORAGE_BINDING: ${srgbStorage}`.slice(0, 90), 10, 316);
ink.fillText(`256 x 384 with mipLevelCount 10: ${tooManyMips}`.slice(0, 90), 10, 332);
function frame(now) {
device.queue.writeBuffer(time, 0, new Float32Array([now / 1000]));
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(pipeline);
pass.setBindGroup(0, group);
pass.draw(4, 3);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();
</script>