• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mlange-42 / ark / 15075416534

16 May 2025 06:54PM CUT coverage: 99.833%. Remained the same
15075416534

Pull #251

github

web-flow
Merge a3a4140a2 into 94436e9e0
Pull Request #251: Prepare release v0.4.3

8352 of 8366 relevant lines covered (99.83%)

18973.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.22
/ecs/storage.go
1
package ecs
2

3
import (
4
        "fmt"
5
        "unsafe"
6
)
7

8
type storage struct {
9
        entities           []entityIndex
10
        isTarget           []bool
11
        graph              graph
12
        archetypes         []archetype
13
        relationArchetypes []archetypeID
14
        tables             []table
15
        components         []componentStorage
16
        cache              cache
17
        entityPool         entityPool
18
        registry           componentRegistry
19
        config             config
20
}
21

22
type componentStorage struct {
23
        columns []*column
24
}
25

26
func newStorage(numArchetypes int, capacity ...int) storage {
256✔
27
        config := newConfig(capacity...)
256✔
28

256✔
29
        reg := newComponentRegistry()
256✔
30
        entities := make([]entityIndex, reservedEntities, config.initialCapacity+reservedEntities)
256✔
31
        isTarget := make([]bool, reservedEntities, config.initialCapacity+reservedEntities)
256✔
32
        // Reserved zero and wildcard entities
256✔
33
        for i := range reservedEntities {
768✔
34
                entities[i] = entityIndex{table: maxTableID, row: 0}
512✔
35
        }
512✔
36
        componentsMap := make([]int16, maskTotalBits)
256✔
37
        for i := range maskTotalBits {
65,792✔
38
                componentsMap[i] = -1
65,536✔
39
        }
65,536✔
40

41
        archetypes := make([]archetype, 0, numArchetypes)
256✔
42
        archetypes = append(archetypes, newArchetype(0, 0, &bitMask{}, []ID{}, []tableID{0}, &reg))
256✔
43
        tables := make([]table, 0, numArchetypes)
256✔
44
        tables = append(tables, newTable(0, &archetypes[0], uint32(config.initialCapacity), &reg, []Entity{}, []RelationID{}))
256✔
45
        return storage{
256✔
46
                config:     config,
256✔
47
                registry:   reg,
256✔
48
                cache:      newCache(),
256✔
49
                entities:   entities,
256✔
50
                isTarget:   isTarget,
256✔
51
                entityPool: newEntityPool(uint32(config.initialCapacity), reservedEntities),
256✔
52
                graph:      newGraph(),
256✔
53
                archetypes: archetypes,
256✔
54
                tables:     tables,
256✔
55
                components: make([]componentStorage, 0, maskTotalBits),
256✔
56
        }
256✔
57
}
58

59
func (s *storage) findOrCreateTable(oldTable *table, add []ID, remove []ID, relations []RelationID, outMask *bitMask) *table {
517,945✔
60
        startNode := s.archetypes[oldTable.archetype].node
517,945✔
61

517,945✔
62
        node := s.graph.Find(startNode, add, remove, outMask)
517,945✔
63
        var arch *archetype
517,945✔
64
        if archID, ok := node.GetArchetype(); ok {
1,035,404✔
65
                arch = &s.archetypes[archID]
517,459✔
66
        } else {
517,945✔
67
                arch = s.createArchetype(node)
486✔
68
                node.archetype = arch.id
486✔
69
        }
486✔
70

71
        var allRelations []RelationID
517,945✔
72
        if len(relations) > 0 {
520,630✔
73
                allRelations = appendNew(oldTable.relationIDs, relations...)
2,685✔
74
        } else {
517,945✔
75
                allRelations = oldTable.relationIDs
515,260✔
76
        }
515,260✔
77
        table, ok := arch.GetTable(s, allRelations)
517,945✔
78
        if !ok {
520,493✔
79
                table = s.createTable(arch, allRelations)
2,548✔
80
        }
2,548✔
81
        return table
517,944✔
82
}
83

