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

mlange-42 / ark / 13618572525

02 Mar 2025 07:23PM CUT coverage: 96.93% (-0.5%) from 97.422%
13618572525

Pull #91

github

web-flow
Merge a19dd6549 into a609eb208
Pull Request #91: Batch operations

511 of 550 new or added lines in 6 files covered. (92.91%)

14 existing lines in 1 file now uncovered.

4389 of 4528 relevant lines covered (96.93%)

40787.75 hits per line

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

94.68
/ecs/storage.go
1
package ecs
2

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

8
type storage struct {
9
        registry   componentRegistry
10
        entities   entities
11
        isTarget   []bool
12
        entityPool entityPool
13

14
        archetypes         []archetype
15
        relationArchetypes []archetypeID
16
        tables             []table
17
        components         []componentStorage
18
        initialCapacity    uint32
19
}
20

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

25
func newStorage(capacity uint32) storage {
137✔
26
        reg := newComponentRegistry()
137✔
27
        entities := make([]entityIndex, reservedEntities, capacity+reservedEntities)
137✔
28
        isTarget := make([]bool, reservedEntities, capacity+reservedEntities)
137✔
29
        // Reserved zero and wildcard entities
137✔
30
        for i := range reservedEntities {
411✔
31
                entities[i] = entityIndex{table: maxTableID, row: 0}
274✔
32
        }
274✔
33
        componentsMap := make([]int16, MaskTotalBits)
137✔
34
        for i := range MaskTotalBits {
35,209✔
35
                componentsMap[i] = -1
35,072✔
36
        }
35,072✔
37

38
        tables := make([]table, 0, 128)
137✔
39
        tables = append(tables, newTable(0, 0, capacity, &reg, []ID{}, componentsMap, []bool{}, []Entity{}, []RelationID{}))
137✔
40
        archetypes := make([]archetype, 0, 128)
137✔
41
        archetypes = append(archetypes, newArchetype(0, &Mask{}, []ID{}, []tableID{0}, &reg))
137✔
42
        return storage{
137✔
43
                registry:        reg,
137✔
44
                entities:        entities,
137✔
45
                isTarget:        isTarget,
137✔
46
                entityPool:      newEntityPool(capacity, reservedEntities),
137✔
47
                archetypes:      archetypes,
137✔
48
                tables:          tables,
137✔
49
                initialCapacity: capacity,
137✔
50
                components:      make([]componentStorage, 0, MaskTotalBits),
137✔
51
        }
137✔
52
}
53

54
func (s *storage) findOrCreateTable(oldTable *table, mask *Mask, relations []RelationID) *table {
493,390✔
55
        // TODO: use archetype graph
493,390✔
56
        var arch *archetype
493,390✔
57
        for i := range s.archetypes {
1,480,148✔
58
                if s.archetypes[i].mask.Equals(mask) {
1,479,938✔
59
                        arch = &s.archetypes[i]
493,180✔
60
                        break
493,180✔
61
                }
62
        }
63
        if arch == nil {
493,600✔
64
                arch = s.createArchetype(mask)
210✔
65
        }
210✔
66
        allRelation := appendNew(oldTable.relationIDs, relations...)
493,390✔
67
        table, ok := arch.GetTable(s, allRelation)
493,390✔
68
        if !ok {
493,625✔
69
                table = s.createTable(arch, allRelation)
235✔
70
        }
235✔
71
        return table
493,390✔
72
}
73

74
func (s *storage) AddComponent(id uint8) {
590✔
75
        if len(s.components) != int(id) {
590✔
76
                panic("components can only be added to a storage sequentially")
×
77
        }
78
        s.components = append(s.components, componentStorage{columns: make([]*column, len(s.tables))})
590✔
79
}
80

