Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
45d24e8
docs: PRD-615 V8 on linux-arm64 plan
jonit-dev Oct 15, 2026
7acb6f7
docs(PRD-615): link independent draft dependency
jonit-dev Oct 15, 2026
6226a08
feat(runtime-native): add opt-in cached Linux arm64 V8 producer
jonit-dev Oct 15, 2026
9363984
docs(PRD-615): record verified producer contracts
jonit-dev Oct 15, 2026
60475ef
build(PRD-615): inherit published PR507 arm64 dependency prerequisites
jonit-dev Oct 15, 2026
b07f20b
fix(runtime-native): bypass depot_tools Ninja launcher for arm64 V8
jonit-dev Oct 15, 2026
aeec347
fix(runtime-native): reuse pinned native arm64 GN from V8 DEPS
jonit-dev Oct 15, 2026
c3b71d9
fix(runtime-native): provision Linux V8 GLib build prerequisites
jonit-dev Oct 15, 2026
d9e6050
fix(runtime-native): adapt pinned Clang 18 runtime paths for V8
jonit-dev Oct 15, 2026
4530793
build(runtime-native): retain compiler-rt notice and enforce SDK prov…
jonit-dev Oct 15, 2026
285c7b6
build(PRD-615): inherit coherent PR507 native runtime prerequisites
jonit-dev Oct 15, 2026
7aaeb8d
fix(ci): allow exact pinned compiler-rt downgrade on hosted arm64
jonit-dev Oct 15, 2026
8eda613
docs(PRD-615): distinguish submitted cleanup from completed cancellation
jonit-dev Oct 15, 2026
2bafeac
test(PRD-615): inherit coherent PR507 native regression and coverage …
jonit-dev Oct 15, 2026
486b13c
fix(native): inherit PR507 coverage floor ratchet repair
jonit-dev Oct 15, 2026
4ea2e78
fix(native): package pinned Chromium build license and resume compile…
jonit-dev Oct 15, 2026
4bd73df
feat(native): consume cached ARM64 V8 and qualify paired starter perf…
jonit-dev Oct 15, 2026
339c354
fix(native): enforce locked V8 producer receipt on cache consumption
jonit-dev Oct 15, 2026
c0bef30
test(native): align ARM64 V8 receipts and register temporary cleanup
jonit-dev Oct 15, 2026
b0e4475
docs(native): record verified consumer gates before ARM64 qualification
jonit-dev Oct 15, 2026
574779a
fix(native): link Linux ARM64 V8 with libatomic
jonit-dev Oct 15, 2026
6f19dcb
docs(prd615): record verified ARM64 atomic-link repair gates
jonit-dev Oct 15, 2026
857c98e
fix(ci): cook shared starter assets for paired native engines
jonit-dev Oct 15, 2026
9c16c18
docs(prd615): record verified shared starter cook gates
jonit-dev Oct 15, 2026
470e4e2
fix(core): emit settled compilation window observations
jonit-dev Oct 15, 2026
a55a88c
docs(prd615): record telemetry candidate qualification
jonit-dev Oct 15, 2026
34997a1
fix(native): serialize complete telemetry stdout records
jonit-dev Oct 15, 2026
d87b42a
docs(prd615): record native stdout repair full gate
jonit-dev Oct 15, 2026
4b31add
fix(ci): preserve verified starter during paired performance runs
jonit-dev Oct 15, 2026
33f753c
fix(core): preserve compute state through deterministic startup
jonit-dev Oct 15, 2026
c312cee
docs(prd615): record verified deterministic startup gates
jonit-dev Oct 15, 2026
53f3927
docs(prd615): complete and archive Linux ARM64 V8 proof
jonit-dev Oct 15, 2026
94c16ca
fix(ci): preserve engine-specific proofs and rapid terrain saves
jonit-dev Oct 15, 2026
a972488
fix(ci): guard the QuickJS step lookup against an undefined split result
jonit-dev Oct 15, 2026
ab366b5
fix(engine): keep a turned figure's Euler and stop wall contacts read…
jonit-dev Oct 15, 2026
120837d
fix(starter): push the crate, a progress bar that only moves forward,…
jonit-dev Oct 15, 2026
e93f189
fix(engine): keep bones on exact quaternions and stop wall contacts r…
jonit-dev Oct 15, 2026
7868c00
feat(starter): walk by default, Shift to run, gaits blend by speed
jonit-dev Oct 15, 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
15 changes: 9 additions & 6 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ jobs:
fog-native:
name: Linux native fog correctness
needs: [fog]
# Reuse the proven native-platforms / native-assets ARM QuickJS route without changing defaults.
# Use the shipped ARM V8/wgpu default for native fog correctness.
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
env:
Expand Down Expand Up @@ -810,7 +810,7 @@ jobs:
name: Linux native fluid correctness
if: ${{ needs.paths.outputs.fluid-native == 'true' && !github.event.pull_request.draft }}
needs: paths
# Established native-platforms ARM QuickJS/wgpu route; no default backend change.
# Use the shipped ARM V8/wgpu default for native fluid correctness.
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
env:
Expand Down Expand Up @@ -888,8 +888,7 @@ jobs:
name: packaged native asset fallback
if: ${{ !github.event.pull_request.draft && needs.paths.outputs.native-assets == 'true' }}
needs: paths
# Match native-platforms starter-linux: this architecture selects QuickJS/wgpu without
# changing any runtime default. Native dependencies use the repository's locked provisioner.
# This fallback proof explicitly selects QuickJS; the ARM desktop default is V8.
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
env:
Expand Down Expand Up @@ -934,8 +933,12 @@ jobs:
libxcursor-dev libxi-dev libxfixes-dev libxss-dev libxkbcommon-dev
libgtk-3-dev libwebkit2gtk-4.1-dev libxcomposite-dev xcompmgr
libvulkan1 mesa-vulkan-drivers vulkan-tools xvfb zlib1g-dev
- name: Build the same QuickJS host the normal ARM desktop lane ships
run: pnpm --filter @threenative/runtime-native native:build
- name: Build the QuickJS host required by the packaged fallback
run: |
pnpm --filter @threenative/runtime-native native:build
node packages/runtime-native/scripts/download-deps.mjs --only quickjs
cmake -S packages/runtime-native -B packages/runtime-native/build/tn-linux -DMYSTRAL_USE_V8=OFF -DMYSTRAL_USE_QUICKJS=ON
cmake --build packages/runtime-native/build/tn-linux --target mystral --parallel 2
- name: Build the real fixture and capture its packaged native executable
env:
THREENATIVE_RUNTIME_BINARY: ${{ github.workspace }}/packages/runtime-native/build/tn-linux/mystral
Expand Down
261 changes: 256 additions & 5 deletions .github/workflows/native-platforms.yml

