Milady Electrobun WebGPU

Use when working with WebGPU in Electrobun — GpuWindow, WGPUView, WGSL shaders, KEEPALIVE pattern, render loops, FFI pointer management, and GPU buffer serialization.

install
source · Clone the upstream repo
git clone https://github.com/milady-ai/milady
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/milady-ai/milady "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/plugins/electrobun-dev/skills/electrobun-webgpu" ~/.claude/skills/milady-ai-milady-electrobun-webgpu && rm -rf "$T"
manifest: .claude/plugins/electrobun-dev/skills/electrobun-webgpu/SKILL.md
source content

Electrobun WebGPU Patterns

Electrobun wraps WGPU (a Rust WebGPU abstraction) via Bun FFI. GPU windows bypass the webview entirely — they render directly to a native surface.

Config Requirement

Always required before WebGPU code will work:

// electrobun.config.ts
mac: { bundleWGPU: true },
win: { bundleWGPU: true },
linux: { bundleWGPU: true },

GpuWindow Setup

import { GpuWindow } from "electrobun/bun";

const gpuWin = new GpuWindow({
  title: "GPU App",
  frame: { width: 800, height: 600 },
  centered: true,
});

const view = gpuWin.createView(); // WGPUView

KEEPALIVE — Critical Pattern

Bun's GC will collect FFI pointers unless you hold a reference to them. Without KEEPALIVE, your app will crash mid-render with a segfault.

// ALWAYS create this array and push every GPU object into it
const KEEPALIVE: unknown[] = [];

const adapter = await navigator.gpu.requestAdapter();
KEEPALIVE.push(adapter);

const device = await adapter.requestDevice();
KEEPALIVE.push(device);

const pipeline = device.createRenderPipeline({ /* ... */ });
KEEPALIVE.push(pipeline);

const buffer = device.createBuffer({ /* ... */ });
KEEPALIVE.push(buffer);

Render Loop Pattern

const FRAME_MS = 16; // ~60fps

function renderFrame() {
  // 1. Update uniform buffer with current state
  const data = new ArrayBuffer(32);
  const view = new DataView(data);
  view.setFloat32(0, performance.now() / 1000, true); // time
  view.setFloat32(4, canvas.width, true);             // resolution.x
  view.setFloat32(8, canvas.height, true);            // resolution.y
  view.setFloat32(12, mouseX, true);                  // mouse.x
  view.setFloat32(16, mouseY, true);                  // mouse.y
  device.queue.writeBuffer(uniformBuffer, 0, data);

  // 2. Create command encoder
  const encoder = device.createCommandEncoder();
  KEEPALIVE.push(encoder);

  // 3. Begin render pass
  const pass = encoder.beginRenderPass({
    colorAttachments: [{
      view: context.getCurrentTexture().createView(),
      clearValue: { r: 0, g: 0, b: 0, a: 1 },
      loadOp: "clear",
      storeOp: "store",
    }],
  });

  // 4. Draw
  pass.setPipeline(pipeline);
  pass.setVertexBuffer(0, vertexBuffer);
  pass.draw(3);
  pass.end();

  // 5. Submit
  device.queue.submit([encoder.finish()]);
}

setInterval(renderFrame, FRAME_MS);

WGSL Shader Structure

// Vertex shader
struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) uv: vec2f,
}

@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
  // Full-screen triangle
  var positions = array<vec2f, 3>(
    vec2f(-1.0, -1.0),
    vec2f( 3.0, -1.0),
    vec2f(-1.0,  3.0),
  );
  var out: VertexOutput;
  out.position = vec4f(positions[idx], 0.0, 1.0);
  out.uv = (positions[idx] + vec2f(1.0)) * 0.5;
  return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
  // Replace this with your shader logic.
  return vec4f(in.uv, 0.5, 1.0);
}

Vertex Buffer with DataView

GPU structs must be manually serialized. Vertex layout:

[x, y, time, res.x, res.y, mouse.x, mouse.y]
(all f32).

// 3 vertices × 7 floats × 4 bytes = 84 bytes
const vertexData = new ArrayBuffer(3 * 7 * 4);
const dv = new DataView(vertexData);

// Full-screen triangle vertices
const verts = [
  [-1, -1], [3, -1], [-1, 3],
];

verts.forEach(([x, y], i) => {
  const offset = i * 7 * 4;
  dv.setFloat32(offset + 0,  x, true);    // position.x
  dv.setFloat32(offset + 4,  y, true);    // position.y
  dv.setFloat32(offset + 8,  time, true); // time
  dv.setFloat32(offset + 12, width, true);
  dv.setFloat32(offset + 16, height, true);
  dv.setFloat32(offset + 20, mouseX, true);
  dv.setFloat32(offset + 24, mouseY, true);
});

Common Mistakes

  1. No KEEPALIVE → segfault or silent corruption mid-render.
  2. bundleWGPU: false
    → runtime error; Electrobun won't include the WGPU native library.
  3. Not recreating swap chain on resize → distorted rendering after window resize.
  4. Forgetting
    little-endian: true
    in
    DataView.setFloat32
    → garbage GPU data on most platforms.
  5. Blocking the render loop → use async I/O outside the render interval, never inside.