84
func (s *storage) AddComponent(id uint8) {
1,330✔
85
        if len(s.components) != int(id) {
1,331✔
86
                panic("components can only be added to a storage sequentially")
1✔
87
        }
88
        s.components = append(s.components, componentStorage{columns: make([]*column, len(s.tables))})
1,329✔
89
}
90

91
// RemoveEntity removes the given entity from the world.
92
func (s *storage) RemoveEntity(entity Entity) {
510,534✔
93
        if !s.entityPool.Alive(entity) {
510,535✔
94
                panic("can't remove a dead entity")
1✔
95
        }
96
        index := &s.entities[entity.id]
510,533✔
97
        table := &s.tables[index.table]
510,533✔
98

510,533✔
99
        swapped := table.Remove(index.row, &s.registry)
510,533✔
100

510,533✔
101
        s.entityPool.Recycle(entity)
510,533✔
102

510,533✔
103
        if swapped {
1,013,936✔
104
                swapEntity := table.GetEntity(uintptr(index.row))
503,403✔
105
                s.entities[swapEntity.id].row = index.row
503,403✔
106
        }
503,403✔
107
        index.table = maxTableID
510,533✔
108

510,533✔
109
        if s.isTarget[entity.id] {
510,536✔
110
                s.cleanupArchetypes(entity)
3✔
111
                s.isTarget[entity.id] = false
3✔
112
        }
3✔
113
}
114

115
func (s *storage) Reset() {
1✔
116
        s.entities = s.entities[:reservedEntities]
1✔
117
        s.entityPool.Reset()
1✔
118
        s.isTarget = s.isTarget[:reservedEntities]
1✔
119
        s.cache.Reset()
1✔
120

1✔
121
        for i := range s.archetypes {
5✔
122
                s.archetypes[i].Reset(s)
4✔
123
        }
4✔
124
}
125

126
func (s *storage) get(entity Entity, component ID) unsafe.Pointer {
5✔
127
        if !s.entityPool.Alive(entity) {
6✔
128
                panic("can't get component of a dead entity")
1✔
129
        }
130
        return s.getUnchecked(entity, component)
4✔
131
}
132

133
func (s *storage) getUnchecked(entity Entity, component ID) unsafe.Pointer {
5✔
134
        s.checkHasComponent(entity, component)
5✔
135
        index := s.entities[entity.id]
5✔
136
        return s.tables[index.table].Get(component, uintptr(index.row))
5✔
137
}
5✔
138

139
func (s *storage) has(entity Entity, component ID) bool {
17✔
140
        if !s.entityPool.Alive(entity) {
18✔
141
                panic("can't get component of a dead entity")
1✔
142
        }
143
        return s.hasUnchecked(entity, component)
16✔
144
}
145

146
func (s *storage) hasUnchecked(entity Entity, component ID) bool {
18✔
147
        index := s.entities[entity.id]
18✔
148
        return s.tables[index.table].Has(component)
18✔
149
}
18✔
150

151
func (s *storage) getRelation(entity Entity, comp ID) Entity {
457✔
152
        if !s.entityPool.Alive(entity) {
458✔
153
                panic("can't get relation for a dead entity")
1✔
154
        }
155
        return s.getRelationUnchecked(entity, comp)
456✔
156
}
157

158
func (s *storage) getRelationUnchecked(entity Entity, comp ID) Entity {
461✔
159
        s.checkHasComponent(entity, comp)
461✔
160
        return s.tables[s.entities[entity.id].table].GetRelation(comp)
461✔
161
}
461✔
162

163
func (s *storage) registerTargets(relations []RelationID) {
518,007✔
164
        for _, rel := range relations {
520,812✔
165
                s.isTarget[rel.target.id] = true
2,805✔
166
        }
2,805✔
167
}
168

169
func (s *storage) registerFilter(filter *filter, relations []RelationID) cacheID {
34✔
170
        return s.cache.register(s, filter, relations)
34✔
171
}
34✔
172

173
func (s *storage) unregisterFilter(entry cacheID) {
29✔
174
        s.cache.unregister(entry)
29✔
175
}
29✔
176

177
func (s *storage) getRegisteredFilter(id cacheID) *cacheEntry {
30✔
178
        return s.cache.getEntry(id)
30✔
179
}
30✔
180

