Repository: mlange-42/arche
Pull request #57 (2023-02-12 11:23:42)

Project structure


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index c8dff67..e0355e1 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -14,8 +14,8 @@ type archetype struct {
 	// Indirection to avoid a fixed-size array of storages
 	// Increases access time by 50-100%
 	indices    [maskTotalBits]uint8
-	entities   base.Storage
-	components []base.Storage
+	entities   storage
+	components []storage
 	toAdd      map[ID]*archetype
 	toRemove   map[ID]*archetype
 }
@@ -26,7 +26,7 @@ var entityType = reflect.TypeOf(Entity{})
 func (a *archetype) Init(capacityIncrement int, components ...base.ComponentType) {
 	var mask base.BitMask
 	a.Ids = make([]ID, len(components))
-	comps := make([]base.Storage, len(components))
+	comps := make([]storage, len(components))
 
 	prev := -1
 	for i, c := range components {
@@ -38,13 +38,13 @@ func (a *archetype) Init(capacityIncrement int, components ...base.ComponentType
 		mask.Set(c.ID, true)
 		a.Ids[i] = c.ID
 		a.indices[c.ID] = uint8(i)
-		comps[i] = base.Storage{}
+		comps[i] = storage{}
 		comps[i].Init(c.Type, capacityIncrement)
 	}
 
 	a.Mask = mask
 	a.components = comps
-	a.entities = base.Storage{}
+	a.entities = storage{}
 	a.toAdd = map[ID]*archetype{}
 	a.toRemove = map[ID]*archetype{}
 	a.entities.Init(entityType, capacityIncrement)
diff --git a/internal/base/bit_pool.go b/ecs/bit_pool.go
similarity index 60%
rename from internal/base/bit_pool.go
rename to ecs/bit_pool.go
index 3314075..046dfe0 100644
--- a/internal/base/bit_pool.go
+++ b/ecs/bit_pool.go
@@ -1,18 +1,18 @@
-package base
+package ecs
 
-// BitPool is an entityPool implementation using implicit linked lists.
+// bitPool is an entityPool implementation using implicit linked lists.
 // Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
-type BitPool struct {
-	bits      [MaskTotalBits]uint8
+type bitPool struct {
+	bits      [maskTotalBits]uint8
 	next      uint8
 	length    uint8
 	available uint8
 }
 
-// NewBitPool creates a new, initialized bit pool
-func NewBitPool() BitPool {
-	return BitPool{
-		bits:      [MaskTotalBits]uint8{},
+// newBitPool creates a new, initialized bit pool
+func newBitPool() bitPool {
+	return bitPool{
+		bits:      [maskTotalBits]uint8{},
 		next:      0,
 		length:    0,
 		available: 0,
@@ -20,9 +20,9 @@ func NewBitPool() BitPool {
 }
 
 // Get returns a fresh or recycled bit
-func (p *BitPool) Get() uint8 {
+func (p *bitPool) Get() uint8 {
 	if p.available == 0 {
-		if p.length >= MaskTotalBits {
+		if p.length >= maskTotalBits {
 			panic("run out of the maximum of 64 bits")
 		}
 		b := p.length
@@ -37,7 +37,7 @@ func (p *BitPool) Get() uint8 {
 }
 
 // Recycle hands a bit back for recycling
-func (p *BitPool) Recycle(b uint8) {
+func (p *bitPool) Recycle(b uint8) {
 	p.next, p.bits[b] = b, p.next
 	p.available++
 }
diff --git a/internal/base/paged.go b/ecs/paged.go
similarity index 61%
rename from internal/base/paged.go
rename to ecs/paged.go
index 7c0a0c5..648fbb6 100644
--- a/internal/base/paged.go
+++ b/ecs/paged.go
@@ -1,15 +1,15 @@
-package base
+package ecs
 
 const fixedPageSize = 32
 
-// PagedArr32 is a paged collection working with pages of length 32 arrays
-type PagedArr32[T any] struct {
+// pagedArr32 is a paged collection working with pages of length 32 arrays
+type pagedArr32[T any] struct {
 	pages   [][fixedPageSize]T
 	len     int
 	lenLast int
 }
 
-func (p *PagedArr32[T]) Add(value T) {
+func (p *pagedArr32[T]) Add(value T) {
 	if len(p.pages) == 0 || p.lenLast == fixedPageSize {
 		p.pages = append(p.pages, [fixedPageSize]T{})
 		p.lenLast = 0
@@ -19,10 +19,10 @@ func (p *PagedArr32[T]) Add(value T) {
 	p.lenLast++
 }
 
-func (p *PagedArr32[T]) Get(index int) *T {
+func (p *pagedArr32[T]) Get(index int) *T {
 	return &p.pages[index/fixedPageSize][index%fixedPageSize]
 }
 
-func (p *PagedArr32[T]) Len() int {
+func (p *pagedArr32[T]) Len() int {
 	return p.len
 }
diff --git a/ecs/query.go b/ecs/query.go
index a52a7b0..512a59c 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -3,10 +3,14 @@ package ecs
 import (
 	"unsafe"
 
-	f "github.com/mlange-42/arche/filter"
 	"github.com/mlange-42/arche/internal/base"
 )
 
+// MaskFilter is the interface for logic filters
+type MaskFilter interface {
+	Matches(mask base.BitMask) bool
+}
+
 // EntityIter is the interface for iterable queries
 type EntityIter interface {
 	// Next proceeds to the next [Entity] in the Query.
@@ -101,11 +105,11 @@ func (q *Query) Next() bool {
 //	}
 type Filter struct {
 	queryIter
-	filter f.MaskFilter
+	filter MaskFilter
 }
 
 // newFilter creates a new Filter
-func newFilter(world *World, filter f.MaskFilter, lockBit uint8) Filter {
+func newFilter(world *World, filter MaskFilter, lockBit uint8) Filter {
 	return Filter{
 		queryIter: queryIter{
 			world:   world,
diff --git a/ecs/registry.go b/ecs/registry.go
new file mode 100644
index 0000000..46b4e8f
--- /dev/null
+++ b/ecs/registry.go
@@ -0,0 +1,40 @@
+package ecs
+
+import "reflect"
+
+// componentRegistry keeps track of component IDs
+type componentRegistry struct {
+	Components map[reflect.Type]ID
+	Types      [maskTotalBits]reflect.Type
+}
+
+// newComponentRegistry creates a new ComponentRegistry
+func newComponentRegistry() componentRegistry {
+	return componentRegistry{
+		Components: map[reflect.Type]ID{},
+	}
+}
+
+// RegisterComponent registers a components and assigns an ID for it
+func (r *componentRegistry) RegisterComponent(tp reflect.Type, totalBits int) ID {
+	id := ID(len(r.Components))
+	if int(id) >= totalBits {
+		panic("maximum of 64 component types exceeded")
+	}
+	r.Components[tp] = id
+	r.Types[id] = tp
+	return id
+}
+
+// ComponentID returns the ID for a component type, and registers it if not already registered
+func (r *componentRegistry) ComponentID(tp reflect.Type) ID {
+	if id, ok := r.Components[tp]; ok {
+		return id
+	}
+	return r.RegisterComponent(tp, maskTotalBits)
+}
+
+// ComponentType returns the type of a component by ID
+func (r *componentRegistry) ComponentType(id ID) reflect.Type {
+	return r.Types[id]
+}
diff --git a/internal/base/storage.go b/ecs/storage.go
similarity index 80%
rename from internal/base/storage.go
rename to ecs/storage.go
index 78dcfd3..71d4bba 100644
--- a/internal/base/storage.go
+++ b/ecs/storage.go
@@ -1,4 +1,4 @@
-package base
+package ecs
 
 import (
 	"math"
@@ -6,8 +6,8 @@ import (
 	"unsafe"
 )
 
-// Storage is a Storage implementation that works with reflection
-type Storage struct {
+// storage is a storage implementation that works with reflection
+type storage struct {
 	buffer            reflect.Value
 	bufferAddress     unsafe.Pointer
 	typeOf            reflect.Type
@@ -18,7 +18,7 @@ type Storage struct {
 }
 
 // Init initializes a storage
-func (s *Storage) Init(tp reflect.Type, increment int) {
+func (s *storage) Init(tp reflect.Type, increment int) {
 	size := tp.Size()
 	align := uintptr(tp.Align())
 	size = (size + (align - 1)) / align * align
@@ -33,13 +33,13 @@ func (s *Storage) Init(tp reflect.Type, increment int) {
 }
 
 // Get retrieves an unsafe pointer to an element
-func (s *Storage) Get(index uint32) unsafe.Pointer {
+func (s *storage) Get(index uint32) unsafe.Pointer {
 	ptr := unsafe.Add(s.bufferAddress, uintptr(index)*s.itemSize)
 	return unsafe.Pointer(ptr)
 }
 
 // Add adds an element to the end of the storage
-func (s *Storage) Add(value interface{}) (index uint32) {
+func (s *storage) Add(value interface{}) (index uint32) {
 	s.extend()
 	s.len++
 	s.Set(s.len-1, value)
@@ -47,7 +47,7 @@ func (s *Storage) Add(value interface{}) (index uint32) {
 }
 
 // AddPointer adds an element to the end of the storage, based on a pointer
-func (s *Storage) AddPointer(value unsafe.Pointer) (index uint32) {
+func (s *storage) AddPointer(value unsafe.Pointer) (index uint32) {
 	s.extend()
 	s.len++
 	s.setPointer(s.len-1, value)
@@ -55,14 +55,14 @@ func (s *Storage) AddPointer(value unsafe.Pointer) (index uint32) {
 }
 
 // Alloc adds an empty element to the end of the storage
-func (s *Storage) Alloc() (index uint32) {
+func (s *storage) Alloc() (index uint32) {
 	s.extend()
 	s.len++
 	s.Zero(s.len - 1)
 	return s.len - 1
 }
 
-func (s *Storage) extend() {
+func (s *storage) extend() {
 	if s.cap < s.len+1 {
 		old := s.buffer
 		s.cap = s.cap + s.capacityIncrement
@@ -73,7 +73,7 @@ func (s *Storage) extend() {
 }
 
 // Remove swap-removes an element
-func (s *Storage) Remove(index uint32) bool {
+func (s *storage) Remove(index uint32) bool {
 	o := s.len - 1
 	n := index
 
@@ -98,7 +98,7 @@ func (s *Storage) Remove(index uint32) bool {
 }
 
 // Set sets the storage at the given index
-func (s *Storage) Set(index uint32, value interface{}) unsafe.Pointer {
+func (s *storage) Set(index uint32, value interface{}) unsafe.Pointer {
 	rValue := reflect.ValueOf(value)
 	dst := s.Get(index)
 	var src unsafe.Pointer
@@ -113,7 +113,7 @@ func (s *Storage) Set(index uint32, value interface{}) unsafe.Pointer {
 	return dst
 }
 
-func (s *Storage) setPointer(index uint32, value unsafe.Pointer) {
+func (s *storage) setPointer(index uint32, value unsafe.Pointer) {
 	dst := s.Get(index)
 	size := s.itemSize
 
@@ -124,7 +124,7 @@ func (s *Storage) setPointer(index uint32, value unsafe.Pointer) {
 }
 
 // Zero resets a block of storage
-func (s *Storage) Zero(index uint32) {
+func (s *storage) Zero(index uint32) {
 	dst := s.Get(index)
 
 	for i := uintptr(0); i < s.itemSize; i++ {
@@ -134,12 +134,12 @@ func (s *Storage) Zero(index uint32) {
 }
 
 // Len returns the number of items in the storage
-func (s *Storage) Len() uint32 {
+func (s *storage) Len() uint32 {
 	return s.len
 }
 
 // toSlice converts the content of a storage to a slice of structs
-func toSlice[T any](s Storage) []T {
+func toSlice[T any](s storage) []T {
 	res := make([]T, s.Len())
 	for i := 0; i < int(s.Len()); i++ {
 		ptr := (*T)(s.Get(uint32(i)))
diff --git a/ecs/types.go b/ecs/types.go
index aaf56c6..cbf5424 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -4,8 +4,8 @@ import (
 	"github.com/mlange-42/arche/internal/base"
 )
 
-// eid is the entity identifier/index type
-type eid = base.Eid
+// Eid is the entity identifier/index type
+type eid uint32
 
 // ID is the component identifier type
 type ID = base.ID
diff --git a/ecs/world.go b/ecs/world.go
index ce7a385..94c9fbd 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -4,7 +4,6 @@ import (
 	"reflect"
 	"unsafe"
 
-	f "github.com/mlange-42/arche/filter"
 	"github.com/mlange-42/arche/internal/base"
 )
 
@@ -18,10 +17,10 @@ func ComponentID[T any](w *World) ID {
 type World struct {
 	config     Config
 	entities   []entityIndex
-	archetypes base.PagedArr32[archetype]
+	archetypes pagedArr32[archetype]
 	entityPool entityPool
-	bitPool    base.BitPool
-	registry   base.ComponentRegistry
+	bitPool    bitPool
+	registry   componentRegistry
 	locks      bitMask
 }
 
@@ -34,14 +33,14 @@ func NewWorld() World {
 func FromConfig(conf Config) World {
 	arch := archetype{}
 	arch.Init(conf.CapacityIncrement)
-	arches := base.PagedArr32[archetype]{}
+	arches := pagedArr32[archetype]{}
 	arches.Add(arch)
 	return World{
 		config:     conf,
 		entities:   []entityIndex{{arch: nil, index: 0}},
 		entityPool: newEntityPool(conf.CapacityIncrement),
-		bitPool:    base.NewBitPool(),
-		registry:   base.NewComponentRegistry(),
+		bitPool:    newBitPool(),
+		registry:   newComponentRegistry(),
 		archetypes: arches,
 		locks:      bitMask(0),
 	}
@@ -108,7 +107,7 @@ func (w *World) Query(comps ...ID) Query {
 // Locks the world to prevent changes to component compositions.
 //
 // There is no generic alternative for filters.
-func (w *World) Filter(filter f.MaskFilter) Filter {
+func (w *World) Filter(filter MaskFilter) Filter {
 	lock := w.bitPool.Get()
 	w.locks.Set(ID(lock), true)
 	return newFilter(w, filter, lock)
@@ -371,7 +370,7 @@ func (w *World) nextArchetype(mask, exclude bitMask, index int) (int, archetypeI
 	return len, archetypeIter{}, false
 }
 
-func (w *World) nextArchetypeFilter(filter f.MaskFilter, index int) (int, archetypeIter, bool) {
+func (w *World) nextArchetypeFilter(filter MaskFilter, index int) (int, archetypeIter, bool) {
 	len := w.archetypes.Len()
 	if index >= len {
 		panic("exceeded end of query")
diff --git a/filter/filter.go b/filter/filter.go
index 6002faa..660aaec 100644
--- a/filter/filter.go
+++ b/filter/filter.go
@@ -1,17 +1,13 @@
 package filter
 
 import (
+	"github.com/mlange-42/arche/ecs"
 	"github.com/mlange-42/arche/internal/base"
 )
 
 // Mask is a mask for a combination of components.
 type Mask = base.Mask
 
-// MaskFilter is the interface for logic filters
-type MaskFilter interface {
-	Matches(mask base.BitMask) bool
-}
-
 // All matches all the given components.
 //
 // Like [And] for combining individual components.
@@ -28,12 +24,12 @@ func OneOf(compA base.ID, compB base.ID) *OR {
 
 // AND is a filter for ANDing together components
 type AND struct {
-	L MaskFilter
-	R MaskFilter
+	L ecs.MaskFilter
+	R ecs.MaskFilter
 }
 
 // And constructs a pointer to a AND filter
-func And(l, r MaskFilter) *AND {
+func And(l, r ecs.MaskFilter) *AND {
 	return &AND{L: l, R: r}
 }
 
@@ -44,12 +40,12 @@ func (f *AND) Matches(mask base.BitMask) bool {
 
 // OR is a filter for ORing together components
 type OR struct {
-	L MaskFilter
-	R MaskFilter
+	L ecs.MaskFilter
+	R ecs.MaskFilter
 }
 
 // Or constructs a pointer to a OR filter
-func Or(l, r MaskFilter) *OR {
+func Or(l, r ecs.MaskFilter) *OR {
 	return &OR{L: l, R: r}
 }
 
@@ -60,12 +56,12 @@ func (f *OR) Matches(mask base.BitMask) bool {
 
 // XOR is a filter for XORing together components
 type XOR struct {
-	L MaskFilter
-	R MaskFilter
+	L ecs.MaskFilter
+	R ecs.MaskFilter
 }
 
 // XOr constructs a pointer to a XOR filter
-func XOr(l, r MaskFilter) *XOR {
+func XOr(l, r ecs.MaskFilter) *XOR {
 	return &XOR{L: l, R: r}
 }
 
diff --git a/internal/base/bitmask.go b/internal/base/bitmask.go
index ea886dc..8e60720 100644
--- a/internal/base/bitmask.go
+++ b/internal/base/bitmask.go
@@ -1,5 +1,10 @@
 package base
 
+// MaskTotalBits is the size of Mask in bits.
+//
+// It is the maximum number of component types that may exist in any [World].
+const MaskTotalBits = 64
+
 // BitMask is a bitmask.
 type BitMask uint64
 
diff --git a/internal/base/registry.go b/internal/base/registry.go
deleted file mode 100644
index 0d27960..0000000
--- a/internal/base/registry.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package base
-
-import "reflect"
-
-// MaskTotalBits is the size of Mask in bits.
-//
-// It is the maximum number of component types that may exist in any [World].
-const MaskTotalBits = 64
-
-// ComponentRegistry keeps track of component IDs
-type ComponentRegistry struct {
-	Components map[reflect.Type]ID
-	Types      [MaskTotalBits]reflect.Type
-}
-
-// NewComponentRegistry creates a new ComponentRegistry
-func NewComponentRegistry() ComponentRegistry {
-	return ComponentRegistry{
-		Components: map[reflect.Type]ID{},
-	}
-}
-
-// RegisterComponent registers a components and assigns an ID for it
-func (r *ComponentRegistry) RegisterComponent(tp reflect.Type, totalBits int) ID {
-	id := ID(len(r.Components))
-	if int(id) >= totalBits {
-		panic("maximum of 64 component types exceeded")
-	}
-	r.Components[tp] = id
-	r.Types[id] = tp
-	return id
-}
-
-// ComponentID returns the ID for a component type, and registers it if not already registered
-func (r *ComponentRegistry) ComponentID(tp reflect.Type) ID {
-	if id, ok := r.Components[tp]; ok {
-		return id
-	}
-	return r.RegisterComponent(tp, MaskTotalBits)
-}
-
-// ComponentType returns the type of a component by ID
-func (r *ComponentRegistry) ComponentType(id ID) reflect.Type {
-	return r.Types[id]
-}
diff --git a/internal/base/types.go b/internal/base/types.go
index 6a0164f..7fba07e 100644
--- a/internal/base/types.go
+++ b/internal/base/types.go
@@ -5,9 +5,6 @@ import (
 	"unsafe"
 )
 
-// Eid is the entity identifier/index type
-type Eid uint32
-
 // ID is the component identifier type
 type ID uint8
 


Test patch:
diff --git a/internal/base/bit_pool_test.go b/ecs/bit_pool_test.go
similarity index 82%
rename from internal/base/bit_pool_test.go
rename to ecs/bit_pool_test.go
index 35df12e..b1b4dea 100644
--- a/internal/base/bit_pool_test.go
+++ b/ecs/bit_pool_test.go
@@ -1,4 +1,4 @@
-package base
+package ecs
 
 import (
 	"testing"
@@ -7,9 +7,9 @@ import (
 )
 
 func TestBitPool(t *testing.T) {
-	p := NewBitPool()
+	p := newBitPool()
 
-	for i := 0; i < MaskTotalBits; i++ {
+	for i := 0; i < maskTotalBits; i++ {
 		assert.Equal(t, i, int(p.Get()))
 	}
 
diff --git a/internal/base/paged_test.go b/ecs/paged_test.go
similarity index 84%
rename from internal/base/paged_test.go
rename to ecs/paged_test.go
index be895df..aa45244 100644
--- a/internal/base/paged_test.go
+++ b/ecs/paged_test.go
@@ -1,4 +1,4 @@
-package base
+package ecs
 
 import (
 	"testing"
@@ -7,7 +7,7 @@ import (
 )
 
 func TestPagedArr32(t *testing.T) {
-	a := PagedArr32[int]{}
+	a := pagedArr32[int]{}
 
 	for i := 0; i < 66; i++ {
 		a.Add(i)
diff --git a/ecs/query_test.go b/ecs/query_test.go
index 7e80a5d..ec7825d 100644
--- a/ecs/query_test.go
+++ b/ecs/query_test.go
@@ -3,7 +3,6 @@ package ecs
 import (
 	"testing"
 
-	"github.com/mlange-42/arche/filter"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -74,73 +73,6 @@ func TestQuery(t *testing.T) {
 	assert.Equal(t, 2, cnt)
 }
 
-func TestFilter(t *testing.T) {
-	w := NewWorld()
-
-	posID := ComponentID[position](&w)
-	rotID := ComponentID[rotation](&w)
-
-	e0 := w.NewEntity()
-	e1 := w.NewEntity()
-	e2 := w.NewEntity()
-	e3 := w.NewEntity()
-	e4 := w.NewEntity()
-
-	w.Add(e0, posID)
-	w.Add(e1, posID, rotID)
-	w.Add(e2, posID, rotID)
-	w.Add(e3, rotID)
-	w.Add(e4, rotID)
-
-	q := w.Filter(filter.All(posID, rotID))
-	cnt := 0
-	for q.Next() {
-		ent := q.Entity()
-		pos := (*position)(q.Get(posID))
-		rot := (*rotation)(q.Get(rotID))
-		_ = ent
-		_ = pos
-		_ = rot
-		cnt++
-	}
-	assert.Equal(t, 2, cnt)
-
-	q = w.Filter(filter.All(posID))
-	cnt = 0
-	for q.Next() {
-		ent := q.Entity()
-		pos := (*position)(q.Get(posID))
-		_ = ent
-		_ = pos
-		cnt++
-	}
-	assert.Equal(t, 3, cnt)
-
-	q = w.Filter(filter.All(rotID))
-	cnt = 0
-	for q.Next() {
-		ent := q.Entity()
-		rot := (*rotation)(q.Get(rotID))
-		_ = ent
-		_ = rot
-		hasPos := q.Has(posID)
-		_ = hasPos
-		cnt++
-	}
-	assert.Equal(t, 4, cnt)
-
-	assert.Panics(t, func() { q.Next() })
-
-	q = w.Filter(&filter.AND{L: filter.All(rotID), R: filter.NotANY(filter.All(posID))})
-
-	cnt = 0
-	for q.Next() {
-		_ = q.Entity()
-		cnt++
-	}
-	assert.Equal(t, 2, cnt)
-}
-
 func TestInterface(t *testing.T) {
 	w := NewWorld()
 
@@ -150,8 +82,4 @@ func TestInterface(t *testing.T) {
 	q := w.Query(posID, rotID)
 	var q2 EntityIter = &q
 	_ = q2
-
-	f := w.Filter(filter.All(posID))
-	var f2 EntityIter = &f
-	_ = f2
 }
diff --git a/internal/base/registry_test.go b/ecs/registry_test.go
similarity index 76%
rename from internal/base/registry_test.go
rename to ecs/registry_test.go
index c0a55c3..dcc9e1f 100644
--- a/internal/base/registry_test.go
+++ b/ecs/registry_test.go
@@ -1,4 +1,4 @@
-package base
+package ecs
 
 import (
 	"reflect"
@@ -7,22 +7,13 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-type position struct {
-	X int
-	Y int
-}
-
-type rotation struct {
-	Angle int
-}
-
 func TestComponentRegistry(t *testing.T) {
-	reg := NewComponentRegistry()
+	reg := newComponentRegistry()
 
 	posType := reflect.TypeOf((*position)(nil)).Elem()
 	rotType := reflect.TypeOf((*rotation)(nil)).Elem()
 
-	reg.RegisterComponent(posType, MaskTotalBits)
+	reg.RegisterComponent(posType, maskTotalBits)
 
 	assert.Equal(t, ID(0), reg.ComponentID(posType))
 	assert.Equal(t, ID(1), reg.ComponentID(rotType))
@@ -32,7 +23,7 @@ func TestComponentRegistry(t *testing.T) {
 }
 
 func TestComponentRegistryOverflow(t *testing.T) {
-	reg := NewComponentRegistry()
+	reg := newComponentRegistry()
 
 	reg.RegisterComponent(reflect.TypeOf((*position)(nil)).Elem(), 1)
 
diff --git a/internal/base/storage_test.go b/ecs/storage_test.go
similarity index 95%
rename from internal/base/storage_test.go
rename to ecs/storage_test.go
index 772b16a..e3dbf91 100644
--- a/internal/base/storage_test.go
+++ b/ecs/storage_test.go
@@ -1,4 +1,4 @@
-package base
+package ecs
 
 import (
 	"reflect"
@@ -20,12 +20,12 @@ type simpleStruct struct {
 
 func TestStorageAddGet(t *testing.T) {
 	obj1 := testStruct{}
-	s := Storage{}
+	s := storage{}
 	s.Init(reflect.TypeOf(obj1), 1)
 	storageAddGet(t, s)
 }
 
-func storageAddGet(t *testing.T, s Storage) {
+func storageAddGet(t *testing.T, s storage) {
 	obj1 := testStruct{}
 	obj2 := testStruct{1, 2, true, false}
 
@@ -53,13 +53,13 @@ func storageAddGet(t *testing.T, s Storage) {
 
 func TestStorageRemove(t *testing.T) {
 	ref := simpleStruct{}
-	s := Storage{}
+	s := storage{}
 	s.Init(reflect.TypeOf(ref), 32)
 
 	storageRemove(t, s)
 }
 
-func storageRemove(t *testing.T, s Storage) {
+func storageRemove(t *testing.T, s storage) {
 	for i := 0; i < 5; i++ {
 		obj := simpleStruct{i}
 		s.Add(&obj)
@@ -78,7 +78,7 @@ func storageRemove(t *testing.T, s Storage) {
 
 func TestStorageDataSize(t *testing.T) {
 	ref := simpleStruct{}
-	s := Storage{}
+	s := storage{}
 	s.Init(reflect.TypeOf(ref), 1)
 
 	for i := 0; i < 5; i++ {
@@ -103,18 +103,18 @@ func TestStorageDataSize(t *testing.T) {
 }
 
 func TestNewStorage(t *testing.T) {
-	s := Storage{}
+	s := storage{}
 	s.Init(reflect.TypeOf(simpleStruct{}), 32)
 }
 
 func TestStoragePointer(t *testing.T) {
-	a := Storage{}
+	a := storage{}
 	a.Init(reflect.TypeOf(simpleStruct{}), 32)
 	a.Add(&simpleStruct{})
 	s := (*simpleStruct)(a.Get(0))
 	s.Index = 10
 
-	b := Storage{}
+	b := storage{}
 	b.Init(reflect.TypeOf(simpleStruct{}), 32)
 
 	ptr := a.Get(0)
@@ -126,7 +126,7 @@ func TestStoragePointer(t *testing.T) {
 func BenchmarkIterStorage_1000(b *testing.B) {
 	b.StopTimer()
 	ref := testStruct{}
-	s := Storage{}
+	s := storage{}
 	s.Init(reflect.TypeOf(ref), 128)
 	for i := 0; i < 1000; i++ {
 		s.Add(&testStruct{})
@@ -181,7 +181,7 @@ func BenchmarkAddStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		s := Storage{}
+		s := storage{}
 		s.Init(reflect.TypeOf(ref), 1024)
 		b.StartTimer()
 
@@ -229,7 +229,7 @@ func BenchmarkRemoveStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		s := Storage{}
+		s := storage{}
 		s.Init(reflect.TypeOf(ref), 1024)
 		for i := 0; i < 1000; i++ {
 			s.Add(&ref)
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 0d5d028..a7c9fe3 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -390,9 +390,9 @@ func Test1000Archetypes(t *testing.T) {
 
 func TestTypeSizes(t *testing.T) {
 	printTypeSize[World]()
-	printTypeSizeName[base.PagedArr32[archetype]]("PagedArr32")
+	printTypeSizeName[pagedArr32[archetype]]("PagedArr32")
 	printTypeSize[archetype]()
-	printTypeSize[base.Storage]()
+	printTypeSize[storage]()
 	printTypeSize[EntityIter]()
 	printTypeSize[archetypeIter]()
 }
diff --git a/filter/filter_test.go b/filter/filter_test.go
index 67d54c2..06b69ef 100644
--- a/filter/filter_test.go
+++ b/filter/filter_test.go
@@ -3,9 +3,19 @@ package filter
 import (
 	"testing"
 
+	"github.com/mlange-42/arche/ecs"
 	"github.com/stretchr/testify/assert"
 )
 
+type position struct {
+	X int
+	Y int
+}
+
+type rotation struct {
+	Angle int
+}
+
 func TestLogicFilters(t *testing.T) {
 
 	hasA := All(0)
@@ -13,7 +23,7 @@ func TestLogicFilters(t *testing.T) {
 	hasAll := All(0, 1)
 	hasNone := All()
 
-	var filter MaskFilter
+	var filter ecs.MaskFilter
 	filter = hasA
 	assert.True(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
@@ -89,10 +99,87 @@ func TestLogicFilters(t *testing.T) {
 	assert.Equal(t, Or(hasA, hasB), OneOf(0, 1))
 }
 
-func match(f MaskFilter, m Mask) bool {
+func TestFilter(t *testing.T) {
+	w := ecs.NewWorld()
+
+	posID := ecs.ComponentID[position](&w)
+	rotID := ecs.ComponentID[rotation](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	e4 := w.NewEntity()
+
+	w.Add(e0, posID)
+	w.Add(e1, posID, rotID)
+	w.Add(e2, posID, rotID)
+	w.Add(e3, rotID)
+	w.Add(e4, rotID)
+
+	q := w.Filter(All(posID, rotID))
+	cnt := 0
+	for q.Next() {
+		ent := q.Entity()
+		pos := (*position)(q.Get(posID))
+		rot := (*rotation)(q.Get(rotID))
+		_ = ent
+		_ = pos
+		_ = rot
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+
+	q = w.Filter(All(posID))
+	cnt = 0
+	for q.Next() {
+		ent := q.Entity()
+		pos := (*position)(q.Get(posID))
+		_ = ent
+		_ = pos
+		cnt++
+	}
+	assert.Equal(t, 3, cnt)
+
+	q = w.Filter(All(rotID))
+	cnt = 0
+	for q.Next() {
+		ent := q.Entity()
+		rot := (*rotation)(q.Get(rotID))
+		_ = ent
+		_ = rot
+		hasPos := q.Has(posID)
+		_ = hasPos
+		cnt++
+	}
+	assert.Equal(t, 4, cnt)
+
+	assert.Panics(t, func() { q.Next() })
+
+	q = w.Filter(&AND{L: All(rotID), R: NotANY(All(posID))})
+
+	cnt = 0
+	for q.Next() {
+		_ = q.Entity()
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func match(f ecs.MaskFilter, m Mask) bool {
 	return f.Matches(m.BitMask)
 }
 
+func TestInterface(t *testing.T) {
+	w := ecs.NewWorld()
+
+	posID := ecs.ComponentID[position](&w)
+
+	f := w.Filter(All(posID))
+	var f2 ecs.EntityIter = &f
+	_ = f2
+}
+
 func BenchmarkFilterStackOr(b *testing.B) {
 	b.StopTimer()
 	mask := All(1, 2, 3, 4, 5)