Чому саме обчислення WebGPU?
У WebGL є обчислювальні шейдери — умовно. Transform feedback та рендеринг у текстуру можна використати як GPGPU, але лише для векторних операцій, що вписуються в координату текстури. Обчислювальні конвеєри WebGPU — повноцінні: довільний доступ до даних, буфери зберігання з читанням-записом, атомарні операції та спільна пам'ять робочих груп. Це те, що CUDA та Metal Compute дали нативним застосункам; тепер це є й у браузері.
Обидві наші цільові симуляції — потік Латіса-Больцмана D2Q9 та рідина SPH — по-справжньому паралельні на рівні клітинки/частинки. Крок зіткнення відвідує кожну клітинку незалежно. Крок оцінки густини відвідує кожну пару частинок у межах радіуса. Це підручникові задачі GPGPU.
Бенчмарк: JavaScript проти WebGPU
| Симуляція | Сітка / кількість | JS (fps) | WebGPU (fps) | Прискорення |
|---|---|---|---|---|
| Латіс-Больцман 2D | 512 × 512 | 4 | 60 | 15× |
| LBM 2D високої роздільної здатності | 1024 × 1024 | <1 | 48 | ~50× |
| Латіс-Больцман 3D | 128³ (нове!) | неможливо | 24 | ∞ |
| Рідина SPH | 4 000 частинок | 18 | 60 | 3.3× |
| Рідина SPH | 16 000 частинок | 1 | 38 | 38× |
Обчислювальний шейдер WGSL
WGSL (WebGPU Shading Language) — мова шейдерів WebGPU. Вона ближча до Rust, ніж до GLSL — явні типи, жодних неявних перетворень, явні групи прив'язки. Ось основний крок зіткнення LBM:
// схема прив'язки: група 0, прив'язки 0-2
@group(0) @binding(0) var<storage, read> f_in : array<f32>;
@group(0) @binding(1) var<storage, read_write> f_out : array<f32>;
@group(0) @binding(2) var<uniform> params: Params;
struct Params { nx: u32, ny: u32, tau: f32 }
@compute @workgroup_size(16, 16)
fn lbm_collision(@builtin(global_invocation_id) id: vec3<u32>) {
let x = id.x; let y = id.y;
if (x >= params.nx || y >= params.ny) { return; }
let base = (y * params.nx + x) * 9u;
var rho = 0.0; var ux = 0.0; var uy = 0.0;
// Обчислюємо густину та швидкість з функцій розподілу
for (var i = 0u; i < 9u; i++) {
let fi = f_in[base + i];
rho += fi;
ux += fi * EX[i];
uy += fi * EY[i];
}
ux /= rho; uy /= rho;
// Зіткнення BGK: f_out = f_in - (f_in - f_eq) / tau
let usq = ux*ux + uy*uy;
for (var i = 0u; i < 9u; i++) {
let eu = EX[i]*ux + EY[i]*uy;
let feq = rho * W[i] * (1.0 + 3.0*eu + 4.5*eu*eu - 1.5*usq);
f_out[base + i] = f_in[base + i] - (f_in[base + i] - feq) / params.tau;
}
}
Стратегія відкату
Наприкінці 2026 року підтримка WebGPU становить приблизно 65% браузерів (Chrome 113+, Edge, Safari Technology Preview, Firefox за прапорцем). Для решти 35% ми відкочуємось до версії на JavaScript. Виявлення — це одна перевірка можливостей:
async function initSimulation() {
if ('gpu' in navigator) {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
const device = await adapter.requestDevice();
return new LBMSimulatorWebGPU(device); // швидкий шлях
}
}
return new LBMSimulatorJS(); // відкат — той самий API, реалізація на JS
}
Ключова ідея: обидві реалізації — WebGPU та
JavaScript — надають рівно той самий інтерфейс
step() та getVelocityField(). Код
рендерингу не знає, з яким бекендом він розмовляє. Це означає,
що відкат повністю прозорий для користувача — він просто бачить
повільнішу симуляцію в непідтримуваних браузерах.
Підводні камені WebGPU, на які ми натрапили
⚠️ Вирівнювання буфера
Буфери зберігання WGSL мають бути вирівняні по 256 байтах. Наш буфер LBM із 9 float на клітинку потребував явного доповнення, щоб уникнути тихого пошкодження даних на деяких GPU.
⚠️ Немає динамічної індексації в uniform-буферах
Константи ваг та напрямків D2Q9 не можна розмістити в uniform-буфері з динамічним індексом. Ми вбудували їх як константи WGSL (масиви, обчислювані на етапі компіляції).
✅ Налаштування розміру робочої групи
Робочі групи 16×16 показали кращі результати, ніж 8×8 та 32×32, на більшості протестованого обладнання. AMD та Apple Silicon віддавали перевагу 8×8. Ми виносимо це як пресет якості.
✅ Потокова передача з обчислень у рендеринг
Буфер поля швидкості прив'язаний одночасно і як буфер зберігання для обчислень, і як вершинний буфер WebGPU. Зчитування на CPU не потрібне — дані живуть на GPU протягом усього кадру.