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

mlange-42 / arche / 4839913040

29 Apr 2023 05:15PM CUT coverage: 100.0%. Remained the same
4839913040

Pull #237

github

GitHub
Merge a98c2f581 into 8b13271cc
Pull Request #237: Archetype size optimization

43 of 43 new or added lines in 2 files covered. (100.0%)

2954 of 2954 relevant lines covered (100.0%)

4221.48 hits per line

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

100.0
/ecs/archetype.go
1
package ecs
2

3
import (
4
        "math"
5
        "reflect"
6
        "unsafe"
7

8
        "github.com/mlange-42/arche/ecs/stats"
9
)
10

11
// layoutSize is the size of an archetype column layout in bytes.
12
var layoutSize = unsafe.Sizeof(layout{})
13

14
// archetypeNode is a node in the archetype graph.
15
type archetypeNode struct {
16
        mask              Mask       // Mask of the archetype
17
        Ids               []ID       // List of component IDs.
18
        archetype         *archetype // The archetype
19
        archetypes        map[Entity]*archetype
20
        TransitionAdd     idMap[*archetypeNode] // Mapping from component ID to add to the resulting archetype
21
        TransitionRemove  idMap[*archetypeNode] // Mapping from component ID to remove to the resulting archetype
22
        relation          int8
23
        zeroValue         []byte         // Used as source for setting storage to zero
24
        zeroPointer       unsafe.Pointer // Points to zeroValue for fast access
25
        capacityIncrement uint32         // Capacity increment
26
}
27

28
// Creates a new archetypeNode
29
func newArchetypeNode(mask Mask, relation int8, capacityIncrement int) archetypeNode {
1,226✔
30
        var arch map[Entity]*archetype
1,226✔
31
        if relation >= 0 {
1,232✔
32
                arch = map[Entity]*archetype{}
6✔
33
        }
6✔
34
        return archetypeNode{
1,226✔
35
                mask:              mask,
1,226✔
36
                archetypes:        arch,
1,226✔
37
                TransitionAdd:     newIDMap[*archetypeNode](),
1,226✔
38
                TransitionRemove:  newIDMap[*archetypeNode](),
1,226✔
39
                relation:          relation,
1,226✔
40
                capacityIncrement: uint32(capacityIncrement),
1,226✔
41
        }
1,226✔
42
}
43

44
func (a *archetypeNode) GetArchetype(id Entity) *archetype {
7,539✔
45
        if a.relation >= 0 {
12,561✔
46
                return a.archetypes[id]
5,022✔
47
        }
5,022✔
48
        return a.archetype
2,517✔
49
}
50

51
func (a *archetypeNode) SetArchetype(id Entity, arch *archetype) {
1,220✔
52
        if a.relation >= 0 {
1,253✔
53
                a.archetypes[id] = arch
33✔
54
        } else {
1,220✔
55
                a.archetype = arch
1,187✔
56
        }
1,187✔
57
}
58

59
// Helper for accessing data from an archetype
60
type archetypeAccess struct {
61
        Mask              Mask           // Archetype's mask
62
        basePointer       unsafe.Pointer // Pointer to the first component column layout.
63
        entityPointer     unsafe.Pointer // Pointer to the entity storage
64
        Relation          Entity
65
        RelationComponent int8
66
}
67

68
// Matches checks if the archetype matches the given mask.
69
func (a *archetype) Matches(f Filter) bool {
3,140✔
70
        return f.Matches(a.Mask, &a.Relation)
3,140✔
71
}
3,140✔
72

73
// GetEntity returns the entity at the given index
74
func (a *archetypeAccess) GetEntity(index uintptr) Entity {
32,321✔
75
        return *(*Entity)(unsafe.Add(a.entityPointer, entitySize*index))
32,321✔
76
}
32,321✔
77

