Repository: mlange-42/arche
Pull request #56 (2023-02-11 22:19:42)

Query interface, test coverage


Patch:
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 1855700..34487d0 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -17,6 +17,7 @@ jobs:
         run: go get .
       - name: Benchmarks
         run: |
-          go test -benchmem -run=^$ -bench ^.*$ github.com/mlange-42/arche/ecs
+          go test -benchmem -run=^$ -bench ^.*$ ./internal/base
+          go test -benchmem -run=^$ -bench ^.*$ ./...
           cd benchmark
-          go test -benchmem -run=^$ -bench ^.*$ github.com/mlange-42/arche/benchmark/...
+          go test -benchmem -run=^$ -bench ^.*$ ./...
diff --git a/README.md b/README.md
index 241e281..35be866 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,9 @@
 [Department for Ecological Modelling](https://www.ufz.de/index.php?en=34213) at the
 [Helmholtz Centre for Environmental Research](https://www.ufz.de).
 
+**:warning: Arche is still under rapid development! Be prepared for frequent API changes. :warning:**  
+
+
 ## Installations
 
 ```shell
@@ -33,7 +36,8 @@ For details on Arche's architecture, see section [Architecture](#architecture).
 Here is a minimal usage example.
 You will likely create systems with a method that takes a pointer to the `World` as argument.
 
-See the [API docs](https://pkg.go.dev/github.com/mlange-42/arche) for details.
+See the [API docs](https://pkg.go.dev/github.com/mlange-42/arche) and
+[examples](https://github.com/mlange-42/arche/tree/main/examples) for details.
 
 ```go
 package main
@@ -42,6 +46,7 @@ import (
 	"math/rand"
 
 	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/generic"
 )
 
 // Position component
@@ -65,7 +70,7 @@ func main() {
 		// Create a new Entity.
 		entity := world.NewEntity()
 		// Add components to it.
-		pos, vel := ecs.Add2[Position, Velocity](&world, entity)
+		pos, vel := generic.Add2[Position, Velocity](&world, entity)
 
 		// Initialize component fields.
 		pos.X = rand.Float64() * 100
@@ -80,7 +85,7 @@ func main() {
 		// Get a fresh query.
 		// Generic queries support up to 8 components.
 		// For more components, use World.Query()
-		query := ecs.Query2[Position, Velocity](&world)
+		query := generic.Query2[Position, Velocity](&world)
 		// Iterate it
 		for query.Next() {
 			// Component access through a Query.
@@ -147,7 +152,8 @@ Component IDs are retrieved like this:
 posID := ComponentID[Position](&world)
 ```
 
-For more details, see the [API docs](https://pkg.go.dev/github.com/mlange-42/arche).
+For more details, see the [API docs](https://pkg.go.dev/github.com/mlange-42/arche) and
+[examples](https://github.com/mlange-42/arche/tree/main/examples).
 
 ## Benchmarks
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index f6378c1..c8dff67 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -1,9 +1,10 @@
 package ecs
 
 import (
-	"internal/base"
 	"reflect"
 	"unsafe"
+
+	"github.com/mlange-42/arche/internal/base"
 )
 
 // archetype represents an ECS archetype
@@ -12,7 +13,7 @@ type archetype struct {
 	Ids  []ID
 	// Indirection to avoid a fixed-size array of storages
 	// Increases access time by 50-100%
-	indices    [MaskTotalBits]uint8
+	indices    [maskTotalBits]uint8
 	entities   base.Storage
 	components []base.Storage
 	toAdd      map[ID]*archetype
diff --git a/ecs/query.go b/ecs/query.go
index d722aa2..a52a7b0 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -1,12 +1,29 @@
 package ecs
 
 import (
-	"internal/base"
 	"unsafe"
 
 	f "github.com/mlange-42/arche/filter"
+	"github.com/mlange-42/arche/internal/base"
 )
 
+// EntityIter is the interface for iterable queries
+type EntityIter interface {
+	// Next proceeds to the next [Entity] in the Query.
+	Next() bool
+	// Has returns whether the current [Entity] has the given component
+	Has(comp ID) bool
+	// Get returns the pointer to the given component at the iterator's current [Entity]
+	Get(comp ID) unsafe.Pointer
+	// Entity returns the [Entity] at the iterator's position
+	Entity() Entity
+	// Close closes the Query and unlocks the world.
+	//
+	// Automatically called when iteration finishes.
+	// Needs to be called only if breaking out of the query iteration.
+	Close()
+}
+
 // Query is a simple iterator to iterate entities.
 //
 // Create queries through the [World] using [World.Query].
@@ -14,9 +31,9 @@ import (
 //
 // # Example:
 //
-//	query := world.Query(posID, rotID)
-//	for query.Next() {
-//	    pos := (*position)(query.Get(posID))
+//	Query := world.Query(posID, rotID)
+//	for Query.Next() {
+//	    pos := (*position)(Query.Get(posID))
 //	    pos.X += 1.0
 //	}
 type Query struct {
diff --git a/ecs/types.go b/ecs/types.go
index 6ec5b50..aaf56c6 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -1,7 +1,7 @@
 package ecs
 
 import (
-	"internal/base"
+	"github.com/mlange-42/arche/internal/base"
 )
 
 // eid is the entity identifier/index type
@@ -13,10 +13,10 @@ type ID = base.ID
 // bitMask is a bitmask.
 type bitMask = base.BitMask
 
-// MaskTotalBits is the size of Mask in bits.
+// maskTotalBits is the size of Mask in bits.
 //
 // It is the maximum number of component types that may exist in any [World].
-const MaskTotalBits = base.MaskTotalBits
+const maskTotalBits = base.MaskTotalBits
 
 // Mask is a mask for a combination of components.
 type Mask = base.Mask
diff --git a/ecs/world.go b/ecs/world.go
index bd623ac..ce7a385 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -1,11 +1,11 @@
 package ecs
 
 import (
-	"internal/base"
 	"reflect"
 	"unsafe"
 
 	f "github.com/mlange-42/arche/filter"
+	"github.com/mlange-42/arche/internal/base"
 )
 
 // ComponentID returns the ID for a component type. Registers the type if it is not already registered.
@@ -339,7 +339,7 @@ func (w *World) createArchetype(mask bitMask) *archetype {
 	types := make([]base.ComponentType, count)
 
 	idx := 0
-	for i := 0; i < MaskTotalBits; i++ {
+	for i := 0; i < maskTotalBits; i++ {
 		id := ID(i)
 		if mask.Get(id) {
 			types[idx] = base.ComponentType{ID: id, Type: w.registry.Types[id]}
diff --git a/examples/base/main.go b/examples/base/main.go
new file mode 100644
index 0000000..0c35fdc
--- /dev/null
+++ b/examples/base/main.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+	"math/rand"
+
+	"github.com/mlange-42/arche/ecs"
+)
+
+// Position component
+type Position struct {
+	X float64
+	Y float64
+}
+
+// Velocity component
+type Velocity struct {
+	X float64
+	Y float64
+}
+
+func main() {
+	// Create a World.
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	// Create entities
+	for i := 0; i < 1000; i++ {
+		// Create a new Entity.
+		entity := world.NewEntity()
+		// Add components to it.
+		world.Add(entity, posID, velID)
+		pos := (*Position)(world.Get(entity, posID))
+		vel := (*Position)(world.Get(entity, velID))
+
+		// Initialize component fields.
+		pos.X = rand.Float64() * 100
+		pos.Y = rand.Float64() * 100
+
+		vel.X = rand.NormFloat64()
+		vel.Y = rand.NormFloat64()
+	}
+
+	// Time loop.
+	for t := 0; t < 1000; t++ {
+		// Get a fresh query.
+		query := world.Query(posID, velID)
+		// Iterate it
+		for query.Next() {
+			// Component access through a Query.
+			pos := (*Position)(query.Get(posID))
+			vel := (*Position)(query.Get(velID))
+			// Update component fields.
+			pos.X += vel.X
+			pos.Y += vel.Y
+		}
+	}
+}
diff --git a/examples/doc.go b/examples/doc.go
new file mode 100644
index 0000000..fab5a4e
--- /dev/null
+++ b/examples/doc.go
@@ -0,0 +1,4 @@
+// Package examples contains examples for Arche.
+//
+// See package [github.com/mlange-42/arche/ecs] for Arche's API.
+package examples
diff --git a/examples/filter/main.go b/examples/filter/main.go
new file mode 100644
index 0000000..3846700
--- /dev/null
+++ b/examples/filter/main.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+	"math/rand"
+
+	"github.com/mlange-42/arche/ecs"
+	fi "github.com/mlange-42/arche/filter"
+)
+
+// Position component
+type Position struct {
+	X float64
+	Y float64
+}
+
+// Velocity component
+type Velocity struct {
+	X float64
+	Y float64
+}
+
+// Rotation component
+type Rotation struct {
+	A float64
+}
+
+// Elevation component
+type Elevation struct {
+	E float64
+}
+
+func main() {
+	// Create a World.
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+	rotID := ecs.ComponentID[Rotation](&world)
+	eleID := ecs.ComponentID[Elevation](&world)
+
+	// Create entities
+	for i := 0; i < 1000; i++ {
+		// Create a new Entity.
+		entity := world.NewEntity()
+		// Add components to it.
+		comps := []ecs.ID{posID, velID}
+		if i%2 == 0 {
+			comps = append(comps, rotID)
+		}
+		if i%3 == 0 {
+			comps = append(comps, eleID)
+		}
+		world.Add(entity, comps...)
+
+		pos := (*Position)(world.Get(entity, posID))
+		vel := (*Position)(world.Get(entity, velID))
+
+		// Initialize component fields.
+		pos.X = rand.Float64() * 100
+		pos.Y = rand.Float64() * 100
+
+		vel.X = rand.NormFloat64()
+		vel.Y = rand.NormFloat64()
+	}
+
+	// Create a logic filter
+	filter := fi.And(
+		fi.All(posID, velID),
+		fi.Or(
+			fi.All(rotID),
+			fi.Not(eleID),
+		),
+	)
+
+	// Time loop.
+	for t := 0; t < 1000; t++ {
+		// Get a fresh filter query.
+		query := world.Filter(filter)
+		// Iterate it
+		for query.Next() {
+			// Component access through a Query.
+			pos := (*Position)(query.Get(posID))
+			vel := (*Position)(query.Get(velID))
+			// Update component fields.
+			pos.X += vel.X
+			pos.Y += vel.Y
+		}
+	}
+}
diff --git a/examples/generic/main.go b/examples/generic/main.go
new file mode 100644
index 0000000..98790dc
--- /dev/null
+++ b/examples/generic/main.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+	"math/rand"
+
+	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/generic"
+)
+
+// Position component
+type Position struct {
+	X float64
+	Y float64
+}
+
+// Velocity component
+type Velocity struct {
+	X float64
+	Y float64
+}
+
+func main() {
+	// Create a World.
+	world := ecs.NewWorld()
+
+	// Create entities
+	for i := 0; i < 1000; i++ {
+		// Create a new Entity.
+		entity := world.NewEntity()
+		// Add components to it.
+		pos, vel := generic.Add2[Position, Velocity](&world, entity)
+
+		// Initialize component fields.
+		pos.X = rand.Float64() * 100
+		pos.Y = rand.Float64() * 100
+
+		vel.X = rand.NormFloat64()
+		vel.Y = rand.NormFloat64()
+	}
+
+	// Time loop.
+	for t := 0; t < 1000; t++ {
+		// Get a fresh query.
+		// Generic queries support up to 8 components.
+		// For more components, use World.Query()
+		query := generic.Query2[Position, Velocity](&world)
+		// Iterate it
+		for query.Next() {
+			// Component access through a Query.
+			pos, vel := query.GetAll()
+			// Update component fields.
+			pos.X += vel.X
+			pos.Y += vel.Y
+		}
+	}
+}
diff --git a/filter/filter.go b/filter/filter.go
index a713cba..6002faa 100644
--- a/filter/filter.go
+++ b/filter/filter.go
@@ -1,7 +1,7 @@
 package filter
 
 import (
-	"internal/base"
+	"github.com/mlange-42/arche/internal/base"
 )
 
 // Mask is a mask for a combination of components.
@@ -22,47 +22,80 @@ func All(comps ...base.ID) Mask {
 // OneOf matches any of the two components.
 //
 // Like [Or] for combining individual components.
-func OneOf(compA base.ID, compB base.ID) Or {
-	return Or{base.NewMask(compA), base.NewMask(compB)}
+func OneOf(compA base.ID, compB base.ID) *OR {
+	return &OR{base.NewMask(compA), base.NewMask(compB)}
 }
 
-// And is a filter for ANDing together components
-type And struct {
-	a MaskFilter
-	b MaskFilter
+// AND is a filter for ANDing together components
+type AND struct {
+	L MaskFilter
+	R MaskFilter
 }
 
-// Or is a filter for ORing together components
-type Or struct {
-	a MaskFilter
-	b MaskFilter
+// And constructs a pointer to a AND filter
+func And(l, r MaskFilter) *AND {
+	return &AND{L: l, R: r}
 }
 
-// XOr is a filter for XORing together components
-type XOr struct {
-	a MaskFilter
-	b MaskFilter
+// Matches matches a filter against a mask
+func (f *AND) Matches(mask base.BitMask) bool {
+	return f.L.Matches(mask) && f.R.Matches(mask)
+}
+
+// OR is a filter for ORing together components
+type OR struct {
+	L MaskFilter
+	R MaskFilter
 }
 
-// Not is a filter for excluding components
-type Not Mask
+// Or constructs a pointer to a OR filter
+func Or(l, r MaskFilter) *OR {
+	return &OR{L: l, R: r}
+}
 
 // Matches matches a filter against a mask
-func (f *And) Matches(mask base.BitMask) bool {
-	return f.a.Matches(mask) && f.b.Matches(mask)
+func (f *OR) Matches(mask base.BitMask) bool {
+	return f.L.Matches(mask) || f.R.Matches(mask)
+}
+
+// XOR is a filter for XORing together components
+type XOR struct {
+	L MaskFilter
+	R MaskFilter
+}
+
+// XOr constructs a pointer to a XOR filter
+func XOr(l, r MaskFilter) *XOR {
+	return &XOR{L: l, R: r}
 }
 
 // Matches matches a filter against a mask
-func (f *Or) Matches(mask base.BitMask) bool {
-	return f.a.Matches(mask) || f.b.Matches(mask)
+func (f *XOR) Matches(mask base.BitMask) bool {
+	return f.L.Matches(mask) != f.R.Matches(mask)
+}
+
+// NOT is a filter for excluding entities with all given components
+type NOT Mask
+
+// Not constructs a NOT filter
+func Not(comps ...base.ID) NOT {
+	return NOT(base.NewMask(comps...))
 }
 
 // Matches matches a filter against a mask
-func (f *XOr) Matches(mask base.BitMask) bool {
-	return f.a.Matches(mask) != f.b.Matches(mask)
+func (f NOT) Matches(mask base.BitMask) bool {
+	return !mask.Contains(f.BitMask)
+}
+
+// NotANY is a filter for excluding entities with any of the the given components
+type NotANY Mask
+
+// NotAny constructs a NotANY filter
+func NotAny(comps ...base.ID) NotANY {
+	return NotANY(base.NewMask(comps...))
 }
 
 // Matches matches a filter against a mask
-func (f Not) Matches(mask base.BitMask) bool {
+func (f NotANY) Matches(mask base.BitMask) bool {
 	return !mask.ContainsAny(f.BitMask)
 }
diff --git a/generic/query_generic.go b/generic/query_generic.go
index fbc5ea6..f1a3409 100644
--- a/generic/query_generic.go
+++ b/generic/query_generic.go
@@ -1,9 +1,8 @@
 package generic
 
 import (
-	"internal/base"
-
 	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/internal/base"
 )
 
 // Q0 is a generic query for no components.
diff --git a/go.mod b/go.mod
index 06080ad..6ef6626 100644
--- a/go.mod
+++ b/go.mod
@@ -2,15 +2,7 @@ module github.com/mlange-42/arche
 
 go 1.20
 
-require (
-	github.com/stretchr/testify v1.8.1
-
-	internal/base v0.0.0
-)
-
-replace (
-	internal/base => ./internal/base
-)
+require github.com/stretchr/testify v1.8.1
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
diff --git a/go.work b/go.work
index 9766735..73bdcd7 100644
--- a/go.work
+++ b/go.work
@@ -4,5 +4,4 @@ use (
 	.
 	./benchmark
 	./profile
-	./internal/base
 )
diff --git a/internal/base/bitmask.go b/internal/base/bitmask.go
index 8e60720..ea886dc 100644
--- a/internal/base/bitmask.go
+++ b/internal/base/bitmask.go
@@ -1,10 +1,5 @@
 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/go.mod b/internal/base/go.mod
deleted file mode 100644
index 950ea20..0000000
--- a/internal/base/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module base
-
-go 1.20
diff --git a/internal/base/registry.go b/internal/base/registry.go
index b270027..0d27960 100644
--- a/internal/base/registry.go
+++ b/internal/base/registry.go
@@ -2,6 +2,11 @@ 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


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 040b2c9..2770c37 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -38,6 +38,23 @@ jobs:
           go test -v ./internal/base ./... -covermode=count -coverprofile="coverage.out"
           go tool cover -func="coverage.out"
 
+  examples:
+    name: Run examples
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Setup Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '1.19.x'
+      - name: Install dependencies
+        run: go get .
+      - name: Test
+        run: |
+          go run ./examples/base
+          go run ./examples/generic
+          go run ./examples/filter
+
   lint:
     name: Run linters
     runs-on: ubuntu-latest
diff --git a/benchmark/iterate/arche_test.go b/benchmark/iterate/arche_test.go
index abd3bba..aebb075 100644
--- a/benchmark/iterate/arche_test.go
+++ b/benchmark/iterate/arche_test.go
@@ -32,7 +32,7 @@ func runArcheQuery(b *testing.B, count int) {
 	}
 }
 
-func runArcheQueryFilter(b *testing.B, count int) {
+func runArcheFilter(b *testing.B, count int) {
 	b.StopTimer()
 	world := ecs.NewWorld()
 
@@ -173,6 +173,42 @@ func runArcheQuery1kArch(b *testing.B, count int) {
 	}
 }
 
+func runArcheFilter1kArch(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+	registerAll(&world)
+
+	perArch := 2 * count / 1000
+
+	for i := 0; i < 1024; i++ {
+		mask := i
+		add := make([]ecs.ID, 0, 10)
+		for j := 0; j < 10; j++ {
+			id := ecs.ID(j)
+			m := 1 << j
+			if mask&m == m {
+				add = append(add, id)
+			}
+		}
+		for j := 0; j < perArch; j++ {
+			entity := world.NewEntity()
+			world.Add(entity, add...)
+		}
+	}
+
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		query := world.Filter(filter.All(6))
+		b.StartTimer()
+		for query.Next() {
+			pos := (*position)(query.Get(6))
+			_ = pos
+		}
+	}
+}
+
 func runArcheWorld(b *testing.B, count int) {
 	b.StopTimer()
 	world := ecs.NewWorld()
@@ -237,16 +273,16 @@ func BenchmarkArcheIterQueryID_100_000(b *testing.B) {
 	runArcheQuery(b, 100000)
 }
 
-func BenchmarkArcheIterQueryFilter_1_000(b *testing.B) {
-	runArcheQueryFilter(b, 1000)
+func BenchmarkArcheIterFilter_1_000(b *testing.B) {
+	runArcheFilter(b, 1000)
 }
 
-func BenchmarkArcheIterQueryFilter_10_000(b *testing.B) {
-	runArcheQueryFilter(b, 10000)
+func BenchmarkArcheIterFilter_10_000(b *testing.B) {
+	runArcheFilter(b, 10000)
 }
 
-func BenchmarkArcheIterQueryFilter_100_000(b *testing.B) {
-	runArcheQueryFilter(b, 100000)
+func BenchmarkArcheIterFilter_100_000(b *testing.B) {
+	runArcheFilter(b, 100000)
 }
 
 func BenchmarkArcheIterQueryGeneric_1_000(b *testing.B) {
@@ -320,3 +356,15 @@ func BenchmarkArcheIter1kArchID_10_000(b *testing.B) {
 func BenchmarkArcheIter1kArchID_100_000(b *testing.B) {
 	runArcheQuery1kArch(b, 100000)
 }
+
+func BenchmarkArcheFilter1kArchID_1_000(b *testing.B) {
+	runArcheFilter1kArch(b, 1000)
+}
+
+func BenchmarkArcheFilter1kArchID_10_000(b *testing.B) {
+	runArcheFilter1kArch(b, 10000)
+}
+
+func BenchmarkArcheFilter1kArchID_100_000(b *testing.B) {
+	runArcheFilter1kArch(b, 100000)
+}
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index c486d61..f7b7a85 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -1,10 +1,10 @@
 package ecs
 
 import (
-	"internal/base"
 	"reflect"
 	"testing"
 
+	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/ecs/query_test.go b/ecs/query_test.go
index 1f58e5f..7e80a5d 100644
--- a/ecs/query_test.go
+++ b/ecs/query_test.go
@@ -3,6 +3,7 @@ package ecs
 import (
 	"testing"
 
+	"github.com/mlange-42/arche/filter"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -72,3 +73,85 @@ 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()
+
+	posID := ComponentID[position](&w)
+	rotID := ComponentID[rotation](&w)
+
+	q := w.Query(posID, rotID)
+	var q2 EntityIter = &q
+	_ = q2
+
+	f := w.Filter(filter.All(posID))
+	var f2 EntityIter = &f
+	_ = f2
+}
diff --git a/ecs/types_test.go b/ecs/types_test.go
index 3509028..dc9c02f 100644
--- a/ecs/types_test.go
+++ b/ecs/types_test.go
@@ -1,9 +1,9 @@
 package ecs
 
 import (
-	"internal/base"
 	"testing"
 
+	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 68f3dae..0d5d028 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -2,10 +2,10 @@ package ecs
 
 import (
 	"fmt"
-	"internal/base"
 	"reflect"
 	"testing"
 
+	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -264,7 +264,7 @@ func TestWorldIter(t *testing.T) {
 		assert.Panics(t, func() { query.Next() })
 	}
 
-	for i := 0; i < MaskTotalBits-1; i++ {
+	for i := 0; i < maskTotalBits-1; i++ {
 		query := world.Query(posID, rotID)
 		for query.Next() {
 			pos := (*position)(query.Get(posID))
@@ -393,7 +393,7 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSizeName[base.PagedArr32[archetype]]("PagedArr32")
 	printTypeSize[archetype]()
 	printTypeSize[base.Storage]()
-	printTypeSize[Query]()
+	printTypeSize[EntityIter]()
 	printTypeSize[archetypeIter]()
 }
 
diff --git a/filter/filter_test.go b/filter/filter_test.go
index 32eccda..67d54c2 100644
--- a/filter/filter_test.go
+++ b/filter/filter_test.go
@@ -11,7 +11,7 @@ func TestLogicFilters(t *testing.T) {
 	hasA := All(0)
 	hasB := All(1)
 	hasAll := All(0, 1)
-	hasNone := Mask{}
+	hasNone := All()
 
 	var filter MaskFilter
 	filter = hasA
@@ -20,13 +20,13 @@ func TestLogicFilters(t *testing.T) {
 	assert.False(t, match(filter, hasB))
 	assert.False(t, match(filter, hasNone))
 
-	filter = &Or{hasA, hasB}
+	filter = Or(hasA, hasB)
 	assert.True(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
 	assert.True(t, match(filter, hasB))
 	assert.False(t, match(filter, hasNone))
 
-	filter = &XOr{hasA, hasB}
+	filter = XOr(hasA, hasB)
 	assert.False(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
 	assert.True(t, match(filter, hasB))
@@ -38,31 +38,108 @@ func TestLogicFilters(t *testing.T) {
 	assert.False(t, match(filter, hasB))
 	assert.False(t, match(filter, hasNone))
 
-	filter = &And{hasA, hasB}
+	filter = And(hasA, hasB)
 	assert.True(t, match(filter, hasAll))
 	assert.False(t, match(filter, hasA))
 	assert.False(t, match(filter, hasB))
 	assert.False(t, match(filter, hasNone))
 
-	filter = Not(hasB)
+	filter = NotAny(1)
 	assert.False(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
 	assert.False(t, match(filter, hasB))
 	assert.True(t, match(filter, hasNone))
 
-	filter = &And{hasA, Not(hasB)}
+	filter = NotAny(0, 1)
+	assert.False(t, match(filter, hasAll))
+	assert.False(t, match(filter, hasA))
+	assert.False(t, match(filter, hasB))
+	assert.True(t, match(filter, hasNone))
+
+	filter = Not(0)
+	assert.False(t, match(filter, hasAll))
+	assert.False(t, match(filter, hasA))
+	assert.True(t, match(filter, hasB))
+	assert.True(t, match(filter, hasNone))
+
+	filter = Not(0, 1)
+	assert.False(t, match(filter, hasAll))
+	assert.True(t, match(filter, hasA))
+	assert.True(t, match(filter, hasB))
+	assert.True(t, match(filter, hasNone))
+
+	filter = And(hasA, NotANY(hasB))
 	assert.False(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
 	assert.False(t, match(filter, hasB))
 	assert.False(t, match(filter, hasNone))
 
-	filter = &Or{hasAll, Not(hasB)}
+	filter = Or(hasAll, NotAny(1))
 	assert.True(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
 	assert.False(t, match(filter, hasB))
 	assert.True(t, match(filter, hasNone))
+
+	filter = Or(OneOf(0, 1), NotANY(hasB))
+	assert.True(t, match(filter, hasAll))
+	assert.True(t, match(filter, hasA))
+	assert.True(t, match(filter, hasB))
+	assert.True(t, match(filter, hasNone))
+
+	assert.Equal(t, Or(hasA, hasB), OneOf(0, 1))
 }
 
 func match(f MaskFilter, m Mask) bool {
 	return f.Matches(m.BitMask)
 }
+
+func BenchmarkFilterStackOr(b *testing.B) {
+	b.StopTimer()
+	mask := All(1, 2, 3, 4, 5)
+
+	filter := OR{All(1), All(2)}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = filter.Matches(mask.BitMask)
+	}
+}
+
+func BenchmarkFilterStack5And(b *testing.B) {
+	b.StopTimer()
+	mask := All(1, 2, 3, 4, 5)
+
+	a1 := AND{All(1), All(2)}
+	a2 := AND{&a1, All(3)}
+	a3 := AND{&a2, All(4)}
+	filter := AND{&a3, All(5)}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = filter.Matches(mask.BitMask)
+	}
+}
+
+func BenchmarkFilterHeapOr(b *testing.B) {
+	b.StopTimer()
+	mask := All(1, 2, 3, 4, 5)
+
+	filter := Or(All(1), All(2))
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = filter.Matches(mask.BitMask)
+	}
+}
+
+func BenchmarkFilterHeap5And(b *testing.B) {
+	b.StopTimer()
+	mask := All(1, 2, 3, 4, 5)
+
+	filter := And(All(1), And(All(2), And(All(3), And(All(4), All(5)))))
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = filter.Matches(mask.BitMask)
+	}
+}
diff --git a/generic/query_generic_test.go b/generic/query_generic_test.go
index bf8d610..7ba3a3f 100644
--- a/generic/query_generic_test.go
+++ b/generic/query_generic_test.go
@@ -1,10 +1,10 @@
 package generic
 
 import (
-	"internal/base"
 	"testing"
 
 	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/internal/base/storage_test.go b/internal/base/storage_test.go
index 4246071..772b16a 100644
--- a/internal/base/storage_test.go
+++ b/internal/base/storage_test.go
@@ -18,7 +18,7 @@ type simpleStruct struct {
 	Index int
 }
 
-func TestReflectStorageAddGet(t *testing.T) {
+func TestStorageAddGet(t *testing.T) {
 	obj1 := testStruct{}
 	s := Storage{}
 	s.Init(reflect.TypeOf(obj1), 1)
@@ -51,7 +51,7 @@ func storageAddGet(t *testing.T, s Storage) {
 	assert.Equal(t, []testStruct{{}, {1, 1001, true, false}, {}}, toSlice[testStruct](s), "Wrong extracted struct slice")
 }
 
-func TestReflectStorageRemove(t *testing.T) {
+func TestStorageRemove(t *testing.T) {
 	ref := simpleStruct{}
 	s := Storage{}
 	s.Init(reflect.TypeOf(ref), 32)
@@ -76,7 +76,7 @@ func storageRemove(t *testing.T, s Storage) {
 	assert.Equal(t, []simpleStruct{{0}, {3}, {2}}, toSlice[simpleStruct](s), "Wrong slice after remove")
 }
 
-func TestReflectStorageDataSize(t *testing.T) {
+func TestStorageDataSize(t *testing.T) {
 	ref := simpleStruct{}
 	s := Storage{}
 	s.Init(reflect.TypeOf(ref), 1)
@@ -102,12 +102,28 @@ func TestReflectStorageDataSize(t *testing.T) {
 	assert.Equal(t, 6, int(s.cap))
 }
 
-func TestNewReflectStorage(t *testing.T) {
+func TestNewStorage(t *testing.T) {
 	s := Storage{}
 	s.Init(reflect.TypeOf(simpleStruct{}), 32)
 }
 
-func BenchmarkIterReflectStorage_1000(b *testing.B) {
+func TestStoragePointer(t *testing.T) {
+	a := Storage{}
+	a.Init(reflect.TypeOf(simpleStruct{}), 32)
+	a.Add(&simpleStruct{})
+	s := (*simpleStruct)(a.Get(0))
+	s.Index = 10
+
+	b := Storage{}
+	b.Init(reflect.TypeOf(simpleStruct{}), 32)
+
+	ptr := a.Get(0)
+	b.AddPointer(ptr)
+	s = (*simpleStruct)(b.Get(0))
+	assert.Equal(t, 10, s.Index)
+}
+
+func BenchmarkIterStorage_1000(b *testing.B) {
 	b.StopTimer()
 	ref := testStruct{}
 	s := Storage{}
@@ -161,7 +177,7 @@ func BenchmarkIterSliceInterface_1000(b *testing.B) {
 	}
 }
 
-func BenchmarkAddReflectStorage_1000(b *testing.B) {
+func BenchmarkAddStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
@@ -209,7 +225,7 @@ func BenchmarkAddSliceInterface_1000(b *testing.B) {
 	}
 }
 
-func BenchmarkRemoveReflectStorage_1000(b *testing.B) {
+func BenchmarkRemoveStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
diff --git a/internal/base/types_test.go b/internal/base/types_test.go
new file mode 100644
index 0000000..29d902c
--- /dev/null
+++ b/internal/base/types_test.go
@@ -0,0 +1,17 @@
+package base
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMask(t *testing.T) {
+	filter := NewMask(0, 2, 4)
+	other := NewBitMask(0, 1, 2)
+
+	assert.False(t, filter.Matches(other))
+
+	other = NewBitMask(0, 1, 2, 3, 4)
+	assert.True(t, filter.Matches(other))
+}