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

mlange-42 / arche / 7515952855

14 Jan 2024 01:00AM CUT coverage: 100.0%. Remained the same
7515952855

push

github

web-flow
Reduce archetype size by storing layouts in a growing slice (#327)

Use a growing slice to hold archetype mem layouts, instead of a fixes-size array.

Reduces archetype size from 4 kB to 88 B, plus the heap-allocated slice. The slice starts with size 16 (i.e. 16 component types) and grows in steps of 16 up to 256.

# Commits

* reduce archetype size by storing components in a growing slice
* re-arrange test functions
* document undefined behavior for IDs out of range, update changelog
* move non-zero capacity calculation to a separate function
* check component registry limit vs. int, not uint8
* move arche-serde up in the tools list in README

4871 of 4871 relevant lines covered (100.0%)

65485.4 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
const layoutChunkSize uint8 = 16
12

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

16
// Helper for accessing data from an archetype
17
type archetypeAccess struct {
18
        Mask              Mask           // Archetype's mask
19
        basePointer       unsafe.Pointer // Pointer to the first component column layout.
20
        entityPointer     unsafe.Pointer // Pointer to the entity storage
21
        RelationTarget    Entity
22
        RelationComponent int8
23
}
24

25
// GetEntity returns the entity at the given index
26
func (a *archetypeAccess) GetEntity(index uint32) Entity {
2,536,137✔
27
        return *(*Entity)(unsafe.Add(a.entityPointer, entitySize*index))
2,536,137✔
28
}
2,536,137✔
29

30
// Get returns the component with the given ID at the given index
31
func (a *archetypeAccess) Get(index uint32, id ID) unsafe.Pointer {
26,643✔
32
        return a.getLayout(id).Get(index)
26,643✔
33
}
26,643✔
34

35
// HasComponent returns whether the archetype contains the given component ID.
36
func (a *archetypeAccess) HasComponent(id ID) bool {
1,773✔
37
        return a.getLayout(id).pointer != nil
1,773✔
38
}
1,773✔
39

40
// HasRelation returns whether the archetype has a relation component.
41
func (a *archetypeAccess) HasRelation() bool {
28,861✔
42
        return a.RelationComponent >= 0
28,861✔
43
}
28,861✔
44

45
// GetLayout returns the column layout for a component.
46
func (a *archetypeAccess) getLayout(id ID) *layout {
90,685✔
47
        return (*layout)(unsafe.Add(a.basePointer, layoutSize*uint32(id)))
90,685✔
48
}
90,685✔
49

50
// layout specification of a component column.
51
type layout struct {
52
        pointer  unsafe.Pointer // Pointer to the first element in the component column.
53
        itemSize uint32         // Component/step size
54
}
55

56
// Get returns a pointer to the item at the given index.
57
func (l *layout) Get(index uint32) unsafe.Pointer {
26,641✔
58
        if l.pointer == nil {
26,642✔
59
                return nil
1✔
60
        }
1✔
61
        return unsafe.Add(l.pointer, l.itemSize*index)
26,640✔
62
}
63

64
// archetype represents an ECS archetype
65
type archetype struct {
66
        archetypeAccess // Access helper, passed to queries.
67
        *archetypeData
68
        node *archNode // Node in the archetype graph.
69
        len  uint32    // Current number of entities
70
        cap  uint32    // Current capacity
71
}
72

73
type archetypeData struct {
74
        layouts      []layout        // Column layouts by ID.
75
        indices      idMap[uint32]   // Mapping from IDs to buffer indices.
76
        buffers      []reflect.Value // Reflection arrays containing component data.
77
        entityBuffer reflect.Value   // Reflection array containing entity data.
78
        index        int32           // Index of the archetype in the world.
79
}
80

81
// Init initializes an archetype
82
func (a *archetype) Init(node *archNode, data *archetypeData, index int32, forStorage bool, layouts uint8, relation Entity) {
1,463✔
83
        if !node.IsActive {
2,768✔
84
                node.IsActive = true
1,305✔
85
        }
1,305✔
86

87
        a.archetypeData = data
1,463✔
88
        a.buffers = make([]reflect.Value, len(node.Ids))
1,463✔
89
        a.indices = newIDMap[uint32]()
1,463✔
90
        a.index = index
1,463✔
91
        a.layouts = make([]layout, layouts)
1,463✔
92

1,463✔
93
        cap := 1
1,463✔
94
        if forStorage {
2,798✔
95
                cap = int(node.capacityIncrement)
1,335✔
96
        }
1,335✔
97

98
        for i, id := range node.Ids {
7,005✔
99
                tp := node.Types[i]
5,542✔
100
                size, align := tp.Size(), uintptr(tp.Align())
5,542✔
101
                size = (size + (align - 1)) / align * align
5,542✔
102

5,542✔
103
                a.buffers[i] = reflect.New(reflect.ArrayOf(cap, tp)).Elem()
5,542✔
104
                a.layouts[id] = layout{
5,542✔
105
                        a.buffers[i].Addr().UnsafePointer(),
5,542✔
106
                        uint32(size),
5,542✔
107
                }
5,542✔
108
                a.indices.Set(id, uint32(i))
5,542✔
109
        }
5,542✔
110
        a.entityBuffer = reflect.New(reflect.ArrayOf(cap, entityType)).Elem()
1,463✔
111

1,463✔
112
        a.archetypeAccess = archetypeAccess{
1,463✔
113
                basePointer:       unsafe.Pointer(&a.layouts[0]),
1,463✔
114
                entityPointer:     a.entityBuffer.Addr().UnsafePointer(),
1,463✔
115
                Mask:              node.Mask,
1,463✔
116
                RelationTarget:    relation,
1,463✔
117
                RelationComponent: node.Relation,
1,463✔
118
        }
1,463✔
119

1,463✔
120
        a.node = node
1,463✔
121

1,463✔
122
        a.len = 0
1,463✔
123
        a.cap = uint32(cap)
1,463✔
124
}
125

126
// Add adds an entity with optionally zeroed components to the archetype
127
func (a *archetype) Alloc(entity Entity) uint32 {
35,443✔
128
        idx := a.len
35,443✔
129
        a.extend(1)
35,443✔
130
        a.addEntity(idx, &entity)
35,443✔
131
        a.len++
35,443✔
132
        return idx
35,443✔
133
}
35,443✔
134

135
// AllocN allocates storage for the given number of entities.
136
func (a *archetype) AllocN(count uint32) {
27,610✔
137
        a.extend(count)
27,610✔
138
        a.len += count
27,610✔
139
}
27,610✔
140

141
// Add adds an entity with components to the archetype.
142
func (a *archetype) Add(entity Entity, components ...Component) uint32 {
9✔
143
        if len(components) != len(a.node.Ids) {
10✔
144
                panic("Invalid number of components")
1✔
145
        }
146
        idx := a.len
8✔
147

8✔
148
        a.extend(1)
8✔
149
        a.addEntity(idx, &entity)
8✔
150
        for _, c := range components {
24✔
151
                lay := a.getLayout(c.ID)
16✔
152
                size := lay.itemSize
16✔
153
                if size == 0 {
18✔
154
                        continue
2✔
155
                }
156
                src := reflect.ValueOf(c.Comp).UnsafePointer()
14✔
157
                dst := a.Get(idx, c.ID)
14✔
158
                a.copy(src, dst, size)
14✔
159
        }
160
        a.len++
8✔
161
        return idx
8✔
162
}
163

164
// Remove removes an entity and its components from the archetype.
165
//
166
// Performs a swap-remove and reports whether a swap was necessary
167
// (i.e. not the last entity that was removed).
168
func (a *archetype) Remove(index uint32) bool {
30,564✔
169
        swapped := a.removeEntity(index)
30,564✔
170

30,564✔
171
        old := a.len - 1
30,564✔
172

30,564✔
173
        if index != old {
55,796✔
174
                for _, id := range a.node.Ids {
50,610✔
175
                        lay := a.getLayout(id)
25,378✔
176
                        size := lay.itemSize
25,378✔
177
                        if size == 0 {
25,687✔
178
                                continue
309✔
179
                        }
180
                        src := unsafe.Add(lay.pointer, old*size)
25,069✔
181
                        dst := unsafe.Add(lay.pointer, index*size)
25,069✔
182
                        a.copy(src, dst, size)
25,069✔
183
                }
184
        }
185
        a.ZeroAll(old)
30,564✔
186
        a.len--
30,564✔
187

30,564✔
188
        return swapped
30,564✔
189
}
190

191
// ZeroAll resets a block of storage in all buffers.
192
func (a *archetype) ZeroAll(index uint32) {
30,564✔
193
        for _, id := range a.node.Ids {
59,275✔
194
                a.Zero(index, id)
28,711✔
195
        }
28,711✔
196
}
197

198
// ZeroAll resets a block of storage in one buffer.
199
func (a *archetype) Zero(index uint32, id ID) {
28,711✔
200
        lay := a.getLayout(id)
28,711✔
201
        size := lay.itemSize
28,711✔
202
        if size == 0 {
31,857✔
203
                return
3,146✔
204
        }
3,146✔
205
        dst := unsafe.Add(lay.pointer, index*size)
25,565✔
206
        a.copy(a.node.zeroPointer, dst, size)
25,565✔
207
}
208

209
// SetEntity overwrites an entity
210
func (a *archetype) SetEntity(index uint32, entity Entity) {
2,760,838✔
211
        a.addEntity(index, &entity)
2,760,838✔
212
}
2,760,838✔
213

214
// Set overwrites a component with the data behind the given pointer
215
func (a *archetype) Set(index uint32, id ID, comp interface{}) unsafe.Pointer {
837✔
216
        lay := a.getLayout(id)
837✔
217
        dst := a.Get(index, id)
837✔
218
        size := lay.itemSize
837✔
219
        if size == 0 {
867✔
220
                return dst
30✔
221
        }
30✔
222
        rValue := reflect.ValueOf(comp)
807✔
223

807✔
224
        src := rValue.UnsafePointer()
807✔
225
        a.copy(src, dst, size)
807✔
226
        return dst
807✔
227
}
228

229
// SetPointer overwrites a component with the data behind the given pointer
230
func (a *archetype) SetPointer(index uint32, id ID, comp unsafe.Pointer) unsafe.Pointer {
7,161✔
231
        lay := a.getLayout(id)
7,161✔
232
        dst := a.Get(index, id)
7,161✔
233
        size := lay.itemSize
7,161✔
234
        if size == 0 {
12,097✔
235
                return dst
4,936✔
236
        }
4,936✔
237

238
        a.copy(comp, dst, size)
2,225✔
239
        return dst
2,225✔
240
}
241

242
// Reset removes all entities and components.
243
//
244
// Does NOT free the reserved memory.
245
func (a *archetype) Reset() {
52,541✔
246
        if a.len == 0 {
77,598✔
247
                return
25,057✔
248
        }
25,057✔
249
        a.len = 0
27,484✔
250
        for _, buf := range a.buffers {
54,989✔
251
                buf.SetZero()
27,505✔
252
        }
27,505✔
253
}
254

255
// Deactivate the archetype for later re-use.
256
func (a *archetype) Deactivate() {
27,427✔
257
        a.Reset()
27,427✔
258
        a.index = -1
27,427✔
259
}
27,427✔
260

261
// Activate reactivates a de-activated archetype.
262
func (a *archetype) Activate(target Entity, index int32) {
27,404✔
263
        a.index = index
27,404✔
264
        a.RelationTarget = target
27,404✔
265
}
27,404✔
266

267
func (a *archetype) ExtendLayouts(count uint8) {
6✔
268
        if len(a.layouts) >= int(count) {
8✔
269
                return
2✔
270
        }
2✔
271
        temp := a.layouts
4✔
272
        a.layouts = make([]layout, count)
4✔
273
        copy(a.layouts, temp)
4✔
274
        a.archetypeAccess.basePointer = unsafe.Pointer(&a.layouts[0])
4✔
275
}
276

277
// IsActive returns whether the archetype is active.
278
// Otherwise, it is eligible for re-use.
279
func (a *archetype) IsActive() bool {
5,102,544✔
280
        return a.index >= 0
5,102,544✔
281
}
5,102,544✔
282

283
// Components returns the component IDs for this archetype
284
func (a *archetype) Components() []ID {
2,969✔
285
        return a.node.Ids
2,969✔
286
}
2,969✔
287

288
// Len reports the number of entities in the archetype
289
func (a *archetype) Len() uint32 {
5,261,810✔
290
        return a.len
5,261,810✔
291
}
5,261,810✔
292

293
// Cap reports the current capacity of the archetype
294
func (a *archetype) Cap() uint32 {
5,100,047✔
295
        return a.cap
5,100,047✔
296
}
5,100,047✔
297

298
// Stats generates statistics for an archetype
299
func (a *archetype) Stats(reg *componentRegistry[ID]) stats.ArchetypeStats {
113✔
300
        ids := a.Components()
113✔
301
        aCompCount := len(ids)
113✔
302
        aTypes := make([]reflect.Type, aCompCount)
113✔
303
        for j, id := range ids {
225✔
304
                aTypes[j], _ = reg.ComponentType(id)
112✔
305
        }
112✔
306

307
        cap := int(a.Cap())
113✔
308
        memPerEntity := 0
113✔
309
        for _, id := range a.node.Ids {
225✔
310
                lay := a.getLayout(id)
112✔
311
                memPerEntity += int(lay.itemSize)
112✔
312
        }
112✔
313
        memory := cap * (int(entitySize) + memPerEntity)
113✔
314

113✔
315
        return stats.ArchetypeStats{
113✔
316
                IsActive: a.IsActive(),
113✔
317
                Size:     int(a.Len()),
113✔
318
                Capacity: cap,
113✔
319
                Memory:   memory,
113✔
320
        }
113✔
321
}
322

323
// UpdateStats updates statistics for an archetype
324
func (a *archetype) UpdateStats(node *stats.NodeStats, stats *stats.ArchetypeStats, reg *componentRegistry[ID]) {
5,099,924✔
325
        cap := int(a.Cap())
5,099,924✔
326
        memory := cap * (int(entitySize) + node.MemoryPerEntity)
5,099,924✔
327

5,099,924✔
328
        stats.IsActive = a.IsActive()
5,099,924✔
329
        stats.Size = int(a.Len())
5,099,924✔
330
        stats.Capacity = cap
5,099,924✔
331
        stats.Memory = memory
5,099,924✔
332
}
5,099,924✔
333

334
// copy from one pointer to another.
335
func (a *archetype) copy(src, dst unsafe.Pointer, itemSize uint32) {
2,875,201✔
336
        dstSlice := (*[math.MaxInt32]byte)(dst)[:itemSize:itemSize]
2,875,201✔
337
        srcSlice := (*[math.MaxInt32]byte)(src)[:itemSize:itemSize]
2,875,201✔
338
        copy(dstSlice, srcSlice)
2,875,201✔
339
}
2,875,201✔
340

341
// extend the memory buffers if necessary for adding an entity.
342
func (a *archetype) extend(by uint32) {
63,064✔
343
        required := a.len + by
63,064✔
344
        if a.cap >= required {
126,076✔
345
                return
63,012✔
346
        }
63,012✔
347
        a.cap = capacityU32(required, a.node.capacityIncrement)
52✔
348

52✔
349
        old := a.entityBuffer
52✔
350
        a.entityBuffer = reflect.New(reflect.ArrayOf(int(a.cap), entityType)).Elem()
52✔
351
        a.entityPointer = a.entityBuffer.Addr().UnsafePointer()
52✔
352
        reflect.Copy(a.entityBuffer, old)
52✔
353

52✔
354
        for _, id := range a.node.Ids {
106✔
355
                lay := a.getLayout(id)
54✔
356
                if lay.itemSize == 0 {
60✔
357
                        continue
6✔
358
                }
359
                index, _ := a.indices.Get(id)
48✔
360
                old := a.buffers[index]
48✔
361
                a.buffers[index] = reflect.New(reflect.ArrayOf(int(a.cap), old.Type().Elem())).Elem()
48✔
362
                lay.pointer = a.buffers[index].Addr().UnsafePointer()
48✔
363
                reflect.Copy(a.buffers[index], old)
48✔
364
        }
365
}
366

367
// Adds an entity at the given index. Does not extend the entity buffer.
368
func (a *archetype) addEntity(index uint32, entity *Entity) {
2,796,289✔
369
        dst := unsafe.Add(a.entityPointer, entitySize*index)
2,796,289✔
370
        src := unsafe.Pointer(entity)
2,796,289✔
371
        a.copy(src, dst, entitySize)
2,796,289✔
372
}
2,796,289✔
373

374
// removeEntity removes an entity from tne archetype.
375
// Components need to be removed separately.
376
func (a *archetype) removeEntity(index uint32) bool {
30,564✔
377
        old := a.len - 1
30,564✔
378

30,564✔
379
        if index == old {
35,896✔
380
                return false
5,332✔
381
        }
5,332✔
382

383
        src := unsafe.Add(a.entityPointer, old*entitySize)
25,232✔
384
        dst := unsafe.Add(a.entityPointer, index*entitySize)
25,232✔
385
        a.copy(src, dst, entitySize)
25,232✔
386

25,232✔
387
        return true
25,232✔
388
}
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