78
// Get returns the component with the given ID at the given index
79
func (a *archetypeAccess) Get(index uintptr, id ID) unsafe.Pointer {
147,152✔
80
        return a.getLayout(id).Get(index)
147,152✔
81
}
147,152✔
82

83
// GetEntity returns the entity at the given index
84
func (a *archetypeAccess) GetRelation() Entity {
1✔
85
        return a.Relation
1✔
86
}
1✔
87

88
// HasComponent returns whether the archetype contains the given component ID
89
func (a *archetypeAccess) HasComponent(id ID) bool {
48,494✔
90
        return a.getLayout(id).pointer != nil
48,494✔
91
}
48,494✔
92

93
// GetLayout returns the column layout for a component.
94
func (a *archetypeAccess) getLayout(id ID) *layout {
242,472✔
95
        return (*layout)(unsafe.Add(a.basePointer, layoutSize*uintptr(id)))
242,472✔
96
}
242,472✔
97

98
// layout specification of a component column.
99
type layout struct {
100
        pointer  unsafe.Pointer // Pointer to the first element in the component column.
101
        itemSize uintptr        // Component/step size
102
}
103

104
// Get returns a pointer to the item at the given index.
105
func (l *layout) Get(index uintptr) unsafe.Pointer {
147,150✔
106
        if l.pointer == nil {
147,151✔
107
                return nil
1✔
108
        }
1✔
109
        return unsafe.Add(l.pointer, l.itemSize*index)
147,149✔
110
}
111

112
// archetype represents an ECS archetype
113
type archetype struct {
114
        archetypeAccess                 // Access helper, passed to queries.
115
        graphNode       *archetypeNode  // Node in the archetype graph.
116
        layouts         []layout        // Column layouts by ID.
117
        indices         idMap[uint32]   // Mapping from IDs to buffer indices.
118
        buffers         []reflect.Value // Reflection arrays containing component data.
119
        entityBuffer    reflect.Value   // Reflection array containing entity data.
120
        len             uint32          // Current number of entities
121
        cap             uint32          // Current capacity
122
}
123

124
// Init initializes an archetype
125
func (a *archetype) Init(node *archetypeNode, forStorage bool, relation Entity, relationComp int8, components ...componentType) {
1,229✔
126
        var mask Mask
1,229✔
127
        if len(components) > 0 && len(node.Ids) == 0 {
2,348✔
128
                node.Ids = make([]ID, len(components))
1,119✔
129

1,119✔
130
                var maxSize uintptr = 0
1,119✔
131
                for i, c := range components {
6,392✔
132
                        node.Ids[i] = c.ID
5,273✔
133
                        size, align := c.Type.Size(), uintptr(c.Type.Align())
5,273✔
134
                        size = (size + (align - 1)) / align * align
5,273✔
135
                        if size > maxSize {
6,391✔
136
                                maxSize = size
1,118✔
137
                        }
1,118✔
138
                }
139

140
                if maxSize > 0 {
2,235✔
141
                        node.zeroValue = make([]byte, maxSize)
1,116✔
142
                        node.zeroPointer = unsafe.Pointer(&node.zeroValue[0])
1,116✔
143
                }
1,116✔
144
        }
145

146
        a.buffers = make([]reflect.Value, len(components))
1,229✔
147
        a.layouts = make([]layout, MaskTotalBits)
1,229✔
148
        a.indices = newIDMap[uint32]()
1,229✔
149

1,229✔
150
        cap := 1
1,229✔
151
        if forStorage {
2,374✔
152
                cap = int(node.capacityIncrement)
1,145✔
153
        }
1,145✔
154

155
        prev := -1
1,229✔
156
        for i, c := range components {
6,535✔
157
                if int(c.ID) <= prev {
5,307✔
158
                        panic("component arguments must be sorted by ID")
1✔
159
                }
160
                prev = int(c.ID)
5,305✔
161
                mask.Set(c.ID, true)
5,305✔
162

5,305✔
163
                size, align := c.Type.Size(), uintptr(c.Type.Align())
5,305✔
164
                size = (size + (align - 1)) / align * align
5,305✔
165

5,305✔
166
                a.buffers[i] = reflect.New(reflect.ArrayOf(cap, c.Type)).Elem()
5,305✔
167
                a.layouts[c.ID] = layout{
5,305✔
168
                        a.buffers[i].Addr().UnsafePointer(),
5,305✔
169
                        size,
5,305✔
170
                }
5,305✔
171
                a.indices.Set(c.ID, uint32(i))
5,305✔
172
        }
173
        a.entityBuffer = reflect.New(reflect.ArrayOf(cap, entityType)).Elem()
1,228✔
174

1,228✔
175
        a.archetypeAccess = archetypeAccess{
1,228✔
176
                basePointer:       unsafe.Pointer(&a.layouts[0]),
1,228✔
177
                entityPointer:     a.entityBuffer.Addr().UnsafePointer(),
1,228✔
178
                Mask:              mask,
1,228✔
179
                Relation:          relation,
1,228✔
180
                RelationComponent: relationComp,
1,228✔
181
        }
1,228✔
182

1,228✔
183
        a.graphNode = node
1,228✔
184

1,228✔
185
        a.len = 0
1,228✔
186
        a.cap = uint32(cap)
1,228✔
187
}
188

