@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/vehicle.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { createRayHit } from './query.js';
|
|
2
|
+
/** A curve that rises to a peak and falls away, which is what a tyre does. */
|
|
3
|
+
export function defaultTyreCurve(peak = 1) {
|
|
4
|
+
return {
|
|
5
|
+
slip: new Float32Array([0, 0.1, 0.2, 0.4, 0.8, 1.5]),
|
|
6
|
+
force: new Float32Array([0, peak * 0.75, peak, peak * 0.9, peak * 0.75, peak * 0.7]),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export class Vehicle {
|
|
10
|
+
chassis;
|
|
11
|
+
wheelCount;
|
|
12
|
+
/** Whether each wheel found ground on the last update. */
|
|
13
|
+
grounded;
|
|
14
|
+
/** How far each wheel's suspension is compressed, in metres. */
|
|
15
|
+
compression;
|
|
16
|
+
/** Where each wheel's contact is, xyz-packed. Meaningful where `grounded`. */
|
|
17
|
+
contact;
|
|
18
|
+
local;
|
|
19
|
+
radius;
|
|
20
|
+
travel;
|
|
21
|
+
stiffness;
|
|
22
|
+
damping;
|
|
23
|
+
steers;
|
|
24
|
+
driven;
|
|
25
|
+
maxSteerTangent;
|
|
26
|
+
engineForce;
|
|
27
|
+
brakeForce;
|
|
28
|
+
longitudinal;
|
|
29
|
+
lateral;
|
|
30
|
+
/**
|
|
31
|
+
* The suspension ray must ignore the car it holds up.
|
|
32
|
+
*
|
|
33
|
+
* A wheel anchor sits *inside* the chassis by construction — that is what a suspension mount is —
|
|
34
|
+
* and a ray starting inside a body reports that body at fraction zero, which is the right answer
|
|
35
|
+
* to the question a line-of-sight test asks and the wrong one here. Left unfiltered, every wheel
|
|
36
|
+
* found the chassis at zero distance, read the suspension as fully compressed, and pushed the car
|
|
37
|
+
* against itself: it sank onto its own chassis and sat there.
|
|
38
|
+
*/
|
|
39
|
+
filter;
|
|
40
|
+
hit = createRayHit();
|
|
41
|
+
probe;
|
|
42
|
+
/** Scratch for the probe, claimed once: every wheel asks it every tick. */
|
|
43
|
+
probed = { y: 0, normalX: 0, normalY: 1, normalZ: 0 };
|
|
44
|
+
/**
|
|
45
|
+
* Every wheel's impulse, gathered before any is applied: `rx ry rz px py pz` per wheel.
|
|
46
|
+
*
|
|
47
|
+
* **Applying them one at a time makes the car curve.** Each wheel then sees a velocity the
|
|
48
|
+
* previous wheel has already changed, so the left front's lateral impulse biases what the right
|
|
49
|
+
* front computes, and the asymmetry accumulates into a yaw rather than cancelling. Measured on a
|
|
50
|
+
* symmetric car under straight throttle: 0.011 metres of drift after 2 metres, 0.64 after 39, and
|
|
51
|
+
* **5.49 after 155** — a curve, not a wobble. Forces at an instant are simultaneous, and gathering
|
|
52
|
+
* them is both the correct physics and the cheaper fix.
|
|
53
|
+
*/
|
|
54
|
+
pending;
|
|
55
|
+
constructor(chassis, options) {
|
|
56
|
+
this.chassis = chassis;
|
|
57
|
+
this.wheelCount = options.wheels.length;
|
|
58
|
+
const n = this.wheelCount;
|
|
59
|
+
this.local = new Float32Array(n * 3);
|
|
60
|
+
this.radius = new Float32Array(n);
|
|
61
|
+
this.travel = new Float32Array(n);
|
|
62
|
+
this.stiffness = new Float32Array(n);
|
|
63
|
+
this.damping = new Float32Array(n);
|
|
64
|
+
this.steers = new Uint8Array(n);
|
|
65
|
+
this.driven = new Uint8Array(n);
|
|
66
|
+
this.grounded = new Uint8Array(n);
|
|
67
|
+
this.compression = new Float32Array(n);
|
|
68
|
+
this.contact = new Float32Array(n * 3);
|
|
69
|
+
options.wheels.forEach((w, i) => {
|
|
70
|
+
this.local[i * 3] = w.x;
|
|
71
|
+
this.local[i * 3 + 1] = w.y;
|
|
72
|
+
this.local[i * 3 + 2] = w.z;
|
|
73
|
+
this.radius[i] = w.radius ?? 0.35;
|
|
74
|
+
this.travel[i] = w.suspensionTravel ?? 0.3;
|
|
75
|
+
this.stiffness[i] = w.stiffness ?? 40000;
|
|
76
|
+
this.damping[i] = w.damping ?? 4000;
|
|
77
|
+
this.steers[i] = w.steers ? 1 : 0;
|
|
78
|
+
this.driven[i] = w.driven ? 1 : 0;
|
|
79
|
+
});
|
|
80
|
+
this.maxSteerTangent = options.maxSteerTangent ?? 0.6;
|
|
81
|
+
this.engineForce = options.engineForce ?? 6000;
|
|
82
|
+
this.brakeForce = options.brakeForce ?? 9000;
|
|
83
|
+
this.longitudinal = options.longitudinal ?? defaultTyreCurve(1.2);
|
|
84
|
+
this.lateral = options.lateral ?? defaultTyreCurve(1.4);
|
|
85
|
+
this.filter = { ...options.filter, ignore: chassis };
|
|
86
|
+
this.probe = options.ground ?? null;
|
|
87
|
+
this.pending = new Float64Array(n * 6);
|
|
88
|
+
}
|
|
89
|
+
/** Advance the vehicle one fixed tick. Call before `world.step`. */
|
|
90
|
+
update(world, dt, input) {
|
|
91
|
+
const body = this.chassis;
|
|
92
|
+
const bodies = world.bodies;
|
|
93
|
+
this.pending.fill(0);
|
|
94
|
+
let drivenCount = 0;
|
|
95
|
+
for (let i = 0; i < this.wheelCount; i++)
|
|
96
|
+
if (this.driven[i])
|
|
97
|
+
drivenCount++;
|
|
98
|
+
const perWheelDrive = drivenCount > 0 ? (input.throttle * this.engineForce) / drivenCount : 0;
|
|
99
|
+
for (let i = 0; i < this.wheelCount; i++) {
|
|
100
|
+
rotate(bodies, body, this.local, i * 3, ANCHOR);
|
|
101
|
+
const ax = (bodies.posX[body] ?? 0) + ANCHOR[0];
|
|
102
|
+
const ay = (bodies.posY[body] ?? 0) + ANCHOR[1];
|
|
103
|
+
const az = (bodies.posZ[body] ?? 0) + ANCHOR[2];
|
|
104
|
+
// The suspension looks along the chassis's own down, not world down.
|
|
105
|
+
axis(bodies, body, 0, -1, 0, DOWN);
|
|
106
|
+
const reach = (this.travel[i] ?? 0) + (this.radius[i] ?? 0);
|
|
107
|
+
if (!world.raycast(ax, ay, az, DOWN[0], DOWN[1], DOWN[2], reach, this.hit, this.filter) &&
|
|
108
|
+
!this.probeUnder(ax, ay, az, reach)) {
|
|
109
|
+
this.grounded[i] = 0;
|
|
110
|
+
this.compression[i] = 0;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
this.grounded[i] = 1;
|
|
114
|
+
this.contact[i * 3] = this.hit.x;
|
|
115
|
+
this.contact[i * 3 + 1] = this.hit.y;
|
|
116
|
+
this.contact[i * 3 + 2] = this.hit.z;
|
|
117
|
+
const distance = this.hit.fraction * reach;
|
|
118
|
+
/*
|
|
119
|
+
* Compression is capped at the travel. Past that the wheel is bottomed out and the *chassis*
|
|
120
|
+
* takes the load through its own collider, which is what a bump stop is.
|
|
121
|
+
*
|
|
122
|
+
* **No test here reaches it, and that is worth saying rather than leaving somebody to look.**
|
|
123
|
+
* With wheels mounted inside the chassis — the ordinary arrangement — the chassis collider
|
|
124
|
+
* meets the ground before the suspension can bottom out, so removing this line changes a drop
|
|
125
|
+
* from six metres by a centimetre and nothing else. It stays because a vehicle with outboard
|
|
126
|
+
* mounts *can* reach it, and an uncapped spring then answers a hard landing with several
|
|
127
|
+
* times the car's weight.
|
|
128
|
+
*/
|
|
129
|
+
let compression = reach - distance;
|
|
130
|
+
const bumpStop = this.travel[i] ?? 0;
|
|
131
|
+
if (compression > bumpStop)
|
|
132
|
+
compression = bumpStop;
|
|
133
|
+
this.compression[i] = compression;
|
|
134
|
+
// The anchor's velocity, which is what the damper answers to.
|
|
135
|
+
pointVelocity(bodies, body, ANCHOR, VEL);
|
|
136
|
+
const alongDown = VEL[0] * DOWN[0] + VEL[1] * DOWN[1] + VEL[2] * DOWN[2];
|
|
137
|
+
/*
|
|
138
|
+
* **The damper adds to the load while the spring is compressing.** `alongDown` is positive
|
|
139
|
+
* when the anchor is moving toward the ground, which is exactly when compression is
|
|
140
|
+
* increasing, so a damper opposing that motion pushes *up*. Subtracting it instead — which is
|
|
141
|
+
* what `k·x − c·v` reads like if `v` is taken as the wrong sign — leaves the spring undamped
|
|
142
|
+
* and clamps the load to zero on the way down: the car fell, bounced on a pure spring, and
|
|
143
|
+
* launched itself upside down.
|
|
144
|
+
*/
|
|
145
|
+
let load = (this.stiffness[i] ?? 0) * compression + (this.damping[i] ?? 0) * alongDown;
|
|
146
|
+
if (load < 0)
|
|
147
|
+
load = 0;
|
|
148
|
+
// Along the surface normal rather than along the ray, so a slope pushes sideways too.
|
|
149
|
+
this.gather(i, ANCHOR, this.hit.nx * load * dt, this.hit.ny * load * dt, this.hit.nz * load * dt);
|
|
150
|
+
// The wheel's own frame: forward turned by steering, and lateral across it.
|
|
151
|
+
const steer = this.steers[i] ? input.steer * this.maxSteerTangent : 0;
|
|
152
|
+
axis(bodies, body, steer, 0, 1, FORWARD);
|
|
153
|
+
normalise(FORWARD);
|
|
154
|
+
axis(bodies, body, 1, 0, -steer, SIDE);
|
|
155
|
+
normalise(SIDE);
|
|
156
|
+
// Flattened onto the surface, so grip acts along the ground rather than into it.
|
|
157
|
+
project(FORWARD, this.hit.nx, this.hit.ny, this.hit.nz);
|
|
158
|
+
project(SIDE, this.hit.nx, this.hit.ny, this.hit.nz);
|
|
159
|
+
normalise(FORWARD);
|
|
160
|
+
normalise(SIDE);
|
|
161
|
+
pointVelocity(bodies, body, ANCHOR, VEL);
|
|
162
|
+
const vForward = VEL[0] * FORWARD[0] + VEL[1] * FORWARD[1] + VEL[2] * FORWARD[2];
|
|
163
|
+
const vSide = VEL[0] * SIDE[0] + VEL[1] * SIDE[1] + VEL[2] * SIDE[2];
|
|
164
|
+
const speed = Math.sqrt(vForward * vForward + vSide * vSide);
|
|
165
|
+
/*
|
|
166
|
+
* Slip is the sideways or braking velocity against the speed the wheel is going, which is what
|
|
167
|
+
* a tyre curve is indexed by. At a standstill the ratio is undefined, so the slip is taken as
|
|
168
|
+
* the velocity itself — a stationary car with a sideways nudge should still resist it.
|
|
169
|
+
*/
|
|
170
|
+
const denom = speed > 1 ? speed : 1;
|
|
171
|
+
const lateralGrip = sampleTyreCurve(this.lateral, Math.abs(vSide) / denom) * load;
|
|
172
|
+
const sideImpulse = clampMagnitude(-vSide * massAt(bodies, body), lateralGrip * dt);
|
|
173
|
+
this.gather(i, ANCHOR, SIDE[0] * sideImpulse, SIDE[1] * sideImpulse, SIDE[2] * sideImpulse);
|
|
174
|
+
let forwardForce = this.driven[i] ? perWheelDrive : 0;
|
|
175
|
+
if (input.brake > 0) {
|
|
176
|
+
const brake = input.brake * this.brakeForce;
|
|
177
|
+
forwardForce += vForward > 0 ? -brake : brake;
|
|
178
|
+
}
|
|
179
|
+
const traction = sampleTyreCurve(this.longitudinal, Math.abs(vForward) / denom) * load;
|
|
180
|
+
const limit = traction > 0 ? traction : load;
|
|
181
|
+
const drive = clampMagnitude(forwardForce, limit);
|
|
182
|
+
this.gather(i, ANCHOR, FORWARD[0] * drive * dt, FORWARD[1] * drive * dt, FORWARD[2] * drive * dt);
|
|
183
|
+
}
|
|
184
|
+
// Now apply, all from the state every wheel was measured against.
|
|
185
|
+
for (let i = 0; i < this.wheelCount; i++) {
|
|
186
|
+
const at = i * 6;
|
|
187
|
+
ANCHOR[0] = this.pending[at];
|
|
188
|
+
ANCHOR[1] = this.pending[at + 1];
|
|
189
|
+
ANCHOR[2] = this.pending[at + 2];
|
|
190
|
+
applyImpulse(bodies, body, ANCHOR, this.pending[at + 3], this.pending[at + 4], this.pending[at + 5]);
|
|
191
|
+
}
|
|
192
|
+
world.wakeIsland(body);
|
|
193
|
+
}
|
|
194
|
+
/** Add an impulse at a wheel's anchor to what will be applied at the end of the pass. */
|
|
195
|
+
/**
|
|
196
|
+
* Rest a wheel on the analytic surface, filling `hit` as a ray against a body would have.
|
|
197
|
+
*
|
|
198
|
+
* Everything downstream — the spring, the damper, the tyre frame flattened onto the ground —
|
|
199
|
+
* reads `hit`, so filling it is the whole of the wiring and no grip code learns that a surface
|
|
200
|
+
* exists.
|
|
201
|
+
*
|
|
202
|
+
* **The column is the anchor's own and the drop is measured along the chassis's down**, which is
|
|
203
|
+
* exact while the chassis is upright and off by its tilt when it is not: the true contact sits a
|
|
204
|
+
* little to the side of the column that was sampled, and on a slope that column is a little
|
|
205
|
+
* higher or lower than the contact. At the lean angles a car spends its life at the error is
|
|
206
|
+
* millimetres, and the alternative is solving a ray against an arbitrary height function, which
|
|
207
|
+
* is an iteration this package does not allow itself. A vehicle on its roof finds no ground here
|
|
208
|
+
* at all, because there is no downward intersection to find, which is the honest answer.
|
|
209
|
+
*/
|
|
210
|
+
probeUnder(ax, ay, az, reach) {
|
|
211
|
+
if (this.probe === null || DOWN[1] >= 0)
|
|
212
|
+
return false;
|
|
213
|
+
if (!this.probe.sample(ax, az, this.probed, ay))
|
|
214
|
+
return false;
|
|
215
|
+
const distance = (this.probed.y - ay) / DOWN[1];
|
|
216
|
+
if (!(distance >= 0) || distance > reach)
|
|
217
|
+
return false;
|
|
218
|
+
this.hit.body = -1;
|
|
219
|
+
this.hit.fraction = distance / reach;
|
|
220
|
+
this.hit.x = ax + DOWN[0] * distance;
|
|
221
|
+
this.hit.y = ay + DOWN[1] * distance;
|
|
222
|
+
this.hit.z = az + DOWN[2] * distance;
|
|
223
|
+
this.hit.nx = this.probed.normalX;
|
|
224
|
+
this.hit.ny = this.probed.normalY;
|
|
225
|
+
this.hit.nz = this.probed.normalZ;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
gather(i, r, px, py, pz) {
|
|
229
|
+
const at = i * 6;
|
|
230
|
+
this.pending[at] = r[0];
|
|
231
|
+
this.pending[at + 1] = r[1];
|
|
232
|
+
this.pending[at + 2] = r[2];
|
|
233
|
+
this.pending[at + 3] += px;
|
|
234
|
+
this.pending[at + 4] += py;
|
|
235
|
+
this.pending[at + 5] += pz;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Sample a curve at `at`, holding the last value beyond its end. */
|
|
239
|
+
export function sampleTyreCurve(curve, at) {
|
|
240
|
+
const n = curve.slip.length;
|
|
241
|
+
if (n === 0)
|
|
242
|
+
return 0;
|
|
243
|
+
if (at <= (curve.slip[0] ?? 0))
|
|
244
|
+
return curve.force[0] ?? 0;
|
|
245
|
+
for (let i = 1; i < n; i++) {
|
|
246
|
+
const s1 = curve.slip[i] ?? 0;
|
|
247
|
+
if (at <= s1) {
|
|
248
|
+
const s0 = curve.slip[i - 1] ?? 0;
|
|
249
|
+
const span = s1 - s0;
|
|
250
|
+
const t = span > 0 ? (at - s0) / span : 0;
|
|
251
|
+
const f0 = curve.force[i - 1] ?? 0;
|
|
252
|
+
return f0 + ((curve.force[i] ?? 0) - f0) * t;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return curve.force[n - 1] ?? 0;
|
|
256
|
+
}
|
|
257
|
+
const ANCHOR = new Float64Array(3);
|
|
258
|
+
const DOWN = new Float64Array(3);
|
|
259
|
+
const FORWARD = new Float64Array(3);
|
|
260
|
+
const SIDE = new Float64Array(3);
|
|
261
|
+
const VEL = new Float64Array(3);
|
|
262
|
+
const INV_I = new Float32Array(6);
|
|
263
|
+
function clampMagnitude(v, limit) {
|
|
264
|
+
if (v > limit)
|
|
265
|
+
return limit;
|
|
266
|
+
if (v < -limit)
|
|
267
|
+
return -limit;
|
|
268
|
+
return v;
|
|
269
|
+
}
|
|
270
|
+
/** A representative mass at the anchor, for turning a wanted velocity change into an impulse. */
|
|
271
|
+
function massAt(bodies, body) {
|
|
272
|
+
const im = bodies.invMass[body] ?? 0;
|
|
273
|
+
return im > 0 ? 1 / im : 0;
|
|
274
|
+
}
|
|
275
|
+
function normalise(v) {
|
|
276
|
+
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
277
|
+
if (len === 0)
|
|
278
|
+
return;
|
|
279
|
+
v[0] /= len;
|
|
280
|
+
v[1] /= len;
|
|
281
|
+
v[2] /= len;
|
|
282
|
+
}
|
|
283
|
+
/** Remove the component of `v` along a normal, leaving it in the surface. */
|
|
284
|
+
function project(v, nx, ny, nz) {
|
|
285
|
+
const d = v[0] * nx + v[1] * ny + v[2] * nz;
|
|
286
|
+
v[0] -= nx * d;
|
|
287
|
+
v[1] -= ny * d;
|
|
288
|
+
v[2] -= nz * d;
|
|
289
|
+
}
|
|
290
|
+
function rotate(bodies, i, src, at, out) {
|
|
291
|
+
const x = src[at] ?? 0;
|
|
292
|
+
const y = src[at + 1] ?? 0;
|
|
293
|
+
const z = src[at + 2] ?? 0;
|
|
294
|
+
const qx = bodies.rotX[i] ?? 0;
|
|
295
|
+
const qy = bodies.rotY[i] ?? 0;
|
|
296
|
+
const qz = bodies.rotZ[i] ?? 0;
|
|
297
|
+
const qw = bodies.rotW[i] ?? 1;
|
|
298
|
+
const tx = 2 * (qy * z - qz * y);
|
|
299
|
+
const ty = 2 * (qz * x - qx * z);
|
|
300
|
+
const tz = 2 * (qx * y - qy * x);
|
|
301
|
+
out[0] = x + qw * tx + (qy * tz - qz * ty);
|
|
302
|
+
out[1] = y + qw * ty + (qz * tx - qx * tz);
|
|
303
|
+
out[2] = z + qw * tz + (qx * ty - qy * tx);
|
|
304
|
+
}
|
|
305
|
+
/** A direction in the body's frame, rotated into world. */
|
|
306
|
+
function axis(bodies, i, x, y, z, out) {
|
|
307
|
+
const qx = bodies.rotX[i] ?? 0;
|
|
308
|
+
const qy = bodies.rotY[i] ?? 0;
|
|
309
|
+
const qz = bodies.rotZ[i] ?? 0;
|
|
310
|
+
const qw = bodies.rotW[i] ?? 1;
|
|
311
|
+
const tx = 2 * (qy * z - qz * y);
|
|
312
|
+
const ty = 2 * (qz * x - qx * z);
|
|
313
|
+
const tz = 2 * (qx * y - qy * x);
|
|
314
|
+
out[0] = x + qw * tx + (qy * tz - qz * ty);
|
|
315
|
+
out[1] = y + qw * ty + (qz * tx - qx * tz);
|
|
316
|
+
out[2] = z + qw * tz + (qx * ty - qy * tx);
|
|
317
|
+
}
|
|
318
|
+
function pointVelocity(bodies, i, r, out) {
|
|
319
|
+
const wx = bodies.angX[i] ?? 0;
|
|
320
|
+
const wy = bodies.angY[i] ?? 0;
|
|
321
|
+
const wz = bodies.angZ[i] ?? 0;
|
|
322
|
+
out[0] = (bodies.velX[i] ?? 0) + (wy * r[2] - wz * r[1]);
|
|
323
|
+
out[1] = (bodies.velY[i] ?? 0) + (wz * r[0] - wx * r[2]);
|
|
324
|
+
out[2] = (bodies.velZ[i] ?? 0) + (wx * r[1] - wy * r[0]);
|
|
325
|
+
}
|
|
326
|
+
function applyImpulse(bodies, i, r, px, py, pz) {
|
|
327
|
+
const im = bodies.invMass[i] ?? 0;
|
|
328
|
+
if (im === 0)
|
|
329
|
+
return;
|
|
330
|
+
bodies.velX[i] = (bodies.velX[i] ?? 0) + px * im;
|
|
331
|
+
bodies.velY[i] = (bodies.velY[i] ?? 0) + py * im;
|
|
332
|
+
bodies.velZ[i] = (bodies.velZ[i] ?? 0) + pz * im;
|
|
333
|
+
bodies.worldInverseInertia(i, INV_I);
|
|
334
|
+
const tx = r[1] * pz - r[2] * py;
|
|
335
|
+
const ty = r[2] * px - r[0] * pz;
|
|
336
|
+
const tz = r[0] * py - r[1] * px;
|
|
337
|
+
bodies.angX[i] =
|
|
338
|
+
(bodies.angX[i] ?? 0) + (INV_I[0] ?? 0) * tx + (INV_I[3] ?? 0) * ty + (INV_I[4] ?? 0) * tz;
|
|
339
|
+
bodies.angY[i] =
|
|
340
|
+
(bodies.angY[i] ?? 0) + (INV_I[3] ?? 0) * tx + (INV_I[1] ?? 0) * ty + (INV_I[5] ?? 0) * tz;
|
|
341
|
+
bodies.angZ[i] =
|
|
342
|
+
(bodies.angZ[i] ?? 0) + (INV_I[4] ?? 0) * tx + (INV_I[5] ?? 0) * ty + (INV_I[2] ?? 0) * tz;
|
|
343
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Islands solved on other threads, and every way that can decline to happen.
|
|
3
|
+
*
|
|
4
|
+
* **The handshake was never the problem.** `executor.ts` measured a fork-join over a
|
|
5
|
+
* `SharedArrayBuffer` at 11.8 microseconds for four workers against 2.6 milliseconds of solve. What
|
|
6
|
+
* stops a pool is that `SharedArrayBuffer` does not exist on a page that is not cross-origin
|
|
7
|
+
* isolated, and that is a header on the consumer's site rather than anything the engine can arrange.
|
|
8
|
+
* So this module's real subject is the four ways it can fail to start and the one way it must never
|
|
9
|
+
* fail: quietly.
|
|
10
|
+
*
|
|
11
|
+
* **Islands are claimed with `Atomics.add` on a cursor, which balances load and costs nothing.**
|
|
12
|
+
* Handing work out in arrival order is normally a determinism hazard. It is not one here, because
|
|
13
|
+
* two islands never write the same body — the property `DisjointExecutor` asserts — so the result
|
|
14
|
+
* does not depend on who solved what or when. A pool of eight and a pool of one produce the same
|
|
15
|
+
* bits, and so does the serial runner.
|
|
16
|
+
*
|
|
17
|
+
* **The caller solves its own share.** That is what keeps the join short on a browser's main thread,
|
|
18
|
+
* where `Atomics.wait` throws and the wait has to be a spin: by the time the caller runs out of
|
|
19
|
+
* islands to claim, the only thing left to wait for is the imbalance.
|
|
20
|
+
*/
|
|
21
|
+
import { SolveStage, StagedExecutor } from './stage.ts';
|
|
22
|
+
import type { Executor } from './executor.ts';
|
|
23
|
+
/**
|
|
24
|
+
* The part of a worker this pool uses.
|
|
25
|
+
*
|
|
26
|
+
* A browser `Worker` satisfies it and so does `node:worker_threads`, which is what lets the gate run
|
|
27
|
+
* the real entry point in Node instead of a stand-in that would prove nothing about threads.
|
|
28
|
+
*/
|
|
29
|
+
export interface PoolWorker {
|
|
30
|
+
postMessage(value: unknown): void;
|
|
31
|
+
terminate(): void | Promise<number>;
|
|
32
|
+
}
|
|
33
|
+
export declare const CTL_EPOCH = 0;
|
|
34
|
+
export declare const CTL_CURSOR = 1;
|
|
35
|
+
export declare const CTL_ISLANDS = 2;
|
|
36
|
+
export declare const CTL_DONE = 3;
|
|
37
|
+
export declare const CTL_ACK = 4;
|
|
38
|
+
export declare const CTL_RELOAD = 5;
|
|
39
|
+
export declare const CTL_STOP = 6;
|
|
40
|
+
/** One slot per worker from here, counting the islands it has solved. */
|
|
41
|
+
export declare const CTL_SOLVED = 7;
|
|
42
|
+
/** What a consumer asked for. */
|
|
43
|
+
export interface PoolRequest {
|
|
44
|
+
workers: number;
|
|
45
|
+
spawn?: () => PoolWorker;
|
|
46
|
+
}
|
|
47
|
+
/** What they got, and why, in a sentence fit to put on a debug overlay. */
|
|
48
|
+
export interface PoolOutcome {
|
|
49
|
+
executor: Executor | null;
|
|
50
|
+
running: number;
|
|
51
|
+
reason: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Start a pool, or say why there is none.
|
|
55
|
+
*
|
|
56
|
+
* Never throws. Every path out sets a reason, because a consumer who asked for four workers and got
|
|
57
|
+
* none is owed the difference between "your browser will not let me" and "your bundler could not
|
|
58
|
+
* find the worker". A silent fallback here is a frame rate nobody can attribute, which is the exact
|
|
59
|
+
* complaint `capabilityClamp` exists to answer elsewhere in this engine.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createIslandPool(request: PoolRequest): PoolOutcome;
|
|
62
|
+
/**
|
|
63
|
+
* The pool itself: `StagedExecutor` with the serial solve replaced by a fork and a join.
|
|
64
|
+
*
|
|
65
|
+
* Everything about mirroring the state is inherited, which is what lets `stage.test.ts` hold the
|
|
66
|
+
* mirror to bit-identical physics with no threads in the picture and leaves this class responsible
|
|
67
|
+
* for one question only.
|
|
68
|
+
*/
|
|
69
|
+
export declare class WorkerPoolExecutor extends StagedExecutor implements Executor {
|
|
70
|
+
readonly name = "workers";
|
|
71
|
+
/** Set when the pool has given up. Read by `PhysicsWorld` so `parallelism` stops claiming workers. */
|
|
72
|
+
failure: string | null;
|
|
73
|
+
private readonly control;
|
|
74
|
+
private readonly blocking;
|
|
75
|
+
private posted;
|
|
76
|
+
private started;
|
|
77
|
+
private readonly workers;
|
|
78
|
+
constructor(workers: PoolWorker[]);
|
|
79
|
+
/** How many islands each worker has solved since the pool started. The engagement evidence. */
|
|
80
|
+
get solvedPerWorker(): number[];
|
|
81
|
+
/** Stop every worker. Idempotent, because a consumer may dispose a world twice. */
|
|
82
|
+
dispose(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Fork, take a share, join. Returns false when the pool failed, which sends the tick back to the
|
|
85
|
+
* live state and a serial solve.
|
|
86
|
+
*/
|
|
87
|
+
protected solve(count: number, stage: SolveStage): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Wait for every worker to finish this epoch, and give up rather than freeze.
|
|
90
|
+
*
|
|
91
|
+
* **A worker that dies mid-island is the failure this deadline exists for.** Without it the join
|
|
92
|
+
* spins until the tab is closed, which is a frozen game and the worst outcome available. With it
|
|
93
|
+
* the tick is re-solved from the live state, which is untouched until `pull`, and the pool is
|
|
94
|
+
* marked dead so every later tick is serial with a reason attached.
|
|
95
|
+
*/
|
|
96
|
+
private join;
|
|
97
|
+
/**
|
|
98
|
+
* Hand the workers a new staging buffer and wait for them to adopt it.
|
|
99
|
+
*
|
|
100
|
+
* A worker parked in `Atomics.wait` cannot process a message, so it has to be let out first:
|
|
101
|
+
* `CTL_RELOAD` is set, the epoch is bumped, and a woken worker returns to its event loop instead
|
|
102
|
+
* of solving. It adopts the buffer in its message handler, acknowledges through `CTL_ACK`, and
|
|
103
|
+
* parks again. Rare by construction — a re-plan happens when a set doubles, which is about
|
|
104
|
+
* `log2(n)` times in a world's life and never inside a tick.
|
|
105
|
+
*/
|
|
106
|
+
private reload;
|
|
107
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Islands solved on other threads, and every way that can decline to happen.
|
|
3
|
+
*
|
|
4
|
+
* **The handshake was never the problem.** `executor.ts` measured a fork-join over a
|
|
5
|
+
* `SharedArrayBuffer` at 11.8 microseconds for four workers against 2.6 milliseconds of solve. What
|
|
6
|
+
* stops a pool is that `SharedArrayBuffer` does not exist on a page that is not cross-origin
|
|
7
|
+
* isolated, and that is a header on the consumer's site rather than anything the engine can arrange.
|
|
8
|
+
* So this module's real subject is the four ways it can fail to start and the one way it must never
|
|
9
|
+
* fail: quietly.
|
|
10
|
+
*
|
|
11
|
+
* **Islands are claimed with `Atomics.add` on a cursor, which balances load and costs nothing.**
|
|
12
|
+
* Handing work out in arrival order is normally a determinism hazard. It is not one here, because
|
|
13
|
+
* two islands never write the same body — the property `DisjointExecutor` asserts — so the result
|
|
14
|
+
* does not depend on who solved what or when. A pool of eight and a pool of one produce the same
|
|
15
|
+
* bits, and so does the serial runner.
|
|
16
|
+
*
|
|
17
|
+
* **The caller solves its own share.** That is what keeps the join short on a browser's main thread,
|
|
18
|
+
* where `Atomics.wait` throws and the wait has to be a spin: by the time the caller runs out of
|
|
19
|
+
* islands to claim, the only thing left to wait for is the imbalance.
|
|
20
|
+
*/
|
|
21
|
+
import { solveIslandInto } from './islandSolve.js';
|
|
22
|
+
import { SolveStage, StagedExecutor } from './stage.js';
|
|
23
|
+
/* Slots in the control block, which is its own buffer and outlives every re-plan of the stage. */
|
|
24
|
+
export const CTL_EPOCH = 0;
|
|
25
|
+
export const CTL_CURSOR = 1;
|
|
26
|
+
export const CTL_ISLANDS = 2;
|
|
27
|
+
export const CTL_DONE = 3;
|
|
28
|
+
export const CTL_ACK = 4;
|
|
29
|
+
export const CTL_RELOAD = 5;
|
|
30
|
+
export const CTL_STOP = 6;
|
|
31
|
+
/** One slot per worker from here, counting the islands it has solved. */
|
|
32
|
+
export const CTL_SOLVED = 7;
|
|
33
|
+
/** How long a join waits before it gives up on a worker and solves the tick itself. */
|
|
34
|
+
const JOIN_DEADLINE_MS = 250;
|
|
35
|
+
function sharedAlloc(bytes) {
|
|
36
|
+
return new SharedArrayBuffer(bytes);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether this thread may block.
|
|
40
|
+
*
|
|
41
|
+
* `Atomics.wait` is refused on a browser's main thread and permitted everywhere else, including a
|
|
42
|
+
* worker and Node. Asked by trying it, because the alternative is sniffing for `window` and being
|
|
43
|
+
* wrong in every runtime nobody thought of. The probe waits zero milliseconds on a value that does
|
|
44
|
+
* not match, so it returns `not-equal` immediately and blocks nothing.
|
|
45
|
+
*/
|
|
46
|
+
function canBlock() {
|
|
47
|
+
try {
|
|
48
|
+
const probe = new Int32Array(new SharedArrayBuffer(4));
|
|
49
|
+
Atomics.wait(probe, 0, 1, 0);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start a pool, or say why there is none.
|
|
58
|
+
*
|
|
59
|
+
* Never throws. Every path out sets a reason, because a consumer who asked for four workers and got
|
|
60
|
+
* none is owed the difference between "your browser will not let me" and "your bundler could not
|
|
61
|
+
* find the worker". A silent fallback here is a frame rate nobody can attribute, which is the exact
|
|
62
|
+
* complaint `capabilityClamp` exists to answer elsewhere in this engine.
|
|
63
|
+
*/
|
|
64
|
+
export function createIslandPool(request) {
|
|
65
|
+
const wanted = Math.max(0, Math.floor(request.workers));
|
|
66
|
+
if (wanted === 0)
|
|
67
|
+
return { executor: null, running: 0, reason: '' };
|
|
68
|
+
if (typeof SharedArrayBuffer === 'undefined') {
|
|
69
|
+
return {
|
|
70
|
+
executor: null,
|
|
71
|
+
running: 0,
|
|
72
|
+
reason: 'this page is not cross-origin isolated, so SharedArrayBuffer is undefined. Serve it with ' +
|
|
73
|
+
'Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp ' +
|
|
74
|
+
'(or credentialless) to enable the pool.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const spawn = request.spawn ?? defaultSpawn;
|
|
78
|
+
if (spawn === null) {
|
|
79
|
+
return {
|
|
80
|
+
executor: null,
|
|
81
|
+
running: 0,
|
|
82
|
+
reason: 'this runtime has no Worker, and no spawn was given.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const workers = [];
|
|
86
|
+
try {
|
|
87
|
+
for (let i = 0; i < wanted; i++)
|
|
88
|
+
workers.push(spawn());
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
for (const worker of workers)
|
|
92
|
+
worker.terminate();
|
|
93
|
+
const message = String(error?.message ?? error);
|
|
94
|
+
return {
|
|
95
|
+
executor: null,
|
|
96
|
+
running: 0,
|
|
97
|
+
reason: `the worker entry failed to load: ${message}. Pass a spawn option building the worker ` +
|
|
98
|
+
'with your own bundler if it does not understand a worker entry point.',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { executor: new WorkerPoolExecutor(workers), running: workers.length, reason: '' };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The worker this package ships, built the way a bundler understands.
|
|
105
|
+
*
|
|
106
|
+
* Vite, webpack 5 and Parcel all resolve `new URL('./x.ts', import.meta.url)` inside a `new Worker`.
|
|
107
|
+
* A build that does not throws, which becomes a reason and serial physics — and `spawn` is the way
|
|
108
|
+
* out, which is why this is a default and not the only path. Null where there is no `Worker` at all.
|
|
109
|
+
*/
|
|
110
|
+
const defaultSpawn = typeof Worker === 'undefined'
|
|
111
|
+
? null
|
|
112
|
+
: () => new Worker(new URL('./islandWorker.ts', import.meta.url), { type: 'module' });
|
|
113
|
+
/**
|
|
114
|
+
* The pool itself: `StagedExecutor` with the serial solve replaced by a fork and a join.
|
|
115
|
+
*
|
|
116
|
+
* Everything about mirroring the state is inherited, which is what lets `stage.test.ts` hold the
|
|
117
|
+
* mirror to bit-identical physics with no threads in the picture and leaves this class responsible
|
|
118
|
+
* for one question only.
|
|
119
|
+
*/
|
|
120
|
+
export class WorkerPoolExecutor extends StagedExecutor {
|
|
121
|
+
name = 'workers';
|
|
122
|
+
/** Set when the pool has given up. Read by `PhysicsWorld` so `parallelism` stops claiming workers. */
|
|
123
|
+
failure = null;
|
|
124
|
+
control;
|
|
125
|
+
blocking = canBlock();
|
|
126
|
+
posted = -1;
|
|
127
|
+
started = false;
|
|
128
|
+
workers;
|
|
129
|
+
constructor(workers) {
|
|
130
|
+
super(sharedAlloc);
|
|
131
|
+
this.workers = workers;
|
|
132
|
+
this.control = new Int32Array(new SharedArrayBuffer((CTL_SOLVED + workers.length) * 4));
|
|
133
|
+
}
|
|
134
|
+
/** How many islands each worker has solved since the pool started. The engagement evidence. */
|
|
135
|
+
get solvedPerWorker() {
|
|
136
|
+
return this.workers.map((_, i) => Atomics.load(this.control, CTL_SOLVED + i));
|
|
137
|
+
}
|
|
138
|
+
/** Stop every worker. Idempotent, because a consumer may dispose a world twice. */
|
|
139
|
+
dispose() {
|
|
140
|
+
if (this.workers.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
Atomics.store(this.control, CTL_STOP, 1);
|
|
143
|
+
Atomics.add(this.control, CTL_EPOCH, 1);
|
|
144
|
+
Atomics.notify(this.control, CTL_EPOCH);
|
|
145
|
+
for (const worker of this.workers)
|
|
146
|
+
worker.terminate();
|
|
147
|
+
this.workers.length = 0;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Fork, take a share, join. Returns false when the pool failed, which sends the tick back to the
|
|
151
|
+
* live state and a serial solve.
|
|
152
|
+
*/
|
|
153
|
+
solve(count, stage) {
|
|
154
|
+
if (this.failure !== null || this.workers.length === 0)
|
|
155
|
+
return false;
|
|
156
|
+
const ctl = this.control;
|
|
157
|
+
if (!this.started || stage.generation !== this.posted) {
|
|
158
|
+
if (!this.reload(stage))
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
Atomics.store(ctl, CTL_CURSOR, 0);
|
|
162
|
+
Atomics.store(ctl, CTL_ISLANDS, count);
|
|
163
|
+
Atomics.store(ctl, CTL_DONE, 0);
|
|
164
|
+
Atomics.add(ctl, CTL_EPOCH, 1);
|
|
165
|
+
Atomics.notify(ctl, CTL_EPOCH);
|
|
166
|
+
/* The caller's own share, from the same cursor, so what is left to wait for is the imbalance
|
|
167
|
+
and not the solve. */
|
|
168
|
+
for (;;) {
|
|
169
|
+
const island = Atomics.add(ctl, CTL_CURSOR, 1);
|
|
170
|
+
if (island >= count)
|
|
171
|
+
break;
|
|
172
|
+
solveIslandInto(stage.state, island);
|
|
173
|
+
}
|
|
174
|
+
return this.join();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Wait for every worker to finish this epoch, and give up rather than freeze.
|
|
178
|
+
*
|
|
179
|
+
* **A worker that dies mid-island is the failure this deadline exists for.** Without it the join
|
|
180
|
+
* spins until the tab is closed, which is a frozen game and the worst outcome available. With it
|
|
181
|
+
* the tick is re-solved from the live state, which is untouched until `pull`, and the pool is
|
|
182
|
+
* marked dead so every later tick is serial with a reason attached.
|
|
183
|
+
*/
|
|
184
|
+
join() {
|
|
185
|
+
const ctl = this.control;
|
|
186
|
+
const wanted = this.workers.length;
|
|
187
|
+
const deadline = Date.now() + JOIN_DEADLINE_MS;
|
|
188
|
+
for (;;) {
|
|
189
|
+
const done = Atomics.load(ctl, CTL_DONE);
|
|
190
|
+
if (done >= wanted)
|
|
191
|
+
return true;
|
|
192
|
+
if (Date.now() > deadline) {
|
|
193
|
+
this.failure =
|
|
194
|
+
'a worker stopped answering, so island solving went back to this thread. The world is ' +
|
|
195
|
+
'correct and serial from here.';
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (this.blocking)
|
|
199
|
+
Atomics.wait(ctl, CTL_DONE, done, 50);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Hand the workers a new staging buffer and wait for them to adopt it.
|
|
204
|
+
*
|
|
205
|
+
* A worker parked in `Atomics.wait` cannot process a message, so it has to be let out first:
|
|
206
|
+
* `CTL_RELOAD` is set, the epoch is bumped, and a woken worker returns to its event loop instead
|
|
207
|
+
* of solving. It adopts the buffer in its message handler, acknowledges through `CTL_ACK`, and
|
|
208
|
+
* parks again. Rare by construction — a re-plan happens when a set doubles, which is about
|
|
209
|
+
* `log2(n)` times in a world's life and never inside a tick.
|
|
210
|
+
*/
|
|
211
|
+
reload(stage) {
|
|
212
|
+
const ctl = this.control;
|
|
213
|
+
Atomics.store(ctl, CTL_ACK, 0);
|
|
214
|
+
Atomics.store(ctl, CTL_RELOAD, 1);
|
|
215
|
+
Atomics.add(ctl, CTL_EPOCH, 1);
|
|
216
|
+
Atomics.notify(ctl, CTL_EPOCH);
|
|
217
|
+
for (let i = 0; i < this.workers.length; i++) {
|
|
218
|
+
this.workers[i].postMessage({
|
|
219
|
+
kind: 'stage',
|
|
220
|
+
index: i,
|
|
221
|
+
control: ctl.buffer,
|
|
222
|
+
buffer: stage.buffer,
|
|
223
|
+
plan: stage.plan,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const deadline = Date.now() + JOIN_DEADLINE_MS;
|
|
227
|
+
while (Atomics.load(ctl, CTL_ACK) < this.workers.length) {
|
|
228
|
+
if (Date.now() > deadline) {
|
|
229
|
+
this.failure =
|
|
230
|
+
'the workers did not take the shared buffer, so island solving stayed on this thread.';
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
Atomics.store(ctl, CTL_RELOAD, 0);
|
|
235
|
+
this.posted = stage.generation;
|
|
236
|
+
this.started = true;
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|