Large diffs are not rendered by default.

176 changes: 176 additions & 0 deletions docs/PRDs/done/PRD-615-v8-on-linux-arm64.md

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions docs/verification/native-coverage-2026-08-28.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ targets could not be built and are named below.
| `src/cli/` | 1690 | 1242 | 73.49% |
| `src/fs/` | 235 | 189 | 80.43% |
| `src/http/` | 410 | 377 | 91.95% |
| `src/js/` | 2785 | 2212 | 79.43% |
| `src/platform/` | 1460 | 1003 | 68.70% |
| `src/js/` | 2793 | 2216 | 79.34% |
| `src/platform/` | 1465 | 1008 | 68.81% |
| `src/raytracing/` | 461 | 399 | 86.55% |
| `src/runtime.cpp` | 2425 | 1890 | 77.94% |
| `src/runtime.cpp` | 2442 | 1896 | 77.64% |
| `src/screenshot_gate.cpp` | 27 | 24 | 88.89% |
| `src/storage/` | 327 | 286 | 87.46% |
| `src/utils/` | 0 | 0 | 0.00% |
| `src/vfs/` | 239 | 195 | 81.59% |
| `src/webgpu/` | 9842 | 7566 | 76.87% |
| `src/webgpu/` | 9885 | 7581 | 76.69% |
| `src/webtransport/` | 1391 | 1078 | 77.50% |
| `src/workers/` | 615 | 527 | 85.69% |
| **TOTAL** | **24785** | **19459** | **78.51%** |
| **TOTAL** | **24858** | **19489** | **78.40%** |