189
// Add adds an entity with optionally zeroed components to the archetype
190
func (a *archetype) Alloc(entity Entity) uintptr {
9,703✔
191
        idx := uintptr(a.len)
9,703✔
192
        a.extend(1)
9,703✔
193
        a.addEntity(idx, &entity)
9,703✔
194
        a.len++
9,703✔
195
        return idx
9,703✔
196
}
9,703✔
197

198
// Add adds storage to the archetype
199
func (a *archetype) AllocN(count uint32) {
28✔
200
        a.extend(count)
28✔
201
        a.len += count
28✔
202
}
28✔
203

204
// Add adds an entity with components to the archetype
205
func (a *archetype) Add(entity Entity, components ...Component) uintptr {
9✔
206
        if len(components) != len(a.graphNode.Ids) {
10✔
207
                panic("Invalid number of components")
1✔
208
        }
209
        idx := uintptr(a.len)
8✔
210

8✔
211
        a.extend(1)
8✔
212
        a.addEntity(idx, &entity)
8✔
213
        for _, c := range components {
24✔
214
                lay := a.getLayout(c.ID)
16✔
215
                size := lay.itemSize
16✔
216
                if size == 0 {
18✔
217
                        continue
2✔
218
                }
219
                src := reflect.ValueOf(c.Comp).UnsafePointer()
14✔
220
                dst := a.Get(uintptr(idx), c.ID)
14✔
221
                a.copy(src, dst, size)
14✔
222
        }
223
        a.len++
8✔
224
        return idx
8✔
225
}
226

227
// Remove removes an entity and its components from the archetype.
228
//
229
// Performs a swap-remove and reports whether a swap was necessary
230
// (i.e. not the last entity that was removed).
231
func (a *archetype) Remove(index uintptr) bool {
4,915✔
232
        swapped := a.removeEntity(index)
4,915✔
233

4,915✔
234
        old := uintptr(a.len - 1)
4,915✔
235

4,915✔
236
        if index != old {
5,083✔
237
                for _, id := range a.graphNode.Ids {
379✔
238
                        lay := a.getLayout(id)
211✔
239
                        size := lay.itemSize
211✔
240
                        if size == 0 {
214✔
241
                                continue
3✔
242
                        }
243
                        src := unsafe.Add(lay.pointer, old*size)
208✔
244
                        dst := unsafe.Add(lay.pointer, index*size)
208✔
245
                        a.copy(src, dst, size)
208✔
246
                }
247
        }
248
        a.ZeroAll(old)
4,915✔
249
        a.len--
4,915✔
250

4,915✔
251
        return swapped
4,915✔
252
}
253

