# WebGPU in Orbium

> How the engine talks to the GPU. The device and the frame's encoder, uniform blocks, WGSL modules, compute kernels and materials.

WebGPU gives a web page direct access to the graphics card: buffers, textures,
compute and render pipelines, and shaders written in **WGSL**. The engine wraps
it in a few small pieces, each in `engine/gpu/` or
`engine/render/`.

## The device and the frame

GPU.js (`engine/gpu/GPU.js`) holds the device, its queue, the shared
samplers and **one command encoder a frame**. Every system records its compute
dispatches and render passes into that encoder, in call order, and the frame
ends with a single submit:

```js
await GPU.init({ canvas });
// each frame
GPU.beginFrame();
ocean.update(dt); // records compute passes into the frame's encoder
sceneRenderer.render(); // records render passes
GPU.submit(); // one submit for the whole frame
```

One subtlety shapes much of the engine: `queue.writeBuffer` calls land before
the frame's commands run. A buffer written twice in one frame keeps only the
last value for every pass, so anything that changes between passes needs its own
buffer, or its own offset.

## Uniform blocks

A UniformBlock (`engine/gpu/Uniforms.js`) is a WGSL struct, its CPU mirror
and its GPU buffer, declared once:

```js
const U = new UniformBlock("OceanParams", {
  choppiness: ["f32", 0.9],
  sizes: ["vec4f", [733, 157, 33.3, 7.1]],
  viewProj: "mat4x4f",
});
U.values.choppiness = 1.2; // uploaded on the next bind, only if it changed
```

The block lays the struct out by WGSL's alignment rules and writes the
declaration for the shader, so the CPU and the GPU never disagree on an offset.

## WGSL modules

The engine composes shaders from **modules**: a piece of WGSL (functions,
structs, constants) together with the resources it reads
(Shader.js (`engine/gpu/Shader.js`)). The terrain, for example, exports a
module that defines `terrainHeightAt` and carries its height map. Any shader
that lists it, directly or through another module, gets its code once and its
bindings declared. Simplified, from
TerrainGPU.js (`engine/terrain/TerrainGPU.js`):

```js
export const terrainModule = new ShaderModule({
  name: "terrain",
  deps: [commonModule],
  uniforms: terrainUniforms,
  bindings: {
    terrainHeightTex: { texture: () => heightTex },
  },
  code: /* wgsl */ `
    fn terrainHeightAt(xz: vec2f) -> f32 {
      let uv = (xz - terrainParams.origin) / terrainParams.size;
      return textureSampleLevel(terrainHeightTex, smpLinearClamp, uv, 0.0).r;
    }`,
});
```

Bind groups follow one convention everywhere:

| Group | Holds                                                          |
| ----- | -------------------------------------------------------------- |
| 0     | The frame: camera, time, sun, and the shared samplers          |
| 1     | The modules' and the material's resources, composed per shader |
| 2     | Per-draw data (render pipelines only)                          |

Modules can use a small preprocessor (`#if`, `#ifdef`, `#else`, `#endif`): the
graphics presets switch features with defines instead of separate shaders.

## Compute kernels

A ComputeKernel (`engine/gpu/Compute.js`) is a compute pipeline built from
modules and a `main` body. The ocean's FFT, the surf simulation, particles and
the terrain's selection all run this way:

```js
const k = new ComputeKernel({
  label: "fft rows",
  modules: [fftModule],
  bindings: { waveData: { storage: waveBuf, access: "read_write" } },
  workgroupSize: [256, 1, 1],
  code: /* wgsl */ `
    @compute @workgroup_size(WG_X, WG_Y, WG_Z)
    fn main(@builtin(global_invocation_id) gid: vec3u) { /* ... */ }`,
});
k.dispatch(Math.ceil(n / 256)); // recorded into the frame's encoder
```

## Materials

A Material (`engine/render/Material.js`) is a set of WGSL snippets plugged
into one mesh shader template
(MeshShader.js (`engine/render/MeshShader.js`)): a `vertex` snippet that can
move vertices and a `surface` snippet that sets albedo, roughness and normals.
The template adds the lighting, shadows, the light under the water and the
outputs every pass needs (colour, motion vectors for the temporal upscaler, the
water mask):

```js
const rock = new Material({
  name: "rock",
  modules: [terrainModule],
  uniforms: { tint: ["vec3f", new Color(0.5, 0.45, 0.4)] },
  surface: `s.albedo = mat.tint; s.roughness = 0.85;`,
});
```

The mesh renderer (`engine/render/MeshRenderer.js`) caches a pipeline per
material and pass, culls and sorts the draws, and records them.

## Keeping the GPU healthy

What the game builds during play is disposed when it is done with, and a test
checks that GPU memory stays flat over rounds
(gpu-leaks.test.mjs (`test/gpu-leaks.test.mjs`)). A lost or failing device
stops the game loop and says so, instead of feeding a dead GPU.