181
func (s *storage) createEntity(table tableID) (Entity, uint32) {
514,259✔
182
        entity := s.entityPool.Get()
514,259✔
183

514,259✔
184
        idx := s.tables[table].Add(entity)
514,259✔
185
        len := len(s.entities)
514,259✔
186
        if int(entity.id) == len {
519,257✔
187
                s.entities = append(s.entities, entityIndex{table: table, row: idx})
4,998✔
188
                s.isTarget = append(s.isTarget, false)
4,998✔
189
        } else {
514,259✔
190
                s.entities[entity.id] = entityIndex{table: table, row: idx}
509,261✔
191
        }
509,261✔
192
        return entity, idx
514,259✔
193
}
194

195
func (s *storage) createEntities(table *table, count int) {
196✔
196
        startIdx := table.Len()
196✔
197
        table.Alloc(uint32(count))
196✔
198

196✔
199
        len := len(s.entities)
196✔
200
        for i := range count {
3,859✔
201
                index := uint32(startIdx + i)
3,663✔
202
                entity := s.entityPool.Get()
3,663✔
203
                table.SetEntity(index, entity)
3,663✔
204

3,663✔
205
                if int(entity.id) == len {
7,100✔
206
                        s.entities = append(s.entities, entityIndex{table: table.id, row: index})
3,437✔
207
                        s.isTarget = append(s.isTarget, false)
3,437✔
208
                        len++
3,437✔
209
                } else {
3,663✔
210
                        s.entities[entity.id] = entityIndex{table: table.id, row: index}
226✔
211
                }
226✔
212
        }
213
}
214

215
func (s *storage) createArchetype(node *node) *archetype {
486✔
216
        comps := node.mask.toTypes(&s.registry.registry)
486✔
217
        index := len(s.archetypes)
486✔
218
        s.archetypes = append(s.archetypes, newArchetype(archetypeID(index), node.id, &node.mask, comps, nil, &s.registry))
486✔
219
        archetype := &s.archetypes[index]
486✔
220
        if archetype.HasRelations() {
546✔
221
                s.relationArchetypes = append(s.relationArchetypes, archetype.id)
60✔
222
        }
60✔
223
        return archetype
486✔
224
}
225

226
func (s *storage) createTable(archetype *archetype, relations []RelationID) *table {
2,579✔
227
        // TODO: maybe use a pool of slices?
2,579✔
228
        targets := make([]Entity, len(archetype.components))
2,579✔
229

2,579✔
230
        if uint8(len(relations)) < archetype.numRelations {
2,579✔
231
                // TODO: is there way to trigger this?
×
232
                panic("relation targets must be fully specified")
×
233
        }
234
        for _, rel := range relations {
4,746✔
235
                idx := archetype.componentsMap[rel.component.id]
2,167✔
236
                targets[idx] = rel.target
2,167✔
237
        }
2,167✔
238
        for i := range relations {
4,746✔
239
                rel := &relations[i]
2,167✔
240
                s.checkRelationComponent(rel.component)
2,167✔
241
                s.checkRelationTarget(rel.target)
2,167✔
242
        }
2,167✔
243

244
        var newTableID tableID
2,579✔
245
        recycled := false
2,579✔
246
        if id, ok := archetype.GetFreeTable(); ok {
2,583✔
247
                newTableID = id
4✔
248
                s.tables[newTableID].recycle(targets, relations)
4✔
249
                recycled = true
4✔
250
        } else {
2,579✔
251
                newTableID = tableID(len(s.tables))
2,575✔
252
                cap := s.config.initialCapacity
2,575✔
253
                if archetype.HasRelations() {
4,724✔
254
                        cap = s.config.initialCapacityRelations
2,149✔
255
                }
2,149✔
256
                s.tables = append(s.tables, newTable(
2,575✔
257
                        newTableID, archetype, uint32(cap), &s.registry,
2,575✔
258
                        targets, relations))
2,575✔
259
        }
260
        archetype.AddTable(&s.tables[newTableID])
2,579✔
261

2,579✔
262
        table := &s.tables[newTableID]
2,579✔
263
        if !recycled {
5,154✔
264
                for i := range s.components {
11,063✔
265
                        id := ID{id: uint8(i)}
8,488✔
266
                        comps := &s.components[i]
8,488✔
267
                        if archetype.mask.Get(id) {
15,857✔
268
                                comps.columns = append(comps.columns, table.GetColumn(id))
7,369✔
269
                        } else {
8,488✔
270
                                comps.columns = append(comps.columns, nil)
1,119✔
271
                        }
1,119✔
272
                }
273
        }
274

275
        s.cache.addTable(s, table)
2,579✔
276
        return table
2,579✔
277
}
278

