Repository navigation
ecs: new @adobe/ecs package - #217
Merged
Merged
Conversation
…gh main-thread systems and kernels and the sync point: spawn bulk, bulk from a kernel, singles across four archetypes and from value objects over sixteen archetypes, more than spawns cache; despawn bulk, bulk from a kernel and one; add a tag, from a kernel and from four source archetypes, a value, several at once, and a replacement; remove a tag, from a kernel and from four source archetypes, and several at once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ried spawns: node and chromium, structural benchmarks included Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it ran with Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt.Key, Resource.Key and Trait.Key, now exported, replace Component.Name, Resource.Name and the trait name type; systems, rules and finalizers keep names; a type test shows each key type takes its own kind's keys only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rchetype.create, which returns the list given, typed as its tuple, with a program-wide index in a hidden property, and throws for a component listed twice; a resource or string in one is a type error, and archetypes go wherever component lists do Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, returning the world's spawner for that set of components, the same each time, and values exact against the list, a tag or a component whose type allows null left out to start at true or null, undefined never leaving a component out, a list that isn't a tuple taking any of its components' values; add takes one component and its value, none for a tag, or a list and its values; remove takes one component or one list; a key that isn't a component commands may name, or one listed twice, throws at first use Lists resolve to shapes shared by every list of the same components: archetypes by their index, other lists by their keys among the last eight. A shape's spawner is code generated for it, one store per value, or a loop over its columns where code can't be generated; the plugin passes literal values of main-thread spawn and add sites positionally, to a spawner generated per site key order and shape. At the sync point, entries move between pairs of archetypes as blocks, copying shared columns column by column, rows left are removed last first, so an archetype moving out drops rows without copies, and list adds and removes resolve their target from each source archetype once. Kernels spawn, add and remove through array literals and module constants holding them or Archetype.create of them, and declare components through them; tests, samples and benchmarks use the new forms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s module and the module's path in that package, so a workspace-linked package names no directory outside it, else a dependency's package name and subpath, else the path from the root; a key's label is its description while no other registered key has it, else its id, or without one its description numbered by registration; tests expect description labels, and ids where descriptions are shared Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t name components first, spawners, grouped moves at the sync point, ids from package names and labels short unless ambiguous; guide examples declare an archetype and give tags no values; a transform test checks a main-thread list declared as a module constant against its body Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unction per kind of site, so finding a plan allocates no closure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gainst the baseline 4e63c93: spawns, despawns and removes faster, adds within noise or faster, spawn bulk kernel +9% and spawn singles +7%; perf records generated against loop spawners, prepared sites kept, the archetype index table, grouped moves, and the variants not kept Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on's or command's list may be a constant of any module, followed through the linker with re-exports and namespace members, its array literal or Archetype.create read in that module and each key named by the kernel's own binding of the same definition or else as List[k], so editing the list's module compiles the kernel again; a list the build can't read, such as one computed when its module runs, fails the build at the call or declaration naming it, and an imported list given to add or remove is no longer taken for one key; CLAUDE.md puts performance above every other concern Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing at every entry, order a pair's entries without a second pass when each pair's entries are consecutive, as a system's spawns or moves from one archetype are, and skip removing rows when no entry leaves one; spawn batches keep their archetype, so committing one looks nothing up: spawn bulk kernel 130 → 120 ns, spawn singles 489 → 396 ns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent, since a subarray to copy from costs more than the rows: spawn singles 396 → 292 ns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…31 → 121 ns, back to the baseline's 120, spawn singles 500 → 292 ns, every other node and chromium benchmark within noise; perf.md gives each sync point step's own measurement, and a spawn site remembering its last archetype goes under tried and not kept Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names are labels that needn't be unique, Rule.create returns its rule (typed Rule, its name), reports name systems and finalizers by label, and the "thread" failure kind leaves World.Report and the report resource's schema until threads exist; design.md states identity, labels and ids for items, threads.md that the kind returns with threads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, reports and errors give labels, Rule.create returns its rule, report failure kinds without "thread"; a new section on identity and ids, with an example of systems sharing a name and of a module evaluated again matching by id; the kernels example spawns from a list imported from another module, and the guide names the lists the build rejects Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r by name: no global name uniqueness, so two libraries can both name a system "linked"; errors and reports label each by its name, else its name with its plugin id, or numbered without one; the plugin gives a top-level binding's System, Rule or Finalizer constructor call (a kernel's wrapped call too) the id <module>#<binding> on its declaration, so a module evaluated again declaring the same is the same item (a new system value schedules compare as the first, the rule or finalizer already recorded) and a changed one throws with the reload message; schedules and finalizer order resolve constraints by identity, GPU records key by a number per system, and finalizer and GPU failures carry the item, labeled only when one is reported Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… when its definition is Symbol(...), identified or not, also through a namespace, a list when it's a list, and otherwise a build error naming the call and the reason; a name from a package external to the build is one key, and registration rejects a list there Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…: System.create and System.render check registration of every key, references written, reductions, conditions, up, GPU and render placement and render accessors; registering a compiled kernel checks it against its declaration and schemas; Component.register rejects the component closing a within cycle; finalizers check their keys are registered. A world's first run keeps only giving ids, adding resources, uncompiled kernels, loading kernels and generating GPU shaders Conflicts with identity by value resolved: the moved checks keep the plugin id and claimSystem/addFinalizer(…, id), registering a kernel labels it with labelOfItem, the declaration docs and design.md drop name uniqueness and the thread kind, and the tests check a rejected declaration records nothing by the label the next one gets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tatuses, payloads and claim lines to the arena once each worker that had a window acknowledges a message, so resizing the pool no longer grows what the arena holds; one that doesn't within a second, or died with a window open, keeps them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alues, and observers that throw then, those of report included, are failures in that tick's report rather than the next run's; report holds them without notifying its observers again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…build, which drops entries' signatures, so the GPU sample's vite build no longer emits empty kernels; its worker.js resolves on Vite 8, and a test builds the sample Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ers or not, so a run awaits nothing more than before: run idle back to 375 ns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s needing no world run at creation: System.create for lists, lookups, conditions and reductions, where its module registers a kernel for one that doesn't match its declaration, Component.register for a within cycle, and a world's first run only for an uncompiled kernel, a failed load or an ungenerated shader; kernels' add and remove take a Symbol(...) name as one key and a list as a list, a build error otherwise, with an external package's name one key; guide tests for a kernel removing an imported key and a within cycle rejected at registration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n list of a kernel gets a positional spawner, generated for its components' layout, writing into a batch per component set, on the main thread per runner, with each chunk's buffer naming the rows it staged so they commit in its place in merge order, and on workers per task, posted back with the chunk's commands; the sync point commits staged rows before any command, as it does main-thread batches: spawn bulk kernel 120 → 34 ns Cherry-picked onto knye/new-ecs: conflict in workers.browser.test.ts resolved keeping the new spawn systems without the labels system the branch removed; the duplicate-component spawn check moves from bindKernel to checkKernel, which also rejects a spawn site naming an index outside its keys, both at registration; dropped two failure screenshots the original commit added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed array rather than an array of numbers: spawn bulk kernel 34 → 18 ns, spawn bulk 41 → 25 ns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s doubling to 4096, so workers spawning together don't contend for the cursor; each run or task ends its block, and the sync point returns the positions it left unused to the free list, keeping their generations since they were never handed out, so the leak scan stays for chunks that failed: spawn bulk kernel 18 → 10 ns, and in chromium 18 → 10.5 ns on the main thread (128 ns before staging) and 54 → 7.5 ns on workers (177 ns before staging); a browser benchmark of kernel spawns on the main thread and on workers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nly when its kernel spawns, which every other system's run had paid for: run 50 idle 120 → 113 ns per system, back to the record's 108–112 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…21 → 10.3 ns, spawn bulk 40.4 → 25.4 ns, spawn sixteen archetypes 56.5 → 41.1 ns; in chromium the new spawn bulk kernel benchmarks 128 → 11 ns on the main thread and 177 → 7.65 ns on workers, against ebde4c9's implementation; every other node and chromium benchmark within noise. perf.md gives each step's measurement, and world batches for main-thread kernels, blocks left to the leak scan, a first block of 64 and staged kernel adds and removes go under tried and not kept Regenerated from 3f92a772 on knye/new-ecs: the record is taken again at the merged 2b07c57 rather than cherry-picking the worktree's record at 872d68fd; perf.md's steps and tried-and-not-kept entries are kept as the original commit gave them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… system that declares nothing is created from its body alone, System.create(({ commands }) => …); an unnamed item is labeled by its binding and id, "spawnOnce (src/setup.ts#spawnOnce)", or else by its kind numbered, "system #3"; Observe is also a namespace, so consumers write Observe.toPromise; no interface doc mentions another package
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ords the entity an element shows under a unique symbol, with Object.assign, read through in narrowing; entityOf(event) gives the first one on the event's composed path, and entityOfElement a todo-move's item and before. They replace todoOf and its WeakMap; followups.md's finding 3 is resolved Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lter of its system, as a resource in changed is: every row passes when an entity entered or left it or moved to another key or position since the system last ran; a system whose only change filters are these, with no up, is skipped before its body when none changed, keeping its lastRun, at one comparison per such lookup against the index's tick of its last refresh that changed an entry. Its lists' changed reports entries alone. Observer systems and CPU kernels take it; finalizers don't, by type and check, nor lookups by entity; systems and observer systems on joined threads are rejected. Type tests red then green, index-changed.test.ts and index-joined.test.ts red without the runner's filter and the checks; the guide's player count uses it; benchmarks observer index unchanged 100 and its by-lists variant Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… system, bindCompletion, looking up completion/byDone with changed: true, so it runs only after a pass in which a todo was added, removed or toggled, with no early return Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… add as its value: a plain or string key fails the build only in a spawn's values, or in an add's whose components are a list as written, an array literal or a module constant of one or of Archetype.create, where commands.add(entity, caption, { text: "a" }) failed the build before. transform.test.ts red without the fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g, a union of string literals, and either with null. A thread on one is rejected in types (ThreadOf, IsShared, the thread trait) and at registration, a string places no system, and an index keyed by strings is kept on the world's thread. The world's thread holds each string as a value, as it does an object, at an object's cost (string read 10K 2.78 ns a row, string write 10K 19.5 ns); from the first share until the last joined thread is gone it also keeps a record of each one in its memory, a column of handles naming them, 0 for null (strings.ts): immutable, carved from slabs by size class up to 512 bytes, a write making a new one (string write 10K joined 126 ns) and freeing the old at the end of the second sync point after, which no task, tail or row observer post outlives. A joined thread decodes a record once and keeps its text by generation (joined observer strings 10K 358 µs a tick against 309 for numbers). A joined thread's finalizers read copies in their stop lists, let go once acknowledged, and an observer system reading a string resource doesn't post copies of its resources, which nothing waits for. Only the world's thread writes strings: a system placed on a joined thread writing one is rejected, a command there gives one only its start; kernels don't read them yet. The todo sample's label and filter drop thread: "main". Tests whose strings stood for a joined thread's objects use objects. Type tests, registration and index tests, strings.test.ts (values, records kept from share to gone, records freed), a joined thread's observer reading a world string, the joined rejections, a joined finalizer's stops after their records were reused, and a joined observer reading a string resource while runs rewrite it, each red without its part; the observer-joined fixture waits for the last stalled run before counting switches, which ticks of strings exposed. Benchmarks string and object read and write 10K, string write 10K joined, joined observer strings and numbers 10K; every other touched benchmark within noise against c6bb3c8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… still private: every @adobe/data-ecs is @adobe/ecs, module augmentations, the ids registry's symbol and the join protocol's names included, and the plugin's kernel ids are /@ecs-kernel/ and \0ecs-kernel:. The joined observer fixture's string resource scenario passes round 0's posts through its gate and runs round 0 until the worker's observer system ran, since chromium took the system up after the first run's pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…be/ecs" gives the type and a value whose create is the implementation's, typed Create, as the other namespaces give theirs. src/index.ts is compiled with the implementation now, and src/observe imports the interface's index, the public API but for World.create, which its factories never use. The guide and the todo sample create worlds through it, and the sample's imports.test.ts rejects any import of the implementation's index. exports.test.ts's consumer and world.test.ts red without it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s record at e3c30aa, the same before strings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the package root, join typed by the interface's new Join, (port, { threads? }) => World, which the implementation's join now is. The transfers and particles samples create and join worlds through them, so no sample imports the implementation. exports.test.ts's consumer and world.test.ts red without it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch a pool's worker_threads import without the dev server: in a consumer's node tests a kernel system failed on the pool, which imported the server's /@ecs-kernel/ id. A page and its workers still import /@ecs-kernel/ from the server. plugin.test.ts's node worker importing a served kernel red without it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one literal of the leaves' loads and one store per leaf, the closures per leaf where 'unsafe-eval' is forbidden: struct copy 100K 147 → 17.6 ns a row in node, 20.4 in chromium. A command following its entity's previous one in the same buffer extends that one's range of values instead of a map entry: struct replace kernel main 144 → 60.9 ns a command. A kernel takes resources.is conditions, which registration rejected as unread resources; conditions.test.ts red without the fix. layout.test.ts covers generated structs of every leaf kind, a __proto__ member included. followups: tail late snapshot 10K is 58× slower than the e3c30aa record, already at ace6707 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er systems can't draw with per-row CPU values such as bind groups Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he compiler and the runtime matched them already, and the kernel declaration's type now allows them (contract widened). Type tests red then green; the workers fixture's kernel and main-thread system both look up through has and not, red when kernels ignore not. The guide teaches wake for follow-up ticks, a lesson red without the wake, and Entity.none for an entity resource with no entity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… while the entity had them before and after, removed and added again meanwhile or not, so an edit committed as one add is one change with its previous and next values (contract widened). An entry between the same archetypes as the last, whose one range of values names the same components, joins its group: struct replace kernel watched 187 → 76.8 ns a command, against 63.6 unwatched. IsData looks at members five levels deep, so an object holding a value that isn't data, such as a record of GPU or DOM handles, isn't data and registers without a schema (contract changed). finalizers.test.ts and the type tests red without each Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in schedule order, each system's by row in the order its rows run, whichever threads ran them, so of two adds of one component to one entity the later one's value stands. commands.test.ts pins it over a kernel, a main-thread system adding twice and a kernel after them, red with the buffers' order reversed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation compares the schema's type and the declared one as they are, and when they differ, with every object at any depth up to eight flattened into one, so Box & { id } matches a schema of an object with id, x and y. The resource declaration type test red without it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… type error's value is each object component and resource it uses, by key, with its thread ({ [audio]: "threadsTypeTest"; [document]: "main" }), and the runtime error names each key's label on its thread. threads.type-test.ts and the create tests red before
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rows or struct resources: one reading only its own thread's objects has nothing there a sync point writes. Since every run spawns its report, until 256 are, almost every sync point applies something, and settling every tail made a run wait out a body-only tail's whole body once its head was claimed. tail late snapshot 10K 474-484 -> 9.0-9.5 ns a row copied in node, against 8.9 for e3c30aa beside it (record 8.2), and 450 -> 6.0 ns in chromium (record 5.0); the benchmark times the run alone and reads its report after, as runReported timed a second run too. tail prompt, tail overlap and tail overlap waiting within noise. tails.cases.ts: a run spawning rows finishes while a tail reading only its thread's objects is held in its body, red without the fix in node Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the whole column. Components a GPU kernel or render system uses keep ticks, and kernels writing them keep their chunk stamps (not their row stamps unless observed); an upload writes one writeBuffer per run of adjacent chunks stamped since its last upload or holding rows a sync point appended or disturbed, clamped to the row count, and a column the GPU wrote that the CPU hasn't read back writes only its dirty rows. markWrites no longer marks such storages whole. Also fixes a WASM kernel loaded again to stamp a component that already kept ticks passing the muted column's cached address 0 and writing its stamps over the entity table: addresses are kept per load as well as per archetype version. In chromium, new gpu upload one row 100K (a target system writing one of 100K positions a render system draws) 0.44-0.47 -> 0.048-0.058 ms a run, the upload 1.2 MB -> 12 KB; new gpu upload all rows 100K 5.35-5.55 -> 5.24-5.47 ns a row and observe gpu entity factory within noise. gpu.browser.test.ts: a row system, WASM and JS kernels, on the main thread and on workers, write one row of a 2,500-row GPU column and the next upload writes only that chunk's bytes, red before; wasm-kernels.test.ts: a kernel stamps what it writes once a change filter observes a component that already kept ticks, red before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r on System.render giving one table over its archetypes in an index's order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ass helper several render systems share declares its context and tables by name (contract widened). system.type-test.ts red without them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the rest of the monorepo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se, 0.1.0, LICENSE) and keeps its own version: bump skips a publishable package whose package.json sets independentVersion true. The package leaves out compiled tests, fixtures, benchmarks, samples and the guide, 9.6 to 4.3 MB unpacked; the runtime's imports, entry points and worker included, reach none of them, and the packed tarball installs and runs a world in node Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read the sources, tests, guide and samples; it leaves out dist's internal observe tooling and the build info files that leaked into it. 1.7 MB packed, 8.0 MB unpacked, every map source in the package Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ignored, and the two committed by mistake are untracked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olute error of 2^-11 on [-π, π], where its inputs lie: CI's software adapter gives cos within 1.9e-4, which the 1e-4 every other builtin keeps failed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
@adobe/ecs, a new entity component system package, and makes it publishable.@adobe/ecsis public (private: false) at its own version, 0.1.0. It publishes with the other public packages throughpnpm -r publish.scripts/bump.mjsskips a publishable package whose own package.json sets"independentVersion": true, so the lockstep bump leaves@adobe/ecsalone. There is no package list to maintain. Running bump on copies of the package files bumps the 10 lockstep packages and keeps@adobe/ecsat 0.1.0.distwithout its compiled tests, fixtures, benchmarks, samples or guide, so it is 4.3 MB unpacked rather than 9.6 MB. An import trace from the entry points and the worker reaches none of them, and the packed tarball installs and runs a world in node.pnpm run buildandpnpm testinpackages/ecs, 133 files and 1005 tests.🤖 Generated with Claude Code