Texture Formats

Texture Dimensions, Formats and Usage Flags

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:

GPUTextureUsage flags in Chrome 154 1
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").

A 1D, a 2D and a 3D texture drawn side by side, with the usage and mip-count rules checked in error scopesHTMLLive
<!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>