# @vgpu/render/perf

WebGPU verification helpers for the optimize loop. These utilities live under `@vgpu/render/perf`; they are not meant for runtime use.

---

# gpuFrameTime

Measures GPU frame time statistics for a render routine. The harness handles warmup, command encoding, submission, and timing (timestamp queries when available, wall clock otherwise).

## Import

```ts
import { gpuFrameTime } from "@vgpu/render/perf";
```

## Signature

```ts
export function gpuFrameTime(
  device: Device,
  encode: GpuFrameEncoder,
  options?: GpuFrameTimeOptions,
): Promise<GpuFrameTimeResult>;
```

## Parameters

| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
| device | Device | ✔ | — | Source device used to create command encoders, submit work, and flush the queue. Must expose `timestamp-query` if you want GPU timestamps. |
| encode | GpuFrameEncoder | ✔ | — | Callback invoked per frame with a fresh `GPUCommandEncoder`; record passes exactly as in production. |
| options | GpuFrameTimeOptions | ✖ | `{}` | Tunables for warmup, sample count, and measurement mode. |
| options.frames | number | ✖ | 120 | Measured frames after warmup. Values below 1 are clamped to 1. |
| options.warmup | number | ✖ | 30 | Throws away the first N frames so shader compilation and lazy allocations settle. |
| options.batch | number | ✖ | 8 | Frames per batch when falling back to wall-clock. Ignored for timestamp queries. |
| options.forceWallClock | boolean | ✖ | false | Forces the wall-clock path even if `timestamp-query` is available. Useful when you intentionally want queue-submit timings. |
| options.label | string | ✖ | "vgpu-gpuFrameTime" | Assigns encoder labels and buffer names to make debugging captures easier. |

**Returns:** `Promise<GpuFrameTimeResult>` — resolves with median, mean, min, p95, sample count, and the measurement `method` (`"timestamp-query"` or `"wall-clock"`).

**Throws:** Propagates any error from the encoder callback, queue submission, or timestamp buffer mapping. Timestamp-specific errors automatically fall back to wall-clock before surfacing.

## Examples

```ts
import { gpuFrameTime } from "@vgpu/render/perf";
import { createMockAdapter } from "@vgpu/adapter-mock";

async function bench(): Promise<void> {
  const adapter = createMockAdapter();
  const device = await adapter.requestDevice();
  const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [{ view: {} as GPUTextureView, loadOp: "clear", storeOp: "store" }] };
  const pipeline = device.gpu.createRenderPipeline({
    layout: "auto",
    vertex: { module: device.gpu.createShaderModule({ code: "@vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(); }" }), entryPoint: "vs_main" },
    fragment: { module: device.gpu.createShaderModule({ code: "@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1.0); }" }), entryPoint: "fs_main", targets: [{ format: "bgra8unorm" }] },
    primitive: { topology: "triangle-list" },
  });

  const result = await gpuFrameTime(device, (encoder) => {
    const pass = encoder.beginRenderPass(renderPassDescriptor);
    pass.setPipeline(pipeline);
    pass.draw(3);
    pass.end();
  }, { frames: 60, warmup: 10 });

  console.log(result.method, result.medianMs);
}

bench().catch((error) => {
  console.error(error);
});
```

## Notes

- Timestamp queries run only when the device exposes the `timestamp-query` feature; otherwise wall-clock timings use `device.queue.flush()` to amortize submit latency via batching.
- Warmup still submits commands; budget time for shader compilation before measuring.
- Always compare two runs with identical options; `medianMs` is the headline figure for relative regressions.
- **See also:** `pixelDiff`

---

# GpuFrameTimeOptions

Optional configuration passed to `gpuFrameTime`.

## Fields

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| frames | number | ✖ | 120 | Number of measured frames (post-warmup). Clamped to at least 1. |
| warmup | number | ✖ | 30 | Frames to submit and discard before measuring. |
| batch | number | ✖ | 8 | Wall-clock batch size; ignored for timestamp mode. |
| forceWallClock | boolean | ✖ | false | Skip timestamp queries even when supported. |
| label | string | ✖ | "vgpu-gpuFrameTime" | Label used for encoders and buffers created internally. |

---

# GpuFrameTimeResult

Return type for `gpuFrameTime`.

## Fields

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| medianMs | number | ✔ | — | 50th percentile frame time, the main number to compare across builds. |
| meanMs | number | ✔ | — | Arithmetic mean of collected samples. |
| minMs | number | ✔ | — | Fastest observed frame. |
| p95Ms | number | ✔ | — | 95th percentile; highlights long tails. |
| samples | number | ✔ | — | Count of valid samples captured. |
| method | "timestamp-query" \| "wall-clock" | ✔ | — | Measurement backend used. |

---

# pixelDiff

Compares two renders byte-for-byte. Accepts either `Texture` instances (will call `read()`) or already read `Uint8Array`s.

## Import

```ts
import { pixelDiff } from "@vgpu/render/perf";
```

## Signature

```ts
export function pixelDiff(
  a: Texture | Uint8Array,
  b: Texture | Uint8Array,
): Promise<PixelDiffResult>;
```

## Parameters

| Param | Type | Required | Default | Notes |
|---|---|---|---|---|
| a | Texture \| Uint8Array | ✔ | — | First render; when a `Texture`, it is read via `Texture.read()`. |
| b | Texture \| Uint8Array | ✔ | — | Second render to compare against `a`. |

**Returns:** `Promise<PixelDiffResult>` — contains `maxByte`, `meanByte`, `changedBytes`, `totalBytes`, and `changedFraction`.

**Throws:** Propagates any error while reading textures (e.g., unreadable usage flags) or when the inputs cannot be read.

## Examples

```ts
import { pixelDiff } from "@vgpu/render/perf";
import type { Texture } from "@vgpu/core";

async function assertVisualParity(before: Texture, after: Texture): Promise<void> {
  const result = await pixelDiff(before, after);
  if (result.maxByte > 2) {
    throw new Error(`Visual regression detected (max delta ${result.maxByte})`);
  }
}
```

## Notes

- A length mismatch forces `maxByte` to `255`; inspect `totalBytes` before trusting small `maxByte` values.
- Treat `maxByte <= 2` as driver rounding noise on most devices; higher values indicate true visual changes.
- When comparing multiple frames, feed already-read `Uint8Array`s to avoid repeated GPU readbacks.
- **See also:** `gpuFrameTime`

---

# PixelDiffResult

Shape returned by `pixelDiff`.

## Fields

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| maxByte | number | ✔ | — | Largest absolute per-byte difference (0–255). Use this as the headline regression metric. |
| meanByte | number | ✔ | — | Mean absolute per-byte difference across compared buffers. |
| changedBytes | number | ✔ | — | Count of bytes whose value differs at all. |
| totalBytes | number | ✔ | — | Number of bytes compared (min of both buffer lengths). |
| changedFraction | number | ✔ | — | `changedBytes / totalBytes`; near-zero fractions with low `maxByte` typically indicate harmless rounding noise. |
