electrobun-webgpu

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Electrobun WebGPU Patterns

Electrobun WebGPU 模式

Electrobun wraps WGPU (a Rust WebGPU abstraction) via Bun FFI. GPU windows bypass the webview entirely — they render directly to a native surface.
Electrobun 通过 Bun FFI 封装了 WGPU(一个 Rust 的 WebGPU 抽象层)。GPU 窗口完全绕过 webview —— 它们直接渲染到原生 surface。

Config Requirement

配置要求

Always required before WebGPU code will work:
typescript
// electrobun.config.ts
mac: { bundleWGPU: true },
win: { bundleWGPU: true },
linux: { bundleWGPU: true },
在 WebGPU 代码生效前始终必须配置:
typescript
// electrobun.config.ts
mac: { bundleWGPU: true },
win: { bundleWGPU: true },
linux: { bundleWGPU: true },

GpuWindow Setup

GpuWindow 设置

typescript
import { GpuWindow } from "electrobun/bun";

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

const view = gpuWin.createView(); // WGPUView
typescript
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

KEEPALIVE —— 关键模式

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.
typescript
// 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);
除非持有对 FFI 指针的引用,否则 Bun 的 GC 会将其回收。没有 KEEPALIVE,你的应用会在渲染过程中因段错误而崩溃。
typescript
// 始终创建该数组,并把每个 GPU 对象 push 进去
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

渲染循环模式

typescript
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);
typescript
const FRAME_MS = 16; // ~60fps

function renderFrame() {
  // 1. 使用当前状态更新 uniform buffer
  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. 创建 command encoder
  const encoder = device.createCommandEncoder();
  KEEPALIVE.push(encoder);

  // 3. 开始 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. 绘制
  pass.setPipeline(pipeline);
  pass.setVertexBuffer(0, vertexBuffer);
  pass.draw(3);
  pass.end();

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

setInterval(renderFrame, FRAME_MS);

WGSL Shader Structure

WGSL 着色器结构

wgsl
// 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 {
  // TODO: replace with your shader logic
  return vec4f(in.uv, 0.5, 1.0);
}
wgsl
// 顶点着色器
struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) uv: vec2f,
}

@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
  // 全屏三角形
  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 {
  // TODO: 替换为你的着色器逻辑
  return vec4f(in.uv, 0.5, 1.0);
}

Vertex Buffer with DataView

使用 DataView 构建 Vertex Buffer

GPU structs must be manually serialized. Vertex layout:
[x, y, time, res.x, res.y, mouse.x, mouse.y]
(all f32).
typescript
// 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);
});
GPU 结构体必须手动序列化。顶点布局:
[x, y, time, res.x, res.y, mouse.x, mouse.y]
(均为 f32)。
typescript
// 3 个顶点 × 7 个 float × 4 字节 = 84 字节
const vertexData = new ArrayBuffer(3 * 7 * 4);
const dv = new DataView(vertexData);

// 全屏三角形顶点
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.
  1. 缺少 KEEPALIVE → 渲染过程中出现段错误或静默数据损坏。
  2. bundleWGPU: false
    → 运行时错误;Electrobun 不会包含 WGPU 原生库。
  3. 窗口 resize 时未重建 swap chain → 调整窗口尺寸后渲染画面变形。
  4. 在
    DataView.setFloat32
    中忘记
    little-endian: true
    → 在大多数平台上产生垃圾 GPU 数据。
  5. 阻塞渲染循环 → 应在渲染 interval 之外使用 async I/O,绝不要在其中进行。