@adobe/ecs 0.2.4 → 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/inferred.js +3 -2
- package/dist/implementation/compiler/inferred.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 +19 -13
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/commands.js +3 -2
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +4 -4
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.js +97 -11
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -2
- package/dist/implementation/world/finalizers.js +42 -13
- package/dist/implementation/world/finalizers.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/state.d.ts +2 -0
- package/dist/implementation/world/state.js +9 -0
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +16 -9
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +2 -1
- package/dist/implementation/world/tables.js +55 -2
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +24 -3
- package/dist/interface/finalizer/create.d.ts +3 -3
- package/dist/interface/finalizer/create.js +43 -11
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +25 -5
- 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 +1 -0
- package/dist/interface/registry/schedule.js +86 -11
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/check.js +4 -0
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +8 -3
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.js +2 -2
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.js +1 -1
- package/dist/interface/system/describe.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +4 -3
- package/dist/interface/system/render.js +2 -2
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +12 -3
- package/dist/interface/system/resolve.js +35 -4
- package/dist/interface/system/resolve.js.map +1 -1
- package/package.json +1 -1
- 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 +14 -6
- package/src/guide/06-sync-points.out +2 -0
- package/src/guide/06-sync-points.ts +15 -0
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +4 -4
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/inferred.ts +3 -2
- package/src/implementation/compiler/transform.test.ts +4 -4
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +24 -11
- package/src/implementation/world/commands.ts +3 -2
- package/src/implementation/world/create.ts +4 -4
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +31 -8
- package/src/implementation/world/execute.ts +98 -9
- package/src/implementation/world/finalizers.ts +43 -14
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +9 -0
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/state.ts +8 -0
- package/src/implementation/world/sync-points.test.ts +12 -0
- package/src/implementation/world/synchronize.ts +15 -9
- package/src/implementation/world/tables.test.ts +36 -3
- package/src/implementation/world/tables.ts +48 -3
- 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/interface/finalizer/body.ts +22 -3
- package/src/interface/finalizer/create.test.ts +4 -1
- package/src/interface/finalizer/create.ts +40 -12
- package/src/interface/finalizer/declaration.ts +30 -5
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +89 -16
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/check.ts +2 -0
- package/src/interface/system/commands.ts +8 -3
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +3 -3
- package/src/interface/system/create.ts +2 -2
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +19 -0
- package/src/interface/system/describe.ts +1 -1
- package/src/interface/system/leaf.ts +4 -3
- package/src/interface/system/render.ts +2 -2
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +40 -5
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/perftest/benchmarks.ts +2 -0
- package/src/perftest/chained.ts +108 -0
|
@@ -11,7 +11,7 @@ import type { Body } from './body.js';
|
|
|
11
11
|
import type { ReturnsNoPromise } from './input.js';
|
|
12
12
|
import { accessorsOf } from '../gpu/accessors.js';
|
|
13
13
|
import { checkLeaf } from './check.js';
|
|
14
|
-
import { accessOf, compact, resourceUses } from './resolve.js';
|
|
14
|
+
import { accessOf, compact, noCommands, resourceUses } from './resolve.js';
|
|
15
15
|
import type { Leaf } from './leaf.js';
|
|
16
16
|
import type { StructComponent } from './struct.js';
|
|
17
17
|
import type { Shared } from '../component/value.js';
|
|
@@ -96,7 +96,7 @@ export const render = <const D extends RenderDeclaration, B extends Body<D>>(
|
|
|
96
96
|
},
|
|
97
97
|
input: undefined,
|
|
98
98
|
body,
|
|
99
|
-
commands:
|
|
99
|
+
commands: noCommands,
|
|
100
100
|
};
|
|
101
101
|
if (uses.write.length > 0 || uses.reduce.length > 0) throw new Error(`${what} writes or reduces into resources; a render system only reads.`);
|
|
102
102
|
checkLeaf(system);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import "../registry/declarations.js";
|
|
4
|
-
import { componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
|
|
4
|
+
import { commandListsOf, componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
|
|
5
5
|
|
|
6
6
|
const mass = Symbol("resolveTest/mass");
|
|
7
7
|
const pool = Symbol("resolveTest/pool");
|
|
@@ -47,3 +47,19 @@ describe("resourceUses", () => {
|
|
|
47
47
|
expect(() => resourceUses("System \"s\"", changing(pool), undefined)).not.toThrow();
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
describe("commandListsOf", () => {
|
|
52
|
+
const commanding = (commands: () => unknown): Access => ({ components: null, resources: null, lookup: null, up: null, commands, at: "a test" });
|
|
53
|
+
it("reads what the commands name at the first call, once", () => {
|
|
54
|
+
let reads = 0;
|
|
55
|
+
const lists = commandListsOf("System \"s\"", commanding(() => { reads++; return { spawn: [[mass, position]], add: [mass], remove: [], despawn: false }; }));
|
|
56
|
+
expect(reads).toBe(0);
|
|
57
|
+
expect([lists(), lists(), reads]).toEqual([{ spawn: [mass, position], add: [mass] }, { spawn: [mass, position], add: [mass] }, 1]);
|
|
58
|
+
});
|
|
59
|
+
it("names the system and the declaration its commands name before it's initialized", () => {
|
|
60
|
+
for (const message of ["Cannot access 'standard' before initialization", "can't access lexical declaration 'standard' before initialization"]) {
|
|
61
|
+
const lists = commandListsOf("System \"s\"", commanding(() => { throw new ReferenceError(message); }));
|
|
62
|
+
expect(lists).toThrow(`System "s" names "standard" in its commands before it's initialized: declare it above the system.`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -23,7 +23,10 @@ export type Access = {
|
|
|
23
23
|
readonly up: readonly Key[] | null;
|
|
24
24
|
/** The resources the predicates of its declaration's `where` read; absent without predicates. */
|
|
25
25
|
readonly where?: readonly Key[] | null;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* What its commands name, keys or lists of them, by kind, and whether it despawns, or a function giving that; absent
|
|
28
|
+
* where no build said.
|
|
29
|
+
*/
|
|
27
30
|
readonly commands?: unknown;
|
|
28
31
|
readonly at: string;
|
|
29
32
|
};
|
|
@@ -35,7 +38,7 @@ const isKeys = (value: unknown): value is readonly Key[] => Array.isArray(value)
|
|
|
35
38
|
|
|
36
39
|
const isNamed = (value: unknown): value is Named => Array.isArray(value) && value.every(item => typeof item === "symbol" || isKeys(item));
|
|
37
40
|
|
|
38
|
-
/** A binding a command named that holds neither a key nor a list of them
|
|
41
|
+
/** A binding a command named that holds neither a key nor a list of them isn't followed. */
|
|
39
42
|
const isCommanded = (value: unknown): value is Commanded => typeof value === "object" && value !== null
|
|
40
43
|
&& isNamed(Reflect.get(value, "spawn")) && isNamed(Reflect.get(value, "add")) && isNamed(Reflect.get(value, "remove"))
|
|
41
44
|
&& typeof Reflect.get(value, "despawn") === "boolean";
|
|
@@ -173,9 +176,28 @@ export const whereReadsOf = (what: string, components: ComponentAccess | undefin
|
|
|
173
176
|
return reads;
|
|
174
177
|
};
|
|
175
178
|
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
/**
|
|
180
|
+
* What a body's commands name, as the world takes it, read at the first call and kept: null where the build couldn't
|
|
181
|
+
* follow them, nothing for none. The build gives them as a function, so a list declared below the system is read once
|
|
182
|
+
* its module has loaded; one still uninitialized then, as an import cycle leaves it, throws naming it.
|
|
183
|
+
*/
|
|
184
|
+
export const commandListsOf = (what: string, access: Access): (() => CommandLists | null | undefined) => {
|
|
185
|
+
let read = false;
|
|
186
|
+
let lists: CommandLists | null | undefined;
|
|
187
|
+
return () => {
|
|
188
|
+
if (!read) {
|
|
189
|
+
lists = listsOf(what, access.commands);
|
|
190
|
+
read = true;
|
|
191
|
+
}
|
|
192
|
+
return lists;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The command lists of a system whose body issues none. */
|
|
197
|
+
export const noCommands = (): undefined => undefined;
|
|
198
|
+
|
|
199
|
+
const listsOf = (what: string, given: unknown): CommandLists | null | undefined => {
|
|
200
|
+
const commands = typeof given === "function" ? evaluated(what, () => Reflect.apply(given, undefined, [])) : given;
|
|
179
201
|
if (commands === undefined) return undefined;
|
|
180
202
|
if (!isCommanded(commands)) return null;
|
|
181
203
|
if (!commands.despawn && commands.spawn.length + commands.add.length + commands.remove.length === 0) return undefined;
|
|
@@ -184,5 +206,18 @@ export const commandLists = (access: Access): CommandLists | null | undefined =>
|
|
|
184
206
|
return Object.keys(lists).length === 0 ? undefined : lists;
|
|
185
207
|
};
|
|
186
208
|
|
|
209
|
+
/** What the build's function of a body's commands gives. */
|
|
210
|
+
const evaluated = (what: string, commands: () => unknown): unknown => {
|
|
211
|
+
try {
|
|
212
|
+
return commands();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!(error instanceof ReferenceError)) throw error;
|
|
215
|
+
// Engines quote the binding in the message: "Cannot access 'standard' before initialization", or "can't access
|
|
216
|
+
// lexical declaration 'standard' before initialization".
|
|
217
|
+
const binding = /'([A-Za-z_$][\w$]*)'/.exec(error.message)?.[1];
|
|
218
|
+
throw new Error(`${what} names ${binding === undefined ? "a declaration" : `"${binding}"`} in its commands before it's initialized: declare it above the system.`, { cause: error });
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
187
222
|
/** What a body reads of its parent's values. */
|
|
188
223
|
export const upReads = (what: string, access: Access): readonly Key[] => followed(what, access.up, "its parent's values", access);
|
|
@@ -154,6 +154,12 @@ System.create({
|
|
|
154
154
|
// @ts-expect-error ordering takes systems
|
|
155
155
|
after: ["test/unregistered"],
|
|
156
156
|
}, () => {});
|
|
157
|
+
// Ordering by a resource: before or after every other system of the composite that uses it.
|
|
158
|
+
System.create({ before: [time], after: [integrate, score] }, ({ resources }) => { resources[time] = resources[score]; });
|
|
159
|
+
System.create({
|
|
160
|
+
// @ts-expect-error ordering takes systems and resources, not components
|
|
161
|
+
after: [position],
|
|
162
|
+
}, () => {});
|
|
157
163
|
|
|
158
164
|
// @ts-expect-error a system with input can't use change filters
|
|
159
165
|
System.create({ components: { changed: [position] } }, (_row, _context, input: { readonly distance: number }) => { void input; });
|
|
@@ -36,6 +36,7 @@ import { entitiesWith } from "./observer.js";
|
|
|
36
36
|
import { invariantBenchmarks } from "./invariants.js";
|
|
37
37
|
import { journalBenchmarks } from "./journal.js";
|
|
38
38
|
import { tableBenchmarks } from "./tables.js";
|
|
39
|
+
import { chainedBenchmarks } from "./chained.js";
|
|
39
40
|
|
|
40
41
|
/** A world whose kernels run on this thread, which the benchmarks without a placement measure; `workers` ones use the pool. */
|
|
41
42
|
const inline = (): World => createWorld({ [workers]: 0 });
|
|
@@ -723,4 +724,5 @@ export const benchmarks: readonly Benchmark[] = [
|
|
|
723
724
|
...invariantBenchmarks,
|
|
724
725
|
...journalBenchmarks,
|
|
725
726
|
...tableBenchmarks,
|
|
727
|
+
...chainedBenchmarks,
|
|
726
728
|
];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { workers } from "../interface/resources/workers.js";
|
|
3
|
+
import { System } from "../interface/system/system.js";
|
|
4
|
+
import { createWorld } from "../implementation/index.js";
|
|
5
|
+
import type { Benchmark } from "./benchmark.js";
|
|
6
|
+
import { fastest } from "./suite.js";
|
|
7
|
+
|
|
8
|
+
// Eight systems over rows no entity has, each with a command its body never issues, run alone or ordered one after
|
|
9
|
+
// another: what orderings and the sync points they need cost a frame whose commands don't fire.
|
|
10
|
+
|
|
11
|
+
const event = Symbol("chained/event");
|
|
12
|
+
const note = Symbol("chained/note");
|
|
13
|
+
declare module "@adobe/ecs" {
|
|
14
|
+
export interface Components {
|
|
15
|
+
[event]: { type: true };
|
|
16
|
+
[note]: { type: true };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Never true, so no body issues its command. */
|
|
21
|
+
let firing = false;
|
|
22
|
+
/** Systems in each composite. */
|
|
23
|
+
const length = 8;
|
|
24
|
+
/** Runs in each timed sample. */
|
|
25
|
+
const runs = 100;
|
|
26
|
+
|
|
27
|
+
/** A system spawning, when firing, what each of the composite's systems matches. */
|
|
28
|
+
const commanding = (after: readonly System[]): System => System.create({ components: { with: [event] }, after }, (_row, { commands }) => {
|
|
29
|
+
if (firing) commands.spawn([event])();
|
|
30
|
+
});
|
|
31
|
+
/** A system spawning, when firing, what none of them matches. */
|
|
32
|
+
const noting = (after: readonly System[]): System => System.create({ components: { with: [event] }, after }, (_row, { commands }) => {
|
|
33
|
+
if (firing) commands.spawn([note])();
|
|
34
|
+
});
|
|
35
|
+
const quiet = (after: readonly System[]): System => System.create({ components: { with: [event] }, after }, () => {});
|
|
36
|
+
|
|
37
|
+
/** `length` systems made by `make`, each after the one before when `ordered`. */
|
|
38
|
+
const composite = (make: (after: readonly System[]) => System, ordered: boolean): System => {
|
|
39
|
+
const systems: System[] = [];
|
|
40
|
+
for (let k = 0; k < length; k++) {
|
|
41
|
+
const previous = systems[k - 1];
|
|
42
|
+
systems.push(make(ordered && previous !== undefined ? [previous] : []));
|
|
43
|
+
}
|
|
44
|
+
return System.create(systems);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const unordered = composite(commanding, false);
|
|
48
|
+
const chainedQuiet = composite(quiet, true);
|
|
49
|
+
const chainedNoting = composite(noting, true);
|
|
50
|
+
const chainedCommanding = composite(commanding, true);
|
|
51
|
+
|
|
52
|
+
const check = (condition: boolean, message: string): void => {
|
|
53
|
+
if (!condition) throw new Error(message);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const timed = (system: System, tiers: number, syncs: number) => async (): Promise<number> => {
|
|
57
|
+
const { tiers: scheduled } = System.describe(system);
|
|
58
|
+
check(scheduled.length === tiers && scheduled.filter(tier => tier.sync !== null).length === syncs, `chained: ${scheduled.length} tiers and ${scheduled.filter(tier => tier.sync !== null).length} sync points`);
|
|
59
|
+
firing = false;
|
|
60
|
+
const world = createWorld({ [workers]: 0 });
|
|
61
|
+
try {
|
|
62
|
+
return await fastest(async () => { for (let k = 0; k < runs; k++) await world.run(system); });
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await world.dispose();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const runBest = { bytes: 64, basis: "A run reads at least one cache line of its system's state." };
|
|
70
|
+
|
|
71
|
+
export const chainedBenchmarks: readonly Benchmark[] = [
|
|
72
|
+
{
|
|
73
|
+
name: "chained unordered",
|
|
74
|
+
operation: "Running eight systems no entity matches, each with a command it never issues, unordered: one tier, and a sync point at the run's end that applies nothing.",
|
|
75
|
+
per: "runs",
|
|
76
|
+
n: runs,
|
|
77
|
+
typical: [1, 1],
|
|
78
|
+
best: runBest,
|
|
79
|
+
run: timed(unordered, 1, 0),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "chained quiet",
|
|
83
|
+
operation: "Running eight systems no entity matches, without commands, each after the one before: eight tiers and no sync point between them, as orderings that only settle conflicts make.",
|
|
84
|
+
per: "runs",
|
|
85
|
+
n: runs,
|
|
86
|
+
typical: [1, 1],
|
|
87
|
+
best: runBest,
|
|
88
|
+
run: timed(chainedQuiet, length, 0),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "chained noting",
|
|
92
|
+
operation: "Running eight systems no entity matches, each after the one before, each with a command it never issues of what none of them matches: eight tiers, no sync point between them.",
|
|
93
|
+
per: "runs",
|
|
94
|
+
n: runs,
|
|
95
|
+
typical: [1, 1],
|
|
96
|
+
best: runBest,
|
|
97
|
+
run: timed(chainedNoting, length, 0),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "chained commanding",
|
|
101
|
+
operation: "Running eight systems no entity matches, each after the one before, each with a command it never issues of what the next matches: eight tiers and seven sync points that apply nothing.",
|
|
102
|
+
per: "runs",
|
|
103
|
+
n: runs,
|
|
104
|
+
typical: [1, 1],
|
|
105
|
+
best: runBest,
|
|
106
|
+
run: timed(chainedCommanding, length, length - 1),
|
|
107
|
+
},
|
|
108
|
+
];
|