@adobe/ecs 0.2.3 → 0.2.4
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/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 +4 -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/world/activate.js +14 -2
- 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 +40 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- 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/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 +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- 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 +3 -2
- package/dist/implementation/world/tables.js +83 -37
- 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 +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- 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/schedule.d.ts +28 -1
- package/dist/interface/registry/schedule.js +182 -21
- 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 +17 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +26 -5
- package/dist/interface/system/create-resolved.js +1 -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/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 +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -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/03-systems.out +1 -1
- package/src/guide/06-sync-points.md +12 -8
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +9 -5
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +3 -150
- 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 +4 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/world/activate.ts +11 -2
- 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 +36 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.ts +27 -4
- 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/names.test.ts +1 -2
- package/src/implementation/world/perf.md +3 -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/runner.ts +17 -13
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +106 -4
- package/src/implementation/world/tables.ts +71 -33
- 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/where.ts +38 -10
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- 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/schedule.ts +196 -23
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +15 -2
- package/src/interface/system/commands.ts +23 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +8 -0
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -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 +18 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { indexDefinitionOf } from '../registry/indexes.js';
|
|
3
|
+
import { labelOf } from '../registry/names.js';
|
|
4
|
+
import { requireDefinition } from '../registry/program.js';
|
|
5
|
+
import { isLeaf, placementsOf, scheduleOf, type Placement } from '../registry/schedule.js';
|
|
6
|
+
import type { Leaf } from './leaf.js';
|
|
7
|
+
import type { System } from './system.js';
|
|
8
|
+
|
|
9
|
+
type Key = symbol;
|
|
10
|
+
|
|
11
|
+
const none: readonly never[] = [];
|
|
12
|
+
|
|
13
|
+
/** A leaf as the system value it is. */
|
|
14
|
+
const asSystem = (leaf: Leaf): System<unknown, unknown> => leaf;
|
|
15
|
+
|
|
16
|
+
/** What the build inferred one system uses, and what placed it, as plain data. */
|
|
17
|
+
const describeLeaf = (leaf: Leaf, placements: readonly Placement[]) => {
|
|
18
|
+
const { components, resources, lookup, up, order } = leaf.declaration;
|
|
19
|
+
const { commands } = leaf;
|
|
20
|
+
return {
|
|
21
|
+
/** The system, the value its constructor returned. */
|
|
22
|
+
system: asSystem(leaf),
|
|
23
|
+
name: leaf.name,
|
|
24
|
+
components: {
|
|
25
|
+
read: [...components?.read ?? none, ...up === undefined ? none : [up.through]],
|
|
26
|
+
write: components?.write ?? none,
|
|
27
|
+
/** What it requires without reading: `with` members it doesn't use, `added` and `changed` filters' among them. */
|
|
28
|
+
has: components?.has ?? none,
|
|
29
|
+
optional: components?.optional ?? none,
|
|
30
|
+
not: components?.not ?? none,
|
|
31
|
+
added: components?.added ?? none,
|
|
32
|
+
changed: components?.changed ?? none,
|
|
33
|
+
/** What it reads of each row's parent, through `up`. */
|
|
34
|
+
parent: up?.read ?? none,
|
|
35
|
+
},
|
|
36
|
+
resources: {
|
|
37
|
+
read: resources?.read ?? none,
|
|
38
|
+
write: resources?.write ?? none,
|
|
39
|
+
reduce: resources?.reduce ?? none,
|
|
40
|
+
changed: resources?.changed ?? none,
|
|
41
|
+
is: resources?.is ?? none,
|
|
42
|
+
not: resources?.not ?? none,
|
|
43
|
+
},
|
|
44
|
+
lookups: Object.entries(lookup ?? {}).map(([name, used]) => ({
|
|
45
|
+
name,
|
|
46
|
+
/** The label of the index it looks up through, or null for a lookup by entity. */
|
|
47
|
+
index: "index" in used && used.index !== undefined ? indexDefinitionOf(used.index)?.name ?? null : null,
|
|
48
|
+
read: used.read ?? none,
|
|
49
|
+
})),
|
|
50
|
+
/** The label of the sorted index ordering its rows, or null. */
|
|
51
|
+
order: indexDefinitionOf(order)?.name ?? null,
|
|
52
|
+
/** What its commands name by kind, and whether it despawns; null where the build couldn't follow them. */
|
|
53
|
+
commands: commands === null ? null : {
|
|
54
|
+
spawn: commands?.spawn ?? none,
|
|
55
|
+
add: commands?.add ?? none,
|
|
56
|
+
remove: commands?.remove ?? none,
|
|
57
|
+
despawn: commands?.despawn === true,
|
|
58
|
+
},
|
|
59
|
+
/** Whether it takes its row whole (`components: { whole: true }`), every member written. */
|
|
60
|
+
whole: components?.whole === true,
|
|
61
|
+
/** The systems it follows directly, with what ordered it after each and the keys that need that order. */
|
|
62
|
+
after: placements.map(placement => ({ system: asSystem(placement.leaf), name: placement.leaf.name, because: placement.because, conflicts: placement.conflicts })),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type Described = ReturnType<typeof describeLeaf>;
|
|
67
|
+
|
|
68
|
+
const keys = (list: readonly Key[]): string => list.map(labelOf).join(" ");
|
|
69
|
+
|
|
70
|
+
/** A system's lines of text, after its name in a column of `width`. */
|
|
71
|
+
const linesOf = (system: Described, width: number): readonly string[] => {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
const add = (label: string, text: string): void => { if (text !== "") lines.push(`${label.padEnd(10)}${text}`); };
|
|
74
|
+
const { components, resources, commands } = system;
|
|
75
|
+
add("reads", keys(components.read));
|
|
76
|
+
add("writes", `${keys(components.write)}${system.whole ? " (its whole row)" : ""}`);
|
|
77
|
+
add("requires", [keys(components.has), components.not.length === 0 ? "" : `not ${keys(components.not)}`, components.optional.length === 0 ? "" : `optional ${keys(components.optional)}`].filter(text => text !== "").join(", "));
|
|
78
|
+
add("filters", [...components.added.map(key => `added ${labelOf(key)}`), ...components.changed.map(key => `changed ${labelOf(key)}`), ...resources.changed.map(key => `changed ${labelOf(key)}`)].join(", "));
|
|
79
|
+
add("parent", keys(components.parent));
|
|
80
|
+
add("resources", [resources.read.length === 0 ? "" : `reads ${keys(resources.read)}`, resources.write.length === 0 ? "" : `writes ${keys(resources.write)}`,
|
|
81
|
+
resources.reduce.length === 0 ? "" : `reduces ${keys(resources.reduce)}`, resources.is.length === 0 ? "" : `while ${keys(resources.is)}`,
|
|
82
|
+
resources.not.length === 0 ? "" : `unless ${keys(resources.not)}`].filter(text => text !== "").join(", "));
|
|
83
|
+
for (const lookup of system.lookups) add("looks up", `${lookup.name}${lookup.index === null ? "" : ` through ${lookup.index}`}${lookup.read.length === 0 ? "" : `: ${keys(lookup.read)}`}`);
|
|
84
|
+
if (system.order !== null) add("ordered", `by ${system.order}`);
|
|
85
|
+
if (commands === null) add("commands", "unknown: the build couldn't follow them");
|
|
86
|
+
else add("commands", [commands.spawn.length === 0 ? "" : `spawn ${keys(commands.spawn)}`, commands.add.length === 0 ? "" : `add ${keys(commands.add)}`,
|
|
87
|
+
commands.remove.length === 0 ? "" : `remove ${keys(commands.remove)}`, commands.despawn ? "despawn" : ""].filter(text => text !== "").join(", "));
|
|
88
|
+
add("after", system.after.map(placement => `${placement.name} (${placement.because}${placement.conflicts.length === 0 ? "" : `: ${keys(placement.conflicts)}`})`).join(", "));
|
|
89
|
+
const indent = " ".repeat(width);
|
|
90
|
+
return lines.length === 0 ? [system.name] : lines.map((line, i) => `${i === 0 ? system.name.padEnd(width) : indent}${line}`);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Describes how a system or composite runs: its tiers in run order, systems in one tier having no ordering path between
|
|
95
|
+
* them, each with the sync point after it, if any, and its readonly systems apart, which run after its last sync
|
|
96
|
+
* point. For each system, what the build inferred it reads, writes, looks up and commands, whether it takes its row
|
|
97
|
+
* whole, and the systems it follows directly, with what ordered it after each. `text` gives it all to read; the rest
|
|
98
|
+
* is data to assert on.
|
|
99
|
+
*/
|
|
100
|
+
export const describe = <I, R>(system: System<I, R>) => {
|
|
101
|
+
const definition = requireDefinition(system);
|
|
102
|
+
const schedule = scheduleOf(definition);
|
|
103
|
+
const placements = placementsOf(definition);
|
|
104
|
+
const tiers = schedule.tiers.map((tier, t) => ({
|
|
105
|
+
systems: tier.map(leaf => describeLeaf(leaf, placements.get(leaf) ?? none)),
|
|
106
|
+
/**
|
|
107
|
+
* Why a sync point follows the tier, applying the commands so far: each system whose commands may change what a
|
|
108
|
+
* later one that follows it uses, through `keys`, none listed where it despawns or a cascade could; null for none.
|
|
109
|
+
*/
|
|
110
|
+
sync: schedule.syncs[t]?.map(reason => ({ from: asSystem(reason.from), to: asSystem(reason.to), keys: reason.keys })) ?? null,
|
|
111
|
+
}));
|
|
112
|
+
const readonly = schedule.observer.flatMap(member => isLeaf(member) ? [describeLeaf(member, none)] : []);
|
|
113
|
+
const width = Math.max(0, ...[...tiers.flatMap(tier => tier.systems), ...readonly].map(described => described.name.length)) + 2;
|
|
114
|
+
const text = [
|
|
115
|
+
...tiers.flatMap((tier, t) => [
|
|
116
|
+
...tier.systems.flatMap((described, i) => linesOf(described, width).map((line, l) => `${(i === 0 && l === 0 ? `tier ${t}` : "").padEnd(8)}${line}`)),
|
|
117
|
+
...(tier.sync ?? none).map((reason, r) => `${(r === 0 ? "sync" : "").padEnd(8)}${reason.from.name}'s commands${reason.keys.length === 0 ? "" : ` of ${keys(reason.keys)}`}, for ${reason.to.name}`),
|
|
118
|
+
]),
|
|
119
|
+
...readonly.length === 0 ? none : ["readonly", ...readonly.flatMap(described => linesOf(described, width).map(line => `${"".padEnd(8)}${line}`))],
|
|
120
|
+
].map(line => line.trimEnd()).join("\n");
|
|
121
|
+
return { tiers, readonly, text };
|
|
122
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Schema } from '../schema/schema.js';
|
|
3
|
+
import type { CommandLists } from './access.js';
|
|
3
4
|
import type { Resolved } from './declaration.js';
|
|
4
5
|
import type { Name } from './name.js';
|
|
5
6
|
|
|
@@ -17,4 +18,9 @@ export type Leaf = {
|
|
|
17
18
|
readonly declaration: Resolved;
|
|
18
19
|
readonly input: InputSchema | undefined;
|
|
19
20
|
readonly body: (...parameters: never[]) => void;
|
|
21
|
+
/**
|
|
22
|
+
* What its body's commands name, as the build inferred them: null where it couldn't follow them, undefined for
|
|
23
|
+
* none. Apart from its declaration, so a module evaluated again with other commands declares the same system.
|
|
24
|
+
*/
|
|
25
|
+
readonly commands?: CommandLists | null;
|
|
20
26
|
};
|
|
@@ -96,6 +96,7 @@ export const render = <const D extends RenderDeclaration, B extends Body<D>>(
|
|
|
96
96
|
},
|
|
97
97
|
input: undefined,
|
|
98
98
|
body,
|
|
99
|
+
commands: undefined,
|
|
99
100
|
};
|
|
100
101
|
if (uses.write.length > 0 || uses.reduce.length > 0) throw new Error(`${what} writes or reduces into resources; a render system only reads.`);
|
|
101
102
|
checkLeaf(system);
|
|
@@ -3,7 +3,7 @@ import { takeAccessOf } from '../registry/ids.js';
|
|
|
3
3
|
import { indexDefinitionOf } from '../registry/indexes.js';
|
|
4
4
|
import { declarationOf, labelOf } from '../registry/names.js';
|
|
5
5
|
import { isString, isStruct } from '../registry/schemas.js';
|
|
6
|
-
import type { ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
|
|
6
|
+
import type { CommandLists, ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
|
|
7
7
|
|
|
8
8
|
type Key = symbol;
|
|
9
9
|
|
|
@@ -23,11 +23,23 @@ 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
|
+
/** What its commands name, keys or lists of them, by kind, and whether it despawns; absent where no build said. */
|
|
27
|
+
readonly commands?: unknown;
|
|
26
28
|
readonly at: string;
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
type Named = readonly (Key | readonly Key[])[];
|
|
32
|
+
type Commanded = { readonly spawn: Named; readonly add: Named; readonly remove: Named; readonly despawn: boolean };
|
|
33
|
+
|
|
29
34
|
const isKeys = (value: unknown): value is readonly Key[] => Array.isArray(value) && value.every(key => typeof key === "symbol");
|
|
30
35
|
|
|
36
|
+
const isNamed = (value: unknown): value is Named => Array.isArray(value) && value.every(item => typeof item === "symbol" || isKeys(item));
|
|
37
|
+
|
|
38
|
+
/** A binding a command named that holds neither a key nor a list of them, as one declared below its system, isn't followed. */
|
|
39
|
+
const isCommanded = (value: unknown): value is Commanded => typeof value === "object" && value !== null
|
|
40
|
+
&& isNamed(Reflect.get(value, "spawn")) && isNamed(Reflect.get(value, "add")) && isNamed(Reflect.get(value, "remove"))
|
|
41
|
+
&& typeof Reflect.get(value, "despawn") === "boolean";
|
|
42
|
+
|
|
31
43
|
const isUsed = (value: unknown): value is Used => typeof value === "object" && value !== null
|
|
32
44
|
&& isKeys(Reflect.get(value, "read")) && isKeys(Reflect.get(value, "write"))
|
|
33
45
|
&& (Reflect.get(value, "mutate") === undefined || isKeys(Reflect.get(value, "mutate")));
|
|
@@ -106,7 +118,7 @@ export const componentLists = (components: ComponentAccess, used: Used | null, r
|
|
|
106
118
|
: used.write;
|
|
107
119
|
const read = used === null ? without(held, write) : without(used.read, write);
|
|
108
120
|
const has = [...without(members, read, write), ...without(filtered, read, write)];
|
|
109
|
-
return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads });
|
|
121
|
+
return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads, whole: components.whole });
|
|
110
122
|
};
|
|
111
123
|
|
|
112
124
|
/**
|
|
@@ -161,5 +173,16 @@ export const whereReadsOf = (what: string, components: ComponentAccess | undefin
|
|
|
161
173
|
return reads;
|
|
162
174
|
};
|
|
163
175
|
|
|
176
|
+
/** What a body's commands name, as the world takes it: null where the build couldn't follow them, nothing for none. */
|
|
177
|
+
export const commandLists = (access: Access): CommandLists | null | undefined => {
|
|
178
|
+
const { commands } = access;
|
|
179
|
+
if (commands === undefined) return undefined;
|
|
180
|
+
if (!isCommanded(commands)) return null;
|
|
181
|
+
if (!commands.despawn && commands.spawn.length + commands.add.length + commands.remove.length === 0) return undefined;
|
|
182
|
+
const flat = (named: Named): Key[] => [...new Set(named.flat())];
|
|
183
|
+
const lists = compact({ spawn: flat(commands.spawn), add: flat(commands.add), remove: flat(commands.remove), despawn: commands.despawn ? true as const : undefined });
|
|
184
|
+
return Object.keys(lists).length === 0 ? undefined : lists;
|
|
185
|
+
};
|
|
186
|
+
|
|
164
187
|
/** What a body reads of its parent's values. */
|
|
165
188
|
export const upReads = (what: string, access: Access): readonly Key[] => followed(what, access.up, "its parent's values", access);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Shared } from '../component/value.js';
|
|
3
|
+
|
|
4
|
+
/** A `where`'s keys that aren't shared components, each an error: `where` names only shared components. */
|
|
5
|
+
export type SharedOnly<W> = { readonly [K in Exclude<keyof W, Shared>]: { readonly "error: where names only shared components, which each archetype holds one value of": K } };
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Component } from '../component/component.js';
|
|
3
|
+
import type { Shared } from '../component/value.js';
|
|
3
4
|
import type { Resource } from '../resource/resource.js';
|
|
4
5
|
import type { Table } from '../world/table/table.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* What a readonly system's `tables` reads: of every archetype matching `with` and `
|
|
8
|
+
* What a readonly system's `tables` reads: of every archetype matching `with`, `not` and `where` that holds any of
|
|
8
9
|
* `components`, a table of those it holds; and, with `resources`, each resource's value.
|
|
9
10
|
*/
|
|
10
11
|
export type TablesDeclaration = {
|
|
11
12
|
readonly components: readonly Component.Key[];
|
|
12
13
|
readonly with?: readonly Component.Key[];
|
|
13
14
|
readonly not?: readonly Component.Key[];
|
|
15
|
+
/**
|
|
16
|
+
* Keeps the tables to the archetypes holding these values of shared components, each one required as `with`
|
|
17
|
+
* requires it, as a system's `where` of values keeps its system to them.
|
|
18
|
+
*/
|
|
19
|
+
readonly where?: { readonly [K in Shared]?: Component<K> };
|
|
14
20
|
readonly resources?: readonly Resource.Key[];
|
|
15
21
|
};
|
|
16
22
|
|
|
@@ -62,7 +62,17 @@ void ecs.Report;
|
|
|
62
62
|
// @ts-expect-error no run numbers
|
|
63
63
|
const numbered: Promise<number> = world.run(main);
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// Asked for its changes, after its input, a run resolves to them, in the shape changes finalizers take.
|
|
66
|
+
const changes = world.run(move, { distance: 1 }, { changes: true });
|
|
67
|
+
const alone = world.run(main, { changes: true });
|
|
68
|
+
check<Equal<typeof changes, typeof alone>>();
|
|
69
|
+
check<Equal<Awaited<typeof alone>["resources"][number]["key"], Ecs.Resource.Key>>();
|
|
70
|
+
// @ts-expect-error its input comes first
|
|
71
|
+
void world.run(move, { changes: true }, { distance: 1 });
|
|
72
|
+
// @ts-expect-error a run asks only for its changes
|
|
73
|
+
void world.run(main, { changes: false });
|
|
74
|
+
|
|
75
|
+
void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, nothing, numbered, time, changes];
|
|
66
76
|
|
|
67
77
|
// A body returns no promise: it starts the work, and a later run finishes it.
|
|
68
78
|
// @ts-expect-error a body returns no promise
|
|
@@ -13,7 +13,8 @@ export type Column =
|
|
|
13
13
|
| { readonly kind: "value"; readonly value: unknown };
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* What `commands.spawnTables` spawned: each table's
|
|
17
|
-
*
|
|
16
|
+
* What `commands.spawnTables` spawned: each table's entities in its row order, the one spawned for each row, the one
|
|
17
|
+
* existing for a row its `existing` maps, and `Entity.none` for a row it didn't spawn; and how many rows it didn't,
|
|
18
|
+
* those owned by an entity outside the tables.
|
|
18
19
|
*/
|
|
19
20
|
export type Spawned = { readonly entities: readonly Float64Array[]; readonly dropped: number };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Input } from '../system/input.js';
|
|
3
3
|
import type { System } from '../system/system.js';
|
|
4
|
+
import type { ChangeSet } from '../finalizer/body.js';
|
|
5
|
+
import type { Essential } from '../component/value.js';
|
|
6
|
+
import type { Resource } from '../resource/resource.js';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* A world. Systems read and change it; readonly systems read it after each run's and tick's sync point. Every member's
|
|
@@ -23,6 +26,16 @@ export type World = {
|
|
|
23
26
|
* added or GPU work, is reported out of band, as ticks' failures are (see `add`).
|
|
24
27
|
*/
|
|
25
28
|
run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
|
|
29
|
+
/**
|
|
30
|
+
* Runs as `run` does, and resolves to this run's own changes, in the shape `Finalizer.changes` gives, rather than its
|
|
31
|
+
* value: the structural changes its commands made, rules and cascades included, to every component that isn't
|
|
32
|
+
* derived or an object a joined thread fills, the values its adds gave components entities had before and after, and
|
|
33
|
+
* each resource its systems write or reduce into whose value changed, compared as `Finalizer.changes` compares them;
|
|
34
|
+
* `previous` and `next` give values before and after the run. Commands the finalizers of an earlier sync point
|
|
35
|
+
* queued apply first, at a sync point of their own. Values written to rows aren't structural changes, so they're in
|
|
36
|
+
* none. The run pays for this only when asked. Only on the world's thread: a joined thread's rejects.
|
|
37
|
+
*/
|
|
38
|
+
run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
|
|
26
39
|
/**
|
|
27
40
|
* Adds a system to every tick, from the next one on, ordered with those already added by its constraints, as a
|
|
28
41
|
* composite's members are. A tick runs on a beat of the `cadence` resource where `always` is set, `wake` is due or
|
|
@@ -35,6 +35,7 @@ import { Observe } from "../observe/index.js";
|
|
|
35
35
|
import { entitiesWith } from "./observer.js";
|
|
36
36
|
import { invariantBenchmarks } from "./invariants.js";
|
|
37
37
|
import { journalBenchmarks } from "./journal.js";
|
|
38
|
+
import { tableBenchmarks } from "./tables.js";
|
|
38
39
|
|
|
39
40
|
/** A world whose kernels run on this thread, which the benchmarks without a placement measure; `workers` ones use the pool. */
|
|
40
41
|
const inline = (): World => createWorld({ [workers]: 0 });
|
|
@@ -648,6 +649,22 @@ export const benchmarks: readonly Benchmark[] = [
|
|
|
648
649
|
return best;
|
|
649
650
|
},
|
|
650
651
|
},
|
|
652
|
+
...[10, 1_000, 100_000].flatMap(n => ([
|
|
653
|
+
["rows", "a command per row", structural.despawnDoomedRows, 24],
|
|
654
|
+
["where", "one `despawnWhere`", structural.despawnDoomedWhere, 16],
|
|
655
|
+
] as const).map(([how, by, system, bytes]): Benchmark => {
|
|
656
|
+
const name = `despawn tagged ${n >= 1_000 ? `${n / 1_000}K` : n} ${how}`;
|
|
657
|
+
return {
|
|
658
|
+
name,
|
|
659
|
+
operation: `A run despawning the ${n >= 1_000 ? `${n / 1_000}K` : n} entities of a tag beside 100K others, by ${by}: clearing one kind, such as expired effects.`,
|
|
660
|
+
per: "despawns",
|
|
661
|
+
n,
|
|
662
|
+
typical: [10, 100_000],
|
|
663
|
+
best: { bytes, basis: bytes === 16 ? "Each despawn frees its 8-byte location and entity; the archetype empties, so no row moves." : "Each row reads its entity; each despawn frees its location and entity." },
|
|
664
|
+
run: () => structuralBest(1, world => world.run(structural.spawnDoomed, { count: n }), world => world.run(system),
|
|
665
|
+
async world => await countOf(world, [structural.doomed]) === 0, name),
|
|
666
|
+
};
|
|
667
|
+
})),
|
|
651
668
|
...([
|
|
652
669
|
["add tag", "Adding a tag to 100K entities of one archetype: a state flag set on many at once.", 1, structural.removeFlag, structural.addFlag, [structural.flag], 16],
|
|
653
670
|
["add tag kernel", "The same adds from a kernel.", 1, structural.removeFlagKernel, structural.addFlagKernel, [structural.flag], 20],
|
|
@@ -705,4 +722,5 @@ export const benchmarks: readonly Benchmark[] = [
|
|
|
705
722
|
...sharedBenchmarks,
|
|
706
723
|
...invariantBenchmarks,
|
|
707
724
|
...journalBenchmarks,
|
|
725
|
+
...tableBenchmarks,
|
|
708
726
|
];
|
|
@@ -12,6 +12,7 @@ export const flag = Symbol("structural/flag");
|
|
|
12
12
|
export const amount = Symbol("structural/amount");
|
|
13
13
|
export const extra = Symbol("structural/extra");
|
|
14
14
|
export const spawned = Symbol("structural/spawned");
|
|
15
|
+
export const doomed = Symbol("structural/doomed");
|
|
15
16
|
const source1 = Symbol("structural/source1");
|
|
16
17
|
const source2 = Symbol("structural/source2");
|
|
17
18
|
const t0 = Symbol("structural/t0");
|
|
@@ -27,6 +28,7 @@ declare module "@adobe/ecs" {
|
|
|
27
28
|
[amount]: { type: F32 };
|
|
28
29
|
[extra]: { type: F32 };
|
|
29
30
|
[spawned]: { type: true };
|
|
31
|
+
[doomed]: { type: true };
|
|
30
32
|
[source1]: { type: true };
|
|
31
33
|
[source2]: { type: true };
|
|
32
34
|
[t0]: { type: true };
|
|
@@ -128,6 +130,24 @@ export const removeFlagKernel = System.create({ kernel: true, components: { with
|
|
|
128
130
|
commands.remove(row.entity, flag);
|
|
129
131
|
});
|
|
130
132
|
|
|
133
|
+
const doomedList = [index, value, doomed] as const;
|
|
134
|
+
const doomedQuery = { with: [doomed] } as const;
|
|
135
|
+
|
|
136
|
+
/** `count` spawns of entities tagged `doomed`, beside the seeded ones. */
|
|
137
|
+
export const spawnDoomed = System.create({}, ({ commands }, input: { readonly count: number }) => {
|
|
138
|
+
for (let i = 0; i < input.count; i++) commands.spawn(doomedList)({ [index]: i, [value]: i });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** Despawns every `doomed` entity, a command per row. */
|
|
142
|
+
export const despawnDoomedRows = System.create({ kernel: false, components: { with: [doomed] } }, (row, { commands }) => {
|
|
143
|
+
commands.despawn(row.entity);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** Despawns every `doomed` entity, by one command. */
|
|
147
|
+
export const despawnDoomedWhere = System.create({}, ({ commands }) => {
|
|
148
|
+
commands.despawnWhere(doomedQuery);
|
|
149
|
+
});
|
|
150
|
+
|
|
131
151
|
export const addAmount = System.create({ kernel: false, components: { with: [index], not: [amount, spawned] } }, (row, { commands }) => {
|
|
132
152
|
commands.add(row.entity, amount, row[index]);
|
|
133
153
|
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Entity } from "../interface/entity/entity.js";
|
|
3
|
+
import type { Vec3 } from "../interface/math/index.js";
|
|
4
|
+
import { workers } from "../interface/resources/workers.js";
|
|
5
|
+
import { System } from "../interface/system/system.js";
|
|
6
|
+
import type { Tables } from "../interface/system/tables.js";
|
|
7
|
+
import type { Table } from "../interface/world/table/table.js";
|
|
8
|
+
import { createWorld } from "../implementation/index.js";
|
|
9
|
+
import type { Benchmark } from "./benchmark.js";
|
|
10
|
+
|
|
11
|
+
// Spawning 1M entities into a fresh world in one sync point, each owned by an earlier one or by none: from tables, as
|
|
12
|
+
// loading a snapshot does, and from commands; and the same tables without the owner reference.
|
|
13
|
+
|
|
14
|
+
const position = Symbol("tables/position");
|
|
15
|
+
const owner = Symbol("tables/owner");
|
|
16
|
+
declare module "@adobe/ecs" {
|
|
17
|
+
export interface Components {
|
|
18
|
+
[position]: { type: Vec3 };
|
|
19
|
+
[owner]: { type: Entity; reference: "owner" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const count = 1_000_000;
|
|
24
|
+
|
|
25
|
+
/** A root and `count - 1` entities, each owned by the one at half its position. */
|
|
26
|
+
const build = System.create(({ commands }) => {
|
|
27
|
+
const made: Entity[] = [commands.spawn([position])({ [position]: [0, 0, 0] })];
|
|
28
|
+
const owned = commands.spawn([position, owner]);
|
|
29
|
+
for (let i = 1; i < count; i++) made.push(owned({ [position]: [i, i, i], [owner]: made[i >> 1] }));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const tablesOf = (components: readonly (typeof position | typeof owner)[]) =>
|
|
33
|
+
System.create({ readonly: true, tables: { components } }, ({ tables }): Tables => tables);
|
|
34
|
+
|
|
35
|
+
const given: { tables: readonly Table[] } = { tables: [] };
|
|
36
|
+
const spawnGiven = System.create(({ commands }) => {
|
|
37
|
+
commands.spawnTables(given.tables);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** The fastest of a few runs spawning `tables` into a fresh world. */
|
|
41
|
+
const spawnFastest = async (tables: readonly Table[]): Promise<number> => {
|
|
42
|
+
let best = Infinity;
|
|
43
|
+
for (let k = 0; k < 7; k++) {
|
|
44
|
+
const world = createWorld({ [workers]: 0 });
|
|
45
|
+
given.tables = tables;
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
await world.run(spawnGiven);
|
|
48
|
+
const time = performance.now() - start;
|
|
49
|
+
// The first runs warm up.
|
|
50
|
+
if (k >= 2) best = Math.min(best, time);
|
|
51
|
+
await world.dispose();
|
|
52
|
+
}
|
|
53
|
+
given.tables = [];
|
|
54
|
+
return best;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const tables = async (components: readonly (typeof position | typeof owner)[]): Promise<number> => {
|
|
58
|
+
const world = createWorld({ [workers]: 0 });
|
|
59
|
+
await world.run(build);
|
|
60
|
+
const { archetypes } = await world.run(tablesOf(components));
|
|
61
|
+
await world.dispose();
|
|
62
|
+
return spawnFastest(archetypes);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The fastest of a few runs of `build` in a fresh world. */
|
|
66
|
+
const commanded = async (): Promise<number> => {
|
|
67
|
+
let best = Infinity;
|
|
68
|
+
for (let k = 0; k < 7; k++) {
|
|
69
|
+
const world = createWorld({ [workers]: 0 });
|
|
70
|
+
const start = performance.now();
|
|
71
|
+
await world.run(build);
|
|
72
|
+
const time = performance.now() - start;
|
|
73
|
+
if (k >= 2) best = Math.min(best, time);
|
|
74
|
+
await world.dispose();
|
|
75
|
+
}
|
|
76
|
+
return best;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const tableBenchmarks: readonly Benchmark[] = [
|
|
80
|
+
{
|
|
81
|
+
name: "spawn tables 1M owner",
|
|
82
|
+
operation: "commands.spawnTables of 1M entities into a fresh world, each but one owned by another of them, and its sync point: loading a snapshot of a hierarchy.",
|
|
83
|
+
per: "spawns",
|
|
84
|
+
n: count,
|
|
85
|
+
typical: [1_000, 1_000_000],
|
|
86
|
+
best: { bytes: 36, basis: "Each entity copies its 12-byte position and 8-byte owner, and writes its 8-byte entity and location." },
|
|
87
|
+
run: () => tables([position, owner]),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "spawn tables 1M",
|
|
91
|
+
operation: "As `spawn tables 1M owner`, without the owner reference: its cost is the difference.",
|
|
92
|
+
per: "spawns",
|
|
93
|
+
n: count,
|
|
94
|
+
typical: [1_000, 1_000_000],
|
|
95
|
+
best: { bytes: 28, basis: "Each entity copies its 12-byte position, and writes its 8-byte entity and location." },
|
|
96
|
+
run: () => tables([position]),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "spawn owned 1M",
|
|
100
|
+
operation: "A main-thread system spawning 1M entities through one spawner, each but one owned by an earlier one, and its sync point.",
|
|
101
|
+
per: "spawns",
|
|
102
|
+
n: count,
|
|
103
|
+
typical: [1_000, 1_000_000],
|
|
104
|
+
best: { bytes: 36, basis: "Each entity writes its 12-byte position and 8-byte owner, its 8-byte entity and location." },
|
|
105
|
+
run: commanded,
|
|
106
|
+
},
|
|
107
|
+
];
|