From b1626a9be01fa7291d71def051216db0a0bfded2 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 25 Apr 2024 07:26:38 +0200 Subject: [PATCH] Add mirror --- benches/query.bench.lua | 54 +++-- mirror/init.lua | 512 ++++++++++++++++++++++++++++++++++++++++ test.project.json | 3 + 3 files changed, 547 insertions(+), 22 deletions(-) create mode 100644 mirror/init.lua diff --git a/benches/query.bench.lua b/benches/query.bench.lua index 842c456..e7f104d 100644 --- a/benches/query.bench.lua +++ b/benches/query.bench.lua @@ -11,6 +11,8 @@ local world = Rewrite.World.new() local component = Rewrite.component local jecs = require(ReplicatedStorage.Lib) +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() local ecs = jecs.World.new() local A1 = Matter.component() @@ -50,10 +52,20 @@ local D7 = ecs:entity() local D8 = ecs:entity() +local E1 = mcs:entity() +local E2 = mcs:entity() +local E3 = mcs:entity() +local E4 = mcs:entity() +local E5 = mcs:entity() +local E6 = mcs:entity() +local E7 = mcs:entity() +local E8 = mcs:entity() + + local registry2 = ecr.registry() local function flip() - return math.random() >= 0.15 + return math.random() >= 0.5 end local common = 0 @@ -65,12 +77,14 @@ for i = 1, N do local n = newWorld:spawn() local entity = ecs:entity() local e = world:spawn() + local m = mcs:entity() if flip() then combination ..= "B" registry2:set(id, B2, {value = true}) world:insert(e, C2({ value = true})) ecs:set(entity, D2, { value = true}) + mcs:set(m, E2, { value = 2}) newWorld:insert(n, A2({value = true})) end if flip() then @@ -78,6 +92,7 @@ for i = 1, N do registry2:set(id, B3, {value = true}) world:insert(e, C3({ value = true})) ecs:set(entity, D3, { value = true}) + mcs:set(m, E3, { value = 2}) newWorld:insert(n, A3({value = true})) end if flip() then @@ -85,6 +100,8 @@ for i = 1, N do registry2:set(id, B4, {value = true}) world:insert(e, C4({ value = true})) ecs:set(entity, D4, { value = true}) + mcs:set(m, E4, { value = 2}) + newWorld:insert(n, A4({value = true})) end if flip() then @@ -92,6 +109,8 @@ for i = 1, N do registry2:set(id, B5, {value = true}) world:insert(e, C5({value = true})) ecs:set(entity, D5, { value = true}) + mcs:set(m, E5, { value = 2}) + newWorld:insert(n, A5({value = true})) end if flip() then @@ -99,6 +118,8 @@ for i = 1, N do registry2:set(id, B6, {value = true}) world:insert(e, C6({value = true})) ecs:set(entity, D6, { value = true}) + mcs:set(m, E6, { value = 2}) + newWorld:insert(n, A6({value = true})) end if flip() then @@ -106,6 +127,8 @@ for i = 1, N do registry2:set(id, B7, {value = true}) world:insert(e, C7{ value = true}) ecs:set(entity, D7, { value = true}) + mcs:set(m, E7, { value = 2}) + newWorld:insert(n, A7({value = true})) end @@ -115,7 +138,8 @@ for i = 1, N do world:insert(e, C8{ value = true}) newWorld:insert(n, A8({value = true})) ecs:set(entity, D8, { value = true}) - + mcs:set(m, E8, { value = 2}) + end if #combination == 7 then @@ -125,6 +149,8 @@ for i = 1, N do world:insert(e, C1{ value = true}) ecs:set(entity, D1, { value = true}) newWorld:insert(n, A1({value = true})) + mcs:set(m, E1, { value = 2}) + end archetypes[combination] = true @@ -156,34 +182,18 @@ print( return { ParameterGenerator = function() return - end, + end, Functions = { - - Matter = function() + Mirror = function() local matched = 0 - for entityId, firstComponent in newWorld:query(A5, A6, A3, A4, A8, A7) do + for entityId, firstComponent in mcs:query(E1, E2, E3, E4) do matched += 1 end end, - - ECR = function() - local matched = 0 - for entityId, firstComponent in registry2:view(B5, B6, B3, B4, B8, B7) do - matched += 1 - end - end, - - Rewrite = function() - local matched = 0 - for entityId, firstComponent in world:query(C5, C6, C3, C4, C8, C7) do - matched += 1 - end - end, - Jecs = function() local matched = 0 - for entityId, firstComponent in ecs:query(D5, D6, D3, D4, D8, D7) do + for entityId, firstComponent in ecs:query(D1, D2, D3, D4) do matched += 1 end end diff --git a/mirror/init.lua b/mirror/init.lua new file mode 100644 index 0000000..47dac46 --- /dev/null +++ b/mirror/init.lua @@ -0,0 +1,512 @@ +--!optimize 2 +--!native +--!strict +--draft 4 + +type i53 = number +type i24 = number + +type Ty = { i53 } +type ArchetypeId = number + +type Column = { any } + +type Archetype = { + id: number, + edges: { + [i24]: { + add: Archetype, + remove: Archetype, + }, + }, + types: Ty, + type: string | number, + entities: { number }, + columns: { Column }, + records: {}, +} + +type Record = { + archetype: Archetype, + row: number, +} + +type EntityIndex = { [i24]: Record } +type ComponentIndex = { [i24]: ArchetypeMap} + +type ArchetypeRecord = number +type ArchetypeMap = { map: { [ArchetypeId]: ArchetypeRecord } , size: number } +type Archetypes = { [ArchetypeId]: Archetype } + +local function transitionArchetype( + entityIndex: EntityIndex, + destinationArchetype: Archetype, + destinationRow: i24, + sourceArchetype: Archetype, + sourceRow: i24 +) + local columns = sourceArchetype.columns + local sourceEntities = sourceArchetype.entities + local destinationEntities = destinationArchetype.entities + local destinationColumns = destinationArchetype.columns + + for componentId, column in columns do + local targetColumn = destinationColumns[componentId] + if targetColumn then + targetColumn[destinationRow] = column[sourceRow] + end + column[sourceRow] = column[#column] + column[#column] = nil + end + + destinationEntities[destinationRow] = sourceEntities[sourceRow] + local moveAway = #sourceEntities + sourceEntities[sourceRow] = sourceEntities[moveAway] + sourceEntities[moveAway] = nil + entityIndex[destinationEntities[destinationRow]].row = sourceRow +end + +local function archetypeAppend(entity: i53, archetype: Archetype): i24 + local entities = archetype.entities + table.insert(entities, entity) + return #entities +end + +local function newEntity(entityId: i53, record: Record, archetype: Archetype) + local row = archetypeAppend(entityId, archetype) + record.archetype = archetype + record.row = row + return record +end + +local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) + local sourceRow = record.row + local from = record.archetype + local destinationRow = archetypeAppend(entityId, to) + transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) + record.archetype = to + record.row = destinationRow +end + +local function hash(arr): string | number + if true then + return table.concat(arr, "_") + end + local hashed = 5381 + for i = 1, #arr do + hashed = ((bit32.lshift(hashed, 5)) + hashed) + arr[i] + end + return hashed +end + +local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?) + local destinationCount = #to.types + local destinationIds = to.types + + for i = 1, destinationCount do + local destinationId = destinationIds[i] + + if not componentIndex[destinationId] then + componentIndex[destinationId] = { size = 0, map = {} } + end + + local archetypesMap = componentIndex[destinationId] + archetypesMap.map[to.id] = i + to.records[destinationId] = i + end +end + +local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype + local ty = hash(types) + + world.nextArchetypeId = (world.nextArchetypeId::number)+ 1 + local id = world.nextArchetypeId + + local columns = {} :: { any } + + for _ in types do + table.insert(columns, {}) + end + + local archetype = { + id = id, + types = types, + type = ty, + columns = columns, + entities = {}, + edges = {}, + records = {}, + } + world.archetypeIndex[ty] = archetype + world.archetypes[id] = archetype + createArchetypeRecords(world.componentIndex, archetype, prev) + + return archetype +end + +local World = {} +World.__index = World +function World.new() + local self = setmetatable({ + entityIndex = {}, + componentIndex = {}, + archetypes = {}, + archetypeIndex = {}, + ROOT_ARCHETYPE = nil :: Archetype?, + nextId = 0, + nextArchetypeId = 0 + }, World) + self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) + return self +end + +type World = typeof(World.new()) + +local function ensureArchetype(world: World, types, prev) + if #types < 1 then + + if not world.ROOT_ARCHETYPE then + local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE + return ROOT_ARCHETYPE + end + end + local ty = hash(types) + local archetype = world.archetypeIndex[ty] + if archetype then + return archetype + end + + return archetypeOf(world, types, prev) +end + +local function findInsert(types: { i53 }, toAdd: i53) + local count = #types + for i = 1, count do + local id = types[i] + if id == toAdd then + return -1 + end + if id > toAdd then + return i + end + end + return count + 1 +end + +local function findArchetypeWith(world: World, node: Archetype, componentId: i53) + local types = node.types + local at = findInsert(types, componentId) + if at == -1 then + return node + end + + local destinationType = table.clone(node.types) + table.insert(destinationType, at, componentId) + return ensureArchetype(world, destinationType, node) +end + +local function ensureEdge(archetype: Archetype, componentId: i53) + if not archetype.edges[componentId] then + archetype.edges[componentId] = {} :: any + end + return archetype.edges[componentId] +end + +local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype + local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype + local edge = ensureEdge(from, componentId) + + if not edge.add then + edge.add = findArchetypeWith(world, from, componentId) + end + + return edge.add +end + +function World.ensureRecord(world: World, entityId: i53) + local entityIndex = world.entityIndex + local id = entityId + if not entityIndex[id] then + entityIndex[id] = {} :: Record + end + return entityIndex[id] +end + +function World.set(world: World, entityId: i53, componentId: i53, data: unknown) + local record = world:ensureRecord(entityId) + local sourceArchetype = record.archetype + local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) + + if sourceArchetype and not (sourceArchetype == destinationArchetype) then + moveEntity(world.entityIndex, entityId, record, destinationArchetype) + else + -- if it has any components, then it wont be the root archetype + if #destinationArchetype.types > 0 then + newEntity(entityId, record, destinationArchetype) + end + end + local archetypeRecord = destinationArchetype.records[componentId] + destinationArchetype.columns[archetypeRecord][record.row] = data +end + +local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype + local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype + local edge = ensureEdge(from, componentId) + + + if not edge.remove then + local to = table.clone(from.types) + table.remove(to, table.find(to, componentId)) + edge.remove = ensureArchetype(world, to, from) + end + + return edge.remove +end + +function World.remove(world: World, entityId: i53, componentId: i53) + local record = world:ensureRecord(entityId) + local sourceArchetype = record.archetype + local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) + + if sourceArchetype and not (sourceArchetype == destinationArchetype) then + moveEntity(world.entityIndex, entityId, record, destinationArchetype) + end +end + +local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) + local archetype = record.archetype + local archetypeRecord = componentIndex[componentId][archetype.id] + + if not archetypeRecord then + return nil + end + + return archetype.columns[archetypeRecord][record.row] +end + +function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) + local id = entityId + local componentIndex = world.componentIndex + local record = world.entityIndex[id] + if not record then + return nil + end + + local va = get(componentIndex, record, a) + + if b == nil then + return va + elseif c == nil then + return va, get(componentIndex, record, b) + elseif d == nil then + return va, get(componentIndex, record, b), get(componentIndex, record, c) + elseif e == nil then + return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d) + else + error("args exceeded") + end +end + +function World.entity(world: World) + world.nextId += 1 + return world.nextId +end + +local function noop(): any + return function() + end +end + +local function getSmallestMap(componentIndex, components) + local s: any + + for i, componentId in components do + local map = componentIndex[componentId] + if s == nil or map.size < s.size then + s = map + end + end + + return s.map +end + +function World.query(world: World, ...: i53): (() -> (number, ...any)) | () -> () + local compatibleArchetypes = {} + local components = { ... } + local archetypes = world.archetypes + local queryLength = #components + local firstArchetypeMap = getSmallestMap(world.componentIndex, components) + + local a, b, c, d, e, f, g, h = ... + + if not firstArchetypeMap then + return noop() + end + + for id in firstArchetypeMap do + local archetype = archetypes[id] + local columns = archetype.columns + local archetypeRecords = archetype.records + local indices = {} + if queryLength == 1 then + indices = { + columns[archetypeRecords[a]], + } + elseif queryLength == 2 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]] + } + elseif queryLength == 3 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]], + columns[archetypeRecords[c]] + } + elseif queryLength == 4 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]], + columns[archetypeRecords[c]], + columns[archetypeRecords[d]] + } + elseif queryLength == 5 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]], + columns[archetypeRecords[c]], + columns[archetypeRecords[d]], + columns[archetypeRecords[e]] + } + elseif queryLength == 6 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]], + columns[archetypeRecords[c]], + columns[archetypeRecords[d]], + columns[archetypeRecords[e]], + columns[archetypeRecords[f]], + } + elseif queryLength == 7 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]], + columns[archetypeRecords[c]], + columns[archetypeRecords[d]], + columns[archetypeRecords[e]], + columns[archetypeRecords[f]], + columns[archetypeRecords[g]], + } + elseif queryLength == 8 then + indices = { + columns[archetypeRecords[a]], + columns[archetypeRecords[b]], + columns[archetypeRecords[c]], + columns[archetypeRecords[d]], + columns[archetypeRecords[e]], + columns[archetypeRecords[f]], + columns[archetypeRecords[g]], + columns[archetypeRecords[h]], + } + else + for i, componentId in components do + indices[i] = columns[archetypeRecords[componentId]] + end + end + + local i = 0 + for _ in indices do + i += 1 + end + if queryLength == i then + table.insert(compatibleArchetypes, { + archetype = archetype, + indices = indices + }) + end + end + + local lastArchetype, compatibleArchetype = next(compatibleArchetypes) + + local lastRow + + return function() + local archetype = compatibleArchetype.archetype + local indices = compatibleArchetype.indices + local row = next(archetype.entities, lastRow) + while row == nil do + lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) + if lastArchetype == nil then + return + end + archetype = compatibleArchetype.archetype + row = next(archetype.entities, row) + end + lastRow = row + + local entityId = archetype.entities[row :: number] + + if queryLength == 1 then + return entityId, indices[1][row] + elseif queryLength == 2 then + return entityId, indices[1][row], indices[2][row] + elseif queryLength == 3 then + return entityId, + indices[1][row], + indices[2][row], + indices[3][row] + elseif queryLength == 4 then + return entityId, + indices[1][row], + indices[2][row], + indices[3][row], + indices[4][row] + elseif queryLength == 5 then + return entityId, + indices[1][row], + indices[2][row], + indices[3][row], + indices[4][row] + elseif queryLength == 6 then + return entityId, + indices[1][row], + indices[2][row], + indices[3][row], + indices[4][row], + indices[5][row], + indices[6][row] + elseif queryLength == 7 then + return entityId, + indices[1][row], + indices[2][row], + indices[3][row], + indices[4][row], + indices[5][row], + indices[6][row], + indices[7][row] + + elseif queryLength == 8 then + return entityId, + indices[1][row], + indices[2][row], + indices[3][row], + indices[4][row], + indices[5][row], + indices[6][row], + indices[7][row], + indices[8][row] + end + + local queryOutput = {} + for i, componentId in components do + queryOutput[i] = indices[i][row] + end + + return entityId, unpack(queryOutput, 1, queryLength) + end +end + +return { + World = World +} \ No newline at end of file diff --git a/test.project.json b/test.project.json index 18e35f5..28d10df 100644 --- a/test.project.json +++ b/test.project.json @@ -25,6 +25,9 @@ }, "rewrite": { "$path": "matterRewrite" + }, + "mirror": { + "$path": "mirror" } }, "TestService": {