A shader never binds a texture, only a view. texture.createView() with no argument gives the whole resource; the descriptor narrows it with baseMipLevel/mipLevelCount, baseArrayLayer/arrayLayerCount, dimension ('2d' for one layer of an array, '2d-array', 'cube', 'cube-array'), aspect and format. Mipmaps and Loading's mipmap generator renders into one level per view, and Cover Texture Array samples six layers through one '2d-array' view.
The aspect matters for combined formats: binding a depth24plus-stencil8 view with the default 'all' failed with "Multiple aspects (Depth|Stencil 672,850 ) selected"; aspect: 'depth-only' binds as a depth texture and 'stencil-only' as a uint texture. A view may also reinterpret the format, but only between sRGB and non-sRGB twins listed in the texture's viewFormats: without the list, an rgba8unorm-srgb view of an rgba8unorm texture failed, and bgra8unorm over rgba8unorm is never view-compatible. Create views once per resource; only canvas textures need a fresh view each frame, because getCurrentTexture() returns a new texture.
<!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);
}
// Draws whatever 2D view it is given, filling the current viewport.
const code = /* wgsl */ `
@group(0) @binding(0) var image: texture_2d<f32>;
@group(0) @binding(1) var smp: sampler;
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f }
@vertex fn vs(@builtin(vertex_index) v: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
return Out(vec4f(q * 2 - 1, 0, 1), vec2f(q.x, 1 - q.y));
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return textureSampleLevel(image, smp, in.uv, 0); }`;
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;
// A 4-layer array of 64 x 96 covers with 3 mip levels; each level is painted a different shade,
// and rgba8unorm-srgb is listed in viewFormats so an sRGB view is allowed.
const covers = device.createTexture({ size: [64, 96, 4], mipLevelCount: 3, format: 'rgba8unorm',
viewFormats: ['rgba8unorm-srgb'], usage: T.TEXTURE_BINDING | T.COPY_DST });
const tints = [[20, 102, 191], [181, 69, 46], [64, 125, 59], [224, 153, 15]];
for (let layer = 0; layer < 4; layer++) for (let level = 0; level < 3; level++) {
const w = 64 >> level, h = 96 >> level, px = new Uint8Array(w * h * 4);
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
const band = y > h * 0.3 && y < h * 0.42, dim = 1 - level * 0.3;
px.set(band ? [240, 230, 205, 255] : [...tints[layer].map((c) => c * dim), 255], (y * w + x) * 4);
}
device.queue.writeTexture({ texture: covers, mipLevel: level, origin: [0, 0, layer] }, px, { bytesPerRow: w * 4 }, [w, h]);
}
// A depth-stencil texture: binding it with the default aspect 'all' is an error.
const ds = device.createTexture({ size: [4, 4], format: 'depth24plus-stencil8', usage: T.TEXTURE_BINDING | T.RENDER_ATTACHMENT });
const dsLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } }] });
device.pushErrorScope('validation');
device.createBindGroup({ layout: dsLayout, entries: [{ binding: 0, resource: ds.createView() }] });
const allAspects = await device.popErrorScope();
device.pushErrorScope('validation');
device.createBindGroup({ layout: dsLayout, entries: [{ binding: 0, resource: ds.createView({ aspect: 'depth-only' }) }] });
const depthOnly = await device.popErrorScope();
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module }, fragment: { module, targets: [{ format }] } });
const sampler = device.createSampler();
const views = [
...[0, 1, 2, 3].map((layer) => [`layer ${layer}`, { dimension: '2d', baseArrayLayer: layer, arrayLayerCount: 1 }]),
...[1, 2].map((level) => [`layer 0, mip ${level}`, { dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1, baseMipLevel: level, mipLevelCount: 1 }]),
['as rgba8unorm-srgb', { dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1, format: 'rgba8unorm-srgb' }]];
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);
views.forEach(([, descriptor], i) => {
pass.setViewport(12 + i * 84, 40, 64, 96, 0, 1); // every view at the same on-screen size
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
{ binding: 0, resource: covers.createView(descriptor) }, { binding: 1, resource: sampler }] }));
pass.draw(4);
});
pass.end();
device.queue.submit([encoder.finish()]);
ink.font = '10.5px system-ui, sans-serif'; ink.fillStyle = '#222'; ink.textAlign = 'center';
views.forEach(([name], i) => ink.fillText(name, 44 + i * 84, 154));
ink.textAlign = 'left'; ink.font = '12px ui-monospace, monospace';
ink.fillText("covers: 64 x 96 x 4 layers, 3 mip levels, viewFormats: ['rgba8unorm-srgb']", 12, 24);
ink.fillText("createView({ dimension: '2d', baseArrayLayer, baseMipLevel, format, ... })", 12, 184);
ink.font = '11.5px system-ui, sans-serif'; ink.fillStyle = '#444';
ink.fillText('Smaller mips are magnified back to the same size; the sRGB view decodes the same bytes as darker linear values.', 12, 204);
ink.font = '11px ui-monospace, monospace';
ink.fillStyle = '#8a2b2b'; ink.fillText(`depth24plus-stencil8, aspect 'all': ${allAspects ? allAspects.message.split('\n')[0] : 'ok'}`.slice(0, 92), 12, 250);
ink.fillStyle = '#1e6b3a'; ink.fillText(`aspect: 'depth-only': ${depthOnly ? depthOnly.message.split('\n')[0] : 'binds as a depth texture'}`, 12, 270);
}
main();
</script>