81
// RemoveEntity removes the given entity from the world.
82
func (s *storage) RemoveEntity(entity Entity) {
491,714✔
83
        if !s.entityPool.Alive(entity) {
491,714✔
84
                panic("can't remove a dead entity")
×
85
        }
86
        index := &s.entities[entity.id]
491,714✔
87
        table := &s.tables[index.table]
491,714✔
88

491,714✔
89
        swapped := table.Remove(index.row)
491,714✔
90

491,714✔
91
        s.entityPool.Recycle(entity)
491,714✔
92

491,714✔
93
        if swapped {
982,333✔
94
                swapEntity := table.GetEntity(uintptr(index.row))
490,619✔
95
                s.entities[swapEntity.id].row = index.row
490,619✔
96
        }
490,619✔
97
        index.table = maxTableID
491,714✔
98

491,714✔
99
        if s.isTarget[entity.id] {
491,716✔
100
                s.cleanupArchetypes(entity)
2✔
101
                s.isTarget[entity.id] = false
2✔
102
        }
2✔
103
}
104

105
func (s *storage) Reset() {
1✔
106
        s.entities = s.entities[:reservedEntities]
1✔
107
        s.entityPool.Reset()
1✔
108
        s.isTarget = s.isTarget[:reservedEntities]
1✔
109

1✔
110
        for i := range s.archetypes {
5✔
111
                s.archetypes[i].Reset(s)
4✔
112
        }
4✔
113
}
114

115
func (s *storage) get(entity Entity, component ID) unsafe.Pointer {
5✔
116
        if !s.entityPool.Alive(entity) {
6✔
117
                panic("can't get component of a dead entity")
1✔
118
        }
119
        return s.getUnchecked(entity, component)
4✔
120
}
121

122
func (s *storage) getUnchecked(entity Entity, component ID) unsafe.Pointer {
5✔
123
        s.checkHasComponent(entity, component)
5✔
124
        index := s.entities[entity.id]
5✔
125
        return s.tables[index.table].Get(component, uintptr(index.row))
5✔
126
}
5✔
127

128
func (s *storage) has(entity Entity, component ID) bool {
17✔
129
        if !s.entityPool.Alive(entity) {
18✔
130
                panic("can't get component of a dead entity")
1✔
131
        }
132
        return s.hasUnchecked(entity, component)
16✔
133
}
134

135
func (s *storage) hasUnchecked(entity Entity, component ID) bool {
18✔
136
        s.checkHasComponent(entity, component)
18✔
137
        index := s.entities[entity.id]
18✔
138
        return s.tables[index.table].Has(component)
18✔
139
}
18✔
140

141
func (s *storage) getRelation(entity Entity, comp ID) Entity {
129✔
142
        if !s.entityPool.Alive(entity) {
129✔
143
                panic("can't get relation for a dead entity")
×
144
        }
145
        return s.getRelationUnchecked(entity, comp)
129✔
146
}
147

148
func (s *storage) getRelationUnchecked(entity Entity, comp ID) Entity {
133✔
149
        s.checkHasComponent(entity, comp)
133✔
150
        return s.tables[s.entities[entity.id].table].GetRelation(comp)
133✔
151
}
133✔
152

153
func (s *storage) registerTargets(relations []RelationID) {
493,433✔
154
        for _, rel := range relations {
493,909✔
155
                s.isTarget[rel.target.id] = true
476✔
156
        }
476✔
157
}
158

159
func (s *storage) createEntity(table tableID) (Entity, uint32) {
492,964✔
160
        entity := s.entityPool.Get()
492,964✔
161

492,964✔
162
        idx := s.tables[table].Add(entity)
492,964✔
163
        len := len(s.entities)
492,964✔
164
        if int(entity.id) == len {
497,415✔
165
                s.entities = append(s.entities, entityIndex{table: table, row: idx})
4,451✔
166
                s.isTarget = append(s.isTarget, false)
4,451✔
167
        } else {
492,964✔
168
                s.entities[entity.id] = entityIndex{table: table, row: idx}
488,513✔
169
        }
488,513✔
170
        return entity, idx
492,964✔
171
}
172

