Repository: mlange-42/arche
Pull request #184 (2023-03-29 09:10:11)

Bugfix: Archetype pointer persistence
Use slices instead of arrays inside paged archetype list to ensure pointer persistence.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4b2c77..24d58be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@
 ### Bugfixes
 
 * Archetype storage buffers are "zeroed" when removing entities, to allow GC on pointers and slices in components (#147)
+* Use slices instead of arrays inside paged archetype list to ensure pointer persistence (#184)
 
 ### Documentation
 
diff --git a/ecs/paged.go b/ecs/paged.go
index 3c8177a..6074653 100644
--- a/ecs/paged.go
+++ b/ecs/paged.go
@@ -2,19 +2,27 @@ package ecs
 
 const fixedPageSize = 32
 
-// pagedArr32 is a paged collection working with pages of length 32 arrays.
+// pagedSlice is a paged collection working with pages of length 32 slices.
+// It's primary purpose is pointer persistence, which is not given using simple slices.
 //
 // Implements [archetypes].
-type pagedArr32[T any] struct {
-	pages   [][fixedPageSize]T
-	len     int
-	lenLast int
+type pagedSlice[T any] struct {
+	pages    [][]T
+	len      int
+	lenLast  int
+	pageSize int
+}
+
+func newPagedSlice[T any](pageSize int) pagedSlice[T] {
+	return pagedSlice[T]{
+		pageSize: pageSize,
+	}
 }
 
 // Add adds a value to the paged array.
-func (p *pagedArr32[T]) Add(value T) {
+func (p *pagedSlice[T]) Add(value T) {
 	if p.len == 0 || p.lenLast == fixedPageSize {
-		p.pages = append(p.pages, [fixedPageSize]T{})
+		p.pages = append(p.pages, make([]T, fixedPageSize))
 		p.lenLast = 0
 	}
 	p.pages[len(p.pages)-1][p.lenLast] = value
@@ -23,12 +31,12 @@ func (p *pagedArr32[T]) Add(value T) {
 }
 
 // Get returns the value at the given index.
-func (p *pagedArr32[T]) Get(index int) *T {
+func (p *pagedSlice[T]) Get(index int) *T {
 	return &p.pages[index/fixedPageSize][index%fixedPageSize]
 }
 
 // Len returns the current number of items in the paged array.
-func (p *pagedArr32[T]) Len() int {
+func (p *pagedSlice[T]) Len() int {
 	return p.len
 }
 
@@ -36,7 +44,7 @@ func (p *pagedArr32[T]) Len() int {
 //
 // Implements [archetypes].
 type pagedPointerArr32[T any] struct {
-	pages   [][fixedPageSize]*T
+	pages   [][]*T
 	len     int
 	lenLast int
 }
@@ -44,7 +52,7 @@ type pagedPointerArr32[T any] struct {
 // Add adds a value to the paged array.
 func (p *pagedPointerArr32[T]) Add(value *T) {
 	if p.len == 0 || p.lenLast == fixedPageSize {
-		p.pages = append(p.pages, [fixedPageSize]*T{})
+		p.pages = append(p.pages, make([]*T, fixedPageSize))
 		p.lenLast = 0
 	}
 	p.pages[len(p.pages)-1][p.lenLast] = value
diff --git a/ecs/world.go b/ecs/world.go
index 5aae604..4425fa1 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -61,8 +61,8 @@ type World struct {
 	resources   Resources                 // World resources.
 	entities    []entityIndex             // Mapping from entities to archetype and index.
 	entityPool  entityPool                // Pool for entities.
-	archetypes  pagedArr32[archetype]     // The archetypes.
-	graph       pagedArr32[archetypeNode] // The archetype graph.
+	archetypes  pagedSlice[archetype]     // The archetypes.
+	graph       pagedSlice[archetypeNode] // The archetype graph.
 	locks       lockMask                  // World locks.
 	registry    componentRegistry[ID]     // Component registry.
 	filterCache Cache                     // Cache for registered filters.
@@ -94,8 +94,8 @@ func fromConfig(conf Config) World {
 		entities:    entities,
 		entityPool:  newEntityPool(conf.CapacityIncrement),
 		registry:    newComponentRegistry(),
-		archetypes:  pagedArr32[archetype]{},
-		graph:       pagedArr32[archetypeNode]{},
+		archetypes:  newPagedSlice[archetype](32),
+		graph:       newPagedSlice[archetypeNode](32),
 		locks:       lockMask{},
 		listener:    nil,
 		resources:   newResources(),


Test patch:
diff --git a/ecs/paged_test.go b/ecs/paged_test.go
index aa45244..122e74d 100644
--- a/ecs/paged_test.go
+++ b/ecs/paged_test.go
@@ -2,12 +2,13 @@ package ecs
 
 import (
 	"testing"
+	"unsafe"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestPagedArr32(t *testing.T) {
-	a := pagedArr32[int]{}
+	a := newPagedSlice[int](32)
 
 	for i := 0; i < 66; i++ {
 		a.Add(i)
@@ -15,3 +16,23 @@ func TestPagedArr32(t *testing.T) {
 		assert.Equal(t, i+1, a.Len())
 	}
 }
+
+func TestPagedArrPointerPersistence(t *testing.T) {
+	a := newPagedSlice[int](32)
+
+	a.Add(0)
+	p1 := a.Get(0)
+
+	for i := 1; i < 66; i++ {
+		a.Add(i)
+		assert.Equal(t, i, *a.Get(i))
+		assert.Equal(t, i+1, a.Len())
+	}
+
+	p2 := a.Get(0)
+
+	assert.Equal(t, unsafe.Pointer(p1), unsafe.Pointer(p2))
+
+	*p1 = 100
+	assert.Equal(t, 100, *p2)
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 729cfbd..de79282 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -909,7 +909,7 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSize[entityIndex]()
 	printTypeSize[Mask]()
 	printTypeSize[World]()
-	printTypeSizeName[pagedArr32[archetype]]("pagedArr32")
+	printTypeSizeName[pagedSlice[archetype]]("pagedArr32")
 	printTypeSize[archetype]()
 	printTypeSize[archetypeAccess]()
 	printTypeSize[archetypeNode]()