electrobun-webgpu
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseElectrobun 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(); // WGPUViewtypescript
import { GpuWindow } from "electrobun/bun";
const gpuWin = new GpuWindow({
title: "GPU App",
frame: { width: 800, height: 600 },
centered: true,
});
const view = gpuWin.createView(); // WGPUViewKEEPALIVE — 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: (all f32).
[x, y, time, res.x, res.y, mouse.x, mouse.y]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 结构体必须手动序列化。顶点布局:(均为 f32)。
[x, y, time, res.x, res.y, mouse.x, mouse.y]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
常见错误
- No KEEPALIVE → segfault or silent corruption mid-render.
- → runtime error; Electrobun won't include the WGPU native library.
bundleWGPU: false - Not recreating swap chain on resize → distorted rendering after window resize.
- Forgetting in
little-endian: true→ garbage GPU data on most platforms.DataView.setFloat32 - Blocking the render loop → use async I/O outside the render interval, never inside.
- 缺少 KEEPALIVE → 渲染过程中出现段错误或静默数据损坏。
- → 运行时错误;Electrobun 不会包含 WGPU 原生库。
bundleWGPU: false - 窗口 resize 时未重建 swap chain → 调整窗口尺寸后渲染画面变形。
- 在 中忘记
DataView.setFloat32→ 在大多数平台上产生垃圾 GPU 数据。little-endian: true - 阻塞渲染循环 → 应在渲染 interval 之外使用 async I/O,绝不要在其中进行。