/** * The GPU device, requested the same way everywhere (app, tests, scripts). * * One limit matters here. A fused kernel binds one storage buffer per distinct * field its line reads, plus its output and the parameter block, and the air * update reads a lot of fields at once — the two pressure histories, the * medium, the wall mask, the Laplacian, the coupling source. WebGPU only * guarantees 8 storage buffers per compute stage, so we ask for whatever the * adapter will give up to 16. When that is not enough the planner splits the * kernel instead of failing (see `fitToBudget` in plan.ts), so this is a * performance request rather than a requirement. */ export const MAX_STORAGE_BUFFERS = 16; /** The adapter the device came from, kept so its limits and info stay * available for as long as the device is in use. */ let heldAdapter: GPUAdapter | null = null; export async function requestAcousticDevice(): Promise { if (!navigator.gpu) { throw new Error( 'this browser has no WebGPU. Chrome and Edge 113+, Safari 26+, and ' + 'Firefox 141+ on Windows have it; on Linux Firefox and Chrome may need ' + 'it enabled explicitly.', ); } const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }); if (!adapter) throw new Error('WebGPU found no adapter on this machine.'); heldAdapter = adapter; const want = Math.min( MAX_STORAGE_BUFFERS, adapter.limits.maxStorageBuffersPerShaderStage ?? 8, ); return adapter.requestDevice({ requiredLimits: { maxStorageBuffersPerShaderStage: want }, }); } /** The adapter the current device came from, if there is one. */ export const currentAdapter = (): GPUAdapter | null => heldAdapter; /** Grid fields one kernel may read, given the device's binding limit: every * binding but the output and the parameter block. */ export const kernelOperandBudget = (device: GPUDevice): number => Math.max(2, (device.limits.maxStorageBuffersPerShaderStage ?? 8) - 2);