Source digest: `sha256:2828ae43f9f7f863426676c8b76acf9a02576361e410f0fafac09a8df706b1b9`
Source digest: `sha256:1a8ed34cf063a9530e8fa11b2722ae0b4c475402ee1baef998b5dea0acd3a4d7`

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 All @@ -41,7 +41,7 @@ change requires this opt-in command to refresh the record.
| `src/fs/` | 75.43% |
| `src/http/` | 86.95% |
| `src/js/` | 74.50% |
| `src/platform/` | 63.70% |
| `src/platform/` | 63.81% |
| `src/raytracing/` | 81.55% |
| `src/runtime.cpp` | 73.28% |
| `src/screenshot_gate.cpp` | 88.89% |
Expand Down
117 changes: 116 additions & 1 deletion examples/prd242-compute-lifetime/src/conformance.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Mesh, MeshBasicNodeMaterial, PlaneGeometry } from "three/webgpu";
// The root package bundles its Three.js dependency; this source-module import keeps this
// conformance bundle on the same Three.js runtime as the scene while exercising the exported
// registry implementation.
import { ComputeDrivenRegistry } from "../../../packages/core/src/compute-driven.ts";
import { warmUpScene } from "../../../packages/core/src/warmup.ts";
import { createPipelineCensus } from "../../../packages/core/src/pipeline-census.ts";
import { SoftBody3D } from "../../../packages/core/src/softbody.ts";
import { warmUpComputeNodes, warmUpScene } from "../../../packages/core/src/warmup.ts";
import {
assertCondition,
startVisualScene,
Expand All @@ -13,12 +16,123 @@ async function waitForSubmittedWork(renderer) {
await renderer.backend?.device?.queue?.onSubmittedWorkDone?.();
}

// Cached-pipeline proof: the baseline prepares GPU buffers with zero workgroups first.
async function assertCachedClothWarmupPreservesBuffers(renderer) {
assertCondition(renderer.backend?.isWebGPUBackend === true, "cloth warmup proof requires WebGPU");
const cloth = new SoftBody3D(
new Mesh(new PlaneGeometry(1, 1, 2, 2), new MeshBasicNodeMaterial()),
{
damping: 1.8,
gravity: [0, 0, 0],
pinned: [0],
stiffness: 36,
wind: [0, 0.4, 4.5],
},
);
const attributes = new Set();
const backend = renderer.backend;
const originalCreate = backend.createStorageAttribute;
const census = createPipelineCensus({ kind: "webgpu", adapterIdentity: "conformance-probe" });
census.installRenderer(renderer);
backend.createStorageAttribute = function (attribute) {
attributes.add(attribute);
return originalCreate.call(this, attribute);
};
try {
for (const node of cloth.warmupNodes) await renderer.computeAsync(node, 0);
} finally {
backend.createStorageAttribute = originalCreate;
}
const pipelines = census
.snapshot()
.events.filter((event) => event.kind === "compute" && event.status === "created");
census.dispose();
assertCondition(
pipelines.length === 2,
"cached cloth baseline must observe both real compute pipelines",
);
// This snapshots every solver storage attribute, including both position and velocity pairs.
assertCondition(
attributes.size >= 4,
"cloth proof must capture all four nonempty solver buffers",
);
const snapshots = async () =>
Promise.all(
[...attributes].map(async (attribute) => {
const bytes = new Uint8Array(await renderer.getArrayBufferAsync(attribute));
assertCondition(bytes.byteLength > 0, "cloth GPU readback must not be empty");
return bytes.slice();
}),
);
const before = await snapshots();
let calls = 0;
let clock = 0;
let completion;
let report;
const descriptor = Object.getOwnPropertyDescriptor(performance, "now");
const seam = {
backend,
computeAsync(node, count) {
calls += 1;
completion = renderer.computeAsync(node, count);
clock = 20;
return completion;
},
};
// Controlled deadline, not a wall-clock timeout; GPU dispatch and readback remain real.
try {
Object.defineProperty(performance, "now", { configurable: true, value: () => clock });
report = await warmUpComputeNodes(seam, cloth.warmupNodes, {
budgetMs: 10,
compileTimeoutMs: 10,
});
} finally {
if (descriptor === undefined) Reflect.deleteProperty(performance, "now");
else Object.defineProperty(performance, "now", descriptor);
}
assertCondition(completion?.then !== undefined, "cloth proof must await its real GPU delegate");
await completion;
await waitForSubmittedWork(renderer);
const after = await snapshots();
assertCondition(
calls === 1 && report.compiled === 0 && report.timedOut && report.abandoned === 2,
`cached cloth warmup must time out during the first half of its ordered pair: ${JSON.stringify({ calls, report })}`,
);
const equal = (left, right) =>
left.length === right.length && left.every((value, index) => value === right[index]);
assertCondition(
before.every((bytes, index) => equal(bytes, after[index])),
"cached cloth warmup mutated live GPU storage at its controlled half-pair deadline",
);
cloth.process({ kind: "webgpu", compute: (node) => renderer.compute(node) });
await waitForSubmittedWork(renderer);
const advanced = await snapshots();
const changed = after.filter((bytes, index) => !equal(bytes, advanced[index])).length;
assertCondition(
changed >= 4,
"the next real cloth solver step must mutate both position and velocity pairs",
);
cloth.detach();
const detail = {
pipelines: pipelines.length,
buffers: before.length,
unchanged: true,
changed,
calls,
report,
deadline: { budgetMs: 10, start: 0, end: 20 },
};
console.info(`TN_COMPUTE_WARMUP_CACHED_CONTROLLED:${JSON.stringify(detail)}`);
return detail;
}

export async function startScene(canvas, dimensions) {
return startVisualScene(
canvas,
dimensions,
"compute-driven-lifetime",
async ({ camera, renderer, scene }) => {
const cachedClothWarmup = await assertCachedClothWarmupPreservesBuffers(renderer);
const registry = new ComputeDrivenRegistry();
const add = (object) => {
scene.add(object);
Expand Down Expand Up @@ -74,6 +188,7 @@ export async function startScene(canvas, dimensions) {
return {
field: second,
detail: {
cachedClothWarmup,
attachments: 2,
firstReleased: first.released,
passesPerStep: 2,
Expand Down
14 changes: 14 additions & 0 deletions packages/core/__tests__/animation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,20 @@ describe("AnimationPlayer stride sync on in-place clips", () => {
),
]);

it("preserves the root's exact Euler while measuring a foot-plant stride", () => {
const root = new Object3D();
const foot = new Object3D();
foot.name = "Foot";
root.add(foot);
root.rotation.set(0, Math.PI, 0);
const player = new AnimationPlayer({ root, clips: [plantedWalk()] });
player.play("walk");
player.update(1 / 60);

expect(player.stride.clipGroundSpeed).toBeGreaterThan(0);
expect(root.rotation.toArray()).toEqual([0, Math.PI, 0, "XYZ"]);
});

it("matches an in-place walk cycle from the ground its planted foot sweeps", () => {
const { body, player } = character({ clips: [plantedWalk()] });
player.play("walk");
Expand Down
73 changes: 73 additions & 0 deletions packages/core/__tests__/game-frame-budget-surface.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe("the frame budget names the surface the game's own loop drew", () => {
const reported = JSON.parse(marker?.slice(FRAME_BUDGET_MARKER.length + 1) ?? "{}");
expect(reported.surface).toEqual({
atFloor: false,
compiling: false,
drawingBufferHeight: 540,
drawingBufferWidth: 1200,
resolutionScale: 0.5,
Expand Down Expand Up @@ -399,3 +400,75 @@ describe("the frame budget names the surface the game's own loop drew", () => {
}
});
});

describe("game frame budget surface compilation telemetry", () => {
it("reports explicit compiling state across active compile overlap and clean windows", async () => {
const canvas = testCanvas();
let frame: ((time: number) => void) | undefined;
const surfaces: Array<boolean | undefined> = [];
const game = defineGame({
frameBudget: {
report: () => {},
reportEvery: 1,
onWindow: (window) => {
surfaces.push(window.surface?.compiling);
},
},
renderer: {
canvas,
preferWebGPU: false,
webgl2Factory: () => ({
compileAsync: () => Promise.resolve(),
domElement: canvas,
render: () => undefined,
setSize: () => undefined,
}),
},
scenes: { test: EmptySceneForReporting },
start: "test",
});
const requestFrame = globalThis.requestAnimationFrame;
Object.defineProperty(globalThis, "requestAnimationFrame", {
configurable: true,
value: (callback: (time: number) => void) => {
frame = callback;
return 1;
},
});
try {
await game.start();
if (frame === undefined) throw new Error("Game did not start its loop.");
let time = performance.now();
for (let index = 0; index < 10; index += 1) {
time += 8;
frame(time);
await Promise.resolve();
}
await game.ctx?.startup.whenReady();
surfaces.length = 0;
const ctx = game.ctx;
if (ctx === undefined) throw new Error("Game context was not created.");
const active = ctx.renderer.compileAsync(ctx.scene, ctx.camera);
expect(ctx.renderer.compiling).toBe(true);
time += 16.7;
frame(time);
await active;
await Promise.resolve();
await ctx.renderer.compileAsync(ctx.scene, ctx.camera);
expect(ctx.renderer.compiling).toBe(false);
time += 16.7;
frame(time);
await Promise.resolve();
time += 16.7;
frame(time);
await Promise.resolve();
expect(surfaces.slice(-3)).toEqual([true, true, false]);
} finally {
await game.stop();
Object.defineProperty(globalThis, "requestAnimationFrame", {
configurable: true,
value: requestFrame,
});
}
});
});
Loading
Loading