Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7e79688
docs(PRD-860): a repeat web run is ready in about a second, with a lo…
jonit-dev Nov 8, 2026
3ebe059
perf(PRD-860): load bench, ctx.assets.memo, immutable asset headers, …
jonit-dev Nov 8, 2026
1bc012e
perf(PRD-860): the sky's PMREM bake takes 4 GPU round trips, not 10
jonit-dev Nov 8, 2026
40babae
perf(PRD-860): load bench can trace the GPU process, inject a probe a…
jonit-dev Nov 8, 2026
e6f2415
perf(PRD-860): colliders cull by one matrix row, the sky table and se…
jonit-dev Nov 8, 2026
03414c3
perf(PRD-860): collectathon builds instance matrices and path distanc…
jonit-dev Nov 8, 2026
bf9a557
perf(PRD-860): a matrix over shared engine memory is a view onto it, …
jonit-dev Nov 8, 2026
3ae3ff0
perf(PRD-860): Rapier downloads beside the engine boot, not in the en…
jonit-dev Nov 9, 2026
637eda3
perf(PRD-860): the collectathon paints its sky while its surface text…
jonit-dev Nov 9, 2026
a1fe300
perf(PRD-860): a built page starts the engine's Wasm with itself, the…
jonit-dev Nov 9, 2026
49c1ae3
perf(PRD-860): the engine Wasm preload runs only for a Wasm this brow…
jonit-dev Nov 9, 2026
6a1f7c2
docs(PRD-860): tick the verified levers, record the dropped ones, AC-…
jonit-dev Nov 9, 2026
b170c93
chore(PRD-860): sort the kits' memo imports, recompute their scaffold…
jonit-dev Nov 9, 2026
4345594
chore(PRD-860): the load bench can keep each visit's ready frame (TN_…
jonit-dev Nov 9, 2026
5a7d16f
docs(PRD-860): AC-4 visual judge NEUTRAL
jonit-dev Nov 9, 2026
a97dbf0
perf(PRD-860): setMatrixAt writes the instance array and sends the it…
jonit-dev Nov 9, 2026
e13c0b5
perf(PRD-860): setColorAt writes the instance colours the same way on…
jonit-dev Nov 9, 2026
e0951e5
docs(PRD-860): instance array writes, 130 ms off a repeat run paired
jonit-dev Nov 9, 2026
d2ed89f
docs(PRD-860): phase 2 playtests on the laptop, same GPU p95 miss as …
jonit-dev Nov 9, 2026
3e15fa1
docs(PRD-860): what a first visit still spends, and why the next cuts…
jonit-dev Nov 9, 2026
dacbbf0
perf(PRD-860): collectathon paints its sky and plants its island fast…
jonit-dev Nov 9, 2026
522a556
perf(PRD-860): Rapier and Recast initialize while the scene loads
jonit-dev Nov 9, 2026
38bc9b7
docs(PRD-860): rapier() names when ctx.physics is ready
jonit-dev Nov 9, 2026
ef302e3
Merge remote-tracking branch 'origin/develop' into perf/prd-860-fast-…
jonit-dev Nov 9, 2026
baf2afb
docs(PRD-860): AC-2 met at 2.48-2.58 s; AC-1 at 1.27-1.32 s, merged o…
jonit-dev Nov 9, 2026
f9ee18e
fix(PRD-860): asset-loader fakes carry memo; the bench reads resource…
jonit-dev Nov 9, 2026
02dbc82
docs(PRD-860): native coverage record follows the renderer change
jonit-dev Nov 9, 2026
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

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions docs/verification/native-coverage-2026-08-28.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ targets could not be built and are named below.
| `src/audio/` | 1471 | 1301 | 88.44% |
| `src/canvas/` | 1334 | 1110 | 83.21% |
| `src/cli/` | 1691 | 1243 | 73.51% |
| `src/engine/` | 43273 | 36221 | 83.70% |
| `src/engine/` | 43282 | 36230 | 83.71% |
| `src/fs/` | 235 | 189 | 80.43% |
| `src/http/` | 410 | 377 | 91.95% |
| `src/js/` | 2838 | 2230 | 78.58% |
Expand All @@ -22,12 +22,12 @@ targets could not be built and are named below.
| `src/storage/` | 370 | 329 | 88.92% |
| `src/utils/` | 0 | 0 | 0.00% |
| `src/vfs/` | 239 | 195 | 81.59% |
| `src/webgpu/` | 10076 | 7745 | 76.87% |
| `src/webgpu/` | 10076 | 7736 | 76.78% |
| `src/webtransport/` | 1391 | 1078 | 77.50% |
| `src/workers/` | 615 | 524 | 85.20% |
| **TOTAL** | **68603** | **56083** | **81.75%** |
| `src/workers/` | 615 | 527 | 85.69% |
| **TOTAL** | **68612** | **56086** | **81.74%** |

