@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/src/stage.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
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
|
+
|
|
34
|
+
import { BodySet } from './bodies.ts';
|
|
35
|
+
import { IslandSet } from './island.ts';
|
|
36
|
+
import { JointSet } from './joints.ts';
|
|
37
|
+
import { ContactConstraints } from './solver.ts';
|
|
38
|
+
import { DIAL_COUNT, solveIslandInto } from './islandSolve.ts';
|
|
39
|
+
import type { IslandSolveState } from './islandSolve.ts';
|
|
40
|
+
import type { Executor, IslandSolver } from './executor.ts';
|
|
41
|
+
|
|
42
|
+
/** Every typed array kind the four sets use. */
|
|
43
|
+
type Lane =
|
|
44
|
+
| Float32Array<ArrayBufferLike>
|
|
45
|
+
| Float64Array<ArrayBufferLike>
|
|
46
|
+
| Int32Array<ArrayBufferLike>
|
|
47
|
+
| Uint32Array<ArrayBufferLike>
|
|
48
|
+
| Uint16Array<ArrayBufferLike>
|
|
49
|
+
| Uint8Array<ArrayBufferLike>;
|
|
50
|
+
|
|
51
|
+
const KINDS = {
|
|
52
|
+
Float32Array,
|
|
53
|
+
Float64Array,
|
|
54
|
+
Int32Array,
|
|
55
|
+
Uint32Array,
|
|
56
|
+
Uint16Array,
|
|
57
|
+
Uint8Array,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
type KindName = keyof typeof KINDS;
|
|
61
|
+
|
|
62
|
+
/** Where one typed array sits in the staging buffer. Plain data, so it crosses `postMessage`. */
|
|
63
|
+
export interface LanePlan {
|
|
64
|
+
set: string;
|
|
65
|
+
name: string;
|
|
66
|
+
kind: KindName;
|
|
67
|
+
offset: number;
|
|
68
|
+
length: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The whole mirror's shape, posted to a worker once and again whenever a set grows. */
|
|
72
|
+
export interface StagePlan {
|
|
73
|
+
lanes: LanePlan[];
|
|
74
|
+
bytes: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Slots in the counts lane, which carries the scalars the solve and its callers read. */
|
|
78
|
+
export const COUNT_BODIES = 0;
|
|
79
|
+
export const COUNT_CONSTRAINTS = 1;
|
|
80
|
+
export const COUNT_POINTS = 2;
|
|
81
|
+
export const COUNT_JOINTS = 3;
|
|
82
|
+
export const COUNT_ISLANDS = 4;
|
|
83
|
+
export const COUNT_SLOTS = 5;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every typed array a set owns, by field name.
|
|
87
|
+
*
|
|
88
|
+
* Reflection and not a written list: `Object.entries` walks own enumerable properties in insertion
|
|
89
|
+
* order, which for these classes is constructor assignment order, and `grow` reassigns rather than
|
|
90
|
+
* adds so the order survives. The object arrays `BodySet` keeps for shapes are not views and drop
|
|
91
|
+
* out here, which is correct — the solve never reads them.
|
|
92
|
+
*/
|
|
93
|
+
export function lanesOf(set: object): Record<string, Lane> {
|
|
94
|
+
const out: Record<string, Lane> = {};
|
|
95
|
+
for (const [name, value] of Object.entries(set)) {
|
|
96
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) out[name] = value as Lane;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One typed array into another of the same kind.
|
|
103
|
+
*
|
|
104
|
+
* The cast is here and nowhere else. `Lane` is a union, so `into.set(from)` cannot typecheck
|
|
105
|
+
* against it however the two are narrowed; every pair this is called with was planned from one
|
|
106
|
+
* reflected field, so the kinds are equal by construction.
|
|
107
|
+
*/
|
|
108
|
+
function copyLane(into: Lane, from: Lane): void {
|
|
109
|
+
(into as Float32Array<ArrayBufferLike>).set(from as unknown as Float32Array<ArrayBufferLike>);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function kindOf(lane: Lane): KindName {
|
|
113
|
+
const name = lane.constructor.name as KindName;
|
|
114
|
+
if (!(name in KINDS)) throw new Error(`physics: no staging kind for ${name}`);
|
|
115
|
+
return name;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Round an offset up so every view starts aligned for its element size. Eight covers them all. */
|
|
119
|
+
function aligned(at: number): number {
|
|
120
|
+
return (at + 7) & ~7;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A live world's solve state, and the mirror of it a pool works in.
|
|
125
|
+
*
|
|
126
|
+
* The mirror is authoritative for the length of a tick: `push` fills it, every character solves into
|
|
127
|
+
* it, `pull` takes the result back. The caller solves its own share in the mirror too, so there is
|
|
128
|
+
* one set of views and not a main-thread copy racing a worker's.
|
|
129
|
+
*/
|
|
130
|
+
export class SolveStage {
|
|
131
|
+
buffer: ArrayBufferLike;
|
|
132
|
+
plan: StagePlan;
|
|
133
|
+
/** Bumped whenever the plan changes, which is how a worker knows its views are stale. */
|
|
134
|
+
generation = 0;
|
|
135
|
+
|
|
136
|
+
private views = new Map<string, Lane>();
|
|
137
|
+
private live: IslandSolveState;
|
|
138
|
+
private mirror!: IslandSolveState;
|
|
139
|
+
private counts!: Int32Array<ArrayBufferLike>;
|
|
140
|
+
|
|
141
|
+
private readonly alloc: (bytes: number) => ArrayBufferLike;
|
|
142
|
+
|
|
143
|
+
constructor(live: IslandSolveState, alloc: (bytes: number) => ArrayBufferLike) {
|
|
144
|
+
this.alloc = alloc;
|
|
145
|
+
this.live = live;
|
|
146
|
+
this.plan = SolveStage.planFor(live);
|
|
147
|
+
this.buffer = alloc(this.plan.bytes);
|
|
148
|
+
this.build();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The state a character solves into. Never the live one. */
|
|
152
|
+
get state(): IslandSolveState {
|
|
153
|
+
return this.mirror;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Re-plan when a set has grown, and report whether anything moved.
|
|
158
|
+
*
|
|
159
|
+
* A grow doubles a capacity, so this is true about `log2(n)` times in a world's life and false on
|
|
160
|
+
* every other tick. `PhysicsWorld` only grows outside `step`, so a plan can never change under a
|
|
161
|
+
* running solve.
|
|
162
|
+
*/
|
|
163
|
+
resize(): boolean {
|
|
164
|
+
const next = SolveStage.planFor(this.live);
|
|
165
|
+
if (next.bytes === this.plan.bytes && next.lanes.length === this.plan.lanes.length) {
|
|
166
|
+
let same = true;
|
|
167
|
+
for (let i = 0; i < next.lanes.length && same; i++) {
|
|
168
|
+
const a = next.lanes[i] as LanePlan;
|
|
169
|
+
const b = this.plan.lanes[i] as LanePlan;
|
|
170
|
+
same =
|
|
171
|
+
a.offset === b.offset && a.length === b.length && a.name === b.name && a.set === b.set;
|
|
172
|
+
}
|
|
173
|
+
if (same) return false;
|
|
174
|
+
}
|
|
175
|
+
this.plan = next;
|
|
176
|
+
this.buffer = this.alloc(next.bytes);
|
|
177
|
+
this.generation += 1;
|
|
178
|
+
this.build();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Copy the live sets into the mirror, dials and counts included. */
|
|
183
|
+
push(islandCount: number): void {
|
|
184
|
+
const live = this.liveLanes();
|
|
185
|
+
for (const [key, into] of this.views) {
|
|
186
|
+
const from = live.get(key);
|
|
187
|
+
if (from) copyLane(into, from);
|
|
188
|
+
}
|
|
189
|
+
copyLane(this.mirror.dials, this.live.dials);
|
|
190
|
+
this.counts[COUNT_BODIES] = this.live.bodies.count;
|
|
191
|
+
this.counts[COUNT_CONSTRAINTS] = this.live.constraints.count;
|
|
192
|
+
this.counts[COUNT_POINTS] = this.live.constraints.points;
|
|
193
|
+
this.counts[COUNT_JOINTS] = this.live.joints.count;
|
|
194
|
+
this.counts[COUNT_ISLANDS] = islandCount;
|
|
195
|
+
this.applyCounts(islandCount);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Copy the mirror back over the live sets.
|
|
200
|
+
*
|
|
201
|
+
* Every lane and not a list of the ones the solve writes. A whitelist would be faster by the
|
|
202
|
+
* width of the read-only lanes and would be wrong the first time somebody made a read-only lane
|
|
203
|
+
* writable, silently and only in the pool. The copy is already beneath the noise floor.
|
|
204
|
+
*/
|
|
205
|
+
pull(): void {
|
|
206
|
+
const live = this.liveLanes();
|
|
207
|
+
for (const [key, from] of this.views) {
|
|
208
|
+
const into = live.get(key);
|
|
209
|
+
if (into) copyLane(into, from);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The live sets' arrays, keyed the way the plan keys them. Rebuilt per call because `grow`
|
|
214
|
+
replaces the arrays and a cached view would then write into a buffer nothing reads. */
|
|
215
|
+
private liveLanes(): Map<string, Lane> {
|
|
216
|
+
const out = new Map<string, Lane>();
|
|
217
|
+
const sets: [string, object][] = [
|
|
218
|
+
['bodies', this.live.bodies],
|
|
219
|
+
['constraints', this.live.constraints],
|
|
220
|
+
['joints', this.live.joints],
|
|
221
|
+
['islands', this.live.islands],
|
|
222
|
+
];
|
|
223
|
+
for (const [set, owner] of sets) {
|
|
224
|
+
for (const [name, lane] of Object.entries(lanesOf(owner))) out.set(`${set}.${name}`, lane);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private applyCounts(islandCount: number): void {
|
|
230
|
+
const m = this.mirror;
|
|
231
|
+
m.bodies.count = this.live.bodies.count;
|
|
232
|
+
m.constraints.count = this.live.constraints.count;
|
|
233
|
+
m.constraints.points = this.live.constraints.points;
|
|
234
|
+
m.joints.count = this.live.joints.count;
|
|
235
|
+
m.islands.count = islandCount;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private build(): void {
|
|
239
|
+
const built = SolveStage.adopt(this.buffer, this.plan);
|
|
240
|
+
this.views = built.views;
|
|
241
|
+
this.mirror = built.state;
|
|
242
|
+
this.counts = built.counts;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Lay out every lane of every set, plus the dial and count blocks the worker reads. */
|
|
246
|
+
static planFor(live: IslandSolveState): StagePlan {
|
|
247
|
+
const lanes: LanePlan[] = [];
|
|
248
|
+
let at = 0;
|
|
249
|
+
const take = (set: string, name: string, kind: KindName, length: number): void => {
|
|
250
|
+
at = aligned(at);
|
|
251
|
+
lanes.push({ set, name, kind, offset: at, length });
|
|
252
|
+
at += length * KINDS[kind].BYTES_PER_ELEMENT;
|
|
253
|
+
};
|
|
254
|
+
const sets: [string, object][] = [
|
|
255
|
+
['bodies', live.bodies],
|
|
256
|
+
['constraints', live.constraints],
|
|
257
|
+
['joints', live.joints],
|
|
258
|
+
['islands', live.islands],
|
|
259
|
+
];
|
|
260
|
+
for (const [name, set] of sets) {
|
|
261
|
+
for (const [field, lane] of Object.entries(lanesOf(set))) {
|
|
262
|
+
take(name, field, kindOf(lane), lane.length);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
take('meta', 'dials', 'Float64Array', DIAL_COUNT);
|
|
266
|
+
take('meta', 'counts', 'Int32Array', COUNT_SLOTS);
|
|
267
|
+
return { lanes, bytes: aligned(at) };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Rebuild the mirror's views over a buffer. The worker's whole share of the layout question.
|
|
272
|
+
*
|
|
273
|
+
* The sets are real instances with their arrays swapped, so `solveIslandInto` takes them without
|
|
274
|
+
* a cast and without a structural type that would have to be kept in step with four classes.
|
|
275
|
+
* **They are solve-only**: their private capacities describe the throwaway allocation the
|
|
276
|
+
* constructor made, so `add` and `grow` must never be called on one. Nothing in the solve path
|
|
277
|
+
* reads a capacity, which is what makes that safe.
|
|
278
|
+
*/
|
|
279
|
+
static adopt(
|
|
280
|
+
buffer: ArrayBufferLike,
|
|
281
|
+
plan: StagePlan,
|
|
282
|
+
): {
|
|
283
|
+
state: IslandSolveState;
|
|
284
|
+
views: Map<string, Lane>;
|
|
285
|
+
counts: Int32Array<ArrayBufferLike>;
|
|
286
|
+
dials: Float64Array<ArrayBufferLike>;
|
|
287
|
+
} {
|
|
288
|
+
const views = new Map<string, Lane>();
|
|
289
|
+
let dials: Float64Array<ArrayBufferLike> = new Float64Array(DIAL_COUNT);
|
|
290
|
+
let counts: Int32Array<ArrayBufferLike> = new Int32Array(COUNT_SLOTS);
|
|
291
|
+
for (const lane of plan.lanes) {
|
|
292
|
+
const view = new KINDS[lane.kind](buffer as ArrayBuffer, lane.offset, lane.length) as Lane;
|
|
293
|
+
if (lane.set === 'meta') {
|
|
294
|
+
if (lane.name === 'dials') dials = view as Float64Array<ArrayBufferLike>;
|
|
295
|
+
else counts = view as Int32Array<ArrayBufferLike>;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
views.set(`${lane.set}.${lane.name}`, view);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const state: IslandSolveState = {
|
|
302
|
+
bodies: new BodySet(1),
|
|
303
|
+
constraints: new ContactConstraints(1),
|
|
304
|
+
joints: new JointSet(1),
|
|
305
|
+
islands: new IslandSet(1),
|
|
306
|
+
dials,
|
|
307
|
+
};
|
|
308
|
+
for (const [key, view] of views) {
|
|
309
|
+
const [set, name] = key.split('.') as [string, string];
|
|
310
|
+
const owner =
|
|
311
|
+
set === 'bodies'
|
|
312
|
+
? state.bodies
|
|
313
|
+
: set === 'constraints'
|
|
314
|
+
? state.constraints
|
|
315
|
+
: set === 'joints'
|
|
316
|
+
? state.joints
|
|
317
|
+
: state.islands;
|
|
318
|
+
/* One cast, at the one place a field name is a string. Every name came from reflecting over
|
|
319
|
+
the same class this is writing back to, so there is no name here that class does not have. */
|
|
320
|
+
(owner as unknown as Record<string, Lane>)[name] = view;
|
|
321
|
+
}
|
|
322
|
+
state.bodies.count = counts[COUNT_BODIES] ?? 0;
|
|
323
|
+
state.constraints.count = counts[COUNT_CONSTRAINTS] ?? 0;
|
|
324
|
+
state.constraints.points = counts[COUNT_POINTS] ?? 0;
|
|
325
|
+
state.joints.count = counts[COUNT_JOINTS] ?? 0;
|
|
326
|
+
state.islands.count = counts[COUNT_ISLANDS] ?? 0;
|
|
327
|
+
return { state, views, counts, dials };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The default allocator. A pool replaces it with one that returns a `SharedArrayBuffer`. */
|
|
332
|
+
export const heapAlloc = (bytes: number): ArrayBufferLike => new ArrayBuffer(bytes);
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The pool, minus the threads.
|
|
336
|
+
*
|
|
337
|
+
* **This exists to split one question into two.** A pooled tick does four things: mirror the state,
|
|
338
|
+
* hand islands out, solve them somewhere else, copy the result back. Three of those are the mirror
|
|
339
|
+
* and one is the threading, and a divergence in a pooled world is otherwise a bug report that could
|
|
340
|
+
* be either. This character does everything the pool does except the part that needs a worker, so
|
|
341
|
+
* `stage.test.ts` can hold the mirror to bit-identical physics with nothing concurrent in the
|
|
342
|
+
* picture, and a later failure in `workerPool.test.ts` is threading by elimination.
|
|
343
|
+
*
|
|
344
|
+
* It is also what a runtime with no `Worker` gets when a consumer asked for one and there is none,
|
|
345
|
+
* which makes the fallback path a tested path instead of an untested one.
|
|
346
|
+
*/
|
|
347
|
+
export class StagedExecutor implements Executor {
|
|
348
|
+
readonly name: string = 'staged';
|
|
349
|
+
|
|
350
|
+
private stage: SolveStage | null = null;
|
|
351
|
+
|
|
352
|
+
private readonly alloc: (bytes: number) => ArrayBufferLike;
|
|
353
|
+
|
|
354
|
+
constructor(alloc: (bytes: number) => ArrayBufferLike = heapAlloc) {
|
|
355
|
+
this.alloc = alloc;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The mirror, once a tick has built one. A pool subclassing this reads its buffer to post it. */
|
|
359
|
+
protected get staged(): SolveStage | null {
|
|
360
|
+
return this.stage;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
run(count: number, solver: IslandSolver): void {
|
|
364
|
+
const live = solver.solveState;
|
|
365
|
+
if (!live) {
|
|
366
|
+
/* A consumer's own `IslandSolver` owes us no flat state, and correct physics beats staged
|
|
367
|
+
physics. Nothing here warns: `PhysicsWorld` always provides it, so this is unreachable
|
|
368
|
+
from the engine and is a courtesy to anybody implementing the seam themselves. */
|
|
369
|
+
for (let i = 0; i < count; i++) solver.solveIsland(i);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const stage = this.mirror(live);
|
|
373
|
+
stage.push(count);
|
|
374
|
+
if (this.solve(count, stage)) {
|
|
375
|
+
stage.pull();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
/*
|
|
379
|
+
* The mirror is abandoned rather than copied back, and the tick is re-run against the live
|
|
380
|
+
* state. That is only sound because `pull` has not happened: the live sets still hold what they
|
|
381
|
+
* held before `push`, so a half-finished mirror is discarded whole and nothing partial reaches
|
|
382
|
+
* the world. A pool whose worker died takes this path once and then reports itself serial.
|
|
383
|
+
*/
|
|
384
|
+
for (let i = 0; i < count; i++) solver.solveIsland(i);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Where a pool forks instead. Serial here, so the mirror can be judged on its own.
|
|
389
|
+
*
|
|
390
|
+
* Returns whether the mirror holds a finished tick. False sends the caller back to the live state,
|
|
391
|
+
* so a character that cannot complete must say so instead of leaving a partial result to be copied.
|
|
392
|
+
*/
|
|
393
|
+
protected solve(count: number, stage: SolveStage): boolean {
|
|
394
|
+
for (let i = 0; i < count; i++) solveIslandInto(stage.state, i);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private mirror(live: IslandSolveState): SolveStage {
|
|
399
|
+
if (this.stage === null) {
|
|
400
|
+
this.stage = new SolveStage(live, this.alloc);
|
|
401
|
+
return this.stage;
|
|
402
|
+
}
|
|
403
|
+
this.stage.resize();
|
|
404
|
+
return this.stage;
|
|
405
|
+
}
|
|
406
|
+
}
|