@driftengine/physics 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
package/dist/stage.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The island solve's state, mirrored into one buffer a worker can see.
|
|
3
|
+
*
|
|
4
|
+
* **This is a copy and not a re-allocation of the live sets, and that is a deliberate reversal of
|
|
5
|
+
* what the design document proposed.** The first plan gave `BodySet`, `ContactConstraints`,
|
|
6
|
+
* `JointSet` and `IslandSet` an injected allocator so their arrays could be `SharedArrayBuffer`
|
|
7
|
+
* directly, with no copy at all. Two things argued against it.
|
|
8
|
+
*
|
|
9
|
+
* The first is arithmetic, and it was measured rather than estimated after this comment first
|
|
10
|
+
* claimed "under a tenth of one per cent" from a back-of-envelope that was wrong by more than an
|
|
11
|
+
* order of magnitude. `StagedExecutor` against `SerialExecutor` on one machine, 2026-09-05:
|
|
12
|
+
*
|
|
13
|
+
* | stacks of ten | serial | staged | copy |
|
|
14
|
+
* |---|---|---|---|
|
|
15
|
+
* | 16 (160 bodies) | 3,202 us | 3,331 us | **4.0%** |
|
|
16
|
+
* | 64 (640 bodies) | 13,381 us | 13,187 us | **within noise, and negative** |
|
|
17
|
+
* | 256 (2,560 bodies) | 60,951 us | 61,987 us | **1.7%** |
|
|
18
|
+
*
|
|
19
|
+
* So the round trip is one to four per cent of a tick and not a tenth of one, which is still small
|
|
20
|
+
* against a pool measured at 1.26x on the same scene but is not nothing, and the 4% at the smallest
|
|
21
|
+
* size is the shape to watch: the copy is `O(capacity)` while the solve is `O(work)`, so a world of
|
|
22
|
+
* many bodies doing little is where this trade is worst.
|
|
23
|
+
*
|
|
24
|
+
* The second is blast radius. The allocator plan edited four classes that 524 tests stand on, for a
|
|
25
|
+
* saving that does not show up. This edits none of them.
|
|
26
|
+
*
|
|
27
|
+
* **Nothing here maintains a list of fields.** `lanesOf` reflects over a set and takes every typed
|
|
28
|
+
* array it finds, so a field added to `BodySet` tomorrow is staged tomorrow with nothing to
|
|
29
|
+
* remember. `stage.test.ts` asserts that reflection against each class, because a mirror that
|
|
30
|
+
* quietly omits a lane would be a defect that ships green: the solve would read a stale value and
|
|
31
|
+
* the world would drift only where that lane mattered.
|
|
32
|
+
*/
|
|
33
|
+
import type { IslandSolveState } from './islandSolve.ts';
|
|
34
|
+
import type { Executor, IslandSolver } from './executor.ts';
|
|
35
|
+
/** Every typed array kind the four sets use. */
|
|
36
|
+
type Lane = Float32Array<ArrayBufferLike> | Float64Array<ArrayBufferLike> | Int32Array<ArrayBufferLike> | Uint32Array<ArrayBufferLike> | Uint16Array<ArrayBufferLike> | Uint8Array<ArrayBufferLike>;
|
|
37
|
+
declare const KINDS: {
|
|
38
|
+
readonly Float32Array: Float32ArrayConstructor;
|
|
39
|
+
readonly Float64Array: Float64ArrayConstructor;
|
|
40
|
+
readonly Int32Array: Int32ArrayConstructor;
|
|
41
|
+
readonly Uint32Array: Uint32ArrayConstructor;
|
|
42
|
+
readonly Uint16Array: Uint16ArrayConstructor;
|
|
43
|
+
readonly Uint8Array: Uint8ArrayConstructor;
|
|
44
|
+
};
|
|
45
|
+
type KindName = keyof typeof KINDS;
|
|
46
|
+
/** Where one typed array sits in the staging buffer. Plain data, so it crosses `postMessage`. */
|
|
47
|
+
export interface LanePlan {
|
|
48
|
+
set: string;
|
|
49
|
+
name: string;
|
|
50
|
+
kind: KindName;
|
|
51
|
+
offset: number;
|
|
52
|
+
length: number;
|
|
53
|
+
}
|
|
54
|
+
/** The whole mirror's shape, posted to a worker once and again whenever a set grows. */
|
|
55
|
+
export interface StagePlan {
|
|
56
|
+
lanes: LanePlan[];
|
|
57
|
+
bytes: number;
|
|
58
|
+
}
|
|
59
|
+
export declare const COUNT_BODIES = 0;
|
|
60
|
+
export declare const COUNT_CONSTRAINTS = 1;
|
|
61
|
+
export declare const COUNT_POINTS = 2;
|
|
62
|
+
export declare const COUNT_JOINTS = 3;
|
|
63
|
+
export declare const COUNT_ISLANDS = 4;
|
|
64
|
+
export declare const COUNT_SLOTS = 5;
|
|
65
|
+
/**
|
|
66
|
+
* Every typed array a set owns, by field name.
|
|
67
|
+
*
|
|
68
|
+
* Reflection and not a written list: `Object.entries` walks own enumerable properties in insertion
|
|
69
|
+
* order, which for these classes is constructor assignment order, and `grow` reassigns rather than
|
|
70
|
+
* adds so the order survives. The object arrays `BodySet` keeps for shapes are not views and drop
|
|
71
|
+
* out here, which is correct — the solve never reads them.
|
|
72
|
+
*/
|
|
73
|
+
export declare function lanesOf(set: object): Record<string, Lane>;
|
|
74
|
+
/**
|
|
75
|
+
* A live world's solve state, and the mirror of it a pool works in.
|
|
76
|
+
*
|
|
77
|
+
* The mirror is authoritative for the length of a tick: `push` fills it, every character solves into
|
|
78
|
+
* it, `pull` takes the result back. The caller solves its own share in the mirror too, so there is
|
|
79
|
+
* one set of views and not a main-thread copy racing a worker's.
|
|
80
|
+
*/
|
|
81
|
+
export declare class SolveStage {
|
|
82
|
+
buffer: ArrayBufferLike;
|
|
83
|
+
plan: StagePlan;
|
|
84
|
+
/** Bumped whenever the plan changes, which is how a worker knows its views are stale. */
|
|
85
|
+
generation: number;
|
|
86
|
+
private views;
|
|
87
|
+
private live;
|
|
88
|
+
private mirror;
|
|
89
|
+
private counts;
|
|
90
|
+
private readonly alloc;
|
|
91
|
+
constructor(live: IslandSolveState, alloc: (bytes: number) => ArrayBufferLike);
|
|
92
|
+
/** The state a character solves into. Never the live one. */
|
|
93
|
+
get state(): IslandSolveState;
|
|
94
|
+
/**
|
|
95
|
+
* Re-plan when a set has grown, and report whether anything moved.
|
|
96
|
+
*
|
|
97
|
+
* A grow doubles a capacity, so this is true about `log2(n)` times in a world's life and false on
|
|
98
|
+
* every other tick. `PhysicsWorld` only grows outside `step`, so a plan can never change under a
|
|
99
|
+
* running solve.
|
|
100
|
+
*/
|
|
101
|
+
resize(): boolean;
|
|
102
|
+
/** Copy the live sets into the mirror, dials and counts included. */
|
|
103
|
+
push(islandCount: number): void;
|
|
104
|
+
/**
|
|
105
|
+
* Copy the mirror back over the live sets.
|
|
106
|
+
*
|
|
107
|
+
* Every lane and not a list of the ones the solve writes. A whitelist would be faster by the
|
|
108
|
+
* width of the read-only lanes and would be wrong the first time somebody made a read-only lane
|
|
109
|
+
* writable, silently and only in the pool. The copy is already beneath the noise floor.
|
|
110
|
+
*/
|
|
111
|
+
pull(): void;
|
|
112
|
+
/** The live sets' arrays, keyed the way the plan keys them. Rebuilt per call because `grow`
|
|
113
|
+
replaces the arrays and a cached view would then write into a buffer nothing reads. */
|
|
114
|
+
private liveLanes;
|
|
115
|
+
private applyCounts;
|
|
116
|
+
private build;
|
|
117
|
+
/** Lay out every lane of every set, plus the dial and count blocks the worker reads. */
|
|
118
|
+
static planFor(live: IslandSolveState): StagePlan;
|
|
119
|
+
/**
|
|
120
|
+
* Rebuild the mirror's views over a buffer. The worker's whole share of the layout question.
|
|
121
|
+
*
|
|
122
|
+
* The sets are real instances with their arrays swapped, so `solveIslandInto` takes them without
|
|
123
|
+
* a cast and without a structural type that would have to be kept in step with four classes.
|
|
124
|
+
* **They are solve-only**: their private capacities describe the throwaway allocation the
|
|
125
|
+
* constructor made, so `add` and `grow` must never be called on one. Nothing in the solve path
|
|
126
|
+
* reads a capacity, which is what makes that safe.
|
|
127
|
+
*/
|
|
128
|
+
static adopt(buffer: ArrayBufferLike, plan: StagePlan): {
|
|
129
|
+
state: IslandSolveState;
|
|
130
|
+
views: Map<string, Lane>;
|
|
131
|
+
counts: Int32Array<ArrayBufferLike>;
|
|
132
|
+
dials: Float64Array<ArrayBufferLike>;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** The default allocator. A pool replaces it with one that returns a `SharedArrayBuffer`. */
|
|
136
|
+
export declare const heapAlloc: (bytes: number) => ArrayBufferLike;
|
|
137
|
+
/**
|
|
138
|
+
* The pool, minus the threads.
|
|
139
|
+
*
|
|
140
|
+
* **This exists to split one question into two.** A pooled tick does four things: mirror the state,
|
|
141
|
+
* hand islands out, solve them somewhere else, copy the result back. Three of those are the mirror
|
|
142
|
+
* and one is the threading, and a divergence in a pooled world is otherwise a bug report that could
|
|
143
|
+
* be either. This character does everything the pool does except the part that needs a worker, so
|
|
144
|
+
* `stage.test.ts` can hold the mirror to bit-identical physics with nothing concurrent in the
|
|
145
|
+
* picture, and a later failure in `workerPool.test.ts` is threading by elimination.
|
|
146
|
+
*
|
|
147
|
+
* It is also what a runtime with no `Worker` gets when a consumer asked for one and there is none,
|
|
148
|
+
* which makes the fallback path a tested path instead of an untested one.
|
|
149
|
+
*/
|
|
150
|
+
export declare class StagedExecutor implements Executor {
|
|
151
|
+
readonly name: string;
|
|
152
|
+
private stage;
|
|
153
|
+
private readonly alloc;
|
|
154
|
+
constructor(alloc?: (bytes: number) => ArrayBufferLike);
|
|
155
|
+
/** The mirror, once a tick has built one. A pool subclassing this reads its buffer to post it. */
|
|
156
|
+
protected get staged(): SolveStage | null;
|
|
157
|
+
run(count: number, solver: IslandSolver): void;
|
|
158
|
+
/**
|
|
159
|
+
* Where a pool forks instead. Serial here, so the mirror can be judged on its own.
|
|
160
|
+
*
|
|
161
|
+
* Returns whether the mirror holds a finished tick. False sends the caller back to the live state,
|
|
162
|
+
* so a character that cannot complete must say so instead of leaving a partial result to be copied.
|
|
163
|
+
*/
|
|
164
|
+
protected solve(count: number, stage: SolveStage): boolean;
|
|
165
|
+
private mirror;
|
|
166
|
+
}
|
|
167
|
+
export {};
|
package/dist/stage.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The island solve's state, mirrored into one buffer a worker can see.
|
|
3
|
+
*
|
|
4
|
+
* **This is a copy and not a re-allocation of the live sets, and that is a deliberate reversal of
|
|
5
|
+
* what the design document proposed.** The first plan gave `BodySet`, `ContactConstraints`,
|
|
6
|
+
* `JointSet` and `IslandSet` an injected allocator so their arrays could be `SharedArrayBuffer`
|
|
7
|
+
* directly, with no copy at all. Two things argued against it.
|
|
8
|
+
*
|
|
9
|
+
* The first is arithmetic, and it was measured rather than estimated after this comment first
|
|
10
|
+
* claimed "under a tenth of one per cent" from a back-of-envelope that was wrong by more than an
|
|
11
|
+
* order of magnitude. `StagedExecutor` against `SerialExecutor` on one machine, 2026-09-05:
|
|
12
|
+
*
|
|
13
|
+
* | stacks of ten | serial | staged | copy |
|
|
14
|
+
* |---|---|---|---|
|
|
15
|
+
* | 16 (160 bodies) | 3,202 us | 3,331 us | **4.0%** |
|
|
16
|
+
* | 64 (640 bodies) | 13,381 us | 13,187 us | **within noise, and negative** |
|
|
17
|
+
* | 256 (2,560 bodies) | 60,951 us | 61,987 us | **1.7%** |
|
|
18
|
+
*
|
|
19
|
+
* So the round trip is one to four per cent of a tick and not a tenth of one, which is still small
|
|
20
|
+
* against a pool measured at 1.26x on the same scene but is not nothing, and the 4% at the smallest
|
|
21
|
+
* size is the shape to watch: the copy is `O(capacity)` while the solve is `O(work)`, so a world of
|
|
22
|
+
* many bodies doing little is where this trade is worst.
|
|
23
|
+
*
|
|
24
|
+
* The second is blast radius. The allocator plan edited four classes that 524 tests stand on, for a
|
|
25
|
+
* saving that does not show up. This edits none of them.
|
|
26
|
+
*
|
|
27
|
+
* **Nothing here maintains a list of fields.** `lanesOf` reflects over a set and takes every typed
|
|
28
|
+
* array it finds, so a field added to `BodySet` tomorrow is staged tomorrow with nothing to
|
|
29
|
+
* remember. `stage.test.ts` asserts that reflection against each class, because a mirror that
|
|
30
|
+
* quietly omits a lane would be a defect that ships green: the solve would read a stale value and
|
|
31
|
+
* the world would drift only where that lane mattered.
|
|
32
|
+
*/
|
|
33
|
+
import { BodySet } from './bodies.js';
|
|
34
|
+
import { IslandSet } from './island.js';
|
|
35
|
+
import { JointSet } from './joints.js';
|
|
36
|
+
import { ContactConstraints } from './solver.js';
|
|
37
|
+
import { DIAL_COUNT, solveIslandInto } from './islandSolve.js';
|
|
38
|
+
const KINDS = {
|
|
39
|
+
Float32Array,
|
|
40
|
+
Float64Array,
|
|
41
|
+
Int32Array,
|
|
42
|
+
Uint32Array,
|
|
43
|
+
Uint16Array,
|
|
44
|
+
Uint8Array,
|
|
45
|
+
};
|
|
46
|
+
/* Slots in the counts lane, which carries the scalars the solve and its callers read. */
|
|
47
|
+
export const COUNT_BODIES = 0;
|
|
48
|
+
export const COUNT_CONSTRAINTS = 1;
|
|
49
|
+
export const COUNT_POINTS = 2;
|
|
50
|
+
export const COUNT_JOINTS = 3;
|
|
51
|
+
export const COUNT_ISLANDS = 4;
|
|
52
|
+
export const COUNT_SLOTS = 5;
|
|
53
|
+
/**
|
|
54
|
+
* Every typed array a set owns, by field name.
|
|
55
|
+
*
|
|
56
|
+
* Reflection and not a written list: `Object.entries` walks own enumerable properties in insertion
|
|
57
|
+
* order, which for these classes is constructor assignment order, and `grow` reassigns rather than
|
|
58
|
+
* adds so the order survives. The object arrays `BodySet` keeps for shapes are not views and drop
|
|
59
|
+
* out here, which is correct — the solve never reads them.
|
|
60
|
+
*/
|
|
61
|
+
export function lanesOf(set) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [name, value] of Object.entries(set)) {
|
|
64
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView))
|
|
65
|
+
out[name] = value;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One typed array into another of the same kind.
|
|
71
|
+
*
|
|
72
|
+
* The cast is here and nowhere else. `Lane` is a union, so `into.set(from)` cannot typecheck
|
|
73
|
+
* against it however the two are narrowed; every pair this is called with was planned from one
|
|
74
|
+
* reflected field, so the kinds are equal by construction.
|
|
75
|
+
*/
|
|
76
|
+
function copyLane(into, from) {
|
|
77
|
+
into.set(from);
|
|
78
|
+
}
|
|
79
|
+
function kindOf(lane) {
|
|
80
|
+
const name = lane.constructor.name;
|
|
81
|
+
if (!(name in KINDS))
|
|
82
|
+
throw new Error(`physics: no staging kind for ${name}`);
|
|
83
|
+
return name;
|
|
84
|
+
}
|
|
85
|
+
/** Round an offset up so every view starts aligned for its element size. Eight covers them all. */
|
|
86
|
+
function aligned(at) {
|
|
87
|
+
return (at + 7) & ~7;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A live world's solve state, and the mirror of it a pool works in.
|
|
91
|
+
*
|
|
92
|
+
* The mirror is authoritative for the length of a tick: `push` fills it, every character solves into
|
|
93
|
+
* it, `pull` takes the result back. The caller solves its own share in the mirror too, so there is
|
|
94
|
+
* one set of views and not a main-thread copy racing a worker's.
|
|
95
|
+
*/
|
|
96
|
+
export class SolveStage {
|
|
97
|
+
buffer;
|
|
98
|
+
plan;
|
|
99
|
+
/** Bumped whenever the plan changes, which is how a worker knows its views are stale. */
|
|
100
|
+
generation = 0;
|
|
101
|
+
views = new Map();
|
|
102
|
+
live;
|
|
103
|
+
mirror;
|
|
104
|
+
counts;
|
|
105
|
+
alloc;
|
|
106
|
+
constructor(live, alloc) {
|
|
107
|
+
this.alloc = alloc;
|
|
108
|
+
this.live = live;
|
|
109
|
+
this.plan = SolveStage.planFor(live);
|
|
110
|
+
this.buffer = alloc(this.plan.bytes);
|
|
111
|
+
this.build();
|
|
112
|
+
}
|
|
113
|
+
/** The state a character solves into. Never the live one. */
|
|
114
|
+
get state() {
|
|
115
|
+
return this.mirror;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Re-plan when a set has grown, and report whether anything moved.
|
|
119
|
+
*
|
|
120
|
+
* A grow doubles a capacity, so this is true about `log2(n)` times in a world's life and false on
|
|
121
|
+
* every other tick. `PhysicsWorld` only grows outside `step`, so a plan can never change under a
|
|
122
|
+
* running solve.
|
|
123
|
+
*/
|
|
124
|
+
resize() {
|
|
125
|
+
const next = SolveStage.planFor(this.live);
|
|
126
|
+
if (next.bytes === this.plan.bytes && next.lanes.length === this.plan.lanes.length) {
|
|
127
|
+
let same = true;
|
|
128
|
+
for (let i = 0; i < next.lanes.length && same; i++) {
|
|
129
|
+
const a = next.lanes[i];
|
|
130
|
+
const b = this.plan.lanes[i];
|
|
131
|
+
same =
|
|
132
|
+
a.offset === b.offset && a.length === b.length && a.name === b.name && a.set === b.set;
|
|
133
|
+
}
|
|
134
|
+
if (same)
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this.plan = next;
|
|
138
|
+
this.buffer = this.alloc(next.bytes);
|
|
139
|
+
this.generation += 1;
|
|
140
|
+
this.build();
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/** Copy the live sets into the mirror, dials and counts included. */
|
|
144
|
+
push(islandCount) {
|
|
145
|
+
const live = this.liveLanes();
|
|
146
|
+
for (const [key, into] of this.views) {
|
|
147
|
+
const from = live.get(key);
|
|
148
|
+
if (from)
|
|
149
|
+
copyLane(into, from);
|
|
150
|
+
}
|
|
151
|
+
copyLane(this.mirror.dials, this.live.dials);
|
|
152
|
+
this.counts[COUNT_BODIES] = this.live.bodies.count;
|
|
153
|
+
this.counts[COUNT_CONSTRAINTS] = this.live.constraints.count;
|
|
154
|
+
this.counts[COUNT_POINTS] = this.live.constraints.points;
|
|
155
|
+
this.counts[COUNT_JOINTS] = this.live.joints.count;
|
|
156
|
+
this.counts[COUNT_ISLANDS] = islandCount;
|
|
157
|
+
this.applyCounts(islandCount);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Copy the mirror back over the live sets.
|
|
161
|
+
*
|
|
162
|
+
* Every lane and not a list of the ones the solve writes. A whitelist would be faster by the
|
|
163
|
+
* width of the read-only lanes and would be wrong the first time somebody made a read-only lane
|
|
164
|
+
* writable, silently and only in the pool. The copy is already beneath the noise floor.
|
|
165
|
+
*/
|
|
166
|
+
pull() {
|
|
167
|
+
const live = this.liveLanes();
|
|
168
|
+
for (const [key, from] of this.views) {
|
|
169
|
+
const into = live.get(key);
|
|
170
|
+
if (into)
|
|
171
|
+
copyLane(into, from);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** The live sets' arrays, keyed the way the plan keys them. Rebuilt per call because `grow`
|
|
175
|
+
replaces the arrays and a cached view would then write into a buffer nothing reads. */
|
|
176
|
+
liveLanes() {
|
|
177
|
+
const out = new Map();
|
|
178
|
+
const sets = [
|
|
179
|
+
['bodies', this.live.bodies],
|
|
180
|
+
['constraints', this.live.constraints],
|
|
181
|
+
['joints', this.live.joints],
|
|
182
|
+
['islands', this.live.islands],
|
|
183
|
+
];
|
|
184
|
+
for (const [set, owner] of sets) {
|
|
185
|
+
for (const [name, lane] of Object.entries(lanesOf(owner)))
|
|
186
|
+
out.set(`${set}.${name}`, lane);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
applyCounts(islandCount) {
|
|
191
|
+
const m = this.mirror;
|
|
192
|
+
m.bodies.count = this.live.bodies.count;
|
|
193
|
+
m.constraints.count = this.live.constraints.count;
|
|
194
|
+
m.constraints.points = this.live.constraints.points;
|
|
195
|
+
m.joints.count = this.live.joints.count;
|
|
196
|
+
m.islands.count = islandCount;
|
|
197
|
+
}
|
|
198
|
+
build() {
|
|
199
|
+
const built = SolveStage.adopt(this.buffer, this.plan);
|
|
200
|
+
this.views = built.views;
|
|
201
|
+
this.mirror = built.state;
|
|
202
|
+
this.counts = built.counts;
|
|
203
|
+
}
|
|
204
|
+
/** Lay out every lane of every set, plus the dial and count blocks the worker reads. */
|
|
205
|
+
static planFor(live) {
|
|
206
|
+
const lanes = [];
|
|
207
|
+
let at = 0;
|
|
208
|
+
const take = (set, name, kind, length) => {
|
|
209
|
+
at = aligned(at);
|
|
210
|
+
lanes.push({ set, name, kind, offset: at, length });
|
|
211
|
+
at += length * KINDS[kind].BYTES_PER_ELEMENT;
|
|
212
|
+
};
|
|
213
|
+
const sets = [
|
|
214
|
+
['bodies', live.bodies],
|
|
215
|
+
['constraints', live.constraints],
|
|
216
|
+
['joints', live.joints],
|
|
217
|
+
['islands', live.islands],
|
|
218
|
+
];
|
|
219
|
+
for (const [name, set] of sets) {
|
|
220
|
+
for (const [field, lane] of Object.entries(lanesOf(set))) {
|
|
221
|
+
take(name, field, kindOf(lane), lane.length);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
take('meta', 'dials', 'Float64Array', DIAL_COUNT);
|
|
225
|
+
take('meta', 'counts', 'Int32Array', COUNT_SLOTS);
|
|
226
|
+
return { lanes, bytes: aligned(at) };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Rebuild the mirror's views over a buffer. The worker's whole share of the layout question.
|
|
230
|
+
*
|
|
231
|
+
* The sets are real instances with their arrays swapped, so `solveIslandInto` takes them without
|
|
232
|
+
* a cast and without a structural type that would have to be kept in step with four classes.
|
|
233
|
+
* **They are solve-only**: their private capacities describe the throwaway allocation the
|
|
234
|
+
* constructor made, so `add` and `grow` must never be called on one. Nothing in the solve path
|
|
235
|
+
* reads a capacity, which is what makes that safe.
|
|
236
|
+
*/
|
|
237
|
+
static adopt(buffer, plan) {
|
|
238
|
+
const views = new Map();
|
|
239
|
+
let dials = new Float64Array(DIAL_COUNT);
|
|
240
|
+
let counts = new Int32Array(COUNT_SLOTS);
|
|
241
|
+
for (const lane of plan.lanes) {
|
|
242
|
+
const view = new KINDS[lane.kind](buffer, lane.offset, lane.length);
|
|
243
|
+
if (lane.set === 'meta') {
|
|
244
|
+
if (lane.name === 'dials')
|
|
245
|
+
dials = view;
|
|
246
|
+
else
|
|
247
|
+
counts = view;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
views.set(`${lane.set}.${lane.name}`, view);
|
|
251
|
+
}
|
|
252
|
+
const state = {
|
|
253
|
+
bodies: new BodySet(1),
|
|
254
|
+
constraints: new ContactConstraints(1),
|
|
255
|
+
joints: new JointSet(1),
|
|
256
|
+
islands: new IslandSet(1),
|
|
257
|
+
dials,
|
|
258
|
+
};
|
|
259
|
+
for (const [key, view] of views) {
|
|
260
|
+
const [set, name] = key.split('.');
|
|
261
|
+
const owner = set === 'bodies'
|
|
262
|
+
? state.bodies
|
|
263
|
+
: set === 'constraints'
|
|
264
|
+
? state.constraints
|
|
265
|
+
: set === 'joints'
|
|
266
|
+
? state.joints
|
|
267
|
+
: state.islands;
|
|
268
|
+
/* One cast, at the one place a field name is a string. Every name came from reflecting over
|
|
269
|
+
the same class this is writing back to, so there is no name here that class does not have. */
|
|
270
|
+
owner[name] = view;
|
|
271
|
+
}
|
|
272
|
+
state.bodies.count = counts[COUNT_BODIES] ?? 0;
|
|
273
|
+
state.constraints.count = counts[COUNT_CONSTRAINTS] ?? 0;
|
|
274
|
+
state.constraints.points = counts[COUNT_POINTS] ?? 0;
|
|
275
|
+
state.joints.count = counts[COUNT_JOINTS] ?? 0;
|
|
276
|
+
state.islands.count = counts[COUNT_ISLANDS] ?? 0;
|
|
277
|
+
return { state, views, counts, dials };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** The default allocator. A pool replaces it with one that returns a `SharedArrayBuffer`. */
|
|
281
|
+
export const heapAlloc = (bytes) => new ArrayBuffer(bytes);
|
|
282
|
+
/**
|
|
283
|
+
* The pool, minus the threads.
|
|
284
|
+
*
|
|
285
|
+
* **This exists to split one question into two.** A pooled tick does four things: mirror the state,
|
|
286
|
+
* hand islands out, solve them somewhere else, copy the result back. Three of those are the mirror
|
|
287
|
+
* and one is the threading, and a divergence in a pooled world is otherwise a bug report that could
|
|
288
|
+
* be either. This character does everything the pool does except the part that needs a worker, so
|
|
289
|
+
* `stage.test.ts` can hold the mirror to bit-identical physics with nothing concurrent in the
|
|
290
|
+
* picture, and a later failure in `workerPool.test.ts` is threading by elimination.
|
|
291
|
+
*
|
|
292
|
+
* It is also what a runtime with no `Worker` gets when a consumer asked for one and there is none,
|
|
293
|
+
* which makes the fallback path a tested path instead of an untested one.
|
|
294
|
+
*/
|
|
295
|
+
export class StagedExecutor {
|
|
296
|
+
name = 'staged';
|
|
297
|
+
stage = null;
|
|
298
|
+
alloc;
|
|
299
|
+
constructor(alloc = heapAlloc) {
|
|
300
|
+
this.alloc = alloc;
|
|
301
|
+
}
|
|
302
|
+
/** The mirror, once a tick has built one. A pool subclassing this reads its buffer to post it. */
|
|
303
|
+
get staged() {
|
|
304
|
+
return this.stage;
|
|
305
|
+
}
|
|
306
|
+
run(count, solver) {
|
|
307
|
+
const live = solver.solveState;
|
|
308
|
+
if (!live) {
|
|
309
|
+
/* A consumer's own `IslandSolver` owes us no flat state, and correct physics beats staged
|
|
310
|
+
physics. Nothing here warns: `PhysicsWorld` always provides it, so this is unreachable
|
|
311
|
+
from the engine and is a courtesy to anybody implementing the seam themselves. */
|
|
312
|
+
for (let i = 0; i < count; i++)
|
|
313
|
+
solver.solveIsland(i);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const stage = this.mirror(live);
|
|
317
|
+
stage.push(count);
|
|
318
|
+
if (this.solve(count, stage)) {
|
|
319
|
+
stage.pull();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
/*
|
|
323
|
+
* The mirror is abandoned rather than copied back, and the tick is re-run against the live
|
|
324
|
+
* state. That is only sound because `pull` has not happened: the live sets still hold what they
|
|
325
|
+
* held before `push`, so a half-finished mirror is discarded whole and nothing partial reaches
|
|
326
|
+
* the world. A pool whose worker died takes this path once and then reports itself serial.
|
|
327
|
+
*/
|
|
328
|
+
for (let i = 0; i < count; i++)
|
|
329
|
+
solver.solveIsland(i);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Where a pool forks instead. Serial here, so the mirror can be judged on its own.
|
|
333
|
+
*
|
|
334
|
+
* Returns whether the mirror holds a finished tick. False sends the caller back to the live state,
|
|
335
|
+
* so a character that cannot complete must say so instead of leaving a partial result to be copied.
|
|
336
|
+
*/
|
|
337
|
+
solve(count, stage) {
|
|
338
|
+
for (let i = 0; i < count; i++)
|
|
339
|
+
solveIslandInto(stage.state, i);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
mirror(live) {
|
|
343
|
+
if (this.stage === null) {
|
|
344
|
+
this.stage = new SolveStage(live, this.alloc);
|
|
345
|
+
return this.stage;
|
|
346
|
+
}
|
|
347
|
+
this.stage.resize();
|
|
348
|
+
return this.stage;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Axis, Body } from './collide/index.ts';
|
|
2
|
+
import type { ColliderSet } from './colliderSet.ts';
|
|
3
|
+
export declare const ESCAPE_IDLE = 0;
|
|
4
|
+
/** The body was pushed along `axis`/`sign`, through the sweep, so geometry still decided. */
|
|
5
|
+
export declare const ESCAPE_PUSHED = 1;
|
|
6
|
+
/** Nothing had room, so the body was eased upward by hand. */
|
|
7
|
+
export declare const ESCAPE_EASED = 2;
|
|
8
|
+
/** No room in any of the six directions, and the budget is spent. The consumer decides. */
|
|
9
|
+
export declare const ESCAPE_SEALED = 3;
|
|
10
|
+
export type EscapeResult = 0 | 1 | 2 | 3;
|
|
11
|
+
/**
|
|
12
|
+
* A world's own veto over a direction the collider set says is clear.
|
|
13
|
+
*
|
|
14
|
+
* **Declared structurally, so nothing is imported in either direction** — the same shape
|
|
15
|
+
* `GroundProbe` uses on `CharacterController`, and for the same reason: a consumer with an
|
|
16
|
+
* analytic surface has a second authority about where a body may be, and a probe that consulted
|
|
17
|
+
* only the colliders would hand back directions the ordinary move refuses.
|
|
18
|
+
*
|
|
19
|
+
* Absent means the collider set is the whole world, which is every consumer that has not asked.
|
|
20
|
+
*/
|
|
21
|
+
export interface EscapeVeto {
|
|
22
|
+
/**
|
|
23
|
+
* Would the world refuse a body that had moved to `reached` along a horizontal `axis`?
|
|
24
|
+
*
|
|
25
|
+
* `reached` is the coordinate on that axis the probe arrived at.
|
|
26
|
+
*/
|
|
27
|
+
horizontal(axis: Axis, reached: number): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Would the world undo a downward escape from where the body is now?
|
|
30
|
+
*
|
|
31
|
+
* Down only. A body *above* its surface falling is the ordinary case and a surface has no
|
|
32
|
+
* opinion about it.
|
|
33
|
+
*/
|
|
34
|
+
below(): boolean;
|
|
35
|
+
}
|
|
36
|
+
/** What the consumer knows about this tick that the geometry cannot say. */
|
|
37
|
+
export interface StallInput {
|
|
38
|
+
/** Is the body being asked to move or jump at all? Standing still is not being stuck. */
|
|
39
|
+
trying: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Did the world refuse this tick's move?
|
|
42
|
+
*
|
|
43
|
+
* Without it the arming window fits a body turning on the spot or wiggling in open ground, and
|
|
44
|
+
* both were being pushed and then killed for it.
|
|
45
|
+
*/
|
|
46
|
+
refused: boolean;
|
|
47
|
+
/** Is there something under the feet? */
|
|
48
|
+
grounded: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Was the contact that stopped the fall genuinely *support*, rather than a graze against a side,
|
|
51
|
+
* an edge or an underside?
|
|
52
|
+
*
|
|
53
|
+
* Two different facts, and conflating them is the courtyard-lamp trap: a body with no support gets
|
|
54
|
+
* no jump however solid the thing under it looks.
|
|
55
|
+
*/
|
|
56
|
+
supported: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface StallEscapeOptions {
|
|
59
|
+
/**
|
|
60
|
+
* How far from where the window opened counts as getting somewhere, metres.
|
|
61
|
+
*
|
|
62
|
+
* **A question about net progress over a window, not about this tick's displacement.** A body
|
|
63
|
+
* jitters in a notch — thirty times a per-tick threshold — while sitting at the same coordinates
|
|
64
|
+
* for as long as the key is held. Displacement is not progress, and a trap that shakes you is
|
|
65
|
+
* still a trap.
|
|
66
|
+
*/
|
|
67
|
+
progressM?: number;
|
|
68
|
+
/** How many ticks of going nowhere before the window is full. */
|
|
69
|
+
stalledTicks?: number;
|
|
70
|
+
/** How many of those the world must have been pushing back for. */
|
|
71
|
+
refusedTicks?: number;
|
|
72
|
+
/** How far each nudge moves, metres. */
|
|
73
|
+
pushM?: number;
|
|
74
|
+
/** How far the whole escape may push before the body is called sealed, metres. */
|
|
75
|
+
budgetM?: number;
|
|
76
|
+
/** How far each of the six probes reaches, metres. */
|
|
77
|
+
probeM?: number;
|
|
78
|
+
/** How much clear air overhead counts as being able to jump out, metres. */
|
|
79
|
+
headroomM?: number;
|
|
80
|
+
/**
|
|
81
|
+
* How much a vertical direction's room is worth against a horizontal one's.
|
|
82
|
+
*
|
|
83
|
+
* Under one, and by a margin. Being eased *upward* out of scenery is the most visible possible
|
|
84
|
+
* correction — it reads as levitating. Slipping sideways off the thing you are caught on reads
|
|
85
|
+
* as slipping off it, which is what actually happened. Up stays available, because a pocket
|
|
86
|
+
* sometimes leaves nothing else.
|
|
87
|
+
*/
|
|
88
|
+
verticalBias?: number;
|
|
89
|
+
}
|
|
90
|
+
export declare class StallEscape {
|
|
91
|
+
/** Which axis the last push took, and which way along it. Read after `ESCAPE_PUSHED`. */
|
|
92
|
+
axis: Axis;
|
|
93
|
+
sign: number;
|
|
94
|
+
private readonly progressM;
|
|
95
|
+
private readonly stalledTicks;
|
|
96
|
+
private readonly refusedTicksNeeded;
|
|
97
|
+
private readonly pushM;
|
|
98
|
+
private readonly budgetM;
|
|
99
|
+
private readonly probeM;
|
|
100
|
+
private readonly headroomM;
|
|
101
|
+
private readonly verticalBias;
|
|
102
|
+
private stalled;
|
|
103
|
+
private refused;
|
|
104
|
+
private spent;
|
|
105
|
+
private anchorX;
|
|
106
|
+
private anchorY;
|
|
107
|
+
private anchorZ;
|
|
108
|
+
private anchored;
|
|
109
|
+
private lastX;
|
|
110
|
+
private lastY;
|
|
111
|
+
private lastZ;
|
|
112
|
+
constructor(options?: StallEscapeOptions);
|
|
113
|
+
/** Back to watching, for a respawn or a restart. */
|
|
114
|
+
reset(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Watch this tick, and act where the body has genuinely stopped getting anywhere.
|
|
117
|
+
*
|
|
118
|
+
* Call it last in a tick, after the move has been resolved: it reads where the body ended up.
|
|
119
|
+
*/
|
|
120
|
+
step(body: Body, colliders: ColliderSet, input: StallInput, veto?: EscapeVeto): EscapeResult;
|
|
121
|
+
/** Clear the window and re-anchor where the body is now. */
|
|
122
|
+
private standDown;
|
|
123
|
+
private remember;
|
|
124
|
+
/**
|
|
125
|
+
* Is the body standing on something, with room over its head to get off it?
|
|
126
|
+
*
|
|
127
|
+
* The one question that separates a trap from a player leaning on scenery. **Grounded** and
|
|
128
|
+
* **supported** are two different facts; **headroom** is what the notch lacked.
|
|
129
|
+
*
|
|
130
|
+
* Probed through the sweep rather than assumed, and the body is put back: a question may not
|
|
131
|
+
* move anything.
|
|
132
|
+
*/
|
|
133
|
+
private canJumpOut;
|
|
134
|
+
/**
|
|
135
|
+
* The freest of the six axis directions, in metres of room. Writes the winner into
|
|
136
|
+
* `axis`/`sign`.
|
|
137
|
+
*
|
|
138
|
+
* Every probe is undone before the next: a question may not move anything.
|
|
139
|
+
*/
|
|
140
|
+
private bestEscape;
|
|
141
|
+
}
|