279
// Removes empty archetypes that have a target relation to the given entity.
280
func (s *storage) cleanupArchetypes(target Entity) {
12✔
281
        newRelations := []RelationID{}
12✔
282
        for _, arch := range s.relationArchetypes {
31✔
283
                archetype := &s.archetypes[arch]
19✔
284
                len := len(archetype.tables)
19✔
285
                for i := len - 1; i >= 0; i-- {
47✔
286
                        table := &s.tables[archetype.tables[i]]
28✔
287

28✔
288
                        foundTarget := false
28✔
289
                        for _, rel := range table.relationIDs {
64✔
290
                                if rel.target.id == target.id {
50✔
291
                                        newRelations = append(newRelations, RelationID{component: rel.component, target: Entity{}})
14✔
292
                                        foundTarget = true
14✔
293
                                }
14✔
294
                        }
295
                        if !foundTarget {
42✔
296
                                continue
14✔
297
                        }
298

299
                        if table.Len() > 0 {
15✔
300
                                allRelations := s.getExchangeTargetsUnchecked(table, newRelations)
1✔
301
                                newTable, ok := archetype.GetTable(s, allRelations)
1✔
302
                                if !ok {
2✔
303
                                        newTable = s.createTable(archetype, newRelations)
1✔
304
                                        // Get the old table again, as pointers may have changed.
1✔
305
                                        table = &s.tables[table.id]
1✔
306
                                }
1✔
307
                                s.moveEntities(table, newTable, uint32(table.Len()))
1✔
308
                        }
309
                        archetype.FreeTable(table.id)
14✔
310
                        s.cache.removeTable(s, table)
14✔
311

14✔
312
                        newRelations = newRelations[:0]
14✔
313
                }
314
                archetype.RemoveTarget(target)
19✔
315
        }
316
}
317

318
// moveEntities moves all entities from src to dst.
319
func (s *storage) moveEntities(src, dst *table, count uint32) {
14✔
320
        oldLen := dst.Len()
14✔
321
        dst.AddAll(src, count, &s.registry)
14✔
322

14✔
323
        newLen := dst.Len()
14✔
324
        newTable := dst.id
14✔
325
        for i := oldLen; i < newLen; i++ {
358✔
326
                entity := dst.GetEntity(uintptr(i))
344✔
327
                s.entities[entity.id] = entityIndex{table: newTable, row: uint32(i)}
344✔
328
        }
344✔
329
        src.Reset()
14✔
330
}
331

332
func (s *storage) getExchangeTargetsUnchecked(oldTable *table, relations []RelationID) []RelationID {
1✔
333
        // TODO: maybe use a pool of slices?
1✔
334
        targets := make([]Entity, len(oldTable.columns))
1✔
335
        for i := range oldTable.columns {
3✔
336
                targets[i] = oldTable.columns[i].target
2✔
337
        }
2✔
338
        for _, rel := range relations {
2✔
339
                column := oldTable.components[rel.component.id]
1✔
340
                if rel.target == targets[column.index] {
1✔
341
                        continue
×
342
                }
343
                targets[column.index] = rel.target
1✔
344
        }
345

346
        result := make([]RelationID, 0, len(oldTable.relationIDs))
1✔
347
        for i, e := range targets {
3✔
348
                if !oldTable.columns[i].isRelation {
3✔
349
                        continue
1✔
350
                }
351
                id := oldTable.ids[i]
1✔
352
                result = append(result, RelationID{component: id, target: e})
1✔
353
        }
354

355
        return result
1✔
356
}
357

