/ proof-of-concept / fmm3dbie-webgpu-poc
Sign in
proof-of-concept / fmm3dbie-webgpu-poc
fmm3dbie-webgpu-poc / src / gpu / device.ts
180 lines · 6.4 KBCodeBlameHistory
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 1/** The GPU device, plus small helpers for buffers, uniforms, readback and paced dispatching. */
3export async function requestSolverDevice(): Promise<Gpu> {
4 if (!navigator.gpu) {
5 throw new Error(
6 'this browser has no WebGPU. Chrome and Edge 113+, Safari 26+, and Firefox 141+ on Windows have it; ' +
7 'on Linux, Chrome may need chrome://flags/#enable-unsafe-webgpu and Firefox dom.webgpu.enabled.',
8 );
9 }
10 const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
11 if (!adapter) throw new Error('WebGPU found no adapter on this machine.');
12 const lim = adapter.limits;
13 // The near weights of a medium-sized problem are a few hundred MB, so ask
14 // for the largest buffers the adapter will give rather than the 128 MB default.
15 const device = await adapter.requestDevice({
16 requiredLimits: {
17 maxStorageBufferBindingSize: lim.maxStorageBufferBindingSize,
18 maxBufferSize: lim.maxBufferSize,
19 maxComputeWorkgroupStorageSize: Math.max(16384, Math.min(lim.maxComputeWorkgroupStorageSize, 32768)),
20 },
21 });
22 device.lost.then((info) => {
23 if (info.reason !== 'destroyed') console.error(`WebGPU device lost: ${info.message}`);
24 });
25 return new Gpu(device, adapter);
28export function adapterDescription(adapter: GPUAdapter | null): string {
29 if (!adapter) return 'no adapter';
30 const info = (adapter as GPUAdapter & { info?: GPUAdapterInfo }).info;
31 if (!info) return 'unknown adapter';
32 return [info.description, info.device, info.architecture, info.vendor].filter((s) => s).join(', ') || 'unknown adapter';
35/** A device wrapper with the conveniences the solver needs. */
36export class Gpu {
37 private pipelines = new Map<string, GPUComputePipeline>();
39 /**
40 * The adapter is held for as long as the device is used: in Chrome, async
41 * device calls fail ("a valid external Instance reference no longer exists")
42 * once the adapter has been garbage collected.
43 */
44 constructor(
45 readonly device: GPUDevice,
46 readonly adapter?: GPUAdapter,
47 ) {}
49 storage(data: ArrayBufferView | number, extraUsage: GPUBufferUsageFlags = 0): GPUBuffer {
50 const size = typeof data === 'number' ? data : data.byteLength;
51 const buf = this.device.createBuffer({
52 size: Math.max(16, Math.ceil(size / 16) * 16),
53 usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC | extraUsage,
54 });
55 if (typeof data !== 'number') this.device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
56 return buf;
57 }
59 uniform(bytes: number): GPUBuffer {
60 return this.device.createBuffer({ size: Math.max(16, Math.ceil(bytes / 16) * 16), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
61 }
63 write(buf: GPUBuffer, data: ArrayBufferView, offset = 0) {
64 this.device.queue.writeBuffer(buf, offset, data.buffer, data.byteOffset, data.byteLength);
65 }
67 /** Compile a pipeline once; throws with the WGSL diagnostics if the shader does not compile. */
68 async pipeline(key: string, code: string, entry: string): Promise<GPUComputePipeline> {
69 let p = this.pipelines.get(key);
70 if (!p) {
71 const module = this.device.createShaderModule({ code, label: key });
72 try {
73 p = await this.device.createComputePipelineAsync({ layout: 'auto', compute: { module, entryPoint: entry }, label: key });
74 } catch (e) {
75 throw new Error(`could not compile the ${key} kernel: ${e instanceof Error ? e.message : String(e)}`);
76 }
77 this.pipelines.set(key, p);
78 }
79 return p;
80 }
82 bind(pipeline: GPUComputePipeline, buffers: GPUBuffer[]): GPUBindGroup {
83 return this.device.createBindGroup({
84 layout: pipeline.getBindGroupLayout(0),
85 entries: buffers.map((b, i) => ({ binding: i, resource: { buffer: b } })),
86 });
87 }
89 dispatch(pipeline: GPUComputePipeline, group: GPUBindGroup, nGroups: number) {
90 const enc = this.device.createCommandEncoder();
91 const pass = enc.beginComputePass();
92 pass.setPipeline(pipeline);
93 pass.setBindGroup(0, group);
94 pass.dispatchWorkgroups(nGroups);
95 pass.end();
96 this.device.queue.submit([enc.finish()]);
97 }
99 clear(buf: GPUBuffer) {
100 const enc = this.device.createCommandEncoder();
101 enc.clearBuffer(buf);
102 this.device.queue.submit([enc.finish()]);
103 }
105 async read(buf: GPUBuffer, bytes = buf.size): Promise<ArrayBuffer> {
106 const staging = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
107 const enc = this.device.createCommandEncoder();
108 enc.copyBufferToBuffer(buf, 0, staging, 0, bytes);
109 this.device.queue.submit([enc.finish()]);
110 await staging.mapAsync(GPUMapMode.READ);
111 const copy = staging.getMappedRange().slice(0);
112 staging.unmap();
113 staging.destroy();
114 return copy;
115 }
117 async sync() {
118 await this.device.queue.onSubmittedWorkDone();
119 }
122/** Little-endian writer for uniform structs of 32-bit scalars. */
123export class UniformWriter {
124 private view: DataView;
125 private off = 0;
126 readonly bytes: ArrayBuffer;
128 constructor(nWords: number) {
129 this.bytes = new ArrayBuffer(Math.ceil((4 * nWords) / 16) * 16);
130 this.view = new DataView(this.bytes);
131 }
132 u32(v: number) {
133 this.view.setUint32(this.off, v >>> 0, true);
134 this.off += 4;
135 return this;
136 }
137 i32(v: number) {
138 this.view.setInt32(this.off, v | 0, true);
139 this.off += 4;
140 return this;
141 }
142 f32(v: number) {
143 this.view.setFloat32(this.off, v, true);
144 this.off += 4;
145 return this;
146 }
147 /** Skip to the next 16-byte boundary (vec4 alignment). */
148 align16() {
149 this.off = Math.ceil(this.off / 16) * 16;
150 return this;
151 }
154/**
155 * Chooses how much work to submit per dispatch so that each one takes about
156 * `target` seconds: long enough to keep the GPU busy, short enough that the
157 * page stays responsive, progress can be reported, and no driver watchdog is
158 * tripped. Work is measured in arbitrary units chosen by the caller.
159 */
160export class Pacer {
161 private rate = 0; // units per second, 0 = unknown
163 constructor(
164 private target = 0.15,
165 private initial = 1,
166 ) {}
168 /** Units to submit next, given a hard cap. */
169 next(cap: number): number {
170 if (this.rate <= 0) return Math.min(cap, this.initial);
171 return Math.max(1, Math.min(cap, Math.floor(this.rate * this.target)));
172 }
174 /** Record that `units` took `seconds`. */
175 record(units: number, seconds: number) {
176 if (seconds <= 0) return;
177 const r = units / Math.max(seconds, 1e-3);
178 this.rate = this.rate > 0 ? 0.5 * (this.rate + r) : r;
179 }
↑↓ move↵openescclose