173
func (s *storage) createEntities(table *table, count int) {
48✔
174
        startIdx := table.Len()
48✔
175
        table.Alloc(uint32(count))
48✔
176

48✔
177
        len := len(s.entities)
48✔
178
        for i := range count {
816✔
179
                index := uint32(startIdx + i)
768✔
180
                entity := s.entityPool.Get()
768✔
181
                table.SetEntity(index, entity)
768✔
182

768✔
183
                if int(entity.id) == len {
1,536✔
184
                        s.entities = append(s.entities, entityIndex{table: table.id, row: index})
768✔
185
                        s.isTarget = append(s.isTarget, false)
768✔
186
                        len++
768✔
187
                } else {
768✔
188
                        s.entities[entity.id] = entityIndex{table: table.id, row: index}
×
189
                }
×
190
        }
191
}
192

193
func (s *storage) createArchetype(mask *Mask) *archetype {
210✔
194
        comps := mask.toTypes(&s.registry.registry)
210✔
195
        index := len(s.archetypes)
210✔
196
        s.archetypes = append(s.archetypes, newArchetype(archetypeID(index), mask, comps, nil, &s.registry))
210✔
197
        archetype := &s.archetypes[index]
210✔
198
        if archetype.HasRelations() {
238✔
199
                s.relationArchetypes = append(s.relationArchetypes, archetype.id)
28✔
200
        }
28✔
201
        return archetype
210✔
202
}
203

204
func (s *storage) createTable(archetype *archetype, relations []RelationID) *table {
249✔
205
        targets := make([]Entity, len(archetype.components))
249✔
206
        numRelations := uint8(0)
249✔
207
        for _, rel := range relations {
324✔
208
                idx := archetype.componentsMap[rel.component.id]
75✔
209
                targets[idx] = rel.target
75✔
210
                numRelations++
75✔
211
        }
75✔
212
        if numRelations != archetype.numRelations {
249✔
213
                panic("relations must be fully specified")
×
214
        }
215

216
        var newTableID tableID
249✔
217
        if id, ok := archetype.GetFreeTable(); ok {
250✔
218
                newTableID = id
1✔
219
                s.tables[newTableID].recycle(targets, relations)
1✔
220
        } else {
249✔
221
                newTableID = tableID(len(s.tables))
248✔
222
                s.tables = append(s.tables, newTable(
248✔
223
                        newTableID, archetype.id, s.initialCapacity, &s.registry,
248✔
224
                        archetype.components, archetype.componentsMap,
248✔
225
                        archetype.isRelation, targets, relations))
248✔
226
        }
248✔
227
        archetype.AddTable(&s.tables[newTableID])
249✔
228

249✔
229
        table := &s.tables[newTableID]
249✔
230
        for i := range s.components {
1,489✔
231
                id := ID{id: uint8(i)}
1,240✔
232
                comps := &s.components[i]
1,240✔
233
                if archetype.mask.Get(id) {
2,110✔
234
                        comps.columns = append(comps.columns, table.GetColumn(id))
870✔
235
                } else {
1,240✔
236
                        comps.columns = append(comps.columns, nil)
370✔
237
                }
370✔
238
        }
239
        return table
249✔
240
}
241

242
func (s *storage) getExchangeMask(mask *Mask, add []ID, rem []ID) {
710✔
243
        for _, comp := range rem {
1,844✔
244
                if !mask.Get(comp) {
1,134✔
245
                        panic(fmt.Sprintf("entity does not have a component of type %v, can't remove", s.registry.Types[comp.id]))
×
246
                }
247
                mask.Set(comp, false)
1,134✔
248
        }
249
        for _, comp := range add {
1,602✔
250
                if mask.Get(comp) {
892✔
251
                        panic(fmt.Sprintf("entity already has component of type %v, can't add", s.registry.Types[comp.id]))
×
252
                }
253
                mask.Set(comp, true)
892✔
254
        }
255
}
256

