From 10f1acc83ae3b78851269f2f7e2b5dd0df1e9f32 Mon Sep 17 00:00:00 2001 From: RowDaBoat Date: Mon, 7 Sep 2026 19:03:28 -0300 Subject: [PATCH 1/6] chore: start measured optimization work From 56f479a0f81b7611eb8832ca450805bdd75ed5d3 Mon Sep 17 00:00:00 2001 From: RowDaBoat Date: Mon, 7 Sep 2026 19:41:39 -0300 Subject: [PATCH 2/6] Write Meta ids directly during entity creation --- src/world.nim | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/world.nim b/src/world.nim index e2df96f..a9195b1 100644 --- a/src/world.nim +++ b/src/world.nim @@ -445,6 +445,12 @@ proc consolidateRemoveComponents(world: var World, id: EntityId, compIdsToRemove world.entities[id.value] = entity +proc setMetaId(archetype: Archetype, archetypeEntityId: int, id: EntityId) {.inline.} = + let metaIndex = archetype.getIndex(Meta.toComponentId) + let metaComponents = cast[EcsSeq[Meta]](archetype.componentLists[metaIndex]) + metaComponents[archetypeEntityId].id = id + + iterator archetypes*(world: var World): Archetype = ## Iterate through all the world's archetypes. ## @@ -787,16 +793,14 @@ proc add*[T: tuple](world: var World, components: T, mode: OperationMode = Defer let entity = Entity(archetypeIndex: archetypeIndex, archetypeEntityId: archetypeEntityId) result = world.allocateEntity(entity) - for meta in world.write(result, Meta): - meta.id = result + world.archetypes[archetypeIndex].setMetaId(archetypeEntityId, result) else: let archetypeIndex = world.archetypeFrom (Meta,) let archetypeEntityId = world.archetypes[archetypeIndex].add (Meta(),) let entity = Entity(archetypeIndex: archetypeIndex, archetypeEntityId: archetypeEntityId) result = world.allocateEntity(entity) - for meta in world.write(result, Meta): - meta.id = result + world.archetypes[archetypeIndex].setMetaId(archetypeEntityId, result) world.add(result, components, mode) @@ -818,8 +822,7 @@ proc addEmpty*(world: var World): EntityId {.discardable.} = let entity = Entity(archetypeIndex: archetypeIndex, archetypeEntityId: archetypeEntityId) result = world.allocateEntity(entity) - for meta in world.write(result, Meta): - meta.id = result + world.archetypes[archetypeIndex].setMetaId(archetypeEntityId, result) proc addWithSpecificId*(world: var World, id: EntityId) = From dd526fdf34f0c3752514f8d9db259bee95a9bb5a Mon Sep 17 00:00:00 2001 From: RowDaBoat Date: Mon, 7 Sep 2026 19:59:58 -0300 Subject: [PATCH 3/6] Avoid removal ID allocation for cached archetypes --- .gitignore | 1 + src/world.nim | 17 ++++++-- tests/tremoval.nim | 100 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 tests/tremoval.nim diff --git a/.gitignore b/.gitignore index 301bcfc..71410bb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ tests/world tests/events tests/manycomponents tests/tbenchmarks +tests/tremoval benchs/vecs_bench benchs/vecs.csv diff --git a/src/world.nim b/src/world.nim index a9195b1..465a4ce 100644 --- a/src/world.nim +++ b/src/world.nim @@ -432,12 +432,21 @@ proc consolidateAddComponents(world: var World, id: EntityId, componentsToAdd: T proc consolidateRemoveComponents(world: var World, id: EntityId, compIdsToRemove: PackedSet[ComponentId]) = var entity = world.entities[id.value] var previousArchetype = world.archetypes[entity.archetypeIndex] - var componentIds: seq[ComponentId] + var nextArchetypeId = previousArchetype.id - for compId in compIdsToRemove.items: - componentIds.add compId + for componentId in compIdsToRemove.items: + nextArchetypeId.excl componentId + + var nextIndex = world.archIdToIndex.getOrDefault(nextArchetypeId, -1) + + if nextIndex < 0: + var componentIds: seq[ComponentId] + + for componentId in compIdsToRemove.items: + componentIds.add componentId + + nextIndex = world.nextArchetypeRemovingFrom(previousArchetype, componentIds) - let nextIndex = world.nextArchetypeRemovingFrom(previousArchetype, componentIds) var nextArchetype = world.archetypes[nextIndex] entity.archetypeIndex = nextIndex diff --git a/tests/tremoval.nim b/tests/tremoval.nim new file mode 100644 index 0000000..b7d6213 --- /dev/null +++ b/tests/tremoval.nim @@ -0,0 +1,100 @@ +# ISC License +# Copyright (c) 2025 RowDaBoat +# `vecs` is a free open source ECS library for Nim. +import unittest +import ../src/[examples, vecs] + + +type RemovalPayload = ref object + value: string + + +type RetainedComponent = object + payload: RemovalPayload + + +suite "Component removal should": + test "accept duplicate component types": + var world = World() + let entityId = world.add((Character(name: "Marcus"), Health(health: 100), Weapon(name: "Sword")), Immediate) + + world.remove(entityId, (Health, Weapon, Health), Immediate) + + check world.has(entityId) + check world.read(entityId, Meta).id == entityId + check world.read(entityId, Character).name == "Marcus" + check not world.has(entityId, Health) + check not world.has(entityId, Weapon) + + + test "retain reference values when removal creates and then reuses a destination": + var world = World() + let firstPayload = RemovalPayload(value: "First") + let secondPayload = RemovalPayload(value: "Second") + let firstId = world.add((RetainedComponent(payload: firstPayload), Health(health: 100), Character(name: "Marcus"), Weapon(name: "Sword")), Immediate) + let secondId = world.add((RetainedComponent(payload: secondPayload), Health(health: 80), Character(name: "Elena"), Weapon(name: "Staff")), Immediate) + + world.remove(firstId, (Weapon, Health), Immediate) + world.remove(secondId, (Health, Weapon), Immediate) + GC_fullCollect() + + var query: Query[(Meta, Character, RetainedComponent)] + var entityCount = 0 + + for (meta, character, retained) in world.query(query): + inc entityCount + check not world.has(meta.id, Health) + check not world.has(meta.id, Weapon) + + if meta.id == firstId: + check character.name == "Marcus" + check retained.payload == firstPayload + check retained.payload.value == "First" + else: + check meta.id == secondId + check character.name == "Elena" + check retained.payload == secondPayload + check retained.payload.value == "Second" + + check entityCount == 2 + + + test "retain reference values when destination columns have a different order": + var world = World() + let existingPayload = RemovalPayload(value: "Existing") + let movedPayload = RemovalPayload(value: "Moved") + let existingId = world.add((Character(name: "Elena"), RetainedComponent(payload: existingPayload)), Immediate) + let movedId = world.add((RetainedComponent(payload: movedPayload), Health(health: 100), Character(name: "Marcus"), Weapon(name: "Sword")), Immediate) + + world.remove(movedId, (Weapon, Health), Immediate) + GC_fullCollect() + + check world.read(existingId, Character).name == "Elena" + check world.read(existingId, RetainedComponent).payload == existingPayload + check world.read(existingId, RetainedComponent).payload.value == "Existing" + check world.read(movedId, Meta).id == movedId + check world.read(movedId, Character).name == "Marcus" + check world.read(movedId, RetainedComponent).payload == movedPayload + check world.read(movedId, RetainedComponent).payload.value == "Moved" + check not world.has(movedId, Health) + check not world.has(movedId, Weapon) + + + test "preserve a pending addition across immediate removal": + var world = World() + let entityId = world.add((Character(name: "Marcus"), Health(health: 100)), Immediate) + world.add(entityId, Weapon(name: "Sword", attack: 10)) + + world.remove(entityId, Health, Immediate) + + check not world.has(entityId, Health) + check not world.has(entityId, Weapon) + check world.read(entityId, Character).name == "Marcus" + + world.consolidate() + + check not world.has(entityId, Health) + check world.read(entityId, Meta).id == entityId + check world.read(entityId, Character).name == "Marcus" + check world.read(entityId, Weapon).name == "Sword" + check world.read(entityId, Weapon).attack == 10 From 36b54ae3d5034a59df6263abe5feb8841afb2153 Mon Sep 17 00:00:00 2001 From: RowDaBoat Date: Mon, 7 Sep 2026 20:22:13 -0300 Subject: [PATCH 4/6] Avoid registration checks during query setup --- src/world.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/world.nim b/src/world.nim index 465a4ce..7a7223e 100644 --- a/src/world.nim +++ b/src/world.nim @@ -353,7 +353,7 @@ macro buildComponentColumns(world: var World, t: typedesc, archetype: untyped): if isOp(fieldType, "Opt"): quote do: block: - let componentId = `world`.componentIdFrom typeof `componentType` + let componentId = `componentType`.toComponentId if `archetype`.contains(componentId): let ind = `archetype`.getIndex(componentId) @@ -364,7 +364,7 @@ macro buildComponentColumns(world: var World, t: typedesc, archetype: untyped): else: quote do: block: - let componentId = `world`.componentIdFrom typeof `componentType` + let componentId = `componentType`.toComponentId let ind = `archetype`.getIndex(componentId) let componentList = cast[EcsSeq[`componentType`]](`archetype`.componentLists[ind]) componentData(componentList) From b89adf0f27d08c99715dad293b196f565ea611ba Mon Sep 17 00:00:00 2001 From: RowDaBoat Date: Mon, 7 Sep 2026 20:26:52 -0300 Subject: [PATCH 5/6] Use raw data pointers for query columns --- src/world.nim | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/world.nim b/src/world.nim index 7a7223e..1cbfc44 100644 --- a/src/world.nim +++ b/src/world.nim @@ -3,7 +3,7 @@ # `vecs` is a free open source ECS library for Nim. import std/[packedsets, hashes, macros, intsets, options] import typetraits, tables, sets -import entityid, componentid, archetypeid, archetype, entity, ecsseq, queries, components, operations, operationmodes, events +import entityid, componentid, archetypeid, archetype, entity, ecsseq, unsafeseq, queries, components, operations, operationmodes, events export entityid, components.Meta, operationmodes export components export events @@ -287,10 +287,7 @@ template accessor[T](world: var World, archetype: Archetype, archetypeEntityId: proc componentData[T](componentList: EcsSeq[T]): ptr UncheckedArray[T] {.inline.} = - if componentList.len == 0: - return nil - - cast[ptr UncheckedArray[T]](unsafeAddr componentList[0]) + cast[ptr UncheckedArray[T]](componentList.rawPtr.rawSeqDataPtr) macro buildReadTuple(world: var World, t: typedesc, archetype: untyped, archetypeEntityId: untyped): untyped = From f6350c2d6953820eac2efaa203420a855d8140e6 Mon Sep 17 00:00:00 2001 From: RowDaBoat Date: Mon, 7 Sep 2026 20:31:15 -0300 Subject: [PATCH 6/6] Read query column indexes directly --- src/world.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/world.nim b/src/world.nim index 1cbfc44..53781a0 100644 --- a/src/world.nim +++ b/src/world.nim @@ -353,7 +353,7 @@ macro buildComponentColumns(world: var World, t: typedesc, archetype: untyped): let componentId = `componentType`.toComponentId if `archetype`.contains(componentId): - let ind = `archetype`.getIndex(componentId) + let ind = `archetype`.toIndexMap[componentId.int] - 1 let componentList = cast[EcsSeq[`componentType`]](`archetype`.componentLists[ind]) componentData(componentList) else: @@ -362,7 +362,7 @@ macro buildComponentColumns(world: var World, t: typedesc, archetype: untyped): quote do: block: let componentId = `componentType`.toComponentId - let ind = `archetype`.getIndex(componentId) + let ind = `archetype`.toIndexMap[componentId.int] - 1 let componentList = cast[EcsSeq[`componentType`]](`archetype`.componentLists[ind]) componentData(componentList)