diff --git a/benches/visual/query.bench.luau b/benches/visual/query.bench.luau index 90426f1..ddaaa8c 100644 --- a/benches/visual/query.bench.luau +++ b/benches/visual/query.bench.luau @@ -57,7 +57,7 @@ local function flip() end local common = 0 -local N = 2^16-2 +local N = 5000 local archetypes = {} local hm = 0 @@ -170,13 +170,8 @@ return { end, Functions = { - Matter = function() - for entityId, firstComponent in newWorld:query(A1, A2, A3, A4) do - end - end, - - ECR = function() - for entityId, firstComponent in registry2:view(B1, B2, B3, B3) do + Mirror = function() + for entityId, firstComponent in mcs:query(E1, E2, E3, E4) do end end, diff --git a/src/init.luau b/src/init.luau index 74931ca..9365224 100644 --- a/src/init.luau +++ b/src/init.luau @@ -942,73 +942,70 @@ local EmptyQuery = { setmetatable(EmptyQuery, EmptyQuery) -local function columns_replace_values(row, columns, ...) - for i, column in columns do - column[row] = select(i, ...) - end -end - -local function world_query(world: World, ...) - local compatible_archetypes = {} - local length = 0 - - local ids = { ... } - local A, B, C, D, E, F, G, H, I = ... - local a, b, c, d, e, f, g, h - - local archetypes = world.archetypes - - local idr: ArchetypeMap - local componentIndex = world.componentIndex - - for _, id in ids do - local map = componentIndex[id] - if not map then - return EmptyQuery - end - - if idr == nil or map.size < idr.size then - idr = map - end - end - - for archetype_id in idr.cache do - local compatibleArchetype = archetypes[archetype_id] - if #compatibleArchetype.entities == 0 then - continue - end - local records = compatibleArchetype.records - - local skip = false - - for i, id in ids do - local tr = records[id] - if not tr then - skip = true - break - end - end - - if skip then - continue - end - - length += 1 - compatible_archetypes[length] = compatibleArchetype - end - - if length == 0 then +local function query_init(query) + local world_query_iter_next = query.iter_next + if world_query_iter_next then + return world_query_iter_next + end + local compatible_archetypes = query.compatible_archetypes + local ids = query.ids + local A, B, C, D, E, F, G, H, I = unpack(ids) + local a, b, c, d, e, f, g, h + local lastArchetype = 1 + local archetype = compatible_archetypes[1] + if not archetype then return EmptyQuery end + local columns = archetype.columns + local entities = archetype.entities + local i = #entities - local lastArchetype = 1 - local archetype - local columns - local entities - local i - local queryOutput - - local world_query_iter_next + local records = archetype.records + if not B then + a = columns[records[A].column] + elseif not C then + a = columns[records[A].column] + b = columns[records[B].column] + elseif not D then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + elseif not E then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + elseif not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end if not B then function world_query_iter_next(): any @@ -1121,6 +1118,7 @@ local function world_query(world: World, ...) return entityId, a[row], b[row], c[row], d[row] end else + local queryOutput = {} function world_query_iter_next(): any local entityId = entities[i] while entityId == nil do @@ -1194,221 +1192,217 @@ local function world_query(world: World, ...) end end - local init = false - local drain = false + query.iter_next = world_query_iter_next + return world_query_iter_next +end - local function query_init(query) - if init and drain then - return true - end +local function query_iter(query) + return query_init(query) +end - init = true - lastArchetype = 1 - archetype = compatible_archetypes[lastArchetype] +local function query_drain(query) + local query_iter_next = query_init(query) + query.next = query_iter_next + return query +end - if not archetype then - return false - end - - queryOutput = {} - - entities = archetype.entities - i = #entities - columns = archetype.columns +local function query_next(query) + error("Did you forget to call drain?") +end +local function query_without(query, ...) + local compatible_archetypes = query.compatible_archetypes + local length: number = query.length + local N = select("#", ...) + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] local records = archetype.records - if not B then - a = columns[records[A].column] - elseif not C then - a = columns[records[A].column] - b = columns[records[B].column] - elseif not D then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - elseif not E then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - elseif not F then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - elseif not G then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - elseif not H then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - elseif not I then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - h = columns[records[H].column] - end - return true - end + local shouldRemove = false - local function world_query_without(query, ...) - local N = select("#", ...) - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local shouldRemove = false - - for j = 1, N do - local id = select(j, ...) - if records[id] then - shouldRemove = true - break - end - end - - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil - length -= 1 + for j = 1, N do + local id = select(j, ...) + if records[id] then + shouldRemove = true + break end end - if length == 0 then - return EmptyQuery - end - - return query - end - - local function world_query_replace(query, fn: (...any) -> ...any) - query_init(query) - - for i, archetype in compatible_archetypes do - local columns = archetype.columns - local records = archetype.records - for row in archetype.entities do - if not B then - local va = columns[records[A].column] - local pa = fn(va[row]) - - va[row] = pa - elseif not C then - local va = columns[records[A].column] - local vb = columns[records[B].column] - - va[row], vb[row] = fn(va[row], vb[row]) - elseif not D then - local va = columns[records[A].column] - local vb = columns[records[B].column] - local vc = columns[records[C].column] - - va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row]) - elseif not E then - local va = columns[records[A].column] - local vb = columns[records[B].column] - local vc = columns[records[C].column] - local vd = columns[records[D].column] - - va[row], vb[row], vc[row], vd[row] = fn(va[row], vb[row], vc[row], vd[row]) - else - for j, id in ids do - local tr = records[id] - queryOutput[j] = columns[tr.column][row] - end - columns_replace_values(row, columns, fn(unpack(queryOutput))) - end + if shouldRemove then + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] end + compatible_archetypes[last] = nil + length -= 1 end end - local function world_query_with(query, ...) - local N = select("#", ...) - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local shouldRemove = false + query.length = length - for j = 1, N do - local id = select(j, ...) - if not records[id] then - shouldRemove = true - break - end - end - - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil - length -= 1 - end - end - if length == 0 then - return EmptyQuery - end - return query - end - - -- Meant for directly iterating over archetypes to minimize - -- function call overhead. Should not be used unless iterating over - -- hundreds of thousands of entities in bulk. - local function world_query_archetypes() - return compatible_archetypes - end - - local function world_query_drain(query) - drain = true - if query_init(query) then - return query - end + if length == 0 then return EmptyQuery end - local function world_query_iter(query) - query_init(query) - return world_query_iter_next - end + return query +end - local function world_query_next(world) - if not drain then - error("Did you forget to call query:drain()?") +local function query_with(query, ...) + local compatible_archetypes = query.compatible_archetypes + local length: number = query.length + local N = select("#", ...) + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local shouldRemove = false + + for j = 1, N do + local id = select(j, ...) + if not records[id] then + shouldRemove = true + break + end + end + + if shouldRemove then + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil + length -= 1 + end + end + query.length = length + if length == 0 then + return EmptyQuery + end + return query +end + +local function columns_replace_values(row, columns, ...) + for i, column in columns do + column[row] = select(i, ...) + end +end + +local function query_replace(query, fn: (...any) -> ...any) + local compatible_archetypes = query.compatible_archetypes + local ids = query.ids + local A, B, C, D, E = unpack(ids, 1, 5) + local queryOutput = {} + for i, archetype in compatible_archetypes do + local columns = archetype.columns + local records = archetype.records + for row in archetype.entities do + if not B then + local va = columns[records[A].column] + local pa = fn(va[row]) + + va[row] = pa + elseif not C then + local va = columns[records[A].column] + local vb = columns[records[B].column] + + va[row], vb[row] = fn(va[row], vb[row]) + elseif not D then + local va = columns[records[A].column] + local vb = columns[records[B].column] + local vc = columns[records[C].column] + + va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row]) + elseif not E then + local va = columns[records[A].column] + local vb = columns[records[B].column] + local vc = columns[records[C].column] + local vd = columns[records[D].column] + + va[row], vb[row], vc[row], vd[row] = fn(va[row], vb[row], vc[row], vd[row]) + else + for j, id in ids do + local tr = records[id] + queryOutput[j] = columns[tr.column][row] + end + columns_replace_values(row, columns, fn(unpack(queryOutput))) + end + end + end +end + +-- Meant for directly iterating over archetypes to minimize +-- function call overhead. Should not be used unless iterating over +-- hundreds of thousands of entities in bulk. +local function query_archetypes(query) + return query.compatible_archetypes +end + +local Query = {} +Query.__index = Query +Query.__iter = query_iter +Query.without = query_without +Query.with = query_with +Query.archetypes = query_archetypes +Query.drain = query_drain +Query.next = query_next +Query.replace = query_replace + +local function world_query(world: World, ...) + local compatible_archetypes = {} + local length = 0 + + local ids = { ... } + + local archetypes = world.archetypes + + local idr: ArchetypeMap + local componentIndex = world.componentIndex + + for _, id in ids do + local map = componentIndex[id] + if not map then + return EmptyQuery + end + + if idr == nil or map.size < idr.size then + idr = map end - return world_query_iter_next(world) end - local it = { - __iter = world_query_iter, - iter = world_query_iter, - drain = world_query_drain, - next = world_query_next, - with = world_query_with, - without = world_query_without, - replace = world_query_replace, - archetypes = world_query_archetypes, - } :: any + for archetype_id in idr.cache do + local compatibleArchetype = archetypes[archetype_id] + if #compatibleArchetype.entities == 0 then + continue + end + local records = compatibleArchetype.records - setmetatable(it, it) + local skip = false - return it + for i, id in ids do + local tr = records[id] + if not tr then + skip = true + break + end + end + + if skip then + continue + end + + length += 1 + compatible_archetypes[length] = compatibleArchetype + end + + if length == 0 then + return EmptyQuery + end + + local q = setmetatable({ + compatible_archetypes = compatible_archetypes, + length = length, + ids = ids, + }, Query) + + return q end local World = {} @@ -1519,6 +1513,7 @@ function World.new() end world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) + world_add(self, EcsWildcard, EcsComponent) return self end @@ -1655,7 +1650,7 @@ return { Name = EcsName :: Entity, Rest = EcsRest :: Entity, - pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, + pair = ECS_PAIR, -- Inwards facing API for testing ECS_ID = ECS_ENTITY_T_LO,