diff --git a/.gitignore b/.gitignore index 4c5ee2a..e95c862 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ website/dist *.tsbuildinfo tmp .DS_Store + +# labs bench results +.labs diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 0000000..3de1928 --- /dev/null +++ b/benches/README.md @@ -0,0 +1,50 @@ +# maath benches + +Performance benchmarks powered by [`@pmndrs/labs`](https://github.com/pmndrs/labs) — statistically rigorous benchmarking (Mann-Whitney U, Cliff's delta, adaptive sampling) with baseline comparison. + +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 auto timestamp +pnpm bench "vec3" # filter by file/bench name +pnpm bench "@core" # filter by tag (@core, @noise, @algo, ...) +pnpm bench -n "v1.0.0" -b # save with a name and set as baseline +pnpm bench compare # compare latest run against the baseline +pnpm bench run # run without saving +``` + +## Layout + +- `core/`, `noise/` — micro benches of individual functions in tight loops (`@core`, `@noise`) +- `algorithms/` — composite benches (`@algo`) that implement a complete minimal feature from + navigation/collision-style libraries, exercising many maath functions together: funnel string + pulling (`@nav`), frustum culling (`@culling`), closest-hit raycasting (`@raycast`), transform + hierarchy propagation (`@scene`), and a full sphere physics step (`@physics`) + +The composite benches run 100+ µs per iteration, which keeps them well above this machine's +micro-bench noise floor — prefer them for regression comparisons; use the micro benches to +localize a regression once one shows up. + +Results are saved to `.labs/` (gitignored). Comparisons only report a change when it is statistically significant and the effect size is meaningful — see the labs README for details. + +## Writing a bench + +Benches use a generator: code before `yield` is setup, the yielded function is measured, code after is teardown. Chain `.gc('inner')` to force GC between samples. + +```ts +import { bench, group } from '@pmndrs/labs'; + +group('my group @mytag', () => { + bench('my bench', function* () { + // setup + yield () => { + // measured + }; + // teardown + }).gc('inner'); +}); +``` diff --git a/benches/algorithms/frustum-culling.bench.ts b/benches/algorithms/frustum-culling.bench.ts new file mode 100644 index 0000000..60818d5 --- /dev/null +++ b/benches/algorithms/frustum-culling.bench.ts @@ -0,0 +1,124 @@ +import { bench, group } from '@pmndrs/labs'; +import * as mat4 from '../../src/core/mat4'; +import * as vec3 from '../../src/core/vec3'; +import type { Vec3 } from '../../src/core/vec3'; +import * as mulberry32 from '../../src/random/mulberry32'; +import * as box3 from '../../src/shapes/box3'; +import type { Box3 } from '../../src/shapes/box3'; +import * as plane3 from '../../src/shapes/plane3'; +import type { Plane3 } from '../../src/shapes/plane3'; +import type { Sphere } from '../../src/shapes/sphere'; + +// Camera frustum culling — build view + projection matrices, extract the six +// frustum planes (Gribb-Hartmann), then cull a field of bounding volumes. + +const N = 4096; + +const view = mat4.create(); +const proj = mat4.create(); +const viewProj = mat4.create(); +const planeNormal = vec3.create(); + +function buildFrustum(out: Plane3[]): void { + mat4.lookAt(view, [30, 30, 30], [0, 0, 0], [0, 1, 0]); + mat4.perspectiveNO(proj, Math.PI / 3, 16 / 9, 0.1, 100); + mat4.multiply(viewProj, proj, view); + + // extract the six frustum planes (Gribb-Hartmann) from the column-major + // view-projection matrix; rows: row3 + row_i (left/bottom/near), row3 - row_i (right/top/far) + const m = viewProj; + for (let i = 0; i < 3; i++) { + vec3.set(planeNormal, m[3] + m[i], m[7] + m[4 + i], m[11] + m[8 + i]); + plane3.fromNormalAndConstant(out[i * 2], planeNormal, m[15] + m[12 + i]); + plane3.normalize(out[i * 2], out[i * 2]); + + vec3.set(planeNormal, m[3] - m[i], m[7] - m[4 + i], m[11] - m[8 + i]); + plane3.fromNormalAndConstant(out[i * 2 + 1], planeNormal, m[15] - m[12 + i]); + plane3.normalize(out[i * 2 + 1], out[i * 2 + 1]); + } +} + +function makePlanes(): Plane3[] { + const planes: Plane3[] = []; + for (let i = 0; i < 6; i++) planes.push(plane3.create()); + return planes; +} + +function randPos(rand: ReturnType): Vec3 { + return [ + (mulberry32.sample(rand) - 0.5) * 80, + (mulberry32.sample(rand) - 0.5) * 80, + (mulberry32.sample(rand) - 0.5) * 80, + ]; +} + +let sink = 0; + +group('frustum culling 4096 @algo @culling', () => { + bench('spheres', function* () { + const rand = mulberry32.create(42); + const spheres: Sphere[] = []; + for (let i = 0; i < N; i++) { + spheres.push({ center: randPos(rand), radius: 0.5 + mulberry32.sample(rand) * 1.5 }); + } + const planes = makePlanes(); + + yield () => { + buildFrustum(planes); + let visible = 0; + for (let i = 0; i < N; i++) { + const sphere = spheres[i]; + let inside = true; + for (let p = 0; p < 6; p++) { + if (plane3.distanceToPoint(planes[p], sphere.center) < -sphere.radius) { + inside = false; + break; + } + } + if (inside) visible++; + } + sink = visible; + }; + }).gc('inner'); + + bench('aabbs', function* () { + const rand = mulberry32.create(42); + const boxes: Box3[] = []; + for (let i = 0; i < N; i++) { + const b = box3.create(); + box3.setFromCenterAndSize(b, randPos(rand), [ + 1 + mulberry32.sample(rand) * 3, + 1 + mulberry32.sample(rand) * 3, + 1 + mulberry32.sample(rand) * 3, + ]); + boxes.push(b); + } + const planes = makePlanes(); + const center = vec3.create(); + const extents = vec3.create(); + + yield () => { + buildFrustum(planes); + let visible = 0; + for (let i = 0; i < N; i++) { + const b = boxes[i]; + box3.center(center, b); + box3.extents(extents, b); + let inside = true; + for (let p = 0; p < 6; p++) { + const n = planes[p].normal; + const effectiveRadius = + extents[0] * Math.abs(n[0]) + extents[1] * Math.abs(n[1]) + extents[2] * Math.abs(n[2]); + if (plane3.distanceToPoint(planes[p], center) < -effectiveRadius) { + inside = false; + break; + } + } + if (inside) visible++; + } + sink = visible; + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/algorithms/funnel-path.bench.ts b/benches/algorithms/funnel-path.bench.ts new file mode 100644 index 0000000..b36fdf0 --- /dev/null +++ b/benches/algorithms/funnel-path.bench.ts @@ -0,0 +1,138 @@ +import { bench, group } from '@pmndrs/labs'; +import * as vec2 from '../../src/core/vec2'; +import type { Vec2 } from '../../src/core/vec2'; +import type { Vec3 } from '../../src/core/vec3'; +import * as mulberry32 from '../../src/random/mulberry32'; + +// Simple stupid funnel (string pulling) over navmesh portal edges — the path +// smoothing step of a navigation pipeline. Composes vec2 subtract/cross/copy/ +// exactEquals/distance. + +const CORRIDORS = 16; +const PORTALS = 256; // per corridor, plus degenerate start/end portals + +type Corridor = { left: Vec2[]; right: Vec2[]; count: number }; + +const edgeAB = vec2.create(); +const edgeAC = vec2.create(); +const crossOut: Vec3 = [0, 0, 0]; + +// twice the signed area of triangle (a, b, c); negative when c is left of ab +function triarea2(a: Vec2, b: Vec2, c: Vec2): number { + vec2.subtract(edgeAB, b, a); + vec2.subtract(edgeAC, c, a); + vec2.cross(crossOut, edgeAC, edgeAB); + return crossOut[2]; +} + +const portalApex = vec2.create(); +const portalLeft = vec2.create(); +const portalRight = vec2.create(); + +function stringPull(left: Vec2[], right: Vec2[], count: number, outCorners: Vec2[]): number { + let n = 0; + vec2.copy(portalApex, left[0]); + vec2.copy(portalLeft, left[0]); + vec2.copy(portalRight, right[0]); + let apexIndex = 0; + let leftIndex = 0; + let rightIndex = 0; + vec2.copy(outCorners[n++], portalApex); + + for (let i = 1; i < count; i++) { + const pl = left[i]; + const pr = right[i]; + + // update right vertex + if (triarea2(portalApex, portalRight, pr) <= 0) { + if (vec2.exactEquals(portalApex, portalRight) || triarea2(portalApex, portalLeft, pr) > 0) { + vec2.copy(portalRight, pr); + rightIndex = i; + } else { + // right crossed over left: left becomes the new apex + vec2.copy(outCorners[n++], portalLeft); + vec2.copy(portalApex, portalLeft); + apexIndex = leftIndex; + vec2.copy(portalLeft, portalApex); + vec2.copy(portalRight, portalApex); + leftIndex = apexIndex; + rightIndex = apexIndex; + i = apexIndex; + continue; + } + } + + // update left vertex + if (triarea2(portalApex, portalLeft, pl) >= 0) { + if (vec2.exactEquals(portalApex, portalLeft) || triarea2(portalApex, portalRight, pl) < 0) { + vec2.copy(portalLeft, pl); + leftIndex = i; + } else { + // left crossed over right: right becomes the new apex + vec2.copy(outCorners[n++], portalRight); + vec2.copy(portalApex, portalRight); + apexIndex = rightIndex; + vec2.copy(portalLeft, portalApex); + vec2.copy(portalRight, portalApex); + leftIndex = apexIndex; + rightIndex = apexIndex; + i = apexIndex; + continue; + } + } + } + + vec2.copy(outCorners[n++], left[count - 1]); + return n; +} + +let sink = 0; + +group('funnel string pull 16x256 @algo @nav', () => { + bench('string pull + path length', function* () { + const rand = mulberry32.create(42); + const corridors: Corridor[] = []; + for (let c = 0; c < CORRIDORS; c++) { + const left: Vec2[] = []; + const right: Vec2[] = []; + let centerX = 0; + // degenerate start portal + left.push([0, 0]); + right.push([0, 0]); + for (let i = 0; i < PORTALS; i++) { + const y = i + 1; + centerX += (mulberry32.sample(rand) - 0.5) * 1.5; + const halfWidth = 0.5 + mulberry32.sample(rand); + left.push([centerX - halfWidth, y]); + right.push([centerX + halfWidth, y]); + } + // degenerate end portal + left.push([centerX, PORTALS + 1]); + right.push([centerX, PORTALS + 1]); + corridors.push({ left, right, count: left.length }); + } + + const corners: Vec2[] = []; + for (let i = 0; i < PORTALS + 4; i++) corners.push(vec2.create()); + + // sanity: every corridor must pull to a valid multi-corner path + for (const c of corridors) { + const n = stringPull(c.left, c.right, c.count, corners); + if (n < 2 || !vec2.finite(corners[n - 1])) throw new Error('funnel produced a degenerate path'); + } + + yield () => { + let totalLength = 0; + for (let c = 0; c < corridors.length; c++) { + const corridor = corridors[c]; + const n = stringPull(corridor.left, corridor.right, corridor.count, corners); + for (let i = 1; i < n; i++) { + totalLength += vec2.distance(corners[i - 1], corners[i]); + } + } + sink = totalLength; + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/algorithms/physics-step.bench.ts b/benches/algorithms/physics-step.bench.ts new file mode 100644 index 0000000..2bae2f4 --- /dev/null +++ b/benches/algorithms/physics-step.bench.ts @@ -0,0 +1,130 @@ +import { bench, group } from '@pmndrs/labs'; +import * as vec3 from '../../src/core/vec3'; +import type { Vec3 } from '../../src/core/vec3'; +import * as mulberry32 from '../../src/random/mulberry32'; +import * as plane3 from '../../src/shapes/plane3'; +import type { Plane3 } from '../../src/shapes/plane3'; + +// One full step of a minimal sphere physics world: integrate gravity and +// velocities, bounce off six arena wall planes, then resolve all pairwise +// sphere-sphere contacts with equal-mass impulses. State is restored between +// samples so every sample simulates the identical step. + +const N = 512; +const RADIUS = 0.5; +const ARENA = 15; +const DT = 1 / 60; +const RESTITUTION = 0.6; + +const GRAVITY: Vec3 = [0, -9.81, 0]; + +let sink = 0; + +group('sphere physics step 512 @algo @physics', () => { + bench('integrate + walls + pair resolve', function* () { + const rand = mulberry32.create(42); + + const positions: Vec3[] = []; + const velocities: Vec3[] = []; + const initialPositions: Vec3[] = []; + const initialVelocities: Vec3[] = []; + for (let i = 0; i < N; i++) { + const p: Vec3 = [ + (mulberry32.sample(rand) - 0.5) * 2 * (ARENA - RADIUS), + (mulberry32.sample(rand) - 0.5) * 2 * (ARENA - RADIUS), + (mulberry32.sample(rand) - 0.5) * 2 * (ARENA - RADIUS), + ]; + const v: Vec3 = [ + (mulberry32.sample(rand) - 0.5) * 10, + (mulberry32.sample(rand) - 0.5) * 10, + (mulberry32.sample(rand) - 0.5) * 10, + ]; + positions.push(vec3.clone(p)); + velocities.push(vec3.clone(v)); + initialPositions.push(p); + initialVelocities.push(v); + } + + const walls: Plane3[] = []; + for (let axis = 0; axis < 3; axis++) { + for (const sign of [1, -1]) { + const normal = vec3.create(); + normal[axis] = sign; + const wall = plane3.create(); + plane3.fromNormalAndConstant(wall, normal, ARENA); + walls.push(wall); + } + } + + const contactNormal = vec3.create(); + const relativeVelocity = vec3.create(); + + yield { + bench: () => { + // integrate + for (let i = 0; i < N; i++) { + vec3.scaleAndAdd(velocities[i], velocities[i], GRAVITY, DT); + vec3.scaleAndAdd(positions[i], positions[i], velocities[i], DT); + } + + // wall contacts + for (let i = 0; i < N; i++) { + const p = positions[i]; + const v = velocities[i]; + for (let w = 0; w < 6; w++) { + const wall = walls[w]; + const distance = plane3.distanceToPoint(wall, p); + if (distance < RADIUS) { + vec3.scaleAndAdd(p, p, wall.normal, RADIUS - distance); + const speedIntoWall = vec3.dot(v, wall.normal); + if (speedIntoWall < 0) { + vec3.scaleAndAdd(v, v, wall.normal, -(1 + RESTITUTION) * speedIntoWall); + } + } + } + } + + // pairwise sphere-sphere contacts + const contactDistanceSq = (RADIUS * 2) * (RADIUS * 2); + for (let i = 0; i < N; i++) { + for (let j = i + 1; j < N; j++) { + if (vec3.squaredDistance(positions[i], positions[j]) >= contactDistanceSq) continue; + + vec3.subtract(contactNormal, positions[j], positions[i]); + const distance = vec3.length(contactNormal); + if (distance === 0) continue; + vec3.scale(contactNormal, contactNormal, 1 / distance); + + // separate positions equally + const overlap = RADIUS * 2 - distance; + vec3.scaleAndAdd(positions[i], positions[i], contactNormal, -overlap / 2); + vec3.scaleAndAdd(positions[j], positions[j], contactNormal, overlap / 2); + + // equal-mass impulse along the contact normal + vec3.subtract(relativeVelocity, velocities[j], velocities[i]); + const approachSpeed = vec3.dot(relativeVelocity, contactNormal); + if (approachSpeed < 0) { + const impulse = (-(1 + RESTITUTION) * approachSpeed) / 2; + vec3.scaleAndAdd(velocities[i], velocities[i], contactNormal, -impulse); + vec3.scaleAndAdd(velocities[j], velocities[j], contactNormal, impulse); + } + } + } + + let energy = 0; + for (let i = 0; i < N; i++) { + energy += vec3.squaredLength(velocities[i]); + } + sink = energy; + }, + after: () => { + for (let i = 0; i < N; i++) { + vec3.copy(positions[i], initialPositions[i]); + vec3.copy(velocities[i], initialVelocities[i]); + } + }, + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/algorithms/raycast-scene.bench.ts b/benches/algorithms/raycast-scene.bench.ts new file mode 100644 index 0000000..350fade --- /dev/null +++ b/benches/algorithms/raycast-scene.bench.ts @@ -0,0 +1,91 @@ +import { bench, group } from '@pmndrs/labs'; +import * as vec3 from '../../src/core/vec3'; +import type { Vec3 } from '../../src/core/vec3'; +import * as mulberry32 from '../../src/random/mulberry32'; +import * as box3 from '../../src/shapes/box3'; +import type { Box3 } from '../../src/shapes/box3'; +import * as raycast3 from '../../src/shapes/raycast3'; + +// Closest-hit raycasting against a triangle soup with a per-triangle AABB +// broadphase — the query kernel of a physics or picking system. + +const TRIANGLES = 1024; +const RAYS = 64; +const RAY_LENGTH = 60; + +let sink = 0; + +group('raycast closest-hit 64x1024 @algo @raycast', () => { + bench('rays vs triangle soup', function* () { + const rand = mulberry32.create(42); + + const triA: Vec3[] = []; + const triB: Vec3[] = []; + const triC: Vec3[] = []; + const aabbs: Box3[] = []; + for (let i = 0; i < TRIANGLES; i++) { + const cx = (mulberry32.sample(rand) - 0.5) * 30; + const cy = (mulberry32.sample(rand) - 0.5) * 30; + const cz = (mulberry32.sample(rand) - 0.5) * 30; + const a: Vec3 = [cx + (mulberry32.sample(rand) - 0.5) * 4, cy + (mulberry32.sample(rand) - 0.5) * 4, cz + (mulberry32.sample(rand) - 0.5) * 4]; + const b: Vec3 = [cx + (mulberry32.sample(rand) - 0.5) * 4, cy + (mulberry32.sample(rand) - 0.5) * 4, cz + (mulberry32.sample(rand) - 0.5) * 4]; + const c: Vec3 = [cx + (mulberry32.sample(rand) - 0.5) * 4, cy + (mulberry32.sample(rand) - 0.5) * 4, cz + (mulberry32.sample(rand) - 0.5) * 4]; + triA.push(a); + triB.push(b); + triC.push(c); + + const aabb = box3.create(); + box3.empty(aabb); + box3.expandByPoint(aabb, aabb, a); + box3.expandByPoint(aabb, aabb, b); + box3.expandByPoint(aabb, aabb, c); + aabbs.push(aabb); + } + + const origins: Vec3[] = []; + const directions: Vec3[] = []; + for (let i = 0; i < RAYS; i++) { + const theta = (i / RAYS) * Math.PI * 2; + const origin: Vec3 = [Math.cos(theta) * 25, (mulberry32.sample(rand) - 0.5) * 10, Math.sin(theta) * 25]; + const target: Vec3 = [ + (mulberry32.sample(rand) - 0.5) * 20, + (mulberry32.sample(rand) - 0.5) * 20, + (mulberry32.sample(rand) - 0.5) * 20, + ]; + const direction = vec3.create(); + vec3.subtract(direction, target, origin); + vec3.normalize(direction, direction); + origins.push(origin); + directions.push(direction); + } + + const result = raycast3.createIntersectsTriangleResult(); + const hitPoint = vec3.create(); + + yield () => { + let fractionSum = 0; + let hits = 0; + for (let r = 0; r < RAYS; r++) { + const origin = origins[r]; + const direction = directions[r]; + let bestFraction = Infinity; + for (let t = 0; t < TRIANGLES; t++) { + if (!raycast3.intersectsBox3(origin, direction, RAY_LENGTH, aabbs[t])) continue; + raycast3.intersectsTriangle(result, origin, direction, RAY_LENGTH, triA[t], triB[t], triC[t], false); + if (result.hit && result.fraction < bestFraction) { + bestFraction = result.fraction; + } + } + if (bestFraction < Infinity) { + hits++; + fractionSum += bestFraction; + vec3.scaleAndAdd(hitPoint, origin, direction, bestFraction * RAY_LENGTH); + } + } + + sink = fractionSum + hits; + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/algorithms/transform-hierarchy.bench.ts b/benches/algorithms/transform-hierarchy.bench.ts new file mode 100644 index 0000000..2f13803 --- /dev/null +++ b/benches/algorithms/transform-hierarchy.bench.ts @@ -0,0 +1,70 @@ +import { bench, group } from '@pmndrs/labs'; +import * as mat4 from '../../src/core/mat4'; +import type { Mat4 } from '../../src/core/mat4'; +import * as quat from '../../src/core/quat'; +import type { Quat } from '../../src/core/quat'; +import type { Vec3 } from '../../src/core/vec3'; +import * as mulberry32 from '../../src/random/mulberry32'; +import * as box3 from '../../src/shapes/box3'; + +// Scene graph update — compose each node's local TRS matrix, propagate world +// matrices down a 4-ary tree, then accumulate world-space scene bounds. + +const N = 4096; + +let sink = 0; + +group('transform hierarchy 4096 @algo @scene', () => { + bench('world matrices + scene bounds', function* () { + const rand = mulberry32.create(42); + const axis: Vec3 = [0.267261, 0.534522, 0.801784]; + const unitScale: Vec3 = [1, 1, 1]; + + const positions: Vec3[] = []; + const rotations: Quat[] = []; + const localMats: Mat4[] = []; + const worldMats: Mat4[] = []; + + for (let i = 0; i < N; i++) { + positions.push([ + (mulberry32.sample(rand) - 0.5) * 4, + (mulberry32.sample(rand) - 0.5) * 4, + (mulberry32.sample(rand) - 0.5) * 4, + ]); + + const q = quat.create(); + quat.setAxisAngle(q, axis, mulberry32.sample(rand) * Math.PI * 2); + rotations.push(q); + localMats.push(mat4.create()); + worldMats.push(mat4.create()); + } + + const unitBox = box3.create(); + box3.set(unitBox, -0.5, -0.5, -0.5, 0.5, 0.5, 0.5); + const nodeBox = box3.create(); + const sceneBounds = box3.create(); + + yield () => { + // parent indices precede child indices, so one pass propagates fully + for (let i = 0; i < N; i++) { + mat4.fromRotationTranslationScale(localMats[i], rotations[i], positions[i], unitScale); + if (i === 0) { + mat4.copy(worldMats[i], localMats[i]); + } else { + mat4.multiply(worldMats[i], worldMats[(i - 1) >> 2], localMats[i]); + } + } + + box3.empty(sceneBounds); + + for (let i = 0; i < N; i++) { + box3.transformMat4(nodeBox, unitBox, worldMats[i]); + box3.union(sceneBounds, sceneBounds, nodeBox); + } + + sink = box3.surfaceArea(sceneBounds); + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/core/mat4.bench.ts b/benches/core/mat4.bench.ts new file mode 100644 index 0000000..846f6c7 --- /dev/null +++ b/benches/core/mat4.bench.ts @@ -0,0 +1,72 @@ +import { bench, group } from '@pmndrs/labs'; +import * as mat4 from '../../src/core/mat4'; +import type { Mat4 } from '../../src/core/mat4'; +import * as mulberry32 from '../../src/random/mulberry32'; + +const N = 10_000; + +function makeMats(seed: number): Mat4[] { + const rand = mulberry32.create(seed); + const mats: Mat4[] = []; + for (let i = 0; i < N; i++) { + const m = mat4.create(); + mat4.fromRotation(m, mulberry32.sample(rand) * Math.PI * 2, [0.267261, 0.534522, 0.801784]); + mat4.translate(m, m, [mulberry32.sample(rand), mulberry32.sample(rand), mulberry32.sample(rand)]); + mats.push(m); + } + return mats; +} + +let sink = 0; + +group('mat4 ops 10k @core @mat4', () => { + bench('multiply', function* () { + const a = makeMats(1); + const b = makeMats(2); + const out = mat4.create(); + + yield () => { + for (let i = 0; i < N; i++) { + mat4.multiply(out, a[i], b[i]); + } + }; + }).gc('inner'); + + bench('invert', function* () { + const a = makeMats(1); + const out = mat4.create(); + + yield () => { + for (let i = 0; i < N; i++) { + mat4.invert(out, a[i]); + } + }; + }).gc('inner'); + + bench('determinant', function* () { + const a = makeMats(1); + + yield () => { + let sum = 0; + for (let i = 0; i < N; i++) { + sum += mat4.determinant(a[i]); + } + sink = sum; + }; + }).gc('inner'); + + bench('fromRotationTranslationScale', function* () { + const q: [number, number, number, number] = [0, 0.3826834, 0, 0.9238795]; + const t: [number, number, number] = [1, 2, 3]; + const s: [number, number, number] = [1, 1, 1]; + const out = mat4.create(); + + yield () => { + for (let i = 0; i < N; i++) { + mat4.fromRotationTranslationScale(out, q, t, s); + } + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/core/quat.bench.ts b/benches/core/quat.bench.ts new file mode 100644 index 0000000..e737f22 --- /dev/null +++ b/benches/core/quat.bench.ts @@ -0,0 +1,54 @@ +import { bench, group } from '@pmndrs/labs'; +import * as quat from '../../src/core/quat'; +import type { Quat } from '../../src/core/quat'; +import * as mulberry32 from '../../src/random/mulberry32'; + +const N = 10_000; + +function makeQuats(seed: number): Quat[] { + const rand = mulberry32.create(seed); + const quats: Quat[] = []; + for (let i = 0; i < N; i++) { + const q = quat.create(); + quat.setAxisAngle(q, [0.267261, 0.534522, 0.801784], mulberry32.sample(rand) * Math.PI * 2); + quats.push(q); + } + return quats; +} + +group('quat ops 10k @core @quat', () => { + bench('multiply', function* () { + const a = makeQuats(1); + const b = makeQuats(2); + const out = quat.create(); + + yield () => { + for (let i = 0; i < N; i++) { + quat.multiply(out, a[i], b[i]); + } + }; + }).gc('inner'); + + bench('slerp', function* () { + const a = makeQuats(1); + const b = makeQuats(2); + const out = quat.create(); + + yield () => { + for (let i = 0; i < N; i++) { + quat.slerp(out, a[i], b[i], 0.5); + } + }; + }).gc('inner'); + + bench('setAxisAngle', function* () { + const axis: [number, number, number] = [0.267261, 0.534522, 0.801784]; + const out = quat.create(); + + yield () => { + for (let i = 0; i < N; i++) { + quat.setAxisAngle(out, axis, i * 0.001); + } + }; + }).gc('inner'); +}); diff --git a/benches/core/vec3.bench.ts b/benches/core/vec3.bench.ts new file mode 100644 index 0000000..78790d3 --- /dev/null +++ b/benches/core/vec3.bench.ts @@ -0,0 +1,97 @@ +import { bench, group } from '@pmndrs/labs'; +import * as mat4 from '../../src/core/mat4'; +import type { Mat4 } from '../../src/core/mat4'; +import * as vec3 from '../../src/core/vec3'; +import type { Vec3 } from '../../src/core/vec3'; +import * as mulberry32 from '../../src/random/mulberry32'; + +const N = 10_000; + +function makeVecs(seed: number): Vec3[] { + const rand = mulberry32.create(seed); + const vecs: Vec3[] = []; + for (let i = 0; i < N; i++) { + vecs.push([mulberry32.sample(rand) * 2 - 1, mulberry32.sample(rand) * 2 - 1, mulberry32.sample(rand) * 2 - 1]); + } + return vecs; +} + +let sink = 0; + +group('vec3 ops 10k @core @vec3', () => { + bench('add', function* () { + const a = makeVecs(1); + const b = makeVecs(2); + const out = vec3.create(); + + yield () => { + for (let i = 0; i < N; i++) { + vec3.add(out, a[i], b[i]); + } + }; + }).gc('inner'); + + bench('cross', function* () { + const a = makeVecs(1); + const b = makeVecs(2); + const out = vec3.create(); + + yield () => { + for (let i = 0; i < N; i++) { + vec3.cross(out, a[i], b[i]); + } + }; + }).gc('inner'); + + bench('dot', function* () { + const a = makeVecs(1); + const b = makeVecs(2); + + yield () => { + let sum = 0; + for (let i = 0; i < N; i++) { + sum += vec3.dot(a[i], b[i]); + } + sink = sum; + }; + }).gc('inner'); + + bench('normalize', function* () { + const a = makeVecs(1); + const out = vec3.create(); + + yield () => { + for (let i = 0; i < N; i++) { + vec3.normalize(out, a[i]); + } + }; + }).gc('inner'); + + bench('lerp', function* () { + const a = makeVecs(1); + const b = makeVecs(2); + const out = vec3.create(); + + yield () => { + for (let i = 0; i < N; i++) { + vec3.lerp(out, a[i], b[i], 0.5); + } + }; + }).gc('inner'); + + bench('transformMat4', function* () { + const a = makeVecs(1); + const m: Mat4 = mat4.create(); + mat4.fromRotation(m, 0.5, [0.267261, 0.534522, 0.801784]); + mat4.translate(m, m, [1, 2, 3]); + const out = vec3.create(); + + yield () => { + for (let i = 0; i < N; i++) { + vec3.transformMat4(out, a[i], m); + } + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/labs.config.ts b/benches/labs.config.ts new file mode 100644 index 0000000..3ee5317 --- /dev/null +++ b/benches/labs.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@pmndrs/labs'; + +export default defineConfig({ + benchDir: '.', + benchMatch: '**/*.bench.ts', +}); diff --git a/benches/noise/noise.bench.ts b/benches/noise/noise.bench.ts new file mode 100644 index 0000000..3c255fd --- /dev/null +++ b/benches/noise/noise.bench.ts @@ -0,0 +1,61 @@ +import { bench, group } from '@pmndrs/labs'; +import * as perlin2d from '../../src/noise/perlin2d'; +import * as perlin3d from '../../src/noise/perlin3d'; +import * as simplex2d from '../../src/noise/simplex2d'; +import * as simplex3d from '../../src/noise/simplex3d'; + +const N = 10_000; + +let sink = 0; + +group('noise sample 10k @noise', () => { + bench('perlin2d', function* () { + const gen = perlin2d.create(42); + + yield () => { + let sum = 0; + for (let i = 0; i < N; i++) { + sum += perlin2d.sample(gen, i * 0.01, i * 0.013); + } + sink = sum; + }; + }).gc('inner'); + + bench('perlin3d', function* () { + const gen = perlin3d.create(42); + + yield () => { + let sum = 0; + for (let i = 0; i < N; i++) { + sum += perlin3d.sample(gen, i * 0.01, i * 0.013, i * 0.017); + } + sink = sum; + }; + }).gc('inner'); + + bench('simplex2d', function* () { + const gen = simplex2d.create(42); + + yield () => { + let sum = 0; + for (let i = 0; i < N; i++) { + sum += simplex2d.sample(gen, i * 0.01, i * 0.013); + } + sink = sum; + }; + }).gc('inner'); + + bench('simplex3d', function* () { + const gen = simplex3d.create(42); + + yield () => { + let sum = 0; + for (let i = 0; i < N; i++) { + sum += simplex3d.sample(gen, i * 0.01, i * 0.013, i * 0.017); + } + sink = sum; + }; + }).gc('inner'); +}); + +if (sink === Infinity) throw new Error('unreachable'); diff --git a/benches/package.json b/benches/package.json new file mode 100644 index 0000000..afb721f --- /dev/null +++ b/benches/package.json @@ -0,0 +1,14 @@ +{ + "name": "benches", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "@pmndrs/labs": "^0.6.0" + }, + "devDependencies": { + "tsx": "^4.21.0", + "typescript": "^5.9.2" + } +} + \ No newline at end of file diff --git a/benches/tsconfig.json b/benches/tsconfig.json new file mode 100644 index 0000000..eaf8b90 --- /dev/null +++ b/benches/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true + }, + "include": ["**/*.ts", "../src/**/*.ts"] +} diff --git a/package.json b/package.json index 6e890ba..937e748 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "format": "biome format --write src/. tst/.", "lint": "biome lint --write src/. tst/.", "test": "vitest run ./tst", + "bench": "pnpm --filter benches exec labs", "typecheck-docs": "tsc --project docs/tsconfig.json --noEmit", "docs": "(cd docs && node ./build.js)", "typedoc": "typedoc --tsconfig ./tsconfig.json --name \"maath docs\" --out ./dist-typedoc ./src/index.ts ./src/shapes/index.ts ./src/geometry/index.ts ./src/time/index.ts ./src/random/index.ts ./src/noise/index.ts ./src/color/index.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34860f3..246a59a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,20 +9,11 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: ^2.2.0 + specifier: ^2.5.6 version: 2.5.6 - '@types/three': - specifier: ^0.134.0 - version: 0.134.0 - npm-run-all2: - specifier: ^9.0.1 - version: 9.0.3 rolldown: - specifier: ^1.0.2 + specifier: ^1.2.1 version: 1.2.1 - three: - specifier: ^0.134.0 - version: 0.134.0 typedoc: specifier: ^0.28.16 version: 0.28.20(typescript@5.9.3) @@ -31,7 +22,20 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.7(yaml@2.9.0) + version: 3.2.7(tsx@4.23.6)(yaml@2.9.0) + + benches: + dependencies: + '@pmndrs/labs': + specifier: ^0.6.0 + version: 0.6.0 + devDependencies: + tsx: + specifier: ^4.21.0 + version: 4.23.6 + typescript: + specifier: ^5.9.2 + version: 5.9.3 docs: dependencies: @@ -41,28 +45,34 @@ importers: examples: dependencies: + gpucat: + specifier: github:isaac-mason/gpucat + version: https://codeload.github.com/isaac-mason/gpucat/tar.gz/19af76c1780fcafe39fda1a0232243d1d5d9a8d1 + lil-gui: + specifier: ^0.20.0 + version: 0.20.0 maath: specifier: workspace:* version: link:.. - three: - specifier: ^0.134.0 - version: 0.134.0 devDependencies: - '@types/three': - specifier: ^0.134.0 - version: 0.134.0 + '@playwright/test': + specifier: ^1.50.0 + version: 1.62.1 + '@webgpu/types': + specifier: ^0.1.69 + version: 0.1.71 typescript: specifier: ^5.9.2 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.6(yaml@2.9.0) + version: 7.3.6(tsx@4.23.6)(yaml@2.9.0) website: devDependencies: vite: specifier: ^7.3.1 - version: 7.3.6(yaml@2.9.0) + version: 7.3.6(tsx@4.23.6)(yaml@2.9.0) packages: @@ -119,6 +129,14 @@ packages: cpu: [x64] os: [win32] + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + '@emnapi/core@2.0.0-alpha.3': resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} @@ -306,6 +324,16 @@ packages: '@oxc-project/types@0.142.0': resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@pmndrs/labs@0.6.0': + resolution: {integrity: sha512-RBGGNfUgMR6mYh+u7nhPqvPga/gFZaEXUMmkv6vJj0y7JrTT4c0+1fu0H3aQaSMoH/VcCtB6I4ai1i4FB6f3kg==} + engines: {node: '>=25.0.0', pnpm: '>=10.0.0'} + hasBin: true + '@rolldown/binding-android-arm64@1.2.1': resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -552,9 +580,6 @@ packages: '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} - '@types/three@0.134.0': - resolution: {integrity: sha512-4YB+99Rgqq27EjiYTItEoZtdjLnTh8W9LxowgpC9eWsjaQJIL4Kn/ZcUKAnW3gB/jS4hqGN8iqmid+RcUZDzpA==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -587,9 +612,8 @@ packages: '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} - ansi-styles@7.0.0: - resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==} - engines: {node: '>=22'} + '@webgpu/types@0.1.71': + resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -618,10 +642,6 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -654,6 +674,15 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -663,24 +692,25 @@ packages: picomatch: optional: true + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isexe@4.0.0: - resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} - engines: {node: '>=20'} + gpucat@https://codeload.github.com/isaac-mason/gpucat/tar.gz/19af76c1780fcafe39fda1a0232243d1d5d9a8d1: + resolution: {tarball: https://codeload.github.com/isaac-mason/gpucat/tar.gz/19af76c1780fcafe39fda1a0232243d1d5d9a8d1} + version: 0.0.0-alpha.1 js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - json-parse-even-better-errors@6.0.0: - resolution: {integrity: sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + lil-gui@0.20.0: + resolution: {integrity: sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==} linkify-it@5.0.2: resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} @@ -698,13 +728,13 @@ packages: resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} hasBin: true + mathcat@https://codeload.github.com/isaac-mason/mathcat/tar.gz/fe42b67f84b85efbce1f8aee6b2df30e349e55e2: + resolution: {tarball: https://codeload.github.com/isaac-mason/mathcat/tar.gz/fe42b67f84b85efbce1f8aee6b2df30e349e55e2} + version: 0.0.14 + mdurl@2.1.0: resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} - memorystream@0.3.1: - resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} - engines: {node: '>= 0.10.0'} - minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -717,19 +747,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - npm-normalize-package-bin@6.0.0: - resolution: {integrity: sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - - npm-run-all2@9.0.3: - resolution: {integrity: sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0, npm: '>= 10'} - hasBin: true - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -744,9 +761,14 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} - pidtree@1.0.0: - resolution: {integrity: sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==} - engines: {node: '>=18'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} hasBin: true postcss@8.5.25: @@ -757,10 +779,6 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} - read-package-json-fast@6.0.0: - resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - rolldown@1.2.1: resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -771,21 +789,12 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shell-quote@1.10.0: - resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -799,9 +808,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - three@0.134.0: - resolution: {integrity: sha512-LbBerg7GaSPjYtTOnu41AMp7tV6efUNR3p4Wk5NzkSsNTBuA5mDGOfwwZL1jhhVMLx9V20HolIUo0+U3AXehbg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -827,6 +833,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.6: + resolution: {integrity: sha512-D/YYGUDqKlLvXhM5fBBbiENaGICxLfU4viHnZEkgmgplnDFa+Kczy34VV7AmLJgdzisv0I/J3zitfC26JH3GXg==} + engines: {node: '>=18.0.0'} + hasBin: true + typedoc@0.28.20: resolution: {integrity: sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==} engines: {node: '>= 18', pnpm: '>= 10'} @@ -915,16 +926,6 @@ packages: jsdom: optional: true - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - which@7.0.0: - resolution: {integrity: sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -972,6 +973,18 @@ snapshots: '@biomejs/cli-win32-x64@2.5.6': optional: true + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + '@emnapi/core@2.0.0-alpha.3': dependencies: '@emnapi/wasi-threads': 2.0.1 @@ -1088,6 +1101,15 @@ snapshots: '@oxc-project/types@0.142.0': {} + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@pmndrs/labs@0.6.0': + dependencies: + '@clack/prompts': 1.7.0 + tsx: 4.23.6 + '@rolldown/binding-android-arm64@1.2.1': optional: true @@ -1252,8 +1274,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/three@0.134.0': {} - '@types/unist@3.0.3': {} '@vitest/expect@3.2.7': @@ -1264,13 +1284,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6(yaml@2.9.0))': + '@vitest/mocker@3.2.7(vite@7.3.6(tsx@4.23.6)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(yaml@2.9.0) + vite: 7.3.6(tsx@4.23.6)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -1298,7 +1318,7 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - ansi-styles@7.0.0: {} + '@webgpu/types@0.1.71': {} argparse@2.0.1: {} @@ -1322,12 +1342,6 @@ snapshots: check-error@2.1.3: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -1373,20 +1387,33 @@ snapshots: expect-type@1.4.0: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 - fsevents@2.3.3: + fsevents@2.3.2: optional: true - isexe@2.0.0: {} + fsevents@2.3.3: + optional: true - isexe@4.0.0: {} + gpucat@https://codeload.github.com/isaac-mason/gpucat/tar.gz/19af76c1780fcafe39fda1a0232243d1d5d9a8d1: + dependencies: + mathcat: https://codeload.github.com/isaac-mason/mathcat/tar.gz/fe42b67f84b85efbce1f8aee6b2df30e349e55e2 js-tokens@9.0.1: {} - json-parse-even-better-errors@6.0.0: {} + lil-gui@0.20.0: {} linkify-it@5.0.2: dependencies: @@ -1409,9 +1436,9 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - mdurl@2.1.0: {} + mathcat@https://codeload.github.com/isaac-mason/mathcat/tar.gz/fe42b67f84b85efbce1f8aee6b2df30e349e55e2: {} - memorystream@0.3.1: {} + mdurl@2.1.0: {} minimatch@10.2.6: dependencies: @@ -1421,21 +1448,6 @@ snapshots: nanoid@3.3.16: {} - npm-normalize-package-bin@6.0.0: {} - - npm-run-all2@9.0.3: - dependencies: - ansi-styles: 7.0.0 - cross-spawn: 7.0.6 - memorystream: 0.3.1 - picomatch: 4.0.5 - pidtree: 1.0.0 - read-package-json-fast: 6.0.0 - shell-quote: 1.10.0 - which: 7.0.0 - - path-key@3.1.1: {} - pathe@2.0.3: {} pathval@2.0.1: {} @@ -1444,7 +1456,13 @@ snapshots: picomatch@4.0.5: {} - pidtree@1.0.0: {} + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 postcss@8.5.25: dependencies: @@ -1454,11 +1472,6 @@ snapshots: punycode.js@2.3.1: {} - read-package-json-fast@6.0.0: - dependencies: - json-parse-even-better-errors: 6.0.0 - npm-normalize-package-bin: 6.0.0 - rolldown@1.2.1: dependencies: '@oxc-project/types': 0.142.0 @@ -1512,16 +1525,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.4 fsevents: 2.3.3 - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shell-quote@1.10.0: {} - siginfo@2.0.0: {} + sisteransi@1.0.5: {} + source-map-js@1.2.1: {} stackback@0.0.2: {} @@ -1532,8 +1539,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - three@0.134.0: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1552,6 +1557,12 @@ snapshots: tslib@2.8.1: optional: true + tsx@4.23.6: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + typedoc@0.28.20(typescript@5.9.3): dependencies: '@gerrit0/mini-shiki': 3.23.0 @@ -1565,13 +1576,13 @@ snapshots: uc.micro@2.1.0: {} - vite-node@3.2.4(yaml@2.9.0): + vite-node@3.2.4(tsx@4.23.6)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(yaml@2.9.0) + vite: 7.3.6(tsx@4.23.6)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -1586,7 +1597,7 @@ snapshots: - tsx - yaml - vite@7.3.6(yaml@2.9.0): + vite@7.3.6(tsx@4.23.6)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) @@ -1596,13 +1607,14 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 + tsx: 4.23.6 yaml: 2.9.0 - vitest@3.2.7(yaml@2.9.0): + vitest@3.2.7(tsx@4.23.6)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(yaml@2.9.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(tsx@4.23.6)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -1620,8 +1632,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(yaml@2.9.0) - vite-node: 3.2.4(yaml@2.9.0) + vite: 7.3.6(tsx@4.23.6)(yaml@2.9.0) + vite-node: 3.2.4(tsx@4.23.6)(yaml@2.9.0) why-is-node-running: 2.3.0 transitivePeerDependencies: - jiti @@ -1637,14 +1649,6 @@ snapshots: - tsx - yaml - which@2.0.2: - dependencies: - isexe: 2.0.0 - - which@7.0.0: - dependencies: - isexe: 4.0.0 - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e88e911..ab00a4a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - 'benches' - 'docs' - 'examples' - 'website'