@adobe/ecs 0.2.3 → 0.3.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/dist/implementation/compiler/augmentations.js +8 -1
- package/dist/implementation/compiler/augmentations.js.map +1 -1
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +5 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/compiler/type-schemas.d.ts +1 -0
- package/dist/implementation/compiler/type-schemas.js +35 -2
- package/dist/implementation/compiler/type-schemas.js.map +1 -1
- package/dist/implementation/world/activate.d.ts +14 -3
- package/dist/implementation/world/activate.js +28 -10
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +41 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +57 -6
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +228 -93
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -0
- package/dist/implementation/world/finalizers.js +60 -10
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/program.js +5 -1
- package/dist/implementation/world/program.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +15 -0
- package/dist/implementation/world/state.js +22 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +81 -24
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +4 -2
- package/dist/implementation/world/tables.js +137 -38
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +40 -4
- package/dist/interface/finalizer/create.d.ts +8 -6
- package/dist/interface/finalizer/create.js +45 -10
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +26 -4
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/index.d.ts +1 -1
- package/dist/interface/registry/index.js +1 -1
- package/dist/interface/registry/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +4 -0
- package/dist/interface/registry/names.js +20 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +29 -1
- package/dist/interface/registry/schedule.js +261 -25
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +21 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +31 -5
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +7 -0
- package/dist/interface/system/render.js +2 -1
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +14 -1
- package/dist/interface/system/resolve.js +50 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/01-resources.md +4 -3
- package/src/guide/03-systems.out +1 -1
- package/src/guide/05-scheduling.md +12 -6
- package/src/guide/05-scheduling.out +1 -0
- package/src/guide/05-scheduling.ts +17 -0
- package/src/guide/06-sync-points.md +20 -8
- package/src/guide/06-sync-points.out +3 -2
- package/src/guide/06-sync-points.ts +24 -5
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +6 -153
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +5 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +32 -10
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +37 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +61 -6
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +310 -0
- package/src/implementation/world/execute.ts +222 -86
- package/src/implementation/world/finalizers.ts +63 -11
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +12 -1
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +27 -0
- package/src/implementation/world/sync-points.test.ts +147 -0
- package/src/implementation/world/synchronize.ts +73 -20
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +139 -4
- package/src/implementation/world/tables.ts +118 -35
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
- package/src/implementation/world/watch-traits.test-helper.ts +10 -0
- package/src/implementation/world/watch-traits.test.ts +62 -0
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +33 -4
- package/src/interface/finalizer/create.test.ts +6 -1
- package/src/interface/finalizer/create.ts +45 -12
- package/src/interface/finalizer/declaration.ts +31 -4
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +275 -29
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +17 -2
- package/src/interface/system/commands.ts +28 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +10 -2
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +98 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +7 -0
- package/src/interface/system/render.ts +2 -1
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +60 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +20 -0
- package/src/perftest/chained.ts +108 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -3,14 +3,15 @@ import type { Leaf } from '../../interface/system/leaf.js';
|
|
|
3
3
|
import type { Chunk, ChunkResult, Runner, Workers } from './runner.js';
|
|
4
4
|
import type { Schedule } from '../../interface/registry/schedule.js';
|
|
5
5
|
import type { Archetype, Storage } from './archetype.js';
|
|
6
|
-
import { countRan, idOf, startRan, type State } from './state.js';
|
|
6
|
+
import { countRan, countRanHere, idOf, startRan, type State } from './state.js';
|
|
7
7
|
import type { GpuEngine } from './gpu.js';
|
|
8
8
|
import { applySync, prepareSync, syncPending } from './synchronize.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resourcesWritten } from './resource-changes.js';
|
|
10
|
+
import { activate, activationPending } from './activate.js';
|
|
10
11
|
import { markRun } from './move-log.js';
|
|
11
12
|
import { postTails, settleAll, settleKeys, settleRunners, tailsOf, type TailOutcome } from './tails.js';
|
|
12
13
|
import { settlePosts, settleObserverKeys, settleObserverRunners } from './joined-observer.js';
|
|
13
|
-
import { locate } from './entities.js';
|
|
14
|
+
import { locate, reserved } from './entities.js';
|
|
14
15
|
import { clearBuffer, createBuffer } from './buffer.js';
|
|
15
16
|
import { drainSegment, type Segment } from './command-ring.js';
|
|
16
17
|
import { combine, identityOf, writeReduction, type ReductionTarget } from './reductions.js';
|
|
@@ -39,10 +40,10 @@ export type ExecuteContext = {
|
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* Runs a schedule tier by tier,
|
|
43
|
-
* first so workers run while main-thread systems do; commands merge in
|
|
44
|
-
* ran them. A failing system stops the run after its tier finishes:
|
|
45
|
-
* apply. `system` is the one the run was given, which the GPU's errors are reported under.
|
|
43
|
+
* Runs a schedule tier by tier, applying a sync point after each tier its schedule says and after the last, and reports
|
|
44
|
+
* what went wrong. Within a tier, kernels start first so workers run while main-thread systems do; commands merge in
|
|
45
|
+
* tier order, then chunk order, whichever thread ran them. A failing system stops the run after its tier finishes:
|
|
46
|
+
* later tiers are skipped, and commands so far still apply. `system` is the one the run was given, which the GPU's errors are reported under.
|
|
46
47
|
*/
|
|
47
48
|
export const execute = (context: ExecuteContext, schedule: Schedule, args: unknown, system: { readonly name: string }): RunReport | Promise<RunReport> => {
|
|
48
49
|
const { gpu } = context.state;
|
|
@@ -101,6 +102,18 @@ type Prepared = {
|
|
|
101
102
|
readonly remotes: number[];
|
|
102
103
|
/** The indexes each tier's runners look up through, refreshed before it. */
|
|
103
104
|
readonly uses: (readonly IndexUse[])[];
|
|
105
|
+
/**
|
|
106
|
+
* By tier, the last tier of the pass starting there: tiers whose systems all run on the world's thread as written,
|
|
107
|
+
* which look up through no index from the second on, run in one pass while their sync points apply nothing. Its own
|
|
108
|
+
* tier for any other.
|
|
109
|
+
*/
|
|
110
|
+
readonly passes: number[];
|
|
111
|
+
/**
|
|
112
|
+
* By tier within a pass of several, how many runners it and the pass's later tiers hold, and whether any runs on the
|
|
113
|
+
* GPU: what a pass entered there counts once it ran to its end.
|
|
114
|
+
*/
|
|
115
|
+
readonly passSystems: number[];
|
|
116
|
+
readonly passGpu: boolean[];
|
|
104
117
|
/** The world's `runnersVersion` its runners were read at. */
|
|
105
118
|
version: number;
|
|
106
119
|
readonly started: (readonly Chunk[])[][];
|
|
@@ -128,6 +141,8 @@ type Prepared = {
|
|
|
128
141
|
chunk: number;
|
|
129
142
|
error: unknown;
|
|
130
143
|
tierFailed: boolean;
|
|
144
|
+
/** How many commands the run's sync points dropped so far. */
|
|
145
|
+
dropped: number;
|
|
131
146
|
};
|
|
132
147
|
|
|
133
148
|
/** A schedule's tails: the schedule without them, and their runners, leaves and tiers in schedule order. */
|
|
@@ -139,6 +154,7 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
|
|
|
139
154
|
const leaves: Leaf[] = [];
|
|
140
155
|
const tiers: number[] = [];
|
|
141
156
|
const rest: Leaf[][] = [];
|
|
157
|
+
const syncs: (Schedule["syncs"][number])[] = [];
|
|
142
158
|
schedule.tiers.forEach((tier, t) => {
|
|
143
159
|
const kept = tier.filter((leaf, i) => {
|
|
144
160
|
if (!flags[t][i]) return true;
|
|
@@ -147,9 +163,13 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
|
|
|
147
163
|
return false;
|
|
148
164
|
});
|
|
149
165
|
if (kept.length > 0) rest.push(kept);
|
|
166
|
+
// A sync point after a tier left empty follows the tier kept before it.
|
|
167
|
+
const reasons = schedule.syncs[t];
|
|
168
|
+
if (reasons !== null && rest.length > 0) syncs[rest.length - 1] = [...syncs[rest.length - 1] ?? [], ...reasons];
|
|
150
169
|
});
|
|
151
170
|
if (leaves.length === 0) return null;
|
|
152
|
-
|
|
171
|
+
const kept = rest.map((_, t) => t < rest.length - 1 ? syncs[t] ?? null : null);
|
|
172
|
+
return { schedule: { tiers: rest, reductions: schedule.reductions, observer: schedule.observer, syncs: kept }, runners: leaves.map(context.runnerOf), leaves, tiers };
|
|
153
173
|
};
|
|
154
174
|
|
|
155
175
|
type Outcome = { failures: Failure[] | null; skipped: Leaf[] | null };
|
|
@@ -161,10 +181,11 @@ const preparedFor = (context: ExecuteContext, schedule: Schedule): Prepared => {
|
|
|
161
181
|
let prepared = bySchedule.get(schedule);
|
|
162
182
|
if (prepared === undefined) {
|
|
163
183
|
prepared = {
|
|
164
|
-
runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []),
|
|
184
|
+
runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []),
|
|
185
|
+
passes: schedule.tiers.map((_, t) => t), passSystems: schedule.tiers.map(() => 0), passGpu: schedule.tiers.map(() => false), version: -1,
|
|
165
186
|
started: schedule.tiers.map(tier => new Array<readonly Chunk[]>(tier.length)),
|
|
166
187
|
systems: [], tiers: [], tasks: new Map(), warnings: [], warningCount: 0, warned: null, outcome: { failures: null, skipped: null },
|
|
167
|
-
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false,
|
|
188
|
+
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false, dropped: 0,
|
|
168
189
|
};
|
|
169
190
|
bySchedule.set(schedule, prepared);
|
|
170
191
|
}
|
|
@@ -182,9 +203,38 @@ const refresh = (context: ExecuteContext, schedule: Schedule, prepared: Prepared
|
|
|
182
203
|
prepared.remotes[t] = remote;
|
|
183
204
|
prepared.uses[t] = mergeUses(runners);
|
|
184
205
|
}
|
|
206
|
+
passesOf(schedule, prepared);
|
|
185
207
|
prepared.version = context.state.runnersVersion;
|
|
186
208
|
};
|
|
187
209
|
|
|
210
|
+
/** Whether every runner of a tier runs on the world's thread as written: none a kernel or on a joined thread. */
|
|
211
|
+
const asWritten = (runners: readonly Runner[]): boolean => {
|
|
212
|
+
for (let i = 0; i < runners.length; i++) if (runners[i].kernel !== null || runners[i].remote !== null) return false;
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** Finds a schedule's passes (`Prepared["passes"]`) from its runners as read now. */
|
|
217
|
+
const passesOf = (schedule: Schedule, prepared: Prepared): void => {
|
|
218
|
+
const { runners, uses, passes, passSystems, passGpu } = prepared;
|
|
219
|
+
let last = -1;
|
|
220
|
+
for (let t = 0; t < schedule.tiers.length; t++) {
|
|
221
|
+
if (!asWritten(runners[t])) last = t;
|
|
222
|
+
else if (last < t) {
|
|
223
|
+
last = t;
|
|
224
|
+
while (last + 1 < schedule.tiers.length && uses[last + 1].length === 0 && asWritten(runners[last + 1])) last++;
|
|
225
|
+
}
|
|
226
|
+
passes[t] = last;
|
|
227
|
+
}
|
|
228
|
+
for (let t = schedule.tiers.length - 1; t >= 0; t--) {
|
|
229
|
+
const tier = runners[t];
|
|
230
|
+
const continues = passes[t] > t;
|
|
231
|
+
let gpu = continues && passGpu[t + 1];
|
|
232
|
+
for (let i = 0; i < tier.length; i++) if (tier[i].placement !== "cpu") gpu = true;
|
|
233
|
+
passSystems[t] = tier.length + (continues ? passSystems[t + 1] : 0);
|
|
234
|
+
passGpu[t] = gpu;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
188
238
|
/**
|
|
189
239
|
* Drains a worker's segment into a buffer of its own, queued in its place: commands, spawn blocks staged to commit in
|
|
190
240
|
* its place, and the reservations it left unused. Fails if it doesn't start with its task's merge tag.
|
|
@@ -315,8 +365,8 @@ const runFused = async (context: ExecuteContext, schedule: Schedule, prepared: P
|
|
|
315
365
|
const { gpu } = state;
|
|
316
366
|
let last = first;
|
|
317
367
|
if (prepared.version !== state.runnersVersion) refresh(context, schedule, prepared);
|
|
318
|
-
// A tier using an index starts a task of its own, after the index is refreshed for it.
|
|
319
|
-
while (last + 1 < schedule.tiers.length && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
|
|
368
|
+
// A tier using an index starts a task of its own, after the index is refreshed for it, as does one after a sync point.
|
|
369
|
+
while (last + 1 < schedule.tiers.length && schedule.syncs[last] === null && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
|
|
320
370
|
if (channel.busy()) await channel.idle();
|
|
321
371
|
for (let v = first; v <= last; v++) {
|
|
322
372
|
if (state.tails.length !== 0) {
|
|
@@ -431,7 +481,7 @@ const endTier = (state: State, gpu: GpuEngine | null, prepared: Prepared, schedu
|
|
|
431
481
|
};
|
|
432
482
|
|
|
433
483
|
/**
|
|
434
|
-
* Runs a schedule tier by tier
|
|
484
|
+
* Runs a schedule tier by tier with its sync points, and reports what went wrong (`execute`). Runs with no GPU,
|
|
435
485
|
* tails or observer posts awaiting nothing, so a run that waits for nothing allocates no async frame: at the first
|
|
436
486
|
* tier on a joined thread, index build across the pool or chunk on workers, `executeFrom` goes on from there.
|
|
437
487
|
*/
|
|
@@ -454,11 +504,11 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
454
504
|
if (schedule.reductions.size !== 0) for (const [key, contributors] of schedule.reductions) (folds ??= new Map()).set(key, { target: null, values: [], remaining: contributors, ran: false, failed: false });
|
|
455
505
|
const { outcome } = prepared;
|
|
456
506
|
prepared.warningCount = 0;
|
|
507
|
+
prepared.dropped = 0;
|
|
457
508
|
outcome.failures = null;
|
|
458
509
|
outcome.skipped = null;
|
|
459
510
|
if (state.gpu !== null || tailing !== null || state.tails.length !== 0 || state.observer.posts.length !== 0) return executeFrom(context, schedule, prepared, folds, args, tailing, 0, null, null);
|
|
460
511
|
let handed: Promise<RunReport> | null = null;
|
|
461
|
-
let dropped = 0;
|
|
462
512
|
try {
|
|
463
513
|
for (let t = 0; t < schedule.tiers.length; t++) {
|
|
464
514
|
// A tick in progress takes up runners replaced since it started from its next tier on.
|
|
@@ -470,41 +520,85 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
470
520
|
if (building !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, building, null);
|
|
471
521
|
if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
|
|
472
522
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
523
|
+
const last = prepared.passes[t];
|
|
524
|
+
if (last > t) {
|
|
525
|
+
const reached = runPass(context, prepared, schedule, folds, args, t, last);
|
|
526
|
+
if (typeof reached !== "number") return handed = reached;
|
|
527
|
+
if (reached < 0) break;
|
|
528
|
+
t = reached;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
startTier(context, prepared, t, args);
|
|
532
|
+
const waiting = settleTier(state, folds, prepared, schedule.tiers[t], t, nothing);
|
|
533
|
+
if (waiting !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, null, waiting);
|
|
534
|
+
if (endTier(state, null, prepared, schedule, t)) break;
|
|
535
|
+
}
|
|
536
|
+
if (schedule.syncs[t] !== null && !idleSync(state)) syncNow(context, prepared, false);
|
|
477
537
|
}
|
|
478
538
|
}
|
|
479
539
|
finally {
|
|
480
|
-
if (handed === null)
|
|
540
|
+
if (handed === null) syncNow(context, prepared, true);
|
|
481
541
|
}
|
|
482
|
-
return reportOf(state, prepared
|
|
542
|
+
return reportOf(state, prepared);
|
|
483
543
|
};
|
|
484
544
|
|
|
485
545
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
546
|
+
* Runs tiers `first` to `last`, a pass, one after another with none of the checks between tiers that only a tier on
|
|
547
|
+
* another thread, using an index or after a sync point that applies something needs. Returns the last tier it ran,
|
|
548
|
+
* after which the caller applies its sync point: `last`, or one before it whose sync point applies something or whose
|
|
549
|
+
* bodies replaced runners; -1 when one failed, the tiers after it skipped; or what `executeFrom` returns once one waits
|
|
550
|
+
* for something.
|
|
488
551
|
*/
|
|
489
|
-
const
|
|
552
|
+
const runPass = (context: ExecuteContext, prepared: Prepared, schedule: Schedule, folds: Map<unknown, Fold> | null, args: unknown, first: number, last: number): number | Promise<RunReport> => {
|
|
553
|
+
const { state } = context;
|
|
554
|
+
for (let v = first; v <= last; v++) {
|
|
555
|
+
startTier(context, prepared, v, args);
|
|
556
|
+
const waiting = settleTier(state, folds, prepared, schedule.tiers[v], v, nothing);
|
|
557
|
+
if (waiting !== null) {
|
|
558
|
+
for (let ran = first; ran < v; ran++) countRan(state, prepared.runners[ran]);
|
|
559
|
+
return executeFrom(context, schedule, prepared, folds, args, null, v, null, waiting);
|
|
560
|
+
}
|
|
561
|
+
if (prepared.tierFailed || (v < last && (prepared.version !== state.runnersVersion || (schedule.syncs[v] !== null && !idleSync(state))))) {
|
|
562
|
+
for (let ran = first; ran <= v; ran++) countRan(state, prepared.runners[ran]);
|
|
563
|
+
if (!prepared.tierFailed) return v;
|
|
564
|
+
const { outcome } = prepared;
|
|
565
|
+
for (let later = v + 1; later < schedule.tiers.length; later++) for (const leaf of schedule.tiers[later]) (outcome.skipped ??= []).push(leaf);
|
|
566
|
+
return -1;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
countRanHere(state, prepared.passSystems[first], prepared.passGpu[first]);
|
|
570
|
+
return last;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Whether a sync point within a run, with no joined thread, would apply nothing and leave nothing to do after:
|
|
575
|
+
* nothing queued, drained, unreserved or pending (`syncPending`), no rule or finalizer to activate, and no layout for
|
|
576
|
+
* workers to see.
|
|
577
|
+
*/
|
|
578
|
+
const idleSync = (state: State): boolean => state.queued.length === 0 && state.drained === 0 && state.unused.length === 0
|
|
579
|
+
&& state.channels.length === 0 && state.logs.length === 0 && state.bumped.length === 0 && state.counted.length === 0 && state.carried === null && !state.structure.rederive && state.unplaced.length === 0
|
|
580
|
+
&& (state.resourcesSeen.size === 0 || !resourcesWritten(state)) && !activationPending(state) && !reserved(state.entities);
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A sync point with nothing to wait for: no GPU, tails or observer posts, which `executeTiers` checked before each
|
|
584
|
+
* tier and bodies can't add. The run's `last` lets go of strings no reader holds.
|
|
585
|
+
*/
|
|
586
|
+
const syncNow = (context: ExecuteContext, prepared: Prepared, last: boolean): void => {
|
|
490
587
|
const { state, workers, publish } = context;
|
|
491
|
-
|
|
492
|
-
let dropped = 0;
|
|
493
|
-
// This run's tasks on joined threads are done: they return to their event loops while its sync point applies.
|
|
588
|
+
// The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
|
|
494
589
|
for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
|
|
495
590
|
try {
|
|
496
591
|
if (prepareSync(state)) {
|
|
497
|
-
dropped
|
|
498
|
-
for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
592
|
+
prepared.dropped += state.plan.dropped;
|
|
593
|
+
for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
499
594
|
}
|
|
500
595
|
}
|
|
501
596
|
finally {
|
|
502
597
|
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
503
|
-
state.strings?.settle();
|
|
598
|
+
if (last) state.strings?.settle();
|
|
504
599
|
rewind(state, workers);
|
|
505
600
|
publish();
|
|
506
601
|
}
|
|
507
|
-
return dropped;
|
|
508
602
|
};
|
|
509
603
|
|
|
510
604
|
/**
|
|
@@ -512,12 +606,11 @@ const finishSync = (context: ExecuteContext, prepared: Prepared): number => {
|
|
|
512
606
|
* indexes' build, is done, or once `settling`, what settling it waits for, settles.
|
|
513
607
|
*/
|
|
514
608
|
const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared: Prepared, folds: Map<unknown, Fold> | null, args: unknown, tailing: Tailing | null, first: number, building: Promise<void> | null, settling: Promise<Settled> | null): Promise<RunReport> => {
|
|
515
|
-
const { state, workers
|
|
609
|
+
const { state, workers } = context;
|
|
516
610
|
const { gpu } = state;
|
|
517
611
|
const { outcome } = prepared;
|
|
518
612
|
let built = building;
|
|
519
613
|
let waited = settling;
|
|
520
|
-
let dropped = 0;
|
|
521
614
|
try {
|
|
522
615
|
for (let t = first; t < schedule.tiers.length; t++) {
|
|
523
616
|
const tier = schedule.tiers[t];
|
|
@@ -553,6 +646,10 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
553
646
|
const last = await runFused(context, schedule, prepared, folds, args, t, only);
|
|
554
647
|
if (last < 0) break;
|
|
555
648
|
t = last;
|
|
649
|
+
if (schedule.syncs[t] !== null) {
|
|
650
|
+
const pending = syncPoint(context, prepared, false);
|
|
651
|
+
if (pending !== null) await pending;
|
|
652
|
+
}
|
|
556
653
|
continue;
|
|
557
654
|
}
|
|
558
655
|
if (gpu !== null) await readAhead(state, gpu, runners);
|
|
@@ -578,60 +675,15 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
578
675
|
let waiting = settleTier(state, folds, prepared, tier, t, value);
|
|
579
676
|
while (waiting !== null) waiting = settleTier(state, folds, prepared, tier, t, await waiting);
|
|
580
677
|
if (endTier(state, gpu, prepared, schedule, t)) break;
|
|
678
|
+
if (schedule.syncs[t] !== null) {
|
|
679
|
+
const pending = syncPoint(context, prepared, false);
|
|
680
|
+
if (pending !== null) await pending;
|
|
681
|
+
}
|
|
581
682
|
}
|
|
582
683
|
}
|
|
583
684
|
finally {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
if (gpu !== null) {
|
|
588
|
-
gpu.submit();
|
|
589
|
-
gpu.beforeSync();
|
|
590
|
-
}
|
|
591
|
-
try {
|
|
592
|
-
// A sync point counts as a write to everything a tail still reads.
|
|
593
|
-
if (state.tails.length !== 0) {
|
|
594
|
-
activate(state);
|
|
595
|
-
const waiting = syncPending(state) ? settleAll(state) : null;
|
|
596
|
-
if (waiting !== null) await waiting;
|
|
597
|
-
}
|
|
598
|
-
// Each sync point waits for observer systems on joined threads in progress, and cancels the rest.
|
|
599
|
-
if (state.observer.posts.length !== 0) {
|
|
600
|
-
const waiting = settlePosts(state);
|
|
601
|
-
if (waiting !== null) await waiting;
|
|
602
|
-
}
|
|
603
|
-
if (prepareSync(state)) {
|
|
604
|
-
try {
|
|
605
|
-
if (gpu !== null) {
|
|
606
|
-
// The sync point moves and writes existing rows of these archetypes on the CPU, and
|
|
607
|
-
// finalizers read them, so it needs the GPU's writes there first.
|
|
608
|
-
const disturbed = disturbedBy(state);
|
|
609
|
-
gpu.disturb(disturbed.archetypes);
|
|
610
|
-
if (gpu.anyAhead()) await gpu.readback(disturbed.storages);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
finally {
|
|
614
|
-
// Applied even when the readback fails, which only a lost device makes it do: the CPU state
|
|
615
|
-
// stays whole.
|
|
616
|
-
dropped = state.plan.dropped;
|
|
617
|
-
for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
finally {
|
|
622
|
-
// Even a sync point that failed partway moved rows.
|
|
623
|
-
if (gpu !== null && state.touched !== null) {
|
|
624
|
-
gpu.touched(state.touched);
|
|
625
|
-
state.touched.clear();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
finally {
|
|
630
|
-
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
631
|
-
state.strings?.settle();
|
|
632
|
-
rewind(state, workers);
|
|
633
|
-
publish();
|
|
634
|
-
}
|
|
685
|
+
const pending = syncPoint(context, prepared, true);
|
|
686
|
+
if (pending !== null) await pending;
|
|
635
687
|
}
|
|
636
688
|
if (tailing !== null) {
|
|
637
689
|
// Placed again since, as when a joined thread is gone: they're found again next run.
|
|
@@ -644,12 +696,96 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
644
696
|
countRan(state, tailing.runners);
|
|
645
697
|
}
|
|
646
698
|
}
|
|
647
|
-
return reportOf(state, prepared
|
|
699
|
+
return reportOf(state, prepared);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Applies the commands so far as a sync point, once what it changes is read: by tails and observer systems on joined
|
|
704
|
+
* threads still reading it, and where it moves rows, by the GPU. One within a run lets the tiers after it see them;
|
|
705
|
+
* the run's `last` lets go of strings no reader holds. Returns what to wait for, or null once it applied, so a sync
|
|
706
|
+
* point with nothing to wait for takes no turn of the event loop before what follows it.
|
|
707
|
+
*/
|
|
708
|
+
const syncPoint = (context: ExecuteContext, prepared: Prepared, last: boolean): Promise<void> | null => {
|
|
709
|
+
const { state } = context;
|
|
710
|
+
const { gpu } = state;
|
|
711
|
+
// The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
|
|
712
|
+
for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
|
|
713
|
+
let pending: Promise<void> | null = null;
|
|
714
|
+
try {
|
|
715
|
+
if (gpu !== null) {
|
|
716
|
+
gpu.submit();
|
|
717
|
+
gpu.beforeSync();
|
|
718
|
+
}
|
|
719
|
+
const reading = readersDone(state);
|
|
720
|
+
pending = reading === null ? applyOnDevice(state, prepared) : reading.then(() => applyOnDevice(state, prepared) ?? undefined);
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
if (pending === null) afterSync(context, last);
|
|
724
|
+
}
|
|
725
|
+
return pending === null ? null : pending.finally(() => afterSync(context, last));
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Waits for the readers a sync point changes what they read for: tails, as it counts as a write to everything they
|
|
730
|
+
* read, then observer systems on joined threads in progress, cancelling the rest; null when none needs waiting for.
|
|
731
|
+
*/
|
|
732
|
+
const readersDone = (state: State): Promise<void> | null => {
|
|
733
|
+
if (state.tails.length !== 0) {
|
|
734
|
+
activate(state);
|
|
735
|
+
const waiting = syncPending(state) ? settleAll(state) : null;
|
|
736
|
+
if (waiting !== null) return waiting.then(() => (state.observer.posts.length === 0 ? null : settlePosts(state)) ?? undefined);
|
|
737
|
+
}
|
|
738
|
+
return state.observer.posts.length === 0 ? null : settlePosts(state);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Prepares and applies a sync point, reading back first what the GPU wrote where it moves and writes existing rows,
|
|
743
|
+
* which finalizers read too; applied even when the readback fails, which only a lost device makes it do, so the CPU
|
|
744
|
+
* state stays whole.
|
|
745
|
+
*/
|
|
746
|
+
const applyOnDevice = (state: State, prepared: Prepared): Promise<void> | null => {
|
|
747
|
+
if (!prepareSync(state)) return null;
|
|
748
|
+
const { gpu } = state;
|
|
749
|
+
let reading: Promise<void> | null = null;
|
|
750
|
+
try {
|
|
751
|
+
if (gpu !== null) {
|
|
752
|
+
const disturbed = disturbedBy(state);
|
|
753
|
+
gpu.disturb(disturbed.archetypes);
|
|
754
|
+
if (gpu.anyAhead()) reading = gpu.readback(disturbed.storages);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
if (reading === null) apply(state, prepared);
|
|
759
|
+
}
|
|
760
|
+
return reading === null ? null : reading.finally(() => apply(state, prepared));
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const apply = (state: State, prepared: Prepared): void => {
|
|
764
|
+
prepared.dropped += state.plan.dropped;
|
|
765
|
+
for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
/** Once a sync point applied or failed: the rows it moved, even partway, the strings it let go, and its buffers. */
|
|
769
|
+
const afterSync = (context: ExecuteContext, last: boolean): void => {
|
|
770
|
+
const { state, workers, publish } = context;
|
|
771
|
+
const { gpu } = state;
|
|
772
|
+
try {
|
|
773
|
+
if (gpu !== null && state.touched !== null) {
|
|
774
|
+
gpu.touched(state.touched);
|
|
775
|
+
state.touched.clear();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
780
|
+
if (last) state.strings?.settle();
|
|
781
|
+
rewind(state, workers);
|
|
782
|
+
publish();
|
|
783
|
+
}
|
|
648
784
|
};
|
|
649
785
|
|
|
650
786
|
/** A run's report once its sync point applied, its index conflicts included: a shared one when nothing went wrong. */
|
|
651
|
-
const reportOf = (state: State, prepared: Prepared
|
|
652
|
-
const { outcome } = prepared;
|
|
787
|
+
const reportOf = (state: State, prepared: Prepared): RunReport => {
|
|
788
|
+
const { outcome, dropped } = prepared;
|
|
653
789
|
// Conflicts unique indexes found while refreshing for finalizers.
|
|
654
790
|
if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
|
|
655
791
|
const { failures, skipped } = outcome;
|
|
@@ -14,12 +14,15 @@ import { idOf, type Key, type State } from './state.js';
|
|
|
14
14
|
import type { Queued } from './synchronize.js';
|
|
15
15
|
import type { Essential, ReadonlyType } from '../../interface/component/value.js';
|
|
16
16
|
import type { Leaf } from '../../interface/finalizer/created.js';
|
|
17
|
-
import { newestKeyOf, threadOf } from '../../interface/registry/index.js';
|
|
17
|
+
import { declaredOf, declaresTrait, newestKeyOf, threadOf, threadOfDeclaration } from '../../interface/registry/index.js';
|
|
18
|
+
import type { ResolvedChanges } from '../../interface/finalizer/declaration.js';
|
|
18
19
|
import type { Layout } from '../../interface/registry/layout.js';
|
|
19
20
|
import type { TaskChannel } from './remote-systems.js';
|
|
20
21
|
import { resourceOffsetsOf, writeStops, type StopsOf } from './stop-lists.js';
|
|
21
22
|
import { acquireLookups, refreshUses, type IndexLookups } from './indexes.js';
|
|
22
23
|
import { indexWatchesOf } from './lookup-keys.js';
|
|
24
|
+
import { takeResourceChanges, watchResource, type ResourceChange } from './resource-changes.js';
|
|
25
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
23
26
|
|
|
24
27
|
/** A registered finalizer, resolved for a world: what it matches or watches, its context, and the buffer its commands go to. */
|
|
25
28
|
export type FinalizerRunner = {
|
|
@@ -28,6 +31,8 @@ export type FinalizerRunner = {
|
|
|
28
31
|
readonly excluded: readonly number[];
|
|
29
32
|
/** For a changes finalizer: whether it watches a component, by id. */
|
|
30
33
|
readonly watches: (id: number) => boolean;
|
|
34
|
+
/** For a changes finalizer: the resources it watches, by id, which grow as resources declaring a trait it watches register. */
|
|
35
|
+
readonly resourceWatches: number[];
|
|
31
36
|
/** By archetype id: 1 if it matches, 2 if not, 0 until known. */
|
|
32
37
|
readonly matches: number[];
|
|
33
38
|
readonly context: Context;
|
|
@@ -59,12 +64,23 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
59
64
|
const reads = query?.read ?? [];
|
|
60
65
|
// By default every component that isn't derived, including ones a world first meets later; never resources, nor
|
|
61
66
|
// the objects a joined thread fills, whose storage here holds only tags.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
67
|
+
const everyComponent = (id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
|
|
68
|
+
let watches = everyComponent;
|
|
69
|
+
const resourceWatches: number[] = [];
|
|
70
|
+
if (finalizer.kind === "changes") {
|
|
71
|
+
const { watch } = finalizer.declaration;
|
|
72
|
+
followResources(state, watch, resourceWatches);
|
|
73
|
+
if (watch.components !== true) {
|
|
74
|
+
// `Finalizer.changes` rejected derived components.
|
|
75
|
+
const listed = new Set(watch.components.map(key => idOf(state, key)));
|
|
76
|
+
const traits = watch.componentTraits;
|
|
77
|
+
if (traits.length === 0) watches = id => listed.has(id);
|
|
78
|
+
else {
|
|
79
|
+
/** By id: 1 where its key declares one of the traits, 2 where it doesn't. */
|
|
80
|
+
const declaring: number[] = [];
|
|
81
|
+
watches = id => listed.has(id) || (everyComponent(id) && (declaring[id] ??= declaresTrait(state.keys[id], traits) ? 1 : 2) === 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
68
84
|
}
|
|
69
85
|
const target = { buffer: createBuffer() };
|
|
70
86
|
const commands = createCommands(state, target);
|
|
@@ -79,6 +95,7 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
79
95
|
required: [...new Set([...reads, ...query?.has ?? []])].filter(key => !(query?.optional ?? []).includes(key)).map(key => idOf(state, key)),
|
|
80
96
|
excluded: (query?.not ?? []).map(key => idOf(state, key)),
|
|
81
97
|
watches,
|
|
98
|
+
resourceWatches,
|
|
82
99
|
matches: [],
|
|
83
100
|
context: createContext(state, declaration, commanding ? { commands, indexes: indexes.byName } : { indexes: indexes.byName }),
|
|
84
101
|
target,
|
|
@@ -90,6 +107,30 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
90
107
|
};
|
|
91
108
|
};
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The resources a changes finalizer watches: those it lists, and those registered so far that declare a trait it
|
|
112
|
+
* watches, but those on another thread than the world's.
|
|
113
|
+
*/
|
|
114
|
+
export const watchedResourcesOf = (watch: ResolvedChanges["watch"]): readonly Key[] => {
|
|
115
|
+
if (watch.resourceTraits.length === 0) return watch.resources;
|
|
116
|
+
const found: Key[] = [...watch.resources];
|
|
117
|
+
for (const [key, declared] of declaredOf("resource")) {
|
|
118
|
+
const thread = threadOfDeclaration(declared);
|
|
119
|
+
if ((thread === null || thread === "world") && declaresTrait(key, watch.resourceTraits)) found.push(key);
|
|
120
|
+
}
|
|
121
|
+
return found;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Adds to `ids` each resource a changes finalizer watches that it doesn't hold, following its changes from now. */
|
|
125
|
+
export const followResources = (state: State, watch: ResolvedChanges["watch"], ids: number[]): void => {
|
|
126
|
+
for (const key of watchedResourcesOf(watch)) {
|
|
127
|
+
const id = idOf(state, key);
|
|
128
|
+
if (ids.includes(id)) continue;
|
|
129
|
+
ids.push(id);
|
|
130
|
+
watchResource(state, id);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
93
134
|
/**
|
|
94
135
|
* Places a finalizer on a joined thread's channel, posting it there, or back on the world's thread with null. Its reads
|
|
95
136
|
* are listed as the thread reads them: objects by their tags, linear structs by their bytes, strings by handles of
|
|
@@ -248,9 +289,14 @@ type Grouped = { from: Essential[] | null; to: Essential[] | null; added: Essent
|
|
|
248
289
|
|
|
249
290
|
/** No components, shared by every group that replaces none. */
|
|
250
291
|
const none: readonly number[] = [];
|
|
292
|
+
const noResources: readonly never[] = [];
|
|
293
|
+
const noResourceChanges: readonly ResourceChange[] = [];
|
|
251
294
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
295
|
+
/**
|
|
296
|
+
* The sync point's structural changes to a changes finalizer's components, grouped by how they changed, and of
|
|
297
|
+
* `written`, the resources it watches.
|
|
298
|
+
*/
|
|
299
|
+
const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queued[], written: readonly ResourceChange[]): ChangeSet<Essential, Resource.Key> => {
|
|
254
300
|
const { plan, archetypes, entities } = state;
|
|
255
301
|
const { watches } = runner;
|
|
256
302
|
const names = (archetype: Archetype | null): Essential[] | null =>
|
|
@@ -366,8 +412,12 @@ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queue
|
|
|
366
412
|
const value = commandValue(state, queued, entry, id);
|
|
367
413
|
return isMissing(value) ? live(entity, id) : value;
|
|
368
414
|
};
|
|
415
|
+
const resources = written.length === 0 || runner.resourceWatches.length === 0 ? noResources
|
|
416
|
+
: written.filter(change => runner.resourceWatches.includes(change.id)).map(({ id, previous, next }) => ({ key: newestKeyOf(state.keys[id]), previous, next }));
|
|
369
417
|
return {
|
|
370
418
|
groups: [...groups.values()],
|
|
419
|
+
// Stored values match each resource's registered schema, so they have its type.
|
|
420
|
+
resources: resources as ChangeSet<Essential, Resource.Key>["resources"],
|
|
371
421
|
// Stored and commanded values match each component's registered schema, so they have its type.
|
|
372
422
|
previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyType<K> | null,
|
|
373
423
|
next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyType<K> | null,
|
|
@@ -388,6 +438,8 @@ const staged = (state: State): boolean => {
|
|
|
388
438
|
export const runFinalizers = (state: State, queued: readonly Queued[], born: Map<number, number>, tick: number): Thrown[] => {
|
|
389
439
|
const errors: Thrown[] = [];
|
|
390
440
|
state.born = born;
|
|
441
|
+
// Taken before any finalizer runs, so what finalizers write is seen at the next sync point.
|
|
442
|
+
const written = state.resourcesSeen.size === 0 ? noResourceChanges : takeResourceChanges(state);
|
|
391
443
|
try {
|
|
392
444
|
for (const runner of state.finalizers) {
|
|
393
445
|
const { finalizer, remote } = runner;
|
|
@@ -399,8 +451,8 @@ export const runFinalizers = (state: State, queued: readonly Queued[], born: Map
|
|
|
399
451
|
if (finalizer.kind === "finalizer") finalizeStops(state, runner, finalizer, errors);
|
|
400
452
|
else {
|
|
401
453
|
// The context is built to the finalizer's declaration, so its body accepts it.
|
|
402
|
-
const changes: { body(changes: ChangeSet<Essential>, context: object): void } = finalizer;
|
|
403
|
-
guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued), runner.context.value));
|
|
454
|
+
const changes: { body(changes: ChangeSet<Essential, Resource.Key>, context: object): void } = finalizer;
|
|
455
|
+
guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued, written), runner.context.value));
|
|
404
456
|
}
|
|
405
457
|
if (runner.target.buffer.count > 0) {
|
|
406
458
|
state.queued.push({ buffer: runner.target.buffer, guarded: true });
|
|
@@ -19,7 +19,7 @@ describe("a world given keys with ids", () => {
|
|
|
19
19
|
const again: typeof import("./ids-keys.test-helper.js") = await import(/* @vite-ignore */ `./ids-keys.test-helper.js?again=${Date.now()}`);
|
|
20
20
|
expect(again.score).not.toBe(score);
|
|
21
21
|
const spawnAgain = System.create({}, ({ commands }) => { spawned.push(commands.spawn([again.score])({ [again.score]: 2 })); });
|
|
22
|
-
const add = System.create({ components: { with: [again.score] } }, row => { row[again.score] += 10; });
|
|
22
|
+
const add = System.create({ components: { with: [again.score], whole: true } }, row => { row[again.score] += 10; });
|
|
23
23
|
await world.run(spawnAgain);
|
|
24
24
|
await world.run(add);
|
|
25
25
|
const [first, second] = spawned;
|
|
@@ -253,7 +253,8 @@ const keepTicking = System.create({}, () => {});
|
|
|
253
253
|
const watchGrid = System.create({ lookup: { near: { index: grid } } }, ({ lookup }) => { void lookup.near([0, 0]).length; });
|
|
254
254
|
|
|
255
255
|
describe("index lifetime", () => {
|
|
256
|
-
|
|
256
|
+
// Flaky on CI's runners, which runs no flaky test: run locally until it's hardened (`followups.md`, Correctness).
|
|
257
|
+
it.skipIf(process.env["CI"] !== undefined)("builds an index the first time a system uses it and releases it, arena blocks included, once none does", async () => {
|
|
257
258
|
const driver = backToBack();
|
|
258
259
|
await using world = createWorld({ [workers]: 0, [always]: true }, { loop: driver });
|
|
259
260
|
await world.run(spawnCells, { cells: [[0, 0], [1, 1]] });
|