254
// ZeroAll resets a block of storage in all buffers.
255
func (a *archetype) ZeroAll(index uintptr) {
4,915✔
256
        for _, id := range a.graphNode.Ids {
7,777✔
257
                a.Zero(index, id)
2,862✔
258
        }
2,862✔
259
}
260

261
// ZeroAll resets a block of storage in one buffer.
262
func (a *archetype) Zero(index uintptr, id ID) {
2,862✔
263
        lay := a.getLayout(id)
2,862✔
264
        size := lay.itemSize
2,862✔
265
        if size == 0 {
5,378✔
266
                return
2,516✔
267
        }
2,516✔
268
        dst := unsafe.Add(lay.pointer, index*size)
346✔
269
        a.copy(a.graphNode.zeroPointer, dst, size)
346✔
270
}
271

272
// SetEntity overwrites an entity
273
func (a *archetype) SetEntity(index uintptr, entity Entity) {
71,342✔
274
        a.addEntity(index, &entity)
71,342✔
275
}
71,342✔
276

277
// Set overwrites a component with the data behind the given pointer
278
func (a *archetype) Set(index uintptr, id ID, comp interface{}) unsafe.Pointer {
41,150✔
279
        lay := a.getLayout(id)
41,150✔
280
        dst := a.Get(index, id)
41,150✔
281
        size := lay.itemSize
41,150✔
282
        if size == 0 {
41,151✔
283
                return dst
1✔
284
        }
1✔
285
        rValue := reflect.ValueOf(comp)
41,149✔
286

41,149✔
287
        src := rValue.UnsafePointer()
41,149✔
288
        a.copy(src, dst, size)
41,149✔
289
        return dst
41,149✔
290
}
291

292
// SetPointer overwrites a component with the data behind the given pointer
293
func (a *archetype) SetPointer(index uintptr, id ID, comp unsafe.Pointer) unsafe.Pointer {
2,525✔
294
        lay := a.getLayout(id)
2,525✔
295
        dst := a.Get(index, id)
2,525✔
296
        size := lay.itemSize
2,525✔
297
        if size == 0 {
5,037✔
298
                return dst
2,512✔
299
        }
2,512✔
300

301
        a.copy(comp, dst, size)
13✔
302
        return dst
13✔
303
}
304

305
// Reset removes all entities and components.
306
//
307
// Does NOT free the reserved memory.
308
func (a *archetype) Reset() {
15✔
309
        a.len = 0
15✔
310
        for _, buf := range a.buffers {
31✔
311
                buf.SetZero()
16✔
312
        }
16✔
313
}
314

315
// Components returns the component IDs for this archetype
316
func (a *archetype) Components() []ID {
2,189✔
317
        return a.graphNode.Ids
2,189✔
318
}
2,189✔
319

320
// Len reports the number of entities in the archetype
321
func (a *archetype) Len() uint32 {
2,501✔
322
        return a.len
2,501✔
323
}
2,501✔
324

325
// Cap reports the current capacity of the archetype
326
func (a *archetype) Cap() uint32 {
18✔
327
        return a.cap
18✔
328
}
18✔
329

330
// Stats generates statistics for an archetype
331
func (a *archetype) Stats(reg *componentRegistry[ID]) stats.ArchetypeStats {
5✔
332
        ids := a.Components()
5✔
333
        aCompCount := len(ids)
5✔
334
        aTypes := make([]reflect.Type, aCompCount)
5✔
335
        for j, id := range ids {
9✔
336
                aTypes[j], _ = reg.ComponentType(id)
4✔
337
        }
4✔
338

339
        cap := int(a.Cap())
5✔
340
        memPerEntity := 0
5✔
341
        for _, id := range a.graphNode.Ids {
9✔
342
                lay := a.getLayout(id)
4✔
343
                memPerEntity += int(lay.itemSize)
4✔
344
        }
4✔
345
        memory := cap * (int(entitySize) + memPerEntity)
5✔
346

5✔
347
        return stats.ArchetypeStats{
5✔
348
                Size:            int(a.Len()),
5✔
349
                Capacity:        cap,
5✔
350
                Components:      aCompCount,
5✔
351
                ComponentIDs:    ids,
5✔
352
                ComponentTypes:  aTypes,
5✔
353
                Memory:          memory,
5✔
354
                MemoryPerEntity: memPerEntity,
5✔
355
        }
5✔
356
}
357