Source digest: `sha256:2f9ed85cc1717a794e64a27b65fa0b8af957a162c1733396241ce64111e31162`
Source digest: `sha256:2da952e005c78e4d4a7328bc8b1949e8af153f74dc24b7bd7dbb680262ffb453`

The default `pnpm budgets` gate reads this committed measurement without configuring or compiling
the native host. Any native source, native C++ test, CTest registration, or coverage aggregation
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"api:surface:sync": "tsx scripts/check-api-surface.ts --update && biome format --write scripts/api-surface.json",
"bake:delete-test": "sh scripts/xvfb.sh tsx scripts/bake-delete-test.ts",
"bench:engines": "tsx scripts/engine-load-test/cli.ts",
"bench:load": "tsx scripts/load-bench.ts",
"bench:engines:report": "tsx scripts/engine-load-test/cli.ts --compare",
"bench:engines:monitor": "tsx scripts/engine-load-test/monitor.ts",
"dashboard": "tsx scripts/perf-live.ts",
Expand Down
116 changes: 116 additions & 0 deletions packages/core/__tests__/asset-memo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createAssetMemo, decodeMemo, encodeMemo } from "../src/asset-memo.js";
import { FakeCacheStorage } from "./fixtures/fake-cache.js";

const scope = globalThis as { caches?: unknown; __TN_BUILD_ID__?: unknown };

