Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pnpm --filter examples dev
- `pnpm run lint` - `oxlint`, which also enforces the layer boundaries below.
- `pnpm run typecheck` - `tsc` over `src/`, the config files and, via `test/tsconfig.json`, the tests.
- `pnpm run format` - `prettier`.
- `pnpm run bench` - the performance benchmark suite (`@pmndrs/labs`).
- `pnpm run bench` - the performance benchmarks in [`benches/`](benches) (`@pmndrs/labs`).

## Layers

Expand Down
36 changes: 36 additions & 0 deletions benches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# klipp benches

Performance benchmarks powered by [`@pmndrs/labs`](https://github.com/pmndrs/labs). Benches import directly from `../src`, so no build step is needed.

## Usage

Run from the repo root:

```sh
pnpm bench # run all benches, save results with an automatic name
pnpm bench "@aim" # filter by tag (@body, @aim, @noise, @impulse, @controller, @core, @blend, @targets, @input, @scene)
pnpm bench -n "v1.0.0" -b # save with a name and set it as the baseline
pnpm bench compare # compare the latest run against the baseline
pnpm bench --no-save # run without saving
```

## Layout

The folders follow `src`:

- `core/`: one piece or core step at a time (`@body`, `@aim`, `@noise`, `@impulse`, `@controller`, `@core`, `@blend`)
- `three/`: target reads through the registry (`@targets`)
- `dom/`: input event handlers (`@input`)
- `scenes/`: whole frames of `KlippThree` with 1 to 50 cameras and a blend (`@scene`)

The scene benches take long enough per iteration to stay above the noise of a busy machine. Use them to spot a regression, then the smaller benches to find where it comes from.

Results are saved to `.labs/` (gitignored). A comparison only reports a change when it is statistically significant.

## Reading the output

`avg (min … max) p75 / p99` is the time per call. Compare it with a frame's budget, 16.67 ms at 60 fps or 8.33 ms at 120 fps, to see how many fit in one frame. Don't read allocations off the `heap` row for sub-microsecond calls: it overstates the bytes per call by orders of magnitude.

## Writing a bench

Code before `yield` is setup, and the yielded function is measured. Return a value from the measured function that depends on the work, so it isn't optimized away. Shared moving targets live in `targets.ts`.
80 changes: 80 additions & 0 deletions benches/core/aim.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { bench, group } from '@pmndrs/labs';
import { vec3 } from 'math';
import { Vector3 } from 'three';

import * as cameraState from '../../src/core/CameraState';

import { HardLookAtAimThree } from '../../src/three/aim/HardLookAtAimThree';
import { RotationComposerAimThree } from '../../src/three/aim/RotationComposerAimThree';

import { makeMovingTarget } from '../targets';

group('Aim.update @aim', () => {
bench('HardLookAt', function* () {
const { object, step } = makeMovingTarget();
const aim = new HardLookAtAimThree(object);
const out = cameraState.create();
vec3.set(out.position, 0, 2, 15);
yield () => {
step();
aim.update(out);
return out.quaternion[0];
};
});

bench('RotationComposer (deadZone + hardLimit)', function* () {
const { object, step } = makeMovingTarget();
const aim = new RotationComposerAimThree(object, {
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
});
const out = cameraState.create();
vec3.set(out.position, 0, 2, 15);
yield () => {
step();
aim.update(out, 0.016, false);
return out.quaternion[0];
};
});

bench('RotationComposer (deadZone + hardLimit + lookahead)', function* () {
const { object, step } = makeMovingTarget();
const aim = new RotationComposerAimThree(object, {
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
targetOffset: [0, 0, 0],
lookaheadTime: 0.3,
lookaheadSmoothing: 1,
});
const out = cameraState.create();
vec3.set(out.position, 0, 2, 15);
yield () => {
step();
aim.update(out, 0.016, false);
return out.quaternion[0];
};
});

// Where a live camera spends most frames: both dampers return early.
bench('RotationComposer (damped, converged on a still target)', function* () {
const aim = new RotationComposerAimThree(new Vector3(0, 2, -20), {
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0, 0],
damping: 0.5,
});
const out = cameraState.create();
vec3.set(out.position, 0, 2, 15);
aim.update(out, 0.016, true);
yield () => {
aim.update(out, 0.016, false);
return out.quaternion[0];
};
});
});
46 changes: 46 additions & 0 deletions benches/core/blend.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { bench, group } from '@pmndrs/labs';
import { vec3 } from 'math';
import { Matrix4, Quaternion, Vector3 } from 'three';