358
func (s *storage) getExchangeTargets(oldTable *table, relations []RelationID) ([]RelationID, bool) {
65✔
359
        changed := false
65✔
360
        // TODO: maybe use a pool of slices?
65✔
361
        targets := make([]Entity, len(oldTable.columns))
65✔
362
        for i := range oldTable.columns {
304✔
363
                targets[i] = oldTable.columns[i].target
239✔
364
        }
239✔
365
        for _, rel := range relations {
131✔
366
                // Validity of the target is checked when creating a new table.
66✔
367
                // Whether the component is a relation is checked when creating a new table.
66✔
368
                column := oldTable.components[rel.component.id]
66✔
369
                if column == nil {
67✔
370
                        tp, _ := s.registry.ComponentType(rel.component.id)
1✔
371
                        panic(fmt.Sprintf("entity has no component of type %s to set relation target for", tp.Name()))
1✔
372
                }
373
                if rel.target == targets[column.index] {
66✔
374
                        continue
1✔
375
                }
376
                targets[column.index] = rel.target
64✔
377
                changed = true
64✔
378
        }
379
        if !changed {
65✔
380
                return nil, false
1✔
381
        }
1✔
382

383
        result := make([]RelationID, 0, len(oldTable.relationIDs))
63✔
384
        for i, e := range targets {
297✔
385
                if !oldTable.columns[i].isRelation {
402✔
386
                        continue
168✔
387
                }
388
                id := oldTable.ids[i]
66✔
389
                result = append(result, RelationID{component: id, target: e})
66✔
390
        }
391

392
        return result, true
63✔
393
}
394

395
func (s *storage) getTables(batch *Batch) []tableID {
119✔
396
        tables := []tableID{}
119✔
397

119✔
398
        if batch.cache != maxCacheID {
120✔
399
                cache := s.getRegisteredFilter(batch.cache)
1✔
400
                for _, tableID := range cache.tables {
3✔
401
                        table := &s.tables[tableID]
2✔
402
                        if table.Len() == 0 {
3✔
403
                                continue
1✔
404
                        }
405
                        if !table.Matches(batch.relations) {
1✔
406
                                continue
×
407
                        }
408
                        tables = append(tables, tableID)
1✔
409
                }
410
                return tables
1✔
411
        }
412

413
        for i := range s.archetypes {
546✔
414
                archetype := &s.archetypes[i]
428✔
415
                if !batch.filter.matches(archetype.mask) {
629✔
416
                        continue
201✔
417
                }
418

419
                if !archetype.HasRelations() {
436✔
420
                        table := &s.tables[archetype.tables[0]]
209✔
421
                        tables = append(tables, table.id)
209✔
422
                        continue
209✔
423
                }
424

425
                tableIDs := archetype.GetTables(batch.relations)
18✔
426
                for _, tab := range tableIDs {
42✔
427
                        table := &s.tables[tab]
24✔
428
                        if !table.Matches(batch.relations) {
24✔
429
                                continue
×
430
                        }
431
                        tables = append(tables, tab)
24✔
432
                }
433
        }
434
        return tables
118✔
435
}
436

437
func (s *storage) getTableIDs(filter *filter, relations []RelationID) []tableID {
36✔
438
        tables := []tableID{}
36✔
439

36✔
440
        for i := range s.archetypes {
108✔
441
                archetype := &s.archetypes[i]
72✔
442
                if !filter.matches(archetype.mask) {
109✔
443
                        continue
37✔
444
                }
445

446
                if !archetype.HasRelations() {
45✔
447
                        tables = append(tables, archetype.tables[0])
10✔
448
                        continue
10✔
449
                }
450

451
                tableIDs := archetype.GetTables(relations)
25✔
452
                for _, tab := range tableIDs {
84✔
453
                        table := &s.tables[tab]
59✔
454
                        if !table.Matches(relations) {
59✔
455
                                continue
×
456
                        }
457
                        tables = append(tables, tab)
59✔
458
                }
459
        }
460
        return tables
36✔
461
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc