From fa979155b2a7448ed0646659f70e5d388569051c Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 20:59:21 +0100 Subject: [PATCH 01/12] Add archetype tables, columns and the entity index An ArchetypeTable holds the entities of one component set with a hand-managed column per component type. ArchetypeStorage keeps the tables, the entity-to-(table,row) index, swap-remove row moves and cached add/remove edges between tables. Nothing selects it yet. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Sources/FirebladeECS/ArchetypeStorage.swift | 315 ++++++++++++++++++++ Sources/FirebladeECS/ArchetypeTable.swift | 263 ++++++++++++++++ 2 files changed, 578 insertions(+) create mode 100644 Sources/FirebladeECS/ArchetypeStorage.swift create mode 100644 Sources/FirebladeECS/ArchetypeTable.swift diff --git a/Sources/FirebladeECS/ArchetypeStorage.swift b/Sources/FirebladeECS/ArchetypeStorage.swift new file mode 100644 index 00000000..d5eac206 --- /dev/null +++ b/Sources/FirebladeECS/ArchetypeStorage.swift @@ -0,0 +1,315 @@ +// +// ArchetypeStorage.swift +// FirebladeECS +// +// Created by Christian Treffs on 25.10.26. +// + +extension Nexus { + /// Where an entity lives: its table and the row in it. + @usableFromInline + struct EntityLocation { + @usableFromInline var table: UInt32 + @usableFromInline var row: UInt32 + + @usableFromInline + init(table: UInt32, row: UInt32) { + self.table = table + self.row = row + } + } + + /// Archetype storage: entities with the same component set live in one table with a column per component type. + /// + /// This is the storage design of flecs ("Archetypes", ): one table per + /// component set, columns as arrays, an entity index that maps every entity to its table and row, and cached edges + /// between tables for adding and removing one component type. + /// + /// - Iterating a family visits the tables that match it and walks their columns linearly: no per-entity lookup. + /// The cost grows with the number of matching tables, not with the number of entities. + /// - Adding or removing a component moves the whole row of the entity into another table (swap-remove in the old + /// table keeps the rows dense and updates the entity index of the entity that moved), so a component set that + /// changes every frame is the weak spot. The target table is found through a cached edge. + /// - Random access (`get`) is the entity index, a column lookup and an array read. + /// - Tables are created when a component set is seen first and are never removed while the storage lives: a table + /// that became empty keeps its columns (their capacity is reused when entities of that set return) and stays in + /// the cached query lists, where it costs one skipped table per pass. ``removeAll()`` drops all tables. + /// + /// The storage traps on misuse like ``SparseSetStorage``: a structural change while a scoped access or iteration + /// runs, a second write access to a type that is being written, a read of a type that is being written, and two + /// component types that share an index. + /// + /// Soundness of `@exclusivity(unchecked)` and of the unretained table references: the stored properties change only in + /// structural changes and in the `beginWrite`/`endWrite`/`lockDepth` bookkeeping. Structural changes are blocked while + /// a scoped access runs (`requireUnlocked`), tables and columns are never removed while a scope is open, and a single + /// thread uses a nexus at a time, so no access overlaps a change. + @usableFromInline + final class ArchetypeStorage { + /// Liveness and component set of every entity slot. + @exclusivity(unchecked) + @usableFromInline var entities = EntityTable() + /// The table and row of every entity slot, indexed by `EntityIdentifier.index`. + @exclusivity(unchecked) + @usableFromInline var locations = ContiguousArray() + /// Table 0 is the table of entities without components. + @exclusivity(unchecked) + @usableFromInline var tables = ContiguousArray() + @usableFromInline var tableIndexByMask: [ComponentMask: Int] = [:] + /// An empty column of every known component type, indexed by component index. Seeds the columns of new tables. + @exclusivity(unchecked) + @usableFromInline var prototypes = ContiguousArray() + /// The index of every known component type, by its runtime identifier. + @exclusivity(unchecked) + @usableFromInline var indexByIdentifier: [ComponentIdentifier: Int] = [:] + /// The tables that match each family filter, extended lazily as new tables appear. + @usableFromInline var queryCache: [FamilyFilter: TableQuery] = [:] + /// Greater than zero while a scoped access runs; structural changes trap then. + @exclusivity(unchecked) + @usableFromInline var lockDepth = 0 + /// Component types with a running write access (`modify`, writing iteration, `withComponents`). + @exclusivity(unchecked) + @usableFromInline var activeWrites = ComponentMask() + /// Component types with a running read span. Writes to them trap. + @exclusivity(unchecked) + @usableFromInline var activeReads = ComponentMask() + + @usableFromInline + init() { + makeEmptyTable() + } + + private func makeEmptyTable() { + tables.append(ArchetypeTable(mask: ComponentMask(), columns: [])) + tableIndexByMask[ComponentMask()] = 0 + } + + /// The number of tables, including the one of entities without components. + @usableFromInline var tableCount: Int { + tables.count + } + + // MARK: Helpers + + @inlinable + func beginWrite(_ componentIndex: Int) { + precondition(!activeWrites.contains(componentIndex), "nested write access to component \(componentIndex)") + precondition(!activeReads.contains(componentIndex), "write access to component \(componentIndex) during a read span of it") + activeWrites.insert(componentIndex) + } + + @inlinable + func endWrite(_ componentIndex: Int) { + activeWrites.remove(componentIndex) + } + + @inlinable + func requireReadable(_ componentIndex: Int) { + precondition(!activeWrites.contains(componentIndex), "read of component \(componentIndex) during a write access to it") + } + + @inlinable + func requireUnlocked() { + precondition(lockDepth == 0, "structural change while a scoped access is running") + } + + /// Records the component type, so that tables can be seeded with a column for it. + @usableFromInline + func register(_: C.Type, at index: Int) { + if index >= 0, index < prototypes.count, let known = prototypes[index] { + precondition(known.typeID == ObjectIdentifier(C.self), "component index \(index) is used by two component types") + return + } + precondition(index >= 0 && index < ComponentIndexRegistry.indexLimit, "component index \(index) is out of range") + // Components are value types: a class would alias its row and bypass change tracking. The macro + // rejects classes at compile time, a hand-written conformance is only caught here, in debug builds. + assert(!(C.self is AnyClass), "component type \(C.self) is a class; components must be structs or enums") + while prototypes.count <= index { + prototypes.append(nil) + } + let column = Column() + prototypes[index] = column + indexByIdentifier[column.identifier] = index + } + + @inlinable + func column(_: C.Type, at index: Int, in location: EntityLocation) -> Column? { + tables[Int(location.table)].column(C.self, at: index) + } + + // MARK: Tables + + @usableFromInline + func table(for mask: ComponentMask, makeColumns: () -> [(index: Int, column: AnyColumn)]) -> Int { + if let index = tableIndexByMask[mask] { + return index + } + tables.append(ArchetypeTable(mask: mask, columns: makeColumns())) + tableIndexByMask[mask] = tables.count - 1 + return tables.count - 1 + } + + /// The table after adding component type `C` to entities of table `sourceIndex`, through the cached edge. + @usableFromInline + func tableAfterAdding(_: C.Type, at componentIndex: Int, to sourceIndex: Int) -> Int { + let source = tables[sourceIndex] + let cached = ArchetypeTable.edge(source.addEdges, componentIndex) + if cached >= 0 { + return cached + } + var mask = source.mask + mask.insert(componentIndex) + let target = table(for: mask) { + var columns = source.columnIndices.map { (index: $0, column: source.columns[$0].unsafelyUnwrapped.makeEmpty()) } + columns.append((componentIndex, Column())) + return columns + } + ArchetypeTable.setEdge(&source.addEdges, componentIndex, to: target) + return target + } + + /// The table after removing component type `componentIndex` from entities of table `sourceIndex`, through the cached edge. + @usableFromInline + func tableAfterRemoving(_ componentIndex: Int, from sourceIndex: Int) -> Int { + let source = tables[sourceIndex] + let cached = ArchetypeTable.edge(source.removeEdges, componentIndex) + if cached >= 0 { + return cached + } + var mask = source.mask + mask.remove(componentIndex) + let target = table(for: mask) { + source.columnIndices.filter { $0 != componentIndex }.map { (index: $0, column: source.columns[$0].unsafelyUnwrapped.makeEmpty()) } + } + ArchetypeTable.setEdge(&source.removeEdges, componentIndex, to: target) + return target + } + + /// Removes the entity-index bookkeeping of `row` in `table`; the caller handled the columns. + @usableFromInline + func removeRowEntry(_ row: Int, in table: ArchetypeTable) { + let last = table.entities.count - 1 + if row != last { + let moved = table.entities[last] + table.entities[row] = moved + locations[Int(moved)].row = UInt32(row) + } + table.entities.removeLast() + } + + /// Moves the row of an entity into another table. + /// + /// Columns both tables have are moved. A column only the source has is dropped: its value is returned, to be + /// released by the caller once its own bookkeeping is consistent (at most one column is dropped, a single + /// component type is removed at a time). `movedOut` names a column whose value was moved out of its row already + /// and is not released. Columns only the target has must be appended by the caller afterwards. + @usableFromInline + func move(_ entityIndex: UInt32, to targetIndex: Int, movedOut: Int = -1) -> (any Component)? { + let location = locations[Int(entityIndex)] + let source = tables[Int(location.table)] + let target = tables[targetIndex] + let row = Int(location.row) + let newRow = target.entities.count + target.entities.append(entityIndex) + var dropped: (any Component)? + for index in source.columnIndices { + let sourceColumn = source.columns[index].unsafelyUnwrapped + if index < target.columns.count, let targetColumn = target.columns[index] { + targetColumn.moveAppend(from: sourceColumn, row: row) + } else if index == movedOut { + sourceColumn.swapRemoveMovedOut(row) + } else { + dropped = sourceColumn.swapRemove(row) + } + } + removeRowEntry(row, in: source) + locations[Int(entityIndex)] = EntityLocation(table: UInt32(targetIndex), row: UInt32(newRow)) + return dropped + } + + /// The tables whose component set matches `filter`, cached per filter. Only tables created since the last + /// call for this filter are tested again. + @usableFromInline + func matchingTables(_ filter: FamilyFilter) -> TableQuery { + let query: TableQuery + if let cached = queryCache[filter] { + query = cached + } else { + query = TableQuery() + queryCache[filter] = query + } + while query.scanned < tables.count { + let table = tables[query.scanned] + if filter.accepts(table.mask) { + query.tables.append(Unmanaged.passUnretained(table)) + } + query.scanned += 1 + } + return query + } + + // MARK: Entities + + @usableFromInline var entityCount: Int { + entities.liveCount + } + + @usableFromInline + func insertEntity(_ entityId: EntityIdentifier) { + requireUnlocked() + entities.insert(entityId) + let index = Int(entityId.index) + while locations.count <= index { + locations.append(EntityLocation(table: 0, row: 0)) + } + let empty = tables[0] + locations[index] = EntityLocation(table: 0, row: UInt32(empty.entities.count)) + empty.entities.append(entityId.index) + } + + /// Removes all components of the entity and frees its slot. + /// + /// The removed components are released last, after the slot is free, so a `deinit` that re-enters sees consistent state. + @usableFromInline + @discardableResult + func destroyEntity(_ entityId: EntityIdentifier) -> Bool { + requireUnlocked() + guard entities.isAlive(entityId) else { + return false + } + let location = locations[Int(entityId.index)] + let table = tables[Int(location.table)] + var removed: [any Component] = [] + for index in table.columnIndices { + if let component = table.columns[index].unsafelyUnwrapped.swapRemove(Int(location.row)) { + removed.append(component) + } + } + removeRowEntry(Int(location.row), in: table) + let destroyed = entities.remove(entityId) + withExtendedLifetime(removed) {} + return destroyed + } + + @inlinable + func isAlive(_ entityId: EntityIdentifier) -> Bool { + entities.isAlive(entityId) + } + + /// Removes all entities and components, and forgets all component types and tables. + @usableFromInline + func removeAll() { + requireUnlocked() + // Components are released last, so a `deinit` that re-enters sees the emptied storage. + let released = (tables, prototypes) + entities = EntityTable() + locations = [] + tables = [] + tableIndexByMask = [:] + prototypes = [] + indexByIdentifier.removeAll() + queryCache.removeAll() + makeEmptyTable() + withExtendedLifetime(released) {} + } + } +} diff --git a/Sources/FirebladeECS/ArchetypeTable.swift b/Sources/FirebladeECS/ArchetypeTable.swift new file mode 100644 index 00000000..15a76e02 --- /dev/null +++ b/Sources/FirebladeECS/ArchetypeTable.swift @@ -0,0 +1,263 @@ +// +// ArchetypeTable.swift +// FirebladeECS +// +// Created by Christian Treffs on 25.10.26. +// + +extension Nexus { + /// Type-erased base of one column of an ``ArchetypeTable``. + /// + /// The untyped methods are the slow paths (row moves between tables, destruction, snapshots, inspection); + /// hot paths cast to the typed ``Column`` with `unsafeDowncast` after checking `typeID`. + @usableFromInline + class AnyColumn { + /// The component type of this column; guards `unsafeDowncast` against two types sharing an index. + @usableFromInline final let typeID: ObjectIdentifier + /// The runtime identifier of the component type. + @usableFromInline final let identifier: ComponentIdentifier + + @usableFromInline + init(typeID: ObjectIdentifier, identifier: ComponentIdentifier) { + self.typeID = typeID + self.identifier = identifier + } + + // swiftlint:disable unavailable_function + /// A column of the same component type without rows. + @usableFromInline + func makeEmpty() -> AnyColumn { + fatalError("abstract") + } + + /// Removes `row` by swapping the last row into its place. + /// + /// - Returns: The removed value, or `nil` if releasing it cannot run code (plain data). The caller releases it + /// last, after its own bookkeeping is consistent, because releasing can run a `deinit` that re-enters the nexus. + @usableFromInline + func swapRemove(_: Int) -> (any Component)? { + fatalError("abstract") + } + + /// Removes `row` of the column, which holds a value that was moved out, without releasing anything. + @usableFromInline + func swapRemoveMovedOut(_: Int) { + fatalError("abstract") + } + + /// Moves `row` of `other` (same type) to the end of this column and fills the hole with `other`'s last row. + @usableFromInline + func moveAppend(from _: AnyColumn, row _: Int) { + fatalError("abstract") + } + + /// The component of a row. + @usableFromInline + func anyValue(at _: Int) -> any Component { + fatalError("abstract") + } + // swiftlint:enable unavailable_function + } + + /// One component type's values of an ``ArchetypeTable``: row `i` belongs to `ArchetypeTable.entities[i]`. + /// + /// The values live in a hand-managed buffer this class owns, so moving a row between tables is one `moveInitialize` + /// per column and a pointer to row 0 never comes from a `withUnsafe…` scope. Elements are initialized and + /// deinitialized by hand; `deinit` releases the live ones. Removal swaps the last row into the hole. + /// + /// Soundness of `@exclusivity(unchecked)`: `base`, `count` and `capacity` change only in structural changes, which + /// `ArchetypeStorage` blocks while a scoped access or iteration runs, and a single thread uses a nexus at a time. + @usableFromInline + final class Column: AnyColumn { + /// Address of row 0; dangling (never dereferenced) while `capacity` is 0. + @exclusivity(unchecked) + @usableFromInline var base: UnsafeMutablePointer + @exclusivity(unchecked) + @usableFromInline var count = 0 + @exclusivity(unchecked) + @usableFromInline var capacity = 0 + + @usableFromInline + init() { + base = UnsafeMutablePointer(bitPattern: MemoryLayout.alignment).unsafelyUnwrapped + super.init(typeID: ObjectIdentifier(C.self), identifier: C.identifier) + } + + deinit { + base.deinitialize(count: count) + if capacity > 0 { + base.deallocate() + } + } + + @inlinable + subscript(row: Int) -> C { + unsafeAddress { UnsafePointer(base + row) } + unsafeMutableAddress { base + row } + } + + @inlinable + func append(_ value: C) { + if count == capacity { + grow() + } + (base + count).initialize(to: value) + count += 1 + } + + @usableFromInline + func grow() { + let newCapacity = max(8, capacity * 2) + let newBase = UnsafeMutablePointer.allocate(capacity: newCapacity) + newBase.moveInitialize(from: base, count: count) + if capacity > 0 { + base.deallocate() + } + base = newBase + capacity = newCapacity + } + + /// Fills the hole at `row` (deinitialized or moved out already) with the last row. + @inlinable + func fillHole(_ row: Int) { + let last = count - 1 + if row != last { + (base + row).moveInitialize(from: base + last, count: 1) + } + count = last + } + + @usableFromInline + override func makeEmpty() -> AnyColumn { + Column() + } + + @usableFromInline + override func swapRemove(_ row: Int) -> (any Component)? { + if _isPOD(C.self) { + fillHole(row) + return nil + } + let removed = (base + row).move() + fillHole(row) + return removed + } + + @usableFromInline + override func swapRemoveMovedOut(_ row: Int) { + fillHole(row) + } + + @usableFromInline + override func moveAppend(from other: AnyColumn, row: Int) { + let source = unsafeDowncast(other, to: Column.self) + if count == capacity { + grow() + } + (base + count).moveInitialize(from: source.base + row, count: 1) + count += 1 + source.fillHole(row) + } + + @usableFromInline + override func anyValue(at row: Int) -> any Component { + self[row] + } + } + + /// All entities that share one component set: a column per component type plus the entity index of every row. + /// + /// Tables are created on demand and then live as long as the storage (see ``ArchetypeStorage``). A table keeps cached + /// transitions to the tables after adding or removing one component type (edges, like the table graph of flecs), + /// so a structural change finds its target table without hashing the component set. + @usableFromInline + final class ArchetypeTable { + /// The component set of every entity in this table. + @usableFromInline let mask: ComponentMask + /// The entity index of every row. + @exclusivity(unchecked) + @usableFromInline var entities = ContiguousArray() + /// Indexed by component index; `nil` for component types that are not in `mask`. + @exclusivity(unchecked) + @usableFromInline var columns: ContiguousArray + /// The same columns without reference counting, for loops. `columns` keeps them alive. + @exclusivity(unchecked) + @usableFromInline var unmanagedColumns: ContiguousArray?> + /// The component indices in `mask`, ascending. + @usableFromInline let columnIndices: [Int] + /// Cached transitions: `addEdges[c]` is the table after adding component type `c`, `-1` while unknown. + @exclusivity(unchecked) + @usableFromInline var addEdges = ContiguousArray() + /// Cached transitions: `removeEdges[c]` is the table after removing component type `c`, `-1` while unknown. + @exclusivity(unchecked) + @usableFromInline var removeEdges = ContiguousArray() + + @usableFromInline + init(mask: ComponentMask, columns: [(index: Int, column: AnyColumn)]) { + self.mask = mask + var owned = ContiguousArray() + var unmanaged = ContiguousArray?>() + for (index, column) in columns { + while owned.count <= index { + owned.append(nil) + unmanaged.append(nil) + } + owned[index] = column + unmanaged[index] = Unmanaged.passUnretained(column) + } + self.columns = owned + unmanagedColumns = unmanaged + columnIndices = columns.map(\.index) + } + + /// The number of entities in this table. + @inlinable var rowCount: Int { + entities.count + } + + /// The column of a component type, or `nil` if the table has none. + @inlinable + func column(_: C.Type, at index: Int) -> Column? { + guard index >= 0, index < columns.count, let column = columns[index] else { + return nil + } + precondition(column.typeID == ObjectIdentifier(C.self), "component index \(index) is used by two component types") + return unsafeDowncast(column, to: Column.self) + } + + /// The address of row 0 of a column, without retaining the column. The table must have the column. + @inlinable + func base(_: C.Type, at index: Int) -> UnsafeMutablePointer { + unmanagedColumns[index].unsafelyUnwrapped._withUnsafeGuaranteedRef { column in + precondition(column.typeID == ObjectIdentifier(C.self), "component index \(index) is used by two component types") + return unsafeDowncast(column, to: Column.self).base + } + } + + @usableFromInline + static func edge(_ edges: ContiguousArray, _ componentIndex: Int) -> Int { + componentIndex < edges.count ? Int(edges[componentIndex]) : -1 + } + + @usableFromInline + static func setEdge(_ edges: inout ContiguousArray, _ componentIndex: Int, to table: Int) { + while edges.count <= componentIndex { + edges.append(-1) + } + edges[componentIndex] = Int32(table) + } + } + + /// The tables matching one family filter, as unretained references (``ArchetypeStorage/tables`` owns them). + /// + /// The list is extended by the tables created since the last query and never rebuilt, because tables are not + /// removed while the storage lives. + @usableFromInline + final class TableQuery { + @usableFromInline var tables = ContiguousArray>() + @usableFromInline var scanned = 0 + + @usableFromInline + init() {} + } +} From 29e5509c4ef7ffdd741a643c81350f67adb66288 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:01:12 +0100 Subject: [PATCH 02/12] Add the archetype case to Nexus storage and its structural operations Nexus(storage: .archetype) now selects ArchetypeStorage. Components are added, replaced and removed by moving the entity's row between tables along cached edges; get, modify, scoped access and withComponents work on the columns. Iteration and spans still serve the sparse-set storage only. The default stays the sparse set. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- .../ArchetypeStorage+Access.swift | 126 ++++++++++++++++++ .../ArchetypeStorage+Component.swift | 121 +++++++++++++++++ Sources/FirebladeECS/Nexus+Storage.swift | 59 ++++++++ .../FirebladeECSTests/NexusStorageTests.swift | 2 +- 4 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 Sources/FirebladeECS/ArchetypeStorage+Access.swift create mode 100644 Sources/FirebladeECS/ArchetypeStorage+Component.swift diff --git a/Sources/FirebladeECS/ArchetypeStorage+Access.swift b/Sources/FirebladeECS/ArchetypeStorage+Access.swift new file mode 100644 index 00000000..d86d9dfd --- /dev/null +++ b/Sources/FirebladeECS/ArchetypeStorage+Access.swift @@ -0,0 +1,126 @@ +// +// ArchetypeStorage+Access.swift +// FirebladeECS +// +// Created by Christian Treffs on 25.10.26. +// + +extension Nexus.ArchetypeStorage { + /// Gives `body` write access to the component of an entity. + /// - Returns: `false` if the entity has no such component. + @inlinable + func modify(_: C.Type, for entityId: EntityIdentifier, _ body: (inout C) throws -> Void) rethrows -> Bool { + let componentIndex = C.componentIndex + guard entities.isAlive(entityId) else { + return false + } + let location = locations[Int(entityId.index)] + guard let column = column(C.self, at: componentIndex, in: location) else { + return false + } + beginWrite(componentIndex) + lockDepth += 1 + defer { + lockDepth -= 1 + endWrite(componentIndex) + } + try body(&column[Int(location.row)]) + return true + } + + /// Starts a write access to the component of an entity that spans several statements, for `_modify` accessors. + /// + /// The type is write locked and structural changes trap until ``endAccess(_:)``. + /// - Returns: The address of the component in its column, or `nil` if the entity has no such component. + @inlinable + func beginAccess(_: C.Type, for entityId: EntityIdentifier) -> UnsafeMutablePointer? { + let componentIndex = C.componentIndex + guard entities.isAlive(entityId) else { + return nil + } + let location = locations[Int(entityId.index)] + guard let column = column(C.self, at: componentIndex, in: location) else { + return nil + } + beginWrite(componentIndex) + lockDepth += 1 + return column.base + Int(location.row) + } + + /// Ends the write access started by ``beginAccess(_:for:)``. + @inlinable + func endAccess(_ componentIndex: Int) { + lockDepth -= 1 + endWrite(componentIndex) + } + + /// Removes a component whose value was moved out of its row and not put back, without releasing it. + @inlinable + func discardMovedOut(_: C.Type, for entityId: EntityIdentifier) { + requireUnlocked() + let componentIndex = C.componentIndex + guard entities.isAlive(entityId), has(componentIndex: componentIndex, for: entityId) else { + return + } + removeMovedOut(componentIndex, from: entityId) + } + + @usableFromInline + func removeMovedOut(_ componentIndex: Int, from entityId: EntityIdentifier) { + let target = tableAfterRemoving(componentIndex, from: Int(locations[Int(entityId.index)].table)) + _ = move(entityId.index, to: target, movedOut: componentIndex) + entities.remove(componentIndex, from: entityId) + } + + /// Starts a read span of the type: it must not be written, and writing it traps until ``endRead(_:wasReading:)``. + /// - Returns: Whether the type was in a read span already (spans nest); hand it to ``endRead(_:wasReading:)``. + @inlinable + func beginRead(_ componentIndex: Int) -> Bool { + requireReadable(componentIndex) + let wasReading = activeReads.contains(componentIndex) + activeReads.insert(componentIndex) + return wasReading + } + + @inlinable + func endRead(_ componentIndex: Int, wasReading: Bool) { + if !wasReading { + activeReads.remove(componentIndex) + } + } + + /// Gives `body` the entity indices and component values of the type with write access, one call per table that holds rows of it. + /// + /// An archetype keeps a component type in one column per table, so the values are not one contiguous buffer: `body` is + /// called once for every table that has the type and at least one row. The two buffers of a call have the same length, + /// and element `i` of both belong to the same entity. + @inlinable + func withComponents( + of _: C.Type, _ body: (UnsafeBufferPointer, UnsafeMutableBufferPointer) -> Void + ) { + let componentIndex = C.componentIndex + guard hasRows(of: componentIndex) else { + return + } + beginWrite(componentIndex) + lockDepth += 1 + defer { + lockDepth -= 1 + endWrite(componentIndex) + } + for table in tables where table.rowCount > 0 { + guard let column = table.column(C.self, at: componentIndex) else { + continue + } + table.entities.withUnsafeBufferPointer { ids in + body(ids, UnsafeMutableBufferPointer(start: column.base, count: ids.count)) + } + } + } + + /// Whether any table that holds the component type has a row. + @usableFromInline + func hasRows(of componentIndex: Int) -> Bool { + tables.contains { $0.rowCount > 0 && $0.mask.contains(componentIndex) } + } +} diff --git a/Sources/FirebladeECS/ArchetypeStorage+Component.swift b/Sources/FirebladeECS/ArchetypeStorage+Component.swift new file mode 100644 index 00000000..906fb6a9 --- /dev/null +++ b/Sources/FirebladeECS/ArchetypeStorage+Component.swift @@ -0,0 +1,121 @@ +// +// ArchetypeStorage+Component.swift +// FirebladeECS +// +// Created by Christian Treffs on 25.10.26. +// + +extension Nexus.ArchetypeStorage { + // MARK: Typed component access + + /// Adds a component to a living entity, or replaces the one it has. + @inlinable + func set(_ component: C, at componentIndex: Int, for entityId: EntityIdentifier) { + requireUnlocked() + precondition(entities.isAlive(entityId), "set on a dead entity") + let location = locations[Int(entityId.index)] + if let column = column(C.self, at: componentIndex, in: location) { + column[Int(location.row)] = component + } else { + add(component, at: componentIndex, for: entityId, from: location) + } + } + + @usableFromInline + func add(_ component: C, at componentIndex: Int, for entityId: EntityIdentifier, from location: Nexus.EntityLocation) { + register(C.self, at: componentIndex) + let target = tableAfterAdding(C.self, at: componentIndex, to: Int(location.table)) + _ = move(entityId.index, to: target) + tables[target].column(C.self, at: componentIndex).unsafelyUnwrapped.append(component) + entities.insert(componentIndex, into: entityId) + } + + @inlinable + func get(_: C.Type, at componentIndex: Int, for entityId: EntityIdentifier) -> C? { + requireReadable(componentIndex) + guard entities.isAlive(entityId) else { + return nil + } + let location = locations[Int(entityId.index)] + return column(C.self, at: componentIndex, in: location)?[Int(location.row)] + } + + /// The component of a living entity that must have it. + /// + /// Checks the generation of the identifier and that the entity has the component: a stale identifier or a + /// missing component traps with a message instead of reading another entity's component. + @inlinable + func unsafeGet(_: C.Type, at componentIndex: Int, for entityId: EntityIdentifier) -> C { + requireReadable(componentIndex) + guard entities.isAlive(entityId) else { + preconditionFailure("entity \(entityId) is not alive or has no component \(C.self)") + } + let location = locations[Int(entityId.index)] + guard let column = column(C.self, at: componentIndex, in: location) else { + preconditionFailure("entity \(entityId) has no component \(C.self)") + } + return column[Int(location.row)] + } + + /// Whether a column for the type exists in some table: the type was added to an entity before. + @inlinable + func hasStorage(for componentIndex: Int) -> Bool { + componentIndex >= 0 && componentIndex < prototypes.count && prototypes[componentIndex] != nil + } + + @inlinable + func has(componentIndex: Int, for entityId: EntityIdentifier) -> Bool { + entities.mask(of: entityId)?.contains(componentIndex) ?? false + } + + // MARK: Untyped slow paths + + /// The indices of the component types of a living entity, ascending. + @usableFromInline + func componentIndices(of entityId: EntityIdentifier) -> [Int] { + var result: [Int] = [] + entities.mask(of: entityId)?.forEachIndex { result.append($0) } + return result + } + + /// The number of components of a living entity. + @usableFromInline + func componentCount(of entityId: EntityIdentifier) -> Int { + entities.mask(of: entityId)?.count ?? 0 + } + + /// The number of component instances in all tables. + @usableFromInline var totalComponentCount: Int { + tables.reduce(0) { $0 + $1.rowCount * $1.columnIndices.count } + } + + @usableFromInline + func identifier(ofComponentIndex componentIndex: Int) -> ComponentIdentifier? { + componentIndex >= 0 && componentIndex < prototypes.count ? prototypes[componentIndex]?.identifier : nil + } + + @usableFromInline + @discardableResult + func remove(componentIndex: Int, from entityId: EntityIdentifier) -> Bool { + requireUnlocked() + guard entities.mask(of: entityId)?.contains(componentIndex) == true else { + return false + } + let target = tableAfterRemoving(componentIndex, from: Int(locations[Int(entityId.index)].table)) + let dropped = move(entityId.index, to: target) + entities.remove(componentIndex, from: entityId) + // released last: a `deinit` that re-enters sees the component gone from table and entity + withExtendedLifetime(dropped) {} + return true + } + + @usableFromInline + func anyComponent(componentIndex: Int, for entityId: EntityIdentifier) -> (any Component)? { + requireReadable(componentIndex) + guard entities.mask(of: entityId)?.contains(componentIndex) == true else { + return nil + } + let location = locations[Int(entityId.index)] + return tables[Int(location.table)].columns[componentIndex]?.anyValue(at: Int(location.row)) + } +} diff --git a/Sources/FirebladeECS/Nexus+Storage.swift b/Sources/FirebladeECS/Nexus+Storage.swift index 34bd933e..9b915e34 100644 --- a/Sources/FirebladeECS/Nexus+Storage.swift +++ b/Sources/FirebladeECS/Nexus+Storage.swift @@ -15,6 +15,12 @@ extension Nexus { /// Adding and removing components is O(1) and moves no other data. case sparseSet + /// One table per component set, a column per component type in every table, an index from entity to table and row. + /// + /// Iterating a family walks the columns of its tables linearly. Adding or removing a component moves the + /// entity's row to another table. + case archetype + /// The storage ``Nexus/init(storage:)`` uses by default. public static let `default` = StorageKind.sparseSet } @@ -23,11 +29,14 @@ extension Nexus { @usableFromInline enum Backend { case sparseSet(SparseSetStorage) + case archetype(ArchetypeStorage) init(_ kind: StorageKind) { switch kind { case .sparseSet: self = .sparseSet(SparseSetStorage()) + case .archetype: + self = .archetype(ArchetypeStorage()) } } } @@ -57,6 +66,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entityCount + case let .archetype(storage): + storage.entityCount } } @@ -64,6 +75,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.totalComponentCount + case let .archetype(storage): + storage.totalComponentCount } } @@ -71,6 +84,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entities.livingIds + case let .archetype(storage): + storage.entities.livingIds } } @@ -79,6 +94,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.insertEntity(entityId) + case let .archetype(storage): + storage.insertEntity(entityId) } } @@ -88,6 +105,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.destroyEntity(entityId) + case let .archetype(storage): + storage.destroyEntity(entityId) } } @@ -96,6 +115,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.isAlive(entityId) + case let .archetype(storage): + storage.isAlive(entityId) } } @@ -104,6 +125,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entities.current(at: index) + case let .archetype(storage): + storage.entities.current(at: index) } } @@ -112,6 +135,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.removeAll() + case let .archetype(storage): + storage.removeAll() } } @@ -120,6 +145,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.set(component, at: componentIndex, for: entityId) + case let .archetype(storage): + storage.set(component, at: componentIndex, for: entityId) } } @@ -133,6 +160,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): try storage.modify(type, for: entityId, body) + case let .archetype(storage): + try storage.modify(type, for: entityId, body) } } @@ -141,6 +170,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.beginAccess(type, for: entityId) + case let .archetype(storage): + storage.beginAccess(type, for: entityId) } } @@ -149,6 +180,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.endAccess(componentIndex) + case let .archetype(storage): + storage.endAccess(componentIndex) } } @@ -157,6 +190,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.discardMovedOut(type, for: entityId) + case let .archetype(storage): + storage.discardMovedOut(type, for: entityId) } } @@ -165,6 +200,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.withComponents(of: type, body) + case let .archetype(storage): + storage.withComponents(of: type, body) } } @@ -173,6 +210,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.get(type, at: componentIndex, for: entityId) + case let .archetype(storage): + storage.get(type, at: componentIndex, for: entityId) } } @@ -186,6 +225,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.unsafeGet(type, at: componentIndex, for: entityId) + case let .archetype(storage): + storage.unsafeGet(type, at: componentIndex, for: entityId) } } @@ -199,6 +240,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.hasStorage(for: componentIndex) + case let .archetype(storage): + storage.hasStorage(for: componentIndex) } } @@ -207,6 +250,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.has(componentIndex: componentIndex, for: entityId) + case let .archetype(storage): + storage.has(componentIndex: componentIndex, for: entityId) } } @@ -215,6 +260,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entities.mask(of: entityId) + case let .archetype(storage): + storage.entities.mask(of: entityId) } } @@ -223,6 +270,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.componentIndices(of: entityId) + case let .archetype(storage): + storage.componentIndices(of: entityId) } } @@ -231,6 +280,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.componentCount(of: entityId) + case let .archetype(storage): + storage.componentCount(of: entityId) } } @@ -239,6 +290,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.indexByIdentifier[identifier] + case let .archetype(storage): + storage.indexByIdentifier[identifier] } } @@ -247,6 +300,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.identifier(ofComponentIndex: componentIndex) + case let .archetype(storage): + storage.identifier(ofComponentIndex: componentIndex) } } @@ -256,6 +311,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.remove(componentIndex: componentIndex, from: entityId) + case let .archetype(storage): + storage.remove(componentIndex: componentIndex, from: entityId) } } @@ -264,6 +321,8 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.anyComponent(componentIndex: componentIndex, for: entityId) + case let .archetype(storage): + storage.anyComponent(componentIndex: componentIndex, for: entityId) } } } diff --git a/Tests/FirebladeECSTests/NexusStorageTests.swift b/Tests/FirebladeECSTests/NexusStorageTests.swift index ec852fd2..dd92785a 100644 --- a/Tests/FirebladeECSTests/NexusStorageTests.swift +++ b/Tests/FirebladeECSTests/NexusStorageTests.swift @@ -23,7 +23,7 @@ private struct Tag300: Component, Sendable { @Suite struct NexusStorageTests { @Test func defaultStorageIsTheSparseSet() { #expect(Nexus.StorageKind.default == .sparseSet) - #expect(Nexus.StorageKind.allCases == [.sparseSet]) + #expect(Nexus.StorageKind.allCases == [.sparseSet, .archetype]) } @Test(arguments: Nexus.StorageKind.allCases) From 22448ec51c46de2c6b655ea2f63e4ae17ee2f6ef Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:01:34 +0100 Subject: [PATCH 03/12] Count all tables of a bulk write in one ComponentsModified event The archetype storage calls the bulk write body once per table that holds the component type, so the event sums the calls instead of reporting the last one. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Sources/FirebladeECS/Nexus+ValueChanges.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/FirebladeECS/Nexus+ValueChanges.swift b/Sources/FirebladeECS/Nexus+ValueChanges.swift index 4ffef2ee..39ab532d 100644 --- a/Sources/FirebladeECS/Nexus+ValueChanges.swift +++ b/Sources/FirebladeECS/Nexus+ValueChanges.swift @@ -117,11 +117,13 @@ extension Nexus { /// Gives `body` write access to all components of a type as contiguous buffers and emits one /// ``ComponentsModified`` afterwards, not one event per entity. + /// + /// The archetype storage calls `body` once per table that holds the type; the event counts the components of all calls. @usableFromInline final func withComponents(of type: C.Type, _ body: (UnsafeBufferPointer, UnsafeMutableBufferPointer) -> Void) { var count = 0 backend.withComponents(of: type) { ids, components in - count = components.count + count += components.count body(ids, components) } if count > 0 { From 0c3baa4cc664d706794c2ec38949c6d3ccfec46e Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:09:18 +0100 Subject: [PATCH 04/12] Iterate the matching tables in forEach(reading:writing:) on the archetype storage Every fixed arity overload and the parameter pack fallback switch on the storage once per call. The archetype path resolves the tables a family matches from a cached query, resolves the columns once per table and loops over the rows. The pack fallback keeps the never-specialize attribute on its helpers. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Scripts/generate-read-write-api.sh | 91 +- Sources/FirebladeECS/Family+ReadWrite.swift | 86 +- .../Family+ReadWrite.generated.swift | 2527 +++++++++++------ 3 files changed, 1814 insertions(+), 890 deletions(-) diff --git a/Scripts/generate-read-write-api.sh b/Scripts/generate-read-write-api.sh index f453be74..82919a1e 100755 --- a/Scripts/generate-read-write-api.sh +++ b/Scripts/generate-read-write-api.sh @@ -96,7 +96,8 @@ family() { /// components, and replacing a component with `set`. Collect them and apply them after the pass, and write /// changed values through the `inout` parameters. Reading a written type through the nexus /// (for example `entity.get(_:)`) traps, too. - /// - Members are visited once each, in the order of the family's member list. + /// - Members are visited once each. The sparse-set storage visits them in the order of the family's member list, the + /// archetype storage table by table and row by row; neither order is a contract, and structural changes reorder them. /// - If the delegate subscribed to ``NexusEventMask/valueChanges``, one ``ComponentsModified`` event per written type /// is delivered after the pass, not one per member. If `body` throws, the pass ends there, members already visited, /// including the one that threw, stay changed and the events cover them. @@ -117,46 +118,76 @@ DOC for p in "${params[@]}"; do echo " $p,"; done echo " _ body: ($(join ', ' "${closureParams[@]}")) throws -> Void" echo " ) rethrows {" - echo " guard case let .sparseSet(sparse) = nexus.backend else { return }" for ((i = 1; i <= r; i++)); do echo " requireRequired(R$i.self)"; done for ((i = 1; i <= w; i++)); do echo " requireRequired(W$i.self)"; done - echo " let members = memberIds" + echo " switch nexus.backend {" + echo " case let .sparseSet(sparse):" + echo " let members = memberIds" local conds=("!members.isEmpty") for ((i = 1; i <= r; i++)); do conds+=("let rs$i = sparse.existing(R$i.self, at: R$i.componentIndex)"); done for ((i = 1; i <= w; i++)); do conds+=("let ws$i = sparse.existing(W$i.self, at: W$i.componentIndex)"); done - echo " guard $(join ', ' "${conds[@]}") else { return }" - echo " sparse.lockDepth += 1" - for ((i = 1; i <= w; i++)); do echo " sparse.beginWrite(W$i.componentIndex)"; done - for ((i = 1; i <= r; i++)); do echo " sparse.requireReadable(R$i.componentIndex)"; done - echo " var visited = 0" - echo " defer {" - for ((i = 1; i <= w; i++)); do echo " sparse.endWrite(W$i.componentIndex)"; done - echo " sparse.lockDepth -= 1" + echo " guard $(join ', ' "${conds[@]}") else { return }" + echo " sparse.lockDepth += 1" + for ((i = 1; i <= w; i++)); do echo " sparse.beginWrite(W$i.componentIndex)"; done + for ((i = 1; i <= r; i++)); do echo " sparse.requireReadable(R$i.componentIndex)"; done + echo " var visited = 0" + echo " defer {" + for ((i = 1; i <= w; i++)); do echo " sparse.endWrite(W$i.componentIndex)"; done + echo " sparse.lockDepth -= 1" if ((w > 0)); then - echo " if visited > 0 {" - for ((i = 1; i <= w; i++)); do echo " nexus.emitModified(W$i.self, count: visited)"; done - echo " }" + echo " if visited > 0 {" + for ((i = 1; i <= w; i++)); do echo " nexus.emitModified(W$i.self, count: visited)"; done + echo " }" fi - echo " }" - for ((i = 1; i <= r; i++)); do echo " let rBase$i = rs$i.base"; done - for ((i = 1; i <= w; i++)); do echo " let wBase$i = ws$i.base"; done - echo " let entries = members.storage.dense" - echo " try entries.withUnsafeBufferPointer { entries in" - echo " for entry in entries {" - echo " let key = entry.key" + echo " }" + for ((i = 1; i <= r; i++)); do echo " let rBase$i = rs$i.base"; done + for ((i = 1; i <= w; i++)); do echo " let wBase$i = ws$i.base"; done + echo " let entries = members.storage.dense" + echo " try entries.withUnsafeBufferPointer { entries in" + echo " for entry in entries {" + echo " let key = entry.key" for ((i = 1; i <= r; i++)); do - echo " let rSlot$i = rs$i.slot(of: key)" - echo " precondition(rSlot$i >= 0, \"member without a required component\")" + echo " let rSlot$i = rs$i.slot(of: key)" + echo " precondition(rSlot$i >= 0, \"member without a required component\")" done for ((i = 1; i <= w; i++)); do - echo " let wSlot$i = ws$i.slot(of: key)" - echo " precondition(wSlot$i >= 0, \"member without a required component\")" + echo " let wSlot$i = ws$i.slot(of: key)" + echo " precondition(wSlot$i >= 0, \"member without a required component\")" done - local callArgs=() - for ((i = 1; i <= r; i++)); do callArgs+=("rBase$i[rSlot$i]"); done - for ((i = 1; i <= w; i++)); do callArgs+=("&wBase$i[wSlot$i]"); done - echo " visited += 1" - echo " try body($(join ', ' "${callArgs[@]}"))" + local callArgs=() rowArgs=() + for ((i = 1; i <= r; i++)); do callArgs+=("rBase$i[rSlot$i]"); rowArgs+=("rBase$i[row]"); done + for ((i = 1; i <= w; i++)); do callArgs+=("&wBase$i[wSlot$i]"); rowArgs+=("&wBase$i[row]"); done + echo " visited += 1" + echo " try body($(join ', ' "${callArgs[@]}"))" + echo " }" + echo " }" + echo " case let .archetype(archetype):" + echo " guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return }" + echo " let query = archetype.matchingTables(filter)" + echo " archetype.lockDepth += 1" + for ((i = 1; i <= w; i++)); do echo " archetype.beginWrite(W$i.componentIndex)"; done + for ((i = 1; i <= r; i++)); do echo " archetype.requireReadable(R$i.componentIndex)"; done + if ((w > 0)); then echo " var visited = 0"; fi + echo " defer {" + for ((i = 1; i <= w; i++)); do echo " archetype.endWrite(W$i.componentIndex)"; done + echo " archetype.lockDepth -= 1" + if ((w > 0)); then + echo " if visited > 0 {" + for ((i = 1; i <= w; i++)); do echo " nexus.emitModified(W$i.self, count: visited)"; done + echo " }" + fi + echo " }" + echo " for entry in query.tables {" + echo " try entry._withUnsafeGuaranteedRef { table in" + echo " let count = table.entities.count" + echo " guard count > 0 else { return }" + for ((i = 1; i <= r; i++)); do echo " let rBase$i = table.base(R$i.self, at: R$i.componentIndex)"; done + for ((i = 1; i <= w; i++)); do echo " let wBase$i = table.base(W$i.self, at: W$i.componentIndex)"; done + echo " for row in 0 ..< count {" + if ((w > 0)); then echo " visited += 1"; fi + echo " try body($(join ', ' "${rowArgs[@]}"))" + echo " }" + echo " }" echo " }" echo " }" echo " }" diff --git a/Sources/FirebladeECS/Family+ReadWrite.swift b/Sources/FirebladeECS/Family+ReadWrite.swift index e83438a1..4fad3f2c 100644 --- a/Sources/FirebladeECS/Family+ReadWrite.swift +++ b/Sources/FirebladeECS/Family+ReadWrite.swift @@ -49,11 +49,25 @@ extension Family { writing wTypes: repeat (each W).Type, body: (inout (repeat each W), repeat each R) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { - return - } _ = (repeat requireRequired((each R).self)) _ = (repeat requireRequired((each W).self)) + switch nexus.backend { + case let .sparseSet(sparse): + try forEachPack(in: sparse, reading: repeat (each R).self, writing: repeat (each W).self, body: body) + + case let .archetype(archetype): + try forEachPack(in: archetype, reading: repeat (each R).self, writing: repeat (each W).self, body: body) + } + } + + @inlinable + @_semantics("optimize.sil.specialize.generic.never") + func forEachPack( + in sparse: Nexus.SparseSetStorage, + reading rTypes: repeat (each R).Type, + writing wTypes: repeat (each W).Type, + body: (inout (repeat each W), repeat each R) throws -> Void + ) rethrows { let members = memberIds guard !members.isEmpty else { return @@ -95,6 +109,54 @@ extension Family { } } + @inlinable + @_semantics("optimize.sil.specialize.generic.never") + func forEachPack( + in archetype: Nexus.ArchetypeStorage, + reading rTypes: repeat (each R).Type, + writing wTypes: repeat (each W).Type, + body: (inout (repeat each W), repeat each R) throws -> Void + ) rethrows { + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { + return + } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + _ = (repeat archetype.beginWrite((each W).componentIndex)) + _ = (repeat archetype.requireReadable((each R).componentIndex)) + var visited = 0 + defer { + _ = (repeat archetype.endWrite((each W).componentIndex)) + archetype.lockDepth -= 1 + if visited > 0 { + _ = (repeat nexus.emitModified((each W).self, count: visited)) + } + } + for entry in query.tables { + let table = entry.takeUnretainedValue() + let readBases = (repeat Self.base(in: table, (each R).self)) + let writeBases = (repeat Self.base(in: table, (each W).self)) + for row in 0 ..< table.entities.count { + let readValues = (repeat Self.value(of: each readBases, at: row)) + let writePointers = (repeat Self.address(of: each writeBases, at: row)) + var written = (repeat Self.load(each writePointers)) + visited += 1 + // the write back uses an immutable copy: a pack expansion over a `var` tuple is an l-value (crashes SILGen on 6.2) + func writeBack() { + let final = written + _ = (repeat Self.store(each final, to: each writePointers)) + } + do { + try body(&written, repeat each readValues) + } catch { + writeBack() + throw error + } + writeBack() + } + } + } + /// The storage of a type that a non-empty family requires, so it exists. @inlinable @_semantics("optimize.sil.specialize.generic.never") @@ -121,6 +183,24 @@ extension Family { return storage.base + slot } + @inlinable + @_semantics("optimize.sil.specialize.generic.never") + static func base(in table: Nexus.ArchetypeTable, _ type: T.Type) -> UnsafeMutablePointer { + table.base(type, at: T.componentIndex) + } + + @inlinable + @_semantics("optimize.sil.specialize.generic.never") + static func value(of base: UnsafeMutablePointer, at row: Int) -> T { + base[row] + } + + @inlinable + @_semantics("optimize.sil.specialize.generic.never") + static func address(of base: UnsafeMutablePointer, at row: Int) -> UnsafeMutablePointer { + base + row + } + @inlinable @_semantics("optimize.sil.specialize.generic.never") static func load(_ pointer: UnsafeMutablePointer) -> T { diff --git a/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift b/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift index c29b68d2..16c04116 100644 --- a/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift +++ b/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift @@ -14,29 +14,55 @@ extension Family { writing wType1: W1.Type, _ body: (inout W1) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let wBase1 = ws1.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - visited += 1 - try body(&wBase1[wSlot1]) + let wBase1 = ws1.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + visited += 1 + try body(&wBase1[wSlot1]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let wBase1 = table.base(W1.self, at: W1.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(&wBase1[row]) + } + } } } } @@ -50,36 +76,66 @@ extension Family { _ wType2: W2.Type, _ body: (inout W1, inout W2) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let wBase1 = ws1.base - let wBase2 = ws2.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - visited += 1 - try body(&wBase1[wSlot1], &wBase2[wSlot2]) + let wBase1 = ws1.base + let wBase2 = ws2.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + visited += 1 + try body(&wBase1[wSlot1], &wBase2[wSlot2]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(&wBase1[row], &wBase2[row]) + } + } } } } @@ -94,43 +150,77 @@ extension Family { _ wType3: W3.Type, _ body: (inout W1, inout W2, inout W3) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) - let members = memberIds - guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } } - } - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - visited += 1 - try body(&wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + visited += 1 + try body(&wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(&wBase1[row], &wBase2[row], &wBase3[row]) + } + } } } } @@ -146,50 +236,88 @@ extension Family { _ wType4: W4.Type, _ body: (inout W1, inout W2, inout W3, inout W4) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) requireRequired(W4.self) - let members = memberIds - guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.beginWrite(W4.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.endWrite(W4.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) - nexus.emitModified(W4.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.beginWrite(W4.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.endWrite(W4.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } } - } - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let wBase4 = ws4.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - let wSlot4 = ws4.slot(of: key) - precondition(wSlot4 >= 0, "member without a required component") - visited += 1 - try body(&wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let wBase4 = ws4.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + let wSlot4 = ws4.slot(of: key) + precondition(wSlot4 >= 0, "member without a required component") + visited += 1 + try body(&wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.beginWrite(W4.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.endWrite(W4.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + let wBase4 = table.base(W4.self, at: W4.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(&wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) + } + } } } } @@ -202,25 +330,45 @@ extension Family { reading rType1: R1.Type, _ body: (R1) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.requireReadable(R1.componentIndex) - var visited = 0 - defer { - sparse.lockDepth -= 1 - } - let rBase1 = rs1.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1]) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.requireReadable(R1.componentIndex) + var visited = 0 + defer { + sparse.lockDepth -= 1 + } + let rBase1 = rs1.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.requireReadable(R1.componentIndex) + defer { + archetype.lockDepth -= 1 + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + for row in 0 ..< count { + try body(rBase1[row]) + } + } } } } @@ -250,7 +398,8 @@ extension Family { /// components, and replacing a component with `set`. Collect them and apply them after the pass, and write /// changed values through the `inout` parameters. Reading a written type through the nexus /// (for example `entity.get(_:)`) traps, too. - /// - Members are visited once each, in the order of the family's member list. + /// - Members are visited once each. The sparse-set storage visits them in the order of the family's member list, the + /// archetype storage table by table and row by row; neither order is a contract, and structural changes reorder them. /// - If the delegate subscribed to ``NexusEventMask/valueChanges``, one ``ComponentsModified`` event per written type /// is delivered after the pass, not one per member. If `body` throws, the pass ends there, members already visited, /// including the one that threw, stay changed and the events cover them. @@ -266,34 +415,62 @@ extension Family { writing wType1: W1.Type, _ body: (R1, inout W1) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.requireReadable(R1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.requireReadable(R1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let rBase1 = rs1.base - let wBase1 = ws1.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], &wBase1[wSlot1]) + let rBase1 = rs1.base + let wBase1 = ws1.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], &wBase1[wSlot1]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.requireReadable(R1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], &wBase1[row]) + } + } } } } @@ -308,41 +485,73 @@ extension Family { _ wType2: W2.Type, _ body: (R1, inout W1, inout W2) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.requireReadable(R1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.requireReadable(R1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let rBase1 = rs1.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], &wBase1[wSlot1], &wBase2[wSlot2]) + let rBase1 = rs1.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], &wBase1[wSlot1], &wBase2[wSlot2]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.requireReadable(R1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], &wBase1[row], &wBase2[row]) + } + } } } } @@ -358,48 +567,84 @@ extension Family { _ wType3: W3.Type, _ body: (R1, inout W1, inout W2, inout W3) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.requireReadable(R1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.requireReadable(R1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } } - } - let rBase1 = rs1.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + let rBase1 = rs1.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.requireReadable(R1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], &wBase1[row], &wBase2[row], &wBase3[row]) + } + } } } } @@ -416,55 +661,95 @@ extension Family { _ wType4: W4.Type, _ body: (R1, inout W1, inout W2, inout W3, inout W4) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) requireRequired(W4.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.beginWrite(W4.componentIndex) - sparse.requireReadable(R1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.endWrite(W4.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) - nexus.emitModified(W4.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.beginWrite(W4.componentIndex) + sparse.requireReadable(R1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.endWrite(W4.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } } - } - let rBase1 = rs1.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let wBase4 = ws4.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - let wSlot4 = ws4.slot(of: key) - precondition(wSlot4 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + let rBase1 = rs1.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let wBase4 = ws4.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + let wSlot4 = ws4.slot(of: key) + precondition(wSlot4 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.beginWrite(W4.componentIndex) + archetype.requireReadable(R1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.endWrite(W4.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + let wBase4 = table.base(W4.self, at: W4.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) + } + } } } } @@ -478,30 +763,52 @@ extension Family { _ rType2: R2.Type, _ body: (R1, R2) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - var visited = 0 - defer { - sparse.lockDepth -= 1 - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2]) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + var visited = 0 + defer { + sparse.lockDepth -= 1 + } + let rBase1 = rs1.base + let rBase2 = rs2.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + defer { + archetype.lockDepth -= 1 + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + for row in 0 ..< count { + try body(rBase1[row], rBase2[row]) + } + } } } } @@ -516,39 +823,69 @@ extension Family { writing wType1: W1.Type, _ body: (R1, R2, inout W1) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let wBase1 = ws1.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let wBase1 = ws1.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], &wBase1[row]) + } + } } } } @@ -564,46 +901,80 @@ extension Family { _ wType2: W2.Type, _ body: (R1, R2, inout W1, inout W2) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1], &wBase2[wSlot2]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1], &wBase2[wSlot2]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], &wBase1[row], &wBase2[row]) + } + } } } } @@ -620,53 +991,91 @@ extension Family { _ wType3: W3.Type, _ body: (R1, R2, inout W1, inout W2, inout W3) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], &wBase1[row], &wBase2[row], &wBase3[row]) + } + } } } } @@ -684,60 +1093,102 @@ extension Family { _ wType4: W4.Type, _ body: (R1, R2, inout W1, inout W2, inout W3, inout W4) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) requireRequired(W4.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.beginWrite(W4.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.endWrite(W4.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) - nexus.emitModified(W4.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.beginWrite(W4.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.endWrite(W4.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let wBase4 = ws4.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - let wSlot4 = ws4.slot(of: key) - precondition(wSlot4 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let wBase4 = ws4.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + let wSlot4 = ws4.slot(of: key) + precondition(wSlot4 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.beginWrite(W4.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.endWrite(W4.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + let wBase4 = table.base(W4.self, at: W4.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) + } + } } } } @@ -752,35 +1203,59 @@ extension Family { _ rType3: R3.Type, _ body: (R1, R2, R3) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - var visited = 0 - defer { - sparse.lockDepth -= 1 - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3]) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + var visited = 0 + defer { + sparse.lockDepth -= 1 + } + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + defer { + archetype.lockDepth -= 1 + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + for row in 0 ..< count { + try body(rBase1[row], rBase2[row], rBase3[row]) + } + } } } } @@ -796,44 +1271,76 @@ extension Family { writing wType1: W1.Type, _ body: (R1, R2, R3, inout W1) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let wBase1 = ws1.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let wBase1 = ws1.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row]) + } + } } } } @@ -850,51 +1357,87 @@ extension Family { _ wType2: W2.Type, _ body: (R1, R2, R3, inout W1, inout W2) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1], &wBase2[wSlot2]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1], &wBase2[wSlot2]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row], &wBase2[row]) + } + } } } } @@ -912,58 +1455,98 @@ extension Family { _ wType3: W3.Type, _ body: (R1, R2, R3, inout W1, inout W2, inout W3) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row], &wBase2[row], &wBase3[row]) + } + } } } } @@ -982,7 +1565,6 @@ extension Family { _ wType4: W4.Type, _ body: (R1, R2, R3, inout W1, inout W2, inout W3, inout W4) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) @@ -990,57 +1572,102 @@ extension Family { requireRequired(W2.self) requireRequired(W3.self) requireRequired(W4.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.beginWrite(W4.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.endWrite(W4.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) - nexus.emitModified(W4.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.beginWrite(W4.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.endWrite(W4.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let wBase4 = ws4.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - let wSlot4 = ws4.slot(of: key) - precondition(wSlot4 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let wBase4 = ws4.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + let wSlot4 = ws4.slot(of: key) + precondition(wSlot4 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.beginWrite(W4.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.endWrite(W4.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + let wBase4 = table.base(W4.self, at: W4.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) + } + } } } } @@ -1056,40 +1683,66 @@ extension Family { _ rType4: R4.Type, _ body: (R1, R2, R3, R4) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) requireRequired(R4.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - sparse.requireReadable(R4.componentIndex) - var visited = 0 - defer { - sparse.lockDepth -= 1 - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let rBase4 = rs4.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let rSlot4 = rs4.slot(of: key) - precondition(rSlot4 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4]) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + sparse.requireReadable(R4.componentIndex) + var visited = 0 + defer { + sparse.lockDepth -= 1 + } + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let rBase4 = rs4.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let rSlot4 = rs4.slot(of: key) + precondition(rSlot4 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + archetype.requireReadable(R4.componentIndex) + defer { + archetype.lockDepth -= 1 + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let rBase4 = table.base(R4.self, at: R4.componentIndex) + for row in 0 ..< count { + try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row]) + } + } } } } @@ -1106,49 +1759,83 @@ extension Family { writing wType1: W1.Type, _ body: (R1, R2, R3, R4, inout W1) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) requireRequired(R4.self) requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - sparse.requireReadable(R4.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + sparse.requireReadable(R4.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let rBase4 = rs4.base - let wBase1 = ws1.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let rSlot4 = rs4.slot(of: key) - precondition(rSlot4 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let rBase4 = rs4.base + let wBase1 = ws1.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let rSlot4 = rs4.slot(of: key) + precondition(rSlot4 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + archetype.requireReadable(R4.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let rBase4 = table.base(R4.self, at: R4.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row]) + } + } } } } @@ -1166,56 +1853,94 @@ extension Family { _ wType2: W2.Type, _ body: (R1, R2, R3, R4, inout W1, inout W2) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) requireRequired(R4.self) requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - sparse.requireReadable(R4.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + sparse.requireReadable(R4.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let rBase4 = rs4.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let rSlot4 = rs4.slot(of: key) - precondition(rSlot4 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1], &wBase2[wSlot2]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let rBase4 = rs4.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let rSlot4 = rs4.slot(of: key) + precondition(rSlot4 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1], &wBase2[wSlot2]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + archetype.requireReadable(R4.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let rBase4 = table.base(R4.self, at: R4.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row], &wBase2[row]) + } + } } } } @@ -1234,7 +1959,6 @@ extension Family { _ wType3: W3.Type, _ body: (R1, R2, R3, R4, inout W1, inout W2, inout W3) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) @@ -1242,55 +1966,98 @@ extension Family { requireRequired(W1.self) requireRequired(W2.self) requireRequired(W3.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - sparse.requireReadable(R4.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + sparse.requireReadable(R4.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let rBase4 = rs4.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let rSlot4 = rs4.slot(of: key) - precondition(rSlot4 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let rBase4 = rs4.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let rSlot4 = rs4.slot(of: key) + precondition(rSlot4 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + archetype.requireReadable(R4.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let rBase4 = table.base(R4.self, at: R4.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row], &wBase2[row], &wBase3[row]) + } + } } } } @@ -1310,7 +2077,6 @@ extension Family { _ wType4: W4.Type, _ body: (R1, R2, R3, R4, inout W1, inout W2, inout W3, inout W4) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(R3.self) @@ -1319,61 +2085,108 @@ extension Family { requireRequired(W2.self) requireRequired(W3.self) requireRequired(W4.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - sparse.beginWrite(W3.componentIndex) - sparse.beginWrite(W4.componentIndex) - sparse.requireReadable(R1.componentIndex) - sparse.requireReadable(R2.componentIndex) - sparse.requireReadable(R3.componentIndex) - sparse.requireReadable(R4.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endWrite(W3.componentIndex) - sparse.endWrite(W4.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) - nexus.emitModified(W3.self, count: visited) - nexus.emitModified(W4.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let rs3 = sparse.existing(R3.self, at: R3.componentIndex), let rs4 = sparse.existing(R4.self, at: R4.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex), let ws3 = sparse.existing(W3.self, at: W3.componentIndex), let ws4 = sparse.existing(W4.self, at: W4.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + sparse.beginWrite(W3.componentIndex) + sparse.beginWrite(W4.componentIndex) + sparse.requireReadable(R1.componentIndex) + sparse.requireReadable(R2.componentIndex) + sparse.requireReadable(R3.componentIndex) + sparse.requireReadable(R4.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endWrite(W3.componentIndex) + sparse.endWrite(W4.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } } - } - let rBase1 = rs1.base - let rBase2 = rs2.base - let rBase3 = rs3.base - let rBase4 = rs4.base - let wBase1 = ws1.base - let wBase2 = ws2.base - let wBase3 = ws3.base - let wBase4 = ws4.base - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - for entry in entries { - let key = entry.key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let rSlot3 = rs3.slot(of: key) - precondition(rSlot3 >= 0, "member without a required component") - let rSlot4 = rs4.slot(of: key) - precondition(rSlot4 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - let wSlot3 = ws3.slot(of: key) - precondition(wSlot3 >= 0, "member without a required component") - let wSlot4 = ws4.slot(of: key) - precondition(wSlot4 >= 0, "member without a required component") - visited += 1 - try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + let rBase1 = rs1.base + let rBase2 = rs2.base + let rBase3 = rs3.base + let rBase4 = rs4.base + let wBase1 = ws1.base + let wBase2 = ws2.base + let wBase3 = ws3.base + let wBase4 = ws4.base + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + for entry in entries { + let key = entry.key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let rSlot3 = rs3.slot(of: key) + precondition(rSlot3 >= 0, "member without a required component") + let rSlot4 = rs4.slot(of: key) + precondition(rSlot4 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + let wSlot3 = ws3.slot(of: key) + precondition(wSlot3 >= 0, "member without a required component") + let wSlot4 = ws4.slot(of: key) + precondition(wSlot4 >= 0, "member without a required component") + visited += 1 + try body(rBase1[rSlot1], rBase2[rSlot2], rBase3[rSlot3], rBase4[rSlot4], &wBase1[wSlot1], &wBase2[wSlot2], &wBase3[wSlot3], &wBase4[wSlot4]) + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(W3.componentIndex) + archetype.beginWrite(W4.componentIndex) + archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(R3.componentIndex) + archetype.requireReadable(R4.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endWrite(W3.componentIndex) + archetype.endWrite(W4.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + nexus.emitModified(W3.self, count: visited) + nexus.emitModified(W4.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase3 = table.base(R3.self, at: R3.componentIndex) + let rBase4 = table.base(R4.self, at: R4.componentIndex) + let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase3 = table.base(W3.self, at: W3.componentIndex) + let wBase4 = table.base(W4.self, at: W4.componentIndex) + for row in 0 ..< count { + visited += 1 + try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) + } + } } } } From 038d73fbf07739ecbd11b86a854b61866034d212 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:11:19 +0100 Subject: [PATCH 05/12] Serve component spans from archetype tables forEachComponentSpan passes one batch per matching table, viewing the table's columns. The single-span componentSpan(reading:) and (writing:) view the column when one table holds all components of the type. When several tables hold them the span views a gathered copy, and the writing variant stores it back when the body returns, also if it throws. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- .../ArchetypeStorage+Access.swift | 42 + .../FirebladeECS/Family+ComponentSpan.swift | 784 +++++++++++------- .../FirebladeECS/Nexus+ComponentSpan.swift | 104 ++- 3 files changed, 642 insertions(+), 288 deletions(-) diff --git a/Sources/FirebladeECS/ArchetypeStorage+Access.swift b/Sources/FirebladeECS/ArchetypeStorage+Access.swift index d86d9dfd..aeee4b42 100644 --- a/Sources/FirebladeECS/ArchetypeStorage+Access.swift +++ b/Sources/FirebladeECS/ArchetypeStorage+Access.swift @@ -124,3 +124,45 @@ extension Nexus.ArchetypeStorage { tables.contains { $0.rowCount > 0 && $0.mask.contains(componentIndex) } } } + +extension Nexus.ArchetypeStorage { + /// How many components of a type exist, and the table that holds them all if it is only one. + @inlinable + func rowCount(of componentIndex: Int) -> (total: Int, onlyTable: Nexus.ArchetypeTable?) { + var total = 0 + var only: Nexus.ArchetypeTable? + var holders = 0 + for table in tables where table.rowCount > 0 && table.mask.contains(componentIndex) { + total += table.rowCount + only = table + holders += 1 + } + return (total, holders == 1 ? only : nil) + } + + /// Copies the components of a type from all tables into `buffer`, table by table, in row order. + @inlinable + func gather(_: C.Type, at componentIndex: Int, into buffer: UnsafeMutablePointer) { + var offset = 0 + for table in tables where table.rowCount > 0 { + guard let column = table.column(C.self, at: componentIndex) else { + continue + } + (buffer + offset).initialize(from: column.base, count: column.count) + offset += column.count + } + } + + /// Writes the buffer filled by ``gather(_:at:into:)`` (and changed since) back into the columns. + @inlinable + func scatter(_: C.Type, at componentIndex: Int, from buffer: UnsafeMutablePointer) { + var offset = 0 + for table in tables where table.rowCount > 0 { + guard let column = table.column(C.self, at: componentIndex) else { + continue + } + column.base.update(from: buffer + offset, count: column.count) + offset += column.count + } + } +} diff --git a/Sources/FirebladeECS/Family+ComponentSpan.swift b/Sources/FirebladeECS/Family+ComponentSpan.swift index 7c631a73..0f76713b 100644 --- a/Sources/FirebladeECS/Family+ComponentSpan.swift +++ b/Sources/FirebladeECS/Family+ComponentSpan.swift @@ -17,38 +17,62 @@ extension Family { writing wType1: W1.Type, _ body: (inout MutableSpan) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard ws1.slot(of: next) == wSlot1 + length else { break } - length += 1 + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard ws1.slot(of: next) == wSlot1 + length else { break } + length += 1 + } + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) + visited += length + try body(&wSpan1) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + visited += count + try body(&wSpan1) } - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) - visited += length - try body(&wSpan1) - start += length } } } @@ -62,45 +86,73 @@ extension Family { _ wType2: W2.Type, _ body: (inout MutableSpan, inout MutableSpan) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard ws1.slot(of: next) == wSlot1 + length, ws2.slot(of: next) == wSlot2 + length else { break } - length += 1 + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard ws1.slot(of: next) == wSlot1 + length, ws2.slot(of: next) == wSlot2 + length else { break } + length += 1 + } + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws2.base + wSlot2, count: length)) + visited += length + try body(&wSpan1, &wSpan2) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: W2.componentIndex), count: count)) + visited += count + try body(&wSpan1, &wSpan2) } - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) - var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws2.base + wSlot2, count: length)) - visited += length - try body(&wSpan1, &wSpan2) - start += length } } } @@ -113,35 +165,54 @@ extension Family { reading rType1: R1.Type, _ body: (Span) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex) else { return } - sparse.lockDepth += 1 - let wasReading1 = sparse.beginRead(R1.componentIndex) - var visited = 0 - defer { - sparse.endRead(R1.componentIndex, wasReading: wasReading1) - sparse.lockDepth -= 1 - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard rs1.slot(of: next) == rSlot1 + length else { break } - length += 1 + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex) else { return } + sparse.lockDepth += 1 + let wasReading1 = sparse.beginRead(R1.componentIndex) + var visited = 0 + defer { + sparse.endRead(R1.componentIndex, wasReading: wasReading1) + sparse.lockDepth -= 1 + } + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard rs1.slot(of: next) == rSlot1 + length else { break } + length += 1 + } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) + visited += length + try body(rSpan1) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + let wasReading1 = archetype.beginRead(R1.componentIndex) + defer { + archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.lockDepth -= 1 + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + try body(rSpan1) } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) - visited += length - try body(rSpan1) - start += length } } } @@ -166,8 +237,11 @@ extension Family { /// copied or allocated, so a batch is a maximal run of members whose components are consecutive in every involved /// array. Entities that were created together and never had components removed form one big batch; after /// churn (removing components swap-removes) the batches get shorter, down to 1 element per call in the worst case. - /// Write `body` so that it is correct for any batch size, and do not rely on the number of calls. Storages that - /// keep the columns of a family aligned (archetype tables, owning groups) will always pass one batch per table. + /// Write `body` so that it is correct for any batch size, and do not rely on the number of calls. + /// + /// The archetype storage keeps the columns of a table aligned, so it passes exactly one batch per table that matches + /// the family and has members, in table creation order. Entities with the same component set form one batch, however + /// the component set came about, and a family whose members have several component sets gets several calls. /// /// ### Rules /// @@ -186,44 +260,71 @@ extension Family { writing wType1: W1.Type, _ body: (Span, inout MutableSpan) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - let wasReading1 = sparse.beginRead(R1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endRead(R1.componentIndex, wasReading: wasReading1) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + let wasReading1 = sparse.beginRead(R1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endRead(R1.componentIndex, wasReading: wasReading1) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard rs1.slot(of: next) == rSlot1 + length, ws1.slot(of: next) == wSlot1 + length else { break } - length += 1 + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard rs1.slot(of: next) == rSlot1 + length, ws1.slot(of: next) == wSlot1 + length else { break } + length += 1 + } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) + visited += length + try body(rSpan1, &wSpan1) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + let wasReading1 = archetype.beginRead(R1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + visited += count + try body(rSpan1, &wSpan1) } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) - visited += length - try body(rSpan1, &wSpan1) - start += length } } } @@ -238,51 +339,82 @@ extension Family { _ wType2: W2.Type, _ body: (Span, inout MutableSpan, inout MutableSpan) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - let wasReading1 = sparse.beginRead(R1.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endRead(R1.componentIndex, wasReading: wasReading1) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + let wasReading1 = sparse.beginRead(R1.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endRead(R1.componentIndex, wasReading: wasReading1) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard rs1.slot(of: next) == rSlot1 + length, ws1.slot(of: next) == wSlot1 + length, ws2.slot(of: next) == wSlot2 + length else { break } - length += 1 + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard rs1.slot(of: next) == rSlot1 + length, ws1.slot(of: next) == wSlot1 + length, ws2.slot(of: next) == wSlot2 + length else { break } + length += 1 + } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws2.base + wSlot2, count: length)) + visited += length + try body(rSpan1, &wSpan1, &wSpan2) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + let wasReading1 = archetype.beginRead(R1.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: W2.componentIndex), count: count)) + visited += count + try body(rSpan1, &wSpan1, &wSpan2) } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) - var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws2.base + wSlot2, count: length)) - visited += length - try body(rSpan1, &wSpan1, &wSpan2) - start += length } } } @@ -296,41 +428,63 @@ extension Family { _ rType2: R2.Type, _ body: (Span, Span) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex) else { return } - sparse.lockDepth += 1 - let wasReading1 = sparse.beginRead(R1.componentIndex) - let wasReading2 = sparse.beginRead(R2.componentIndex) - var visited = 0 - defer { - sparse.endRead(R1.componentIndex, wasReading: wasReading1) - sparse.endRead(R2.componentIndex, wasReading: wasReading2) - sparse.lockDepth -= 1 - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard rs1.slot(of: next) == rSlot1 + length, rs2.slot(of: next) == rSlot2 + length else { break } - length += 1 + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex) else { return } + sparse.lockDepth += 1 + let wasReading1 = sparse.beginRead(R1.componentIndex) + let wasReading2 = sparse.beginRead(R2.componentIndex) + var visited = 0 + defer { + sparse.endRead(R1.componentIndex, wasReading: wasReading1) + sparse.endRead(R2.componentIndex, wasReading: wasReading2) + sparse.lockDepth -= 1 + } + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard rs1.slot(of: next) == rSlot1 + length, rs2.slot(of: next) == rSlot2 + length else { break } + length += 1 + } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: rs2.base + rSlot2, count: length)) + visited += length + try body(rSpan1, rSpan2) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + let wasReading1 = archetype.beginRead(R1.componentIndex) + let wasReading2 = archetype.beginRead(R2.componentIndex) + defer { + archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.endRead(R2.componentIndex, wasReading: wasReading2) + archetype.lockDepth -= 1 + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: R2.componentIndex), count: count)) + try body(rSpan1, rSpan2) } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) - let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: rs2.base + rSlot2, count: length)) - visited += length - try body(rSpan1, rSpan2) - start += length } } } @@ -345,50 +499,80 @@ extension Family { writing wType1: W1.Type, _ body: (Span, Span, inout MutableSpan) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(W1.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - let wasReading1 = sparse.beginRead(R1.componentIndex) - let wasReading2 = sparse.beginRead(R2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endRead(R1.componentIndex, wasReading: wasReading1) - sparse.endRead(R2.componentIndex, wasReading: wasReading2) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + let wasReading1 = sparse.beginRead(R1.componentIndex) + let wasReading2 = sparse.beginRead(R2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endRead(R1.componentIndex, wasReading: wasReading1) + sparse.endRead(R2.componentIndex, wasReading: wasReading2) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } } - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard rs1.slot(of: next) == rSlot1 + length, rs2.slot(of: next) == rSlot2 + length, ws1.slot(of: next) == wSlot1 + length else { break } - length += 1 + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard rs1.slot(of: next) == rSlot1 + length, rs2.slot(of: next) == rSlot2 + length, ws1.slot(of: next) == wSlot1 + length else { break } + length += 1 + } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: rs2.base + rSlot2, count: length)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) + visited += length + try body(rSpan1, rSpan2, &wSpan1) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + let wasReading1 = archetype.beginRead(R1.componentIndex) + let wasReading2 = archetype.beginRead(R2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.endRead(R2.componentIndex, wasReading: wasReading2) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: R2.componentIndex), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + visited += count + try body(rSpan1, rSpan2, &wSpan1) } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) - let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: rs2.base + rSlot2, count: length)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) - visited += length - try body(rSpan1, rSpan2, &wSpan1) - start += length } } } @@ -404,57 +588,91 @@ extension Family { _ wType2: W2.Type, _ body: (Span, Span, inout MutableSpan, inout MutableSpan) throws -> Void ) rethrows { - guard case let .sparseSet(sparse) = nexus.backend else { return } requireRequired(R1.self) requireRequired(R2.self) requireRequired(W1.self) requireRequired(W2.self) - let members = memberIds - guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } - sparse.lockDepth += 1 - sparse.beginWrite(W1.componentIndex) - sparse.beginWrite(W2.componentIndex) - let wasReading1 = sparse.beginRead(R1.componentIndex) - let wasReading2 = sparse.beginRead(R2.componentIndex) - var visited = 0 - defer { - sparse.endWrite(W1.componentIndex) - sparse.endWrite(W2.componentIndex) - sparse.endRead(R1.componentIndex, wasReading: wasReading1) - sparse.endRead(R2.componentIndex, wasReading: wasReading2) - sparse.lockDepth -= 1 - if visited > 0 { - nexus.emitModified(W1.self, count: visited) - nexus.emitModified(W2.self, count: visited) + switch nexus.backend { + case let .sparseSet(sparse): + let members = memberIds + guard !members.isEmpty, let rs1 = sparse.existing(R1.self, at: R1.componentIndex), let rs2 = sparse.existing(R2.self, at: R2.componentIndex), let ws1 = sparse.existing(W1.self, at: W1.componentIndex), let ws2 = sparse.existing(W2.self, at: W2.componentIndex) else { return } + sparse.lockDepth += 1 + sparse.beginWrite(W1.componentIndex) + sparse.beginWrite(W2.componentIndex) + let wasReading1 = sparse.beginRead(R1.componentIndex) + let wasReading2 = sparse.beginRead(R2.componentIndex) + var visited = 0 + defer { + sparse.endWrite(W1.componentIndex) + sparse.endWrite(W2.componentIndex) + sparse.endRead(R1.componentIndex, wasReading: wasReading1) + sparse.endRead(R2.componentIndex, wasReading: wasReading2) + sparse.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } } - } - let entries = members.storage.dense - try entries.withUnsafeBufferPointer { entries in - let total = entries.count - var start = 0 - while start < total { - let key = entries[start].key - let rSlot1 = rs1.slot(of: key) - precondition(rSlot1 >= 0, "member without a required component") - let rSlot2 = rs2.slot(of: key) - precondition(rSlot2 >= 0, "member without a required component") - let wSlot1 = ws1.slot(of: key) - precondition(wSlot1 >= 0, "member without a required component") - let wSlot2 = ws2.slot(of: key) - precondition(wSlot2 >= 0, "member without a required component") - var length = 1 - while start + length < total { - let next = entries[start + length].key - guard rs1.slot(of: next) == rSlot1 + length, rs2.slot(of: next) == rSlot2 + length, ws1.slot(of: next) == wSlot1 + length, ws2.slot(of: next) == wSlot2 + length else { break } - length += 1 + let entries = members.storage.dense + try entries.withUnsafeBufferPointer { entries in + let total = entries.count + var start = 0 + while start < total { + let key = entries[start].key + let rSlot1 = rs1.slot(of: key) + precondition(rSlot1 >= 0, "member without a required component") + let rSlot2 = rs2.slot(of: key) + precondition(rSlot2 >= 0, "member without a required component") + let wSlot1 = ws1.slot(of: key) + precondition(wSlot1 >= 0, "member without a required component") + let wSlot2 = ws2.slot(of: key) + precondition(wSlot2 >= 0, "member without a required component") + var length = 1 + while start + length < total { + let next = entries[start + length].key + guard rs1.slot(of: next) == rSlot1 + length, rs2.slot(of: next) == rSlot2 + length, ws1.slot(of: next) == wSlot1 + length, ws2.slot(of: next) == wSlot2 + length else { break } + length += 1 + } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: rs2.base + rSlot2, count: length)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws2.base + wSlot2, count: length)) + visited += length + try body(rSpan1, rSpan2, &wSpan1, &wSpan2) + start += length + } + } + case let .archetype(archetype): + guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } + let query = archetype.matchingTables(filter) + archetype.lockDepth += 1 + archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(W2.componentIndex) + let wasReading1 = archetype.beginRead(R1.componentIndex) + let wasReading2 = archetype.beginRead(R2.componentIndex) + var visited = 0 + defer { + archetype.endWrite(W1.componentIndex) + archetype.endWrite(W2.componentIndex) + archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.endRead(R2.componentIndex, wasReading: wasReading2) + archetype.lockDepth -= 1 + if visited > 0 { + nexus.emitModified(W1.self, count: visited) + nexus.emitModified(W2.self, count: visited) + } + } + for entry in query.tables { + try entry._withUnsafeGuaranteedRef { table in + let count = table.entities.count + guard count > 0 else { return } + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: R2.componentIndex), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: W2.componentIndex), count: count)) + visited += count + try body(rSpan1, rSpan2, &wSpan1, &wSpan2) } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: rs1.base + rSlot1, count: length)) - let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: rs2.base + rSlot2, count: length)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws1.base + wSlot1, count: length)) - var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: ws2.base + wSlot2, count: length)) - visited += length - try body(rSpan1, rSpan2, &wSpan1, &wSpan2) - start += length } } } diff --git a/Sources/FirebladeECS/Nexus+ComponentSpan.swift b/Sources/FirebladeECS/Nexus+ComponentSpan.swift index 1ff1229e..7eb6b089 100644 --- a/Sources/FirebladeECS/Nexus+ComponentSpan.swift +++ b/Sources/FirebladeECS/Nexus+ComponentSpan.swift @@ -32,6 +32,15 @@ extension Nexus { /// /// An empty storage (or a type no entity ever had) passes an empty span. /// + /// ### Storage + /// + /// The sparse-set storage keeps all components of a type in one array, so the span always views the storage itself. + /// The archetype storage keeps one column per table (one table per component set), so entities with different + /// component sets are not next to each other. If one table holds all components of the type the span views its column, + /// as above. If several tables hold them, the span views a temporary copy that is made table by table, which costs + /// one allocation and O(N) copying per call and is released after `body`. Use + /// ``Family/forEachComponentSpan(reading:_:)`` to get zero copy spans, one batch per table. + /// /// - Parameters: /// - type: The component type. /// - body: Receives the span; its result is returned. @@ -39,9 +48,17 @@ extension Nexus { @inlinable @discardableResult public func componentSpan(reading type: C.Type, _ body: (Span) throws -> Result) rethrows -> Result { - guard case let .sparseSet(sparse) = backend else { - return try body(Span()) + switch backend { + case let .sparseSet(sparse): + try componentSpan(in: sparse, reading: type, body) + + case let .archetype(archetype): + try componentSpan(in: archetype, reading: type, body) } + } + + @inlinable + func componentSpan(in sparse: SparseSetStorage, reading _: C.Type, _ body: (Span) throws -> Result) rethrows -> Result { let componentIndex = C.componentIndex guard let storage = sparse.existing(C.self, at: componentIndex), !storage.isEmpty else { sparse.requireReadable(componentIndex) @@ -57,6 +74,35 @@ extension Nexus { return try body(span) } + @inlinable + func componentSpan(in archetype: ArchetypeStorage, reading _: C.Type, _ body: (Span) throws -> Result) rethrows -> Result { + let componentIndex = C.componentIndex + let rows = archetype.rowCount(of: componentIndex) + guard rows.total > 0 else { + archetype.requireReadable(componentIndex) + return try body(Span()) + } + let wasReading = archetype.beginRead(componentIndex) + archetype.lockDepth += 1 + defer { + archetype.lockDepth -= 1 + archetype.endRead(componentIndex, wasReading: wasReading) + } + if let table = rows.onlyTable { + // one table holds all components: view its column + let span = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(C.self, at: componentIndex), count: rows.total)) + return try body(span) + } + // the components are spread over several tables: view a copy + let copy = UnsafeMutableBufferPointer.allocate(capacity: rows.total) + archetype.gather(C.self, at: componentIndex, into: copy.baseAddress.unsafelyUnwrapped) + defer { + copy.baseAddress.unsafelyUnwrapped.deinitialize(count: rows.total) + copy.deallocate() + } + return try body(Span(_unsafeElements: UnsafeBufferPointer(copy))) + } + /// Gives `body` write access to all components of a type as one contiguous `MutableSpan`. /// /// Writes through the span land in the storage. Like with ``componentSpan(reading:_:)`` the span is zero copy and @@ -75,6 +121,12 @@ extension Nexus { /// A delegate subscribed to ``NexusEventMask/valueChanges`` receives one ``ComponentsModified`` after `body` returned /// (also when it threw), not one event per component. An empty storage passes an empty span and emits nothing. /// + /// ### Storage + /// + /// With the archetype storage, a type that lives in several tables (entities with different component sets) is written + /// through a temporary copy, as ``componentSpan(reading:_:)`` describes: the writes reach the components when `body` + /// returns, also if it throws. Nothing can observe the difference meanwhile, because the type is locked for writing. + /// /// - Parameters: /// - type: The component type. /// - body: Receives the span; its result is returned. @@ -82,10 +134,17 @@ extension Nexus { @inlinable @discardableResult public func componentSpan(writing type: C.Type, _ body: (inout MutableSpan) throws -> Result) rethrows -> Result { - guard case let .sparseSet(sparse) = backend else { - var empty = MutableSpan() - return try body(&empty) + switch backend { + case let .sparseSet(sparse): + try componentSpan(in: sparse, writing: type, body) + + case let .archetype(archetype): + try componentSpan(in: archetype, writing: type, body) } + } + + @inlinable + func componentSpan(in sparse: SparseSetStorage, writing _: C.Type, _ body: (inout MutableSpan) throws -> Result) rethrows -> Result { let componentIndex = C.componentIndex guard let storage = sparse.existing(C.self, at: componentIndex), !storage.isEmpty else { sparse.beginWrite(componentIndex) @@ -105,4 +164,39 @@ extension Nexus { var span = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: storage.base, count: count)) return try body(&span) } + + @inlinable + func componentSpan(in archetype: ArchetypeStorage, writing _: C.Type, _ body: (inout MutableSpan) throws -> Result) rethrows -> Result { + let componentIndex = C.componentIndex + let rows = archetype.rowCount(of: componentIndex) + guard rows.total > 0 else { + archetype.beginWrite(componentIndex) + archetype.endWrite(componentIndex) + var empty = MutableSpan() + return try body(&empty) + } + archetype.beginWrite(componentIndex) + archetype.lockDepth += 1 + defer { + archetype.lockDepth -= 1 + archetype.endWrite(componentIndex) + // after the write access ended, so a delegate may read the components + emitModified(C.self, count: rows.total) + } + if let table = rows.onlyTable { + // one table holds all components: write through to its column + var span = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(C.self, at: componentIndex), count: rows.total)) + return try body(&span) + } + // the components are spread over several tables: write to a copy and store it back, also if `body` throws + let copy = UnsafeMutableBufferPointer.allocate(capacity: rows.total) + archetype.gather(C.self, at: componentIndex, into: copy.baseAddress.unsafelyUnwrapped) + defer { + archetype.scatter(C.self, at: componentIndex, from: copy.baseAddress.unsafelyUnwrapped) + copy.baseAddress.unsafelyUnwrapped.deinitialize(count: rows.total) + copy.deallocate() + } + var span = MutableSpan(_unsafeElements: copy) + return try body(&span) + } } From 85a2f0c2ffcef3b9777e611b51fb544d86ebe87e Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:18:38 +0100 Subject: [PATCH 06/12] Separate the switch cases of the storage dispatch with blank lines Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Sources/FirebladeECS/Nexus+Storage.swift | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/FirebladeECS/Nexus+Storage.swift b/Sources/FirebladeECS/Nexus+Storage.swift index 9b915e34..d6c3ccc6 100644 --- a/Sources/FirebladeECS/Nexus+Storage.swift +++ b/Sources/FirebladeECS/Nexus+Storage.swift @@ -35,6 +35,7 @@ extension Nexus { switch kind { case .sparseSet: self = .sparseSet(SparseSetStorage()) + case .archetype: self = .archetype(ArchetypeStorage()) } @@ -66,6 +67,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entityCount + case let .archetype(storage): storage.entityCount } @@ -75,6 +77,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.totalComponentCount + case let .archetype(storage): storage.totalComponentCount } @@ -84,6 +87,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entities.livingIds + case let .archetype(storage): storage.entities.livingIds } @@ -94,6 +98,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.insertEntity(entityId) + case let .archetype(storage): storage.insertEntity(entityId) } @@ -105,6 +110,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.destroyEntity(entityId) + case let .archetype(storage): storage.destroyEntity(entityId) } @@ -115,6 +121,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.isAlive(entityId) + case let .archetype(storage): storage.isAlive(entityId) } @@ -125,6 +132,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entities.current(at: index) + case let .archetype(storage): storage.entities.current(at: index) } @@ -135,6 +143,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.removeAll() + case let .archetype(storage): storage.removeAll() } @@ -145,6 +154,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.set(component, at: componentIndex, for: entityId) + case let .archetype(storage): storage.set(component, at: componentIndex, for: entityId) } @@ -160,6 +170,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): try storage.modify(type, for: entityId, body) + case let .archetype(storage): try storage.modify(type, for: entityId, body) } @@ -170,6 +181,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.beginAccess(type, for: entityId) + case let .archetype(storage): storage.beginAccess(type, for: entityId) } @@ -180,6 +192,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.endAccess(componentIndex) + case let .archetype(storage): storage.endAccess(componentIndex) } @@ -190,6 +203,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.discardMovedOut(type, for: entityId) + case let .archetype(storage): storage.discardMovedOut(type, for: entityId) } @@ -200,6 +214,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.withComponents(of: type, body) + case let .archetype(storage): storage.withComponents(of: type, body) } @@ -210,6 +225,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.get(type, at: componentIndex, for: entityId) + case let .archetype(storage): storage.get(type, at: componentIndex, for: entityId) } @@ -225,6 +241,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.unsafeGet(type, at: componentIndex, for: entityId) + case let .archetype(storage): storage.unsafeGet(type, at: componentIndex, for: entityId) } @@ -240,6 +257,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.hasStorage(for: componentIndex) + case let .archetype(storage): storage.hasStorage(for: componentIndex) } @@ -250,6 +268,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.has(componentIndex: componentIndex, for: entityId) + case let .archetype(storage): storage.has(componentIndex: componentIndex, for: entityId) } @@ -260,6 +279,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.entities.mask(of: entityId) + case let .archetype(storage): storage.entities.mask(of: entityId) } @@ -270,6 +290,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.componentIndices(of: entityId) + case let .archetype(storage): storage.componentIndices(of: entityId) } @@ -280,6 +301,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.componentCount(of: entityId) + case let .archetype(storage): storage.componentCount(of: entityId) } @@ -290,6 +312,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.indexByIdentifier[identifier] + case let .archetype(storage): storage.indexByIdentifier[identifier] } @@ -300,6 +323,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.identifier(ofComponentIndex: componentIndex) + case let .archetype(storage): storage.identifier(ofComponentIndex: componentIndex) } @@ -311,6 +335,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.remove(componentIndex: componentIndex, from: entityId) + case let .archetype(storage): storage.remove(componentIndex: componentIndex, from: entityId) } @@ -321,6 +346,7 @@ extension Nexus.Backend { switch self { case let .sparseSet(storage): storage.anyComponent(componentIndex: componentIndex, for: entityId) + case let .archetype(storage): storage.anyComponent(componentIndex: componentIndex, for: entityId) } From 4ab6976db1819eba144daa5df501c33f571b3a29 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:18:38 +0100 Subject: [PATCH 07/12] Run the storage dependent tests on both storages The tests that create a Nexus take the storage kind as an argument (@Test(arguments: Nexus.StorageKind.allCases)), so one body checks the sparse-set and the archetype storage, including the exit tests for traps. The direct storage tests run on the storage enum. New cases cover churn across component sets against a model, forEach and bulk writes over several tables, single and per-table spans, and table reuse. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- .../ComponentClassTrapTests.swift | 18 +- .../ComponentIndexTests.swift | 6 +- .../ComponentRegistryTests.swift | 36 +-- .../ComponentSpanTests.swift | 141 +++++++--- .../ComponentSpanTrapTests.swift | 202 ++++++++++---- .../ComponentStorageReentrancyTests.swift | 42 +-- .../ComponentStorageSafetyTests.swift | 186 +++++++++---- .../ComponentStorageTests.swift | 74 +++-- .../FirebladeECSTests/EntityCodingTests.swift | 16 +- .../EntityCreationTests.swift | 16 +- Tests/FirebladeECSTests/EntityTests.swift | 36 +-- Tests/FirebladeECSTests/FSMTests.swift | 40 +-- .../FirebladeECSTests/FamilyCodingTests.swift | 44 +-- .../FamilyReadWriteTests.swift | 68 ++--- .../FamilyReadWriteTrapTests.swift | 148 +++++++--- Tests/FirebladeECSTests/FamilyTests1.swift | 12 +- Tests/FirebladeECSTests/FamilyTests2.swift | 8 +- Tests/FirebladeECSTests/FamilyTests3.swift | 8 +- Tests/FirebladeECSTests/FamilyTests4.swift | 8 +- Tests/FirebladeECSTests/FamilyTests5.swift | 8 +- Tests/FirebladeECSTests/FamilyTests6.swift | 8 +- Tests/FirebladeECSTests/FamilyTests7.swift | 8 +- Tests/FirebladeECSTests/FamilyTests8.swift | 8 +- .../FamilyTraitsProvidingTests.swift | 22 +- .../FirebladeECSTests/FamilyTraitsTests.swift | 4 +- Tests/FirebladeECSTests/GenerationTests.swift | 44 +-- .../InspectableComponentTests.swift | 4 +- Tests/FirebladeECSTests/MutationTests.swift | 260 +++++++++++------- Tests/FirebladeECSTests/NexusCloneTests.swift | 32 +-- .../NexusDeterministicOrderTests.swift | 8 +- .../NexusEventDelegateTests.swift | 24 +- .../NexusFamilyEdgeCaseTests.swift | 26 +- .../NexusSerializationTests.swift | 86 +++--- .../NexusSnapshotTests.swift | 40 +-- .../FirebladeECSTests/NexusStorageTests.swift | 209 +++++++++++++- Tests/FirebladeECSTests/NexusTests.swift | 28 +- .../NexusValueChangeEventTests.swift | 36 +-- Tests/FirebladeECSTests/SingleTests.swift | 36 +-- .../SnapshotV1CompatTests.swift | 34 +-- Tests/FirebladeECSTests/SystemsTests.swift | 4 +- 40 files changed, 1311 insertions(+), 727 deletions(-) diff --git a/Tests/FirebladeECSTests/ComponentClassTrapTests.swift b/Tests/FirebladeECSTests/ComponentClassTrapTests.swift index 937714f4..421578ab 100644 --- a/Tests/FirebladeECSTests/ComponentClassTrapTests.swift +++ b/Tests/FirebladeECSTests/ComponentClassTrapTests.swift @@ -16,16 +16,24 @@ import Testing private final class ClassComponent: Component, @unchecked Sendable {} @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func addingAClassComponentAssertsBody() { - let nexus = Nexus() + private func addingAClassComponentAssertsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.createEntity(with: ClassComponent()) } @Suite struct ComponentClassTrapTests { @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func addingAClassComponentAsserts() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - addingAClassComponentAssertsBody() + @Test(arguments: Nexus.StorageKind.allCases) func addingAClassComponentAsserts(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + addingAClassComponentAssertsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + addingAClassComponentAssertsBody(.archetype) + } } let stderr = String(decoding: result?.standardErrorContent ?? [], as: UTF8.self) #expect(stderr.contains("components must be structs or enums")) diff --git a/Tests/FirebladeECSTests/ComponentIndexTests.swift b/Tests/FirebladeECSTests/ComponentIndexTests.swift index 840a3cbd..295f3aad 100644 --- a/Tests/FirebladeECSTests/ComponentIndexTests.swift +++ b/Tests/FirebladeECSTests/ComponentIndexTests.swift @@ -27,9 +27,9 @@ private struct ConcurrentlyIndexed: Component, Sendable {} #expect(indices.allSatisfy { $0 >= 0 && $0 < Nexus.ComponentIndexRegistry.shared.count }) } - @Test func indicesAreProcessWide() { - let first = Nexus() - let second = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func indicesAreProcessWide(_ storage: Nexus.StorageKind) { + let first = Nexus(storage: storage) + let second = Nexus(storage: storage) first.createEntity().set(IndexA()) second.createEntity().set(IndexB()) second.createEntity().set(IndexA()) diff --git a/Tests/FirebladeECSTests/ComponentRegistryTests.swift b/Tests/FirebladeECSTests/ComponentRegistryTests.swift index 54e9063f..4ef52160 100644 --- a/Tests/FirebladeECSTests/ComponentRegistryTests.swift +++ b/Tests/FirebladeECSTests/ComponentRegistryTests.swift @@ -35,8 +35,8 @@ struct ConflictingVelocity: RegistrableComponent, Sendable { #expect(RenamedVelocity.stableIdentifier != RegisteredPosition.stableIdentifier) } - @Test func explicitRegistration() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func explicitRegistration(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) #expect(!nexus.isRegistered(RegisteredPosition.self)) let record = try nexus.register(RegisteredPosition.self) @@ -50,28 +50,28 @@ struct ConflictingVelocity: RegistrableComponent, Sendable { #expect(nexus.componentType(named: "Unknown") == nil) } - @Test func registrationViaStaticMethod() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func registrationViaStaticMethod(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try RenamedVelocity.register(in: nexus) #expect(nexus.isRegistered(RenamedVelocity.self)) #expect(nexus.componentType(named: "Velocity")?.isDefaultInitializable == false) } - @Test func registeringSequenceIsSortedByName() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func registeringSequenceIsSortedByName(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register([RegisteredPosition.self, RenamedVelocity.self]) #expect(nexus.registeredComponentTypes.map(\.typeName) == ["FirebladeECSTests.RegisteredPosition", "Velocity"]) } - @Test func reRegistrationIsNoOp() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func reRegistrationIsNoOp(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register(RegisteredPosition.self) try nexus.register(RegisteredPosition.self) #expect(nexus.registeredComponentTypes.count == 1) } - @Test func duplicateTypeNameThrows() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func duplicateTypeNameThrows(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register(RenamedVelocity.self) #expect(throws: ComponentRegistryError.duplicateTypeName("Velocity")) { try nexus.register(ConflictingVelocity.self) @@ -79,34 +79,34 @@ struct ConflictingVelocity: RegistrableComponent, Sendable { #expect(!nexus.isRegistered(ConflictingVelocity.self)) } - @Test func registersOnFirstAssignment() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func registersOnFirstAssignment(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.createEntity(with: RegisteredPosition()) #expect(nexus.isRegistered(RegisteredPosition.self)) } - @Test func plainComponentsAreNotRegistered() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func plainComponentsAreNotRegistered(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.createEntity(with: Position(x: 1, y: 2)) #expect(!nexus.isRegistered(Position.self)) #expect(nexus.registeredComponentTypes.isEmpty) } - @Test func registrySurvivesClear() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func registrySurvivesClear(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register(RegisteredPosition.self) nexus.clear() #expect(nexus.isRegistered(RegisteredPosition.self)) } - @Test func autoRegistrationConflictReportsNonFatalError() { + @Test(arguments: Nexus.StorageKind.allCases) func autoRegistrationConflictReportsNonFatalError(_ storage: Nexus.StorageKind) { final class Recorder: NexusEventDelegate, @unchecked Sendable { var errors: [String] = [] func nexusEvent(_: NexusEvent) {} func nexusNonFatalError(_ message: String) { errors.append(message) } } let recorder = Recorder() - let nexus = Nexus() + let nexus = Nexus(storage: storage) nexus.delegate = recorder nexus.createEntity(with: RenamedVelocity()) nexus.createEntity(with: ConflictingVelocity()) diff --git a/Tests/FirebladeECSTests/ComponentSpanTests.swift b/Tests/FirebladeECSTests/ComponentSpanTests.swift index 755fc4d2..d10a9da6 100644 --- a/Tests/FirebladeECSTests/ComponentSpanTests.swift +++ b/Tests/FirebladeECSTests/ComponentSpanTests.swift @@ -20,8 +20,8 @@ private final class Recorder: NexusEventDelegate, @unchecked Sendable { } /// `count` entities with `Comp1(number)`, `Comp2(number)` and `Comp3(0)`. -private func makeNexus(count: Int) -> Nexus { - let nexus = Nexus() +private func makeNexus(_ storage: Nexus.StorageKind, count: Int) -> Nexus { + let nexus = Nexus(storage: storage) for number in 0 ..< count { nexus.createEntity(with: Comp1(number), Comp2(number), Comp3(0)) } @@ -32,8 +32,8 @@ private func makeNexus(count: Int) -> Nexus { // MARK: single type @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func readingSpanSeesAllComponents() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func readingSpanSeesAllComponents(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) var sum = 0 var count = 0 nexus.componentSpan(reading: Comp1.self) { (span: Span) in @@ -45,8 +45,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func readingSpanReturnsTheResultOfTheBody() { - let nexus = makeNexus(count: 4) + @Test(arguments: Nexus.StorageKind.allCases) func readingSpanReturnsTheResultOfTheBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 4) let total = nexus.componentSpan(reading: Comp1.self) { span in var sum = 0 for index in span.indices { sum += span[index].value } @@ -56,8 +56,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func writingSpanLandsInStorage() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func writingSpanLandsInStorage(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) nexus.componentSpan(writing: Comp1.self) { (span: inout MutableSpan) in for index in span.indices { span[index].value *= 10 } } @@ -66,8 +66,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func writingSpanErrorsPropagateAfterTheWrite() { - let nexus = makeNexus(count: 3) + @Test(arguments: Nexus.StorageKind.allCases) func writingSpanErrorsPropagateAfterTheWrite(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 3) let recorder = Recorder() nexus.delegate = recorder #expect(throws: Boom.self) { @@ -83,8 +83,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func emptyStorageGivesEmptySpansAndNoEvent() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func emptyStorageGivesEmptySpansAndNoEvent(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = Recorder() nexus.delegate = recorder // a type that never had a storage @@ -99,8 +99,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func spansAfterSwapRemoveChurn() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func spansAfterSwapRemoveChurn(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) let entities = Array(nexus.family(requires: Comp1.self).entities) for entity in entities where entity.get(Comp1.self)!.value.isMultiple(of: 3) { entity.remove(Comp1.self) @@ -118,8 +118,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func writingSpanEmitsOneEventPerPass() { - let nexus = makeNexus(count: 7) + @Test(arguments: Nexus.StorageKind.allCases) func writingSpanEmitsOneEventPerPass(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 7) let recorder = Recorder() nexus.delegate = recorder nexus.componentSpan(writing: Comp1.self) { span in @@ -133,8 +133,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func readingOtherTypesAndNestingReadsIsAllowed() { - let nexus = makeNexus(count: 5) + @Test(arguments: Nexus.StorageKind.allCases) func readingOtherTypesAndNestingReadsIsAllowed(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 5) var sum = 0 nexus.componentSpan(writing: Comp1.self) { writing in nexus.componentSpan(reading: Comp2.self) { reading in @@ -159,8 +159,8 @@ private func makeNexus(count: Int) -> Nexus { // MARK: family batches @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpansPairElementsOfTheSameMember() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func familySpansPairElementsOfTheSameMember(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) var visited = 0 family.forEachComponentSpan(reading: Comp1.self, writing: Comp2.self) { (reads: Span, writes: inout MutableSpan) in @@ -178,8 +178,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpansAreOneBatchWhenColumnsAreAligned() { - let nexus = makeNexus(count: 100) + @Test(arguments: Nexus.StorageKind.allCases) func familySpansAreOneBatchWhenColumnsAreAligned(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 100) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) var calls = 0 family.forEachComponentSpan(reading: Comp1.self, writing: Comp2.self) { reads, _ in @@ -191,8 +191,8 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpansAfterChurnVisitEveryMemberOnce() { - let nexus = makeNexus(count: 40) + @Test(arguments: Nexus.StorageKind.allCases) func familySpansAfterChurnVisitEveryMemberOnce(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 40) let all = Array(nexus.family(requires: Comp1.self).entities) for entity in all where entity.get(Comp1.self)!.value % 5 == 2 { entity.remove(Comp3.self) @@ -218,15 +218,16 @@ private func makeNexus(count: Int) -> Nexus { } #expect(seen.count == expected) #expect(Set(seen).count == expected) - #expect(calls > 1) + // the sparse set loses alignment under churn and passes shorter batches; an archetype table is always one batch + #expect(storage == .archetype ? calls == 1 : calls > 1) for entity in family.entities { #expect(entity.get(Comp3.self)?.value == entity.get(Comp1.self).map { $0.value + 1 }) } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpanOverloads() { - let nexus = makeNexus(count: 6) + @Test(arguments: Nexus.StorageKind.allCases) func familySpanOverloads(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 6) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self) var sum = 0 family.forEachComponentSpan(reading: Comp1.self) { first in @@ -248,22 +249,22 @@ private func makeNexus(count: Int) -> Nexus { } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpansIgnoreNonMembersAndEmptyFamilies() { - let nexus = makeNexus(count: 6) + @Test(arguments: Nexus.StorageKind.allCases) func familySpansIgnoreNonMembersAndEmptyFamilies(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 6) nexus.createEntity(with: Comp1(100)) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) var count = 0 family.forEachComponentSpan(reading: Comp1.self, writing: Comp2.self) { first, _ in count += first.count } #expect(count == 6) - let emptyNexus = Nexus() + let emptyNexus = Nexus(storage: storage) let empty = emptyNexus.family(requiresAll: Comp1.self, Comp2.self) empty.forEachComponentSpan(reading: Comp1.self, writing: Comp2.self) { _, _ in Issue.record("called") } withExtendedLifetime(emptyNexus) {} } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpansEmitOneEventPerWrittenType() { - let nexus = makeNexus(count: 9) + @Test(arguments: Nexus.StorageKind.allCases) func familySpansEmitOneEventPerWrittenType(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 9) for entity in Array(nexus.family(requires: Comp1.self).entities) where entity.get(Comp1.self)!.value.isMultiple(of: 2) { entity.remove(Comp3.self) } @@ -277,4 +278,80 @@ private func makeNexus(count: Int) -> Nexus { family.forEachComponentSpan(reading: Comp1.self, Comp2.self) { _, _ in } #expect(recorder.modified.count == 2) } + + // MARK: entities with different component sets + + /// Entities of three component sets (and with them, in the archetype storage, three tables) that all have `Comp1`. + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + private func makeMixedNexus(_ storage: Nexus.StorageKind) -> Nexus { + let nexus = Nexus(storage: storage) + for number in 0 ..< 4 { + nexus.createEntity(with: Comp1(number), Comp2(number)) + nexus.createEntity(with: Comp1(10 + number), Comp2(10 + number), Comp3(0)) + nexus.createEntity(with: Comp1(20 + number), Comp4(0)) + } + return nexus + } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + @Test(arguments: Nexus.StorageKind.allCases) func singleSpanOfATypeInSeveralTablesSeesEveryComponent(_ storage: Nexus.StorageKind) { + let nexus = makeMixedNexus(storage) + var values: [Int] = [] + nexus.componentSpan(reading: Comp1.self) { (span: Span) in + for index in span.indices { values.append(span[index].value) } + } + let expected = (0 ..< 4).flatMap { [$0, 10 + $0, 20 + $0] } + #expect(values.sorted() == expected.sorted()) + } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + @Test(arguments: Nexus.StorageKind.allCases) func singleWritingSpanOfATypeInSeveralTablesWritesEveryComponent(_ storage: Nexus.StorageKind) { + let nexus = makeMixedNexus(storage) + let recorder = Recorder() + nexus.delegate = recorder + nexus.componentSpan(writing: Comp1.self) { (span: inout MutableSpan) in + for index in span.indices { span[index].value += 100 } + } + var values: [Int] = [] + for entity in nexus.family(requires: Comp1.self).entities { values.append(entity.get(Comp1.self)!.value) } + let expected = (0 ..< 4).flatMap { [100 + $0, 110 + $0, 120 + $0] } + #expect(values.sorted() == expected.sorted()) + // one event for the pass, covering the components of all tables + #expect(recorder.modified.map(\.count) == [12]) + } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + @Test(arguments: Nexus.StorageKind.allCases) func singleWritingSpanKeepsItsWritesWhenTheBodyThrows(_ storage: Nexus.StorageKind) { + let nexus = makeMixedNexus(storage) + #expect(throws: Boom.self) { + try nexus.componentSpan(writing: Comp1.self) { (span: inout MutableSpan) in + for index in span.indices { span[index].value = 7 } + throw Boom() + } + } + for entity in nexus.family(requires: Comp1.self).entities { + #expect(entity.get(Comp1.self)?.value == 7) + } + } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + @Test(arguments: Nexus.StorageKind.allCases) func familySpansVisitEveryMemberOnceAcrossComponentSets(_ storage: Nexus.StorageKind) { + let nexus = makeMixedNexus(storage) + let family = nexus.family(requires: Comp1.self) + var seen: [Int] = [] + var calls = 0 + family.forEachComponentSpan(writing: Comp1.self) { span in + calls += 1 + for index in span.indices { + seen.append(span[index].value) + span[index].value += 1 + } + } + #expect(seen.sorted() == (0 ..< 4).flatMap { [$0, 10 + $0, 20 + $0] }.sorted()) + // the archetype storage passes one batch per table that matches, here one per component set + if storage == .archetype { + #expect(calls == 3) + } + #expect(family.entities.allSatisfy { $0.get(Comp1.self)!.value % 10 >= 1 }) + } } diff --git a/Tests/FirebladeECSTests/ComponentSpanTrapTests.swift b/Tests/FirebladeECSTests/ComponentSpanTrapTests.swift index e1b13ca2..6ef94c9d 100644 --- a/Tests/FirebladeECSTests/ComponentSpanTrapTests.swift +++ b/Tests/FirebladeECSTests/ComponentSpanTrapTests.swift @@ -13,8 +13,8 @@ import Testing // so each trap is a free function. #if os(macOS) || os(Linux) || os(Windows) - private func makeNexus() -> Nexus { - let nexus = Nexus() + private func makeNexus(_ storage: Nexus.StorageKind) -> Nexus { + let nexus = Nexus(storage: storage) for number in 0 ..< 3 { nexus.createEntity(with: Comp1(number), Comp2(number)) } @@ -30,168 +30,256 @@ import Testing } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func creatingAnEntityInAWriteSpanTrapsBody() { - let nexus = makeNexus() + private func creatingAnEntityInAWriteSpanTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) nexus.componentSpan(writing: Comp1.self) { _ in nexus.createEntity(with: Comp3(1)) } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func creatingAnEntityInAReadSpanTrapsBody() { - let nexus = makeNexus() + private func creatingAnEntityInAReadSpanTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) nexus.componentSpan(reading: Comp1.self) { _ in nexus.createEntity() } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func removingAComponentInAReadSpanTrapsBody() { - let nexus = makeNexus() + private func removingAComponentInAReadSpanTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) let entity = nexus.family(requires: Comp2.self).entities.first { _ in true } nexus.componentSpan(reading: Comp1.self) { _ in entity?.remove(Comp2.self) } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func nestedWriteSpanTrapsBody() { - let nexus = makeNexus() + private func nestedWriteSpanTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) nexus.componentSpan(writing: Comp1.self) { _ in nexus.componentSpan(writing: Comp1.self) { _ in } } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func readSpanOfAWrittenTypeTrapsBody() { - let nexus = makeNexus() + private func readSpanOfAWrittenTypeTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) nexus.componentSpan(writing: Comp1.self) { _ in nexus.componentSpan(reading: Comp1.self) { _ in } } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func writeSpanOfAReadTypeTrapsBody() { - let nexus = makeNexus() + private func writeSpanOfAReadTypeTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) nexus.componentSpan(reading: Comp1.self) { _ in nexus.componentSpan(writing: Comp1.self) { _ in } } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func readingAWrittenTypeThroughAnEntityTrapsBody() { - let nexus = makeNexus() + private func readingAWrittenTypeThroughAnEntityTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) let entity = nexus.family(requires: Comp1.self).entities.first { _ in true } nexus.componentSpan(writing: Comp1.self) { _ in _ = entity?.get(Comp1.self) } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func modifyingAReadTypeTrapsBody() { - let nexus = makeNexus() + private func modifyingAReadTypeTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) let entity = nexus.family(requires: Comp1.self).entities.first { _ in true } nexus.componentSpan(reading: Comp1.self) { _ in entity?.modify(Comp1.self) { $0.value = 1 } } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func familySpanReadingAndWritingTheSameTypeTrapsBody() { - let nexus = makeNexus() + private func familySpanReadingAndWritingTheSameTypeTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) family.forEachComponentSpan(reading: Comp1.self, writing: Comp1.self) { _, _ in } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func familySpanStructuralChangeTrapsBody() { - let nexus = makeNexus() + private func familySpanStructuralChangeTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) family.forEachComponentSpan(reading: Comp1.self, writing: Comp2.self) { _, _ in nexus.createEntity() } } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - private func familySpanTypeNotRequiredTrapsBody() { - let nexus = makeNexus() + private func familySpanTypeNotRequiredTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) family.forEachComponentSpan(reading: Comp3.self) { _ in } } @Suite struct ComponentSpanTrapTests { @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func creatingAnEntityInAWriteSpanTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - creatingAnEntityInAWriteSpanTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func creatingAnEntityInAWriteSpanTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingAnEntityInAWriteSpanTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingAnEntityInAWriteSpanTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func creatingAnEntityInAReadSpanTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - creatingAnEntityInAReadSpanTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func creatingAnEntityInAReadSpanTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingAnEntityInAReadSpanTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingAnEntityInAReadSpanTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func removingAComponentInAReadSpanTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - removingAComponentInAReadSpanTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func removingAComponentInAReadSpanTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + removingAComponentInAReadSpanTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + removingAComponentInAReadSpanTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func nestedWriteSpanTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - nestedWriteSpanTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func nestedWriteSpanTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedWriteSpanTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedWriteSpanTrapsBody(.archetype) + } } expectTrapMessage(result, "nested write access") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func readSpanOfAWrittenTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - readSpanOfAWrittenTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func readSpanOfAWrittenTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readSpanOfAWrittenTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readSpanOfAWrittenTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "during a write access") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func writeSpanOfAReadTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - writeSpanOfAReadTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func writeSpanOfAReadTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + writeSpanOfAReadTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + writeSpanOfAReadTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "during a read span") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func readingAWrittenTypeThroughAnEntityTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - readingAWrittenTypeThroughAnEntityTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func readingAWrittenTypeThroughAnEntityTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingAWrittenTypeThroughAnEntityTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingAWrittenTypeThroughAnEntityTrapsBody(.archetype) + } } expectTrapMessage(result, "during a write access") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func modifyingAReadTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - modifyingAReadTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func modifyingAReadTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + modifyingAReadTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + modifyingAReadTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "during a read span") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpanReadingAndWritingTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - familySpanReadingAndWritingTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func familySpanReadingAndWritingTheSameTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + familySpanReadingAndWritingTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + familySpanReadingAndWritingTheSameTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "write access") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpanStructuralChangeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - familySpanStructuralChangeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func familySpanStructuralChangeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + familySpanStructuralChangeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + familySpanStructuralChangeTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) - @Test func familySpanTypeNotRequiredTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - familySpanTypeNotRequiredTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func familySpanTypeNotRequiredTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + familySpanTypeNotRequiredTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + familySpanTypeNotRequiredTrapsBody(.archetype) + } } expectTrapMessage(result, "not required by the family") } diff --git a/Tests/FirebladeECSTests/ComponentStorageReentrancyTests.swift b/Tests/FirebladeECSTests/ComponentStorageReentrancyTests.swift index b94f1195..279812c1 100644 --- a/Tests/FirebladeECSTests/ComponentStorageReentrancyTests.swift +++ b/Tests/FirebladeECSTests/ComponentStorageReentrancyTests.swift @@ -43,8 +43,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { } @Suite(.serialized) struct ComponentStorageReentrancyTests { - @Test func deinitReAddingTheSameTypeKeepsMaskAndStorageInSync() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func deinitReAddingTheSameTypeKeepsMaskAndStorageInSync(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity(with: Reentrant(), Position(x: 1, y: 1)) DeinitHook.onDeinit = { entity.set(Reentrant()) } entity.remove(Reentrant.self) @@ -60,8 +60,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(nexus.numComponents == 1) } - @Test func deinitReAddingDuringDestroyLeavesNoTrace() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func deinitReAddingDuringDestroyLeavesNoTrace(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.assertsOnStaleHandles = false let family = nexus.family(requires: Reentrant.self) let entity = nexus.createEntity(with: Reentrant()) @@ -74,8 +74,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(family.count == 0) } - @Test func deinitReAddingDuringClearDoesNotCrash() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func deinitReAddingDuringClearDoesNotCrash(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.assertsOnStaleHandles = false let entity = nexus.createEntity(with: Reentrant()) DeinitHook.onDeinit = { entity.set(Reentrant()) } @@ -84,8 +84,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(nexus.numComponents == 0) } - @Test func destroyingMembersDuringFamilyIterationSkipsThem() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func destroyingMembersDuringFamilyIterationSkipsThem(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entities = (0 ..< 10).map { nexus.createEntity(with: Position(x: $0, y: $0)) } let family = nexus.family(requires: Position.self) var visited = 0 @@ -99,8 +99,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(family.count == 0) } - @Test func clearingTheNexusDuringFamilyIterationSkipsMembers() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func clearingTheNexusDuringFamilyIterationSkipsMembers(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) for index in 0 ..< 10 { nexus.createEntity(with: Position(x: index, y: index)) } @@ -114,8 +114,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(visited == 1) } - @Test func slotReuseDuringIterationNeverYieldsAnotherEntitysComponent() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func slotReuseDuringIterationNeverYieldsAnotherEntitysComponent(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let first = nexus.createEntity(with: Position(x: 1, y: 1)) let second = nexus.createEntity(with: Position(x: 2, y: 2)) let family = nexus.family(requires: Position.self) @@ -130,8 +130,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(first.get(Position.self)?.x == 1) } - @Test func delegateDestroyingTheEntityDuringAssignDoesNotTrap() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func delegateDestroyingTheEntityDuringAssignDoesNotTrap(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.assertsOnStaleHandles = false let delegate = DestroyOnAdd() delegate.nexus = nexus @@ -148,9 +148,9 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { nexus.delegate = nil } - @Test func existentialComponentsAreStoredUnderTheirDynamicType() { + @Test(arguments: Nexus.StorageKind.allCases) func existentialComponentsAreStoredUnderTheirDynamicType(_ storage: Nexus.StorageKind) { // `Component.identifier` of the value, and with it the storage, is that of the dynamic type. - let nexus = Nexus() + let nexus = Nexus(storage: storage) let erased: Component = Position(x: 4, y: 5) let entity = nexus.createEntity() entity.set(erased) @@ -159,9 +159,9 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(nexus.get(components: entity.identifier) == [Position.identifier]) } - @Test func familyOrderIsDeterministicAfterSwapRemoveChurn() { + @Test(arguments: Nexus.StorageKind.allCases) func familyOrderIsDeterministicAfterSwapRemoveChurn(_ storage: Nexus.StorageKind) { func run() -> [Int] { - let nexus = Nexus() + let nexus = Nexus(storage: storage) let entities = (0 ..< 64).map { nexus.createEntity(with: Position(x: $0, y: 0)) } let family = nexus.family(requires: Position.self) for index in stride(from: 0, to: 64, by: 3) { @@ -177,7 +177,7 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(Set(first) == Set((0 ..< 64).filter { $0 % 3 != 0 || $0 % 6 == 0 })) // a family created after the churn lists its members in entity order - let nexus = Nexus() + let nexus = Nexus(storage: storage) let entities = (0 ..< 64).map { nexus.createEntity(with: Position(x: $0, y: 0)) } for index in stride(from: 0, to: 64, by: 3) { entities[index].remove(Position.self) @@ -186,8 +186,8 @@ private final class DestroyOnAdd: NexusEventDelegate, @unchecked Sendable { #expect(late.map(\.x) == (0 ..< 64).filter { $0 % 3 != 0 }) } - @Test func snapshotsWithAbsurdEntityIndicesAreRejected() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func snapshotsWithAbsurdEntityIndicesAreRejected(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register([SerialPosition.self]) let json = #"{"formatVersion":3,"entities":[{"id":4294967295,"components":{"Position":{"x":1,"y":2}}}]}"# var decoder = JSONDecoder() diff --git a/Tests/FirebladeECSTests/ComponentStorageSafetyTests.swift b/Tests/FirebladeECSTests/ComponentStorageSafetyTests.swift index 517e546c..87f5f6ee 100644 --- a/Tests/FirebladeECSTests/ComponentStorageSafetyTests.swift +++ b/Tests/FirebladeECSTests/ComponentStorageSafetyTests.swift @@ -27,8 +27,8 @@ import Testing static let componentIndex = 2_000_000 } - private func populated() -> (Nexus.SparseSetStorage, EntityIdentifier) { - let storage = Nexus.SparseSetStorage() + private func populated(_ kind: Nexus.StorageKind) -> (Nexus.Backend, EntityIdentifier) { + let storage = Nexus.Backend(kind) let entity = EntityIdentifier(index: 1, generation: 0) storage.insertEntity(entity) storage.set(Position(x: 0, y: 0), for: entity) @@ -44,128 +44,208 @@ import Testing #endif } - private func structuralChangeDuringModifyTrapsBody() { - let (storage, entity) = populated() + private func structuralChangeDuringModifyTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) _ = storage.modify(Position.self, for: entity) { _ in storage.set(Comp1(1), for: entity) } } - private func destroyDuringModifyTrapsBody() { - let (storage, entity) = populated() + private func destroyDuringModifyTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) _ = storage.modify(Position.self, for: entity) { _ in storage.destroyEntity(entity) } } - private func structuralChangeDuringBulkAccessTrapsBody() { - let (storage, entity) = populated() - storage.withComponents(of: Position.self) { _, _ in storage.remove(Name.self, from: entity) } + private func structuralChangeDuringBulkAccessTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) + storage.withComponents(of: Position.self) { _, _ in storage.remove(componentIndex: Name.componentIndex, from: entity) } } - private func modifyInsideModifyOfTheSameTypeTrapsBody() { - let (storage, entity) = populated() + private func modifyInsideModifyOfTheSameTypeTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) _ = storage.modify(Position.self, for: entity) { _ in _ = storage.modify(Position.self, for: entity) { _ in } } } - private func getInsideWriteOfTheSameTypeTrapsBody() { - let (storage, entity) = populated() + private func getInsideWriteOfTheSameTypeTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) _ = storage.modify(Position.self, for: entity) { _ in _ = storage.get(Position.self, for: entity) } } - private func bulkAccessInsideWriteOfTheSameTypeTrapsBody() { - let (storage, _) = populated() + private func bulkAccessInsideWriteOfTheSameTypeTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, _) = populated(kind) storage.withComponents(of: Position.self) { _, _ in storage.withComponents(of: Position.self) { _, _ in } } } - private func twoTypesSharingAComponentIndexTrapBody() { - let storage = Nexus.SparseSetStorage() + private func twoTypesSharingAComponentIndexTrapBody(_ kind: Nexus.StorageKind) { + let storage = Nexus.Backend(kind) let entity = EntityIdentifier(index: 1, generation: 0) storage.insertEntity(entity) storage.set(TwinA(), for: entity) _ = storage.get(TwinB.self, for: entity) } - private func componentIndexAboveTheLimitTrapsBody() { - let (storage, entity) = populated() + private func componentIndexAboveTheLimitTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) storage.set(TooHigh(), for: entity) } - private func settingOnADeadEntityTrapsBody() { - let (storage, entity) = populated() + private func settingOnADeadEntityTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) storage.set(Comp1(1), for: EntityIdentifier(index: entity.index, generation: 1)) } - private func insertingAnOccupiedSlotTrapsBody() { - let (storage, entity) = populated() + private func insertingAnOccupiedSlotTrapsBody(_ kind: Nexus.StorageKind) { + let (storage, entity) = populated(kind) storage.insertEntity(EntityIdentifier(index: entity.index, generation: 1)) } @Suite struct ComponentStorageSafetyTests { - @Test func structuralChangeDuringModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - structuralChangeDuringModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func structuralChangeDuringModifyTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + structuralChangeDuringModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + structuralChangeDuringModifyTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } - @Test func destroyDuringModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - destroyDuringModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func destroyDuringModifyTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + destroyDuringModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + destroyDuringModifyTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } - @Test func structuralChangeDuringBulkAccessTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - structuralChangeDuringBulkAccessTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func structuralChangeDuringBulkAccessTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + structuralChangeDuringBulkAccessTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + structuralChangeDuringBulkAccessTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } - @Test func modifyInsideModifyOfTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - modifyInsideModifyOfTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func modifyInsideModifyOfTheSameTypeTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + modifyInsideModifyOfTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + modifyInsideModifyOfTheSameTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "nested write access") } - @Test func getInsideWriteOfTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - getInsideWriteOfTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func getInsideWriteOfTheSameTypeTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + getInsideWriteOfTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + getInsideWriteOfTheSameTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "during a write access") } - @Test func bulkAccessInsideWriteOfTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - bulkAccessInsideWriteOfTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func bulkAccessInsideWriteOfTheSameTypeTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + bulkAccessInsideWriteOfTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + bulkAccessInsideWriteOfTheSameTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "nested write access") } - @Test func twoTypesSharingAComponentIndexTrap() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - twoTypesSharingAComponentIndexTrapBody() + @Test(arguments: Nexus.StorageKind.allCases) func twoTypesSharingAComponentIndexTrap(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + twoTypesSharingAComponentIndexTrapBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + twoTypesSharingAComponentIndexTrapBody(.archetype) + } } expectTrapMessage(result, "used by two component types") } - @Test func componentIndexAboveTheLimitTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - componentIndexAboveTheLimitTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func componentIndexAboveTheLimitTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + componentIndexAboveTheLimitTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + componentIndexAboveTheLimitTrapsBody(.archetype) + } } expectTrapMessage(result, "out of range") } - @Test func settingOnADeadEntityTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - settingOnADeadEntityTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func settingOnADeadEntityTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + settingOnADeadEntityTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + settingOnADeadEntityTrapsBody(.archetype) + } } expectTrapMessage(result, "dead entity") } - @Test func insertingAnOccupiedSlotTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - insertingAnOccupiedSlotTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func insertingAnOccupiedSlotTraps(_ kind: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch kind { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + insertingAnOccupiedSlotTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + insertingAnOccupiedSlotTrapsBody(.archetype) + } } expectTrapMessage(result, "is in use") } diff --git a/Tests/FirebladeECSTests/ComponentStorageTests.swift b/Tests/FirebladeECSTests/ComponentStorageTests.swift index 581707fd..0e3f5b82 100644 --- a/Tests/FirebladeECSTests/ComponentStorageTests.swift +++ b/Tests/FirebladeECSTests/ComponentStorageTests.swift @@ -44,16 +44,23 @@ private struct Token: Component { } } +extension Nexus.Backend { + @discardableResult + fileprivate func remove(_: C.Type, from entityId: EntityIdentifier) -> Bool { + remove(componentIndex: C.componentIndex, from: entityId) + } +} + private func entityId(_ index: UInt32, generation: UInt32 = 0) -> EntityIdentifier { EntityIdentifier(index: index, generation: generation) } @Suite(.serialized) struct ComponentStorageTests { - typealias Storage = Nexus.SparseSetStorage + typealias Storage = Nexus.Backend - private func makeStorage(entities count: Int) -> (Storage, [EntityIdentifier]) { - let storage = Storage() + private func makeStorage(_ kind: Nexus.StorageKind, entities count: Int) -> (Storage, [EntityIdentifier]) { + let storage = Storage(kind) let ids = (1 ... count).map { entityId(UInt32($0)) } for id in ids { storage.insertEntity(id) @@ -61,8 +68,8 @@ struct ComponentStorageTests { return (storage, ids) } - @Test func setGetReplaceRemove() throws { - let (storage, ids) = makeStorage(entities: 1) + @Test(arguments: Nexus.StorageKind.allCases) func setGetReplaceRemove(_ kind: Nexus.StorageKind) throws { + let (storage, ids) = makeStorage(kind, entities: 1) let id = ids[0] #expect(storage.get(Position.self, for: id) == nil) #expect(!storage.has(componentIndex: Position.componentIndex, for: id)) @@ -82,8 +89,8 @@ struct ComponentStorageTests { #expect(storage.componentCount(of: id) == 0) } - @Test func swapRemoveKeepsOtherEntitiesIntact() { - let (storage, ids) = makeStorage(entities: 10) + @Test(arguments: Nexus.StorageKind.allCases) func swapRemoveKeepsOtherEntitiesIntact(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 10) for (offset, id) in ids.enumerated() { storage.set(Position(x: offset, y: 0), for: id) storage.set(Name(name: "entity \(offset)"), for: id) @@ -100,9 +107,19 @@ struct ComponentStorageTests { #expect(storage.totalComponentCount == 7 + 10) } + private func makeSparseSet(entities count: Int) -> (Nexus.SparseSetStorage, [EntityIdentifier]) { + let storage = Nexus.SparseSetStorage() + let ids = (1 ... count).map { entityId(UInt32($0)) } + for id in ids { + storage.insertEntity(id) + } + return (storage, ids) + } + + /// The paged sparse index is internal to the sparse-set storage. @Test func pagingBoundaries() { let page = Int(Nexus.PagedSparseIndex.pageSize) - let storage = Storage() + let storage = Nexus.SparseSetStorage() let probes = [1, page - 1, page, 2 * page - 1, 2 * page, 3 * page + 5] for probe in probes { let id = entityId(UInt32(probe)) @@ -128,9 +145,10 @@ struct ComponentStorageTests { #expect(storage.get(Position.self, for: entityId(UInt32(page)))?.x == -1) } + /// The dense buffer of a component type is internal to the sparse-set storage. @Test func growthAndReallocation() { let count = 5000 // several buffer doublings and two sparse pages - let (storage, ids) = makeStorage(entities: count) + let (storage, ids) = makeSparseSet(entities: count) for (offset, id) in ids.enumerated() { storage.set(Position(x: offset, y: 0), for: id) if offset.isMultiple(of: 3) { @@ -148,10 +166,10 @@ struct ComponentStorageTests { #expect(storage.existing(Position.self)?.capacity ?? 0 >= count) } - @Test func componentsAreReleasedWithTheStorage() { + @Test(arguments: Nexus.StorageKind.allCases) func componentsAreReleasedWithTheStorage(_ kind: Nexus.StorageKind) { let base = Token.live do { - let (storage, ids) = makeStorage(entities: 50) + let (storage, ids) = makeStorage(kind, entities: 50) for (offset, id) in ids.enumerated() { storage.set(Token(offset), for: id) } @@ -172,8 +190,8 @@ struct ComponentStorageTests { #expect(Token.live == base) } - @Test func equalValuesOfSeveralEntitiesAreStoredAsIndependentCopies() { - let (storage, ids) = makeStorage(entities: 3) + @Test(arguments: Nexus.StorageKind.allCases) func equalValuesOfSeveralEntitiesAreStoredAsIndependentCopies(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 3) let shared = Position(x: 7, y: 7) for id in ids { storage.set(shared, for: id) @@ -188,8 +206,8 @@ struct ComponentStorageTests { #expect(storage.get(Position.self, for: ids[2]) == shared) } - @Test func deadAndStaleEntitiesReportAbsence() { - let (storage, ids) = makeStorage(entities: 2) + @Test(arguments: Nexus.StorageKind.allCases) func deadAndStaleEntitiesReportAbsence(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 2) storage.set(Position(x: 1, y: 1), for: ids[0]) let stale = entityId(ids[0].index, generation: 1) #expect(storage.get(Position.self, for: stale) == nil) @@ -209,8 +227,8 @@ struct ComponentStorageTests { #expect(storage.get(Position.self, for: ids[0]) == nil) } - @Test func outOfRangeComponentIndicesAreAbsent() { - let (storage, ids) = makeStorage(entities: 1) + @Test(arguments: Nexus.StorageKind.allCases) func outOfRangeComponentIndicesAreAbsent(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 1) storage.set(Position(x: 1, y: 1), for: ids[0]) for index in [-1, 100_000, Int.max] { #expect(!storage.has(componentIndex: index, for: ids[0])) @@ -220,8 +238,8 @@ struct ComponentStorageTests { } } - @Test func noLimitOnTheNumberOfComponentTypes() { - let (storage, ids) = makeStorage(entities: 2) + @Test(arguments: Nexus.StorageKind.allCases) func noLimitOnTheNumberOfComponentTypes(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 2) let id = ids[0] storage.set(AtIndex255(), for: id) storage.set(AtIndex256(), for: id) @@ -237,8 +255,8 @@ struct ComponentStorageTests { #expect(storage.componentIndices(of: id) == [255, 1000]) } - @Test func nestedAccessToDifferentTypesSeesRealData() { - let (storage, ids) = makeStorage(entities: 2) + @Test(arguments: Nexus.StorageKind.allCases) func nestedAccessToDifferentTypesSeesRealData(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 2) storage.set(Position(x: 1, y: 1), for: ids[0]) storage.set(Position(x: 2, y: 2), for: ids[1]) storage.set(Name(name: "a"), for: ids[0]) @@ -254,8 +272,8 @@ struct ComponentStorageTests { #expect(storage.get(Position.self, for: ids[0])?.x == 10) } - @Test func bulkAccessCoversEveryComponentOnce() { - let (storage, ids) = makeStorage(entities: 20) + @Test(arguments: Nexus.StorageKind.allCases) func bulkAccessCoversEveryComponentOnce(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 20) for (offset, id) in ids.enumerated() where offset.isMultiple(of: 2) { storage.set(Position(x: offset, y: 0), for: id) } @@ -272,8 +290,8 @@ struct ComponentStorageTests { #expect(sum == (0 ..< 20).filter { $0.isMultiple(of: 2) }.reduce(0, +)) } - @Test func removeAllForgetsEntitiesAndTypes() { - let (storage, ids) = makeStorage(entities: 3) + @Test(arguments: Nexus.StorageKind.allCases) func removeAllForgetsEntitiesAndTypes(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 3) storage.set(Position(x: 1, y: 1), for: ids[0]) storage.removeAll() #expect(storage.entityCount == 0) @@ -282,8 +300,8 @@ struct ComponentStorageTests { #expect(!storage.isAlive(ids[0])) } - @Test func untypedSlowPaths() { - let (storage, ids) = makeStorage(entities: 1) + @Test(arguments: Nexus.StorageKind.allCases) func untypedSlowPaths(_ kind: Nexus.StorageKind) { + let (storage, ids) = makeStorage(kind, entities: 1) let id = ids[0] let position = Position(x: 5, y: 5) storage.set(position, for: id) @@ -291,7 +309,7 @@ struct ComponentStorageTests { let indices = storage.componentIndices(of: id) #expect(indices == [Position.componentIndex, Name.componentIndex].sorted()) #expect(storage.anyComponent(componentIndex: Position.componentIndex, for: id) as? Position == position) - #expect(storage.indexByIdentifier[Position.identifier] == Position.componentIndex) + #expect(storage.componentIndex(of: Position.identifier) == Position.componentIndex) #expect(storage.remove(componentIndex: Position.componentIndex, from: id)) #expect(!storage.remove(componentIndex: Position.componentIndex, from: id)) #expect(storage.componentCount(of: id) == 1) diff --git a/Tests/FirebladeECSTests/EntityCodingTests.swift b/Tests/FirebladeECSTests/EntityCodingTests.swift index 8ccd50d7..627a3fc0 100644 --- a/Tests/FirebladeECSTests/EntityCodingTests.swift +++ b/Tests/FirebladeECSTests/EntityCodingTests.swift @@ -17,16 +17,16 @@ import Testing #expect(try JSONDecoder().decode(EntityIdentifier.self, from: data) == identifier) } - @Test func entityEncodesAsIdentifier() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityEncodesAsIdentifier(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) nexus.createEntity() let entity = nexus.createEntity() let data = try JSONEncoder().encode(entity) #expect(String(decoding: data, as: UTF8.self) == "\(entity.identifier.index)") } - @Test func entityDecodesThroughResolver() throws { - let target = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityDecodesThroughResolver(_ storage: Nexus.StorageKind) throws { + let target = Nexus(storage: storage) let first = target.createEntity() let second = target.createEntity() let resolver = EntityReferenceResolver(nexus: target, mapping: [7: second.identifier, 8: first.identifier]) @@ -43,9 +43,9 @@ import Testing } } - @Test func entityDecodingUnmappedIdentifierThrows() { + @Test(arguments: Nexus.StorageKind.allCases) func entityDecodingUnmappedIdentifierThrows(_ storage: Nexus.StorageKind) { let decoder = JSONDecoder() - decoder.userInfo[.nexusEntityResolver] = EntityReferenceResolver(nexus: Nexus(), mapping: [:]) + decoder.userInfo[.nexusEntityResolver] = EntityReferenceResolver(nexus: Nexus(storage: storage), mapping: [:]) #expect(throws: EntityReferenceError.unresolved(3)) { try decoder.decode(Entity.self, from: Data("3".utf8)) } @@ -61,13 +61,13 @@ import Testing } } - @Test func resolverBoundAfterCreationResolves() throws { + @Test(arguments: Nexus.StorageKind.allCases) func resolverBoundAfterCreationResolves(_ storage: Nexus.StorageKind) throws { // A decoding entry point that creates its own nexus binds the resolver supplied via userInfo. let resolver = EntityReferenceResolver(mapping: [:]) let decoder = JSONDecoder() decoder.userInfo[.nexusEntityResolver] = resolver - let target = Nexus() + let target = Nexus(storage: storage) let entity = target.createEntity() resolver.bind(to: target) resolver.map(9, to: entity.identifier) diff --git a/Tests/FirebladeECSTests/EntityCreationTests.swift b/Tests/FirebladeECSTests/EntityCreationTests.swift index 462728df..8fcdb73b 100644 --- a/Tests/FirebladeECSTests/EntityCreationTests.swift +++ b/Tests/FirebladeECSTests/EntityCreationTests.swift @@ -9,8 +9,8 @@ import FirebladeECS import Testing @Suite struct EntityCreationTests { - @Test func createEntityOneComponent() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func createEntityOneComponent(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity { Position(x: 1, y: 2) } @@ -23,8 +23,8 @@ import Testing #expect(nexus.numFamilies == 0) } - @Test func createEntityMultipleComponents() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func createEntityMultipleComponents(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity { Position(x: 1, y: 2) @@ -41,8 +41,8 @@ import Testing #expect(nexus.numFamilies == 0) } - @Test func bulkCreateEntitiesOneComponent() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func bulkCreateEntitiesOneComponent(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let entities = nexus.createEntities(count: 100) { ctx in Velocity(a: Float(ctx.index)) @@ -56,8 +56,8 @@ import Testing #expect(nexus.numFamilies == 0) } - @Test func bulkCreateEntitiesMultipleComponents() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func bulkCreateEntitiesMultipleComponents(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let entities = nexus.createEntities(count: 100) { ctx in Position(x: ctx.index, y: ctx.index) diff --git a/Tests/FirebladeECSTests/EntityTests.swift b/Tests/FirebladeECSTests/EntityTests.swift index 454a527d..c6c7cc0b 100644 --- a/Tests/FirebladeECSTests/EntityTests.swift +++ b/Tests/FirebladeECSTests/EntityTests.swift @@ -32,8 +32,8 @@ import Testing #expect(max.generation == UInt32.max) } - @Test func allComponentsOfEntity() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func allComponentsOfEntity(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let pos = Position(x: 1, y: 2) let name = Name(name: "Hello") @@ -51,8 +51,8 @@ import Testing #expect(allComponents.compactMap { $0 as? Velocity } == [vel]) } - @Test func entityEquality() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityEquality(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entityA = nexus.createEntity() let entityB = nexus.createEntity() @@ -61,8 +61,8 @@ import Testing #expect(entityA != entityB) } - @Test func removeAllComponentsFromEntity() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func removeAllComponentsFromEntity(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity(with: Position(x: 1, y: 2), Name(name: "MyEntity")) #expect(entity.numComponents == 2) @@ -94,8 +94,8 @@ import Testing #expect(generator.count == 1) } - @Test func entitySubscripts() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entitySubscripts(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let pos = Position(x: 12, y: 45) let name = Name(name: "SomeName") let entity = nexus.createEntity(with: pos, name) @@ -147,16 +147,16 @@ import Testing #expect(entity[\Comp1.value] == 12) } - @Test func componentsIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentsIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() #expect(Array(entity.makeComponentsIterator()).isEmpty) entity.set(Position()) #expect(Array(entity.makeComponentsIterator()).count == 1) } - @Test func setAndGet() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func setAndGet(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() #expect(entity.get(Position.self) == nil) @@ -171,8 +171,8 @@ import Testing #expect(entity.has(Color.self)) } - @Test func setValueThroughWritableKeyPath() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func setValueThroughWritableKeyPath(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let keyPath: WritableKeyPath = \.x @@ -187,8 +187,8 @@ import Testing #expect(entity.get(Position.self)?.x == 4) } - @Test func entityCreationIntrinsic() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityCreationIntrinsic(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let secondEntity = entity.createEntity() @@ -206,8 +206,8 @@ import Testing #expect(nexus.numComponents == 2) } - @Test func entityDescriptions() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityDescriptions(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entt = nexus.createEntity() #expect(!entt.description.isEmpty) #expect(!entt.debugDescription.isEmpty) diff --git a/Tests/FirebladeECSTests/FSMTests.swift b/Tests/FirebladeECSTests/FSMTests.swift index 0d99f91b..629a3392 100644 --- a/Tests/FirebladeECSTests/FSMTests.swift +++ b/Tests/FirebladeECSTests/FSMTests.swift @@ -99,9 +99,9 @@ import Testing #expect(component1 == component2) } - @Test func entitiesGetIndependentCopiesOfTheValue() { + @Test(arguments: Nexus.StorageKind.allCases) func entitiesGetIndependentCopiesOfTheValue(_ storage: Nexus.StorageKind) { // 1.x handed the one shared instance to every entity; now each entity owns its copy - let nexus = Nexus() + let nexus = Nexus(storage: storage) let provider = ComponentSingletonProvider(type: MockComponent.self) let entity1 = nexus.createEntity().set(provider.getComponent()) let entity2 = nexus.createEntity().set(provider.getComponent()) @@ -358,8 +358,8 @@ import Testing // MARK: - @Suite struct EntityStateMachineTests { - @Test func enterStateAddsStatesComponents() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func enterStateAddsStatesComponents(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state = EntityState() @@ -370,8 +370,8 @@ import Testing #expect(entity.get(MockComponent.self) == component) } - @Test func enterSecondStateAddsSecondStatesComponents() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func enterSecondStateAddsSecondStatesComponents(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state1 = EntityState() @@ -388,8 +388,8 @@ import Testing #expect(entity.get(MockComponent2.self) == component2) } - @Test func enterSecondStateRemovesFirstStatesComponents() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func enterSecondStateRemovesFirstStatesComponents(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state1 = EntityState() @@ -407,8 +407,8 @@ import Testing #expect(!entity.has(MockComponent.self)) } - @Test func enterSecondStateDoesNotRemoveOverlappingComponents() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func enterSecondStateDoesNotRemoveOverlappingComponents(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) class EventDelegate: NexusEventDelegate, @unchecked Sendable { @@ -442,8 +442,8 @@ import Testing #expect(entity.get(MockComponent.self) == component1) } - @Test func enterSecondStateRemovesDifferentComponentsOfSameType() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func enterSecondStateRemovesDifferentComponentsOfSameType(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state1 = EntityState() @@ -463,8 +463,8 @@ import Testing #expect(entity.get(MockComponent.self) == component3) } - @Test func createStateAddsState() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func createStateAddsState(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state = fsm.createState(name: "test") @@ -474,8 +474,8 @@ import Testing #expect(entity.get(MockComponent.self) == component) } - @Test func createStateDoesNotChangeState() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func createStateDoesNotChangeState(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state = fsm.createState(name: "test") @@ -484,8 +484,8 @@ import Testing #expect(entity.get(MockComponent.self) == nil) } - @Test func callChangeStateWithSameNameLeavesEntityComponentsIntact() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func callChangeStateWithSameNameLeavesEntityComponentsIntact(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() let fsm = EntityStateMachine(entity: entity) let state = fsm.createState(name: "test") @@ -502,7 +502,7 @@ import Testing #expect(entity.get(MockComponent2.self) == component2) } - @Test func getsDeinitedWhileBeingStronglyReferencedByComponentAssignedToEntity() { + @Test(arguments: Nexus.StorageKind.allCases) func getsDeinitedWhileBeingStronglyReferencedByComponentAssignedToEntity(_ storage: Nexus.StorageKind) { struct Marker: Component, Sendable { let fsm: EntityStateMachine init(fsm: EntityStateMachine) { @@ -510,7 +510,7 @@ import Testing } } - let nexus = Nexus() + let nexus = Nexus(storage: storage) var entity: Entity? = nexus.createEntity() var markerComponent: Marker? = Marker(fsm: EntityStateMachine(entity: entity!)) entity?.set(markerComponent!) diff --git a/Tests/FirebladeECSTests/FamilyCodingTests.swift b/Tests/FirebladeECSTests/FamilyCodingTests.swift index 4c85e8ce..c1826f5d 100644 --- a/Tests/FirebladeECSTests/FamilyCodingTests.swift +++ b/Tests/FirebladeECSTests/FamilyCodingTests.swift @@ -10,8 +10,8 @@ import Testing import Foundation @Suite struct FamilyCodingTests { - @Test func encodingFamily1() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func encodingFamily1(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let family = nexus.family(requires: MyComponent.self) family.createMember(with: MyComponent(name: "My Name", flag: true)) @@ -23,7 +23,7 @@ import Foundation #expect(encodedData.count >= 90) } - @Test func decodingFamily1() throws { + @Test(arguments: Nexus.StorageKind.allCases) func decodingFamily1(_ storage: Nexus.StorageKind) throws { let jsonString = """ [ { @@ -42,7 +42,7 @@ import Foundation """ let jsonData = jsonString.data(using: .utf8)! - let nexus = Nexus() + let nexus = Nexus(storage: storage) let family = nexus.family(requires: MyComponent.self) #expect(family.isEmpty) var jsonDecoder = JSONDecoder() @@ -51,8 +51,8 @@ import Foundation #expect(family.count == 2) } - @Test func encodeFamily2() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func encodeFamily2(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self) family.createMember(with: MyComponent(name: "My Name", flag: true), YourComponent(number: 1.23)) @@ -64,7 +64,7 @@ import Foundation #expect(encodedData.count >= 91) } - @Test func decodingFamily2() throws { + @Test(arguments: Nexus.StorageKind.allCases) func decodingFamily2(_ storage: Nexus.StorageKind) throws { let jsonString = """ [ { @@ -90,7 +90,7 @@ import Foundation let jsonData = jsonString.data(using: .utf8)! - let nexus = Nexus() + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: YourComponent.self, MyComponent.self) #expect(family.isEmpty) @@ -100,8 +100,8 @@ import Foundation #expect(family.count == 2) } - @Test func encodeFamily3() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func encodeFamily3(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self, Position.self) family.createMember(with: MyComponent(name: "My Name", flag: true), YourComponent(number: 1.23), Position(x: 1, y: 2)) @@ -113,7 +113,7 @@ import Foundation #expect(encodedData.count >= 200) } - @Test func decodingFamily3() throws { + @Test(arguments: Nexus.StorageKind.allCases) func decodingFamily3(_ storage: Nexus.StorageKind) throws { let jsonString = """ [ { @@ -147,7 +147,7 @@ import Foundation let jsonData = jsonString.data(using: .utf8)! - let nexus = Nexus() + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: YourComponent.self, MyComponent.self, Position.self) let family2 = nexus.family(requiresAll: YourComponent.self, MyComponent.self, excludesAll: Index.self) @@ -159,8 +159,8 @@ import Foundation #expect(family2.count == 2) } - @Test func encodeFamily4() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func encodeFamily4(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self, Position.self, Color.self) family.createMember(with: MyComponent(name: "My Name", flag: true), YourComponent(number: 1.23), Position(x: 1, y: 2), Color(r: 1, g: 2, b: 3)) @@ -172,7 +172,7 @@ import Foundation #expect(encodedData.count >= 250) } - @Test func decodeFamily4() throws { + @Test(arguments: Nexus.StorageKind.allCases) func decodeFamily4(_ storage: Nexus.StorageKind) throws { let jsonString = """ [ { @@ -216,7 +216,7 @@ import Foundation let jsonData = jsonString.data(using: .utf8)! - let nexus = Nexus() + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: YourComponent.self, MyComponent.self, Position.self, Color.self) #expect(family.isEmpty) @@ -226,8 +226,8 @@ import Foundation #expect(family.count == 2) } - @Test func encodeFamily5() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func encodeFamily5(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self, Position.self, Color.self, Party.self) family.createMember(with: MyComponent(name: "My Name", flag: true), YourComponent(number: 1.23), Position(x: 1, y: 2), Color(r: 1, g: 2, b: 3), Party(partying: true)) @@ -239,7 +239,7 @@ import Foundation #expect(encodedData.count >= 320) } - @Test func decodeFamily5() throws { + @Test(arguments: Nexus.StorageKind.allCases) func decodeFamily5(_ storage: Nexus.StorageKind) throws { let jsonString = """ [ { @@ -289,7 +289,7 @@ import Foundation let jsonData = jsonString.data(using: .utf8)! - let nexus = Nexus() + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: YourComponent.self, MyComponent.self, Position.self, Color.self, Party.self) #expect(family.isEmpty) @@ -299,7 +299,7 @@ import Foundation #expect(family.count == 2) } - @Test func failDecodingFamily() { + @Test(arguments: Nexus.StorageKind.allCases) func failDecodingFamily(_ storage: Nexus.StorageKind) { let jsonString = """ [ { @@ -341,7 +341,7 @@ import Foundation let jsonData = jsonString.data(using: .utf8)! var jsonDecoder = JSONDecoder() - let nexus = Nexus() + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: YourComponent.self, MyComponent.self) #expect(throws: Error.self) { diff --git a/Tests/FirebladeECSTests/FamilyReadWriteTests.swift b/Tests/FirebladeECSTests/FamilyReadWriteTests.swift index 1fd44f39..7cb9aa88 100644 --- a/Tests/FirebladeECSTests/FamilyReadWriteTests.swift +++ b/Tests/FirebladeECSTests/FamilyReadWriteTests.swift @@ -23,8 +23,8 @@ private final class Recorder: NexusEventDelegate, @unchecked Sendable { } /// `count` entities with `Comp1 ... Comp8` all holding the entity's number, plus a `Marker` on the even ones. -private func makeNexus(count: Int) -> Nexus { - let nexus = Nexus() +private func makeNexus(_ storage: Nexus.StorageKind, count: Int) -> Nexus { + let nexus = Nexus(storage: storage) for number in 0 ..< count { let entity = nexus.createEntity(with: Comp1(number), Comp2(number), Comp3(number), Comp4(number)) entity.set(Comp5(number), Comp6(number), Comp7(number), Comp8(number)) @@ -36,8 +36,8 @@ private func makeNexus(count: Int) -> Nexus { } @Suite struct FamilyReadWriteTests { - @Test func readingAndWritingChangesTheStorageInPlace() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func readingAndWritingChangesTheStorageInPlace(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) var visited = 0 family.forEach(reading: Comp1.self, writing: Comp2.self) { read, write in @@ -51,8 +51,8 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func excludedMembersAreNotVisited() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func excludedMembersAreNotVisited(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, excludesAll: Marker.self) var seen: [Int] = [] family.forEach(reading: Comp1.self, writing: Comp2.self) { read, write in @@ -66,23 +66,23 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func familyOverSubsetOfAStorageTouchesOnlyMembers() { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func familyOverSubsetOfAStorageTouchesOnlyMembers(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 10) let family = nexus.family(requiresAll: Comp1.self, Marker.self) family.forEach(writing: Comp1.self) { $0.value += 100 } let all = nexus.family(requires: Comp1.self).map(\.value).sorted() #expect(all == [100, 1, 102, 3, 104, 5, 106, 7, 108, 9].sorted()) } - @Test func emptyFamilyNeverCallsBody() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func emptyFamilyNeverCallsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) family.forEach(reading: Comp1.self, writing: Comp2.self) { _, _ in Issue.record("called") } family.forEach(writing: Comp1.self) { _ in Issue.record("called") } } - @Test func zeroReadsAndFourReadsWithoutWrites() { - let nexus = makeNexus(count: 4) + @Test(arguments: Nexus.StorageKind.allCases) func zeroReadsAndFourReadsWithoutWrites(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 4) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self) var sum = 0 family.forEach(reading: Comp1.self, Comp2.self, Comp3.self, Comp4.self) { a, b, c, d in @@ -97,8 +97,8 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func fourReadsAndFourWrites() { - let nexus = makeNexus(count: 5) + @Test(arguments: Nexus.StorageKind.allCases) func fourReadsAndFourWrites(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 5) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self, Comp7.self, Comp8.self) family.forEach(reading: Comp1.self, Comp2.self, Comp3.self, Comp4.self, writing: Comp5.self, Comp6.self, Comp7.self, Comp8.self) { r1, r2, r3, r4, w1, w2, w3, w4 in @@ -116,8 +116,8 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func mixedArities() { - let nexus = makeNexus(count: 5) + @Test(arguments: Nexus.StorageKind.allCases) func mixedArities(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 5) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self) family.forEach(reading: Comp1.self, Comp2.self, writing: Comp3.self, Comp4.self, Comp5.self) { r1, r2, w1, w2, w3 in w1.value = r1.value + r2.value @@ -134,8 +134,8 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func parameterPackFallbackReadsAndWrites() { - let nexus = makeNexus(count: 6) + @Test(arguments: Nexus.StorageKind.allCases) func parameterPackFallbackReadsAndWrites(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 6) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self, Comp7.self, Comp8.self) // 3 reads and 5 writes: above the generated arity family.forEach(reading: Comp1.self, Comp2.self, Comp3.self, @@ -166,8 +166,8 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func oneModifiedEventPerWrittenTypeAndPass() throws { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func oneModifiedEventPerWrittenTypeAndPass(_ storage: Nexus.StorageKind) throws { + let nexus = makeNexus(storage, count: 10) let recorder = Recorder() nexus.delegate = recorder let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self) @@ -186,13 +186,13 @@ private func makeNexus(count: Int) -> Nexus { #expect(recorder.modified.count == 2) } - @Test func noEventsWithoutValueChangeSubscription() { + @Test(arguments: Nexus.StorageKind.allCases) func noEventsWithoutValueChangeSubscription(_ storage: Nexus.StorageKind) { final class Structural: NexusEventDelegate, @unchecked Sendable { var count = 0 func nexusEvent(_: NexusEvent) { count += 1 } func nexusNonFatalError(_: String) {} } - let nexus = makeNexus(count: 3) + let nexus = makeNexus(storage, count: 3) let family = nexus.family(requiresAll: Comp1.self) let delegate = Structural() nexus.delegate = delegate @@ -200,8 +200,8 @@ private func makeNexus(count: Int) -> Nexus { #expect(delegate.count == 0) } - @Test func throwingEndsThePassAndReleasesTheLocks() throws { - let nexus = makeNexus(count: 10) + @Test(arguments: Nexus.StorageKind.allCases) func throwingEndsThePassAndReleasesTheLocks(_ storage: Nexus.StorageKind) throws { + let nexus = makeNexus(storage, count: 10) let recorder = Recorder() nexus.delegate = recorder let family = nexus.family(requiresAll: Comp1.self, Comp2.self) @@ -225,8 +225,8 @@ private func makeNexus(count: Int) -> Nexus { } } - @Test func readsOfOtherTypesAndNestedReadPassesAreAllowed() { - let nexus = makeNexus(count: 4) + @Test(arguments: Nexus.StorageKind.allCases) func readsOfOtherTypesAndNestedReadPassesAreAllowed(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 4) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) var pairs = 0 family.forEach(reading: Comp1.self, writing: Comp2.self) { outer, write in @@ -238,8 +238,8 @@ private func makeNexus(count: Int) -> Nexus { #expect(pairs == 4) } - @Test func familiesCreatedAfterwardsAndNewMembersAreIncluded() { - let nexus = makeNexus(count: 3) + @Test(arguments: Nexus.StorageKind.allCases) func familiesCreatedAfterwardsAndNewMembersAreIncluded(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 3) let family = nexus.family(requiresAll: Comp1.self) nexus.createEntity(with: Comp1(10)) var seen: [Int] = [] @@ -249,8 +249,8 @@ private func makeNexus(count: Int) -> Nexus { // MARK: Entity - @Test func entityComponentsMatrix() { - let nexus = makeNexus(count: 1) + @Test(arguments: Nexus.StorageKind.allCases) func entityComponentsMatrix(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 1) let entity = nexus.family(requires: Comp1.self).entities.first { _ in true } let subject = entity.unsafelyUnwrapped #expect(subject.components(Comp1.self, Comp2.self, Comp3.self, Comp4.self, writing: Comp5.self, Comp6.self, Comp7.self, Comp8.self) { r1, r2, r3, r4, w1, w2, w3, w4 in @@ -270,15 +270,15 @@ private func makeNexus(count: Int) -> Nexus { #expect(subject.get(Comp5.self)?.value == 3) } - @Test func entityComponentsFailsWhenAComponentIsMissing() { - let nexus = makeNexus(count: 1) + @Test(arguments: Nexus.StorageKind.allCases) func entityComponentsFailsWhenAComponentIsMissing(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage, count: 1) let entity = nexus.createEntity(with: Comp1(1)) #expect(!entity.components(Comp1.self, writing: Comp2.self) { _, _ in Issue.record("called") }) #expect(!entity.components(Comp2.self) { _ in Issue.record("called") }) } - @Test func entityParameterPackFallback() throws { - let nexus = makeNexus(count: 1) + @Test(arguments: Nexus.StorageKind.allCases) func entityParameterPackFallback(_ storage: Nexus.StorageKind) throws { + let nexus = makeNexus(storage, count: 1) let recorder = Recorder() nexus.delegate = recorder let entity = try #require(nexus.family(requires: Comp1.self).entities.first { _ in true }) diff --git a/Tests/FirebladeECSTests/FamilyReadWriteTrapTests.swift b/Tests/FirebladeECSTests/FamilyReadWriteTrapTests.swift index 80d5cc3a..a8974336 100644 --- a/Tests/FirebladeECSTests/FamilyReadWriteTrapTests.swift +++ b/Tests/FirebladeECSTests/FamilyReadWriteTrapTests.swift @@ -12,8 +12,8 @@ import Testing // unavailable on iOS, tvOS, watchOS and WebAssembly. Exit test bodies cannot capture context, so each trap is a free function. #if os(macOS) || os(Linux) || os(Windows) - private func family() -> (Nexus, Family) { - let nexus = Nexus() + private func family(_ storage: Nexus.StorageKind) -> (Nexus, Family) { + let nexus = Nexus(storage: storage) for number in 0 ..< 3 { nexus.createEntity(with: Comp1(number), Comp2(number), Comp3(number)) } @@ -28,114 +28,178 @@ import Testing #endif } - private func creatingAnEntityTrapsBody() { - let (nexus, family) = family() + private func creatingAnEntityTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(reading: Comp1.self, writing: Comp2.self) { _, _ in nexus.createEntity() } } - private func addingAComponentTrapsBody() { - let (nexus, family) = family() + private func addingAComponentTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() family.forEach(reading: Comp1.self) { _ in entity.set(Comp4(1)) } } - private func readingAWrittenTypeThroughTheNexusTrapsBody() { - let (nexus, family) = family() + private func readingAWrittenTypeThroughTheNexusTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(writing: Comp2.self) { _ in _ = family.entities.first { _ in true }?.get(Comp2.self) } } - private func readingAndWritingTheSameTypeTrapsBody() { - let (nexus, family) = family() + private func readingAndWritingTheSameTypeTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(reading: Comp1.self, writing: Comp1.self) { _, _ in } } - private func writingTheSameTypeTwiceTrapsBody() { - let (nexus, family) = family() + private func writingTheSameTypeTwiceTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(writing: Comp1.self, Comp1.self) { _, _ in } } - private func nestedWriteOfTheSameTypeTrapsBody() { - let (nexus, family) = family() + private func nestedWriteOfTheSameTypeTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(writing: Comp1.self) { _ in family.forEach(writing: Comp1.self) { _ in } } } - private func nestedReadOfAWrittenTypeTrapsBody() { - let (nexus, family) = family() + private func nestedReadOfAWrittenTypeTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(writing: Comp1.self) { _ in family.forEach(reading: Comp1.self) { _ in } } } - private func typeNotRequiredByTheFamilyTrapsBody() { - let (nexus, family) = family() + private func typeNotRequiredByTheFamilyTrapsBody(_ storage: Nexus.StorageKind) { + let (nexus, family) = family(storage) defer { withExtendedLifetime(nexus) {} } family.forEach(reading: Comp3.self) { _ in } } @Suite struct FamilyReadWriteTrapTests { - @Test func creatingAnEntityTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - creatingAnEntityTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func creatingAnEntityTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingAnEntityTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingAnEntityTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } - @Test func addingAComponentTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - addingAComponentTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func addingAComponentTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + addingAComponentTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + addingAComponentTrapsBody(.archetype) + } } expectTrapMessage(result, "structural change") } - @Test func readingAWrittenTypeThroughTheNexusTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - readingAWrittenTypeThroughTheNexusTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func readingAWrittenTypeThroughTheNexusTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingAWrittenTypeThroughTheNexusTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingAWrittenTypeThroughTheNexusTrapsBody(.archetype) + } } expectTrapMessage(result, "during a write access") } - @Test func readingAndWritingTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - readingAndWritingTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func readingAndWritingTheSameTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingAndWritingTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingAndWritingTheSameTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "during a write access") } - @Test func writingTheSameTypeTwiceTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - writingTheSameTypeTwiceTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func writingTheSameTypeTwiceTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + writingTheSameTypeTwiceTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + writingTheSameTypeTwiceTrapsBody(.archetype) + } } expectTrapMessage(result, "nested write access") } - @Test func nestedWriteOfTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - nestedWriteOfTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func nestedWriteOfTheSameTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedWriteOfTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedWriteOfTheSameTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "nested write access") } - @Test func nestedReadOfAWrittenTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - nestedReadOfAWrittenTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func nestedReadOfAWrittenTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedReadOfAWrittenTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedReadOfAWrittenTypeTrapsBody(.archetype) + } } expectTrapMessage(result, "during a write access") } - @Test func typeNotRequiredByTheFamilyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - typeNotRequiredByTheFamilyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func typeNotRequiredByTheFamilyTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + typeNotRequiredByTheFamilyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + typeNotRequiredByTheFamilyTrapsBody(.archetype) + } } expectTrapMessage(result, "not required by the family") } diff --git a/Tests/FirebladeECSTests/FamilyTests1.swift b/Tests/FirebladeECSTests/FamilyTests1.swift index 6024a43b..78b3104e 100644 --- a/Tests/FirebladeECSTests/FamilyTests1.swift +++ b/Tests/FirebladeECSTests/FamilyTests1.swift @@ -11,8 +11,8 @@ import Foundation @Suite struct FamilyTests1 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1)) @@ -22,8 +22,8 @@ import Foundation #expect(entity.has(Comp1.self)) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self) #expect(family.isEmpty) @@ -42,8 +42,8 @@ import Foundation #expect(i == count) } - @Test func familyEncoding() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func familyEncoding(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests2.swift b/Tests/FirebladeECSTests/FamilyTests2.swift index 5cb80293..d2d30cf9 100644 --- a/Tests/FirebladeECSTests/FamilyTests2.swift +++ b/Tests/FirebladeECSTests/FamilyTests2.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests2 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2)) @@ -22,8 +22,8 @@ import Testing #expect(entity.has(Comp2.self)) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests3.swift b/Tests/FirebladeECSTests/FamilyTests3.swift index 425921b7..accacdba 100644 --- a/Tests/FirebladeECSTests/FamilyTests3.swift +++ b/Tests/FirebladeECSTests/FamilyTests3.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests3 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2), Comp3(3)) @@ -23,8 +23,8 @@ import Testing #expect(entity.has(Comp3.self)) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests4.swift b/Tests/FirebladeECSTests/FamilyTests4.swift index 6f1d5b17..5a6d403a 100644 --- a/Tests/FirebladeECSTests/FamilyTests4.swift +++ b/Tests/FirebladeECSTests/FamilyTests4.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests4 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2), Comp3(3), Comp4(4)) @@ -20,8 +20,8 @@ import Testing #expect(entity.numComponents == 4) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests5.swift b/Tests/FirebladeECSTests/FamilyTests5.swift index 8c7de8a5..12f5f3d3 100644 --- a/Tests/FirebladeECSTests/FamilyTests5.swift +++ b/Tests/FirebladeECSTests/FamilyTests5.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests5 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2), Comp3(3), Comp4(4), Comp5(5)) @@ -20,8 +20,8 @@ import Testing #expect(entity.numComponents == 5) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests6.swift b/Tests/FirebladeECSTests/FamilyTests6.swift index 81138494..c4532efc 100644 --- a/Tests/FirebladeECSTests/FamilyTests6.swift +++ b/Tests/FirebladeECSTests/FamilyTests6.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests6 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2), Comp3(3), Comp4(4), Comp5(5), Comp6(6)) @@ -20,8 +20,8 @@ import Testing #expect(entity.numComponents == 6) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests7.swift b/Tests/FirebladeECSTests/FamilyTests7.swift index fe966041..0b2fab16 100644 --- a/Tests/FirebladeECSTests/FamilyTests7.swift +++ b/Tests/FirebladeECSTests/FamilyTests7.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests7 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self, Comp7.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2), Comp3(3), Comp4(4), Comp5(5), Comp6(6), Comp7(7)) @@ -20,8 +20,8 @@ import Testing #expect(entity.numComponents == 7) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self, Comp7.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTests8.swift b/Tests/FirebladeECSTests/FamilyTests8.swift index e0f4cfe6..60f2abd7 100644 --- a/Tests/FirebladeECSTests/FamilyTests8.swift +++ b/Tests/FirebladeECSTests/FamilyTests8.swift @@ -10,8 +10,8 @@ import Testing @Suite struct FamilyTests8 { - @Test func memberCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func memberCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self, Comp7.self, Comp8.self) #expect(family.isEmpty) let entity = family.createMember(with: Comp1(1), Comp2(2), Comp3(3), Comp4(4), Comp5(5), Comp6(6), Comp7(7), Comp8(8)) @@ -20,8 +20,8 @@ import Testing #expect(entity.numComponents == 8) } - @Test func componentIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Comp1.self, Comp2.self, Comp3.self, Comp4.self, Comp5.self, Comp6.self, Comp7.self, Comp8.self) #expect(family.isEmpty) diff --git a/Tests/FirebladeECSTests/FamilyTraitsProvidingTests.swift b/Tests/FirebladeECSTests/FamilyTraitsProvidingTests.swift index 15f85d34..5cd46ea6 100644 --- a/Tests/FirebladeECSTests/FamilyTraitsProvidingTests.swift +++ b/Tests/FirebladeECSTests/FamilyTraitsProvidingTests.swift @@ -43,8 +43,8 @@ private struct Plain: FamilyTraitsProviding { } @Suite struct FamilyTraitsProvidingTests { - @Test func boundFamilyTracksMembership() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func boundFamilyTracksMembership(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let movers = Movers(in: nexus) #expect(movers.isEmpty) let entity = nexus.createEntity(with: Comp1(1), Comp2(2)) @@ -56,8 +56,8 @@ private struct Plain: FamilyTraitsProviding { #expect(!movers.isMember(entity)) } - @Test func familyIsCachedPerNexus() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func familyIsCachedPerNexus(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) _ = Movers(in: nexus) let families = nexus.numFamilies _ = Movers(in: nexus) @@ -66,9 +66,9 @@ private struct Plain: FamilyTraitsProviding { #expect(Movers(in: nexus).family == nexus.family(requiresAll: Comp1.self, Comp2.self, excludesAll: Frozen.self)) } - @Test func sameDeclarationBindsToAnotherNexus() { - let first = Nexus() - let second = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func sameDeclarationBindsToAnotherNexus(_ storage: Nexus.StorageKind) { + let first = Nexus(storage: storage) + let second = Nexus(storage: storage) first.createEntity(with: Comp1(1), Comp2(2)) #expect(Movers(in: first).count == 1) #expect(Movers(in: second).count == 0) @@ -95,8 +95,8 @@ private struct Plain: FamilyTraitsProviding { #expect(traits(Movers.self) == nexusTraits) } - @Test func clearRebindsTheFamily() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func clearRebindsTheFamily(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) _ = Movers(in: nexus) nexus.createEntity(with: Comp1(1), Comp2(2)) nexus.clear() @@ -105,8 +105,8 @@ private struct Plain: FamilyTraitsProviding { #expect(Movers(in: nexus).count == 1) } - @Test func plainConformanceUsesLazyIndex() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func plainConformanceUsesLazyIndex(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.createEntity(with: Comp3(3)) #expect(Plain(in: nexus).count == 1) } diff --git a/Tests/FirebladeECSTests/FamilyTraitsTests.swift b/Tests/FirebladeECSTests/FamilyTraitsTests.swift index 0aa77a31..319474c7 100644 --- a/Tests/FirebladeECSTests/FamilyTraitsTests.swift +++ b/Tests/FirebladeECSTests/FamilyTraitsTests.swift @@ -19,8 +19,8 @@ import Testing #expect(t1.hashValue == t2.hashValue) } - @Test func traitMatching() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func traitMatching(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let a = nexus.createEntity() a.set(Position(x: 1, y: 2)) a.set(Name(name: "myName")) diff --git a/Tests/FirebladeECSTests/GenerationTests.swift b/Tests/FirebladeECSTests/GenerationTests.swift index 6a675a0b..69b28b53 100644 --- a/Tests/FirebladeECSTests/GenerationTests.swift +++ b/Tests/FirebladeECSTests/GenerationTests.swift @@ -21,8 +21,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { @Suite struct GenerationTests { /// A nexus whose first entity was destroyed and whose slot was reused: `stale` is `1v0`, `reused` is `1v1`. - private func makeReusedSlot() -> (nexus: Nexus, stale: Entity, reused: Entity) { - let nexus = Nexus() + private func makeReusedSlot(_ storage: Nexus.StorageKind) -> (nexus: Nexus, stale: Entity, reused: Entity) { + let nexus = Nexus(storage: storage) nexus.assertsOnStaleHandles = false let stale = nexus.createEntity(with: Position(x: 1, y: 1)) nexus.destroy(entity: stale) @@ -30,8 +30,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { return (nexus, stale, reused) } - @Test func destroyBumpsGenerationOfReusedSlot() { - let (nexus, stale, reused) = makeReusedSlot() + @Test(arguments: Nexus.StorageKind.allCases) func destroyBumpsGenerationOfReusedSlot(_ storage: Nexus.StorageKind) { + let (nexus, stale, reused) = makeReusedSlot(storage) #expect(nexus.numEntities == 1) #expect(stale.identifier == EntityIdentifier(index: 1, generation: 0)) #expect(reused.identifier == EntityIdentifier(index: 1, generation: 1)) @@ -39,16 +39,16 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(stale != reused) } - @Test func staleIdentifierIsNotAlive() throws { - let (nexus, stale, reused) = makeReusedSlot() + @Test(arguments: Nexus.StorageKind.allCases) func staleIdentifierIsNotAlive(_ storage: Nexus.StorageKind) throws { + let (nexus, stale, reused) = makeReusedSlot(storage) #expect(!nexus.isAlive(stale.identifier)) #expect(nexus.isAlive(reused.identifier)) #expect(nexus.entity(stale.identifier) == nil) #expect(try #require(nexus.entity(reused.identifier)) == reused) } - @Test func invalidAndUnknownIdentifiersAreNotAlive() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func invalidAndUnknownIdentifiersAreNotAlive(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.createEntity() #expect(!nexus.isAlive(.invalid)) #expect(nexus.entity(.invalid) == nil) @@ -56,8 +56,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(!nexus.isAlive(EntityIdentifier(index: 1, generation: 1))) } - @Test func staleHandleNeverSeesTheNewEntity() { - let (nexus, stale, reused) = makeReusedSlot() + @Test(arguments: Nexus.StorageKind.allCases) func staleHandleNeverSeesTheNewEntity(_ storage: Nexus.StorageKind) { + let (nexus, stale, reused) = makeReusedSlot(storage) #expect(!stale.has(Position.self)) #expect(stale.get(Position.self) == nil) #expect(stale.numComponents == 0) @@ -69,8 +69,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(nexus.isMember(reused, in: FamilyTraitSet(requiresAll: [Name.self], excludesAll: []))) } - @Test func mutatingStaleHandleReturnsFalseAndKeepsTheNewEntity() { - let (nexus, stale, reused) = makeReusedSlot() + @Test(arguments: Nexus.StorageKind.allCases) func mutatingStaleHandleReturnsFalseAndKeepsTheNewEntity(_ storage: Nexus.StorageKind) { + let (nexus, stale, reused) = makeReusedSlot(storage) #expect(!nexus.assign(component: Position(x: 2, y: 2), entityId: stale.identifier)) #expect(!nexus.assign(components: [Position(x: 2, y: 2)], to: stale.identifier)) @@ -90,8 +90,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(nexus.numComponents == 1) } - @Test func mutatingDestroyedEntityWithoutReuseReturnsFalse() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func mutatingDestroyedEntityWithoutReuseReturnsFalse(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.assertsOnStaleHandles = false let entity = nexus.createEntity() nexus.destroy(entity: entity) @@ -101,8 +101,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(nexus.numEntities == 0) } - @Test func staleMutationNamesBothVersions() { - let (nexus, stale, _) = makeReusedSlot() + @Test(arguments: Nexus.StorageKind.allCases) func staleMutationNamesBothVersions(_ storage: Nexus.StorageKind) { + let (nexus, stale, _) = makeReusedSlot(storage) let recorder = ErrorRecorder() nexus.delegate = recorder stale.set(Position(x: 1, y: 1)) @@ -114,8 +114,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(recorder.messages.last == "stale handle 2v0, slot is free") } - @Test func slotIsReusedWithIncreasingGenerations() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func slotIsReusedWithIncreasingGenerations(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) var identifier = nexus.createEntity().identifier var seen: Set = [identifier] for generation in 1 ..< 100 { @@ -127,8 +127,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { } } - @Test func slotIsRetiredAtMaxGeneration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func slotIsRetiredAtMaxGeneration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) // injection: the first entity starts three generations below the limit let start = EntityIdentifier(index: 1, generation: .max - 3) nexus.entityIdGenerator = DefaultEntityIdGenerator(startProviding: [start]) @@ -161,8 +161,8 @@ private final class ErrorRecorder: NexusEventDelegate, @unchecked Sendable { #expect(gen.nextId() == EntityIdentifier(index: 1, generation: 0)) } - @Test func eventsCarryTheFullIdentifier() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func eventsCarryTheFullIdentifier(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = EventRecorder() nexus.delegate = recorder let first = nexus.createEntity() diff --git a/Tests/FirebladeECSTests/InspectableComponentTests.swift b/Tests/FirebladeECSTests/InspectableComponentTests.swift index 2836d894..6828b01e 100644 --- a/Tests/FirebladeECSTests/InspectableComponentTests.swift +++ b/Tests/FirebladeECSTests/InspectableComponentTests.swift @@ -53,8 +53,8 @@ struct InspectedTransform: InspectableComponent, RegistrableComponent, Sendable #expect(component.name == "transform") } - @Test func typeErasedInspection() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func typeErasedInspection(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity(with: InspectedTransform()) let component = try #require(entity.makeComponentsIterator().first(where: { _ in true })) let inspectable = try #require(component as? any InspectableComponent) diff --git a/Tests/FirebladeECSTests/MutationTests.swift b/Tests/FirebladeECSTests/MutationTests.swift index 6ccf4f1f..443bdf19 100644 --- a/Tests/FirebladeECSTests/MutationTests.swift +++ b/Tests/FirebladeECSTests/MutationTests.swift @@ -72,8 +72,8 @@ private final class Recorder: NexusEventDelegate, @unchecked Sendable { } } -private func makeNexus() -> Nexus { - let nexus = Nexus() +private func makeNexus(_ storage: Nexus.StorageKind) -> Nexus { + let nexus = Nexus(storage: storage) nexus.assertsOnStaleHandles = false return nexus } @@ -81,8 +81,8 @@ private func makeNexus() -> Nexus { @Suite struct MutationTests { // MARK: modify - @Test func modifyChangesInPlaceAndLosesNoWrites() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func modifyChangesInPlaceAndLosesNoWrites(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Counter()) for _ in 0..<1000 { @@ -93,8 +93,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Counter.self)?.value == 3000) } - @Test func modifyOfMissingComponentReturnsFalseAndSkipsBody() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func modifyOfMissingComponentReturnsFalseAndSkipsBody(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() @@ -104,8 +104,8 @@ private func makeNexus() -> Nexus { #expect(!entity.has(Health.self)) } - @Test func modifyDefaultEvaluatesDefaultOnlyWhenMissing() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func modifyDefaultEvaluatesDefaultOnlyWhenMissing(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() @@ -122,8 +122,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Health.self)?.hp == 9) } - @Test func modifyOfDestroyedEntityIsANoOp() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func modifyOfDestroyedEntityIsANoOp(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let stale = nexus.createEntity(with: Health()) stale.destroy() @@ -138,8 +138,8 @@ private func makeNexus() -> Nexus { #expect(nexus.numComponents == 0) } - @Test func modifyRethrowsAndStillReportsTheChange() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func modifyRethrowsAndStillReportsTheChange(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let recorder = Recorder() let entity = nexus.createEntity(with: Health()) @@ -158,8 +158,8 @@ private func makeNexus() -> Nexus { // MARK: subscripts - @Test func optionalSubscriptReadsACopyAndNeverInserts() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func optionalSubscriptReadsACopyAndNeverInserts(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() @@ -170,8 +170,8 @@ private func makeNexus() -> Nexus { #expect(entity.numComponents == 0) } - @Test func optionalSubscriptMutatesInPlace() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func optionalSubscriptMutatesInPlace(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) @@ -185,8 +185,8 @@ private func makeNexus() -> Nexus { #expect(!entity.has(Health.self)) } - @Test func optionalSubscriptAssigningNilThroughInoutRemoves() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func optionalSubscriptAssigningNilThroughInoutRemoves(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let recorder = Recorder() let entity = nexus.createEntity(with: Health(), Counter()) @@ -206,8 +206,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Counter.self)?.value == 0) } - @Test func optionalSubscriptAssigningAValueThroughInoutToAMissingComponentAdds() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func optionalSubscriptAssigningAValueThroughInoutToAMissingComponentAdds(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() @@ -216,8 +216,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Health.self)?.hp == 4) } - @Test func defaultSubscriptReadsWithoutInserting() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func defaultSubscriptReadsWithoutInserting(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() @@ -225,8 +225,8 @@ private func makeNexus() -> Nexus { #expect(!entity.has(Counter.self)) } - @Test func defaultSubscriptInsertsWhenMutating() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func defaultSubscriptInsertsWhenMutating(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity() @@ -236,8 +236,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Counter.self)?.value == 2) } - @Test func defaultSubscriptEvaluatesDefaultOnlyWhenNeeded() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func defaultSubscriptEvaluatesDefaultOnlyWhenNeeded(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Counter()) @@ -251,8 +251,8 @@ private func makeNexus() -> Nexus { #expect(made == 0) } - @Test func defaultSubscriptOfDestroyedEntityDoesNotInsert() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func defaultSubscriptOfDestroyedEntityDoesNotInsert(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let stale = nexus.createEntity() stale.destroy() @@ -264,8 +264,8 @@ private func makeNexus() -> Nexus { // MARK: key paths - @Test func writableKeyPathChangesTheStoredComponent() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func writableKeyPathChangesTheStoredComponent(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) @@ -278,8 +278,8 @@ private func makeNexus() -> Nexus { #expect(entity[\Health.armor] == nil) } - @Test func writableKeyPathAddsADefaultComponent() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func writableKeyPathAddsADefaultComponent(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let recorder = Recorder() nexus.delegate = recorder @@ -290,8 +290,8 @@ private func makeNexus() -> Nexus { #expect(entity.set(value: 4, for: \Health.hp)) } - @Test func writableKeyPathOnDestroyedEntityReturnsFalse() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func writableKeyPathOnDestroyedEntityReturnsFalse(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let stale = nexus.createEntity() stale.destroy() @@ -301,8 +301,8 @@ private func makeNexus() -> Nexus { // MARK: set and remove - @Test func setReplacesAnExistingComponent() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func setReplacesAnExistingComponent(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health(hp: 1)) @@ -312,8 +312,8 @@ private func makeNexus() -> Nexus { #expect(entity.numComponents == 1) } - @Test func removeReturnsWhetherAComponentWasRemoved() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func removeReturnsWhetherAComponentWasRemoved(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) @@ -324,8 +324,8 @@ private func makeNexus() -> Nexus { #expect(!entity.remove(Health.identifier)) } - @Test func removeOfDestroyedEntityReturnsFalse() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func removeOfDestroyedEntityReturnsFalse(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let stale = nexus.createEntity(with: Health()) stale.destroy() @@ -334,8 +334,8 @@ private func makeNexus() -> Nexus { // MARK: events - @Test func eventsPerOperation() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func eventsPerOperation(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let recorder = Recorder() nexus.delegate = recorder @@ -367,13 +367,13 @@ private func makeNexus() -> Nexus { #expect(recorder.names.count == 9) } - @Test func subscribingToStructuralEventsOnlyHidesValueChanges() { + @Test(arguments: Nexus.StorageKind.allCases) func subscribingToStructuralEventsOnlyHidesValueChanges(_ storage: Nexus.StorageKind) { final class Structural: NexusEventDelegate, @unchecked Sendable { var count = 0 func nexusEvent(_: NexusEvent) { count += 1 } func nexusNonFatalError(_: String) {} } - let nexus = makeNexus() + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let delegate = Structural() nexus.delegate = delegate @@ -388,8 +388,8 @@ private func makeNexus() -> Nexus { // MARK: components(_:writing:) - @Test func componentsReadsCopiesAndWritesInPlace() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentsReadsCopiesAndWritesInPlace(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Position(x: 2, y: 3), Speed(dx: 1, dy: 1)) @@ -401,8 +401,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Speed.self)?.dy == 4) } - @Test func componentsWritesSeveralComponents() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentsWritesSeveralComponents(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Position(x: 0, y: 0), Speed(dx: 1, dy: 2), Health()) @@ -420,8 +420,8 @@ private func makeNexus() -> Nexus { #expect(entity.get(Position.self)?.x == 5) } - @Test func componentsWithAMissingComponentDoesNothing() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentsWithAMissingComponentDoesNothing(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let recorder = Recorder() let entity = nexus.createEntity(with: Position(x: 1, y: 1)) @@ -433,8 +433,8 @@ private func makeNexus() -> Nexus { #expect(recorder.names.isEmpty) } - @Test func componentsEmitsModifiedPerWrittenComponent() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentsEmitsModifiedPerWrittenComponent(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let recorder = Recorder() let entity = nexus.createEntity(with: Position(x: 1, y: 1), Speed(dx: 0, dy: 0)) @@ -445,8 +445,8 @@ private func makeNexus() -> Nexus { #expect(recorder.names == ["modified", "modified", "modified"]) } - @Test func componentsOfDestroyedEntityReturnsFalse() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentsOfDestroyedEntityReturnsFalse(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let stale = nexus.createEntity(with: Position(x: 1, y: 1), Speed(dx: 0, dy: 0)) stale.destroy() @@ -457,8 +457,8 @@ private func makeNexus() -> Nexus { /// The 2.0 contract: `get` is a copy, so changing a `var` copy never changes the entity; only `set` and /// `modify` do. - @Test func valueCopyIsDetached() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func valueCopyIsDetached(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) @@ -471,8 +471,8 @@ private func makeNexus() -> Nexus { /// The hazard of plain iteration: the loop variables are copies, so `for var` compiles and silently changes nothing. /// Writes go through `modify`, `set` or the subscripts of the entity. - @Test func mutatingTheLoopVariableOfAFamilyChangesNothing() { - let nexus = makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func mutatingTheLoopVariableOfAFamilyChangesNothing(_ storage: Nexus.StorageKind) { + let nexus = makeNexus(storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Speed(dx: 1, dy: 2)) let family = nexus.family(requires: Speed.self) @@ -501,116 +501,180 @@ private func makeNexus() -> Nexus { #endif } - private func addingInsideModifyTrapsBody() { - let nexus = Nexus() + private func addingInsideModifyTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health(), Counter()) entity.modify(Health.self) { _ in entity.set(Speed(dx: 0, dy: 0)) } } - private func removingInsideModifyTrapsBody() { - let nexus = Nexus() + private func removingInsideModifyTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health(), Counter()) entity.modify(Health.self) { _ in entity.remove(Counter.self) } } - private func replacingInsideModifyTrapsBody() { - let nexus = Nexus() + private func replacingInsideModifyTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health(), Counter()) entity.modify(Health.self) { _ in entity.set(Counter()) } } - private func destroyingInsideModifyTrapsBody() { - let nexus = Nexus() + private func destroyingInsideModifyTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) entity.modify(Health.self) { _ in entity.destroy() } } - private func creatingInsideSubscriptModifyTrapsBody() { - let nexus = Nexus() + private func creatingInsideSubscriptModifyTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) func create(_ health: inout Health?) { entity.createEntity() } create(&entity[Health.self]) } - private func readingTheModifiedComponentTrapsBody() { - let nexus = Nexus() + private func readingTheModifiedComponentTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) entity.modify(Health.self) { _ in _ = entity.get(Health.self) } } - private func nestedModifyOfTheSameTypeTrapsBody() { - let nexus = Nexus() + private func nestedModifyOfTheSameTypeTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) entity.modify(Health.self) { _ in entity.modify(Health.self) { _ in } } } - private func writingTheSameTypeTwiceTrapsBody() { - let nexus = Nexus() + private func writingTheSameTypeTwiceTrapsBody(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) defer { withExtendedLifetime(nexus) {} } let entity = nexus.createEntity(with: Health()) entity.components(writing: Health.self, Health.self) { _, _ in } } @Suite struct MutationTrapTests { - @Test func addingInsideModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - addingInsideModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func addingInsideModifyTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + addingInsideModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + addingInsideModifyTrapsBody(.archetype) + } } trapMessage(result, "structural change") } - @Test func removingInsideModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - removingInsideModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func removingInsideModifyTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + removingInsideModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + removingInsideModifyTrapsBody(.archetype) + } } trapMessage(result, "structural change") } - @Test func replacingInsideModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - replacingInsideModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func replacingInsideModifyTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + replacingInsideModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + replacingInsideModifyTrapsBody(.archetype) + } } trapMessage(result, "structural change") } - @Test func destroyingInsideModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - destroyingInsideModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func destroyingInsideModifyTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + destroyingInsideModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + destroyingInsideModifyTrapsBody(.archetype) + } } trapMessage(result, "structural change") } - @Test func creatingInsideSubscriptModifyTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - creatingInsideSubscriptModifyTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func creatingInsideSubscriptModifyTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingInsideSubscriptModifyTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + creatingInsideSubscriptModifyTrapsBody(.archetype) + } } trapMessage(result, "structural change") } - @Test func readingTheModifiedComponentTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - readingTheModifiedComponentTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func readingTheModifiedComponentTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingTheModifiedComponentTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + readingTheModifiedComponentTrapsBody(.archetype) + } } trapMessage(result, "during a write access") } - @Test func nestedModifyOfTheSameTypeTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - nestedModifyOfTheSameTypeTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func nestedModifyOfTheSameTypeTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedModifyOfTheSameTypeTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + nestedModifyOfTheSameTypeTrapsBody(.archetype) + } } trapMessage(result, "nested write access") } - @Test func writingTheSameTypeTwiceTraps() async { - let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { - writingTheSameTypeTwiceTrapsBody() + @Test(arguments: Nexus.StorageKind.allCases) func writingTheSameTypeTwiceTraps(_ storage: Nexus.StorageKind) async { + let result: ExitTest.Result? + switch storage { + case .sparseSet: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + writingTheSameTypeTwiceTrapsBody(.sparseSet) + } + case .archetype: + result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { + writingTheSameTypeTwiceTrapsBody(.archetype) + } } trapMessage(result, "nested write access") } diff --git a/Tests/FirebladeECSTests/NexusCloneTests.swift b/Tests/FirebladeECSTests/NexusCloneTests.swift index c81b853f..83416292 100644 --- a/Tests/FirebladeECSTests/NexusCloneTests.swift +++ b/Tests/FirebladeECSTests/NexusCloneTests.swift @@ -51,8 +51,8 @@ struct CloneChildren: CloneableComponent, Sendable { } @Suite struct NexusCloneTests { - @Test func cloneEntityCopiesValuesAndSharesReferences() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func cloneEntityCopiesValuesAndSharesReferences(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let payload = SharedPayload() let source = nexus.createEntity(with: CloneValue(value: 3, payload: payload)) @@ -73,8 +73,8 @@ struct CloneChildren: CloneableComponent, Sendable { #expect(clone.get(CloneValue.self)?.value == 4) } - @Test func cloneEntityRemapsSelfReferenceAndKeepsExternal() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func cloneEntityRemapsSelfReferenceAndKeepsExternal(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let external = nexus.createEntity() let source = nexus.createEntity() source.set(CloneParent(parent: external), CloneChildren(children: [external, source], self_: source.identifier)) @@ -88,8 +88,8 @@ struct CloneChildren: CloneableComponent, Sendable { #expect(children.self_ == clone.identifier) } - @Test func cloneEntitiesRemapsReferencesWithinGroup() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func cloneEntitiesRemapsReferencesWithinGroup(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let root = nexus.createEntity() let child = nexus.createEntity(with: CloneParent(parent: root)) root.set(CloneChildren(children: [child], self_: root.identifier)) @@ -105,14 +105,14 @@ struct CloneChildren: CloneableComponent, Sendable { #expect(clonedParent.parent == clones[0]) } - @Test func cloneIntoTargetRemapsAllReferences() throws { - let source = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func cloneIntoTargetRemapsAllReferences(_ storage: Nexus.StorageKind) throws { + let source = Nexus(storage: storage) let root = source.createEntity() let child = source.createEntity(with: CloneParent(parent: root), CloneValue(value: 1, payload: SharedPayload())) root.set(CloneChildren(children: [child], self_: root.identifier)) source.createEntity() - let target = Nexus() + let target = Nexus(storage: storage) let mapping = try source.clone(into: target) #expect(target.numEntities == 3) @@ -125,8 +125,8 @@ struct CloneChildren: CloneableComponent, Sendable { #expect(target.family(requires: CloneParent.self).count == 1) } - @Test func nonCloneableComponentThrowsWithoutSideEffects() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func nonCloneableComponentThrowsWithoutSideEffects(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let cloneable = nexus.createEntity(with: CloneValue(value: 1, payload: SharedPayload())) let plain = nexus.createEntity(with: Position(x: 1, y: 2)) @@ -135,15 +135,15 @@ struct CloneChildren: CloneableComponent, Sendable { } #expect(nexus.numEntities == 2) - let target = Nexus() + let target = Nexus(storage: storage) #expect(throws: ComponentCloneError.notCloneable(typeName: "FirebladeECSTests.Position")) { try nexus.clone(into: target) } #expect(target.numEntities == 0) } - @Test func unknownEntityThrows() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func unknownEntityThrows(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity() nexus.destroy(entity: entity) #expect(throws: ComponentCloneError.unknownEntity(entity.identifier)) { @@ -151,8 +151,8 @@ struct CloneChildren: CloneableComponent, Sendable { } } - @Test func cloneContextIdentityForUnmappedValues() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func cloneContextIdentityForUnmappedValues(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let context = ComponentCloneContext(nexus: nexus, mapping: [1: 5]) #expect(context.clone(42) == 42) #expect(context.clone("text") == "text") diff --git a/Tests/FirebladeECSTests/NexusDeterministicOrderTests.swift b/Tests/FirebladeECSTests/NexusDeterministicOrderTests.swift index 0d31910d..ab262aa6 100644 --- a/Tests/FirebladeECSTests/NexusDeterministicOrderTests.swift +++ b/Tests/FirebladeECSTests/NexusDeterministicOrderTests.swift @@ -11,8 +11,8 @@ import Testing @Suite struct NexusDeterministicOrderTests { private static let entityCount = 256 - @Test func familyCreatedAfterEntitiesHasDeterministicOrder() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func familyCreatedAfterEntitiesHasDeterministicOrder(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let expected = (0.. (nexus: Nexus, root: Entity, child: Entity, sibling: Entity) { - let nexus = Nexus() + private func makeScene(_ storage: Nexus.StorageKind) -> (nexus: Nexus, root: Entity, child: Entity, sibling: Entity) { + let nexus = Nexus(storage: storage) let root = nexus.createEntity(with: SerialPosition(x: 1, y: 2)) let destroyed = nexus.createEntity() let child = nexus.createEntity(with: SerialParent(parent: root), SerialTarget(target: root.identifier)) @@ -185,28 +185,28 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { } } - @Test(arguments: SerialFormat.all) - func snapshotListsEveryCopyAndNeverWritesSharedComponents(format: SerialFormat) throws { - let source = makeScene() + @Test(arguments: Nexus.StorageKind.allCases, SerialFormat.all) + func snapshotListsEveryCopyAndNeverWritesSharedComponents(_ storage: Nexus.StorageKind, format: SerialFormat) throws { + let source = makeScene(storage) let data = try format.encodeSnapshot(source.nexus) #expect(!String(decoding: data, as: UTF8.self).contains("sharedComponents")) } - @Test(arguments: SerialFormat.all) - func restoreSnapshotPreservesIdentity(format: SerialFormat) throws { - let source = makeScene() + @Test(arguments: Nexus.StorageKind.allCases, SerialFormat.all) + func restoreSnapshotPreservesIdentity(_ storage: Nexus.StorageKind, format: SerialFormat) throws { + let source = makeScene(storage) let data = try format.encodeSnapshot(source.nexus) - let restored = Nexus() + let restored = Nexus(storage: storage) try restored.register(Self.componentTypes.map { $0 as any RegistrableComponent.Type }) try format.restoreSnapshot(data, restored) try expectRestored(restored, matches: source) } - @Test(arguments: SerialFormat.all) - func nexusCodableRoundTrip(format: SerialFormat) throws { - let source = makeScene() + @Test(arguments: Nexus.StorageKind.allCases, SerialFormat.all) + func nexusCodableRoundTrip(_ storage: Nexus.StorageKind, format: SerialFormat) throws { + let source = makeScene(storage) let data = try format.encode(SerialScene(name: "Level 1", nexus: source.nexus), [:]) let scene = try format.decodeScene(data, [ @@ -218,14 +218,14 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { try expectRestored(scene.nexus, matches: source) } - @Test func repeatedRoundTripsAreStable() throws { + @Test(arguments: Nexus.StorageKind.allCases) func repeatedRoundTripsAreStable(_ storage: Nexus.StorageKind) throws { // Sorted keys make the output deterministic, e.g. for scene files under version control. let format = SerialFormat.make("Sorted JSON", encoder: { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys] return encoder }, decoder: { JSONDecoder() }) - let first = try format.encode(SerialScene(name: "Level", nexus: makeScene().nexus), [:]) + let first = try format.encode(SerialScene(name: "Level", nexus: makeScene(storage).nexus), [:]) let userInfo: [CodingUserInfoKey: any Sendable] = [ .nexusComponentTypes: Self.componentTypes, .nexusEntityResolver: EntityReferenceResolver(mapping: [:]) @@ -236,13 +236,13 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(try format.decodeScene(first, userInfo).nexus.numComponents == format.decodeScene(third, userInfo).nexus.numComponents) } - @Test func restoreEmitsEvents() throws { - let source = makeScene() + @Test(arguments: Nexus.StorageKind.allCases) func restoreEmitsEvents(_ storage: Nexus.StorageKind) throws { + let source = makeScene(storage) var encoder = JSONEncoder() let data = try source.nexus.encodeSnapshot(using: &encoder, handling: .throwError) let recorder = SerialEventRecorder() - let restored = Nexus() + let restored = Nexus(storage: storage) restored.delegate = recorder try restored.register(Self.componentTypes.map { $0 as any RegistrableComponent.Type }) var decoder = JSONDecoder() @@ -252,8 +252,8 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(recorder.addedComponents == source.nexus.numComponents) } - @Test func nexusEncodingThrowsForNonSerializableComponentsByDefault() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func nexusEncodingThrowsForNonSerializableComponentsByDefault(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) nexus.createEntity(with: SerialPosition(x: 1, y: 1), Position(x: 2, y: 2)) #expect(throws: ComponentSerializationError.notSerializable(typeName: "FirebladeECSTests.Position")) { @@ -268,16 +268,16 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(restored.numComponents == 1) } - @Test func nexusDecodingWithoutComponentTypesThrows() throws { - let data = try JSONEncoder().encode(makeScene().nexus) + @Test(arguments: Nexus.StorageKind.allCases) func nexusDecodingWithoutComponentTypesThrows(_ storage: Nexus.StorageKind) throws { + let data = try JSONEncoder().encode(makeScene(storage).nexus) #expect(throws: ComponentRegistryError.self) { try JSONDecoder().decode(Nexus.self, from: data) } } - @Test func renamedTypeLoadsByAlias() throws { + @Test(arguments: Nexus.StorageKind.allCases) func renamedTypeLoadsByAlias(_ storage: Nexus.StorageKind) throws { let data = Data(#"{"formatVersion":1,"entities":[{"id":0,"components":{"OldHealth":{"value":3}}}]}"#.utf8) - let nexus = Nexus() + let nexus = Nexus(storage: storage) try nexus.register(SerialHealth.self) var decoder = JSONDecoder() try nexus.restoreSnapshot(from: data, using: &decoder) @@ -289,8 +289,8 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(snapshot.members.first.map { type(of: $0.components[0]).componentTypeName } == "Health") } - @Test func conflictingAliasThrows() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func conflictingAliasThrows(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register(SerialHealth.self) #expect(throws: ComponentRegistryError.duplicateTypeName("Health")) { try nexus.register(SerialConflictingHealth.self) @@ -298,9 +298,9 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(!nexus.isRegistered(SerialConflictingHealth.self)) } - @Test func formatVersion1RestoresIdentity() throws { + @Test(arguments: Nexus.StorageKind.allCases) func formatVersion1RestoresIdentity(_ storage: Nexus.StorageKind) throws { let data = Data(#"{"formatVersion":1,"entities":[{"id":0,"components":{"Health":{"value":1}}},{"id":2,"components":{"Health":{"value":2}}}]}"#.utf8) - let nexus = Nexus() + let nexus = Nexus(storage: storage) try nexus.register(SerialHealth.self) var decoder = JSONDecoder() try nexus.restoreSnapshot(from: data, using: &decoder) @@ -313,10 +313,10 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(nexus.createEntity().identifier == 4) } - @Test func restoreIntoNonEmptyNexusThrowsWithoutSideEffects() throws { + @Test(arguments: Nexus.StorageKind.allCases) func restoreIntoNonEmptyNexusThrowsWithoutSideEffects(_ storage: Nexus.StorageKind) throws { var encoder = JSONEncoder() - let data = try makeScene().nexus.encodeSnapshot(using: &encoder, handling: .throwError) - let nexus = Nexus() + let data = try makeScene(storage).nexus.encodeSnapshot(using: &encoder, handling: .throwError) + let nexus = Nexus(storage: storage) let existing = nexus.createEntity(with: SerialHealth(value: 1)) var decoder = JSONDecoder() #expect(throws: ComponentSerializationError.nexusNotEmpty) { @@ -326,10 +326,10 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(existing.get(SerialHealth.self)?.value == 1) } - @Test func failedRestoreRollsBack() throws { + @Test(arguments: Nexus.StorageKind.allCases) func failedRestoreRollsBack(_ storage: Nexus.StorageKind) throws { var encoder = JSONEncoder() - let data = try makeScene().nexus.encodeSnapshot(using: &encoder, handling: .throwError) - let nexus = Nexus() + let data = try makeScene(storage).nexus.encodeSnapshot(using: &encoder, handling: .throwError) + let nexus = Nexus(storage: storage) try nexus.register(SerialPosition.self) var decoder = JSONDecoder() #expect(throws: ComponentRegistryError.unknownTypeName("Parent")) { @@ -340,9 +340,9 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(nexus.createEntity().identifier == 1) } - @Test func missingSharedOwnerThrows() throws { + @Test(arguments: Nexus.StorageKind.allCases) func missingSharedOwnerThrows(_ storage: Nexus.StorageKind) throws { let data = Data(#"{"formatVersion":2,"entities":[{"id":0,"components":{}},{"id":1,"components":{},"sharedComponents":{"Health":0}}]}"#.utf8) - let nexus = Nexus() + let nexus = Nexus(storage: storage) try nexus.register(SerialHealth.self) var decoder = JSONDecoder() #expect(throws: DecodingError.self) { @@ -351,9 +351,9 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(nexus.numEntities == 0) } - @Test func duplicateEntityIdentifierThrows() throws { + @Test(arguments: Nexus.StorageKind.allCases) func duplicateEntityIdentifierThrows(_ storage: Nexus.StorageKind) throws { let data = Data(#"{"formatVersion":2,"entities":[{"id":0,"components":{}},{"id":0,"components":{}}]}"#.utf8) - let nexus = Nexus() + let nexus = Nexus(storage: storage) var decoder = JSONDecoder() #expect(throws: DecodingError.self) { try nexus.decodeSnapshot(from: data, using: &decoder) @@ -361,8 +361,8 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(nexus.numEntities == 0) } - @Test func nonPersistableGeneratorKeepsIdentity() throws { - let source = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func nonPersistableGeneratorKeepsIdentity(_ storage: Nexus.StorageKind) throws { + let source = Nexus(storage: storage) source.entityIdGenerator = NonPersistableEntityIdGenerator(startProviding: []) source.createEntity() let kept = source.createEntity(with: SerialHealth(value: 4)) @@ -371,7 +371,7 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { let data = try source.encodeSnapshot(using: &encoder, handling: .throwError) #expect(!String(decoding: data, as: UTF8.self).contains("entityIdGenerator")) - let restored = Nexus() + let restored = Nexus(storage: storage) restored.entityIdGenerator = NonPersistableEntityIdGenerator(startProviding: []) try restored.register(SerialHealth.self) var decoder = JSONDecoder() @@ -380,10 +380,10 @@ struct SerialFormat: CustomTestStringConvertible, @unchecked Sendable { #expect(restored.entityIdGenerator is NonPersistableEntityIdGenerator) } - @Test func generatorStateIsIgnoredForNonPersistableGenerator() throws { + @Test(arguments: Nexus.StorageKind.allCases) func generatorStateIsIgnoredForNonPersistableGenerator(_ storage: Nexus.StorageKind) throws { var encoder = JSONEncoder() - let data = try makeScene().nexus.encodeSnapshot(using: &encoder, handling: .throwError) - let restored = Nexus() + let data = try makeScene(storage).nexus.encodeSnapshot(using: &encoder, handling: .throwError) + let restored = Nexus(storage: storage) restored.entityIdGenerator = NonPersistableEntityIdGenerator(startProviding: []) try restored.register(Self.componentTypes.map { $0 as any RegistrableComponent.Type }) var decoder = JSONDecoder() diff --git a/Tests/FirebladeECSTests/NexusSnapshotTests.swift b/Tests/FirebladeECSTests/NexusSnapshotTests.swift index 33dadc07..07f370f9 100644 --- a/Tests/FirebladeECSTests/NexusSnapshotTests.swift +++ b/Tests/FirebladeECSTests/NexusSnapshotTests.swift @@ -36,8 +36,8 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { return encoder } - @Test func encodedFormat() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func encodedFormat(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let root = nexus.createEntity(with: SerialPosition(x: 1, y: 2)) nexus.createEntity(with: SerialParent(parent: root)) @@ -50,8 +50,8 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(json == expected) } - @Test func roundTripRemapsEntityReferences() throws { - let source = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func roundTripRemapsEntityReferences(_ storage: Nexus.StorageKind) throws { + let source = Nexus(storage: storage) let child = source.createEntity() let root = source.createEntity(with: SerialPosition(x: 3, y: 4)) child.set(SerialParent(parent: root)) @@ -59,7 +59,7 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { var encoder = makeEncoder() let data = try source.encodeSnapshot(using: &encoder, handling: .throwError) - let target = Nexus() + let target = Nexus(storage: storage) target.createEntity() try target.register([SerialPosition.self, SerialParent.self]) var decoder = JSONDecoder() @@ -77,13 +77,13 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(decoder.userInfo[.nexusEntityResolver] == nil) } - @Test func unregisteredTypeThrowsAndRollsBack() throws { - let source = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func unregisteredTypeThrowsAndRollsBack(_ storage: Nexus.StorageKind) throws { + let source = Nexus(storage: storage) source.createEntity(with: SerialPosition(x: 1, y: 1)) var encoder = makeEncoder() let data = try source.encodeSnapshot(using: &encoder, handling: .throwError) - let target = Nexus() + let target = Nexus(storage: storage) var decoder = JSONDecoder() #expect(throws: ComponentRegistryError.unknownTypeName("Position")) { try target.decodeSnapshot(from: data, using: &decoder) @@ -91,8 +91,8 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(target.numEntities == 0) } - @Test func nonSerializableComponents() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func nonSerializableComponents(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) nexus.createEntity(with: SerialPosition(x: 1, y: 1), Position(x: 2, y: 2)) #expect(throws: ComponentSerializationError.notSerializable(typeName: "FirebladeECSTests.Position")) { @@ -104,8 +104,8 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(snapshot.members.first?.components.count == 1) } - @Test func registeredNonSerializableTypeThrowsOnImport() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func registeredNonSerializableTypeThrowsOnImport(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) try nexus.register(RegisteredPosition.self) let data = Data(#"{"formatVersion":1,"entities":[{"id":0,"components":{"FirebladeECSTests.RegisteredPosition":{}}}]}"#.utf8) var decoder = JSONDecoder() @@ -115,14 +115,14 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(nexus.numEntities == 0) } - @Test func recordReportsSerializability() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func recordReportsSerializability(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) #expect(try nexus.register(SerialPosition.self).isSerializable) #expect(try !nexus.register(RegisteredPosition.self).isSerializable) } - @Test func newerFormatVersionThrowsWithoutSideEffects() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func newerFormatVersionThrowsWithoutSideEffects(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let data = Data(#"{"formatVersion":4,"entities":[{"id":0,"components":{}}]}"#.utf8) var decoder = JSONDecoder() #expect(throws: ComponentSerializationError.unsupportedFormatVersion(4)) { @@ -131,8 +131,8 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(nexus.numEntities == 0) } - @Test func missingFormatVersionThrows() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func missingFormatVersionThrows(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let data = Data(#"{"entities":[{"id":0,"components":{}}]}"#.utf8) var decoder = JSONDecoder() #expect(throws: DecodingError.self) { @@ -141,9 +141,9 @@ struct SerialParent: Equatable, SerializableComponent, Sendable { #expect(nexus.numEntities == 0) } - @Test func snapshotKeepsOriginalEntityIdentifiers() throws { + @Test(arguments: Nexus.StorageKind.allCases) func snapshotKeepsOriginalEntityIdentifiers(_ storage: Nexus.StorageKind) throws { // Identity-preserving restores rely on the original identifiers, including gaps left by destroyed entities. - let nexus = Nexus() + let nexus = Nexus(storage: storage) let first = nexus.createEntity(with: SerialPosition(x: 0, y: 0)) let destroyed = nexus.createEntity() let third = nexus.createEntity() diff --git a/Tests/FirebladeECSTests/NexusStorageTests.swift b/Tests/FirebladeECSTests/NexusStorageTests.swift index dd92785a..17d3d93a 100644 --- a/Tests/FirebladeECSTests/NexusStorageTests.swift +++ b/Tests/FirebladeECSTests/NexusStorageTests.swift @@ -27,8 +27,8 @@ private struct Tag300: Component, Sendable { } @Test(arguments: Nexus.StorageKind.allCases) - func nexusWorksOnEveryStorage(_ kind: Nexus.StorageKind) { - let nexus = Nexus(storage: kind) + func nexusWorksOnEveryStorage(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Position.self, Name.self, excludesAll: Comp1.self) let entity = nexus.createEntity(with: Position(x: 1, y: 2), Name(name: "a")) let other = nexus.createEntity(with: Position(x: 3, y: 4), Name(name: "b"), Comp1(1)) @@ -42,8 +42,8 @@ private struct Tag300: Component, Sendable { #expect(nexus.numComponents == 2) } - @Test func familiesSpanTheInlineBoundaryOfTheComponentMask() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func familiesSpanTheInlineBoundaryOfTheComponentMask(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requiresAll: Tag127.self, Tag300.self, excludesAll: Tag128.self) let match = nexus.createEntity(with: Tag127(), Tag300()) let excluded = nexus.createEntity(with: Tag127(), Tag128(), Tag300()) @@ -61,15 +61,15 @@ private struct Tag300: Component, Sendable { #expect(Set(excluded.makeComponentsIterator().map(\.identifier)) == [Tag127.identifier, Tag300.identifier]) } - @Test func familyCreatedAfterEntitiesFindsComponentsAboveTheInlineBoundary() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func familyCreatedAfterEntitiesFindsComponentsAboveTheInlineBoundary(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity(with: Tag300()) let family = nexus.family(requires: Tag300.self) #expect(family.entities.map(\.identifier) == [entity.identifier]) } - @Test func traitsFromIdentifiersResolveThroughTheStorage() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func traitsFromIdentifiersResolveThroughTheStorage(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity(with: Position(x: 1, y: 1)) let traits = FamilyTraitSet(requiresAll: [Position.identifier], excludesAll: [Name.identifier]) #expect(nexus.canBecomeMember(entity, in: traits)) @@ -83,8 +83,8 @@ private struct Tag300: Component, Sendable { #expect(!nexus.canBecomeMember(entity, in: traits)) } - @Test func clearDropsEntitiesComponentsAndFamilies() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func clearDropsEntitiesComponentsAndFamilies(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let family = nexus.family(requires: Position.self) nexus.createEntity(with: Position(x: 1, y: 1)) nexus.clear() @@ -97,8 +97,8 @@ private struct Tag300: Component, Sendable { #expect(entity.get(Position.self)?.x == 2) } - @Test func componentIdentifiersOfAnEntityComeFromTheStorage() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentIdentifiersOfAnEntityComeFromTheStorage(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let entity = nexus.createEntity(with: Position(x: 1, y: 1), Name(name: "n")) #expect(nexus.get(components: entity.identifier) == [Position.identifier, Name.identifier]) #expect(nexus.has(componentId: Position.identifier, entityId: entity.identifier)) @@ -107,4 +107,189 @@ private struct Tag300: Component, Sendable { nexus.destroy(entity: entity) #expect(nexus.get(components: entity.identifier) == nil) } + + // MARK: structural churn across component sets + + private struct Lcg { + var state: UInt64 + mutating func next(_ bound: Int) -> Int { + state = state &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + return Int((state >> 33) % UInt64(bound)) + } + } + + /// Adds, replaces and removes components of five types on 200 entities in random order and checks every entity against a model. + /// + /// On the archetype storage every step moves rows between tables, which exercises swap-remove and the entity index. + @Test(arguments: Nexus.StorageKind.allCases) + func randomStructuralChurnKeepsEveryEntityIntact(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) + var random = Lcg(state: 42) + var entities: [Entity] = [] + var model: [[Int?]] = [] + for _ in 0 ..< 200 { + entities.append(nexus.createEntity()) + model.append([nil, nil, nil, nil, nil]) + } + for step in 0 ..< 4000 { + let index = random.next(entities.count) + let kind = random.next(5) + let value = step + if random.next(3) == 0 { + switch kind { + case 0: entities[index].remove(Comp1.self) + case 1: entities[index].remove(Comp2.self) + case 2: entities[index].remove(Comp3.self) + case 3: entities[index].remove(Comp4.self) + default: entities[index].remove(Name.self) + } + model[index][kind] = nil + } else { + switch kind { + case 0: entities[index].set(Comp1(value)) + case 1: entities[index].set(Comp2(value)) + case 2: entities[index].set(Comp3(value)) + case 3: entities[index].set(Comp4(value)) + default: entities[index].set(Name(name: "\(value)")) + } + model[index][kind] = value + } + } + var expectedComponents = 0 + for (index, entity) in entities.enumerated() { + let expected = model[index] + expectedComponents += expected.compactMap { $0 }.count + #expect(entity.get(Comp1.self)?.value == expected[0]) + #expect(entity.get(Comp2.self)?.value == expected[1]) + #expect(entity.get(Comp3.self)?.value == expected[2]) + #expect(entity.get(Comp4.self)?.value == expected[3]) + #expect(entity.get(Name.self).map { Int($0.name) } == expected[4].map { .some($0) }) + #expect(entity.numComponents == expected.compactMap { $0 }.count) + } + #expect(nexus.numComponents == expectedComponents) + #expect(nexus.numEntities == 200) + // families agree with the models, whatever the storage + let family = nexus.family(requiresAll: Comp1.self, Comp3.self, excludesAll: Comp4.self) + let expectedMembers = model.indices.filter { model[$0][0] != nil && model[$0][2] != nil && model[$0][3] == nil } + #expect(Set(family.entities.map(\.identifier)) == Set(expectedMembers.map { entities[$0].identifier })) + var visited = 0 + family.forEach(reading: Comp1.self, writing: Comp3.self) { first, third in + #expect(first.value >= 0) + third.value += 1 + visited += 1 + } + #expect(visited == expectedMembers.count) + for index in expectedMembers { + #expect(entities[index].get(Comp3.self)?.value == model[index][2]! + 1) + } + } + + @Test(arguments: Nexus.StorageKind.allCases) + func familyForEachVisitsMembersOfSeveralComponentSetsOnceAndEmitsOneEvent(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) + var ids: [EntityIdentifier] = [] + for number in 0 ..< 5 { + ids.append(nexus.createEntity(with: Comp1(number), Comp2(0)).identifier) + ids.append(nexus.createEntity(with: Comp1(number), Comp2(0), Comp3(0)).identifier) + ids.append(nexus.createEntity(with: Comp1(number), Comp2(0), Comp4(0)).identifier) + nexus.createEntity(with: Comp1(number), Comp2(0), Comp5(0)) + } + let family = nexus.family(requiresAll: Comp1.self, Comp2.self, excludesAll: Comp5.self) + let recorder = ValueChangeRecorder() + nexus.delegate = recorder + var visited = 0 + family.forEach(reading: Comp1.self, writing: Comp2.self) { first, second in + second.value = first.value + 1 + visited += 1 + } + #expect(visited == 15) + #expect(recorder.counts == [15]) + for id in ids { + let entity = Entity(nexus: nexus, id: id) + #expect(entity.get(Comp2.self)?.value == entity.get(Comp1.self).map { $0.value + 1 }) + } + // the entities with the excluded component keep their value + #expect(nexus.family(requires: Comp5.self).allSatisfy { $0.value == 0 }) + } + + @Test(arguments: Nexus.StorageKind.allCases) + func bulkWriteOverSeveralComponentSetsEmitsOneEventWithTheTotal(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) + for number in 0 ..< 4 { + nexus.createEntity(with: Comp1(number)) + nexus.createEntity(with: Comp1(number), Comp2(0)) + nexus.createEntity(with: Comp1(number), Comp3(0)) + } + let recorder = ValueChangeRecorder() + nexus.delegate = recorder + var seen = 0 + nexus.withComponents(of: Comp1.self) { indices, values in + #expect(indices.count == values.count) + seen += values.count + } + #expect(seen == 12) + #expect(recorder.counts == [12]) + } + + @Test(arguments: Nexus.StorageKind.allCases) + func destroyedAndClearedEntitiesReleaseTheirComponents(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) + let entities = (0 ..< 30).map { nexus.createEntity(with: Comp1($0), Name(name: "\($0)")) } + for entity in entities.prefix(10) { + nexus.destroy(entity: entity) + } + #expect(nexus.numEntities == 20) + #expect(nexus.numComponents == 40) + for (offset, entity) in entities.enumerated().dropFirst(10) { + #expect(entity.get(Comp1.self)?.value == offset) + #expect(entity.get(Name.self)?.name == "\(offset)") + } + nexus.clear() + #expect(nexus.numEntities == 0) + #expect(nexus.numComponents == 0) + } + + // MARK: archetype tables + + @Test func archetypeTablesAreSharedByEntitiesWithTheSameComponentSetAndKeptWhenEmpty() throws { + let nexus = Nexus(storage: .archetype) + guard case let .archetype(archetype) = nexus.backend else { + Issue.record("expected the archetype storage") + return + } + // the table of entities without components exists from the start + #expect(archetype.tableCount == 1) + let first = nexus.createEntity(with: Comp1(1), Comp2(2)) + let second = nexus.createEntity(with: Comp2(3), Comp1(4)) + // components are added one by one: {Comp1} and {Comp1, Comp2} for the first, {Comp2} and (again) {Comp1, Comp2} for the second + #expect(archetype.tableCount == 4) + first.remove(Comp2.self) + second.remove(Comp2.self) + // the removal reuses the table {Comp1} through the cached edge + #expect(archetype.tableCount == 4) + second.set(Comp2(5)) + first.set(Comp2(6)) + #expect(archetype.tableCount == 4) + // a table that became empty stays + nexus.destroy(entity: first) + nexus.destroy(entity: second) + #expect(archetype.tableCount == 4) + #expect(nexus.numComponents == 0) + // clear forgets the tables + nexus.clear() + #expect(archetype.tableCount == 1) + } +} + +/// Records the counts of ``ComponentsModified`` events. +private final class ValueChangeRecorder: NexusEventDelegate, @unchecked Sendable { + private(set) var counts: [Int] = [] + var subscribedEvents: NexusEventMask { .valueChanges } + func nexusEvent(_ event: NexusEvent) { + if let modified = event as? ComponentsModified { + counts.append(modified.count) + } + } + + func nexusNonFatalError(_: String) {} } diff --git a/Tests/FirebladeECSTests/NexusTests.swift b/Tests/FirebladeECSTests/NexusTests.swift index 8645c7f3..112714a2 100644 --- a/Tests/FirebladeECSTests/NexusTests.swift +++ b/Tests/FirebladeECSTests/NexusTests.swift @@ -9,8 +9,8 @@ import Testing @Suite struct NexusTests { - @Test func entityCreate() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityCreate(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) #expect(nexus.numEntities == 0) let e0 = nexus.createEntity() @@ -26,8 +26,8 @@ import Testing #expect(!nexus.debugDescription.isEmpty) } - @Test func entityDestroy() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityDestroy(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) _ = nexus.createEntity() let e1 = nexus.createEntity(with: Name(name: "Entity 1")) #expect(nexus.numEntities == 2) @@ -49,8 +49,8 @@ import Testing #expect(nexus.numEntities == 0) } - @Test func componentCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) #expect(nexus.numEntities == 0) let e0: Entity = nexus.createEntity() @@ -68,8 +68,8 @@ import Testing #expect(rP0?.y == 2) } - @Test func componentDeletion() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentDeletion(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let identifier: EntityIdentifier = nexus.createEntity().identifier let e0 = try #require(nexus.entity(identifier)) @@ -115,8 +115,8 @@ import Testing #expect(e0.numComponents == 0) } - @Test func componentRetrieval() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentRetrieval(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let pos = Position(x: 1, y: 2) let name = Name(name: "myName") let vel = Velocity(a: 3) @@ -129,8 +129,8 @@ import Testing #expect(rVel == vel) } - @Test func componentUniqueness() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func componentUniqueness(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let a = nexus.createEntity() let b = nexus.createEntity() let c = nexus.createEntity() @@ -156,8 +156,8 @@ import Testing #expect(c.get(Position.self) == Position(x: 0, y: 0)) } - @Test func entityIteration() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func entityIteration(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) nexus.createEntities(count: 1000) { ctx in Position(x: ctx.index, y: ctx.index) } let entityArray = Array(nexus.makeEntitiesIterator()) diff --git a/Tests/FirebladeECSTests/NexusValueChangeEventTests.swift b/Tests/FirebladeECSTests/NexusValueChangeEventTests.swift index 003ad710..4dd26545 100644 --- a/Tests/FirebladeECSTests/NexusValueChangeEventTests.swift +++ b/Tests/FirebladeECSTests/NexusValueChangeEventTests.swift @@ -51,8 +51,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(NexusEventMask.all == [.structural, .valueChanges]) } - @Test func structuralMaskDeliversStructuralOnly() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func structuralMaskDeliversStructuralOnly(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .structural) nexus.delegate = recorder let entity = nexus.createEntity() @@ -64,8 +64,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(recorder.events[1] is ComponentAdded) } - @Test func valueChangesMaskDeliversModified() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func valueChangesMaskDeliversModified(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .valueChanges) nexus.delegate = recorder let entity = nexus.createEntity() @@ -79,8 +79,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(nexus.backend.get(Health.self, for: entity.identifier)?.points == 1) } - @Test func allMaskDeliversEverything() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func allMaskDeliversEverything(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .all) nexus.delegate = recorder let entity = nexus.createEntity() @@ -89,8 +89,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(recorder.events.map { String(describing: type(of: $0)) } == ["EntityCreated", "ComponentAdded", "ComponentModified"]) } - @Test func modifyDefaultEmitsAddedThenModified() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func modifyDefaultEmitsAddedThenModified(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .all) let entity = nexus.createEntity() nexus.delegate = recorder @@ -108,8 +108,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(recorder.events[2] is ComponentModified) } - @Test func bulkWriteEmitsOneEventPerPass() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func bulkWriteEmitsOneEventPerPass(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .valueChanges) for _ in 0..<5 { nexus.createEntity().set(Health()) @@ -130,8 +130,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(recorder.events.count == 1) } - @Test func eventsCarryGenerationalIdentifiers() throws { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func eventsCarryGenerationalIdentifiers(_ storage: Nexus.StorageKind) throws { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .all) nexus.delegate = recorder let old = nexus.createEntity() @@ -146,8 +146,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(modified.entityId.generation == reused.identifier.generation) } - @Test func defaultPathNeverCallsTheDelegateOnWrites() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func defaultPathNeverCallsTheDelegateOnWrites(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = Recorder() nexus.delegate = recorder #expect(recorder.subscriptionReads == 1) @@ -163,8 +163,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(!nexus.deliversValueChanges) } - @Test func reassigningTheDelegateAppliesANewSubscription() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func reassigningTheDelegateAppliesANewSubscription(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = Recorder() nexus.delegate = recorder #expect(!nexus.deliversValueChanges) @@ -175,8 +175,8 @@ private final class DefaultDelegate: NexusEventDelegate, @unchecked Sendable { #expect(nexus.deliversValueChanges) } - @Test func settingExistingComponentEmitsReplacedNotAdded() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func settingExistingComponentEmitsReplacedNotAdded(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let recorder = Recorder(mask: .all) let entity = nexus.createEntity() entity.set(Health()) diff --git a/Tests/FirebladeECSTests/SingleTests.swift b/Tests/FirebladeECSTests/SingleTests.swift index 944737d5..4b74713b 100644 --- a/Tests/FirebladeECSTests/SingleTests.swift +++ b/Tests/FirebladeECSTests/SingleTests.swift @@ -16,8 +16,8 @@ private final class SingleEventRecorder: NexusEventDelegate, @unchecked Sendable } @Suite struct SingleTests { - @Test func singleCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) #expect(single.nexus === nexus) #expect(single.traits.requiresAll.count == 1) @@ -30,8 +30,8 @@ private final class SingleEventRecorder: NexusEventDelegate, @unchecked Sendable #expect(single.traits == traits) } - @Test func singleReuse() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleReuse(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let singleA = nexus.single(SingleGameState.self) let singleB = nexus.single(SingleGameState.self) @@ -42,16 +42,16 @@ private final class SingleEventRecorder: NexusEventDelegate, @unchecked Sendable #expect(singleA == singleB) } - @Test func singleEntityAndComponentCreation() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleEntityAndComponentCreation(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) let gameState = SingleGameState() #expect(single.component.shouldQuit == gameState.shouldQuit) #expect(single.component.playerHealth == gameState.playerHealth) } - @Test func singleCreationOnExistingFamilyMember() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleCreationOnExistingFamilyMember(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) _ = nexus.createEntity(with: Position(x: 1, y: 2)) let singleGame = SingleGameState() _ = nexus.createEntity(with: singleGame) @@ -59,24 +59,24 @@ private final class SingleEventRecorder: NexusEventDelegate, @unchecked Sendable #expect(single.component == singleGame) } - @Test func singleEntityAccess() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleEntityAccess(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) let entity = single.entity #expect(entity.nexus === nexus) #expect(entity.identifier == single.entityId) } - @Test func singleComponentIsACopy() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleComponentIsACopy(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) var copy = single.component copy.playerHealth = 1 #expect(single.component.playerHealth != 1) } - @Test func singleSetReplacesComponent() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleSetReplacesComponent(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) var state = single.component state.playerHealth = 7 @@ -86,15 +86,15 @@ private final class SingleEventRecorder: NexusEventDelegate, @unchecked Sendable #expect(single.entity[SingleGameState.self] == state) } - @Test func singleModifyWritesInPlace() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleModifyWritesInPlace(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) single.modify { $0.playerHealth = 3 } #expect(single.component.playerHealth == 3) } - @Test func singleSetAndModifyDeliverEvents() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func singleSetAndModifyDeliverEvents(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let single = nexus.single(SingleGameState.self) let recorder = SingleEventRecorder() nexus.delegate = recorder diff --git a/Tests/FirebladeECSTests/SnapshotV1CompatTests.swift b/Tests/FirebladeECSTests/SnapshotV1CompatTests.swift index 96825d6f..d00c79f1 100644 --- a/Tests/FirebladeECSTests/SnapshotV1CompatTests.swift +++ b/Tests/FirebladeECSTests/SnapshotV1CompatTests.swift @@ -29,8 +29,8 @@ import Testing ]} """# - private func makeNexus() throws -> Nexus { - let nexus = Nexus() + private func makeNexus(_ storage: Nexus.StorageKind) throws -> Nexus { + let nexus = Nexus(storage: storage) try nexus.register(Self.componentTypes) return nexus } @@ -40,8 +40,8 @@ import Testing try nexus.restoreSnapshot(from: Data(json.utf8), using: &decoder) } - @Test func version1KeepsIdentifiersExceptReservedIndex() throws { - let nexus = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func version1KeepsIdentifiersExceptReservedIndex(_ storage: Nexus.StorageKind) throws { + let nexus = try makeNexus(storage) try restore(Self.version1, into: nexus) #expect(nexus.numEntities == 2) @@ -56,8 +56,8 @@ import Testing #expect(nexus.createEntity().identifier == EntityIdentifier(index: 3, generation: 0)) } - @Test func version2RestoresGeneratorStateWithGenerationZero() throws { - let nexus = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func version2RestoresGeneratorStateWithGenerationZero(_ storage: Nexus.StorageKind) throws { + let nexus = try makeNexus(storage) try restore(Self.version2, into: nexus) let kept = try #require(nexus.entity(EntityIdentifier(index: 2, generation: 0))) @@ -71,8 +71,8 @@ import Testing #expect(nexus.createEntity().identifier == EntityIdentifier(index: 4, generation: 0)) } - @Test func version2ImportsAsNewEntities() throws { - let nexus = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func version2ImportsAsNewEntities(_ storage: Nexus.StorageKind) throws { + let nexus = try makeNexus(storage) nexus.createEntity() var decoder = JSONDecoder() let mapping = try nexus.decodeSnapshot(from: Data(Self.version2.utf8), using: &decoder) @@ -84,8 +84,8 @@ import Testing #expect(imported.get(SerialTarget.self)?.target == importedZero.identifier) } - @Test func unknownLegacyFieldsAreIgnoredAndOldCurrentVersionsEncodeAsVersion3() throws { - let nexus = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func unknownLegacyFieldsAreIgnoredAndOldCurrentVersionsEncodeAsVersion3(_ storage: Nexus.StorageKind) throws { + let nexus = try makeNexus(storage) try restore(Self.version2, into: nexus) var encoder = JSONEncoder() let data = try nexus.encodeSnapshot(using: &encoder, handling: .throwError) @@ -95,8 +95,8 @@ import Testing #expect(!json.contains(#""recycled":"#)) } - @Test func version3RoundTripKeepsGenerations() throws { - let source = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func version3RoundTripKeepsGenerations(_ storage: Nexus.StorageKind) throws { + let source = try makeNexus(storage) let first = source.createEntity(with: SerialPosition(x: 1, y: 1)) source.destroy(entity: first) let reused = source.createEntity(with: SerialPosition(x: 2, y: 2)) @@ -109,7 +109,7 @@ import Testing let data = try source.encodeSnapshot(using: &encoder, handling: .throwError) #expect(String(decoding: data, as: UTF8.self).contains(#""formatVersion":3"#)) - let restored = try makeNexus() + let restored = try makeNexus(storage) var decoder = JSONDecoder() try restored.restoreSnapshot(from: data, using: &decoder) @@ -123,8 +123,8 @@ import Testing #expect(restored.createEntity().identifier == source.createEntity().identifier) } - @Test func version3RejectsReservedIndex() throws { - let nexus = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func version3RejectsReservedIndex(_ storage: Nexus.StorageKind) throws { + let nexus = try makeNexus(storage) let json = #"{"formatVersion":3,"entities":[{"id":4294967296,"components":{}}]}"# #expect(throws: DecodingError.self) { try restore(json, into: nexus) @@ -132,8 +132,8 @@ import Testing #expect(nexus.numEntities == 0) } - @Test func version3IdentifierIsTheRawValue() throws { - let nexus = try makeNexus() + @Test(arguments: Nexus.StorageKind.allCases) func version3IdentifierIsTheRawValue(_ storage: Nexus.StorageKind) throws { + let nexus = try makeNexus(storage) let json = #"{"formatVersion":3,"entities":[{"id":8589934595,"components":{"Health":{"value":5}}}]}"# try restore(json, into: nexus) let entity = try #require(nexus.entity(EntityIdentifier(index: 3, generation: 2))) diff --git a/Tests/FirebladeECSTests/SystemsTests.swift b/Tests/FirebladeECSTests/SystemsTests.swift index 55947844..d1455445 100644 --- a/Tests/FirebladeECSTests/SystemsTests.swift +++ b/Tests/FirebladeECSTests/SystemsTests.swift @@ -32,8 +32,8 @@ import Testing } } - @Test func systemsUpdate() { - let nexus = Nexus() + @Test(arguments: Nexus.StorageKind.allCases) func systemsUpdate(_ storage: Nexus.StorageKind) { + let nexus = Nexus(storage: storage) let colorSystem = ColorSystem(nexus: nexus) let positionSystem = PositionSystem(nexus: nexus) let num: Int = 10_000 From d307ec30638036e3bc43a97c17475513f49c3da9 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:45:19 +0100 Subject: [PATCH 08/12] Resolve component indices once per call in the archetype loops The index of a component type is a protocol witness. Reading it inside the loop over tables put a witness call into the loop that check-sil.sh inspects, so the archetype paths of forEach(reading:writing:) and forEachComponentSpan read the indices before the loop. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Scripts/generate-read-write-api.sh | 13 +- .../FirebladeECS/Family+ComponentSpan.swift | 126 ++-- .../Family+ReadWrite.generated.swift | 600 ++++++++++-------- 3 files changed, 430 insertions(+), 309 deletions(-) diff --git a/Scripts/generate-read-write-api.sh b/Scripts/generate-read-write-api.sh index 82919a1e..21e683fd 100755 --- a/Scripts/generate-read-write-api.sh +++ b/Scripts/generate-read-write-api.sh @@ -164,12 +164,15 @@ DOC echo " case let .archetype(archetype):" echo " guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return }" echo " let query = archetype.matchingTables(filter)" + # the component indices are resolved once per call, not per table + for ((i = 1; i <= r; i++)); do echo " let rIndex$i = R$i.componentIndex"; done + for ((i = 1; i <= w; i++)); do echo " let wIndex$i = W$i.componentIndex"; done echo " archetype.lockDepth += 1" - for ((i = 1; i <= w; i++)); do echo " archetype.beginWrite(W$i.componentIndex)"; done - for ((i = 1; i <= r; i++)); do echo " archetype.requireReadable(R$i.componentIndex)"; done + for ((i = 1; i <= w; i++)); do echo " archetype.beginWrite(wIndex$i)"; done + for ((i = 1; i <= r; i++)); do echo " archetype.requireReadable(rIndex$i)"; done if ((w > 0)); then echo " var visited = 0"; fi echo " defer {" - for ((i = 1; i <= w; i++)); do echo " archetype.endWrite(W$i.componentIndex)"; done + for ((i = 1; i <= w; i++)); do echo " archetype.endWrite(wIndex$i)"; done echo " archetype.lockDepth -= 1" if ((w > 0)); then echo " if visited > 0 {" @@ -181,8 +184,8 @@ DOC echo " try entry._withUnsafeGuaranteedRef { table in" echo " let count = table.entities.count" echo " guard count > 0 else { return }" - for ((i = 1; i <= r; i++)); do echo " let rBase$i = table.base(R$i.self, at: R$i.componentIndex)"; done - for ((i = 1; i <= w; i++)); do echo " let wBase$i = table.base(W$i.self, at: W$i.componentIndex)"; done + for ((i = 1; i <= r; i++)); do echo " let rBase$i = table.base(R$i.self, at: rIndex$i)"; done + for ((i = 1; i <= w; i++)); do echo " let wBase$i = table.base(W$i.self, at: wIndex$i)"; done echo " for row in 0 ..< count {" if ((w > 0)); then echo " visited += 1"; fi echo " try body($(join ', ' "${rowArgs[@]}"))" diff --git a/Sources/FirebladeECS/Family+ComponentSpan.swift b/Sources/FirebladeECS/Family+ComponentSpan.swift index 0f76713b..8a8ab7e7 100644 --- a/Sources/FirebladeECS/Family+ComponentSpan.swift +++ b/Sources/FirebladeECS/Family+ComponentSpan.swift @@ -55,11 +55,12 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(wIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) + archetype.endWrite(wIndex1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -69,7 +70,7 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: wIndex1), count: count)) visited += count try body(&wSpan1) } @@ -131,13 +132,15 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -148,8 +151,8 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) - var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: W2.componentIndex), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: wIndex1), count: count)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: wIndex2), count: count)) visited += count try body(&wSpan1, &wSpan2) } @@ -200,17 +203,18 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex archetype.lockDepth += 1 - let wasReading1 = archetype.beginRead(R1.componentIndex) + let wasReading1 = archetype.beginRead(rIndex1) defer { - archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.endRead(rIndex1, wasReading: wasReading1) archetype.lockDepth -= 1 } for entry in query.tables { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: rIndex1), count: count)) try body(rSpan1) } } @@ -304,13 +308,15 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - let wasReading1 = archetype.beginRead(R1.componentIndex) + archetype.beginWrite(wIndex1) + let wasReading1 = archetype.beginRead(rIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.endWrite(wIndex1) + archetype.endRead(rIndex1, wasReading: wasReading1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -320,8 +326,8 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: rIndex1), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: wIndex1), count: count)) visited += count try body(rSpan1, &wSpan1) } @@ -390,15 +396,18 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - let wasReading1 = archetype.beginRead(R1.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + let wasReading1 = archetype.beginRead(rIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endRead(R1.componentIndex, wasReading: wasReading1) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endRead(rIndex1, wasReading: wasReading1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -409,9 +418,9 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) - var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: W2.componentIndex), count: count)) + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: rIndex1), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: wIndex1), count: count)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: wIndex2), count: count)) visited += count try body(rSpan1, &wSpan1, &wSpan2) } @@ -469,20 +478,22 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex archetype.lockDepth += 1 - let wasReading1 = archetype.beginRead(R1.componentIndex) - let wasReading2 = archetype.beginRead(R2.componentIndex) + let wasReading1 = archetype.beginRead(rIndex1) + let wasReading2 = archetype.beginRead(rIndex2) defer { - archetype.endRead(R1.componentIndex, wasReading: wasReading1) - archetype.endRead(R2.componentIndex, wasReading: wasReading2) + archetype.endRead(rIndex1, wasReading: wasReading1) + archetype.endRead(rIndex2, wasReading: wasReading2) archetype.lockDepth -= 1 } for entry in query.tables { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) - let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: R2.componentIndex), count: count)) + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: rIndex1), count: count)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: rIndex2), count: count)) try body(rSpan1, rSpan2) } } @@ -549,15 +560,18 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - let wasReading1 = archetype.beginRead(R1.componentIndex) - let wasReading2 = archetype.beginRead(R2.componentIndex) + archetype.beginWrite(wIndex1) + let wasReading1 = archetype.beginRead(rIndex1) + let wasReading2 = archetype.beginRead(rIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endRead(R1.componentIndex, wasReading: wasReading1) - archetype.endRead(R2.componentIndex, wasReading: wasReading2) + archetype.endWrite(wIndex1) + archetype.endRead(rIndex1, wasReading: wasReading1) + archetype.endRead(rIndex2, wasReading: wasReading2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -567,9 +581,9 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) - let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: R2.componentIndex), count: count)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: rIndex1), count: count)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: rIndex2), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: wIndex1), count: count)) visited += count try body(rSpan1, rSpan2, &wSpan1) } @@ -645,17 +659,21 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - let wasReading1 = archetype.beginRead(R1.componentIndex) - let wasReading2 = archetype.beginRead(R2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + let wasReading1 = archetype.beginRead(rIndex1) + let wasReading2 = archetype.beginRead(rIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endRead(R1.componentIndex, wasReading: wasReading1) - archetype.endRead(R2.componentIndex, wasReading: wasReading2) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endRead(rIndex1, wasReading: wasReading1) + archetype.endRead(rIndex2, wasReading: wasReading2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -666,10 +684,10 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: R1.componentIndex), count: count)) - let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: R2.componentIndex), count: count)) - var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: W1.componentIndex), count: count)) - var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: W2.componentIndex), count: count)) + let rSpan1 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R1.self, at: rIndex1), count: count)) + let rSpan2 = Span(_unsafeElements: UnsafeBufferPointer(start: table.base(R2.self, at: rIndex2), count: count)) + var wSpan1 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W1.self, at: wIndex1), count: count)) + var wSpan2 = MutableSpan(_unsafeElements: UnsafeMutableBufferPointer(start: table.base(W2.self, at: wIndex2), count: count)) visited += count try body(rSpan1, rSpan2, &wSpan1, &wSpan2) } diff --git a/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift b/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift index 16c04116..2d69532f 100644 --- a/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift +++ b/Sources/FirebladeECS/Generated/Family+ReadWrite.generated.swift @@ -43,11 +43,12 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) + archetype.beginWrite(wIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) + archetype.endWrite(wIndex1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -57,7 +58,7 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let wBase1 = table.base(W1.self, at: W1.componentIndex) + let wBase1 = table.base(W1.self, at: wIndex1) for row in 0 ..< count { visited += 1 try body(&wBase1[row]) @@ -112,13 +113,15 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -129,8 +132,8 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) for row in 0 ..< count { visited += 1 try body(&wBase1[row], &wBase2[row]) @@ -193,15 +196,18 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -213,9 +219,9 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) for row in 0 ..< count { visited += 1 try body(&wBase1[row], &wBase2[row], &wBase3[row]) @@ -286,17 +292,21 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex + let wIndex4 = W4.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.beginWrite(W4.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.beginWrite(wIndex4) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) - archetype.endWrite(W4.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) + archetype.endWrite(wIndex4) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -309,10 +319,10 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) - let wBase4 = table.base(W4.self, at: W4.componentIndex) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) + let wBase4 = table.base(W4.self, at: wIndex4) for row in 0 ..< count { visited += 1 try body(&wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) @@ -355,8 +365,9 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex archetype.lockDepth += 1 - archetype.requireReadable(R1.componentIndex) + archetype.requireReadable(rIndex1) defer { archetype.lockDepth -= 1 } @@ -364,7 +375,7 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) for row in 0 ..< count { try body(rBase1[row]) } @@ -449,12 +460,14 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.requireReadable(R1.componentIndex) + archetype.beginWrite(wIndex1) + archetype.requireReadable(rIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) + archetype.endWrite(wIndex1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -464,8 +477,8 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let wBase1 = table.base(W1.self, at: wIndex1) for row in 0 ..< count { visited += 1 try body(rBase1[row], &wBase1[row]) @@ -526,14 +539,17 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.requireReadable(R1.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.requireReadable(rIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -544,9 +560,9 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) for row in 0 ..< count { visited += 1 try body(rBase1[row], &wBase1[row], &wBase2[row]) @@ -615,16 +631,20 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.requireReadable(R1.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.requireReadable(rIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -636,10 +656,10 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) for row in 0 ..< count { visited += 1 try body(rBase1[row], &wBase1[row], &wBase2[row], &wBase3[row]) @@ -716,18 +736,23 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex + let wIndex4 = W4.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.beginWrite(W4.componentIndex) - archetype.requireReadable(R1.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.beginWrite(wIndex4) + archetype.requireReadable(rIndex1) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) - archetype.endWrite(W4.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) + archetype.endWrite(wIndex4) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -740,11 +765,11 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) - let wBase4 = table.base(W4.self, at: W4.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) + let wBase4 = table.base(W4.self, at: wIndex4) for row in 0 ..< count { visited += 1 try body(rBase1[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) @@ -793,9 +818,11 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex archetype.lockDepth += 1 - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) defer { archetype.lockDepth -= 1 } @@ -803,8 +830,8 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) for row in 0 ..< count { try body(rBase1[row], rBase2[row]) } @@ -862,13 +889,16 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) + archetype.endWrite(wIndex1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -878,9 +908,9 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let wBase1 = table.base(W1.self, at: wIndex1) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], &wBase1[row]) @@ -947,15 +977,19 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -966,10 +1000,10 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], &wBase1[row], &wBase2[row]) @@ -1044,17 +1078,22 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1066,11 +1105,11 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], &wBase1[row], &wBase2[row], &wBase3[row]) @@ -1153,19 +1192,25 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex + let wIndex4 = W4.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.beginWrite(W4.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.beginWrite(wIndex4) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) - archetype.endWrite(W4.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) + archetype.endWrite(wIndex4) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1178,12 +1223,12 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) - let wBase4 = table.base(W4.self, at: W4.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) + let wBase4 = table.base(W4.self, at: wIndex4) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) @@ -1238,10 +1283,13 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex archetype.lockDepth += 1 - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) defer { archetype.lockDepth -= 1 } @@ -1249,9 +1297,9 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) for row in 0 ..< count { try body(rBase1[row], rBase2[row], rBase3[row]) } @@ -1315,14 +1363,18 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) + archetype.beginWrite(wIndex1) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) + archetype.endWrite(wIndex1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1332,10 +1384,10 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let wBase1 = table.base(W1.self, at: wIndex1) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row]) @@ -1408,16 +1460,21 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1428,11 +1485,11 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row], &wBase2[row]) @@ -1513,18 +1570,24 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1536,12 +1599,12 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row], &wBase2[row], &wBase3[row]) @@ -1630,20 +1693,27 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex + let wIndex4 = W4.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.beginWrite(W4.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.beginWrite(wIndex4) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) - archetype.endWrite(W4.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) + archetype.endWrite(wIndex4) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1656,13 +1726,13 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) - let wBase4 = table.base(W4.self, at: W4.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) + let wBase4 = table.base(W4.self, at: wIndex4) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) @@ -1723,11 +1793,15 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let rIndex4 = R4.componentIndex archetype.lockDepth += 1 - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) - archetype.requireReadable(R4.componentIndex) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) + archetype.requireReadable(rIndex4) defer { archetype.lockDepth -= 1 } @@ -1735,10 +1809,10 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let rBase4 = table.base(R4.self, at: R4.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let rBase4 = table.base(R4.self, at: rIndex4) for row in 0 ..< count { try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row]) } @@ -1808,15 +1882,20 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let rIndex4 = R4.componentIndex + let wIndex1 = W1.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) - archetype.requireReadable(R4.componentIndex) + archetype.beginWrite(wIndex1) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) + archetype.requireReadable(rIndex4) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) + archetype.endWrite(wIndex1) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1826,11 +1905,11 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let rBase4 = table.base(R4.self, at: R4.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let rBase4 = table.base(R4.self, at: rIndex4) + let wBase1 = table.base(W1.self, at: wIndex1) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row]) @@ -1909,17 +1988,23 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let rIndex4 = R4.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) - archetype.requireReadable(R4.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) + archetype.requireReadable(rIndex4) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -1930,12 +2015,12 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let rBase4 = table.base(R4.self, at: R4.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let rBase4 = table.base(R4.self, at: rIndex4) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row], &wBase2[row]) @@ -2022,19 +2107,26 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let rIndex4 = R4.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) - archetype.requireReadable(R4.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) + archetype.requireReadable(rIndex4) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -2046,13 +2138,13 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let rBase4 = table.base(R4.self, at: R4.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let rBase4 = table.base(R4.self, at: rIndex4) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row], &wBase2[row], &wBase3[row]) @@ -2147,21 +2239,29 @@ extension Family { case let .archetype(archetype): guard !memberIds.isEmpty, let filter = nexus.familyFiltersByTraits[traits] else { return } let query = archetype.matchingTables(filter) + let rIndex1 = R1.componentIndex + let rIndex2 = R2.componentIndex + let rIndex3 = R3.componentIndex + let rIndex4 = R4.componentIndex + let wIndex1 = W1.componentIndex + let wIndex2 = W2.componentIndex + let wIndex3 = W3.componentIndex + let wIndex4 = W4.componentIndex archetype.lockDepth += 1 - archetype.beginWrite(W1.componentIndex) - archetype.beginWrite(W2.componentIndex) - archetype.beginWrite(W3.componentIndex) - archetype.beginWrite(W4.componentIndex) - archetype.requireReadable(R1.componentIndex) - archetype.requireReadable(R2.componentIndex) - archetype.requireReadable(R3.componentIndex) - archetype.requireReadable(R4.componentIndex) + archetype.beginWrite(wIndex1) + archetype.beginWrite(wIndex2) + archetype.beginWrite(wIndex3) + archetype.beginWrite(wIndex4) + archetype.requireReadable(rIndex1) + archetype.requireReadable(rIndex2) + archetype.requireReadable(rIndex3) + archetype.requireReadable(rIndex4) var visited = 0 defer { - archetype.endWrite(W1.componentIndex) - archetype.endWrite(W2.componentIndex) - archetype.endWrite(W3.componentIndex) - archetype.endWrite(W4.componentIndex) + archetype.endWrite(wIndex1) + archetype.endWrite(wIndex2) + archetype.endWrite(wIndex3) + archetype.endWrite(wIndex4) archetype.lockDepth -= 1 if visited > 0 { nexus.emitModified(W1.self, count: visited) @@ -2174,14 +2274,14 @@ extension Family { try entry._withUnsafeGuaranteedRef { table in let count = table.entities.count guard count > 0 else { return } - let rBase1 = table.base(R1.self, at: R1.componentIndex) - let rBase2 = table.base(R2.self, at: R2.componentIndex) - let rBase3 = table.base(R3.self, at: R3.componentIndex) - let rBase4 = table.base(R4.self, at: R4.componentIndex) - let wBase1 = table.base(W1.self, at: W1.componentIndex) - let wBase2 = table.base(W2.self, at: W2.componentIndex) - let wBase3 = table.base(W3.self, at: W3.componentIndex) - let wBase4 = table.base(W4.self, at: W4.componentIndex) + let rBase1 = table.base(R1.self, at: rIndex1) + let rBase2 = table.base(R2.self, at: rIndex2) + let rBase3 = table.base(R3.self, at: rIndex3) + let rBase4 = table.base(R4.self, at: rIndex4) + let wBase1 = table.base(W1.self, at: wIndex1) + let wBase2 = table.base(W2.self, at: wIndex2) + let wBase3 = table.base(W3.self, at: wIndex3) + let wBase4 = table.base(W4.self, at: wIndex4) for row in 0 ..< count { visited += 1 try body(rBase1[row], rBase2[row], rBase3[row], rBase4[row], &wBase1[row], &wBase2[row], &wBase3[row], &wBase4[row]) From 89383d13a77d51947281f9d695ee663615ea03d9 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:45:19 +0100 Subject: [PATCH 09/12] List the tables of a family when the family is created The archetype storage caches the tables a family matches. Building the list on the first iteration allocated a query and grew its array inside that iteration; creating the family does it instead, so every iteration starts with a cached list. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Sources/FirebladeECS/Nexus+Internal.swift | 4 +++ .../FirebladeECSTests/NexusStorageTests.swift | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Sources/FirebladeECS/Nexus+Internal.swift b/Sources/FirebladeECS/Nexus+Internal.swift index 3329b309..c1229a7f 100644 --- a/Sources/FirebladeECS/Nexus+Internal.swift +++ b/Sources/FirebladeECS/Nexus+Internal.swift @@ -138,6 +138,10 @@ extension Nexus { familyMembersByTraits[traits] = UnorderedSparseSet() familyFiltersByTraits[traits] = filter familyList.append((traits, filter)) + // the archetype storage lists the tables a family matches once, so that the first iteration does not + if case let .archetype(archetype) = backend { + _ = archetype.matchingTables(filter) + } // FIXME: iterating all entities is costly for many entities // The identifiers ascend by slot index, which makes the member order deterministic instead of following a hash seed. for entityId in backend.livingEntityIds { diff --git a/Tests/FirebladeECSTests/NexusStorageTests.swift b/Tests/FirebladeECSTests/NexusStorageTests.swift index 17d3d93a..33960f89 100644 --- a/Tests/FirebladeECSTests/NexusStorageTests.swift +++ b/Tests/FirebladeECSTests/NexusStorageTests.swift @@ -279,6 +279,38 @@ private struct Tag300: Component, Sendable { nexus.clear() #expect(archetype.tableCount == 1) } + + @Test func archetypeFamilyQueriesAreCachedAndExtendedByNewTables() throws { + let nexus = Nexus(storage: .archetype) + guard case let .archetype(archetype) = nexus.backend else { + Issue.record("expected the archetype storage") + return + } + nexus.createEntity(with: Comp1(1)) + let family = nexus.family(requires: Comp1.self) + func visit() -> Int { + var count = 0 + family.forEach(reading: Comp1.self) { _ in count += 1 } + return count + } + // creating the family lists its tables, so the first pass has nothing left to build + #expect(archetype.queryCache.count == 1) + let query = try #require(archetype.queryCache.values.first) + let matching = query.tables.count + #expect(matching == 1) + #expect(visit() == 1) + #expect(archetype.queryCache.values.first === query) + #expect(visit() == 1) + // the second pass reuses the query instead of scanning the tables again + #expect(archetype.queryCache.count == 1) + #expect(archetype.queryCache.values.first === query) + #expect(query.tables.count == matching) + // a new component set creates a table, which the next pass adds to the query + nexus.createEntity(with: Comp1(2), Comp2(2)) + #expect(visit() == 2) + #expect(archetype.queryCache.values.first === query) + #expect(query.tables.count > matching) + } } /// Records the counts of ``ComponentsModified`` events. From c1806ddcc3c9019c6974da41acedd43738a58aa4 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:49:27 +0100 Subject: [PATCH 10/12] Check the archetype loops and the span loops in check-sil.sh A call site switches on the storage once, so its SIL holds the loops of both storages. The script now also fails when a site's loops do not reach the archetype code, and covers forEachComponentSpan with three more sites. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Scripts/check-sil.sh | 16 ++++++++++++---- Scripts/sil/CallSites.swift | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Scripts/check-sil.sh b/Scripts/check-sil.sh index 1ae026f0..352b6f94 100755 --- a/Scripts/check-sil.sh +++ b/Scripts/check-sil.sh @@ -4,6 +4,10 @@ # the loops of the specialized code: no heap allocation, no existential, no witness_method, no key path, and no call # to a function that was not inlined or specialized into the module of the call site. # +# A call site switches on the storage once, so its SIL holds the loops of both storages: the sparse-set loop over the +# members and the archetype loops over tables and rows. Both are checked together. The "arch" column counts the loop +# instructions that touch an archetype table or column; a site whose loops do not reach the archetype code fails. +# # Usage: Scripts/check-sil.sh (builds FirebladeECS in release first; exits 1 if a loop violates a rule) # # SIL_OUT= output directory (default: $TMPDIR/ecs-sil) @@ -41,6 +45,9 @@ COUNTERS = [ ("keypath", r"\bkeypath\b|swift_getKeyPath|getKeyPath"), ("allocObject", r"swift_allocObject|allocObject"), ] +# not a violation: proves that the archetype loops are part of the analysis +ARCHETYPE = r"ArchetypeTable|ArchetypeStorage" + TERMINATOR = re.compile(r"^\s+(br|cond_br|switch_enum|switch_enum_addr|switch_value|try_apply|checked_cast_br|dynamic_method_br|yield)\b") @@ -86,7 +93,7 @@ def callees(lines): def count(lines): body = "\n".join(lines) - return [len(re.findall(pattern, body)) for _, pattern in COUNTERS] + return [len(re.findall(pattern, body)) for _, pattern in COUNTERS] + [len(re.findall(ARCHETYPE, body))] def analyze(root): @@ -120,17 +127,18 @@ def analyze(root): return count(loop_lines), count(all_lines), sorted(set(external)), loop_blocks -print("%-34s" % "site" + "".join("%12s" % c[0] for c in COUNTERS) + "%7s%9s%7s" % ("calls", "all:wit", "loop")) +print("%-34s" % "site" + "".join("%12s" % c[0] for c in COUNTERS) + "%7s%9s%7s%7s" % ("calls", "all:wit", "loop", "arch")) failed = False for name in sorted(funcs): found = re.search(r"(site_[A-Za-z0-9_]+?)yy\d", name) if not found or "Tf" in name or "Tg" in name or "TA" in name: continue loop_counts, all_counts, external, loop_size = analyze(name) - print("%-34s" % found.group(1) + "".join("%12d" % c for c in loop_counts) + "%7d%9d%7d" % (len(external), all_counts[3], loop_size)) + violations, archetype_lines = loop_counts[:-1], loop_counts[-1] + print("%-34s" % found.group(1) + "".join("%12d" % c for c in violations) + "%7d%9d%7d%7d" % (len(external), all_counts[3], loop_size, archetype_lines)) for callee in external: print(" loop calls @" + callee) - if loop_size == 0 or any(loop_counts) or external: + if loop_size == 0 or any(violations) or archetype_lines == 0 or external: failed = True print("SIL: " + sys.argv[1]) sys.exit(1 if failed else 0) diff --git a/Scripts/sil/CallSites.swift b/Scripts/sil/CallSites.swift index 572fce1f..b1edc141 100644 --- a/Scripts/sil/CallSites.swift +++ b/Scripts/sil/CallSites.swift @@ -95,3 +95,34 @@ public struct Tag: Component { } sum = total } + +// MARK: forEachComponentSpan (macOS 26 and later) + +@available(macOS 26, *) +@inline(never) public func site_ForEachSpan_1R(_ family: Family, _ sum: inout Int) { + var total = 0 + family.forEachComponentSpan(reading: Position.self) { positions in + for index in positions.indices { + total &+= positions[index].x + } + } + sum = total +} + +@available(macOS 26, *) +@inline(never) public func site_ForEachSpan_1R1W(_ family: Family, _ sum: inout Int) { + family.forEachComponentSpan(reading: Velocity.self, writing: Position.self) { velocities, positions in + for index in positions.indices { + positions[index].x &+= velocities[index].dx + } + } +} + +@available(macOS 26, *) +@inline(never) public func site_ForEachSpan_2R1W(_ family: Family, _ sum: inout Int) { + family.forEachComponentSpan(reading: Velocity.self, Health.self, writing: Position.self) { velocities, healths, positions in + for index in positions.indices { + positions[index].x &+= velocities[index].dx &+ healths[index].points + } + } +} From f8b14626f7664cab2fa229407c32b86ce678dbfb Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:49:27 +0100 Subject: [PATCH 11/12] Benchmark both storages: iteration-only variants and the storage spike cases The IterOnly benchmarks run on the sparse-set and the archetype storage, named with the storage in brackets. StorageBenchmarks.swift ports the S2 spike cases: iteration at several arities, filtered and fragmented families, 100k entities in permuted order, string components, random access, add/remove churn and entity creation. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- Benchmarks/Benchmarks/ECSBenchmark/Base.swift | 4 +- .../ECSBenchmark/OneDimensionBenchmarks.swift | 19 +- .../ECSBenchmark/StorageBenchmarks.swift | 303 ++++++++++++++++++ Benchmarks/README.md | 2 + 4 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 Benchmarks/Benchmarks/ECSBenchmark/StorageBenchmarks.swift diff --git a/Benchmarks/Benchmarks/ECSBenchmark/Base.swift b/Benchmarks/Benchmarks/ECSBenchmark/Base.swift index bcce8a31..e4dfd3c7 100644 --- a/Benchmarks/Benchmarks/ECSBenchmark/Base.swift +++ b/Benchmarks/Benchmarks/ECSBenchmark/Base.swift @@ -66,9 +66,9 @@ struct SingleGameState: SingleComponent, Sendable { var playerHealth: Int = 67 } -func setUpNexus() -> Nexus { +func setUpNexus(storage: Nexus.StorageKind = .default) -> Nexus { let numEntities = 10000 - let nexus = Nexus() + let nexus = Nexus(storage: storage) for i in 0 ..< numEntities { nexus.createEntity().set(Position(x: 1 + i, y: 2 + i), diff --git a/Benchmarks/Benchmarks/ECSBenchmark/OneDimensionBenchmarks.swift b/Benchmarks/Benchmarks/ECSBenchmark/OneDimensionBenchmarks.swift index e2d1ce9a..aa35473a 100644 --- a/Benchmarks/Benchmarks/ECSBenchmark/OneDimensionBenchmarks.swift +++ b/Benchmarks/Benchmarks/ECSBenchmark/OneDimensionBenchmarks.swift @@ -254,15 +254,18 @@ let benchmarks: @Sendable () -> Void = { // The benchmarks above build their nexus inside the measured closure, so setup dominates their time and malloc counts. // These variants build the nexus first and measure the iteration alone: the malloc count is the allocations of the // iteration itself, which is 0 when iterating allocates nothing per entity. + // Every variant runs on both storages: the name carries the storage in brackets. func iterationOnly(_ name: String, prepare: @escaping @Sendable (Nexus) -> () -> Void) { - Benchmark(name) { benchmark in - let nexus = setUpNexus() - let iterate = prepare(nexus) - benchmark.startMeasurement() - for _ in benchmark.scaledIterations { - iterate() + for storage in Nexus.StorageKind.allCases { + Benchmark("\(name) [\(storage)]") { benchmark in + let nexus = setUpNexus(storage: storage) + let iterate = prepare(nexus) + benchmark.startMeasurement() + for _ in benchmark.scaledIterations { + iterate() + } + benchmark.stopMeasurement() } - benchmark.stopMeasurement() } } @@ -413,4 +416,6 @@ let benchmarks: @Sendable () -> Void = { } } } } + + storageBenchmarks() } diff --git a/Benchmarks/Benchmarks/ECSBenchmark/StorageBenchmarks.swift b/Benchmarks/Benchmarks/ECSBenchmark/StorageBenchmarks.swift new file mode 100644 index 00000000..69567b2c --- /dev/null +++ b/Benchmarks/Benchmarks/ECSBenchmark/StorageBenchmarks.swift @@ -0,0 +1,303 @@ +// swiftformat:disable preferForLoop +import Benchmark +import FirebladeECS + +// Benchmarks of the storage itself, ported from the S2 spike (Spikes/S2/REPORT.md on branch spike/s2-storage) and +// run on every storage: the name carries the storage in brackets. The nexus is built before the measurement, so the +// numbers are the operation alone, and the malloc count is the allocations of the operation. +// +// Reductions xor the bit patterns: a floating-point accumulator in the closure would chain on the latency of the +// addition and hide the storage. + +struct B0: Component, Sendable { var v: UInt64 = 0 } +struct B1: Component, Sendable { var v: UInt64 = 0 } +struct B2: Component, Sendable { var v: UInt64 = 0 } +struct B3: Component, Sendable { var v: UInt64 = 0 } +struct B4: Component, Sendable { var v: UInt64 = 0 } +struct B5: Component, Sendable { var v: UInt64 = 0 } +struct B6: Component, Sendable { var v: UInt64 = 0 } +struct B7: Component, Sendable { var v: UInt64 = 0 } +struct B8: Component, Sendable { var v: UInt64 = 0 } +struct B9: Component, Sendable { var v: UInt64 = 0 } +struct Toggle: Component, Sendable {} +struct Named: Component, Sendable { + var name: String + var count = 0 +} + +/// Families and entities refer to their nexus without owning it, and the measured closures capture no nexus: the +/// worlds live as long as the process. +private nonisolated(unsafe) var worlds: [Nexus] = [] + +private let entityCount = 10000 +private let largeCount = 100_000 +private let sharedName = String(repeating: "named component payload ", count: 4) + +/// `count` entities with `B0 ... B6`, the shared base of most cases. +private func makeUniform(_ storage: Nexus.StorageKind, count: Int = entityCount) -> (Nexus, [Entity]) { + let nexus = Nexus(storage: storage) + worlds.append(nexus) + var entities: [Entity] = [] + entities.reserveCapacity(count) + for _ in 0 ..< count { + entities.append(nexus.createEntity(with: B0(), B1(), B2(), B3(), B4(), B5(), B6())) + } + return (nexus, entities) +} + +/// All entities have B0; 10% have B1 as well, the rest are spread over {B0, B2} and {B0, B3}. +private func makeTenPercent(_ storage: Nexus.StorageKind) -> Nexus { + let nexus = Nexus(storage: storage) + worlds.append(nexus) + for index in 0 ..< entityCount { + let entity = nexus.createEntity(with: B0()) + if index % 10 == 0 { + entity.set(B1()) + } else if index % 2 == 0 { + entity.set(B2()) + } else { + entity.set(B3()) + } + } + return nexus +} + +/// 1024 distinct component sets: B0, B1 and a different subset of ten tag types per entity group. +private func makeFragmented(_ storage: Nexus.StorageKind) -> (Nexus, [Entity]) { + let nexus = Nexus(storage: storage) + worlds.append(nexus) + var entities: [Entity] = [] + for index in 0 ..< entityCount { + let entity = nexus.createEntity(with: B0(), B1()) + let bits = index % 1024 + if bits & 1 != 0 { + entity.set(Tag0()) + } + if bits & 2 != 0 { + entity.set(Tag1()) + } + if bits & 4 != 0 { + entity.set(Tag2()) + } + if bits & 8 != 0 { + entity.set(Tag3()) + } + if bits & 16 != 0 { + entity.set(Tag4()) + } + if bits & 32 != 0 { + entity.set(Tag5()) + } + if bits & 64 != 0 { + entity.set(Tag6()) + } + if bits & 128 != 0 { + entity.set(Tag7()) + } + if bits & 256 != 0 { + entity.set(Tag8()) + } + if bits & 512 != 0 { + entity.set(Tag9()) + } + entities.append(entity) + } + return (nexus, entities) +} + +struct Tag0: Component, Sendable {} +struct Tag1: Component, Sendable {} +struct Tag2: Component, Sendable {} +struct Tag3: Component, Sendable {} +struct Tag4: Component, Sendable {} +struct Tag5: Component, Sendable {} +struct Tag6: Component, Sendable {} +struct Tag7: Component, Sendable {} +struct Tag8: Component, Sendable {} +struct Tag9: Component, Sendable {} + +/// 100k entities with B0, B1, B2 whose B1 and B2 storage order is permuted against B0. +private func makeShuffled(_ storage: Nexus.StorageKind) -> Nexus { + let nexus = Nexus(storage: storage) + worlds.append(nexus) + let entities = (0 ..< largeCount).map { _ in nexus.createEntity(with: B0(), B1(), B2()) } + var generator = SystemRandomNumberGenerator() + for entity in entities.shuffled(using: &generator) { + entity.remove(B1.self) + entity.remove(B2.self) + } + for entity in entities.shuffled(using: &generator) { + entity.set(B1()) + entity.set(B2()) + } + return nexus +} + +func storageBenchmarks() { + func register(_ name: String, prepare: @escaping @Sendable (Nexus.StorageKind) -> () -> Void) { + for storage in Nexus.StorageKind.allCases { + Benchmark("\(name) [\(storage)]") { benchmark in + let run = prepare(storage) + benchmark.startMeasurement() + for _ in benchmark.scaledIterations { + run() + } + benchmark.stopMeasurement() + } + } + } + + // MARK: iteration + + register("S2Iterate1Read") { storage in + let (nexus, _) = makeUniform(storage) + let family = nexus.family(requires: B0.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self) { s ^= $0.v }; blackHole(s) } + } + register("S2Iterate2Reads") { storage in + let (nexus, _) = makeUniform(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self, B1.self) { s ^= $0.v ^ $1.v }; blackHole(s) } + } + register("S2Iterate4Reads") { storage in + let (nexus, _) = makeUniform(storage) + let family = nexus.family(requiresAll: B0.self, B1.self, B2.self, B3.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self, B1.self, B2.self, B3.self) { s ^= $0.v ^ $1.v ^ $2.v ^ $3.v }; blackHole(s) } + } + register("S2Iterate1R1W") { storage in + let (nexus, _) = makeUniform(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { family.forEach(reading: B0.self, writing: B1.self) { read, write in write.v &+= read.v } } + } + register("S2Iterate2R1W") { storage in + let (nexus, _) = makeUniform(storage) + let family = nexus.family(requiresAll: B0.self, B1.self, B2.self) + return { family.forEach(reading: B0.self, B1.self, writing: B2.self) { first, second, write in write.v &+= first.v &+ second.v } } + } + // above the generated arities: the parameter pack fallback + register("S2IteratePack5R2W") { storage in + let (nexus, _) = makeUniform(storage) + let family = nexus.family(requiresAll: B0.self, B1.self, B2.self, B3.self, B4.self, B5.self, B6.self) + return { family.forEach(reading: B0.self, B1.self, B2.self, B3.self, B4.self, writing: B5.self, B6.self) { (written: inout (B5, B6), a, b, c, d, e) in + written.0.v &+= a.v &+ b.v + written.1.v &+= c.v &+ d.v &+ e.v + } } + } + register("S2Iterate2ReadsTenPercent") { storage in + let nexus = makeTenPercent(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self, B1.self) { s ^= $0.v ^ $1.v }; blackHole(s) } + } + register("S2Iterate1R1WTenPercent") { storage in + let nexus = makeTenPercent(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { family.forEach(reading: B0.self, writing: B1.self) { read, write in write.v &+= read.v } } + } + register("S2Iterate2ReadsExcluding") { storage in + let nexus = makeTenPercent(storage) + let family = nexus.family(requiresAll: B0.self, B1.self, excludesAll: B2.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self, B1.self) { s ^= $0.v ^ $1.v }; blackHole(s) } + } + register("S2IterateShuffled100k2Reads") { storage in + let nexus = makeShuffled(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self, B1.self) { s ^= $0.v ^ $1.v }; blackHole(s) } + } + register("S2IterateShuffled100k1R1W") { storage in + let nexus = makeShuffled(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { family.forEach(reading: B0.self, writing: B1.self) { read, write in write.v &+= read.v } } + } + register("S2IterateString1R1W") { storage in + let (nexus, entities) = makeUniform(storage) + for entity in entities { + entity.set(Named(name: sharedName)) + } + let family = nexus.family(requiresAll: B0.self, Named.self) + return { family.forEach(reading: B0.self, writing: Named.self) { read, write in write.count &+= Int(truncatingIfNeeded: read.v & 1) } } + } + register("S2IterateFragmented2Reads") { storage in + let (nexus, _) = makeFragmented(storage) + let family = nexus.family(requiresAll: B0.self, B1.self) + return { var s: UInt64 = 0; family.forEach(reading: B0.self, B1.self) { s ^= $0.v ^ $1.v }; blackHole(s) } + } + + // MARK: random access + + register("S2RandomGet") { storage in + let (_, entities) = makeUniform(storage) + var generator = SystemRandomNumberGenerator() + let order = entities.shuffled(using: &generator) + return { var s: UInt64 = 0; for entity in order { + s ^= entity.get(B0.self)?.v ?? 0 + }; blackHole(s) } + } + register("S2RandomGetFragmented") { storage in + let (_, entities) = makeFragmented(storage) + var generator = SystemRandomNumberGenerator() + let order = entities.shuffled(using: &generator) + return { var s: UInt64 = 0; for entity in order { + s ^= entity.get(B0.self)?.v ?? 0 + }; blackHole(s) } + } + + // MARK: structural churn + + // One run adds the component to all 10k entities and removes it again: 20k structural changes. + register("S2ChurnAddRemoveTag10k") { storage in + let (_, entities) = makeUniform(storage) + return { + for entity in entities { + entity.set(Toggle()) + } + for entity in entities { + entity.remove(Toggle.self) + } + } + } + register("S2ChurnAddRemovePayload10k") { storage in + let (_, entities) = makeUniform(storage) + return { + for entity in entities { + entity.set(B9()) + } + for entity in entities { + entity.remove(B9.self) + } + } + } + register("S2ChurnAddRemoveString10k") { storage in + let (_, entities) = makeUniform(storage) + return { + for entity in entities { + entity.set(Named(name: sharedName)) + } + for entity in entities { + entity.remove(Named.self) + } + } + } + register("S2ChurnAddRemoveTag1k") { storage in + let (_, entities) = makeUniform(storage) + let tenth = Array(entities.prefix(entityCount / 10)) + return { + for entity in tenth { + entity.set(Toggle()) + } + for entity in tenth { + entity.remove(Toggle.self) + } + } + } + // entity creation and destruction with seven components + register("S2CreateDestroy1k") { storage in + let nexus = Nexus(storage: storage) + worlds.append(nexus) + return { + let created = (0 ..< 1000).map { _ in nexus.createEntity(with: B0(), B1(), B2(), B3(), B4(), B5(), B6()) } + for entity in created { + nexus.destroy(entity: entity) + } + } + } +} diff --git a/Benchmarks/README.md b/Benchmarks/README.md index 1dd028f0..f570a850 100644 --- a/Benchmarks/README.md +++ b/Benchmarks/README.md @@ -2,6 +2,8 @@ Originally seeded by replicating performance tests into a new form leveraging [benchmark](https://github.com/ordo-one/benchmark) [Documentation](https://swiftpackageindex.com/ordo-one/package-benchmark/main/documentation/benchmark). +The iteration-only (`IterOnly...`) and storage (`S2...`) benchmarks run on both storages of `Nexus(storage:)`: the name carries the storage in brackets, for example `IterOnlyForEachReadWrite [archetype]`. The `S2...` benchmarks are ported from the storage spike and cover iteration at several arities, filtered and fragmented families, 100k entities in permuted storage order, random access and structural churn. + To run all the available benchmarks from the repository root: make benchmark From 23fd1441b112c8368fabfc54ea364a5b92de2926 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sun, 25 Oct 2026 21:50:28 +0100 Subject: [PATCH 12/12] Document choosing between the sparse-set and the archetype storage Adds a DocC article that describes both storages, the flecs design the archetype storage follows, the benchmark numbers and the differences a program can observe (iteration order, single-type spans, per-table span batches). Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N --- .../Documentation.docc/ChoosingAStorage.md | 64 +++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + .../MigratingToFirebladeECS2.md | 4 +- .../ReadingAndWritingComponents.md | 4 +- Sources/FirebladeECS/Nexus+Storage.swift | 4 +- 5 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 Sources/FirebladeECS/Documentation.docc/ChoosingAStorage.md diff --git a/Sources/FirebladeECS/Documentation.docc/ChoosingAStorage.md b/Sources/FirebladeECS/Documentation.docc/ChoosingAStorage.md new file mode 100644 index 00000000..98766629 --- /dev/null +++ b/Sources/FirebladeECS/Documentation.docc/ChoosingAStorage.md @@ -0,0 +1,64 @@ +# Choosing a storage + +Pick the sparse-set or the archetype storage for a ``Nexus``, and know what changes. + +## Overview + +A ``Nexus`` keeps its entities and components in one of two storages, chosen when it is created: + +```swift +let nexus = Nexus(storage: .archetype) // or .sparseSet +``` + +Both storages offer the same API with the same observable behavior: generational entity identifiers, events, the structural lock and its traps, families, `forEach(reading:writing:)`, spans, snapshots, cloning and the entity state machine. Every test of the package runs against both. What differs is where the components live in memory, and with it what is cheap. + +``Nexus/StorageKind/default`` is ``Nexus/StorageKind/sparseSet`` for now. There is no storage protocol: the set of storages is closed, and a nexus switches on its storage once per call, so each storage compiles to concrete code. + +### The sparse-set storage + +``Nexus/StorageKind/sparseSet`` keeps one dense array per component type, a paged sparse index per type that maps an entity to its slot in that array, and a bitset of component types per entity. Adding or removing a component touches the data of that one type only, and no other component moves. Looking up one component is a sparse index read plus an array read. + +Iterating a family is the weak spot: the members of a family are stored in each component array in the order the components were added, so every extra component type costs a lookup per member, and the reads are scattered once the arrays were shuffled by churn. + +### The archetype storage + +``Nexus/StorageKind/archetype`` follows the design of [flecs](https://www.flecs.dev/flecs/md_docs_2Queries.html): entities with exactly the same component types live in one table, with a column (an array) per component type. An index maps every entity to its table and row. Tables are created when a component set appears first, and adding or removing one component type moves the entity's row to another table along a cached edge of the table graph. + +Iterating a family visits the tables that match it, and walks their columns linearly: no lookup per member and a sequential read per component. The list of matching tables is cached per family, which the nexus lists when the family is created and extends when a new table appears. A table that became empty stays alive, along with its columns and its entry in the cached lists, where an iteration skips it. Cleaning the nexus with ``Nexus/clear()`` drops all tables. + +In flecs this is the default, and sparse components are the opt-in trait (`Sparse`): "Sparse components are stored outside of tables, which means they do not have to be moved", trading query speed for component add and remove speed (see [component traits](https://github.com/SanderMertens/flecs/blob/master/docs/ComponentTraits.md)). Fireblade 1.x was the reverse, a sparse set per component type; the two storages make that a choice per nexus instead of per component. + +### What the numbers say + +Measured with 10 000 entities, release build, Apple silicon, wall clock p50 per pass (`make benchmark`, see `Benchmarks/`): + +| Case | Sparse set | Archetype | +| :-- | --: | --: | +| `forEach(reading: A)` | 11 μs | 2.5 μs | +| `forEach(reading: A, B)` | 18 μs | 4.3 μs | +| `forEach(reading: A, B, C, D)` | 31 μs | 7.7 μs | +| `forEach(reading: A, writing: B)` | 17 μs | 4.4 μs | +| `forEach(reading: A, B, writing: C)` | 23 μs | 6.1 μs | +| 2 reads, 10% of the entities match | 3.6 μs | 1.3 μs | +| 2 reads, 100 000 entities with shuffled storage order | 227 μs | 27 μs | +| 2 reads, 1 024 different component sets | 18 μs | 11 μs | +| Parameter pack, 5 reads and 2 writes | 2 200 μs | 1 800 μs | +| Plain `for` over a family, 2 components | 455 μs | 455 μs | +| Random `entity.get` of one component, 10 000 lookups | 334 μs | 345 μs | +| Add a component to each entity and remove it again, 10 000 entities | 2 180 μs | 4 900 μs | +| Create and destroy 1 000 entities with 7 components | 1 730 μs | 2 510 μs | + +Fireblade 1.x needed 480 μs for the plain iteration of one component and 1 220 μs for two (`IterOnlyOneComponent`, `IterOnlyTwoComponents`). Both storages iterate without any allocation. + +- Iterating with `forEach(reading:writing:)` is 1.6 to 8 times faster on the archetype storage: about 4 times in the plain case, 8 times when the sparse-set arrays are shuffled against each other, and least when the entities are spread over many small tables. +- Changing the component set of an entity costs about twice as much on the archetype storage: the whole row moves. An entity that changes its component set less than once in a few frames is far ahead on iteration; a world that toggles a component on most entities every frame is better off with the sparse set. +- Plain `for` iteration and random access cost the same on both: they look every component up per entity. +- Entity creation and destruction are a bit slower on the archetype storage, because the row of every component moves with the entity. + +### Differences you can observe + +- **Iteration order.** The sparse-set storage visits the members of a family in the order of the family's member list, the archetype storage table by table and row by row, and rows move when entities change tables. Neither order is a contract; use ``Entity/identifier`` or your own key if you need one. +- **Single-type spans.** ``Nexus/componentSpan(reading:_:)`` and ``Nexus/componentSpan(writing:_:)`` view one contiguous array. On the archetype storage a type is stored in one column per table, so if entities with different component sets have the type, the span views a temporary copy made table by table (and the writing variant stores it back when the closure returns, also when it throws). That costs one allocation and a copy per call, and nothing can observe the difference while the closure runs, because the type is locked. +- **Family spans.** ``Family/forEachComponentSpan(reading:writing:_:)`` passes one batch per matching table on the archetype storage, however the nexus was used before; the sparse-set storage passes shorter batches after churn. Both visit every member exactly once, and the closure must be correct for any batch size. + +See for the spans. diff --git a/Sources/FirebladeECS/Documentation.docc/Documentation.md b/Sources/FirebladeECS/Documentation.docc/Documentation.md index e637ed48..ba9e837c 100644 --- a/Sources/FirebladeECS/Documentation.docc/Documentation.md +++ b/Sources/FirebladeECS/Documentation.docc/Documentation.md @@ -23,6 +23,7 @@ For a more detailed example of FirebladeECS in action, see the [Fireblade ECS De - - - +- - - ``Nexus`` - ``NexusEvent`` diff --git a/Sources/FirebladeECS/Documentation.docc/MigratingToFirebladeECS2.md b/Sources/FirebladeECS/Documentation.docc/MigratingToFirebladeECS2.md index 32cb18ec..9f5e2c71 100644 --- a/Sources/FirebladeECS/Documentation.docc/MigratingToFirebladeECS2.md +++ b/Sources/FirebladeECS/Documentation.docc/MigratingToFirebladeECS2.md @@ -57,10 +57,10 @@ Component properties of type ``EntityIdentifier`` are encoded as 64 bit raw valu ### Component storage -A ``Nexus`` keeps its components in a storage chosen at creation, ``Nexus/init(storage:)``. ``Nexus/StorageKind/sparseSet`` is the only storage so far and the default, so `Nexus()` works as before. +A ``Nexus`` keeps its components in a storage chosen at creation, ``Nexus/init(storage:)``: ``Nexus/StorageKind/sparseSet`` (the default, so `Nexus()` works as before) or ``Nexus/StorageKind/archetype``. See . There is no storage protocol: the set of storages is closed, so each storage compiles to concrete code. -- Components are stored per type in one dense array with a paged sparse index, and each entity holds a bitset of its component types. Adding, removing and looking up a component touches one type's data only. +- The sparse-set storage stores components per type in one dense array with a paged sparse index, and each entity holds a bitset of its component types. Adding, removing and looking up a component touches one type's data only. The archetype storage stores entities with the same component types together in a table with a column per type. - Every component type has a dense ``Component/componentIndex``. Plain conformances get it lazily, in order of first use and shared by all nexuses of the process. There is no limit on the number of component types. - Public API that returned or took component storage internals is unchanged. ``ManagedContiguousArray`` is no longer used by ``Nexus``; it stays available until it is removed. - The components an entity iterates, and the components of a snapshot or clone, come in ``Component/componentIndex`` order now instead of hash order. diff --git a/Sources/FirebladeECS/Documentation.docc/ReadingAndWritingComponents.md b/Sources/FirebladeECS/Documentation.docc/ReadingAndWritingComponents.md index f6bf7b3a..03bdc759 100644 --- a/Sources/FirebladeECS/Documentation.docc/ReadingAndWritingComponents.md +++ b/Sources/FirebladeECS/Documentation.docc/ReadingAndWritingComponents.md @@ -72,7 +72,7 @@ movers.forEachComponentSpan(reading: Velocity.self, writing: Position.self) { ve } ``` -- ``Nexus/componentSpan(reading:_:)`` and ``Nexus/componentSpan(writing:_:)`` view every component of one type, in storage order. +- ``Nexus/componentSpan(reading:_:)`` and ``Nexus/componentSpan(writing:_:)`` view every component of one type, in storage order. On the archetype storage they view a copy when entities with different component sets have the type, see . - ``Family/forEachComponentSpan(reading:writing:_:)`` visits the members of a family in batches. Overloads exist for 0 to 2 read and 0 to 2 written types. - A span exists only inside the closure. `Span` is `~Escapable`, so storing it in a variable outside the closure or returning it does not compile (`var leaked: Span?; nexus.componentSpan(reading: Position.self) { leaked = $0 }` is an error). This is what makes it safe: no span can outlive the storage it views. Returning a span from a property would need the experimental `@lifetime` attribute, so there is none. - `Span` is not a `Sequence`: loop over `indices`, and use `span[i]`, which is bounds checked. @@ -82,7 +82,7 @@ movers.forEachComponentSpan(reading: Velocity.self, writing: Position.self) { ve #### Batches of a family -The sparse set storage keeps every component type in its own array. The components of the members of a family are consecutive in all involved arrays only as long as nothing was removed: removing a component moves the last component of that type into the gap. ``Family/forEachComponentSpan(reading:writing:_:)`` therefore passes a *batch*: a maximal run of members whose components are consecutive in every involved array. The spans view the storage directly (nothing is copied or allocated), so the batch size depends on the history of the nexus: one batch for entities created together, shorter ones after churn, one element at the worst. Write the closure so that it is correct for any batch size. Storages that keep the columns aligned (archetype tables, owning groups) will pass one batch per table. +The sparse set storage keeps every component type in its own array. The components of the members of a family are consecutive in all involved arrays only as long as nothing was removed: removing a component moves the last component of that type into the gap. ``Family/forEachComponentSpan(reading:writing:_:)`` therefore passes a *batch*: a maximal run of members whose components are consecutive in every involved array. The spans view the storage directly (nothing is copied or allocated), so the batch size depends on the history of the nexus: one batch for entities created together, shorter ones after churn, one element at the worst. Write the closure so that it is correct for any batch size. The archetype storage keeps the columns of a table aligned and passes exactly one batch per matching table, see . Spans pay off when the batch is large: ``Nexus/componentSpan(reading:_:)`` over a whole type is about 3 times faster than ``Family/forEach(reading:writing:_:)`` in the benchmarks (10 000 entities, see below). The family batches are not faster on the sparse set storage: finding the batches costs one lookup per member and component type, as much as `forEach`. diff --git a/Sources/FirebladeECS/Nexus+Storage.swift b/Sources/FirebladeECS/Nexus+Storage.swift index d6c3ccc6..706469f1 100644 --- a/Sources/FirebladeECS/Nexus+Storage.swift +++ b/Sources/FirebladeECS/Nexus+Storage.swift @@ -8,7 +8,9 @@ extension Nexus { /// The storage a ``Nexus`` keeps its entities and components in. /// - /// The set of storages is closed: a nexus owns exactly one and switches on it once per call. + /// The set of storages is closed: a nexus owns exactly one and switches on it once per call. Both offer the same API + /// and observable behavior; ``Nexus/StorageKind/archetype`` iterates families faster and ``Nexus/StorageKind/sparseSet`` + /// changes the component set of an entity faster. See . public enum StorageKind: CaseIterable, Sendable { /// One dense array per component type, a paged sparse index per type, a bitset per entity. ///