import * as cameraState from '../../src/core/CameraState';
import { BlendHints } from '../../src/core/blend/BlendHints';

/** Runs every frame of a blend, and never once it settles. */
group('cameraState.lerp @blend', () => {
function makeOrbitingState(position: Vector3, lookAtTarget: Vector3) {
const state = cameraState.create();
position.toArray(state.position);
new Quaternion()
.setFromRotationMatrix(new Matrix4().lookAt(position, lookAtTarget, new Vector3(0, 1, 0)))
.toArray(state.quaternion);
lookAtTarget.toArray(state.target);
state.hasTarget = true;
lookAtTarget.toArray(state.lookAtTarget);
state.hasLookAtTarget = true;
return state;
}

bench('plain slerp (no lookAtTarget)', function* () {
const a = cameraState.create();
vec3.set(a.position, 5, 5, 5);
const b = cameraState.create();
vec3.set(b.position, 0, 0, 5);
new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2).toArray(b.quaternion);
const out = cameraState.create();
yield () => cameraState.lerp(out, a, b, 0.5).position[0];
});

bench('lookAtTarget-driven rotation', function* () {
const a = makeOrbitingState(new Vector3(5, 5, 5), new Vector3(0, 0, 0));
const b = makeOrbitingState(new Vector3(0, 0, 5), new Vector3(0, 0, 0));
const out = cameraState.create();
yield () => cameraState.lerp(out, a, b, 0.5).position[0];
});

bench('lookAtTarget-driven rotation + sphericalPosition hint', function* () {
const a = makeOrbitingState(new Vector3(5, 5, 5), new Vector3(0, 0, 0));
const b = makeOrbitingState(new Vector3(0, 0, 5), new Vector3(0, 0, 0));
const out = cameraState.create();
yield () => cameraState.lerp(out, a, b, 0.5, BlendHints.sphericalPosition).position[0];
});
});
128 changes: 128 additions & 0 deletions benches/core/body.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { bench, group } from '@pmndrs/labs';

import * as cameraState from '../../src/core/CameraState';

import { FollowBodyThree } from '../../src/three/body/FollowBodyThree';
import { HardLockToTargetBodyThree } from '../../src/three/body/HardLockToTargetBodyThree';
import { PositionComposerBodyThree } from '../../src/three/body/PositionComposerBodyThree';

import { makeMovingMeshTarget, makeMovingTarget } from '../targets';

group('Body.update @body', () => {
bench('HardLockToTarget', function* () {
const { object, step } = makeMovingTarget();
const body = new HardLockToTargetBodyThree(object, { damping: 0.5 });
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});

bench('Follow (lockToTarget binding)', function* () {
const { object, step } = makeMovingTarget();
const body = new FollowBodyThree(object, { offset: [0, 3, 8], damping: 0.5 });
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});

bench('PositionComposer (deadZone + hardLimit)', function* () {
const { object, step } = makeMovingTarget();
const body = new PositionComposerBodyThree(object, {
cameraDistance: 10,
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
});
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});

bench('PositionComposer (deadZone + hardLimit + lookahead)', function* () {
const { object, step } = makeMovingTarget();
const body = new PositionComposerBodyThree(object, {
cameraDistance: 10,
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
depthDeadZone: 0,
lookaheadTime: 0.3,
lookaheadSmoothing: 1,
});
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});

bench('PositionComposer (radius extent)', function* () {
const { object, step } = makeMovingTarget();
const body = new PositionComposerBodyThree(object, {
cameraDistance: 10,
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
radius: 1.5,
});
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});

bench('PositionComposer (explicit size extent, rotating box)', function* () {
const { object, step } = makeMovingTarget();
const body = new PositionComposerBodyThree(object, {
cameraDistance: 10,
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
size: [2, 2, 2],
});
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});

bench('PositionComposer (auto-detected Mesh size extent, rotating box)', function* () {
const { object, step } = makeMovingMeshTarget();
const body = new PositionComposerBodyThree(object, {
cameraDistance: 10,
screenPosition: [0, 0],
aspect: 16 / 9,
deadZone: [0.2, 0.2],
damping: 0.5,
hardLimit: [0.4, 0.4],
});
const out = cameraState.create();
yield () => {
step();
body.update(out, 0.016, false);
return out.position[0];
};
});
});
Loading
Loading