Track bulk operation pattern

This commit is contained in:
Ukendio 2025-11-22 16:56:25 +01:00
parent 4db046b925
commit 7bc6935965
3 changed files with 319 additions and 16 deletions

View file

@ -29,7 +29,7 @@ local function observers_new<T...>(
callback = callback
local archetypes = cachedquery.archetypes_map
local terms = query.filter_with :: { jecs.Id<any, any> }
local terms = query.filter_with :: { jecs.Id<any> }
local entity_index = world.entity_index
@ -73,13 +73,13 @@ local function observers_new<T...>(
end
local without = query.filter_without
if without then
if without then
for _, term in without do
if jecs.IS_PAIR(term) then
local rel = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onremoved = world:removed(rel, function(entity, id)
local onremoved = world:removed(rel, function(entity, id, delete: boolean?)
if not wc and id ~= term then
return
end
@ -130,28 +130,49 @@ local function monitors_new(query: Query<...any>): Monitor
local world = (cachedquery :: Query<...any> & { world: World }).world :: jecs.World
local archetypes = cachedquery.archetypes_map
local terms = cachedquery.filter_with :: { jecs.Id<any, any> }
local terms = cachedquery.filter_with :: { jecs.Id<any> }
local entity_index = world.entity_index :: any
local terms_lookup: { [jecs.Id<any>]: boolean } = {}
for _, term in terms do
terms_lookup[term] = true
end
local callback_added: ((jecs.Entity) -> ())?
local callback_removed: ((jecs.Entity) -> ())?
-- NOTE(marcus): Track the last old archetype we processed to detect bulk operations.
-- We can detect this pattern by checking if we've seen this old archetype
-- before with a component in the terms list.
local last_old_archetype: jecs.Archetype? = nil
local function emplaced<a>(
entity: jecs.Entity,
id: jecs.Id<a>,
value: a,
oldarchetype: jecs.Archetype
)
if callback_added == nil then
value: a,
oldarchetype: jecs.Archetype
)
if callback_added == nil then
return
end
-- NOTE(marcus): Skip if we've seen this old archetype before AND
-- this component is in the query's terms. The component-in-terms
-- check ensures we don't skip legitimate separate operations.
if last_old_archetype == oldarchetype and terms_lookup[id] then
return
end
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any) :: jecs.Record
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
if archetypes[r.archetype.id] then
last_old_archetype = oldarchetype
callback_added(entity)
else
-- NOTE(marcus): Clear tracking when we see a different transition pattern
last_old_archetype = nil
end
end
@ -168,6 +189,7 @@ local function monitors_new(query: Query<...any>): Monitor
local dst = jecs.archetype_traverse_remove(world, component, src)
if not archetypes[dst.id] then
last_old_archetype = nil
callback_removed(entity)
end
end
@ -180,10 +202,13 @@ local function monitors_new(query: Query<...any>): Monitor
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onadded = world:added(rel, function(entity, id, _, oldarchetype)
local onadded = world:added(rel, function(entity, id, _, oldarchetype: jecs.Archetype)
if callback_added == nil then
return
end
if last_old_archetype == oldarchetype and terms_lookup[id] then
return
end
if not wc and id ~= term then
return
@ -192,10 +217,8 @@ local function monitors_new(query: Query<...any>): Monitor
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any) :: jecs.Record
if archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
print("???")
end
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
last_old_archetype = oldarchetype
callback_added(entity)
end
end)
@ -209,6 +232,7 @@ local function monitors_new(query: Query<...any>): Monitor
local r = jecs.record(world, entity)
if archetypes[r.archetype.id] then
last_old_archetype = nil
callback_removed(entity)
end
end)
@ -230,7 +254,7 @@ local function monitors_new(query: Query<...any>): Monitor
local rel = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onadded = world:added(rel, function(entity, id, _, oldarchetype)
local onadded = world:added(rel, function(entity, id, _, oldarchetype: jecs.Archetype)
if callback_removed == nil then
return
end
@ -249,6 +273,7 @@ local function monitors_new(query: Query<...any>): Monitor
-- removed a component even though the entity is not
-- apart of the monitor
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
last_old_archetype = nil
callback_removed(entity)
end
end)
@ -262,15 +287,20 @@ local function monitors_new(query: Query<...any>): Monitor
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if not archetype then
return
end
if last_old_archetype == archetype and terms_lookup[id] then
return
end
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
last_old_archetype = archetype
callback_added(entity)
end
end)

View file

@ -72,7 +72,7 @@ type function ecs_id_t(first: type, second: type)
return p
end
export type Entity<T = nil> = { __T: T }
export type Entity<T = any> = { __T: T }
export type Id<T = any> = { __T: T }
export type Pair<First=any, Second=any> = ecs_pair_t<Entity<First>, Entity<Second>>
export type Component<T=any> = { __T: T }

View file

@ -222,10 +222,28 @@ TEST("addons/ob::observer", function()
end
end)
FOCUS()
TEST("addons/ob::monitor", function()
local world = jecs.world()
do CASE [[should not invoke monitor.added callback multiple times in a bulk_move
]]
local A = world:component()
local B = world:component()
local C = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local c = 0
monitor.added(function()
c += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
CHECK(c==1)
print(c)
end
do CASE [[should not invoke monitor.added callback unless it wasn't apart
of the monitor]]
local A = world:component()
@ -490,6 +508,261 @@ TEST("addons/ob::monitor", function()
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "should not invoke monitor.added callback multiple times in bulk_insert with pairs"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
local c = 0
monitor.added(function()
c += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { jecs.pair(A, e1), jecs.pair(A, e2) }, { true, true })
CHECK(c == 1)
end
do CASE "should not invoke monitor.added callback multiple times in bulk_insert with mixed components and pairs"
local A = world:component()
local B = world:component()
local C = world:component()
local e1 = world:entity()
local monitor = ob.monitor(world:query(A, B, jecs.pair(C, e1)))
local c = 0
monitor.added(function()
c += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { A, B, jecs.pair(C, e1) }, { 1, 2, true })
CHECK(c == 1)
end
do CASE "monitor with wildcard pair should handle bulk_insert"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
local monitor = ob.monitor(world:query(A, jecs.pair(B, jecs.w)))
local c = 0
monitor.added(function()
c += 1
end)
local e = world:entity()
world:add(e, A)
CHECK(c == 0)
world:add(e, jecs.pair(B, e1))
CHECK(c == 1)
world:add(e, jecs.pair(B, e2))
CHECK(c == 1)
world:add(e, jecs.pair(B, e3))
CHECK(c == 1)
end
do CASE "monitor with multiple pairs should handle separate operations correctly"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
local c = 0
monitor.added(function()
c += 1
end)
local r = 0
monitor.removed(function()
r += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
CHECK(c == 0)
world:add(e, jecs.pair(A, e2))
CHECK(c == 1)
world:remove(e, jecs.pair(A, e1))
CHECK(r == 1)
world:remove(e, jecs.pair(A, e2))
CHECK(r == 1)
world:add(e, jecs.pair(A, e1))
CHECK(c == 1)
world:add(e, jecs.pair(A, e2))
CHECK(c == 2)
end
if true then return end
do CASE "monitor with pair should handle remove and re-add correctly"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
local c = 0
monitor.added(function()
c += 1
end)
local r = 0
monitor.removed(function()
r += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
CHECK(c == 1)
world:remove(e, jecs.pair(A, e1))
CHECK(r == 1)
world:add(e, jecs.pair(A, e1))
CHECK(c == 2)
world:remove(e, jecs.pair(A, e1))
CHECK(r == 2)
end
do CASE "monitor with pair and without clause should handle bulk operations"
local A = world:component()
local B = world:component()
local C = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1)))
local c = 0
monitor.added(function()
c += 1
end)
local r = 0
monitor.removed(function()
r += 1
end)
local e = world:entity()
world:add(e, A)
CHECK(c == 1)
world:add(e, jecs.pair(B, e2))
CHECK(c == 1)
world:add(e, jecs.pair(B, e1))
CHECK(c == 1)
CHECK(r == 1)
world:remove(e, jecs.pair(B, e1))
CHECK(c == 2)
CHECK(r == 1)
end
do CASE "monitor with wildcard pair in without should handle operations correctly"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, jecs.w)))
local c = 0
monitor.added(function()
c += 1
end)
local r = 0
monitor.removed(function()
r += 1
end)
local e = world:entity()
world:add(e, A)
CHECK(c == 1)
world:add(e, jecs.pair(B, e1))
CHECK(c == 1)
CHECK(r == 1)
world:add(e, jecs.pair(B, e2))
CHECK(c == 1)
CHECK(r == 1)
world:remove(e, jecs.pair(B, e1))
CHECK(c == 1)
CHECK(r == 1)
world:remove(e, jecs.pair(B, e2))
CHECK(c == 2)
CHECK(r == 1)
end
do CASE "monitor with multiple pairs should not skip legitimate transitions"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
local c = 0
monitor.added(function()
c += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
CHECK(c == 1)
world:remove(e, jecs.pair(A, e1))
world:add(e, jecs.pair(A, e2))
CHECK(c == 2)
world:remove(e, jecs.pair(A, e2))
world:add(e, jecs.pair(A, e1))
CHECK(c == 3)
world:add(e, jecs.pair(A, e2))
CHECK(c == 4)
end
do CASE "monitor with pair should handle set operations correctly"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
local c = 0
monitor.added(function()
c += 1
end)
local r = 0
monitor.removed(function()
r += 1
end)
local e = world:entity()
world:set(e, jecs.pair(A, e1), true)
CHECK(c == 1)
world:set(e, jecs.pair(A, e1), false)
CHECK(c == 1)
world:set(e, jecs.pair(A, e1), true)
CHECK(c == 2)
world:remove(e, jecs.pair(A, e1))
CHECK(r == 1)
world:set(e, jecs.pair(A, e1), true)
CHECK(c == 3)
end
do CASE "monitor with pair query should handle non-matching pairs"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
local c = 0
monitor.added(function()
c += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
CHECK(c == 1)
world:add(e, jecs.pair(A, e2))
CHECK(c == 1)
world:add(e, jecs.pair(B, e1))
CHECK(c == 1)
end
end)
return FINISH()