device.queue.writeTexture(destination, data, layout, size) copies bytes from JavaScript into a texture, the texture equivalent of writeBuffer(). The layout's bytesPerRow is the distance between rows in your data and, unlike buffer-to-texture copies (Buffer-Texture Copies), has no alignment rule. This page builds an 8 x 8 BookNest "B" from a string, uploads its 256 bytes, and samples it with five different samplers, one viewport each:
<style>#labels { display: flex; font: 14px sans-serif } span { width: 120px; text-align: center }
</style>
<canvas id="gpu" width="600" height="120" style="image-rendering: pixelated"></canvas>
<div id="labels"></div>
<script type="module">
const rows = ['o.......', '.BBBBB..', '.B....B.', '.BBBBB..', // an 8 x 8 "B"
'.B....B.', '.B....B.', '.BBBBB..', '........']; // o marks the top left
const rgba = { '.': [245, 240, 230, 255], B: [20, 102, 191, 255], o: [240, 107, 0, 255] };
const texels = new Uint8Array(rows.join('').split('').flatMap((c) => rgba[c]));
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const texture = device.createTexture({ size: [8, 8], format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
device.queue.writeTexture({ texture }, texels, { bytesPerRow: 8 * 4 }, [8, 8]); // 4 B/texel
const samplers = { nearest: {}, linear: { magFilter: 'linear' } }; // defaults: nearest
for (const m of ['repeat', 'mirror-repeat', 'clamp-to-edge']) {
samplers[m] = { addressModeU: m, addressModeV: m };
}
labels.innerHTML = Object.keys(samplers).map((name) => `<span>${name}</span>`).join('');
const code = /* wgsl */ `
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) panel: u32) -> Out {
let q = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1),
vec2f(0, 1))[v];
let uv = vec2f(q.x, 1 - q.y); // texture rows run top to bottom
return Out(vec4f(q * 2 - 1, 0, 1), select(uv, uv * 3 - 1, panel >= 2)); // -1..2
}
@group(0) @binding(0) var tex: texture_2d<f32>;
@group(0) @binding(1) var smp: sampler;
@fragment fn fs(in: Out) -> @location(0) vec4f { return textureSample(tex, smp, in.uv); }`;
const context = gpu.getContext('webgpu'), format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module },
fragment: { module, targets: [{ format }] } });
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear', storeOp: 'store',
view: context.getCurrentTexture().createView(), clearValue: [1, 1, 1, 1] }] });
pass.setPipeline(pipeline);
Object.values(samplers).forEach((descriptor, i) => {
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: texture.createView() },
{ binding: 1, resource: device.createSampler(descriptor) }] }));
pass.setViewport(i * 120 + 4, 4, 112, 112, 0, 1);
pass.draw(6, 1, 0, i); // firstInstance = panel number
});
pass.end();
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
window.__done = true;
</script>
The texture and its view never change; only the sampler in each bind group does. The first two panels magnify the texture 14 times, and the last three stretch the coordinates to -1..2 so the address modes show.