Configuring Samplers

device.createSampler() takes these fields; every default is the cheapest choice, so an empty descriptor gives point sampling with clamped coordinates:

GPUSamplerDescriptor fields
Field Values (default first) Effect
addressModeU, V, W clamp-to-edge, repeat, mirror-repeat Coordinates outside 0..1
magFilter, minFilter nearest, linear Enlarged or shrunk texels
mipmapFilter nearest, linear Blend between mip levels (Mipmaps and Loading)
lodMinClamp, lodMaxClamp 0, 32 Restrict the mip levels used
compare none Makes a comparison sampler (Comparison Samplers)
maxAnisotropy 1 (usually up to 16) Anisotropic filtering, all filters linear

The figure in 2D Textures shows the filters and address modes. nearest keeps hard texel edges, right for pixel art, lookup tables and integer data; linear blends four texels, the choice for photos and text. repeat tiles the "B" (the orange corner repeats at every tile), mirror-repeat flips alternate tiles so edges meet seamlessly, and clamp-to-edge smears the border texels outward: the orange corner fills the top-left region. There is no border-color mode. maxAnisotropy: 8 with the default nearest filters failed ("... is not FilterMode::Linear while using anisotropic filter"). Samplers are small immutable objects: create a handful at startup and share them.

A "B" texture through six samplers: nearest and linear filtering with repeat, mirror-repeat and clamp-to-edge addressingHTMLLive
<!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="380"></canvas>
  <canvas id="labels" width="600" height="380"></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);
}

// Texture coordinates run from -0.6 to 1.6, so the address mode decides what lies outside 0..1.
const code = /* wgsl */ `
@group(0) @binding(0) var letter: 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) * 2.2 - 0.6);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
  let edge = max(step(abs(in.uv.x - 0.5), 0.5) * step(abs(in.uv.y - 0.5), 0.5), 0.6);   // dim outside 0..1
  return vec4f(textureSample(letter, smp, in.uv).rgb * edge, 1);
}`;

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 });

  // A tiny 16 x 16 "B" with an orange top-left corner, painted with the 2D API.
  const art = new OffscreenCanvas(16, 16), g = art.getContext('2d');
  g.fillStyle = '#f5efe2'; g.fillRect(0, 0, 16, 16);
  g.fillStyle = '#e0801e'; g.fillRect(0, 0, 5, 5);
  g.fillStyle = '#14509a'; g.font = 'bold 14px sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText('B', 9, 9);
  const texture = device.createTexture({ size: [16, 16], format: 'rgba8unorm',
    usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT });
  device.queue.copyExternalImageToTexture({ source: art }, { texture }, [16, 16]);

  // maxAnisotropy needs linear filters: an error scope catches the mistake.
  device.pushErrorScope('validation');
  device.createSampler({ maxAnisotropy: 8 });
  const anisoError = 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 modes = ['repeat', 'mirror-repeat', 'clamp-to-edge'], filters = ['nearest', 'linear'];
  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);
  filters.forEach((filter, row) => modes.forEach((mode, col) => {
    const sampler = device.createSampler({ addressModeU: mode, addressModeV: mode, magFilter: filter, minFilter: filter });
    pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
      { binding: 0, resource: texture.createView() }, { binding: 1, resource: sampler }] }));
    pass.setViewport(14 + col * 196, 30 + row * 170, 176, 140, 0, 1);
    pass.draw(4);
  }));
  pass.end();
  device.queue.submit([encoder.finish()]);

  ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
  filters.forEach((filter, row) => modes.forEach((mode, col) => ink.fillText(`${filter}, ${mode}`, 14 + col * 196, 24 + row * 170)));
  ink.font = '11px ui-monospace, monospace'; ink.fillStyle = '#8a2b2b';
  ink.fillText(`maxAnisotropy: 8 with nearest filters: ${anisoError ? anisoError.message.split('\n')[0] : 'ok'}`.slice(0, 92), 10, 372);
}
main();
</script>