Shadery obliczeniowe WebGPU w produkcji — 50× szybsza fizyka

Przenieśliśmy symulacje płynów Lattice-Boltzmann i SPH z JavaScriptu na shadery obliczeniowe WebGPU. Wynik: 50× szybciej na desktopie, siatki symulacji 3D, które wcześniej były całkowicie niemożliwe, oraz czysty progresywny fallback dla przeglądarek bez WebGPU. Oto wszystko, czego się nauczyliśmy.

Dlaczego obliczenia WebGPU?

WebGL ma shadery obliczeniowe — w pewnym sensie. Transform feedback i render-to-texture można wykorzystać jako GPGPU, ale tylko dla operacji wektorowych mieszczących się we współrzędnych tekstury. Potoki obliczeniowe WebGPU są pełnoprawne: dowolny dostęp do danych, bufory magazynowe do odczytu i zapisu, operacje atomowe oraz współdzielona pamięć grupy roboczej. To jest to, co CUDA i Metal Compute dały aplikacjom natywnym; teraz jest to w przeglądarce.

Nasze dwie docelowe symulacje — przepływ Lattice-Boltzmann D2Q9 i płyn SPH — są obie zawstydzająco równoległe na poziomie komórki/cząsteczki. Krok kolizji odwiedza każdą komórkę niezależnie. Krok estymacji gęstości odwiedza każdą parę cząsteczek w promieniu. To podręcznikowe obciążenia GPGPU.

Benchmark: JavaScript kontra WebGPU

Symulacja Siatka / liczba JS (fps) WebGPU (fps) Przyspieszenie
Lattice-Boltzmann 2D 512 × 512 4 60 15×
LBM 2D wysoka rozdzielczość 1024 × 1024 <1 48 ~50×
Lattice-Boltzmann 3D 128³ (nowość!) niemożliwe 24
Płyn SPH 4 000 cząsteczek 18 60 3,3×
Płyn SPH 16 000 cząsteczek 1 38 38×

Shader obliczeniowy WGSL

WGSL (WebGPU Shading Language) to język shaderów WebGPU. Jest bliższy Rustowi niż GLSL — jawne typy, brak niejawnych rzutowań, jawne grupy wiązania. Oto rdzeń kroku kolizji LBM:

WGSL — kolizja BGK LBM (uproszczona)
// układ wiązań: grupa 0, powiązania 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;

  // Oblicz gęstość i prędkość z funkcji rozkładu
  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;

  // Kolizja 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;
  }
}

Strategia fallbacku

WebGPU ma ~65% wsparcia przeglądarek pod koniec 2026 roku (Chrome 113+, Edge, Safari Technology Preview, Firefox za flagą). Dla pozostałych 35% wracamy do wersji JavaScript. Wykrywanie to pojedynczy test funkcji:

JavaScript — wykrywanie WebGPU + fallback
async function initSimulation() {
  if ('gpu' in navigator) {
    const adapter = await navigator.gpu.requestAdapter();
    if (adapter) {
      const device = await adapter.requestDevice();
      return new LBMSimulatorWebGPU(device);   // szybka ścieżka
    }
  }
  return new LBMSimulatorJS();  // fallback — ten sam API, implementacja JS
}

Kluczowy wniosek: zarówno implementacja WebGPU, jak i JavaScript udostępniają dokładnie ten sam interfejs step() i getVelocityField(). Kod renderujący nie wie, z którym zapleczem rozmawia. Oznacza to, że fallback jest całkowicie przezroczysty dla użytkownika — po prostu widzą wolniejszą symulację w nieobsługiwanych przeglądarkach.

Pułapki WebGPU, na które trafiliśmy

⚠️ Wyrównanie buforów

Bufory magazynowe WGSL muszą być wyrównane do 256 bajtów. Nasz bufor LBM z 9 liczbami zmiennoprzecinkowymi na komórkę wymagał jawnego dopełnienia, aby uniknąć cichej korupcji danych na niektórych GPU.

⚠️ Brak dynamicznego indeksowania w uniformach

Stałe wag i kierunków D2Q9 nie mogą znajdować się w buforze uniform z dynamicznym indeksem. Osadziliśmy je jako stałe WGSL (tablice czasu kompilacji).

✅ Dostrajanie rozmiaru grupy roboczej

Grupy robocze 16×16 przewyższały 8×8 i 32×32 na większości testowanego sprzętu. AMD i Apple Silicon preferowały 8×8. Udostępniamy to jako preset jakości.

✅ Strumieniowanie z obliczeń do renderowania

Bufor pola prędkości jest powiązany zarówno jako bufor magazynowy obliczeń, jak i bufor wierzchołków WebGPU. Nie jest potrzebny odczyt zwrotny z CPU — dane pozostają na GPU przez całą klatkę.