257
// Removes empty archetypes that have a target relation to the given entity.
258
func (s *storage) cleanupArchetypes(target Entity) {
2✔
259
        newRelations := []RelationID{}
2✔
260
        for _, arch := range s.relationArchetypes {
4✔
261
                archetype := &s.archetypes[arch]
2✔
262
                for _, t := range archetype.tables {
6✔
263
                        table := &s.tables[t]
4✔
264

4✔
265
                        foundTarget := false
4✔
266
                        for _, rel := range table.relationIDs {
8✔
267
                                if rel.target.id == target.id {
6✔
268
                                        newRelations = append(newRelations, RelationID{component: rel.component, target: Entity{}})
2✔
269
                                        foundTarget = true
2✔
270
                                }
2✔
271
                        }
272
                        if !foundTarget {
6✔
273
                                continue
2✔
274
                        }
275

276
                        allRelations := s.getExchangeTargetsUnchecked(table, newRelations)
2✔
277
                        newTable, ok := archetype.GetTable(s, allRelations)
2✔
278
                        if !ok {
4✔
279
                                newTable = s.createTable(archetype, newRelations)
2✔
280
                        }
2✔
281
                        s.moveEntities(table, newTable)
2✔
282
                        archetype.FreeTable(table.id)
2✔
283

2✔
284
                        newRelations = newRelations[:0]
2✔
285
                }
286
                archetype.RemoveTarget(target)
2✔
287
        }
288
}
289

290
// moveEntities moves all entities from src to dst.
291
func (s *storage) moveEntities(src, dst *table) {
2✔
292
        oldLen := dst.Len()
2✔
293
        dst.AddAll(src)
2✔
294

2✔
295
        newLen := dst.Len()
2✔
296
        newTable := dst.id
2✔
297
        for i := oldLen; i < newLen; i++ {
34✔
298
                entity := dst.GetEntity(uintptr(i))
32✔
299
                s.entities[entity.id] = entityIndex{table: newTable, row: uint32(i)}
32✔
300
        }
32✔
301
        src.Reset()
2✔
302
}
303

304
func (s *storage) getExchangeTargetsUnchecked(oldTable *table, relations []RelationID) []RelationID {
2✔
305
        targets := make([]Entity, len(oldTable.columns))
2✔
306
        for i := range oldTable.columns {
6✔
307
                targets[i] = oldTable.columns[i].target
4✔
308
        }
4✔
309
        for _, rel := range relations {
4✔
310
                index := oldTable.components[rel.component.id]
2✔
311
                if rel.target == targets[index] {
2✔
312
                        continue
×
313
                }
314
                targets[index] = rel.target
2✔
315
        }
316

317
        result := make([]RelationID, 0, len(oldTable.relationIDs))
2✔
318
        for i, e := range targets {
6✔
319
                if !oldTable.columns[i].isRelation {
6✔
320
                        continue
2✔
321
                }
322
                id := oldTable.ids[i]
2✔
323
                result = append(result, RelationID{component: id, target: e})
2✔
324
        }
325

326
        return result
2✔
327
}
328

329
func (s *storage) getExchangeTargets(oldTable *table, relations []RelationID) ([]RelationID, bool) {
43✔
330
        changed := false
43✔
331
        targets := make([]Entity, len(oldTable.columns))
43✔
332
        for i := range oldTable.columns {
149✔
333
                targets[i] = oldTable.columns[i].target
106✔
334
        }
106✔
335
        for _, rel := range relations {
87✔
336
                if !rel.target.IsZero() && !s.entityPool.Alive(rel.target) {
44✔
337
                        panic("can't make a dead entity a relation target")
×
338
                }
339
                index := oldTable.components[rel.component.id]
44✔
340
                if !oldTable.columns[index].isRelation {
44✔
341
                        panic(fmt.Sprintf("component %d is not a relation component", rel.component.id))
×
342
                }
343
                if rel.target == targets[index] {
44✔
344
                        continue
×
345
                }
346
                targets[index] = rel.target
44✔
347
                changed = true
44✔
348
        }
349
        if !changed {
43✔
350
                return nil, false
×
351
        }
×
352

353
        result := make([]RelationID, 0, len(oldTable.relationIDs))
43✔
354
        for i, e := range targets {
149✔
355
                if !oldTable.columns[i].isRelation {
167✔
356
                        continue
61✔
357
                }
358
                id := oldTable.ids[i]
45✔
359
                result = append(result, RelationID{component: id, target: e})
45✔
360
        }
361

362
        return result, true
43✔
363
}
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