Backend Translation

Translating Commands to Direct3D 12, Vulkan and Metal

Each implementation picks one native API per platform. Dawn 1,149 uses Direct3D 12 on Windows, Metal on Apple platforms, Vulkan on Linux, Android and ChromeOS, and OpenGL ES for older devices in compatibility mode, and Tint emits the matching shader language: HLSL (compiled by Microsoft's DXC), MSL, SPIR-V or GLSL.

A backend is more than a syntax converter: WebGPU's guarantees (zeroed resources, no out-of-bounds access, barriers computed for you) must hold everywhere, so each backend does work the native API leaves to the programmer, such as tracking resource states to insert Direct3D 12 barriers. This script reads the adapters Dawn found from chrome://gpu:

Listing the adapters Dawn found on this machine, per backendJavaScript
import re
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
    browser = p.chromium.launch(channel='chrome', args=['--headless=new'])   # installed Chrome
    page = browser.new_page()
    page.goto('chrome://gpu')
    page.get_by_text('Dawn Info').wait_for()                   # filled in asynchronously
    dawn = page.locator('#content').inner_text().split('Dawn Info')[1]   # pierces shadow DOM
    browser.close()
pattern = r'<(.+?)> (\w+) backend - (.+)\n\[WebGPU Status\]\n(.+)'
for kind, api, name, status in re.findall(pattern, dawn):
    reason = re.search(r'crbug\.com/\d+: ([^.|]+)', status)    # first blocklist reason, if any
    print(f'{api:6} {"CPU" if kind == "CPU" else "GPU"}  {name:29}  '
          f'{reason.group(1) if reason else status}')
Output
D3D12  GPU  NVIDIA GeForce GTX 1650        Available
D3D12  CPU  Microsoft Basic Render Driver  CPU adapters not fully tested or conformant
Vulkan CPU  SwiftShader Device (Subzero)   CPU adapters not fully tested or conformant
Vulkan GPU  NVIDIA GeForce GTX 1650        Available
D3D11  GPU  NVIDIA GeForce GTX 1650        D3D11 backend not fully implemented
D3D11  CPU  Microsoft Basic Render Driver  D3D11 backend not fully implemented

Dawn found six adapters but allows only two, and by default hands pages the GTX 1650 through Direct3D 12. The blocklisted SwiftShader 2,546 line explains why forced software WebGPU was unreliable here. The same page lists Dawn's toggles, such as use_dxc (compile HLSL with DXC rather than the older FXC).

One WGSL shader fanned out to Dawn's backends: HLSL for Direct3D 12, MSL for Metal, SPIR-V for Vulkan, GLSL for OpenGL ESHTMLLive
<!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="360"></canvas>
  <canvas id="labels" width="600" height="360"></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);
}
function label(text, x, y, size = 12, color = '#2b2b2b', align = 'center', weight = '') {
  ink.font = `${weight} ${size}px system-ui, sans-serif`; ink.fillStyle = color; ink.textAlign = align;
  ink.fillText(text, x, y);
}

// platform, native API, shader language Tint emits, colour
const routes = [['Windows', 'Direct3D 12', 'HLSL, compiled by DXC', [0.20, 0.50, 0.80]],
                ['macOS, iOS', 'Metal', 'MSL', [0.55, 0.55, 0.60]],
                ['Linux, Android, ChromeOS', 'Vulkan', 'SPIR-V', [0.75, 0.25, 0.25]],
                ['Older devices (compat)', 'OpenGL ES', 'GLSL', [0.30, 0.60, 0.40]]];

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 boxes = [[14, 128, 100, 60, 0.98, 0.92, 0.75, 10]];          // the WGSL source
  label('WGSL', 64, 155, 15, '#222', 'center', 'bold');
  label('+ API calls', 64, 173, 11, '#555');
  boxes.push([114, 157, 20, 2, 0.4, 0.4, 0.4, 1], [134, 40, 2, 262, 0.4, 0.4, 0.4, 1]);
  label('Platform', 216, 28, 12, '#666'); label('Native API', 370, 28, 12, '#666'); label('Tint emits', 518, 28, 12, '#666');
  routes.forEach(([platform, api, language, c], i) => {
    const y = 40 + i * 66;
    boxes.push([134, y + 24, 16, 2, 0.4, 0.4, 0.4, 1]);
    boxes.push([150, y, 132, 50, 1, 1, 1, 9], [298, y, 144, 50, ...c, 9], [458, y, 128, 50, 1, 1, 1, 9]);
    boxes.push([282, y + 24, 16, 2, 0.4, 0.4, 0.4, 1], [442, y + 24, 16, 2, 0.4, 0.4, 0.4, 1]);
    label(platform, 216, y + 30, 11.5, '#222');
    label(api, 370, y + 30, 14, '#fff', 'center', 'bold');
    label(language, 522, y + 30, 11.5, '#222');
  });
  // What this browser's adapter reports (the backend itself is not exposed to pages)
  const info = adapter.info;
  label(`This page's adapter: vendor "${info.vendor || 'n/a'}", architecture "${info.architecture || 'n/a'}"`, 300, 324, 12, '#1f4f8a');
  label('Backends also insert barriers, zero new resources and bounds-check every access.', 300, 344, 12, '#444');

  const module = device.createShaderModule({ code: `
    struct Box { rect: vec4f, style: vec4f }          // style: r, g, b, corner radius
    @group(0) @binding(0) var<storage> boxes: array<Box>;
    struct Out { @builtin(position) pos: vec4f, @location(0) local: vec2f,
                 @location(1) @interpolate(flat) i: u32 }
    @vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
      let corner = vec2f(f32(v & 1), f32(v >> 1));   // triangle-strip corners
      let r = boxes[i].rect;
      let px = r.xy + corner * r.zw;
      return Out(vec4f(px.x / 300 - 1, 1 - px.y / 180, 0, 1), corner * r.zw, i);
    }
    @fragment fn fs(in: Out) -> @location(0) vec4f {
      let b = boxes[in.i];
      let half = b.rect.zw / 2;
      let q = abs(in.local - half) - half + b.style.w; // signed distance to a rounded box
      let d = length(max(q, vec2f(0))) + min(max(q.x, q.y), 0) - b.style.w;
      let a = clamp(0.5 - d, 0, 1);
      return vec4f(b.style.rgb * a, a);               // premultiplied alpha
    }` });
  const blend = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' };
  const pipeline = device.createRenderPipeline({ layout: 'auto',
    vertex: { module }, primitive: { topology: 'triangle-strip' },
    fragment: { module, targets: [{ format, blend: { color: blend, alpha: blend } }] } });
  const data = new Float32Array(boxes.flat());
  const buffer = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
  device.queue.writeBuffer(buffer, 0, data);
  const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer } }] });

  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, boxes.length);
  pass.end();
  device.queue.submit([encoder.finish()]);
}
main();
</script>