# Best Practices

> **Scope**: How to write robust tests **before** they fail. For diagnosing and fixing failures, see [troubleshooting.md](./troubleshooting.md).

## Tolerance Selection

### Floating-Point Comparisons

Use `closeRel` for most floating-point comparisons.
Consider `closeAbs` for periodic functions near zero crossings.

### SwiftShader Considerations

SwiftShader (software renderer used in CI/containers) has reduced precision for transcendental functions:

```ts
import { it } from "vitest";
import { gpuFuzzTest } from "vitest-browser-three";
import { sin } from "three/tsl";

it("sin", async () => {
  // SwiftShader fast-math sin has ~1e-5 relative error
  await gpuFuzzTest("sin", {
    instances: 128,
    input: (i) => (i / 128) * Math.PI * 2,
    test: (x) => sin(x),
    expected: (x) => Math.sin(x),
    tolerance: 1e-3, // Use 1e-3 for trig functions (accounts for platform differences)
  });
});
```

**Why use 1e-3 instead of 1e-5?**

Tolerance must account for:

1. **Single operation error**: SwiftShader fast-math sin ≈ 1e-5 relative error
2. **Accumulated errors**: Multiple operations compound the error
3. **Platform variance**: Hardware GPUs may differ slightly from CPU reference

Start with 1e-3 for trig functions, or 1e-4 for simple arithmetic. If tests are flaky, relax the tolerance; if they pass consistently, you can try tightening it.

### Sampling Near Discontinuities

For functions with discontinuities (`fract`, `step`), use half-step sampling so inputs never land exactly on the boundary where f32 and f64 evaluations may disagree:

```ts
input: (i) => (i + 0.5) / instances; // instead of (i / n), instances = total test instances
```

This is especially important when the CPU reference computes in f64 while the GPU uses f32. See `docs/DECISIONS.md` for details.

### Integer Comparisons

Use `eq` for integers and exact values:

```ts
eq(float(2).add(3), float(5)); // Exact match
```

## Backend Selection

### WebGPU vs WebGL

| Feature         | WebGPU           | WebGL                                      |
| --------------- | ---------------- | ------------------------------------------ |
| Precision       | Full             | Limited                                    |
| Performance     | Hardware         | Hardware (may fall back to software in CI) |
| Storage buffers | Multi-read       | Single-read                                |
| Subgroups       | Via feature flag | Not supported                              |

### Default Behavior

Tests run against both backends by default. Use `backends` option to target specific backends:

```ts
import { it } from "vitest";
import { gpuTest } from "vitest-browser-three";
import { float } from "three/tsl";

it("webgpu only", async () => {
  // WebGPU only (required for some features)
  await gpuTest("webgpu only", ({ eq }) => eq(float(2).add(3), float(5)), {
    backends: ["webgpu"],
  });
});
```

## Verify Your Environment

Before writing many GPU tests, consider adding a fail-fast check to your
`beforeAll` so that environments without any working backend fail loudly
instead of silently skipping every test:

```ts
import { beforeAll } from "vitest";
import { isBackendAvailable } from "vitest-browser-three";

beforeAll(async () => {
  const webgpu = await isBackendAvailable("webgpu");
  const webgl = await isBackendAvailable("webgl");
  if (!webgpu && !webgl) {
    throw new Error("No GPU backend is available in this environment");
  }
});
```

If a shader fails to build, `gpuTest` / `gpuFuzzTest` will detect it
automatically (via an internal canary) and throw a clear error — you
do not need a separate pre-flight probe. For diagnostics, see
[troubleshooting.md](./troubleshooting.md).

## Memory Management

### Storage Buffer Constraints

On three.js r0.185, unused storage nodes can break buffer registration:

```ts
// BAD: unused buffer causes registration failure
const unused = storage(new Float32Array([0]), "float", 0);
const kernel = Fn(() => {
  // unused is never used in the kernel
})().compute(1);
```

```ts
// GOOD: only create buffers that are actually used
const buffer = storage(new Float32Array([0]), "float", 0);
const kernel = Fn(() => {
  buffer.element(uint(0)).assign(float(1));
})().compute(1);
```

## Matrix Assertions

### Workaround for TSL mat4 Transpose

TSL's `mat4(Matrix4)` is transposed relative to `Matrix4.elements`:

```ts
// Verify through transformation, not raw elements
const rot = mat4(new Matrix4().makeRotationZ(Math.PI / 2));
closeRel(rot.mul(vec4(1, 0, 0, 1)), vec4(0, 1, 0, 1), 1e-6);
```

### Matrix Assertion Stride

Each `mat4` assertion occupies 4 rows. Set `maxAssertions` accordingly:

```ts
import { it } from "vitest";
import { gpuTest } from "vitest-browser-three";
import { Matrix4 } from "three/webgpu";
import { mat4, vec4 } from "three/tsl";

it("matrices", async () => {
  await gpuTest(
    "matrices",
    ({ closeRel }) => {
      // 2 mat4 assertions = 8 rows needed
      const rot = mat4(new Matrix4().makeRotationZ(Math.PI / 2));
      closeRel(rot.mul(vec4(1, 0, 0, 1)), vec4(0, 1, 0, 1), 1e-6);
      closeRel(rot.mul(vec4(0, 1, 0, 1)), vec4(-1, 0, 0, 1), 1e-6);
    },
    { maxAssertions: 8 },
  );
});
```

## Test Organization

### Parallel Test Files

Vitest runs test files in separate workers, but a single worker may run
several files sequentially — and those files share module state. To
avoid cross-file coupling, pass `backends` per call (or define a
file-local wrapper, which is not shared with other files):

```ts
import { it } from "vitest";
import { gpuTest } from "vitest-browser-three";
import { float } from "three/tsl";

it("parallel test", async () => {
  await gpuTest("parallel test", ({ eq }) => eq(float(2).add(3), float(5)), {
    backends: ["webgpu"],
  });
});
```

A file-local wrapper keeps the call sites clean without any shared state:

```ts
const webgpuTest = (name, fn, opts = {}) =>
  gpuTest(name, fn, { ...opts, backends: opts.backends ?? ["webgpu"] });
```

### Cleanup

Import from the default entry for automatic cleanup:

```ts
import { gpuTest } from "vitest-browser-three"; // auto-disposes renderer
```

For side-effect-free imports:

```ts
import { afterAll } from "vitest";
import { disposeRenderer } from "vitest-browser-three/pure";

afterAll(async () => {
  await disposeRenderer();
});
```
