Skip to content

on_replace: set other_table and set_fields in flecs_invoke_replace_hook#2137

Open
Indra-db wants to merge 6 commits into
SanderMertens:masterfrom
Indra-db:indradb/on-replace
Open

on_replace: set other_table and set_fields in flecs_invoke_replace_hook#2137
Indra-db wants to merge 6 commits into
SanderMertens:masterfrom
Indra-db:indradb/on-replace

Conversation

@Indra-db

@Indra-db Indra-db commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Fix: Populate other_table and set_fields in on_replace hook iterator

Problem

on_replace callbacks had no way to tell whether prev held a valid existing value or uninitialized memory. it.other_table was always NULL and it.set_fields was always 0.

Several call sites also passed r->table after flecs_ensure had already moved the entity, so the table reflected post-move state — making new adds and replacements indistinguishable.

Fix

flecs_invoke_replace_hook — new prev_table parameter

it.table serves two purposes: component storage access (used internally by flecs_on_replace_parent to find name data) and the informational other_table field for user hooks. For the entity-init path these must be different values, so they are now decoupled via an explicit prev_table parameter:

it.other_table = prev_table;
it.set_fields  = (prev_table && ecs_table_has_id(world, prev_table, id)) ? 3 : 2;

it.table stays as the final committed table (required for internal storage access). it.other_table gets the pre-move table. set_fields = 3 means both prev and next are valid (replacement). set_fields = 2 means only next is valid (new add).

Pre-move table at all call sites

  • flecs_set_id_move, ecs_set_id, ecs_cpp_set, ecs_cpp_assign: capture prev_table = r->table before flecs_ensure / flecs_get_mut, pass as prev_table.
  • flecs_cmd_batch_for_entity second-pass: pass start_table (table before any commands in the batch ran).
  • Deferred existing (EcsCmdAddModified): passes r->table — entity already has the component at this point, so set_fields = 3 is correct.
  • Entity-init (ecs_entity() with .set): captures init_table = r->table before any components are added, passes as prev_table.

flecs_copy_id refactor

flecs_copy_id previously contained its own hook call using r->table (always post-ensure). Removed it — flecs_copy_id is now a pure copy-and-notify function. Both call sites fire the hook before flecs_copy_id with the correct pre-move table. The non_trivial_set fast path is safe: it is only false when no non-trivial hooks including on_replace are registered.

Semantics

Path set_fields bit 0
ecs_set / ecs_cpp_set, component not yet on entity 0 (new add)
ecs_set / ecs_cpp_set, component already on entity 1 (replace)
ecs_cpp_assign (requires component to exist) 1 (replace)
Deferred existing — EcsCmdAddModified (fires at insertion) 1 (replace)
Deferred batch flush, new component 0 (new add)
Deferred batch flush, existing component 1 (replace)
Entity-init with .set values 0 (new add)

Tests

All tests register on_replace with a raw hook that captures it->other_table and it->set_fields, then assert both match expected values.

Core (ComponentLifecycle): on_replace_other_table_new_entity, on_replace_other_table_existing_entity, on_replace_other_table_set_existing, on_replace_other_table_batched_new_entity, on_replace_other_table_batched_existing, on_replace_other_table_entity_init.

C++ (Entity): on_replace_other_table_set_new, on_replace_other_table_set_existing, on_replace_other_table_assign_new, on_replace_other_table_assign_existing.

Compatibility

No public API or ABI change. flecs_invoke_replace_hook is an internal function. Callbacks that ignore other_table and set_fields are entirely unaffected.

@Indra-db Indra-db marked this pull request as draft June 12, 2026 03:26
@Indra-db Indra-db force-pushed the indradb/on-replace branch from 4707977 to 5f3b50e Compare June 12, 2026 04:59
@Indra-db Indra-db marked this pull request as ready for review June 12, 2026 08:18
@Indra-db

Copy link
Copy Markdown
Contributor Author

had to add the param for a set fix, if this is not acceptable, you can close this. I don't see any other way around it.

Comment thread src/component_actions.c
it.offset = 0; /* Don't set row because we don't want to offset ptrs */
it.flags = EcsIterIsValid;
it.other_table = prev_table;
it.set_fields = (prev_table != NULL && ecs_table_has_id(world, prev_table, id)) ? 3 : 2;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoking ecs_table_has_id is not feasible here as it would add way too much overhead to on_replace hooks.

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.

2 participants