Skip to content

[core] Fix override refs not refreshing after a base changes - #2397

Open
simon112 wants to merge 2 commits into
SanderMertens:masterfrom
simon112:fix/override-refs-stale-reachable-cache
Open

simon112 wants to merge 2 commits into
SanderMertens:masterfrom
simon112:fix/override-refs-stale-reachable-cache

Conversation

@simon112

Copy link
Copy Markdown
Contributor

Depends on #2396, whose commit is included here; only the second commit is new.

A table's override refs are recomputed when the reachable cache generation of one of its (IsA, base) pairs changes. Invalidating the reachable cache doesn't increment the generation of a cache that is already invalid, so a generation recorded while the cache was invalid could stay the same after a later change to a base:

  • When a base removed an inherited component, overriding it copied from a base that no longer had it. That asserts in flecs_table_invoke_ctor, and crashes in release builds.
  • When a base added the component, the override copied the value of a base further up, or no value.

The reachable cache of each base is now revalidated before its generation is compared or stored. That can rebuild the cache while events are forwarded, which is safe after #2396. Because the cache can't be revalidated in multithreaded mode, flecs_table_get_override no longer updates the shared override cache in that mode, and looks up the base the same way it already did for sparse components.

Tests:

  • Prefab.override_in_observer_after_base_change passes on master. It guards the combination of the two commits.
  • The other new tests fail on master.

🤖 Generated with Claude Code

Simon and others added 2 commits November 12, 2026 02:40
…ache

flecs_emit_forward iterates the reachable cache of a relationship pair
while it invokes observers. An observer that modified the cache (for
example by changing a base and then instantiating it) rebuilt the cache
array in place, so the loop read freed memory. An observer that modified
or deleted a source of the cache left the cache entries, and the table
record passed to the next observers, pointing to the previous table of
the source. That asserted in debug builds, and read an invalid or freed
table record in release builds.

Once observers have been invoked, a rebuild of the cache allocates a new
array instead of modifying the iterated one, and the iterated array is
freed when the iteration is done. A change to a source increments the
generation of the cache, which now also marks the iterators of the cache
as changed. Until then the cached table records are used. After a
change, the table record of the source is looked up in its current table
before each observer (or each result of an observer with a fixed source)
is invoked, and the remaining observers for the id are skipped if the
source is no longer alive or no longer has it.

Tests: up observers that instantiate a changed base, add to or remove from
the base, delete a tag of the base or the base of the base (with the id
recycled into the same table), two observers on the same id, propagation
to an instance's own instances, and fixed source multi observers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A table's override refs are recomputed when the reachable cache generation
of one of its (IsA, base) pairs changes. Invalidating the reachable cache
does not increment the generation of a cache that is already invalid, so a
generation recorded while the cache was invalid could stay the same after
a later change to a base. When a base removed an inherited component, the
override then copied from a base that no longer had it (assert in
flecs_table_invoke_ctor, a crash in release builds). When a base added
the component, the override copied the value of a base further up, or no
value.

The reachable cache of each base is now revalidated before its generation
is compared or stored. This can rebuild the cache while events are
forwarded, which is safe because a rebuild doesn't modify an array that
is being iterated. Because the cache can't be revalidated in
multithreaded mode, flecs_table_get_override no longer updates the shared
override cache in that mode, and instead looks up the base the same way
it already did for sparse components.

Tests: a base and an intermediate base that remove or add the component,
a three level chain, the override policy, ensure from a stage in readonly
and multithreaded mode, OnSet when the override is removed, and an
override created by an observer while the base changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant