@series-inc/rundot-syncplay 5.23.0-beta.7 → 5.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -15
- package/dist/browser.d.ts +87 -55
- package/dist/browser.js +47 -52
- package/dist/certification.d.ts +32 -3
- package/dist/certification.js +88 -3
- package/dist/cjs/browser.js +259 -87
- package/dist/cjs/certification.js +87 -2
- package/dist/cjs/collision.js +58 -0
- package/dist/cjs/creator.js +2 -1
- package/dist/cjs/engine-complete.js +213 -3
- package/dist/cjs/engine-parity-matrix.js +19 -4
- package/dist/cjs/index.js +199 -132
- package/dist/cjs/math.js +342 -15
- package/dist/cjs/movement3d.js +230 -1
- package/dist/cjs/node.js +5 -1
- package/dist/cjs/noise.js +58 -0
- package/dist/cjs/physics-cert.js +147 -6
- package/dist/cjs/physics2d.js +10 -4
- package/dist/cjs/physics3d-joints.js +637 -0
- package/dist/cjs/physics3d-shared.js +288 -0
- package/dist/cjs/physics3d-solver.js +1351 -0
- package/dist/cjs/physics3d-vehicle.js +467 -0
- package/dist/cjs/physics3d.js +1057 -223
- package/dist/cjs/random.js +49 -0
- package/dist/cjs/replay-bundle.js +127 -20
- package/dist/cjs/sample-scenes.js +3 -0
- package/dist/cjs/sdk-offline.js +6 -32
- package/dist/cjs/sdk-session.js +156 -0
- package/dist/cjs/session-build.js +42 -0
- package/dist/cjs/testing.js +107 -0
- package/dist/cli.js +40 -12
- package/dist/collision.d.ts +12 -0
- package/dist/collision.js +52 -0
- package/dist/creator.d.ts +1 -1
- package/dist/creator.js +1 -1
- package/dist/engine-complete.js +214 -4
- package/dist/engine-parity-matrix.js +19 -4
- package/dist/index.d.ts +94 -89
- package/dist/index.js +45 -56
- package/dist/math.d.ts +59 -6
- package/dist/math.js +342 -15
- package/dist/movement3d.d.ts +73 -0
- package/dist/movement3d.js +229 -1
- package/dist/node.d.ts +4 -0
- package/dist/node.js +2 -0
- package/dist/noise.d.ts +7 -0
- package/dist/noise.js +51 -0
- package/dist/physics-cert.d.ts +1 -1
- package/dist/physics-cert.js +147 -6
- package/dist/physics2d.js +10 -4
- package/dist/physics3d-joints.d.ts +94 -0
- package/dist/physics3d-joints.js +634 -0
- package/dist/physics3d-shared.d.ts +72 -0
- package/dist/physics3d-shared.js +257 -0
- package/dist/physics3d-solver.d.ts +197 -0
- package/dist/physics3d-solver.js +1346 -0
- package/dist/physics3d-vehicle.d.ts +84 -0
- package/dist/physics3d-vehicle.js +463 -0
- package/dist/physics3d.d.ts +108 -8
- package/dist/physics3d.js +1006 -177
- package/dist/random.d.ts +7 -0
- package/dist/random.js +49 -0
- package/dist/replay-bundle.d.ts +40 -1
- package/dist/replay-bundle.js +126 -20
- package/dist/sample-scenes.js +3 -0
- package/dist/sdk-offline.js +6 -32
- package/dist/sdk-session.d.ts +47 -0
- package/dist/sdk-session.js +153 -0
- package/dist/session-build.d.ts +21 -0
- package/dist/session-build.js +39 -0
- package/dist/testing.d.ts +52 -0
- package/dist/testing.js +45 -0
- package/dist/tools/vite-plugin.d.ts +8 -0
- package/dist/tools/vite-plugin.js +63 -9
- package/package.json +11 -3
package/dist/cli.js
CHANGED
|
@@ -3123,7 +3123,7 @@ async function runProjectCertify(args) {
|
|
|
3123
3123
|
if (manifest.creatorEntrypoints !== undefined || requiredSections.includes('creator-boundary')) {
|
|
3124
3124
|
sections.push(await certifyProjectCreatorBoundary(manifest, manifestDir));
|
|
3125
3125
|
}
|
|
3126
|
-
sections.push(await certifyProjectStatic(manifest, manifestDir, schemaStateFields));
|
|
3126
|
+
sections.push(await certifyProjectStatic(manifest, manifestDir, schemaStateFields, requiredSections.includes('static-determinism')));
|
|
3127
3127
|
const sectionFailures = sections.flatMap((section) => section.failures.map((failure) => `${section.section}: ${failure}`));
|
|
3128
3128
|
failures.push(...sectionFailures);
|
|
3129
3129
|
const skippedRequiredSections = requiredSections.filter((section) => !projectRequirementSatisfied(section, sections));
|
|
@@ -3267,7 +3267,9 @@ async function runProjectCheck(args) {
|
|
|
3267
3267
|
const browserJsonPath = readString(args, 'browser-json') ?? (browserRuntime === undefined ? undefined : join(outDir, `project-browser-proof-${browserRuntime}.json`));
|
|
3268
3268
|
process.exitCode = undefined;
|
|
3269
3269
|
await runProjectCertify(projectSubcommandArgs('project:certify', [manifestPath], {
|
|
3270
|
-
|
|
3270
|
+
// project:check is the deployable preflight, so it must certify a
|
|
3271
|
+
// file-backed simulation module even when the caller passes no --require.
|
|
3272
|
+
require: readString(args, 'require') ?? 'static-determinism',
|
|
3271
3273
|
frames: readString(args, 'frames'),
|
|
3272
3274
|
agents: readString(args, 'agents'),
|
|
3273
3275
|
slots: readString(args, 'slots'),
|
|
@@ -6258,9 +6260,27 @@ async function certifyProjectAnimations(manifest, manifestDir, frames) {
|
|
|
6258
6260
|
}
|
|
6259
6261
|
return sections;
|
|
6260
6262
|
}
|
|
6261
|
-
async function certifyProjectStatic(manifest, manifestDir, schemaStateFields) {
|
|
6263
|
+
async function certifyProjectStatic(manifest, manifestDir, schemaStateFields, required) {
|
|
6262
6264
|
const paths = manifest.staticPaths ?? [];
|
|
6263
6265
|
if (paths.length === 0) {
|
|
6266
|
+
// A deployable project must prove its simulation is a real, file-backed
|
|
6267
|
+
// module. When the caller requires this section but the manifest lists no
|
|
6268
|
+
// staticPaths, fail loudly with a pointed message instead of silently
|
|
6269
|
+
// skipping. Advisory runs (section not required) still skip.
|
|
6270
|
+
if (required) {
|
|
6271
|
+
return {
|
|
6272
|
+
section: 'static-determinism',
|
|
6273
|
+
result: 'fail',
|
|
6274
|
+
metrics: {
|
|
6275
|
+
checkedPaths: 0,
|
|
6276
|
+
diagnostics: 0,
|
|
6277
|
+
errors: 0,
|
|
6278
|
+
warnings: 0,
|
|
6279
|
+
parser: getDeterminismParserKind(),
|
|
6280
|
+
},
|
|
6281
|
+
failures: ['static-determinism requires at least one staticPaths entry'],
|
|
6282
|
+
};
|
|
6283
|
+
}
|
|
6264
6284
|
return skippedProjectSection('static-determinism');
|
|
6265
6285
|
}
|
|
6266
6286
|
const resolvedPaths = paths.map((path) => resolveManifestPath(manifestDir, path));
|
|
@@ -6484,6 +6504,14 @@ async function certifyProjectReplayRuntime(manifest, manifestDir, outDir, frames
|
|
|
6484
6504
|
projectAuthoredSystemFrames: authoredSystemFrames,
|
|
6485
6505
|
projectAuthoredSystemChecksum: authoredSystemChecksum,
|
|
6486
6506
|
frames: replayFrames,
|
|
6507
|
+
// Game identity for the M8.5 deploy-time verification manifest: the
|
|
6508
|
+
// cloud-run deploy interceptor keys the stored trusted manifest by
|
|
6509
|
+
// deterministicVersion and the score re-run gates on all three. They live
|
|
6510
|
+
// on the game descriptor (not the runtimeReplay sub-object), so they are
|
|
6511
|
+
// copied into the bundle metadata explicitly here.
|
|
6512
|
+
deterministicVersion: projectDescriptor?.deterministicVersion ?? '',
|
|
6513
|
+
inputSchemaId: projectDescriptor?.input.id ?? '',
|
|
6514
|
+
stateSchemaId: projectDescriptor?.state.id ?? '',
|
|
6487
6515
|
},
|
|
6488
6516
|
});
|
|
6489
6517
|
const verification = verifyDeterministicFrameRuntimeReplayBundle(bundle, {
|
|
@@ -8841,7 +8869,7 @@ async function runOfflineReplay(args) {
|
|
|
8841
8869
|
checksums: checksumCount,
|
|
8842
8870
|
mismatches: 0,
|
|
8843
8871
|
finalChecksum,
|
|
8844
|
-
replayRecordingMode: retainReplay ? 'memory' : '
|
|
8872
|
+
replayRecordingMode: retainReplay ? 'memory' : 'none',
|
|
8845
8873
|
replayBytes,
|
|
8846
8874
|
inputBytesPerTick: (inputBytes / frames).toFixed(2),
|
|
8847
8875
|
packedInputBytesPerTick: (packedInputBytes / frames).toFixed(2),
|
|
@@ -9271,7 +9299,7 @@ async function runReplayVerifyBrowser(args) {
|
|
|
9271
9299
|
return;
|
|
9272
9300
|
}
|
|
9273
9301
|
const page = await browser.newPage();
|
|
9274
|
-
await page.goto(`${baseUrl}/dist/
|
|
9302
|
+
await page.goto(`${baseUrl}/dist/testing.js`);
|
|
9275
9303
|
const result = await page.evaluate(async ({ moduleUrl, replay: browserReplay }) => {
|
|
9276
9304
|
const mod = (await import(moduleUrl));
|
|
9277
9305
|
return mod.verifyReplay({
|
|
@@ -9283,7 +9311,7 @@ async function runReplayVerifyBrowser(args) {
|
|
|
9283
9311
|
deterministicVersion: '0.0.0-source',
|
|
9284
9312
|
hydrateInput: mod.hydrateLabInput,
|
|
9285
9313
|
});
|
|
9286
|
-
}, { moduleUrl: `${baseUrl}/dist/
|
|
9314
|
+
}, { moduleUrl: `${baseUrl}/dist/testing.js`, replay });
|
|
9287
9315
|
const elapsedMs = performance.now() - startedAt;
|
|
9288
9316
|
const replayHash = defaultChecksum(replayText);
|
|
9289
9317
|
const browserBundleHash = defaultChecksum(await readFile(resolve(packageRoot, 'dist/browser.js'), 'utf8'));
|
|
@@ -9369,11 +9397,11 @@ async function runNumberBrowser(args) {
|
|
|
9369
9397
|
return;
|
|
9370
9398
|
}
|
|
9371
9399
|
const page = await browser.newPage();
|
|
9372
|
-
await page.goto(`${baseUrl}/dist/
|
|
9400
|
+
await page.goto(`${baseUrl}/dist/testing.js`);
|
|
9373
9401
|
const browserResult = await page.evaluate(async ({ moduleUrl, mode, frames, seed }) => {
|
|
9374
9402
|
const mod = (await import(moduleUrl));
|
|
9375
9403
|
return mod.runNumberLab({ mode, frames, seed });
|
|
9376
|
-
}, { moduleUrl: `${baseUrl}/dist/
|
|
9404
|
+
}, { moduleUrl: `${baseUrl}/dist/testing.js`, mode, frames, seed });
|
|
9377
9405
|
const elapsedMs = performance.now() - startedAt;
|
|
9378
9406
|
const checksumMatch = nodeResult.finalChecksum === browserResult.finalChecksum;
|
|
9379
9407
|
const valueMatch = Object.is(nodeResult.finalValue, browserResult.finalValue);
|
|
@@ -9453,11 +9481,11 @@ async function runEngineCoreBrowser(args) {
|
|
|
9453
9481
|
return;
|
|
9454
9482
|
}
|
|
9455
9483
|
const page = await browser.newPage();
|
|
9456
|
-
await page.goto(`${baseUrl}/dist/
|
|
9484
|
+
await page.goto(`${baseUrl}/dist/testing.js`);
|
|
9457
9485
|
const browserProof = await page.evaluate(async ({ moduleUrl, players, frames, seed }) => {
|
|
9458
9486
|
const mod = (await import(moduleUrl));
|
|
9459
9487
|
return mod.runDeterministicEngineCoreNumericProof({ players, frames, seed });
|
|
9460
|
-
}, { moduleUrl: `${baseUrl}/dist/
|
|
9488
|
+
}, { moduleUrl: `${baseUrl}/dist/testing.js`, players, frames, seed });
|
|
9461
9489
|
const elapsedMs = performance.now() - startedAt;
|
|
9462
9490
|
const browserBundleHash = defaultChecksum(await readFile(resolve(packageRoot, 'dist/browser.js'), 'utf8'));
|
|
9463
9491
|
const packageBuildHash = defaultChecksum(await readFile(resolve(packageRoot, 'dist/index.js'), 'utf8'));
|
|
@@ -10805,8 +10833,8 @@ function readOptionalProjectBrowserRuntime(args) {
|
|
|
10805
10833
|
}
|
|
10806
10834
|
function readPhysicsCertEngine(args) {
|
|
10807
10835
|
const engine = readString(args, 'engine') ?? 'builtin-fixed';
|
|
10808
|
-
if (engine !== 'builtin-fixed' && engine !== 'rapier2d-compat') {
|
|
10809
|
-
throw new Error('--engine must be builtin-fixed or rapier2d-compat');
|
|
10836
|
+
if (engine !== 'builtin-fixed' && engine !== 'builtin-rigid' && engine !== 'rapier2d-compat') {
|
|
10837
|
+
throw new Error('--engine must be builtin-fixed, builtin-rigid, or rapier2d-compat');
|
|
10810
10838
|
}
|
|
10811
10839
|
return engine;
|
|
10812
10840
|
}
|
package/dist/collision.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DeterministicMath } from './math.js';
|
|
1
2
|
export type FixedVec2 = {
|
|
2
3
|
readonly x: number;
|
|
3
4
|
readonly y: number;
|
|
@@ -23,3 +24,14 @@ export declare function translateAabb(aabb: FixedAabb, delta: FixedVec2): FixedA
|
|
|
23
24
|
export declare function sortAabbsByStableId<T extends {
|
|
24
25
|
readonly id: string;
|
|
25
26
|
}>(items: readonly T[]): readonly T[];
|
|
27
|
+
/**
|
|
28
|
+
* Orientation of the ordered triple (a, b, c): +1 counter-clockwise, -1
|
|
29
|
+
* clockwise, 0 collinear. Pure integer cross-product; scale-free.
|
|
30
|
+
*/
|
|
31
|
+
export declare function orientation(a: FixedVec2, b: FixedVec2, c: FixedVec2): -1 | 0 | 1;
|
|
32
|
+
/** True when p lies within the axis-aligned bounding box of segment [a, b]. */
|
|
33
|
+
export declare function pointOnSegment(p: FixedVec2, a: FixedVec2, b: FixedVec2): boolean;
|
|
34
|
+
export declare function segmentsIntersect(a0: FixedVec2, a1: FixedVec2, b0: FixedVec2, b1: FixedVec2): boolean;
|
|
35
|
+
export declare function closestPointOnSegment(p: FixedVec2, a: FixedVec2, b: FixedVec2, math: DeterministicMath): FixedVec2;
|
|
36
|
+
export declare function segmentPointDistance(p: FixedVec2, a: FixedVec2, b: FixedVec2, math: DeterministicMath): number;
|
|
37
|
+
export declare function segmentSegmentDistance(a0: FixedVec2, a1: FixedVec2, b0: FixedVec2, b1: FixedVec2, math: DeterministicMath): number;
|
package/dist/collision.js
CHANGED
|
@@ -83,3 +83,55 @@ function assertInteger(value, name) {
|
|
|
83
83
|
throw new Error(`${name} must be an integer fixed-point value`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Orientation of the ordered triple (a, b, c): +1 counter-clockwise, -1
|
|
88
|
+
* clockwise, 0 collinear. Pure integer cross-product; scale-free.
|
|
89
|
+
*/
|
|
90
|
+
export function orientation(a, b, c) {
|
|
91
|
+
const v = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
92
|
+
return v > 0 ? 1 : v < 0 ? -1 : 0;
|
|
93
|
+
}
|
|
94
|
+
/** True when p lies within the axis-aligned bounding box of segment [a, b]. */
|
|
95
|
+
export function pointOnSegment(p, a, b) {
|
|
96
|
+
return (p.x >= Math.min(a.x, b.x) &&
|
|
97
|
+
p.x <= Math.max(a.x, b.x) &&
|
|
98
|
+
p.y >= Math.min(a.y, b.y) &&
|
|
99
|
+
p.y <= Math.max(a.y, b.y));
|
|
100
|
+
}
|
|
101
|
+
export function segmentsIntersect(a0, a1, b0, b1) {
|
|
102
|
+
const d1 = orientation(a0, a1, b0);
|
|
103
|
+
const d2 = orientation(a0, a1, b1);
|
|
104
|
+
const d3 = orientation(b0, b1, a0);
|
|
105
|
+
const d4 = orientation(b0, b1, a1);
|
|
106
|
+
if (d1 === 0 && pointOnSegment(b0, a0, a1))
|
|
107
|
+
return true;
|
|
108
|
+
if (d2 === 0 && pointOnSegment(b1, a0, a1))
|
|
109
|
+
return true;
|
|
110
|
+
if (d3 === 0 && pointOnSegment(a0, b0, b1))
|
|
111
|
+
return true;
|
|
112
|
+
if (d4 === 0 && pointOnSegment(a1, b0, b1))
|
|
113
|
+
return true;
|
|
114
|
+
return d1 !== d2 && d3 !== d4;
|
|
115
|
+
}
|
|
116
|
+
export function closestPointOnSegment(p, a, b, math) {
|
|
117
|
+
const abx = b.x - a.x;
|
|
118
|
+
const aby = b.y - a.y;
|
|
119
|
+
const abLenSq = math.mul(abx, abx) + math.mul(aby, aby);
|
|
120
|
+
if (abLenSq === 0) {
|
|
121
|
+
return { x: a.x, y: a.y };
|
|
122
|
+
}
|
|
123
|
+
const apx = p.x - a.x;
|
|
124
|
+
const apy = p.y - a.y;
|
|
125
|
+
const t = math.clamp(math.div(math.mul(apx, abx) + math.mul(apy, aby), abLenSq), 0, math.fixedScale);
|
|
126
|
+
return { x: a.x + math.mul(abx, t), y: a.y + math.mul(aby, t) };
|
|
127
|
+
}
|
|
128
|
+
export function segmentPointDistance(p, a, b, math) {
|
|
129
|
+
const c = closestPointOnSegment(p, a, b, math);
|
|
130
|
+
return math.hypot(p.x - c.x, p.y - c.y);
|
|
131
|
+
}
|
|
132
|
+
export function segmentSegmentDistance(a0, a1, b0, b1, math) {
|
|
133
|
+
if (segmentsIntersect(a0, a1, b0, b1)) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
return math.min(math.min(segmentPointDistance(a0, b0, b1, math), segmentPointDistance(a1, b0, b1, math)), math.min(segmentPointDistance(b0, a0, a1, math), segmentPointDistance(b1, a0, a1, math)));
|
|
137
|
+
}
|
package/dist/creator.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { createSessionFromPromotionSnapshot, exportPromotionSnapshot } from './s
|
|
|
7
7
|
export { verifyReplay, verifyReplayWithDescriptor } from './replay.js';
|
|
8
8
|
export { defineSyncplayGame } from './schema.js';
|
|
9
9
|
export { createSchemaArtifacts } from './schema-artifacts.js';
|
|
10
|
-
export { createSyncplayOfflineRuntime } from './sdk-offline.js';
|
|
10
|
+
export { createSyncplayOfflineRuntime, createSyncplayOfflineRuntime as createSyncplayGameRuntime } from './sdk-offline.js';
|
|
11
11
|
export { runSyncplaySynctest } from './synctest.js';
|
|
12
12
|
export type { SyncplaySynctestConfig, SyncplaySynctestDivergence, SyncplaySynctestInputFuzz, SyncplaySynctestPhase, SyncplaySynctestResult, } from './synctest.js';
|
|
13
13
|
export type { CanonicalValue } from './canonical.js';
|
package/dist/creator.js
CHANGED
|
@@ -7,5 +7,5 @@ export { createSessionFromPromotionSnapshot, exportPromotionSnapshot } from './s
|
|
|
7
7
|
export { verifyReplay, verifyReplayWithDescriptor } from './replay.js';
|
|
8
8
|
export { defineSyncplayGame } from './schema.js';
|
|
9
9
|
export { createSchemaArtifacts } from './schema-artifacts.js';
|
|
10
|
-
export { createSyncplayOfflineRuntime } from './sdk-offline.js';
|
|
10
|
+
export { createSyncplayOfflineRuntime, createSyncplayOfflineRuntime as createSyncplayGameRuntime } from './sdk-offline.js';
|
|
11
11
|
export { runSyncplaySynctest } from './synctest.js';
|
package/dist/engine-complete.js
CHANGED
|
@@ -17,7 +17,8 @@ import { runDeterministicLocalAuthorityWireFixture } from './local-authority-wir
|
|
|
17
17
|
import { runDeterministicMulticlientAuthorityFixture } from './multiclient-authority.js';
|
|
18
18
|
import { runDeterministicKcc3DAdversarialFixture, runDeterministicKcc3DCallbackFilterFixture, runDeterministicKcc3DCrowdFixture, runDeterministicKcc3DFilterForceFixture, runDeterministicKcc3DGeometryFixture, stepDeterministicKcc3D } from './movement3d.js';
|
|
19
19
|
import { cookDeterministicNavmesh, findDeterministicNavmeshPath, runDeterministicNavmeshImportTraversalFixture, runDeterministicNavmeshLayered3DFixture, runDeterministicNavmeshStress, runDeterministicNavmeshVelocityObstacleFixture, stepDeterministicNavmeshAgents } from './navmesh.js';
|
|
20
|
-
import { createDeterministicPhysicsWorld3D, runDeterministicPhysics3DCallbackMaskFixture, runDeterministicPhysics3DDetailFixture, runDeterministicPhysics3DMaterialFixture, runDeterministicPhysics3DQueryOptionsFixture, runDeterministicPhysics3DStackFixture, runDeterministicPhysics3DStress, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
|
|
20
|
+
import { createDeterministicPhysicsWorld3D, runDeterministicPhysics3DCallbackMaskFixture, runDeterministicPhysics3DDetailFixture, runDeterministicPhysics3DMaterialFixture, runDeterministicPhysics3DQueryOptionsFixture, runDeterministicPhysics3DStackFixture, runDeterministicPhysics3DStress, applyDeterministicImpulses3D, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
|
|
21
|
+
import { stepDeterministicVehicle3D, } from './physics3d-vehicle.js';
|
|
21
22
|
import { runDeterministicSignalFixture } from './signals.js';
|
|
22
23
|
import { runDeterministicSparseSetFixture } from './sparse-set.js';
|
|
23
24
|
import { runDeterministicLifecycleFixture } from './system-lifecycle.js';
|
|
@@ -1217,6 +1218,11 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
|
|
|
1217
1218
|
const lagComp3DApi = runDeterministicLagCompensation3DApiFixture();
|
|
1218
1219
|
const authoritativeLagComp3D = runDeterministicAuthoritativeLagCompensation3DFixture();
|
|
1219
1220
|
const fullStep = runPhysics3DFullStepMeasurementFixture(Math.min(requestedFrames, 300));
|
|
1221
|
+
// 128-dynamic-body interacting-pile timing sample. Only sampled on long
|
|
1222
|
+
// benchmark runs (>= 2000 frames) so its fixed wall-clock cost amortizes below
|
|
1223
|
+
// the per-frame mean budget; short ad-hoc runs skip it (gates default true).
|
|
1224
|
+
const rigidPile = benchmark && requestedFrames >= 2000 ? runPhysics3DRigidPileMeasurementFixture(128, 200) : undefined;
|
|
1225
|
+
const vehicleFleet = benchmark && requestedFrames >= 2000 ? runVehicle3DFleetMeasurementFixture(16, 200) : undefined;
|
|
1220
1226
|
const highBodySample512 = requestedFrames > 512 ? runDeterministicPhysics3DStress(512, 128) : undefined;
|
|
1221
1227
|
const highBodySample2048 = requestedFrames > 512 ? runDeterministicPhysics3DStress(2048, 32) : undefined;
|
|
1222
1228
|
return categoryReport('physics3d', defaultChecksum({
|
|
@@ -1265,11 +1271,15 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
|
|
|
1265
1271
|
&& stack.settledDynamicBodies === stack.stackLayers,
|
|
1266
1272
|
stackRollbackEquivalent: stack.rollbackEquivalent,
|
|
1267
1273
|
stackAuthoringOrderStable: stack.stableUnderAuthoringOrderPermutation,
|
|
1274
|
+
// Rigid-solver material response (impulse-based) — combine semantics unchanged
|
|
1275
|
+
// (floor-average friction, max restitution); the bounce/friction velocity
|
|
1276
|
+
// magnitudes come from the sequential-impulse solver, not the legacy pusher.
|
|
1268
1277
|
materialResponseCovered: materials.combinedFriction === 3
|
|
1269
1278
|
&& materials.combinedRestitution === 7
|
|
1270
|
-
&& materials.normalVelocityAfterBounce
|
|
1271
|
-
&& materials.tangentVelocityAfterFrictionY
|
|
1272
|
-
&& materials.tangentVelocityAfterFrictionZ
|
|
1279
|
+
&& materials.normalVelocityAfterBounce > 0
|
|
1280
|
+
&& materials.tangentVelocityAfterFrictionY < 10
|
|
1281
|
+
&& materials.tangentVelocityAfterFrictionZ > -10
|
|
1282
|
+
&& materials.tangentVelocityAfterFrictionZ < 0,
|
|
1273
1283
|
materialRollbackEquivalent: materials.rollbackEquivalent,
|
|
1274
1284
|
callbackLifecycleCovered: callbackMasks.collisionEnter > 0
|
|
1275
1285
|
&& callbackMasks.collisionStay > 0
|
|
@@ -1309,6 +1319,17 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
|
|
|
1309
1319
|
&& fullStep.p95MeasuredStepMs > 0
|
|
1310
1320
|
&& fullStep.p99MeasuredStepMs > 0,
|
|
1311
1321
|
fullStepRollbackEquivalent: fullStep.rollbackEquivalent,
|
|
1322
|
+
rigidPileMeasuredTimingCovered: rigidPile === undefined ? true : rigidPile.bodies === 128 && rigidPile.p99MeasuredStepMs > 0,
|
|
1323
|
+
rigidPileRollbackEquivalent: rigidPile === undefined ? true : rigidPile.rollbackEquivalent,
|
|
1324
|
+
vehicleFleetMeasuredTimingCovered: vehicleFleet === undefined
|
|
1325
|
+
? true
|
|
1326
|
+
: vehicleFleet.vehicles === 16 && vehicleFleet.groundedWheelSamples > 0 && vehicleFleet.p99MeasuredStepMs > 0,
|
|
1327
|
+
vehicleFleetRollbackEquivalent: vehicleFleet === undefined ? true : vehicleFleet.rollbackEquivalent,
|
|
1328
|
+
vehicleFleetP99WithinBudget: enforceThresholds && vehicleFleet !== undefined ? vehicleFleet.p99MeasuredStepMs <= 25 : true,
|
|
1329
|
+
// Measured wall-clock (never in checksummed state). The pooled clip scratch
|
|
1330
|
+
// + constraint pool (provenance pass 6) pulled the burst p99 to ~1.5 ms
|
|
1331
|
+
// locally; the 10 ms gate keeps generous headroom for CI variance.
|
|
1332
|
+
rigidPileP99WithinBudget: enforceThresholds && rigidPile !== undefined ? rigidPile.p99MeasuredStepMs <= 10 : true,
|
|
1312
1333
|
fullStepContactsAndQueriesCovered: fullStep.contactCount > 0 && fullStep.queryCount === fullStep.frames,
|
|
1313
1334
|
executedRequestedFrames: metrics.frames === requestedFrames,
|
|
1314
1335
|
...(metrics.extrapolatedStress ? {} : {
|
|
@@ -1435,6 +1456,13 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
|
|
|
1435
1456
|
highBodySample512Frames: highBodySample512?.frames ?? 0,
|
|
1436
1457
|
highBodySample2048Checksum: highBodySample2048?.checksum ?? '',
|
|
1437
1458
|
highBodySample2048Frames: highBodySample2048?.frames ?? 0,
|
|
1459
|
+
vehicleFleetChecksum: vehicleFleet?.checksum ?? '',
|
|
1460
|
+
vehicleFleetVehicles: vehicleFleet?.vehicles ?? 0,
|
|
1461
|
+
vehicleFleetFrames: vehicleFleet?.frames ?? 0,
|
|
1462
|
+
vehicleFleetGroundedWheelSamples: vehicleFleet?.groundedWheelSamples ?? 0,
|
|
1463
|
+
vehicleFleetP95MeasuredStepMs: vehicleFleet?.p95MeasuredStepMs ?? 0,
|
|
1464
|
+
vehicleFleetP99MeasuredStepMs: vehicleFleet?.p99MeasuredStepMs ?? 0,
|
|
1465
|
+
vehicleFleetMaxMeasuredStepMs: vehicleFleet?.maxMeasuredStepMs ?? 0,
|
|
1438
1466
|
}, requestedFrames, metrics.frames));
|
|
1439
1467
|
}
|
|
1440
1468
|
function kccCategory(players, requestedFrames, executedFrames, benchmark, noFastStress) {
|
|
@@ -1616,6 +1644,188 @@ function runPhysics3DFullStepMeasurementFixture(frames) {
|
|
|
1616
1644
|
wallClockElapsedMs: roundMetric(engineCompleteNowMs() - startedAt, 4),
|
|
1617
1645
|
};
|
|
1618
1646
|
}
|
|
1647
|
+
function createRigidPileBodies3D(bodies) {
|
|
1648
|
+
const out = [{
|
|
1649
|
+
id: 'pile-floor', kind: 'static', x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0,
|
|
1650
|
+
shape: { type: 'box', halfX: 40, halfY: 0.5, halfZ: 40 }, layer: 1, mask: 1,
|
|
1651
|
+
material: { friction: 8, restitution: 0 },
|
|
1652
|
+
}];
|
|
1653
|
+
const perRow = 8;
|
|
1654
|
+
for (let index = 0; index < bodies; index += 1) {
|
|
1655
|
+
const column = index % perRow;
|
|
1656
|
+
const rowInLayer = Math.floor(index / perRow) % perRow;
|
|
1657
|
+
const layer = Math.floor(index / (perRow * perRow));
|
|
1658
|
+
out.push({
|
|
1659
|
+
id: `pile-${String(index).padStart(3, '0')}`,
|
|
1660
|
+
kind: 'dynamic',
|
|
1661
|
+
x: (column - perRow / 2) * 1.1,
|
|
1662
|
+
y: 1 + layer * 1.05,
|
|
1663
|
+
z: (rowInLayer - perRow / 2) * 1.1,
|
|
1664
|
+
vx: 0, vy: 0, vz: 0, mass: 1,
|
|
1665
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 }, angularVel: { x: 0, y: 0, z: 0 },
|
|
1666
|
+
shape: { type: 'box', halfX: 0.5, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1,
|
|
1667
|
+
material: { friction: 8, restitution: 0 },
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* 128-dynamic-body interacting-pile step-cost sample, timed via wall clock
|
|
1674
|
+
* (never enters checksummed state). CI-gated at measured p99 <= 10 ms
|
|
1675
|
+
* (rigidPileP99WithinBudget) — headroom over the ~1.5 ms local burst cost
|
|
1676
|
+
* after the pooled clip scratch + constraint pool (provenance pass 6).
|
|
1677
|
+
*/
|
|
1678
|
+
function runPhysics3DRigidPileMeasurementFixture(bodies, frames) {
|
|
1679
|
+
const initial = createDeterministicPhysicsWorld3D(createRigidPileBodies3D(bodies));
|
|
1680
|
+
let world = initial;
|
|
1681
|
+
let snapshot = initial;
|
|
1682
|
+
const timings = [];
|
|
1683
|
+
const half = Math.floor(frames / 2);
|
|
1684
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
1685
|
+
if (frame === half) {
|
|
1686
|
+
snapshot = world;
|
|
1687
|
+
}
|
|
1688
|
+
const stepStartedAt = engineCompleteNowMs();
|
|
1689
|
+
world = stepDeterministicPhysicsWorld3D(world, { gravityY: -0.02, computeChecksum: false }).world;
|
|
1690
|
+
timings.push(engineCompleteNowMs() - stepStartedAt);
|
|
1691
|
+
}
|
|
1692
|
+
const checksum = defaultChecksum(world);
|
|
1693
|
+
let restored = snapshot;
|
|
1694
|
+
for (let frame = half; frame < frames; frame += 1) {
|
|
1695
|
+
restored = stepDeterministicPhysicsWorld3D(restored, { gravityY: -0.02, computeChecksum: false }).world;
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
checksum,
|
|
1699
|
+
rollbackEquivalent: checksum === defaultChecksum(restored),
|
|
1700
|
+
bodies: world.bodies.filter((body) => body.kind === 'dynamic').length,
|
|
1701
|
+
frames,
|
|
1702
|
+
p95MeasuredStepMs: roundMetric(percentileNumber(timings, 95), 6),
|
|
1703
|
+
p99MeasuredStepMs: roundMetric(percentileNumber(timings, 99), 6),
|
|
1704
|
+
maxMeasuredStepMs: roundMetric(timings.reduce((max, value) => Math.max(max, value), 0), 6),
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function fleetVehicleWheel(localAnchor, steered) {
|
|
1708
|
+
return {
|
|
1709
|
+
localAnchor,
|
|
1710
|
+
suspensionDir: { x: 0, y: -1, z: 0 },
|
|
1711
|
+
suspensionRestLength: 0.6,
|
|
1712
|
+
suspensionMaxTravel: 0.3,
|
|
1713
|
+
radius: 0.3,
|
|
1714
|
+
stiffness: 60,
|
|
1715
|
+
damping: 220,
|
|
1716
|
+
grip: 1,
|
|
1717
|
+
maxSteerAngleTurns: 0.1,
|
|
1718
|
+
isSteered: steered,
|
|
1719
|
+
isDriven: steered,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Measured wall-clock sample of an N-car raycast-vehicle fleet running the full
|
|
1724
|
+
* controller→impulse→physics loop per frame. Timings never enter checksummed
|
|
1725
|
+
* state; the p99 is gated within the global 25 ms budget.
|
|
1726
|
+
*/
|
|
1727
|
+
function runVehicle3DFleetMeasurementFixture(vehicleCount, frames) {
|
|
1728
|
+
const bodies = [{
|
|
1729
|
+
id: 'fleet-ground',
|
|
1730
|
+
kind: 'static',
|
|
1731
|
+
x: 0,
|
|
1732
|
+
y: 0,
|
|
1733
|
+
z: 0,
|
|
1734
|
+
vx: 0,
|
|
1735
|
+
vy: 0,
|
|
1736
|
+
vz: 0,
|
|
1737
|
+
shape: { type: 'box', halfX: 200, halfY: 0.5, halfZ: 200 },
|
|
1738
|
+
layer: 1,
|
|
1739
|
+
mask: 1,
|
|
1740
|
+
material: { friction: 8, restitution: 0 },
|
|
1741
|
+
}];
|
|
1742
|
+
const vehicles = [];
|
|
1743
|
+
for (let index = 0; index < vehicleCount; index += 1) {
|
|
1744
|
+
const id = `fleet-car-${index}`;
|
|
1745
|
+
bodies.push({
|
|
1746
|
+
id,
|
|
1747
|
+
kind: 'dynamic',
|
|
1748
|
+
x: (index % 4) * 12 - 18,
|
|
1749
|
+
y: 2.55,
|
|
1750
|
+
z: Math.floor(index / 4) * 12 - 18,
|
|
1751
|
+
vx: 0,
|
|
1752
|
+
vy: 0,
|
|
1753
|
+
vz: 0,
|
|
1754
|
+
mass: 1200,
|
|
1755
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
1756
|
+
angularVel: { x: 0, y: 0, z: 0 },
|
|
1757
|
+
shape: { type: 'box', halfX: 1, halfY: 0.25, halfZ: 2 },
|
|
1758
|
+
layer: 1,
|
|
1759
|
+
mask: 1,
|
|
1760
|
+
});
|
|
1761
|
+
vehicles.push({
|
|
1762
|
+
chassisId: id,
|
|
1763
|
+
driveForce: 12,
|
|
1764
|
+
brakeForce: 40,
|
|
1765
|
+
wheels: [
|
|
1766
|
+
fleetVehicleWheel({ x: -0.9, y: -0.25, z: 1.5 }, true),
|
|
1767
|
+
fleetVehicleWheel({ x: 0.9, y: -0.25, z: 1.5 }, true),
|
|
1768
|
+
fleetVehicleWheel({ x: -0.9, y: -0.25, z: -1.5 }, false),
|
|
1769
|
+
fleetVehicleWheel({ x: 0.9, y: -0.25, z: -1.5 }, false),
|
|
1770
|
+
],
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
const stepFleetFrame = (world, frame) => {
|
|
1774
|
+
// every controller reads the same pre-impulse world (pure reads), then all
|
|
1775
|
+
// impulses land in one batched canonical rebuild — the recommended frame
|
|
1776
|
+
// shape for multi-vehicle games
|
|
1777
|
+
let groundedWheels = 0;
|
|
1778
|
+
const frameImpulses = [];
|
|
1779
|
+
for (let index = 0; index < vehicles.length; index += 1) {
|
|
1780
|
+
const input = {
|
|
1781
|
+
throttle: 10,
|
|
1782
|
+
steer: (Math.trunc(frame / 24) + index) % 2 === 0 ? 10 : -10,
|
|
1783
|
+
brake: 0,
|
|
1784
|
+
};
|
|
1785
|
+
const result = stepDeterministicVehicle3D(vehicles[index], input, world, { dtTicks: 1 });
|
|
1786
|
+
groundedWheels += result.wheels.filter((wheel) => wheel.grounded).length;
|
|
1787
|
+
for (const impulse of result.impulses) {
|
|
1788
|
+
frameImpulses.push({ bodyId: impulse.bodyId, impulse: impulse.impulse, worldPoint: impulse.point });
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
const next = stepDeterministicPhysicsWorld3D(applyDeterministicImpulses3D(world, frameImpulses), {
|
|
1792
|
+
gravityY: -0.02,
|
|
1793
|
+
computeChecksum: false,
|
|
1794
|
+
}).world;
|
|
1795
|
+
return { world: next, groundedWheels };
|
|
1796
|
+
};
|
|
1797
|
+
const initial = createDeterministicPhysicsWorld3D(bodies);
|
|
1798
|
+
let world = initial;
|
|
1799
|
+
let snapshot = initial;
|
|
1800
|
+
let groundedWheelSamples = 0;
|
|
1801
|
+
const timings = [];
|
|
1802
|
+
const half = Math.floor(frames / 2);
|
|
1803
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
1804
|
+
if (frame === half) {
|
|
1805
|
+
snapshot = world;
|
|
1806
|
+
}
|
|
1807
|
+
const stepStartedAt = engineCompleteNowMs();
|
|
1808
|
+
const stepped = stepFleetFrame(world, frame);
|
|
1809
|
+
timings.push(engineCompleteNowMs() - stepStartedAt);
|
|
1810
|
+
world = stepped.world;
|
|
1811
|
+
groundedWheelSamples += stepped.groundedWheels;
|
|
1812
|
+
}
|
|
1813
|
+
const checksum = defaultChecksum(world);
|
|
1814
|
+
let restored = snapshot;
|
|
1815
|
+
for (let frame = half; frame < frames; frame += 1) {
|
|
1816
|
+
restored = stepFleetFrame(restored, frame).world;
|
|
1817
|
+
}
|
|
1818
|
+
return {
|
|
1819
|
+
checksum,
|
|
1820
|
+
rollbackEquivalent: checksum === defaultChecksum(restored),
|
|
1821
|
+
vehicles: vehicles.length,
|
|
1822
|
+
frames,
|
|
1823
|
+
groundedWheelSamples,
|
|
1824
|
+
p95MeasuredStepMs: roundMetric(percentileNumber(timings, 95), 6),
|
|
1825
|
+
p99MeasuredStepMs: roundMetric(percentileNumber(timings, 99), 6),
|
|
1826
|
+
maxMeasuredStepMs: roundMetric(timings.reduce((max, value) => Math.max(max, value), 0), 6),
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1619
1829
|
function runKcc3DFullStepMeasurementFixture(players, frames) {
|
|
1620
1830
|
let bodies = Array.from({ length: players }, (_, slot) => ({
|
|
1621
1831
|
x: slot % 16,
|
|
@@ -617,19 +617,34 @@ const engineParityMatrixEntries = [
|
|
|
617
617
|
currentEvidence: [
|
|
618
618
|
'packages/syncplay/src/physics2d.ts',
|
|
619
619
|
'packages/syncplay/src/physics3d.ts',
|
|
620
|
+
'packages/syncplay/src/physics3d-solver.ts',
|
|
621
|
+
'packages/syncplay/src/physics3d-joints.ts',
|
|
620
622
|
'packages/syncplay/tests/physics2d.test.ts',
|
|
623
|
+
'packages/syncplay/tests/physics3d-solver.test.ts',
|
|
624
|
+
'packages/syncplay/tests/physics3d-joints.test.ts',
|
|
625
|
+
'packages/syncplay/tests/physics3d-ccd.test.ts',
|
|
621
626
|
'packages/syncplay/tests/engine-complete.test.ts',
|
|
627
|
+
'packages/syncplay/src/physics3d-vehicle.ts',
|
|
628
|
+
'packages/syncplay/tests/physics3d-vehicle.test.ts',
|
|
629
|
+
'warm-started sequential-impulse solver: stable stacking, tumbling, sleeping, axis locks, joints (ball/hinge/weld/rope/grab), speculative-contact CCD, compound colliders, orientation-aware queries',
|
|
630
|
+
'builtin-rigid physics-cert lane passes all 4 determinism gates; contact/joint caches are checksummed rollback state',
|
|
631
|
+
'raycast-wheel vehicle controller (stepDeterministicVehicle3D): spring-damper suspension, friction-circle drive/steer/brake, fixed-point steering trig; certified via the physics-rigid-vehicle fixture, the builtin-rigid arena car, and a rollback-every-frame synctest scene',
|
|
632
|
+
'per-world static AABB-tree query index: raycast/overlap/shape-cast are O(log n + hits) with bit-identical results (oracle-tested; cert-lane checksums unchanged); batched applyDeterministicImpulses3D applies a frame of controller impulses in one canonical rebuild',
|
|
633
|
+
'pooled narrow-phase/constraint scratch (scalarized box face-clip over ping-pong clip slots, step-scoped constraint pool) pulled the 128-body pile impact-burst p99 from 2.06 ms to 1.54 ms (p50 0.85 → 0.72 ms, median-of-5); prismatic/breakable/collision-suppressed joints, a soft frequencyHz/dampingRatio spring joint, and per-body gravityScale round out the joint/body surface — joint breaks are sticky checksummed cache state',
|
|
622
634
|
],
|
|
623
635
|
cliProofs: [
|
|
624
636
|
'npm run lab:physics3d -- --bodies 128 --frames 1000 --no-fast-stress --json artifacts/reports/physics3d-no-fast.json',
|
|
625
637
|
'npm run certify:engine-complete -- --filter physics3d --players 128 --frames 5000 --no-fast-stress',
|
|
638
|
+
'npm run lab:physics-cert -- --engine builtin-rigid --shape fixed-arena --players 4 --frames 600 --seed 7 --snapshot-every 8',
|
|
639
|
+
'npm run coverage:package:physics-vehicle',
|
|
626
640
|
],
|
|
627
641
|
gaps: [
|
|
628
|
-
'
|
|
629
|
-
'
|
|
642
|
+
'Vehicles are raycast-wheel only (static ground, flat/axis-aligned contact normals); moving-ground reaction and rigid-wheel vehicles are tracked follow-ons.',
|
|
643
|
+
'Dynamic mesh-vs-mesh collision, convex-hull decomposition / visual collider import, and ragdolls-in-checksummed-state remain out of scope.',
|
|
644
|
+
'Cross-VM certification posture unchanged (V8↔V8 certified; WebKit nightly; Hermes documented gap).',
|
|
630
645
|
],
|
|
631
646
|
nextCliWork: [
|
|
632
|
-
'
|
|
647
|
+
'Profile the remaining burst cost before further pooling; no known single lever.',
|
|
633
648
|
],
|
|
634
649
|
referenceDocs: [
|
|
635
650
|
'external-deterministic-engine-reference:manual/physics/overview',
|
|
@@ -793,7 +808,7 @@ const engineParityMatrixEntries = [
|
|
|
793
808
|
'Integrated samples need stronger per-scene mechanics assertions and replay artifacts.',
|
|
794
809
|
],
|
|
795
810
|
nextCliWork: [
|
|
796
|
-
'Add CLI-rendered or artifact-traced Simple FPS, platform shooter, arena brawler, nav agents, animator, KCC playground, vehicle, and waiting-room samples.',
|
|
811
|
+
'Add CLI-rendered or artifact-traced Simple FPS, platform shooter, arena brawler, nav agents, animator, KCC playground, vehicle (the sample-scenes vehicle is kinematic by design; raycast-wheel physics lives in physics3d-vehicle.ts), and waiting-room samples.',
|
|
797
812
|
],
|
|
798
813
|
referenceDocs: [
|
|
799
814
|
'external-deterministic-engine-reference:game-samples/engine-simple-fps/overview',
|