describe("ctx.assets.memo (PRD-860)", () => {
let storage: FakeCacheStorage;
beforeEach(() => {
storage = new FakeCacheStorage();
scope.caches = storage;
scope.__TN_BUILD_ID__ = "/assets/index-AAAA.js";
vi.spyOn(console, "info").mockImplementation(() => undefined);
});
afterEach(() => {
Reflect.deleteProperty(scope, "caches");
Reflect.deleteProperty(scope, "__TN_BUILD_ID__");
vi.restoreAllMocks();
});

it("builds once, then a later run of the same build reads the stored arrays back", async () => {
const build = vi.fn(() => ({
pixels: new Uint8Array([1, 2, 3]),
at: new Float32Array([0.5, -2]),
}));
const first = await createAssetMemo("game")("sky", build);
await vi.waitFor(async () =>
expect((await storage.open([...storage.caches.keys()][0] ?? "")).store.size).toBe(1),
);
const second = await createAssetMemo("game")("sky", build);
expect(build).toHaveBeenCalledTimes(1);
expect([...second.pixels]).toEqual([...first.pixels]);
expect(second.at).toBeInstanceOf(Float32Array);
expect([...second.at]).toEqual([0.5, -2]);
});

it("rebuilds after a new build and deletes the old build's entries", async () => {
const build = vi.fn(() => ({ v: new Uint16Array([7]) }));
await createAssetMemo("game")("k", build);
await vi.waitFor(() => expect(storage.caches.size).toBe(1));
scope.__TN_BUILD_ID__ = "/assets/index-BBBB.js";
await createAssetMemo("game")("k", build);
expect(build).toHaveBeenCalledTimes(2);
const cache = [...storage.caches.values()][0];
await vi.waitFor(async () => {
const urls = (await cache?.keys())?.map((request) => request.url) ?? [];
expect(urls).toHaveLength(1);
expect(urls[0]).toContain(encodeURIComponent("/assets/index-BBBB.js"));
});
});

it("builds every run, and says why, with no build id or no cache storage", async () => {
Reflect.deleteProperty(scope, "__TN_BUILD_ID__");
const build = vi.fn(() => ({ v: new Int32Array([1]) }));
await createAssetMemo("game")("k", build);
await createAssetMemo("game")("k", build);
expect(build).toHaveBeenCalledTimes(2);
expect(storage.caches.size).toBe(0);
expect(String(vi.mocked(console.info).mock.calls.at(-1)?.[0])).toContain(
'"reason":"no build id',
);
});

it("takes a build typed by an interface, the shape a generator module exports", async () => {
interface IPainted {
readonly sky: Float32Array;
readonly mask: Uint8Array;
}
const paint = (): IPainted => ({ mask: new Uint8Array([1]), sky: new Float32Array([0.5]) });
const painted: IPainted = await createAssetMemo("game")("painted", paint);
expect([...painted.mask]).toEqual([1]);
});

it("rebuilds over a corrupt entry instead of failing the load", async () => {
const build = vi.fn(() => ({ v: new Float64Array([3]) }));
const memo = createAssetMemo("game");
await memo("k", build);
const cache = [...storage.caches.values()][0];
await vi.waitFor(async () => expect((await cache?.keys())?.length).toBe(1));
const url = (await cache?.keys())?.[0]?.url ?? "";
await cache?.put(url, new Response(new Uint8Array([9, 9])));
const again = await createAssetMemo("game")("k", build);
expect(build).toHaveBeenCalledTimes(2);
expect([...again.v]).toEqual([3]);
});

it("rejects a key or a value it cannot store", async () => {
const memo = createAssetMemo("game");
await expect(memo("", () => ({ v: new Uint8Array(1) }))).rejects.toThrow(
"TN_ASSET_MEMO_INVALID",
);
await expect(memo("k", () => ({ v: [1, 2] as unknown as Uint8Array }))).rejects.toThrow(
"TN_ASSET_MEMO_INVALID",
);
});

it("round-trips every typed array kind with its exact values", () => {
const value = {
a: new Float32Array([1.5]),
b: new Float64Array([Math.PI]),
c: new Int8Array([-3]),
d: new Int16Array([-300]),
e: new Int32Array([-70000]),
f: new Uint8Array([250]),
g: new Uint8ClampedArray([255]),
h: new Uint16Array([65000]),
i: new Uint32Array([4000000000]),
empty: new Float32Array(0),
};
const decoded = decodeMemo(encodeMemo(value));
for (const [name, array] of Object.entries(value)) {
expect(decoded[name]?.constructor).toBe(array.constructor);
expect([...(decoded[name] ?? [])]).toEqual([...array]);
}
});
});
1 change: 1 addition & 0 deletions packages/core/__tests__/world-asset-alias.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ function countingLoader(): { assets: IAssetLoader; models: string[] } {
const inner = createAssetLoader({ basePath: "" });
const models: string[] = [];
const assets: IAssetLoader = {
memo: inner.memo,
get compressedTextures() {
return inner.compressedTextures;
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/__tests__/world-cells-terrain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function makeModel(): Object3D {

function cacheLoader(): IAssetLoader {
return {
memo: () => Promise.reject(new Error("unused")),
audio: () => Promise.reject(new Error("unused")),
cache: {
backgroundFill: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/__tests__/world-cells.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ function cacheLoader(load: (path: string) => Object3D | undefined = () => makeMo
const released: string[] = [];
const cleared: number[] = [];
const assets: IAssetLoader = {
memo: () => Promise.reject(new Error("unused")),
audio: () => Promise.reject(new Error("unused")),
cache: {
backgroundFill: false,
Expand Down
5 changes: 4 additions & 1 deletion packages/core/capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -5566,7 +5566,10 @@
},
{
"aliases": [],
"constraints": ["place rapier() before recast() in the plugin list"],
"constraints": [
"place rapier() before recast() in the plugin list",
"ctx.physics is set before Scene.enter(), not during load(): Rapier initializes while the scene loads, so create bodies in enter() or later (TN_PHYSICS_NOT_READY)"
],
"example": "const game = defineGame({ plugins: [rapier()] });",
"importPath": "@threenative/physics",
"kind": "function",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/asset-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ const ACCESS_REFRESH_MS = 3_600_000;
/** Lower is more urgent. */
export const PRIORITY_ORDER: Record<AssetPriority, number> = { now: 0, soon: 1, idle: 2 };

type CacheStorageLike = {
export type CacheStorageLike = {
delete(name: string): Promise<boolean>;
keys(): Promise<readonly string[]>;
open(name: string): Promise<CacheLike>;
};

type CacheLike = {
export type CacheLike = {
delete(request: string): Promise<boolean>;
keys(): Promise<readonly Request[]>;
match(request: string): Promise<Response | undefined>;
Expand All @@ -94,7 +94,7 @@ interface IJob {
priority: AssetPriority;
}

function cacheStorage(): CacheStorageLike | undefined {
export function cacheStorage(): CacheStorageLike | undefined {
return (globalThis as { caches?: CacheStorageLike }).caches;
}

Expand Down
Loading
Loading