Обчислювальні шейдери WebGPU у продакшені — фізика у 50 разів швидша

Ми перенесли симуляції рідин Латіса-Больцмана та SPH з JavaScript на обчислювальні шейдери WebGPU. Результат: у 50 разів швидше на настільних системах, 3D-сітки симуляцій, які раніше були абсолютно неможливі, та чистий поступовий відкат для браузерів без WebGPU. Ось усе, чого ми навчилися.

Чому саме обчислення 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:

WGSL — зіткнення LBM BGK (спрощено)
// схема прив'язки: група 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. Виявлення — це одна перевірка можливостей:

JavaScript — виявлення WebGPU та відкат
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 протягом усього кадру.