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:
// 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:
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ę.