Repository navigation
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 20 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
|
This doesn't provide advanced component tracking; it's just enough to handle major changes. It’s useful when you want to use a physics engine or networking that doesn't integrate seamlessly with ECS. I would have liked to use a flag system that notifies mark entity and lets the user query them, However, that introduces too many complications. Such as what happens if the user doesn't query, and the need to store the components and entity before destruction. Meaning an other world just holding data before final change. One issue with Signal is the fact of not knowing what the other handler can do. Like doing a loop of removing adding back component. Or adding a component that an other handler already added. All of this to say, i look for the simplest solution to feed the lake of notification. There is no problem to fully refuse this draft. I will keep the work for me. |
|
And i added an entry to EntityRef, can be remove it's only add this page because i find the need to mention it in the doc of Signal. |
GC_Drops_Signals_Once_Everyone_Has_Unsubscribed test will need a rewrite base on the anwser of the open question (GC one). Currently it's the only whay to check. Else it will imply relfection.
|
I forgot to rename the method in the doc 🤡 i will force push !!! |
|
Questions:
This is a classic problem. I suggest each registered listener gets notified on "On". A serious issue with this is that listeners must not be invoked while the world is in
Orphans should, when the Archetype is forgotten (world calls
Same has always been true for runners. |
|
Performance impacts: P1: With 0 subscribers, this PR still has a noticeable performance impact. P3: With batches, and actual handlers, even through these numbers are still small, we see an even stronger 59.6 µs becomes 349.8 µs, that's 600% or so. I think that one is actually understandable. Becuase some work has to be done. However, The performance impact is quadratic - number of entities X number of handlers. P1: Therefore I think maybe it is better to delegate this to the user side of things:
|
|
P1: Removal-handler exceptions can corrupt World state. A throwing handler in a secondary aspect can leave world.Count == 1 while world.All.Count == 0 (it's fine for the operation to fail and stop there, but the world can't be left corrupt) Also the world.Count can be inconsistent during a handler's processing. P1: Spawning inside handlers invalidates live references (maybe). Template spawning executes immediately despite the World lock, because there's always logical space to append so we don't need to queue. However, I realized that fennecs can resize the array and then refs are invalid. This is a 50% fennecs, 50% PR problem. In fact I may need to block spawning due to the array resizes. Oof. Thank you for inadvertently bringing this hard crash bug risk to light. :) I might have to do a fennecs update ASAP. |
|
P1: This PR doesn't compile for me. Probably a rename refactor didn't go trhgou all the files, wasn't hard to fix. |
|
Suggestion for the Lifetime of subscriptions: using var subscription =
world.SubscribeAdded<Position>(OnPositionAdded);i.e. a disposable subscription, meaning the world can dispose it, too, but "orphans" don't really exist - they live per component / TypeExpression, not per archetype. |
| A Batch only signals what actually *changes*. Re-writing a component that Entities already have (via `AddConflict.Replace`) is a value change, not an addition – so it stays silent. Likewise, a tolerated no-op removal (`RemoveConflict.Allow` on an Entity that never had the component) raises nothing. | ||
| ::: | ||
|
|
||
| ## Removed... but why? |
There was a problem hiding this comment.
Love this documentation style, very on-brand.
Add `On` method returning a Signale. `On` as the same signature than `Add`. Meaning you can specify if you whant to handle relation. // This an exemple, avoid lambda you can't remove the handler world.On<Position>().Added += (EntityRef e, ref Position p) => ...; world.On<Position>().Removed += (EntityRef e, in Position p) => ...; Signale are contained in a Dictionary with for key the type of the compoent tarrget. Then a list do the job to raise every matching (base on the Expression). World lock are by default on signal to avoid raising other signale will handling one. Open question: - Should the Expression follow a patern ? Or give a new `Signale` each time? Currently the way of storing Expression kind a bad. For 2 differente Expression matching a same case, it will be raised depending base on the first call of `On`. fix: prevent stack overflow when bulk deferred operations signal
Open question, should the Expcetion expose a method to allow for continutation of the operation? Or finishing the operation should not be an option and it should be donne whatever.
Open question: Should orphan Signals be added back to the World? Or should we only count alive signal and remove clean of the GC?
NB: I call this Event because most ECS use the term of Event, where it's a simple Signal.
GC_Drops_Signals_Once_Everyone_Has_Unsubscribed test will need a rewrite base on the anwser of the open question (GC one). Currently it's the only whay to check. Else it will imply relfection.
I run the benchmarks longueur, this to check if the last change on the benchmark change somthing. I use those metric for the doc.
9c57a1c to
743e265
Compare
|
of course i forgotten rebase copy past commits, anyway force with lease |
I'm thinking of creating a With this solution it's simple to catch all entity moved across archetype allowing for the use of span. |
I badly renamed |
|
I fixed the syntax and ran all the tests. For some reason I can't identify, some tests failed. Rerunning them fixed the problem... I suppose my test run exceeded the quota of concurrent active World by running them in parallel, i have no other hint. |
…rks and the tests
|
I am working right on the Pr, i will continue this weekend. |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
I have simplified the code; I believe it would be possible to improve execution time by using well-defined collection types. Interfaces such as |
245288c to
4dbf8c7
Compare
…an orphan GC used to forget idle Signals outright; one kept in a field then accepted handlers and never fired. Now the registry only lets go of them: the World keeps a WeakReference per dropped Signal, On<T>() revives the very same instance while someone still holds it, and subscribing to a kept Signal re-registers it. Signals nobody holds are collected by the runtime. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SignalOverheadBenchmarks and SignalHotpathBenchmarks use only API that also exists on main, so the very same file can be run on both trees to measure what merely having Signals costs when nobody subscribes. Hotpath splits Batch / Spawn / Despawn at two sizes to tell fixed from per-Entity cost. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
4dbf8c7 to
f7c1a4b
Compare
Covers plain Remove, Batch removal, and Despawn with the throwing handler on the Main or on a secondary Aspect. The secondary-Aspect Despawn case currently fails: Main has already deleted the Entity's rows when the secondary Aspect's handler throws, leaving world.Count == 1 while world.All is empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EntityTemplate.Spawn ignored the World lock: there is always room to append, but appending can grow an Archetype's Storages and pull them out from under live references. A Signal handler spawning into the Archetype whose span it was handed then wrote into a discarded array; a runner spawning while iterating had the same hazard. While locked, the Entities are now minted at once (the caller needs the handles) and join only the Root; their Components follow as deferred Adds on catch-up, raising AfterAdded there like any other deferred Add. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
world.SubscribeAdded<T>(handler) / SubscribeRemoved<T>(handler) (and their Match overloads, and the same methods on Signal<T>) return a Subscription whose Dispose unsubscribes the handler. Lambdas can now be subscribed without leaving a Signal behind with a handler nobody can remove; once its last Subscription is disposed the Signal is idle and World.GC lets go of it. A Subscription lives as long as its owner keeps it, not as long as the Component: World.GC never drops a Signal that still has one, even when no Entity stores the Component. It keeps its World, its Signal, the handler and whether it was an Add or a Remove, and Dispose takes that handler back off directly, with no captured lambda. The World tracks the Subscriptions it handed out and disposes them when it is disposed itself: they report Active == false, and disposing them later is a no-op. Disposing the World also clears every Signal's handlers without counting down, so a -= outliving the World is harmless too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
World.Refresh only ever set the per-type watch bits, so a Component type stayed 'watched' after its last handler left: every Add and Remove of it kept taking the World Lock for a dispatch that found nobody. Refresh now recomputes both bits from scratch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Events page still showed the old per-Entity EntityRef handlers. It now documents span handlers, SubscribeAdded/SubscribeRemoved and their Subscription, deferred template spawns from handlers, and that a throwing BeforeRemoved handler leaves the World untouched. The cost section is re-measured against main, with no subscriber and with 1 and 4 handlers. EntityRef no longer claims Signal handlers hand it out, and the Glossary names the Subscribe methods. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on 10 000 Entities with nobody subscribed (median of 3000 runs): DespawnAllWith took 390 us against 320 us on main. Two causes, both from the SignalException wrapper: - the try/catch sat inside the CatchUp loop, so every operation paid for the exception region; it now lives in ApplyFromSignal, called only for operations a handler queued; - FromSignal grew DeferredOperation by a padding slot; Opcode is now a byte, which packs it back to its former size. Neither is enough alone (370 us and 384 us); together the wave is back at ~325 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With nobody subscribed, a 10 000-Entity Template spawn took 47 us against 36 us on main (median of 3000 runs). The Signal path's using-Lock, written inline in World.Spawn, put a try/finally in the method and slowed every spawn. It now lives in SignalSpawned, never inlined, behind a Signalling check: back at ~36 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Maybe move away of events to give a stream of entityRef and allow to specify a priority when subscribing. |
Add
Onmethod returning a Signale.Onas the same signature thanAdd. Meaning you can specify if you whant to handle relation.Signale are contained in a Dictionary with for key the type of the compoent tarrget. Then a list do the job to raise every matching (base on the Expression).
World lock are by default on signal to avoid raising other signale will handling one. This also allow to pass a EntityRef.
Open question:
Signaleeach time? Currently the way of storing Expression kind a bad. For exemple: 2 differente Expression matching a same case, it will be raised depending base on the first call ofOn.For new we only count alive signal. Orphan Signal get remove of the list of Signal to free up space (like GC imply).
Actually Exception are raise and there is now way to fix up/continue the current Action except retrying.
What is done to do:
Summary by cubic
Adds component lifecycle signals so subscribers can observe structural changes across entity, batch, spawn, despawn, relation, and link operations.
World.On<T>(Match)returns a signal memoized per expression, withAfterAddedandBeforeRemovedhandlers; removal handlers receive aRemoveCause.Entityand component values;AfterAddedvalues are a writable slice of storage.SignalException; failures during deferred catch-up are markedDeferred = true.World.GCdrops signals with no subscribers weakly: a signal still held by user code is revived byOn<T>(), and the runtime collects signals nobody references.Benchmarks on both this branch and
mainmeasure what having the feature costs when nobody subscribes. Documentation covers events andEntityRef, and tests exercise dispatch under parallel job runners.Written for commit f7c1a4b. Summary will update on new commits.