358
// UpdateStats updates statistics for an archetype
359
func (a *archetype) UpdateStats(stats *stats.ArchetypeStats) {
3✔
360
        cap := int(a.Cap())
3✔
361
        memory := cap * (int(entitySize) + stats.MemoryPerEntity)
3✔
362

3✔
363
        stats.Size = int(a.Len())
3✔
364
        stats.Capacity = cap
3✔
365
        stats.Memory = memory
3✔
366
}
3✔
367

368
// copy from one pointer to another.
369
func (a *archetype) copy(src, dst unsafe.Pointer, itemSize uintptr) {
122,951✔
370
        dstSlice := (*[math.MaxInt32]byte)(dst)[:itemSize:itemSize]
122,951✔
371
        srcSlice := (*[math.MaxInt32]byte)(src)[:itemSize:itemSize]
122,951✔
372
        copy(dstSlice, srcSlice)
122,951✔
373
}
122,951✔
374

375
// extend the memory buffers if necessary for adding an entity.
376
func (a *archetype) extend(by uint32) {
9,742✔
377
        required := a.len + by
9,742✔
378
        if a.cap >= required {
19,440✔
379
                return
9,698✔
380
        }
9,698✔
381
        a.cap = capacityU32(required, a.graphNode.capacityIncrement)
44✔
382

44✔
383
        old := a.entityBuffer
44✔
384
        a.entityBuffer = reflect.New(reflect.ArrayOf(int(a.cap), entityType)).Elem()
44✔
385
        a.entityPointer = a.entityBuffer.Addr().UnsafePointer()
44✔
386
        reflect.Copy(a.entityBuffer, old)
44✔
387

44✔
388
        for _, id := range a.graphNode.Ids {
102✔
389
                lay := a.getLayout(id)
58✔
390
                if lay.itemSize == 0 {
59✔
391
                        continue
1✔
392
                }
393
                index, _ := a.indices.Get(id)
57✔
394
                old := a.buffers[index]
57✔
395
                a.buffers[index] = reflect.New(reflect.ArrayOf(int(a.cap), old.Type().Elem())).Elem()
57✔
396
                lay.pointer = a.buffers[index].Addr().UnsafePointer()
57✔
397
                reflect.Copy(a.buffers[index], old)
57✔
398
        }
399
}
400

401
// Adds an entity at the given index. Does not extend the entity buffer.
402
func (a *archetype) addEntity(index uintptr, entity *Entity) {
81,053✔
403
        dst := unsafe.Add(a.entityPointer, entitySize*index)
81,053✔
404
        src := unsafe.Pointer(entity)
81,053✔
405
        a.copy(src, dst, entitySize)
81,053✔
406
}
81,053✔
407

408
// removeEntity removes an entity from tne archetype.
409
// Components need to be removed separately.
410
func (a *archetype) removeEntity(index uintptr) bool {
4,915✔
411
        old := uintptr(a.len - 1)
4,915✔
412

4,915✔
413
        if index == old {
9,662✔
414
                return false
4,747✔
415
        }
4,747✔
416

417
        src := unsafe.Add(a.entityPointer, old*entitySize)
168✔
418
        dst := unsafe.Add(a.entityPointer, index*entitySize)
168✔
419
        a.copy(src, dst, entitySize)
168✔
420

168✔
421
        return true
168✔
422
}
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