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

mlange-42 / ark / 13596195716

28 Feb 2025 08:43PM CUT coverage: 96.745% (-0.1%) from 96.882%
13596195716

Pull #81

github

web-flow
Merge bc97e23ac into 9b17c8beb
Pull Request #81: Unsafe API

132 of 141 new or added lines in 6 files covered. (93.62%)

13 existing lines in 1 file now uncovered.

3626 of 3748 relevant lines covered (96.74%)

50383.37 hits per line

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

93.63
/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 {
107✔
26
        reg := newComponentRegistry()
107✔
27
        entities := make([]entityIndex, reservedEntities, capacity+reservedEntities)
107✔
28
        isTarget := make([]bool, reservedEntities, capacity+reservedEntities)
107✔
29
        // Reserved zero and wildcard entities
107✔
30
        for i := range reservedEntities {
321✔
31
                entities[i] = entityIndex{table: maxTableID, row: 0}
214✔
32
        }
214✔
33

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

50
func (s *storage) findOrCreateTable(oldTable *table, mask *Mask, relations []RelationID) *table {
507,747✔
51
        // TODO: use archetype graph
507,747✔
52
        var arch *archetype
507,747✔
53
        for i := range s.archetypes {
1,523,140✔
54
                if s.archetypes[i].mask.Equals(mask) {
1,523,003✔
55
                        arch = &s.archetypes[i]
507,610✔
56
                        break
507,610✔
57
                }
58
        }
59
        if arch == nil {
507,884✔
60
                arch = s.createArchetype(mask)
137✔
61
        }
137✔
62
        allRelation := appendNew(oldTable.relationIDs, relations...)
507,747✔
63
        table, ok := arch.GetTable(s, allRelation)
507,747✔
64
        if !ok {
507,906✔
65
                table = s.createTable(arch, allRelation)
159✔
66
        }
159✔
67
        return table
507,747✔
68
}
69

70
func (s *storage) AddComponent(id uint8) {
466✔
71
        if len(s.components) != int(id) {
466✔
UNCOV
72
                panic("components can only be added to a storage sequentially")
×
73
        }
74
        s.components = append(s.components, componentStorage{columns: make([]*column, len(s.tables))})
466✔
75
}
76

77
// RemoveEntity removes the given entity from the world.
78
func (s *storage) RemoveEntity(entity Entity) {
505,072✔
79
        if !s.entityPool.Alive(entity) {
505,072✔
UNCOV
80
                panic("can't remove a dead entity")
×
81
        }
82
        index := &s.entities[entity.id]
505,072✔
83
        table := &s.tables[index.table]
505,072✔
84

505,072✔
85
        swapped := table.Remove(index.row)
505,072✔
86

505,072✔
87
        s.entityPool.Recycle(entity)
505,072✔
88

505,072✔
89
        if swapped {
1,009,041✔
90
                swapEntity := table.GetEntity(uintptr(index.row))
503,969✔
91
                s.entities[swapEntity.id].row = index.row
503,969✔
92
        }
503,969✔
93
        index.table = maxTableID
505,072✔
94

505,072✔
95
        if s.isTarget[entity.id] {
505,073✔
96
                s.cleanupArchetypes(entity)
1✔
97
                s.isTarget[entity.id] = false
1✔
98
        }
1✔
99
}
100

101
func (s *storage) get(entity Entity, component ID) unsafe.Pointer {
4✔
102
        if !s.entityPool.Alive(entity) {
4✔
NEW
UNCOV
103
                panic("can't get component of a dead entity")
×
104
        }
105
        return s.getUnchecked(entity, component)
4✔
106
}
107

108
func (s *storage) getUnchecked(entity Entity, component ID) unsafe.Pointer {
5✔
109
        s.checkHasComponent(entity, component)
5✔
110
        index := s.entities[entity.id]
5✔
111
        return s.tables[index.table].Get(component, uintptr(index.row))
5✔
112
}
5✔
113

114
func (s *storage) has(entity Entity, component ID) bool {
15✔
115
        if !s.entityPool.Alive(entity) {
15✔
NEW
116
                panic("can't get component of a dead entity")
×
117
        }
118
        return s.hasUnchecked(entity, component)
15✔
119
}
120

121
func (s *storage) hasUnchecked(entity Entity, component ID) bool {
17✔
122
        s.checkHasComponent(entity, component)
17✔
123
        index := s.entities[entity.id]
17✔
124
        return s.tables[index.table].Has(component)
17✔
125
}
17✔
126

127
func (s *storage) getRelation(entity Entity, comp ID) Entity {
129✔
128
        if !s.entityPool.Alive(entity) {
129✔
129
                panic("can't get relation for a dead entity")
×
130
        }
131
        return s.getRelationUnchecked(entity, comp)
129✔
132
}
133

134
func (s *storage) getRelationUnchecked(entity Entity, comp ID) Entity {
133✔
135
        s.checkHasComponent(entity, comp)
133✔
136
        return s.tables[s.entities[entity.id].table].GetRelation(comp)
133✔
137
}
133✔
138

139
func (s *storage) registerTargets(relations []RelationID) {
507,790✔
140
        for _, rel := range relations {
508,234✔
141
                s.isTarget[rel.target.id] = true
444✔
142
        }
444✔
143
}
144

145
func (s *storage) createEntity(table tableID) (Entity, uint32) {
507,424✔
146
        entity := s.entityPool.Get()
507,424✔
147

507,424✔
148
        idx := s.tables[table].Add(entity)
507,424✔
149
        len := len(s.entities)
507,424✔
150
        if int(entity.id) == len {
512,030✔
151
                s.entities = append(s.entities, entityIndex{table: table, row: idx})
4,606✔
152
                s.isTarget = append(s.isTarget, false)
4,606✔
153
        } else {
507,424✔
154
                s.entities[entity.id] = entityIndex{table: table, row: idx}
502,818✔
155
        }
502,818✔
156
        return entity, idx
507,424✔
157
}
158

159
func (s *storage) createEntities(table *table, count int) {
16✔
160
        startIdx := table.Len()
16✔
161
        table.Alloc(uint32(count))
16✔
162

16✔
163
        len := len(s.entities)
16✔
164
        for i := range count {
400✔
165
                index := uint32(startIdx + i)
384✔
166
                entity := s.entityPool.Get()
384✔
167
                table.SetEntity(index, entity)
384✔
168

384✔
169
                if int(entity.id) == len {
768✔
170
                        s.entities = append(s.entities, entityIndex{table: table.id, row: index})
384✔
171
                        s.isTarget = append(s.isTarget, false)
384✔
172
                        len++
384✔
173
                } else {
384✔
UNCOV
174
                        s.entities[entity.id] = entityIndex{table: table.id, row: index}
×
UNCOV
175
                }
×
176
        }
177
}
178

179
func (s *storage) createArchetype(mask *Mask) *archetype {
137✔
180
        comps := mask.toTypes(&s.registry.registry)
137✔
181
        index := len(s.archetypes)
137✔
182
        s.archetypes = append(s.archetypes, newArchetype(archetypeID(index), mask, comps, nil, &s.registry))
137✔
183
        archetype := &s.archetypes[index]
137✔
184
        if archetype.HasRelations() {
163✔
185
                s.relationArchetypes = append(s.relationArchetypes, archetype.id)
26✔
186
        }
26✔
187
        return archetype
137✔
188
}
189

190
func (s *storage) createTable(archetype *archetype, relations []RelationID) *table {
172✔
191
        targets := make([]Entity, len(archetype.components))
172✔
192
        numRelations := uint8(0)
172✔
193
        for _, rel := range relations {
241✔
194
                idx := archetype.componentsMap[rel.component.id]
69✔
195
                targets[idx] = rel.target
69✔
196
                numRelations++
69✔
197
        }
69✔
198
        if numRelations != archetype.numRelations {
172✔
UNCOV
199
                panic("relations must be fully specified")
×
200
        }
201

202
        var newTableID tableID
172✔
203
        if id, ok := archetype.GetFreeTable(); ok {
173✔
204
                newTableID = id
1✔
205
                s.tables[newTableID].recycle(targets, relations)
1✔
206
        } else {
172✔
207
                newTableID = tableID(len(s.tables))
171✔
208
                s.tables = append(s.tables, newTable(
171✔
209
                        newTableID, archetype.id, s.initialCapacity, &s.registry,
171✔
210
                        archetype.components, archetype.componentsMap,
171✔
211
                        archetype.isRelation, targets, relations))
171✔
212
        }
171✔
213
        archetype.AddTable(&s.tables[newTableID])
172✔
214

172✔
215
        table := &s.tables[newTableID]
172✔
216
        for i := range s.components {
953✔
217
                id := ID{id: uint8(i)}
781✔
218
                comps := &s.components[i]
781✔
219
                if archetype.mask.Get(id) {
1,382✔
220
                        comps.columns = append(comps.columns, table.GetColumn(id))
601✔
221
                } else {
781✔
222
                        comps.columns = append(comps.columns, nil)
180✔
223
                }
180✔
224
        }
225
        return table
172✔
226
}
227

228
func (s *storage) getExchangeMask(mask *Mask, add []ID, rem []ID) {
615✔
229
        for _, comp := range rem {
1,594✔
230
                if !mask.Get(comp) {
979✔
UNCOV
231
                        panic(fmt.Sprintf("entity does not have a component of type %v, can't remove", s.registry.Types[comp.id]))
×
232
                }
233
                mask.Set(comp, false)
979✔
234
        }
235
        for _, comp := range add {
1,343✔
236
                if mask.Get(comp) {
728✔
UNCOV
237
                        panic(fmt.Sprintf("entity already has component of type %v, can't add", s.registry.Types[comp.id]))
×
238
                }
239
                mask.Set(comp, true)
728✔
240
        }
241
}
242

243
// Removes empty archetypes that have a target relation to the given entity.
244
func (s *storage) cleanupArchetypes(target Entity) {
1✔
245
        newRelations := []RelationID{}
1✔
246
        for _, arch := range s.relationArchetypes {
2✔
247
                archetype := &s.archetypes[arch]
1✔
248
                for _, t := range archetype.tables {
3✔
249
                        table := &s.tables[t]
2✔
250

2✔
251
                        foundTarget := false
2✔
252
                        for _, rel := range table.relationIDs {
4✔
253
                                if rel.target.id == target.id {
3✔
254
                                        newRelations = append(newRelations, RelationID{component: rel.component, target: Entity{}})
1✔
255
                                        foundTarget = true
1✔
256
                                }
1✔
257
                        }
258
                        if !foundTarget {
3✔
259
                                continue
1✔
260
                        }
261

262
                        allRelations := s.getExchangeTargetsUnchecked(table, newRelations)
1✔
263
                        newTable, ok := archetype.GetTable(s, allRelations)
1✔
264
                        if !ok {
2✔
265
                                newTable = s.createTable(archetype, newRelations)
1✔
266
                        }
1✔
267
                        s.moveEntities(table, newTable)
1✔
268
                        archetype.FreeTable(table.id)
1✔
269

1✔
270
                        newRelations = newRelations[:0]
1✔
271
                }
272
                archetype.RemoveTarget(target)
1✔
273
        }
274
}
275

276
// moveEntities moves all entities from src to dst.
277
func (s *storage) moveEntities(src, dst *table) {
1✔
278
        oldLen := dst.Len()
1✔
279
        dst.AddAll(src)
1✔
280

1✔
281
        newLen := dst.Len()
1✔
282
        newTable := dst.id
1✔
283
        for i := oldLen; i < newLen; i++ {
33✔
284
                entity := dst.GetEntity(uintptr(i))
32✔
285
                s.entities[entity.id] = entityIndex{table: newTable, row: uint32(i)}
32✔
286
        }
32✔
287
        src.Reset()
1✔
288
}
289

290
func (s *storage) getExchangeTargetsUnchecked(oldTable *table, relations []RelationID) []RelationID {
1✔
291
        targets := make([]Entity, len(oldTable.columns))
1✔
292
        for i := range oldTable.columns {
3✔
293
                targets[i] = oldTable.columns[i].target
2✔
294
        }
2✔
295
        for _, rel := range relations {
2✔
296
                index := oldTable.components[rel.component.id]
1✔
297
                if rel.target == targets[index] {
1✔
UNCOV
298
                        continue
×
299
                }
300
                targets[index] = rel.target
1✔
301
        }
302

303
        result := make([]RelationID, 0, len(oldTable.relationIDs))
1✔
304
        for i, e := range targets {
3✔
305
                if !oldTable.columns[i].isRelation {
3✔
306
                        continue
1✔
307
                }
308
                id := oldTable.ids[i]
1✔
309
                result = append(result, RelationID{component: id, target: e})
1✔
310
        }
311

312
        return result
1✔
313
}
314

315
func (s *storage) getExchangeTargets(oldTable *table, relations []RelationID) ([]RelationID, bool) {
43✔
316
        changed := false
43✔
317
        targets := make([]Entity, len(oldTable.columns))
43✔
318
        for i := range oldTable.columns {
149✔
319
                targets[i] = oldTable.columns[i].target
106✔
320
        }
106✔
321
        for _, rel := range relations {
87✔
322
                if !rel.target.IsZero() && !s.entityPool.Alive(rel.target) {
44✔
UNCOV
323
                        panic("can't make a dead entity a relation target")
×
324
                }
325
                index := oldTable.components[rel.component.id]
44✔
326
                if !oldTable.columns[index].isRelation {
44✔
327
                        panic(fmt.Sprintf("component %d is not a relation component", rel.component.id))
×
328
                }
329
                if rel.target == targets[index] {
44✔
UNCOV
330
                        continue
×
331
                }
332
                targets[index] = rel.target
44✔
333
                changed = true
44✔
334
        }
335
        if !changed {
43✔
UNCOV
336
                return nil, false
×
UNCOV
337
        }
×
338

339
        result := make([]RelationID, 0, len(oldTable.relationIDs))
43✔
340
        for i, e := range targets {
149✔
341
                if !oldTable.columns[i].isRelation {
167✔
342
                        continue
61✔
343
                }
344
                id := oldTable.ids[i]
45✔
345
                result = append(result, RelationID{component: id, target: e})
45✔
346
        }
347

348
        return result, true
43✔
349
}
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