diff --git a/README.md b/README.md index 264572d..877fc9b 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ ecs.MapN(world, func(id ecs.Id, a *ComponentA, /*... */, n *ComponentN) { }) ``` -### Advanced queries +#### Advanced queries You can also filter your queries for more advanced usage: ``` // Returns a view of Position and Velocity, but only if the entity also has the `Rotation` component. @@ -78,6 +78,68 @@ query := ecs.Query2[Position, Velocity](world, ecs.With(Rotation)) query := ecs.Query2[Position, Velocity](world, ecs.Optional(Velocity)) ``` +### Systems +There are several types of systems: + 1. Realtime - should be runned as fast as possible (without any deplays). Usefull for Rendering or collecting inputs from user. + 2. Fixed - should be runned ones a specified period of time. Usefull for physics. + 3. Step - can only be runned by manually triggering. Usefull for simulations, like stable updates in P2P games. + +To create system, implement one of the interfaces `RealtimeSystem`, `FixedSystem`, `StepSystem`: +``` +type Position struct { + X, Y float64 +} +type Velocity struct { + X, Y float64 +} + +// --- world initialization here --- + +type movementSystem struct {} + +func (s *movementSystem) GetName() { + return "physics::movement" +} + +func (s *movementSystem) RunFixed(delta time.Duration) { + query := ecs.Query2[Position, Velocity](world) + query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) { + // No need to adjust here to delta time between updates, because + // physics steps should always be the same. Delta time is still available to systems, where + // it can be important (f.e. if we want to detect "throttling") + pos.X += vel.X + pos.Y += vel.Y + + // Add some drag + vel.X *= 0.99 + vel.Y *= 0.99 + }) +} +``` + +To shedule the system, we need to have a system group of one of the types `RealtimeGroup`, `FixedGroup`, `StepGroup`. There are several rules regarding system groups: +1. One system cant belong to multiple groups. +2. Groups are running in parallel and there is no synchronization between them. +3. All the systems in the group can be automatically runned in parallel. +4. Groups share lock to the components on the systems level, so there will be no race conditions while accessing components data. +5. Group can only contain systems with theirs type. + +``` +componentsGuard = group.NewComponentsGuard() +physics = group.NewFixedGroup("physics", 50*time.Millisecond, componentsGuard) +physics.AddSystem(&movementSystem{}) +physics.Build() +physics.StartFixed() +defer physics.StopFixed() +``` + +#### Order +You can specify systems order by implementing `system.RunBeforeSystem` or/and `system.RunAfterSystem` interface for the system. You cant implement order between systems from multiple groups. + +#### Automatic parallelism +In order for sheduler to understand which systems can be runed in parallel, you have to specify components used by systems. By default, if component will not be specified, system will be marked as `exclusive` and will only be runned by locking entire ECS world. +You can do this by implementing `system.ReadComponentsSystem` or/and `system.WriteComponentsSystem` interface for the system. Make sure to use `system.ReadComponentsSystem` as much as possible, because with read access, multiple systems can be runned at the same time. + ### Commands Commands will eventually replace `ecs.Write(...)` once I figure out how their usage will work. Commands essentially buffer some work on the ECS so that the work can be executed later on. You can use them in loop safe ways by calling `Execute()` after your loop has completed. Right now they work like this: @@ -90,7 +152,7 @@ cmd.Execute() ### Still In Progress - [ ] Improving iterator performance: See: https://github.com/golang/go/discussions/54245 -- [ ] Automatic multithreading +- [+] Automatic multithreading - [ ] Without() filter ### Videos diff --git a/arch.go b/arch.go index 00726fd..b770261 100644 --- a/arch.go +++ b/arch.go @@ -5,6 +5,7 @@ import ( ) // This is the identifier for entities in the world +// //cod:struct type Id uint32 @@ -28,17 +29,17 @@ func (s *componentSlice[T]) Write(index int, val T) { // TODO: Rename, this is kind of like an archetype header type lookupList struct { - index *internalMap[Id,int] // A mapping from entity ids to array indices - id []Id // An array of every id in the arch list (essentially a reverse mapping from index to Id) - holes []int // List of indexes that have ben deleted - mask archetypeMask + index *internalMap[Id, int] // A mapping from entity ids to array indices + id []Id // An array of every id in the arch list (essentially a reverse mapping from index to Id) + holes []int // List of indexes that have ben deleted + mask archetypeMask } // Adds ourselves to the last available hole, else appends // Returns the index func (l *lookupList) addToEasiestHole(id Id) int { if len(l.holes) > 0 { - lastHoleIndex := len(l.holes)-1 + lastHoleIndex := len(l.holes) - 1 index := l.holes[lastHoleIndex] l.id[index] = id l.index.Put(id, index) @@ -54,7 +55,6 @@ func (l *lookupList) addToEasiestHole(id Id) int { } } - type storage interface { ReadToEntity(*Entity, archetypeId, int) bool ReadToRawEntity(*RawEntity, archetypeId, int) bool @@ -105,25 +105,21 @@ func (s *componentSliceStorage[T]) print(amount int) { // Provides generic storage for all archetypes type archEngine struct { - generation int + generation int // archCounter archetypeId - lookup []*lookupList // Indexed by archetypeId - compSliceStorage []storage // Indexed by componentId - dcr *componentRegistry - - // TODO - using this makes things not thread safe inside the engine - archCount map[archetypeId]int + lookup []*lookupList // Indexed by archetypeId + compSliceStorage []storage // Indexed by componentId + dcr *componentRegistry } func newArchEngine() *archEngine { return &archEngine{ - generation: 1, // Start at 1 so that anyone with the default int value will always realize they are in the wrong generation + generation: 1, // Start at 1 so that anyone with the default int value will always realize they are in the wrong generation lookup: make([]*lookupList, 0, DefaultAllocation), - compSliceStorage: make([]storage, maxComponentId + 1), + compSliceStorage: make([]storage, maxComponentId+1), dcr: newComponentRegistry(), - archCount: make(map[archetypeId]int), } } @@ -133,10 +129,10 @@ func (e *archEngine) newArchetypeId(archMask archetypeMask) archetypeId { archId := archetypeId(len(e.lookup)) e.lookup = append(e.lookup, &lookupList{ - index: newMap[Id,int](0), + index: newMap[Id, int](0), id: make([]Id, 0, DefaultAllocation), holes: make([]int, 0, DefaultAllocation), - mask: archMask, + mask: archMask, }, ) @@ -163,7 +159,7 @@ func (e *archEngine) getGeneration() int { // } func (e *archEngine) count(anything ...any) int { - comps := make([]componentId, len(anything)) + comps := make([]ComponentId, len(anything)) for i, c := range anything { comps[i] = name(c) } @@ -190,29 +186,24 @@ func (e *archEngine) getArchetypeId(comp ...Component) archetypeId { } // TODO - map might be slower than just having an array. I could probably do a big bitmask and then just do a logical OR -func (e *archEngine) FilterList(archIds []archetypeId, comp []componentId) []archetypeId { +func (e *archEngine) FilterList(archIds []archetypeId, comp []ComponentId) []archetypeId { // TODO: could I maybe do something more optimal with archetypeMask? // New way: With archSets that are just slices // Logic: Go thorugh and keep track of how many times we see each archetype. Then only keep the archetypes that we've seen an amount of times equal to the number of components. If we have 5 components and see 5 for a specific archId, it means that each component has that archId - // TODO: this may be more efficient to use a slice? - - // Clearing Optimization: https://go.dev/doc/go1.11#performance-compiler - for k := range e.archCount { - delete(e.archCount, k) - } + archCount := make([]int, len(e.lookup)) for _, compId := range comp { for _, archId := range e.dcr.archSet[compId] { - e.archCount[archId] = e.archCount[archId] + 1 + archCount[archId]++ } } numComponents := len(comp) archIds = archIds[:0] - for archId, count := range e.archCount { + for archId, count := range archCount { if count >= numComponents { - archIds = append(archIds, archId) + archIds = append(archIds, archetypeId(archId)) // // TODO: How tight do I want my tolerances? // if count > numComponents { @@ -231,7 +222,7 @@ func getStorage[T any](e *archEngine) *componentSliceStorage[T] { } // Note: This will panic if the wrong compId doesn't match the generic type -func getStorageByCompId[T any](e *archEngine, compId componentId) *componentSliceStorage[T] { +func getStorageByCompId[T any](e *archEngine, compId ComponentId) *componentSliceStorage[T] { ss := e.compSliceStorage[compId] if ss == nil { ss = &componentSliceStorage[T]{ diff --git a/bundle.go b/bundle.go index f868b47..5befb11 100644 --- a/bundle.go +++ b/bundle.go @@ -1,7 +1,7 @@ package ecs type Bundle[T any] struct { - compId componentId + compId ComponentId storage *componentSliceStorage[T] // world *ecs.World //Needed? } @@ -11,14 +11,14 @@ func NewBundle[T any](world *World) Bundle[T] { var t T compId := name(t) return Bundle[T]{ - compId: compId, + compId: compId, storage: getStorageByCompId[T](world.engine, compId), } } func (c Bundle[T]) New(comp T) Box[T] { return Box[T]{ - Comp: comp, + Comp: comp, compId: c.compId, // storage: c.storage, } @@ -28,17 +28,17 @@ func (b Bundle[T]) write(engine *archEngine, archId archetypeId, index int, comp writeArch[T](engine, archId, index, b.storage, comp) } -func (b Bundle[T]) id() componentId { +func (b Bundle[T]) id() ComponentId { return b.compId } type Bundle4[A, B, C, D any] struct { - compId componentId - boxA *Box[A] - boxB *Box[B] - boxC *Box[C] - boxD *Box[D] - comps []Component + compId ComponentId + boxA *Box[A] + boxB *Box[B] + boxC *Box[C] + boxD *Box[D] + comps []Component } // Createst the boxed component type @@ -56,16 +56,15 @@ func NewBundle4[A, B, C, D any]() Bundle4[A, B, C, D] { } return Bundle4[A, B, C, D]{ - boxA: boxA, - boxB: boxB, - boxC: boxC, - boxD: boxD, + boxA: boxA, + boxB: boxB, + boxC: boxC, + boxD: boxD, comps: comps, } } - -func (bun Bundle4[A,B,C,D]) Write(world *World, id Id, a A, b B, c C, d D) { +func (bun Bundle4[A, B, C, D]) Write(world *World, id Id, a A, b B, c C, d D) { // bun.boxA.Comp = a // bun.boxB.Comp = b // bun.boxC.Comp = c @@ -86,7 +85,6 @@ func (bun Bundle4[A,B,C,D]) Write(world *World, id Id, a A, b B, c C, d D) { bun.boxD.Comp = d Write(world, id, - bun.comps... + bun.comps..., ) } - diff --git a/component.go b/component.go index 3a0377d..3ed1f77 100644 --- a/component.go +++ b/component.go @@ -5,17 +5,17 @@ import ( // "sort" ) -type componentId uint16 +type ComponentId uint16 type Component interface { write(*archEngine, archetypeId, int) - id() componentId + Id() ComponentId } // This type is used to box a component with all of its type info so that it implements the component interface. I would like to get rid of this and simplify the APIs type Box[T any] struct { Comp T - compId componentId + compId ComponentId } // Createst the boxed component type @@ -26,10 +26,10 @@ func C[T any](comp T) Box[T] { } } func (c Box[T]) write(engine *archEngine, archId archetypeId, index int) { - store := getStorageByCompId[T](engine, c.id()) + store := getStorageByCompId[T](engine, c.Id()) writeArch[T](engine, archId, index, store, c.Comp) } -func (c Box[T]) id() componentId { +func (c Box[T]) Id() ComponentId { if c.compId == invalidComponentId { c.compId = name(c.Comp) } @@ -40,20 +40,20 @@ func (c Box[T]) Get() T { return c.Comp } - // Note: you can increase max component size by increasing maxComponentId and archetypeMask // TODO: I should have some kind of panic if you go over maximum component size const maxComponentId = 255 + // Supports maximum 256 unique component types type archetypeMask [4]uint64 // TODO: can/should I make this configurable? func buildArchMask(comps ...Component) archetypeMask { var mask archetypeMask for _, comp := range comps { // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) - c := comp.id() + c := comp.Id() idx := c / 64 offset := c - (64 * idx) - mask[idx] |= (1< WorldSize { + pos.X = WorldSize + if vel.X > 0 { + vel.X = -vel.X + } + } + + if pos.Y < 0 { + pos.Y = 0 + if vel.Y < 0 { + vel.Y = -vel.Y + } + } + if pos.Y > WorldSize { + pos.Y = WorldSize + if vel.Y > 0 { + vel.Y = -vel.Y + } + } + }) +} diff --git a/examples/balls/ecs/collision.go b/examples/balls/ecs/collision.go new file mode 100644 index 0000000..de729db --- /dev/null +++ b/examples/balls/ecs/collision.go @@ -0,0 +1,76 @@ +package ecs + +import ( + "math" + "time" + + "github.com/unitoftime/ecs" + "github.com/unitoftime/ecs/system" +) + +const CollisionSystemName system.SystemName = "physics::collision" + +type CollisionSystem struct { + World *ecs.World + SpacialHash map[IntVector2][]SpacialCellObject +} + +func (s *CollisionSystem) GetName() system.SystemName { + return CollisionSystemName +} + +func (s *CollisionSystem) GetReadComponents() []ecs.Component { + return []ecs.Component{ecs.C(Position{})} +} + +func (s *CollisionSystem) GetWriteComponents() []ecs.Component { + return []ecs.Component{ecs.C(Velocity{})} +} + +func (s *CollisionSystem) GetRunAfter() []system.SystemName { + return []system.SystemName{MovementSystemName, SpacialSystemName} +} + +func (s *CollisionSystem) RunFixed(delta time.Duration) { + query := ecs.Query2[Position, Velocity](s.World) + query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) { + cellId := IntVector2{ + X: int32(math.Round(pos.X / SpacialCellSize)), + Y: int32(math.Round(pos.Y / SpacialCellSize)), + } + objects := s.SpacialHash[cellId] + + for _, object := range objects { + solveCollision := func(otherCell IntVector2) { + if otherObjects, ok := s.SpacialHash[otherCell]; ok { + speed := math.Sqrt(math.Pow(vel.X, 2) + math.Pow(vel.Y, 2)) + + for _, otherObject := range otherObjects { + if otherObject.Entity == id { + continue + } + + distance := math.Sqrt(math.Pow(otherObject.X-object.X, 2) + math.Pow(otherObject.Y-object.Y, 2)) + if distance < 0.001 { + continue //TODO: fix this bug + } + if distance < 2 { + vel.X = (object.X - otherObject.X) / distance * speed + vel.Y = (object.Y - otherObject.Y) / distance * speed + } + } + } + } + + solveCollision(cellId) + solveCollision(IntVector2{X: cellId.X + 1, Y: cellId.Y + 1}) + solveCollision(IntVector2{X: cellId.X + 1, Y: cellId.Y - 1}) + solveCollision(IntVector2{X: cellId.X + 1, Y: cellId.Y}) + solveCollision(IntVector2{X: cellId.X - 1, Y: cellId.Y + 1}) + solveCollision(IntVector2{X: cellId.X - 1, Y: cellId.Y - 1}) + solveCollision(IntVector2{X: cellId.X - 1, Y: cellId.Y}) + solveCollision(IntVector2{X: cellId.X, Y: cellId.Y + 1}) + solveCollision(IntVector2{X: cellId.X, Y: cellId.Y - 1}) + } + }) +} diff --git a/examples/balls/ecs/components.go b/examples/balls/ecs/components.go new file mode 100644 index 0000000..798d46f --- /dev/null +++ b/examples/balls/ecs/components.go @@ -0,0 +1,26 @@ +package ecs + +type Position struct { + X float64 + Y float64 +} + +type RenderPosition struct { + X float64 + Y float64 +} + +type Velocity struct { + X float64 + Y float64 +} + +/* +type Collider struct { + Size int32 +} + +type RigidBody struct { + Mass int32 +} +*/ diff --git a/examples/balls/ecs/movement.go b/examples/balls/ecs/movement.go new file mode 100644 index 0000000..d1861b7 --- /dev/null +++ b/examples/balls/ecs/movement.go @@ -0,0 +1,34 @@ +package ecs + +import ( + "time" + + "github.com/unitoftime/ecs" + "github.com/unitoftime/ecs/system" +) + +const MovementSystemName system.SystemName = "physics::movement" + +type MovementSystem struct { + World *ecs.World +} + +func (s *MovementSystem) GetName() system.SystemName { + return MovementSystemName +} + +func (s *MovementSystem) GetReadComponents() []ecs.Component { + return []ecs.Component{ecs.C(Velocity{})} +} + +func (s *MovementSystem) GetWriteComponents() []ecs.Component { + return []ecs.Component{ecs.C(Position{})} +} + +func (s *MovementSystem) RunFixed(delta time.Duration) { + query := ecs.Query2[Position, Velocity](s.World) + query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) { + pos.X += vel.X + pos.Y += vel.Y + }) +} diff --git a/examples/balls/ecs/render.go b/examples/balls/ecs/render.go new file mode 100644 index 0000000..9765770 --- /dev/null +++ b/examples/balls/ecs/render.go @@ -0,0 +1,30 @@ +package ecs + +import ( + "time" + + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/unitoftime/ecs" + "github.com/unitoftime/ecs/system" +) + +const RenderSystemName system.SystemName = "render::render" + +type RenderSystem struct { + World *ecs.World +} + +func (s *RenderSystem) GetName() system.SystemName { + return RenderSystemName +} + +func (s *RenderSystem) GetReadComponents() []ecs.Component { + return []ecs.Component{ecs.C(RenderPosition{})} +} + +func (s *RenderSystem) RunRealtime(delta time.Duration) { + query := ecs.Query1[RenderPosition](s.World) + query.MapId(func(id ecs.Id, pos *RenderPosition) { + rl.DrawCircle(int32(pos.X*10), int32(pos.Y*10), 10, rl.Blue) + }) +} diff --git a/examples/balls/ecs/spacial.go b/examples/balls/ecs/spacial.go new file mode 100644 index 0000000..339c777 --- /dev/null +++ b/examples/balls/ecs/spacial.go @@ -0,0 +1,60 @@ +package ecs + +import ( + "math" + "time" + + "github.com/unitoftime/ecs" + "github.com/unitoftime/ecs/system" +) + +const SpacialCellSize = 2.0 +const SpacialSystemName system.SystemName = "physics::spacial" + +type SpacialCellObject struct { + Entity ecs.Id + X float64 + Y float64 +} + +type SpacialSystem struct { + World *ecs.World + SpacialHash map[IntVector2][]SpacialCellObject +} + +func (s *SpacialSystem) GetName() system.SystemName { + return SpacialSystemName +} + +func (s *SpacialSystem) GetReadComponents() []ecs.Component { + return []ecs.Component{ecs.C(Position{})} +} + +func (s *SpacialSystem) GetRunAfter() []system.SystemName { + return []system.SystemName{MovementSystemName} +} + +func (s *SpacialSystem) RunFixed(delta time.Duration) { + for iv := range s.SpacialHash { + delete(s.SpacialHash, iv) + } + + query := ecs.Query1[Position](s.World) + query.MapId(func(id ecs.Id, pos *Position) { + cellId := IntVector2{ + X: int32(math.Round(pos.X / SpacialCellSize)), + Y: int32(math.Round(pos.Y / SpacialCellSize)), + } + if _, ok := s.SpacialHash[cellId]; !ok { + s.SpacialHash[cellId] = []SpacialCellObject{} + } + + cell := s.SpacialHash[cellId] + cell = append(cell, SpacialCellObject{ + Entity: id, + X: pos.X, + Y: pos.Y, + }) + s.SpacialHash[cellId] = cell + }) +} diff --git a/examples/balls/ecs/updateRenderPosition.go b/examples/balls/ecs/updateRenderPosition.go new file mode 100644 index 0000000..3d0c99f --- /dev/null +++ b/examples/balls/ecs/updateRenderPosition.go @@ -0,0 +1,41 @@ +package ecs + +import ( + "time" + + "github.com/unitoftime/ecs" + "github.com/unitoftime/ecs/system" +) + +const UpdateRenderPositionSystemName system.SystemName = "render::updatePosition" + +type UpdateRenderPositionSystem struct { + World *ecs.World +} + +func (s *UpdateRenderPositionSystem) GetName() system.SystemName { + return UpdateRenderPositionSystemName +} + +func (s *UpdateRenderPositionSystem) GetReadComponents() []ecs.Component { + return []ecs.Component{ecs.C(Position{})} +} + +func (s *UpdateRenderPositionSystem) GetWriteComponents() []ecs.Component { + return []ecs.Component{ecs.C(RenderPosition{})} +} + +func (s *UpdateRenderPositionSystem) RunRealtime(delta time.Duration) { + lerpMultiplier := delta.Seconds() * 10 + if lerpMultiplier >= 1 { + lerpMultiplier = 1 + } + + query := ecs.Query2[Position, RenderPosition](s.World) + query.MapId(func(id ecs.Id, pos *Position, rend *RenderPosition) { + xDif := pos.X - rend.X + rend.X = rend.X + xDif*lerpMultiplier + yDif := pos.Y - rend.Y + rend.Y = rend.Y + yDif*lerpMultiplier + }) +} diff --git a/examples/balls/main.go b/examples/balls/main.go new file mode 100644 index 0000000..4d7662b --- /dev/null +++ b/examples/balls/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "math/rand" + "time" + + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/unitoftime/ecs" + ballESC "github.com/unitoftime/ecs/examples/balls/ecs" + "github.com/unitoftime/ecs/system/group" +) + +func main() { + rl.InitWindow(500, 500, "ECS balls example") + defer rl.CloseWindow() + rl.SetTargetFPS(60) + + world := ecs.NewWorld() + + for i := 0; i < 200; i++ { + posX := rand.Float64() * 50 + posY := rand.Float64() * 50 + + id := world.NewId() + ecs.Write( + world, + id, + ecs.C(ballESC.Position{X: posX, Y: posY}), + ecs.C(ballESC.Velocity{X: rand.Float64()/2 - 0.5, Y: rand.Float64()/2 - 0.5}), + ecs.C(ballESC.RenderPosition{X: posX, Y: posY}), + ) + } + + componentsGuard := group.NewComponentsGuard() + + // Init physics + physics := group.NewFixedSystemGroup("physics", 50*time.Millisecond, componentsGuard) + physics.AddSystem(&ballESC.MovementSystem{World: world}) + sharedPhysicsSpacialHash := make(map[ballESC.IntVector2][]ballESC.SpacialCellObject) + physics.AddSystem(&ballESC.SpacialSystem{ + World: world, + SpacialHash: sharedPhysicsSpacialHash, + }) + physics.AddSystem(&ballESC.CollisionSystem{ + World: world, + SpacialHash: sharedPhysicsSpacialHash, + }) + physics.AddSystem(&ballESC.BoundarySystem{ + World: world, + }) + physics.Build() + physics.StartFixed() + defer physics.StopFixed() + + // Init render + render := group.NewRealtimeGroup("render", componentsGuard) + render.OnBeforeUpdate(func(delta time.Duration) { + rl.BeginDrawing() + rl.ClearBackground(rl.RayWhite) + }) + render.OnAfterUpdate(func(delta time.Duration) { + rl.DrawText("Collision!", 180, 225, 40, rl.Black) + rl.EndDrawing() + }) + render.AddSystem(&ballESC.UpdateRenderPositionSystem{World: world}) + render.AddSystem(&ballESC.RenderSystem{World: world}) + render.Build() + + for !rl.WindowShouldClose() { + render.RunRealtime(time.Duration(int64(float64(rl.GetFrameTime()) * float64(time.Second)))) + } +} diff --git a/examples/balls/result.png b/examples/balls/result.png new file mode 100644 index 0000000..fd35a35 Binary files /dev/null and b/examples/balls/result.png differ diff --git a/filter.go b/filter.go index 471c741..b0310c2 100644 --- a/filter.go +++ b/filter.go @@ -24,7 +24,7 @@ package ecs // With - Lets you add additional components that must be present // Without - Lets you add additional components that must not be present type Filter interface { - Filter([]componentId) []componentId + Filter([]ComponentId) []ComponentId } // type without struct { @@ -40,12 +40,12 @@ type Filter interface { // } type with struct { - comps []componentId + comps []ComponentId } // Creates a filter to ensure that entities have the specified components func With(comps ...any) with { - ids := make([]componentId, len(comps)) + ids := make([]ComponentId, len(comps)) for i := range comps { ids[i] = name(comps[i]) } @@ -54,17 +54,17 @@ func With(comps ...any) with { } } -func (w with) Filter(list []componentId) []componentId { +func (w with) Filter(list []ComponentId) []ComponentId { return append(list, w.comps...) } type optional struct { - comps []componentId + comps []ComponentId } // Creates a filter to make the query still iterate even if a specific component is missing, in which case you'll get nil if the component isn't there when accessed func Optional(comps ...any) optional { - ids := make([]componentId, len(comps)) + ids := make([]ComponentId, len(comps)) for i := range comps { ids[i] = name(comps[i]) } @@ -74,7 +74,7 @@ func Optional(comps ...any) optional { } } -func (f optional) Filter(list []componentId) []componentId { +func (f optional) Filter(list []ComponentId) []ComponentId { for i := 0; i < len(list); i++ { for j := range f.comps { if list[i] == f.comps[j] { @@ -92,12 +92,12 @@ func (f optional) Filter(list []componentId) []componentId { } type filterList struct { - comps []componentId + comps []ComponentId cachedArchetypeGeneration int // Denotes the world's archetype generation that was used to create the list of archIds. If the world has a new generation, we should probably regenerate archIds []archetypeId } -func newFilterList(comps []componentId, filters ...Filter) filterList { +func newFilterList(comps []ComponentId, filters ...Filter) filterList { for _, f := range filters { comps = f.Filter(comps) } diff --git a/go.mod b/go.mod index d20e92d..2774682 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/unitoftime/ecs go 1.18 -require github.com/unitoftime/cod v0.0.0-20230616173404-085cf4fe3918 +require ( + github.com/gen2brain/raylib-go/raylib v0.0.0-20231021231620-3f961a273f39 + github.com/unitoftime/cod v0.0.0-20230616173404-085cf4fe3918 + golang.org/x/sync v0.4.0 +) diff --git a/go.sum b/go.sum index 39353ec..c3e48c8 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/gen2brain/raylib-go/raylib v0.0.0-20231021231620-3f961a273f39 h1:Ngqy1X3DdHd7R09ea2O+C3vFgZDeU5gp0wDr6LWOBjc= +github.com/gen2brain/raylib-go/raylib v0.0.0-20231021231620-3f961a273f39/go.mod h1:AwtGA3aTtYdezNxEVbfchaLw/z+CuRDh2Mlxy0FbBro= github.com/unitoftime/cod v0.0.0-20230616173404-085cf4fe3918 h1:C1W4LPOwKCBq0cAzb81508ePBIfAR9ecP9GBR5zi/AI= github.com/unitoftime/cod v0.0.0-20230616173404-085cf4fe3918/go.mod h1:Iufibv9gn5GJb4Qzkf8e8xaXOV77OgkrB5kkBZTEN+M= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= diff --git a/internal/gen/main.go b/internal/gen/main.go index ba2146d..675e9c3 100644 --- a/internal/gen/main.go +++ b/internal/gen/main.go @@ -1,13 +1,12 @@ package main import ( + _ "embed" "os" "strings" - "text/template" - _ "embed" + "text/template" ) - //go:embed view.tgo var viewTemplate string @@ -35,6 +34,9 @@ func main() { } funcs := template.FuncMap{ "join": strings.Join, + "lower": func(val string) string { + return strings.ToLower(val) + }, "nils": func(n int) string { val := make([]string, 0) for i := 0; i < n; i++ { @@ -63,9 +65,23 @@ func main() { } return strings.Join(ret, ", ") }, + "parallelLambdaStructArgs": func(val []string) string { + ret := make([]string, len(val)) + for i := range val { + ret[i] = strings.ToLower(val[i]) + " []" + val[i] + } + return strings.Join(ret, "; ") + }, + "parallelLambdaArgsFromStruct": func(val []string) string { + ret := make([]string, len(val)) + for i := range val { + ret[i] = "param" + val[i] + } + return strings.Join(ret, ", ") + }, } - t := template.Must(template.New("ViewTemplate").Funcs(funcs).Parse(viewTemplate)) + t := template.Must(template.New("ViewTemplate").Funcs(funcs).Parse(viewTemplate)) viewFile, err := os.OpenFile("view_gen.go", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { @@ -73,5 +89,5 @@ func main() { } defer viewFile.Close() - t.Execute(viewFile, data) + t.Execute(viewFile, data) } diff --git a/internal/gen/view.tgo b/internal/gen/view.tgo index 9e7cbbb..b8bc223 100644 --- a/internal/gen/view.tgo +++ b/internal/gen/view.tgo @@ -1,5 +1,10 @@ package ecs +import ( + "sync" + "runtime" +) + // Warning: This is an autogenerated file. Do not modify!! {{range $i, $element := .Views}} @@ -176,6 +181,92 @@ func (v *View{{len $element}}[{{join $element ","}}]) MapId(lambda func(id Id, { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View{{len $element}}[{{join $element ","}}]) MapIdParallel(chunkSize int, lambda func(id Id, {{lambdaArgs $element}})) { + v.filter.regenerate(v.world) + + {{range $ii, $arg := $element}} + var slice{{$arg}} *componentSlice[{{$arg}}] + var comp{{$arg}} []{{$arg}} + {{end}} + + + workDone := &sync.WaitGroup{} + type workPackage struct{start int; end int; ids []Id; {{parallelLambdaStructArgs $element}}} + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + {{range $ii, $arg := $element}} + var param{{$arg}} *{{$arg}}{{end}} + + for i := newWork.start; i < newWork.end; i++ { + {{range $ii, $arg := $element}} + if newWork.{{lower $arg}} != nil { param{{$arg}} = &newWork.{{lower $arg}}[i]}{{end}} + + lambda(newWork.ids[i], {{parallelLambdaArgsFromStruct $element}}) + } + } + } + parallelLevel := runtime.NumCPU()*2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + + for _, archId := range v.filter.archIds { + {{range $ii, $arg := $element}} + slice{{$arg}}, _ = v.storage{{$arg}}.slice[archId]{{end}} + + lookup := v.world.engine.lookup[archId] + if lookup == nil { panic("LookupList is missing!") } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + {{range $ii, $arg := $element}} + comp{{$arg}} = nil + if slice{{$arg}} != nil { + comp{{$arg}} = slice{{$arg}}.comp + }{{end}} + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx - startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx+1, ids: ids, {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map func (v *View{{len $element}}[{{join $element ","}}]) MapSlices(lambda func(id []Id, {{sliceLambdaArgs $element}})) { v.filter.regenerate(v.world) diff --git a/name.go b/name.go index 0ea5170..f077390 100644 --- a/name.go +++ b/name.go @@ -1,16 +1,16 @@ package ecs import ( - "sync" "reflect" + "sync" ) var componentIdMutex sync.Mutex -var registeredComponents = make(map[reflect.Type]componentId, maxComponentId) -var invalidComponentId componentId = 0 -var componentRegistryCounter componentId = 1 +var registeredComponents = make(map[reflect.Type]ComponentId, maxComponentId) +var invalidComponentId ComponentId = 0 +var componentRegistryCounter ComponentId = 1 -func name(t any) componentId { +func name(t any) ComponentId { // Note: We have to lock here in case there are multiple worlds // TODO!! - This probably causes some performance penalty componentIdMutex.Lock() diff --git a/system.go b/system.go deleted file mode 100644 index 3e9e08e..0000000 --- a/system.go +++ /dev/null @@ -1,393 +0,0 @@ -package ecs - -import ( - "fmt" - "sync/atomic" - "time" - - "runtime" -) - -// Represents an individual system -type System struct { - Name string - Func func(dt time.Duration) -} - -// Create a new system. The system name will be automatically created based on the function name that calls this function -func NewSystem(lambda func(dt time.Duration)) System { - systemName := "UnknownSystemName" - - pc, _, _, ok := runtime.Caller(1) - if ok { - details := runtime.FuncForPC(pc) - systemName = details.Name() - } - - return System{ - Name: systemName, - Func: lambda, - } -} - -// type Initializer interface { -// initialize(*World) any -// } - -// func NewSystem1[A Initializer](world *World, lambda func(dt time.Duration, a A)) System { -// var a A -// aRes := a.initialize(world).(A) - -// return System{ -// Name: "TODO - Use Reflection, lambda function name?", -// Func: func(dt time.Duration) { -// // aPointer.Lock() -// // defer aPointer.Unlock() -// lambda(dt, aRes) -// }, -// } -// } - -// TODO - how to support filters? -// type Initializer interface { -// Initialize(*World) -// } - -// // Idea: Automatically created, Generic systems -// func NewSystem1[A Initializer](world *World, lambda func(dt time.Duration, a *A)) System { -// a := new(A) - -// (*a).Initialize(world) -// return System{ -// Name: "TODO - Use Reflection", -// Func: func(dt time.Duration) { -// // aPointer.Lock() -// // defer aPointer.Unlock() -// lambda(dt, a) -// }, -// } -// } - -// func NewSystem1[A any](world, lambda func(a A)) System { -// var a A -// aPointer := a.Initialize(world) -// return System{ -// Name: systemName, -// Func: func(dt time.Duration) { -// aPointer.Lock() -// defer aPointer.Unlock() -// lambda(dt, aPointer) -// }, -// } -// } - -// Executes the system once, returning the time taken. -// This is mostly used by the scheduler, but you can use it too. -func (s *System) Run(dt time.Duration) time.Duration { - // Note: Disable timing - // s.Func(dt) - // return 0 - - // fmt.Println(s.Name) // Spew - - start := time.Now() - s.Func(dt) - return time.Since(start) -} - -// A log of a system and the time it took to execute -type SystemLog struct { - Name string - Time time.Duration -} - -func (s *SystemLog) String() string { - return fmt.Sprintf("%s: %s", s.Name, s.Time) -} - -// // TODO - Just use an atomic here? -// type signal struct { -// mu sync.Mutex -// value bool -// } - -// func (s *signal) Set(val bool) { -// s.mu.Lock() -// s.value = val -// s.mu.Unlock() -// } - -// func (s *signal) Get() bool { -// s.mu.Lock() -// ret := s.value -// s.mu.Unlock() -// return ret -// } - -// Scheduler is a place to put your systems and have them run. -// There are two types of systems: Fixed time systems and dynamic time systems -// 1. Fixed time systems will execute on a fixed time step -// 2. Dynamic time systems will execute as quickly as they possibly can -// The scheduler may change in the future, but right now how it works is simple: -// Input: Execute input systems (Dynamic time systems) -// Physics: Execute physics systems (Fixed time systems) -// Render: Execute render systems (Dynamic time systems) -type Scheduler struct { - input, physics, render []System - sysLogBack, sysLogFront []SystemLog - sysLogBackFixed, sysLogFrontFixed []SystemLog - fixedTimeStep time.Duration - accumulator time.Duration - // gameSpeed int64 - quit atomic.Bool - pauseRender atomic.Bool - maxLoopCount int -} - -// Creates a scheduler -func NewScheduler() *Scheduler { - return &Scheduler{ - input: make([]System, 0), - physics: make([]System, 0), - render: make([]System, 0), - sysLogFront: make([]SystemLog, 0), - sysLogBack: make([]SystemLog, 0), - sysLogFrontFixed: make([]SystemLog, 0), - sysLogBackFixed: make([]SystemLog, 0), - fixedTimeStep: 16 * time.Millisecond, - accumulator: 0, - // gameSpeed: 1, - } -} - -// TODO make SetGameSpeed and SetFixedTimeStep thread safe. - -// Sets the rate at which time accumulates. Also, you want them to only change at the end of a frame, else you might get some inconsistencies. Just use a mutex and a single temporary variable -// func (s *Scheduler) SetGameSpeed(speed int64) { -// s.gameSpeed = speed -// } - -// Tells the scheduler to exit. Scheduler will finish executing its remaining tick before closing. -func (s *Scheduler) SetQuit(value bool) { - s.quit.Store(true) -} - -// Returns the quit value of the scheduler -func (s *Scheduler) Quit() bool { - return s.quit.Load() -} - -// Pauses the set of render systems (ie they will be skipped). -// Deprecated: This API is tentatitive -func (s *Scheduler) PauseRender(value bool) { - s.pauseRender.Store(value) -} - -// Sets the amount of time required before the fixed time systems will execute -func (s *Scheduler) SetFixedTimeStep(t time.Duration) { - s.fixedTimeStep = t -} - -// Adds a system to the list of input systems -func (s *Scheduler) AppendInput(systems ...System) { - s.input = append(s.input, systems...) -} - -// Adds a system to the list of physics systems -func (s *Scheduler) AppendPhysics(systems ...System) { - s.physics = append(s.physics, systems...) -} - -// Adds a system to the list of render systems -func (s *Scheduler) AppendRender(systems ...System) { - s.render = append(s.render, systems...) -} - -// Sets the accumulator maximum point so that if the accumulator gets way to big, we will reset it and continue on, dropping all physics ticks that would have been executed. This is useful in a runtime like WASM where the browser may not let us run as frequently as we may need (for example, when the tab is hidden or minimized). -// Note: This must be set before you call scheduler.Run() -// Note: The default value is 0, which will force every physics tick to run. I highly recommend setting this to something if you plan to build for WASM! -func (s *Scheduler) SetMaxPhysicsLoopCount(count int) { - s.maxLoopCount = count -} - -// Returns the front syslog so the user can analyze it. Note: This is only valid for the current frame, you should call this every frame if you use it! -func (s *Scheduler) Syslog() []SystemLog { - return s.sysLogFront -} - -// Returns the front syslog for fixed-dt systems only. Note: This is only valid for the current frame, you should call this every frame if you use it! -func (s *Scheduler) SyslogFixed() []SystemLog { - return s.sysLogFrontFixed -} - -// Returns an interpolation value which represents how close we are to the next fixed time step execution. Can be useful for interpolating dynamic time systems to the fixed time systems. I might rename this -func (s *Scheduler) GetRenderInterp() float64 { - return s.accumulator.Seconds() / s.fixedTimeStep.Seconds() -} - -// //Separates physics loop from render loop -// func (s *Scheduler) Run2() { -// var worldMu sync.Mutex - -// frameStart := time.Now() -// dt := s.fixedTimeStep -// // var accumulator time.Duration -// s.accumulator = 0 -// maxLoopCount := time.Duration(s.maxLoopCount) - -// physicsTicker := time.NewTicker(s.fixedTimeStep) -// defer physicsTicker.Stop() -// go func() { -// for { -// phyTime, more <-physicsTicker.C -// if !more { break } // Exit early, ticker channel is closed -// fmt.Println(phyTime) -// } -// } - -// for !s.quit.Get() { -// } -// } - -// Note: Would be nice to sleep or something to prevent spinning while we wait for work to do -// Could also separate the render loop from the physics loop (requires some thread safety in ECS) -func (s *Scheduler) Run() { - frameStart := time.Now() - dt := s.fixedTimeStep - // var accumulator time.Duration - s.accumulator = 0 - maxLoopCount := time.Duration(s.maxLoopCount) - - - // go func() { - // for { - // time.Sleep(s.fixedTimeStep) - // for _, sys := range s.physics { - // sysTime := sys.Run(s.fixedTimeStep) - - // s.sysLogBackFixed = append(s.sysLogBackFixed, SystemLog{ - // Name: sys.Name, - // Time: sysTime, - // }) - // } - // } - // }() - - - for !s.quit.Load() { - { - tmpSysLog := s.sysLogFront - s.sysLogFront = s.sysLogBack - s.sysLogBack = tmpSysLog - s.sysLogBack = s.sysLogBack[:0] - } - - // Input Systems - for _, sys := range s.input { - sysTime := sys.Run(dt) - - s.sysLogBack = append(s.sysLogBack, SystemLog{ - Name: sys.Name, - Time: sysTime, - }) - } - - if maxLoopCount > 0 { - if s.accumulator > (maxLoopCount * s.fixedTimeStep) { - s.accumulator = s.fixedTimeStep // Just run one loop - } - } - - // TODO - If we get a double run, then all are accumulated - if s.accumulator >= s.fixedTimeStep { - tmpSysLog := s.sysLogFrontFixed - s.sysLogFrontFixed = s.sysLogBackFixed - s.sysLogBackFixed = tmpSysLog - s.sysLogBackFixed = s.sysLogBackFixed[:0] - } - // Physics Systems - for s.accumulator >= s.fixedTimeStep { - for _, sys := range s.physics { - sysTime := sys.Run(s.fixedTimeStep) - - s.sysLogBackFixed = append(s.sysLogBackFixed, SystemLog{ - Name: sys.Name, - Time: sysTime, - }) - } - s.accumulator -= s.fixedTimeStep - } - - // Render Systems - if !s.pauseRender.Load() { - for _, sys := range s.render { - sysTime := sys.Run(dt) - - s.sysLogBack = append(s.sysLogBack, SystemLog{ - Name: sys.Name, - Time: sysTime, - }) - } - } - - // Edge case for schedules only fixed time steps - if len(s.input) == 0 && len(s.render) == 0 { - // Note: This is guaranteed to be positive because the physics execution loops until the accumulator is less than fixedtimestep - time.Sleep(s.fixedTimeStep - s.accumulator) - } - - // Capture Frame time - now := time.Now() - dt = now.Sub(frameStart) - frameStart = now - - // dt = time.Since(frameStart) - // frameStart = time.Now() - - s.accumulator += dt - - // scaledDt := dt.Nanoseconds() * s.gameSpeed - // s.accumulator += time.Duration(scaledDt) - - // s.accumulator += 16667 * time.Microsecond - // fmt.Println(dt, s.accumulator) - } -} - -// // TODO! - Helpful starting point of commands? Maybe pass a commandlist to systems with dt as they execute. Maybe wrap dt and commandlist inside some general thing that gets passed to systems -// type Command struct { -// Id ecs.Id // If Id is ecs.InvalidEntity, we will spawn this as a new entity -// Entity *ecs.Entity -// } - -// type CommandList struct { -// world *ecs.World -// list []Command -// } -// func NewCommandList(world *ecs.World) *CommandList { -// return &CommandList{ -// world: world, -// list: make([]Command, 0), -// } -// } - -// func (l *CommandList) Add(c Command) { -// l.list = append(l.list, c) -// } - -// func (l *CommandList) Map(lambda func(c *Command)) { -// for i := range l.list { -// lambda(&l.list[i]) -// } -// } - -// func (l *CommandList) Execute() { -// for _, c := range l.list { -// id := c.Id -// if id == ecs.InvalidEntity { -// id = l.world.NewId() -// } - -// ecs.WriteEntity(l.world, id, c.Entity) -// } -// } diff --git a/system/group/componentGuard.go b/system/group/componentGuard.go new file mode 100644 index 0000000..cd73360 --- /dev/null +++ b/system/group/componentGuard.go @@ -0,0 +1,160 @@ +package group + +import ( + "fmt" + "sort" + "sync" + + "github.com/unitoftime/ecs" + "github.com/unitoftime/ecs/system" +) + +type componentSystemRequiredAccess struct { + componentId ecs.ComponentId + read bool +} + +// User by groupd to prevent race conditions for systems which operate on same compoennts +type componentsGuard struct { + registeredSystems map[system.SystemName]struct{} + components []ecs.ComponentId + componentsLocks map[ecs.ComponentId]*sync.RWMutex + requiredSystemsAccesses map[system.SystemName][]componentSystemRequiredAccess + exclusiveSystemsAccess map[system.SystemName]struct{} + /* Lock to use between exclusive systems. We need it because not all the components can be knows to the guard */ + exclusiveSystemsLock *sync.Mutex +} + +type ComponentsGuard interface { + InitForGroup(grp Group) + Lock(s system.System) + Release(s system.System) +} + +func NewComponentsGuard() ComponentsGuard { + return &componentsGuard{ + registeredSystems: map[system.SystemName]struct{}{}, + components: []ecs.ComponentId{}, + componentsLocks: make(map[ecs.ComponentId]*sync.RWMutex), + requiredSystemsAccesses: map[system.SystemName][]componentSystemRequiredAccess{}, + exclusiveSystemsAccess: map[system.SystemName]struct{}{}, + exclusiveSystemsLock: &sync.Mutex{}, + } +} + +func (g *componentsGuard) InitForGroup(grp Group) { + systems := grp.GetAllSystems() + + for _, s := range systems { + if _, ok := g.registeredSystems[s.GetName()]; ok { + panic(fmt.Sprintf("System with name %s is already registered in other gorup. System must have only one group.", s.GetName())) + } + g.registeredSystems[s.GetName()] = struct{}{} + + readComponents, isReadComponents := s.(system.ReadComponentsSystem) + writeComponents, isWriteComponents := s.(system.WriteComponentsSystem) + + if isReadComponents { + for _, component := range readComponents.GetReadComponents() { + componentsLock, ok := g.componentsLocks[component.Id()] + if !ok { + componentsLock = &sync.RWMutex{} + } + g.componentsLocks[component.Id()] = componentsLock + + requiredAccesses, ok := g.requiredSystemsAccesses[s.GetName()] + if !ok { + requiredAccesses = []componentSystemRequiredAccess{} + } + + requiredAccesses = append(requiredAccesses, componentSystemRequiredAccess{ + componentId: component.Id(), + read: true, + }) + g.requiredSystemsAccesses[s.GetName()] = requiredAccesses + } + } + + if isWriteComponents { + for _, component := range writeComponents.GetWriteComponents() { + componentsLock, ok := g.componentsLocks[component.Id()] + if !ok { + componentsLock = &sync.RWMutex{} + } + g.componentsLocks[component.Id()] = componentsLock + + requiredAccesses, ok := g.requiredSystemsAccesses[s.GetName()] + if !ok { + requiredAccesses = []componentSystemRequiredAccess{} + } + + requiredAccesses = append(requiredAccesses, componentSystemRequiredAccess{ + componentId: component.Id(), + read: false, + }) + g.requiredSystemsAccesses[s.GetName()] = requiredAccesses + } + } + + if !isReadComponents && !isWriteComponents { + g.exclusiveSystemsAccess[s.GetName()] = struct{}{} + } + } + + g.components = []ecs.ComponentId{} + for component := range g.componentsLocks { + g.components = append(g.components, component) + } + sort.Slice(g.components, func(i, j int) bool { + return g.components[i] < g.components[j] + }) + + // By sorttin access lists and running locks in order we can ensure that there will be no deadlocks + // TODO: There can be better way to do this + for k, requiredAccesses := range g.requiredSystemsAccesses { + sort.Slice(requiredAccesses, func(i, j int) bool { + return requiredAccesses[i].componentId < requiredAccesses[j].componentId + }) + g.requiredSystemsAccesses[k] = requiredAccesses + } +} + +func (g *componentsGuard) Lock(s system.System) { + if _, ok := g.exclusiveSystemsAccess[s.GetName()]; ok { + // Exclusive system. Get all the write locks to all the components + for _, component := range g.components { + g.componentsLocks[component].Lock() + } + + g.exclusiveSystemsLock.Lock() + } else { + // Just lock for the components that are required by this system + for _, requiredAccess := range g.requiredSystemsAccesses[s.GetName()] { + if requiredAccess.read { + g.componentsLocks[requiredAccess.componentId].RLock() + } else { + g.componentsLocks[requiredAccess.componentId].Lock() + } + } + } +} + +func (g *componentsGuard) Release(s system.System) { + if _, ok := g.exclusiveSystemsAccess[s.GetName()]; ok { + g.exclusiveSystemsLock.Unlock() + + // Exclusive system. Release all the write locks to all the components + for _, component := range g.components { + g.componentsLocks[component].Unlock() + } + } else { + // Just release locks for the components that are required by this system + for _, requiredAccess := range g.requiredSystemsAccesses[s.GetName()] { + if requiredAccess.read { + g.componentsLocks[requiredAccess.componentId].RUnlock() + } else { + g.componentsLocks[requiredAccess.componentId].Unlock() + } + } + } +} diff --git a/system/group/error.go b/system/group/error.go new file mode 100644 index 0000000..c250b2a --- /dev/null +++ b/system/group/error.go @@ -0,0 +1,30 @@ +package group + +import ( + s "github.com/unitoftime/ecs/system" +) + +type GroupError interface { + error + + SystemName() s.SystemName + StackTrace() string +} + +type groupError struct { + innerError error + systemName s.SystemName + trace string +} + +func (e *groupError) Error() string { + return e.innerError.Error() +} + +func (e *groupError) SystemName() s.SystemName { + return e.systemName +} + +func (e *groupError) StackTrace() string { + return e.trace +} diff --git a/system/group/fixed.go b/system/group/fixed.go new file mode 100644 index 0000000..93ce22e --- /dev/null +++ b/system/group/fixed.go @@ -0,0 +1,188 @@ +package group + +import ( + "context" + "sync" + "time" + + s "github.com/unitoftime/ecs/system" +) + +type FixedUpdateEventHandler func(delta time.Duration) + +type FixedSystemGroup interface { + Group + + StartFixed() + StopFixed() + + OnBeforeUpdate(handler FixedUpdateEventHandler) + OnAfterUpdate(handler FixedUpdateEventHandler) +} + +type fixedSystemGroup struct { + group + + fixedTimeStep time.Duration + + runnerContext *context.Context + runnerDone context.CancelFunc + runnerDoneWait sync.WaitGroup + + onBeforeUpdateHandlers []FixedUpdateEventHandler + onAfterUpdateHandlers []FixedUpdateEventHandler +} + +func NewFixedSystemGroup(name string, fixedTimeStep time.Duration, componentsGuard ComponentsGuard) FixedSystemGroup { + return &fixedSystemGroup{ + group: newGroup(name, componentsGuard), + fixedTimeStep: fixedTimeStep, + runnerContext: nil, + runnerDoneWait: sync.WaitGroup{}, + onBeforeUpdateHandlers: []FixedUpdateEventHandler{}, + onAfterUpdateHandlers: []FixedUpdateEventHandler{}, + } +} + +func (g *fixedSystemGroup) runner(ctx context.Context) { + defer g.runnerDoneWait.Done() + lastRunTime := time.Now() + + // Handle panics of update handlers + defer func() { + if err := recover(); err != nil { + g.notifyError(err.(error), nil) + } + }() + + for { + timeToWait := g.fixedTimeStep.Nanoseconds() + time.Now().UnixNano() - lastRunTime.UnixNano() + if timeToWait > 0 { + select { + case <-ctx.Done(): + return + case <-time.After(time.Nanosecond * time.Duration(timeToWait)): + } + } else { + select { + case <-ctx.Done(): + return + default: + } + } + + newRunTime := time.Now() + delta := time.Duration(newRunTime.UnixNano() - lastRunTime.UnixNano()) + lastRunTime = newRunTime + + beforeUpdateHandlersStartedTime := time.Now() + for _, handler := range g.onBeforeUpdateHandlers { + handler(delta) + } + beforeUpdateHandlersEndedTime := time.Now() + + g.orderGuard.Reset() + + systemsUpdatesStatistics := []SystemStatistics{} + systemsUpdatesStatisticsLock := sync.Mutex{} + + systemsCompleted := sync.WaitGroup{} + for _, system := range g.systems { + systemsCompleted.Add(1) + + go func(runnedSystem s.FixedSystem) { + // Handle panics of the running system + defer func() { + if err := recover(); err != nil { + g.notifyError(err.(error), runnedSystem.(s.System)) + } + }() + + defer systemsCompleted.Done() + + waitingForOrderStartedTime := time.Now() + g.orderGuard.Lock(runnedSystem.(s.System)) + defer g.orderGuard.Release(runnedSystem.(s.System)) + waitingForComponentsAccessTime := time.Now() + g.componentsGuard.Lock(runnedSystem.(s.System)) + defer g.componentsGuard.Release(runnedSystem.(s.System)) + + executionStartedTime := time.Now() + runnedSystem.RunFixed(delta) + executionEndedTime := time.Now() + + systemsUpdatesStatisticsLock.Lock() + defer systemsUpdatesStatisticsLock.Unlock() + systemsUpdatesStatistics = append(systemsUpdatesStatistics, SystemStatistics{ + Name: runnedSystem.(s.System).GetName(), + WaitingForOrderStarted: waitingForOrderStartedTime, + WaitingForComponentsAccessStarted: waitingForComponentsAccessTime, + ExecutionStarted: executionStartedTime, + ExecutionEnded: executionEndedTime, + }) + }(system.(s.FixedSystem)) + } + + systemsCompleted.Wait() + + afterUpdateHandlersStartedTime := time.Now() + for _, handler := range g.onAfterUpdateHandlers { + handler(delta) + } + afterUpdateHandlersEndedTime := time.Now() + + g.statistics.pushUpdate(UpdateStatistics{ + BeforeUpdateHandlersStarted: beforeUpdateHandlersStartedTime, + BeforeUpdateHandlersEnded: beforeUpdateHandlersEndedTime, + SystemsStatistics: systemsUpdatesStatistics, + AfterUpdateHandlersStarted: afterUpdateHandlersStartedTime, + AfterUpdateHandlersEnded: afterUpdateHandlersEndedTime, + }) + } +} + +func (g *fixedSystemGroup) StartFixed() { + if g.runnerContext != nil { + return + } + + for _, startHandler := range g.onStartHandlers { + startHandler() + } + + ctx, cancell := context.WithCancel(context.Background()) + g.runnerContext = &ctx + g.runnerDone = cancell + g.runnerDoneWait.Add(1) + go g.runner(ctx) +} +func (g *fixedSystemGroup) StopFixed() { + if g.runnerContext == nil { + return + } + + g.runnerDone() + g.runnerContext = nil + g.runnerDoneWait.Wait() + + for _, stopHandler := range g.onStopHandlers { + stopHandler() + } +} + +func (g *fixedSystemGroup) AddSystem(system s.System) { + _, isRealtime := system.(s.FixedSystem) + if !isRealtime { + panic("system must implement FixedSystem interface") + } + + g.group.AddSystem(system) +} + +func (g *fixedSystemGroup) OnBeforeUpdate(handler FixedUpdateEventHandler) { + g.onBeforeUpdateHandlers = append(g.onBeforeUpdateHandlers, handler) +} + +func (g *fixedSystemGroup) OnAfterUpdate(handler FixedUpdateEventHandler) { + g.onAfterUpdateHandlers = append(g.onAfterUpdateHandlers, handler) +} diff --git a/system/group/group.go b/system/group/group.go new file mode 100644 index 0000000..b5443ec --- /dev/null +++ b/system/group/group.go @@ -0,0 +1,146 @@ +package group + +import ( + "fmt" + "log/slog" + "runtime/debug" + "sync" + + s "github.com/unitoftime/ecs/system" +) + +type GroupErrorHandler func(err GroupError) + +type Group interface { + GetName() string + + Build() + + GetSystem(name s.SystemName) s.System + GetAllSystems() []s.System + AddSystem(system s.System) + + SetLogger(logger *slog.Logger) + + // Run provided function before first update + OnStart(handler func()) + // Run provided function after group was stopped + OnStop(handler func()) + + OnError(handler GroupErrorHandler) + + GetStatistics() Statistics +} + +type group struct { + name string + + systems []s.System + systemsMap map[s.SystemName]s.System + + orderGuard orderGuard + componentsGuard ComponentsGuard + logger *slog.Logger + + onStartHandlers []func() + onStopHandlers []func() + onErrorHandlers []GroupErrorHandler + + statistics statistics +} + +func newGroup(name string, componentsGuard ComponentsGuard) group { + return group{ + name: name, + systems: []s.System{}, + systemsMap: map[s.SystemName]s.System{}, + componentsGuard: componentsGuard, + logger: slog.Default().With(slog.String("systemsGroup", name)), + onStartHandlers: []func(){}, + onStopHandlers: []func(){}, + onErrorHandlers: []GroupErrorHandler{}, + statistics: statistics{maxUpdatesCount: 10, updates: []*UpdateStatistics{}, updatesLock: &sync.RWMutex{}}, + } +} + +func (g *group) Build() { + g.orderGuard = newOrderGuard(g.systems) + g.componentsGuard.InitForGroup(g) + + // TODO: Check for stable system execution flow if required. For example two systems need write access to the same component, but there is no order established between them. That way system that will accuire lock firts, will modify resources first. It can be a problem for games where stable simulations are required. +} + +func (g *group) GetName() string { + return g.name +} + +func (g *group) GetSystem(name s.SystemName) s.System { + if system, ok := g.systemsMap[name]; ok { + return system + } + return nil +} + +func (g *group) GetAllSystems() []s.System { + return g.systems +} + +func (g *group) AddSystem(system s.System) { + _, isFixed := system.(s.FixedSystem) + _, isRealtime := system.(s.RealtimeSystem) + _, isStep := system.(s.StepSystem) + if !isFixed && !isRealtime && !isStep { + panic("system must implement FixedSystem, RealtimeSystem or StepSystem interface") + } + + if _, ok := g.systemsMap[system.GetName()]; ok { + panic(fmt.Sprintf("system with name [%s] already exist", system.GetName())) + } + g.systemsMap[system.GetName()] = system + + g.systems = append(g.systems, system) +} + +func (g *group) SetLogger(logger *slog.Logger) { + g.logger = logger.With(slog.String("systemsGroup", g.name)) +} + +func (g *group) notifyError(err error, system s.System) { + systemName := s.SystemName("") + if system != nil { + systemName = system.GetName() + } + + stackTrace := string(debug.Stack()) + + g.logger.Error("Error while running system", "error", err, "trace", stackTrace, "system", string(systemName)) + for _, handler := range g.onErrorHandlers { + // Handle handle panic to be shure that all handlers will be executed andprint trace of of the error + defer func() { + if herr := recover(); herr != nil { + g.logger.Error("Panic while trying to execute panic handler", "error", err, "trace", stackTrace, "system", string(systemName), "handlerError", herr, "handlerTrace", string(debug.Stack())) + } + }() + + handler(&groupError{ + innerError: err, + systemName: systemName, + trace: stackTrace, + }) + } +} + +func (g *group) OnStart(handler func()) { + g.onStartHandlers = append(g.onStartHandlers, handler) +} +func (g *group) OnStop(handler func()) { + g.onStopHandlers = append(g.onStopHandlers, handler) +} + +func (g *group) OnError(handler GroupErrorHandler) { + g.onErrorHandlers = append(g.onErrorHandlers, handler) +} + +func (g *group) GetStatistics() Statistics { + return &g.statistics +} diff --git a/system/group/orderGuard.go b/system/group/orderGuard.go new file mode 100644 index 0000000..d9a973b --- /dev/null +++ b/system/group/orderGuard.go @@ -0,0 +1,87 @@ +package group + +import ( + "sync" + + "github.com/unitoftime/ecs/system" +) + +type orderGuard struct { + /* Waiter for each system to wait for all the systems that must be runned before this system */ + runAfterWait map[system.SystemName]*sync.WaitGroup + /* After system run, it releases waiter that are waiting for its completion */ + runBeforeReleases map[system.SystemName][]system.SystemName + /* For how much systems, system must wait before start. Used in reset to reset the `runAfterWait` waiters */ + initialWaitersCount map[system.SystemName]int +} + +func newOrderGuard(systems []system.System) orderGuard { + var runBeforeReleases map[system.SystemName][]system.SystemName = map[system.SystemName][]system.SystemName{} + var initialWaitersCount map[system.SystemName]int = map[system.SystemName]int{} + + for _, s := range systems { + runAfter, isRunAfter := s.(system.RunAfterSystem) + runBefore, isRunBefore := s.(system.RunBeforeSystem) + + if isRunAfter { + for _, runAfterSystem := range runAfter.GetRunAfter() { + beforeReleasesList, ok := runBeforeReleases[runAfterSystem] + if !ok { + beforeReleasesList = []system.SystemName{} + } + + beforeReleasesList = append(beforeReleasesList, s.GetName()) + runBeforeReleases[runAfterSystem] = beforeReleasesList + } + + initialWaitersCount[s.GetName()] = len(runAfter.GetRunAfter()) + } + + if isRunBefore { + for _, runBeforeSystem := range runBefore.GetRunBefore() { + beforeReleasesList, ok := runBeforeReleases[s.GetName()] + if !ok { + beforeReleasesList = []system.SystemName{} + } + + beforeReleasesList = append(beforeReleasesList, runBeforeSystem) + runBeforeReleases[s.GetName()] = beforeReleasesList + + count, ok := initialWaitersCount[runBeforeSystem] + if !ok { + count = 0 + } + initialWaitersCount[runBeforeSystem] = count + 1 + } + } + } + + return orderGuard{ + runAfterWait: map[system.SystemName]*sync.WaitGroup{}, + runBeforeReleases: runBeforeReleases, + initialWaitersCount: initialWaitersCount, + } +} + +func (g *orderGuard) Reset() { + for systemName, initialCount := range g.initialWaitersCount { + if _, ok := g.runAfterWait[systemName]; !ok { + g.runAfterWait[systemName] = &sync.WaitGroup{} + } + g.runAfterWait[systemName].Add(initialCount) + } +} + +func (g *orderGuard) Lock(s system.System) { + if waiter, ok := g.runAfterWait[s.GetName()]; ok { + waiter.Wait() + } +} + +func (g *orderGuard) Release(s system.System) { + if releases, ok := g.runBeforeReleases[s.GetName()]; ok { + for _, release := range releases { + g.runAfterWait[release].Done() + } + } +} diff --git a/system/group/realtime.go b/system/group/realtime.go new file mode 100644 index 0000000..14bf562 --- /dev/null +++ b/system/group/realtime.go @@ -0,0 +1,180 @@ +package group + +import ( + "context" + "sync" + "time" + + s "github.com/unitoftime/ecs/system" +) + +type RealtimeUpdateEventHandler func(delta time.Duration) + +type RealtimeGroup interface { + Group + + RunRealtime(delta time.Duration) + StartRealtime() + StopRealtime() + + OnBeforeUpdate(handler RealtimeUpdateEventHandler) + OnAfterUpdate(handler RealtimeUpdateEventHandler) +} + +type realtimeSystemGroup struct { + group + + runnerContext *context.Context + runnerDone context.CancelFunc + runnerDoneWait sync.WaitGroup + + onBeforeUpdateHandlers []RealtimeUpdateEventHandler + onAfterUpdateHandlers []RealtimeUpdateEventHandler +} + +func NewRealtimeGroup(name string, componentsGuard ComponentsGuard) RealtimeGroup { + return &realtimeSystemGroup{ + group: newGroup(name, componentsGuard), + runnerContext: nil, + runnerDoneWait: sync.WaitGroup{}, + onBeforeUpdateHandlers: []RealtimeUpdateEventHandler{}, + onAfterUpdateHandlers: []RealtimeUpdateEventHandler{}, + } +} + +func (g *realtimeSystemGroup) RunRealtime(delta time.Duration) { + beforeUpdateHandlersStartedTime := time.Now() + for _, handler := range g.onBeforeUpdateHandlers { + handler(delta) + } + beforeUpdateHandlersEndedTime := time.Now() + + g.orderGuard.Reset() + + systemsUpdatesStatistics := []SystemStatistics{} + systemsUpdatesStatisticsLock := sync.Mutex{} + + systemsCompleted := sync.WaitGroup{} + for _, system := range g.systems { + systemsCompleted.Add(1) + + go func(runnedSystem s.RealtimeSystem) { + // Handle panics of the running system + defer func() { + if err := recover(); err != nil { + g.notifyError(err.(error), runnedSystem.(s.System)) + } + }() + + defer systemsCompleted.Done() + + waitingForOrderStartedTime := time.Now() + g.orderGuard.Lock(runnedSystem.(s.System)) + defer g.orderGuard.Release(runnedSystem.(s.System)) + waitingForComponentsAccessTime := time.Now() + g.componentsGuard.Lock(runnedSystem.(s.System)) + defer g.componentsGuard.Release(runnedSystem.(s.System)) + + executionStartedTime := time.Now() + runnedSystem.RunRealtime(delta) + executionEndedTime := time.Now() + + systemsUpdatesStatisticsLock.Lock() + defer systemsUpdatesStatisticsLock.Unlock() + systemsUpdatesStatistics = append(systemsUpdatesStatistics, SystemStatistics{ + Name: runnedSystem.(s.System).GetName(), + WaitingForOrderStarted: waitingForOrderStartedTime, + WaitingForComponentsAccessStarted: waitingForComponentsAccessTime, + ExecutionStarted: executionStartedTime, + ExecutionEnded: executionEndedTime, + }) + }(system.(s.RealtimeSystem)) + } + + systemsCompleted.Wait() + + afterUpdateHandlersStartedTime := time.Now() + for _, handler := range g.onAfterUpdateHandlers { + handler(delta) + } + afterUpdateHandlersEndedTime := time.Now() + + g.statistics.pushUpdate(UpdateStatistics{ + BeforeUpdateHandlersStarted: beforeUpdateHandlersStartedTime, + BeforeUpdateHandlersEnded: beforeUpdateHandlersEndedTime, + SystemsStatistics: systemsUpdatesStatistics, + AfterUpdateHandlersStarted: afterUpdateHandlersStartedTime, + AfterUpdateHandlersEnded: afterUpdateHandlersEndedTime, + }) +} + +func (g *realtimeSystemGroup) runner(ctx context.Context) { + defer g.runnerDoneWait.Done() + lastRunTime := time.Now() + + // Handle panics of update handlers + defer func() { + if err := recover(); err != nil { + g.notifyError(err.(error), nil) + } + }() + + for { + select { + case <-ctx.Done(): + return + default: + } + newRunTime := time.Now() + delta := time.Duration(newRunTime.UnixNano() - lastRunTime.UnixNano()) + lastRunTime = newRunTime + + g.RunRealtime(delta) + } +} + +func (g *realtimeSystemGroup) StartRealtime() { + if g.runnerContext != nil { + return + } + + for _, startHandler := range g.onStartHandlers { + startHandler() + } + + ctx, cancell := context.WithCancel(context.Background()) + g.runnerContext = &ctx + g.runnerDone = cancell + g.runnerDoneWait.Add(1) + go g.runner(ctx) +} +func (g *realtimeSystemGroup) StopRealtime() { + if g.runnerContext == nil { + return + } + + g.runnerDone() + g.runnerContext = nil + g.runnerDoneWait.Wait() + + for _, stopHandler := range g.onStopHandlers { + stopHandler() + } +} + +func (g *realtimeSystemGroup) AddSystem(system s.System) { + _, isRealtime := system.(s.RealtimeSystem) + if !isRealtime { + panic("system must implement RealtimeSystem interface") + } + + g.group.AddSystem(system) +} + +func (g *realtimeSystemGroup) OnBeforeUpdate(handler RealtimeUpdateEventHandler) { + g.onBeforeUpdateHandlers = append(g.onBeforeUpdateHandlers, handler) +} + +func (g *realtimeSystemGroup) OnAfterUpdate(handler RealtimeUpdateEventHandler) { + g.onAfterUpdateHandlers = append(g.onAfterUpdateHandlers, handler) +} diff --git a/system/group/statistics.go b/system/group/statistics.go new file mode 100644 index 0000000..7a64dc1 --- /dev/null +++ b/system/group/statistics.go @@ -0,0 +1,74 @@ +package group + +import ( + "sync" + "time" + + "github.com/unitoftime/ecs/system" +) + +type SystemStatistics struct { + Name system.SystemName + + WaitingForOrderStarted time.Time + WaitingForComponentsAccessStarted time.Time + ExecutionStarted time.Time + ExecutionEnded time.Time +} + +func (s *SystemStatistics) GetWaitingForOrderTime() time.Duration { + return time.Duration(s.WaitingForComponentsAccessStarted.Nanosecond() - s.WaitingForOrderStarted.Nanosecond()) +} + +func (s *SystemStatistics) GetWaitingForComponentsAccessTime() time.Duration { + return time.Duration(s.ExecutionStarted.Nanosecond() - s.WaitingForComponentsAccessStarted.Nanosecond()) +} + +func (s *SystemStatistics) GetExecutionTime() time.Duration { + return time.Duration(s.ExecutionEnded.Nanosecond() - s.ExecutionStarted.Nanosecond()) +} + +type UpdateStatistics struct { + BeforeUpdateHandlersStarted time.Time + BeforeUpdateHandlersEnded time.Time + + SystemsStatistics []SystemStatistics + + AfterUpdateHandlersStarted time.Time + AfterUpdateHandlersEnded time.Time +} + +type Statistics interface { + GetUpdates() []*UpdateStatistics +} + +type statistics struct { + maxUpdatesCount int + updates []*UpdateStatistics + updatesLock *sync.RWMutex +} + +func (s *statistics) GetUpdates() []*UpdateStatistics { + s.updatesLock.RLock() + defer s.updatesLock.RUnlock() + + updates := make([]*UpdateStatistics, len(s.updates)) + copy(updates, s.updates) + + return updates +} + +func (s *statistics) pushUpdate(update UpdateStatistics) { + s.updatesLock.Lock() + defer s.updatesLock.Unlock() + + s.updates = append(s.updates, &update) + if len(s.updates) > s.maxUpdatesCount { + updates := make([]*UpdateStatistics, len(s.updates)-1) + copy(updates, s.updates[1:]) + s.updates = updates + } +} + +// TODO: add statistics aggregation for longer periods of time +// TODO: enable/disable statistics diff --git a/system/group/step.go b/system/group/step.go new file mode 100644 index 0000000..235e4ab --- /dev/null +++ b/system/group/step.go @@ -0,0 +1,192 @@ +package group + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "golang.org/x/sync/semaphore" + + s "github.com/unitoftime/ecs/system" +) + +type StepEventHandler func(step int32) + +type StepGroup interface { + Group + + StartStep() + StopStep() + NextStep() + + OnBeforeStep(handler StepEventHandler) + OnAfterStep(handler StepEventHandler) +} + +type stepSystemGroup struct { + group + + stepLock *semaphore.Weighted + currentStep atomic.Int32 + targetStep atomic.Int32 + runnerContext *context.Context + runnerDone context.CancelFunc + runnerDoneWait sync.WaitGroup + + onBeforeStepHandlers []StepEventHandler + onAfterStepHandlers []StepEventHandler +} + +func NewStepGroup(name string, componentsGuard ComponentsGuard) StepGroup { + stepLock := semaphore.NewWeighted(9223372036854775807) + err := stepLock.Acquire(context.Background(), 9223372036854775807) + if err != nil { + panic(err) + } + + return &stepSystemGroup{ + group: newGroup(name, componentsGuard), + stepLock: stepLock, + currentStep: atomic.Int32{}, + targetStep: atomic.Int32{}, + runnerContext: nil, + runnerDoneWait: sync.WaitGroup{}, + onBeforeStepHandlers: []StepEventHandler{}, + onAfterStepHandlers: []StepEventHandler{}, + } +} + +func (g *stepSystemGroup) runner(ctx context.Context) { + defer g.runnerDoneWait.Done() + + // Handle panics of update handlers + defer func() { + if err := recover(); err != nil { + g.notifyError(err.(error), nil) + } + }() + + for { + select { + case <-ctx.Done(): + return + default: + g.stepLock.Acquire(ctx, 1) + g.currentStep.Add(1) + } + + beforeUpdateHandlersStartedTime := time.Now() + for _, handler := range g.onBeforeStepHandlers { + handler(g.currentStep.Load()) + } + beforeUpdateHandlersEndedTime := time.Now() + + g.orderGuard.Reset() + + systemsUpdatesStatistics := []SystemStatistics{} + systemsUpdatesStatisticsLock := sync.Mutex{} + + systemsCompleted := sync.WaitGroup{} + for _, system := range g.systems { + systemsCompleted.Add(1) + + go func(runnedSystem s.StepSystem) { + // Handle panics of the running system + defer func() { + if err := recover(); err != nil { + g.notifyError(err.(error), runnedSystem.(s.System)) + } + }() + + defer systemsCompleted.Done() + + waitingForOrderStartedTime := time.Now() + g.orderGuard.Lock(runnedSystem.(s.System)) + defer g.orderGuard.Release(runnedSystem.(s.System)) + waitingForComponentsAccessTime := time.Now() + g.componentsGuard.Lock(runnedSystem.(s.System)) + defer g.componentsGuard.Release(runnedSystem.(s.System)) + + executionStartedTime := time.Now() + runnedSystem.RunStep(g.currentStep.Load()) + executionEndedTime := time.Now() + + systemsUpdatesStatisticsLock.Lock() + defer systemsUpdatesStatisticsLock.Unlock() + systemsUpdatesStatistics = append(systemsUpdatesStatistics, SystemStatistics{ + Name: runnedSystem.(s.System).GetName(), + WaitingForOrderStarted: waitingForOrderStartedTime, + WaitingForComponentsAccessStarted: waitingForComponentsAccessTime, + ExecutionStarted: executionStartedTime, + ExecutionEnded: executionEndedTime, + }) + }(system.(s.StepSystem)) + } + + systemsCompleted.Wait() + + afterUpdateHandlersStartedTime := time.Now() + for _, handler := range g.onAfterStepHandlers { + handler(g.currentStep.Load()) + } + afterUpdateHandlersEndedTime := time.Now() + + g.statistics.pushUpdate(UpdateStatistics{ + BeforeUpdateHandlersStarted: beforeUpdateHandlersStartedTime, + BeforeUpdateHandlersEnded: beforeUpdateHandlersEndedTime, + SystemsStatistics: systemsUpdatesStatistics, + AfterUpdateHandlersStarted: afterUpdateHandlersStartedTime, + AfterUpdateHandlersEnded: afterUpdateHandlersEndedTime, + }) + } +} + +func (g *stepSystemGroup) StartStep() { + if g.runnerContext != nil { + return + } + + for _, startHandler := range g.onStartHandlers { + startHandler() + } + + ctx, cancell := context.WithCancel(context.Background()) + g.runnerContext = &ctx + g.runnerDone = cancell + g.runnerDoneWait.Add(1) + go g.runner(ctx) +} +func (g *stepSystemGroup) StopStep() { + if g.runnerContext == nil { + return + } + g.runnerDone() + g.runnerContext = nil + g.runnerDoneWait.Wait() + + for _, stopHandler := range g.onStopHandlers { + stopHandler() + } +} +func (g *stepSystemGroup) NextStep() { + g.stepLock.Release(1) + g.targetStep.Add(1) +} + +func (g *stepSystemGroup) AddSystem(system s.System) { + _, isRealtime := system.(s.StepSystem) + if !isRealtime { + panic("system must implement StepSystem interface") + } + + g.group.AddSystem(system) +} + +func (g *stepSystemGroup) OnBeforeStep(handler StepEventHandler) { + g.onBeforeStepHandlers = append(g.onBeforeStepHandlers, handler) +} + +func (g *stepSystemGroup) OnAfterStep(handler StepEventHandler) { + g.onAfterStepHandlers = append(g.onAfterStepHandlers, handler) +} diff --git a/system/system.go b/system/system.go new file mode 100644 index 0000000..eb0351e --- /dev/null +++ b/system/system.go @@ -0,0 +1,61 @@ +package system + +import ( + "time" + + "github.com/unitoftime/ecs" +) + +type SystemName string + +type System interface { + GetName() SystemName +} + +type RealtimeSystem interface { + RunRealtime(delta time.Duration) +} + +type FixedSystem interface { + RunFixed(delta time.Duration) +} + +type StepSystem interface { + RunStep(step int32) +} + +// Ordering and execution flow + +/* Optional interface that can be implemented by the system to indicate that execution engine must run this system before other systems */ +type RunBeforeSystem interface { + /* Get list of systems that should be runned before this system */ + GetRunBefore() []SystemName +} + +/* Optional interface that can be implemented by the system to indicate that execution engine must run this system after other systems */ +type RunAfterSystem interface { + /* Get list of systems that should be runned after this system */ + GetRunAfter() []SystemName +} + +/* +Optional interface that can be implemented by the system to indicate that execution engine must ensure read access to the specified componets list. +During execution of this system, other systems can also read same components. + +If this or `WriteComponentsSystem` interfaces not implemented for the system, execution engine will ensure write access to all the components and lock the entire world during execution of this system. +*/ +type ReadComponentsSystem interface { + /* Get list of components with read access. During execution of this system, other systems can also read same components. */ + GetReadComponents() []ecs.Component +} + +/* +Optional interface that can be implemented by the system to indicate that execution engine must ensure write access to the specified componets list. +During execution of this system, no other system can read or write to specified list of components. + +If this or `ReadComponentsSystem` interfaces not implemented for the system, execution engine will ensure write access to all the components and lock the entire world during execution of this system. +*/ +type WriteComponentsSystem interface { + /* Get list of components with write access. During execution of this system, no other system can read or write to specified list of components */ + GetWriteComponents() []ecs.Component +} diff --git a/system_test.go b/system_test.go deleted file mode 100644 index d894fa8..0000000 --- a/system_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package ecs - -import ( - "fmt" - "testing" - "time" -) - -// // func physicsSystem(id Id, pos *Position, vel *Velocity) { -// func physicsSystem(dt time.Duration, query *View2[position, velocity]) { -// query.MapId(func(id Id, pos *position, vel *velocity) { -// pos.x += vel.x * dt.Seconds() -// pos.y += vel.y * dt.Seconds() -// pos.z += vel.z * dt.Seconds() -// }) -// } - -// func TestSystemCreationNew(t *testing.T) { -// world := setupPhysics(100) -// sys := NewSystem1(world, physicsSystem) -// sys.Run(16 * time.Millisecond) -// } - -var lastTime time.Time - -func TestSchedulerPhysics(t *testing.T) { - scheduler := NewScheduler() - scheduler.AppendPhysics(System{ - Name: "TestSystem", - Func: func(dt time.Duration) { - fmt.Printf("%v - %v\n", dt, time.Since(lastTime)) - lastTime = time.Now() - }, - }) - lastTime = time.Now() - go scheduler.Run() - time.Sleep(1 * time.Second) - scheduler.SetQuit(true) -} - -var lastTimeInput, lastTimePhysics, lastTimeRender time.Time - -func TestSchedulerAll(t *testing.T) { - scheduler := NewScheduler() - scheduler.AppendInput(System{ - Name: "TestSystemInput", - Func: func(dt time.Duration) { - fmt.Printf("Input: %v - %v\n", dt, time.Since(lastTimeInput)) - lastTimeInput = time.Now() - time.Sleep(1 * time.Millisecond) - }, - }) - scheduler.AppendPhysics(System{ - Name: "TestSystemPhysics", - Func: func(dt time.Duration) { - fmt.Printf("Physics: %v - %v\n", dt, time.Since(lastTimePhysics)) - lastTimePhysics = time.Now() - }, - }) - scheduler.AppendRender(System{ - Name: "TestSystemRender", - Func: func(dt time.Duration) { - fmt.Printf("Render: %v - %v\n", dt, time.Since(lastTimeRender)) - lastTimeRender = time.Now() - time.Sleep(100 * time.Millisecond) - }, - }) - lastTimeInput = time.Now() - lastTimePhysics = time.Now() - lastTimeRender = time.Now() - go scheduler.Run() - time.Sleep(1 * time.Second) - scheduler.SetQuit(true) -} diff --git a/view_gen.go b/view_gen.go index 22a6e47..ca6c808 100755 --- a/view_gen.go +++ b/view_gen.go @@ -1,8 +1,11 @@ package ecs -// Warning: This is an autogenerated file. Do not modify!! - +import ( + "runtime" + "sync" +) +// Warning: This is an autogenerated file. Do not modify!! // -------------------------------------------------------------------------------- // - View 1 @@ -10,9 +13,9 @@ package ecs // Represents a view of data in a specific world. Provides access to the components specified in the generic block type View1[A any] struct { - world *World + world *World filter filterList - + storageA *componentSliceStorage[A] } @@ -22,25 +25,22 @@ func (v *View1[A]) initialize(world *World) any { return Query1[A](world) } - // Creates a View for the specified world with the specified component filters. func Query1[A any](world *World, filters ...Filter) *View1[A] { storageA := getStorage[A](world.engine) - var AA A - comps := []componentId{ + comps := []ComponentId{ name(AA), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) v := &View1[A]{ - world: world, + world: world, filter: filterList, storageA: storageA, @@ -52,7 +52,7 @@ func Query1[A any](world *World, filters ...Filter) *View1[A] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View1[A]) Read(id Id) (*A) { +func (v *View1[A]) Read(id Id) *A { if id == InvalidEntity { return nil } @@ -74,14 +74,12 @@ func (v *View1[A]) Read(id Id) (*A) { return nil } - var retA *A - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - return retA } @@ -90,70 +88,72 @@ func (v *View1[A]) Read(id Id) (*A) { func (v *View1[A]) MapId(lambda func(id Id, a *A)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp } - retA = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } lambda(ids[idx], retA) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -176,27 +176,120 @@ func (v *View1[A]) MapId(lambda func(id Id, a *A)) { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View1[A]) MapIdParallel(chunkSize int, lambda func(id Id, a *A)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + + lambda(newWork.ids[i], paramA) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map func (v *View1[A]) MapSlices(lambda func(id []Id, a []A)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) } @@ -207,48 +300,44 @@ func (v *View1[A]) MapSlices(lambda func(id []Id, a []A)) { } } - // -------------------------------------------------------------------------------- // - View 2 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View2[A,B any] struct { - world *World +type View2[A, B any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View2[A,B]) initialize(world *World) any { +func (v *View2[A, B]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query2[A,B](world) + return Query2[A, B](world) } - // Creates a View for the specified world with the specified component filters. -func Query2[A,B any](world *World, filters ...Filter) *View2[A,B] { +func Query2[A, B any](world *World, filters ...Filter) *View2[A, B] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) - var AA A var BB B - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View2[A,B]{ - world: world, + v := &View2[A, B]{ + world: world, filter: filterList, storageA: storageA, @@ -261,7 +350,7 @@ func Query2[A,B any](world *World, filters ...Filter) *View2[A,B] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View2[A,B]) Read(id Id) (*A,*B) { +func (v *View2[A, B]) Read(id Id) (*A, *B) { if id == InvalidEntity { return nil, nil } @@ -283,51 +372,48 @@ func (v *View2[A,B]) Read(id Id) (*A,*B) { return nil, nil } - var retA *A var retB *B - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - return retA, retB } // Maps the lambda function across every entity which matched the specified filters. -func (v *View2[A,B]) MapId(lambda func(id Id, a *A, b *B)) { +func (v *View2[A, B]) MapId(lambda func(id Id, a *A, b *B)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -337,48 +423,53 @@ func (v *View2[A,B]) MapId(lambda func(id Id, a *A, b *B)) { compB = sliceB.comp } - retA = nil retB = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } lambda(ids[idx], retA, retB) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -401,87 +492,191 @@ func (v *View2[A,B]) MapId(lambda func(id Id, a *A, b *B)) { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View2[A, B]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + + lambda(newWork.ids[i], paramA, paramB) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View2[A,B]) MapSlices(lambda func(id []Id, a []A, b []B)) { +func (v *View2[A, B]) MapSlices(lambda func(id []Id, a []A, b []B)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) } for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx], + sliceListA[idx], sliceListB[idx], ) } } - // -------------------------------------------------------------------------------- // - View 3 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View3[A,B,C any] struct { - world *World +type View3[A, B, C any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View3[A,B,C]) initialize(world *World) any { +func (v *View3[A, B, C]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query3[A,B,C](world) + return Query3[A, B, C](world) } - // Creates a View for the specified world with the specified component filters. -func Query3[A,B,C any](world *World, filters ...Filter) *View3[A,B,C] { +func Query3[A, B, C any](world *World, filters ...Filter) *View3[A, B, C] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) storageC := getStorage[C](world.engine) - var AA A var BB B var CC C - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), name(CC), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View3[A,B,C]{ - world: world, + v := &View3[A, B, C]{ + world: world, filter: filterList, storageA: storageA, @@ -495,7 +690,7 @@ func Query3[A,B,C any](world *World, filters ...Filter) *View3[A,B,C] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View3[A,B,C]) Read(id Id) (*A,*B,*C) { +func (v *View3[A, B, C]) Read(id Id) (*A, *B, *C) { if id == InvalidEntity { return nil, nil, nil } @@ -517,61 +712,58 @@ func (v *View3[A,B,C]) Read(id Id) (*A,*B,*C) { return nil, nil, nil } - var retA *A var retB *B var retC *C - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - return retA, retB, retC } // Maps the lambda function across every entity which matched the specified filters. -func (v *View3[A,B,C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { +func (v *View3[A, B, C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -585,50 +777,57 @@ func (v *View3[A,B,C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { compC = sliceC.comp } - retA = nil retB = nil retC = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } lambda(ids[idx], retA, retB, retC) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -651,33 +850,156 @@ func (v *View3[A,B,C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View3[A, B, C]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View3[A,B,C]) MapSlices(lambda func(id []Id, a []A, b []B, c []C)) { +func (v *View3[A, B, C]) MapSlices(lambda func(id []Id, a []A, b []B, c []C)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -685,21 +1007,20 @@ func (v *View3[A,B,C]) MapSlices(lambda func(id []Id, a []A, b []B, c []C)) { for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], ) } } - // -------------------------------------------------------------------------------- // - View 4 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View4[A,B,C,D any] struct { - world *World +type View4[A, B, C, D any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -707,39 +1028,36 @@ type View4[A,B,C,D any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View4[A,B,C,D]) initialize(world *World) any { +func (v *View4[A, B, C, D]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query4[A,B,C,D](world) + return Query4[A, B, C, D](world) } - // Creates a View for the specified world with the specified component filters. -func Query4[A,B,C,D any](world *World, filters ...Filter) *View4[A,B,C,D] { +func Query4[A, B, C, D any](world *World, filters ...Filter) *View4[A, B, C, D] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) storageC := getStorage[C](world.engine) storageD := getStorage[D](world.engine) - var AA A var BB B var CC C var DD D - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), name(CC), name(DD), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View4[A,B,C,D]{ - world: world, + v := &View4[A, B, C, D]{ + world: world, filter: filterList, storageA: storageA, @@ -754,7 +1072,7 @@ func Query4[A,B,C,D any](world *World, filters ...Filter) *View4[A,B,C,D] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View4[A,B,C,D]) Read(id Id) (*A,*B,*C,*D) { +func (v *View4[A, B, C, D]) Read(id Id) (*A, *B, *C, *D) { if id == InvalidEntity { return nil, nil, nil, nil } @@ -776,71 +1094,68 @@ func (v *View4[A,B,C,D]) Read(id Id) (*A,*B,*C,*D) { return nil, nil, nil, nil } - var retA *A var retB *B var retC *C var retD *D - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - return retA, retB, retC, retD } // Maps the lambda function across every entity which matched the specified filters. -func (v *View4[A,B,C,D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { +func (v *View4[A, B, C, D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] sliceD, _ = v.storageD.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -858,52 +1173,61 @@ func (v *View4[A,B,C,D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { compD = sliceD.comp } - retA = nil retB = nil retC = nil retD = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } lambda(ids[idx], retA, retB, retC, retD) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -926,59 +1250,196 @@ func (v *View4[A,B,C,D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { // } } -// Deprecated: This API is a tentative alternative way to map -func (v *View4[A,B,C,D]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D)) { +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View4[A, B, C, D]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D)) { v.filter.regenerate(v.world) - id := make([][]Id, 0) + var sliceA *componentSlice[A] + var compA []A - - sliceListA := make([][]A, 0) - sliceListB := make([][]B, 0) - sliceListC := make([][]C, 0) - sliceListD := make([][]D, 0) + var sliceB *componentSlice[B] + var compB []B - for _, archId := range v.filter.archIds { - - sliceA, ok := v.storageA.slice[archId] - if !ok { continue } - sliceB, ok := v.storageB.slice[archId] - if !ok { continue } - sliceC, ok := v.storageC.slice[archId] - if !ok { continue } - sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + var sliceC *componentSlice[C] + var compC []C - lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } - // lookup, ok := v.world.engine.lookup[archId] - // if !ok { panic("LookupList is missing!") } + var sliceD *componentSlice[D] + var compD []D - id = append(id, lookup.id) - - sliceListA = append(sliceListA, sliceA.comp) - sliceListB = append(sliceListB, sliceB.comp) - sliceListC = append(sliceListC, sliceC.comp) - sliceListD = append(sliceListD, sliceD.comp) + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD) + } + } } - - for idx := range id { - lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx], - ) + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() } -} + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD} + } + } + + close(newWorkChanel) + workDone.Wait() +} + +// Deprecated: This API is a tentative alternative way to map +func (v *View4[A, B, C, D]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D)) { + v.filter.regenerate(v.world) + + id := make([][]Id, 0) + + sliceListA := make([][]A, 0) + sliceListB := make([][]B, 0) + sliceListC := make([][]C, 0) + sliceListD := make([][]D, 0) + + for _, archId := range v.filter.archIds { + + sliceA, ok := v.storageA.slice[archId] + if !ok { + continue + } + sliceB, ok := v.storageB.slice[archId] + if !ok { + continue + } + sliceC, ok := v.storageC.slice[archId] + if !ok { + continue + } + sliceD, ok := v.storageD.slice[archId] + if !ok { + continue + } + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + + id = append(id, lookup.id) + + sliceListA = append(sliceListA, sliceA.comp) + sliceListB = append(sliceListB, sliceB.comp) + sliceListC = append(sliceListC, sliceC.comp) + sliceListD = append(sliceListD, sliceD.comp) + } + + for idx := range id { + lambda(id[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], + ) + } +} // -------------------------------------------------------------------------------- -// - View 5 -// -------------------------------------------------------------------------------- +// - View 5 +// -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View5[A,B,C,D,E any] struct { - world *World +type View5[A, B, C, D, E any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -987,14 +1448,13 @@ type View5[A,B,C,D,E any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View5[A,B,C,D,E]) initialize(world *World) any { +func (v *View5[A, B, C, D, E]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query5[A,B,C,D,E](world) + return Query5[A, B, C, D, E](world) } - // Creates a View for the specified world with the specified component filters. -func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { +func Query5[A, B, C, D, E any](world *World, filters ...Filter) *View5[A, B, C, D, E] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1002,27 +1462,25 @@ func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { storageD := getStorage[D](world.engine) storageE := getStorage[E](world.engine) - var AA A var BB B var CC C var DD D var EE E - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), name(CC), name(DD), name(EE), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View5[A,B,C,D,E]{ - world: world, + v := &View5[A, B, C, D, E]{ + world: world, filter: filterList, storageA: storageA, @@ -1038,7 +1496,7 @@ func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View5[A,B,C,D,E]) Read(id Id) (*A,*B,*C,*D,*E) { +func (v *View5[A, B, C, D, E]) Read(id Id) (*A, *B, *C, *D, *E) { if id == InvalidEntity { return nil, nil, nil, nil, nil } @@ -1060,66 +1518,62 @@ func (v *View5[A,B,C,D,E]) Read(id Id) (*A,*B,*C,*D,*E) { return nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C var retD *D var retE *E - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - return retA, retB, retC, retD, retE } // Maps the lambda function across every entity which matched the specified filters. -func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E)) { +func (v *View5[A, B, C, D, E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -1127,14 +1581,15 @@ func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E sliceE, _ = v.storageE.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -1156,54 +1611,65 @@ func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E compE = sliceE.comp } - retA = nil retB = nil retC = nil retD = nil retE = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -1226,13 +1692,154 @@ func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View5[A, B, C, D, E]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View5[A,B,C,D,E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E)) { +func (v *View5[A, B, C, D, E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -1240,25 +1847,37 @@ func (v *View5[A,B,C,D,E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d sliceListE := make([][]E, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -1268,21 +1887,20 @@ func (v *View5[A,B,C,D,E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], ) } } - // -------------------------------------------------------------------------------- // - View 6 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View6[A,B,C,D,E,F any] struct { - world *World +type View6[A, B, C, D, E, F any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -1292,14 +1910,13 @@ type View6[A,B,C,D,E,F any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View6[A,B,C,D,E,F]) initialize(world *World) any { +func (v *View6[A, B, C, D, E, F]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query6[A,B,C,D,E,F](world) + return Query6[A, B, C, D, E, F](world) } - // Creates a View for the specified world with the specified component filters. -func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F] { +func Query6[A, B, C, D, E, F any](world *World, filters ...Filter) *View6[A, B, C, D, E, F] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1308,7 +1925,6 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F storageE := getStorage[E](world.engine) storageF := getStorage[F](world.engine) - var AA A var BB B var CC C @@ -1316,7 +1932,7 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F var EE E var FF F - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -1324,13 +1940,12 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F name(DD), name(EE), name(FF), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View6[A,B,C,D,E,F]{ - world: world, + v := &View6[A, B, C, D, E, F]{ + world: world, filter: filterList, storageA: storageA, @@ -1347,7 +1962,7 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View6[A,B,C,D,E,F]) Read(id Id) (*A,*B,*C,*D,*E,*F) { +func (v *View6[A, B, C, D, E, F]) Read(id Id) (*A, *B, *C, *D, *E, *F) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil } @@ -1369,7 +1984,6 @@ func (v *View6[A,B,C,D,E,F]) Read(id Id) (*A,*B,*C,*D,*E,*F) { return nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -1377,67 +1991,64 @@ func (v *View6[A,B,C,D,E,F]) Read(id Id) (*A,*B,*C,*D,*E,*F) { var retE *E var retF *F - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - return retA, retB, retC, retD, retE, retF } // Maps the lambda function across every entity which matched the specified filters. -func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F)) { +func (v *View6[A, B, C, D, E, F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -1446,14 +2057,15 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e sliceF, _ = v.storageF.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -1479,7 +2091,6 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e compF = sliceF.comp } - retA = nil retB = nil retC = nil @@ -1487,48 +2098,62 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e retE = nil retF = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -1551,85 +2176,251 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e // } } -// Deprecated: This API is a tentative alternative way to map -func (v *View6[A,B,C,D,E,F]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F)) { +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View6[A, B, C, D, E, F]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F)) { v.filter.regenerate(v.world) - id := make([][]Id, 0) + var sliceA *componentSlice[A] + var compA []A - - sliceListA := make([][]A, 0) - sliceListB := make([][]B, 0) - sliceListC := make([][]C, 0) - sliceListD := make([][]D, 0) - sliceListE := make([][]E, 0) - sliceListF := make([][]F, 0) + var sliceB *componentSlice[B] + var compB []B - for _, archId := range v.filter.archIds { - - sliceA, ok := v.storageA.slice[archId] - if !ok { continue } - sliceB, ok := v.storageB.slice[archId] - if !ok { continue } - sliceC, ok := v.storageC.slice[archId] - if !ok { continue } - sliceD, ok := v.storageD.slice[archId] - if !ok { continue } - sliceE, ok := v.storageE.slice[archId] - if !ok { continue } - sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + var sliceC *componentSlice[C] + var compC []C - lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } - // lookup, ok := v.world.engine.lookup[archId] - // if !ok { panic("LookupList is missing!") } + var sliceD *componentSlice[D] + var compD []D - id = append(id, lookup.id) - - sliceListA = append(sliceListA, sliceA.comp) - sliceListB = append(sliceListB, sliceB.comp) - sliceListC = append(sliceListC, sliceC.comp) - sliceListD = append(sliceListD, sliceD.comp) - sliceListE = append(sliceListE, sliceE.comp) - sliceListF = append(sliceListF, sliceF.comp) - } + var sliceE *componentSlice[E] + var compE []E - for idx := range id { - lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx], - ) + var sliceF *componentSlice[F] + var compF []F + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() } -} + for _, archId := range v.filter.archIds { -// -------------------------------------------------------------------------------- -// - View 7 -// -------------------------------------------------------------------------------- + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] -// Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View7[A,B,C,D,E,F,G any] struct { - world *World - filter filterList - - storageA *componentSliceStorage[A] - storageB *componentSliceStorage[B] - storageC *componentSliceStorage[C] - storageD *componentSliceStorage[D] + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF} + } + } + + close(newWorkChanel) + workDone.Wait() +} + +// Deprecated: This API is a tentative alternative way to map +func (v *View6[A, B, C, D, E, F]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F)) { + v.filter.regenerate(v.world) + + id := make([][]Id, 0) + + sliceListA := make([][]A, 0) + sliceListB := make([][]B, 0) + sliceListC := make([][]C, 0) + sliceListD := make([][]D, 0) + sliceListE := make([][]E, 0) + sliceListF := make([][]F, 0) + + for _, archId := range v.filter.archIds { + + sliceA, ok := v.storageA.slice[archId] + if !ok { + continue + } + sliceB, ok := v.storageB.slice[archId] + if !ok { + continue + } + sliceC, ok := v.storageC.slice[archId] + if !ok { + continue + } + sliceD, ok := v.storageD.slice[archId] + if !ok { + continue + } + sliceE, ok := v.storageE.slice[archId] + if !ok { + continue + } + sliceF, ok := v.storageF.slice[archId] + if !ok { + continue + } + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + + id = append(id, lookup.id) + + sliceListA = append(sliceListA, sliceA.comp) + sliceListB = append(sliceListB, sliceB.comp) + sliceListC = append(sliceListC, sliceC.comp) + sliceListD = append(sliceListD, sliceD.comp) + sliceListE = append(sliceListE, sliceE.comp) + sliceListF = append(sliceListF, sliceF.comp) + } + + for idx := range id { + lambda(id[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], + ) + } +} + +// -------------------------------------------------------------------------------- +// - View 7 +// -------------------------------------------------------------------------------- + +// Represents a view of data in a specific world. Provides access to the components specified in the generic block +type View7[A, B, C, D, E, F, G any] struct { + world *World + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] storageE *componentSliceStorage[E] storageF *componentSliceStorage[F] storageG *componentSliceStorage[G] } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View7[A,B,C,D,E,F,G]) initialize(world *World) any { +func (v *View7[A, B, C, D, E, F, G]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query7[A,B,C,D,E,F,G](world) + return Query7[A, B, C, D, E, F, G](world) } - // Creates a View for the specified world with the specified component filters. -func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E,F,G] { +func Query7[A, B, C, D, E, F, G any](world *World, filters ...Filter) *View7[A, B, C, D, E, F, G] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1639,7 +2430,6 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E storageF := getStorage[F](world.engine) storageG := getStorage[G](world.engine) - var AA A var BB B var CC C @@ -1648,7 +2438,7 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E var FF F var GG G - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -1657,13 +2447,12 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E name(EE), name(FF), name(GG), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View7[A,B,C,D,E,F,G]{ - world: world, + v := &View7[A, B, C, D, E, F, G]{ + world: world, filter: filterList, storageA: storageA, @@ -1681,7 +2470,7 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View7[A,B,C,D,E,F,G]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G) { +func (v *View7[A, B, C, D, E, F, G]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil } @@ -1703,7 +2492,6 @@ func (v *View7[A,B,C,D,E,F,G]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G) { return nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -1712,75 +2500,72 @@ func (v *View7[A,B,C,D,E,F,G]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G) { var retF *F var retG *G - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - return retA, retB, retC, retD, retE, retF, retG } // Maps the lambda function across every entity which matched the specified filters. -func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G)) { +func (v *View7[A, B, C, D, E, F, G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -1790,14 +2575,15 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, sliceG, _ = v.storageG.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -1827,7 +2613,6 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, compG = sliceG.comp } - retA = nil retB = nil retC = nil @@ -1836,49 +2621,65 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, retF = nil retG = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -1901,13 +2702,180 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View7[A, B, C, D, E, F, G]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View7[A,B,C,D,E,F,G]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G)) { +func (v *View7[A, B, C, D, E, F, G]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -1917,29 +2885,45 @@ func (v *View7[A,B,C,D,E,F,G]) MapSlices(lambda func(id []Id, a []A, b []B, c [] sliceListG := make([][]G, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -1951,21 +2935,20 @@ func (v *View7[A,B,C,D,E,F,G]) MapSlices(lambda func(id []Id, a []A, b []B, c [] for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], ) } } - // -------------------------------------------------------------------------------- // - View 8 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View8[A,B,C,D,E,F,G,H any] struct { - world *World +type View8[A, B, C, D, E, F, G, H any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -1977,14 +2960,13 @@ type View8[A,B,C,D,E,F,G,H any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View8[A,B,C,D,E,F,G,H]) initialize(world *World) any { +func (v *View8[A, B, C, D, E, F, G, H]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query8[A,B,C,D,E,F,G,H](world) + return Query8[A, B, C, D, E, F, G, H](world) } - // Creates a View for the specified world with the specified component filters. -func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D,E,F,G,H] { +func Query8[A, B, C, D, E, F, G, H any](world *World, filters ...Filter) *View8[A, B, C, D, E, F, G, H] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1995,7 +2977,6 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D storageG := getStorage[G](world.engine) storageH := getStorage[H](world.engine) - var AA A var BB B var CC C @@ -2005,7 +2986,7 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D var GG G var HH H - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -2015,13 +2996,12 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D name(FF), name(GG), name(HH), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View8[A,B,C,D,E,F,G,H]{ - world: world, + v := &View8[A, B, C, D, E, F, G, H]{ + world: world, filter: filterList, storageA: storageA, @@ -2040,7 +3020,7 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View8[A,B,C,D,E,F,G,H]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H) { +func (v *View8[A, B, C, D, E, F, G, H]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil } @@ -2062,7 +3042,6 @@ func (v *View8[A,B,C,D,E,F,G,H]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H) { return nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -2072,83 +3051,80 @@ func (v *View8[A,B,C,D,E,F,G,H]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H) { var retG *G var retH *H - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH } // Maps the lambda function across every entity which matched the specified filters. -func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H)) { +func (v *View8[A, B, C, D, E, F, G, H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -2159,14 +3135,15 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D sliceH, _ = v.storageH.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -2200,7 +3177,6 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D compH = sliceH.comp } - retA = nil retB = nil retC = nil @@ -2210,50 +3186,68 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D retG = nil retH = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -2276,48 +3270,246 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D // } } -// Deprecated: This API is a tentative alternative way to map -func (v *View8[A,B,C,D,E,F,G,H]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H)) { +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View8[A, B, C, D, E, F, G, H]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H)) { v.filter.regenerate(v.world) - id := make([][]Id, 0) + var sliceA *componentSlice[A] + var compA []A - - sliceListA := make([][]A, 0) - sliceListB := make([][]B, 0) - sliceListC := make([][]C, 0) - sliceListD := make([][]D, 0) - sliceListE := make([][]E, 0) - sliceListF := make([][]F, 0) - sliceListG := make([][]G, 0) - sliceListH := make([][]H, 0) + var sliceB *componentSlice[B] + var compB []B - for _, archId := range v.filter.archIds { - - sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH} + } + } + + close(newWorkChanel) + workDone.Wait() +} + +// Deprecated: This API is a tentative alternative way to map +func (v *View8[A, B, C, D, E, F, G, H]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H)) { + v.filter.regenerate(v.world) + + id := make([][]Id, 0) + + sliceListA := make([][]A, 0) + sliceListB := make([][]B, 0) + sliceListC := make([][]C, 0) + sliceListD := make([][]D, 0) + sliceListE := make([][]E, 0) + sliceListF := make([][]F, 0) + sliceListG := make([][]G, 0) + sliceListH := make([][]H, 0) + + for _, archId := range v.filter.archIds { + + sliceA, ok := v.storageA.slice[archId] + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -2330,21 +3522,20 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapSlices(lambda func(id []Id, a []A, b []B, c for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], ) } } - // -------------------------------------------------------------------------------- // - View 9 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View9[A,B,C,D,E,F,G,H,I any] struct { - world *World +type View9[A, B, C, D, E, F, G, H, I any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -2357,14 +3548,13 @@ type View9[A,B,C,D,E,F,G,H,I any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View9[A,B,C,D,E,F,G,H,I]) initialize(world *World) any { +func (v *View9[A, B, C, D, E, F, G, H, I]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query9[A,B,C,D,E,F,G,H,I](world) + return Query9[A, B, C, D, E, F, G, H, I](world) } - // Creates a View for the specified world with the specified component filters. -func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C,D,E,F,G,H,I] { +func Query9[A, B, C, D, E, F, G, H, I any](world *World, filters ...Filter) *View9[A, B, C, D, E, F, G, H, I] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -2376,7 +3566,6 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C storageH := getStorage[H](world.engine) storageI := getStorage[I](world.engine) - var AA A var BB B var CC C @@ -2387,7 +3576,7 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C var HH H var II I - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -2398,13 +3587,12 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C name(GG), name(HH), name(II), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View9[A,B,C,D,E,F,G,H,I]{ - world: world, + v := &View9[A, B, C, D, E, F, G, H, I]{ + world: world, filter: filterList, storageA: storageA, @@ -2424,7 +3612,7 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View9[A,B,C,D,E,F,G,H,I]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I) { +func (v *View9[A, B, C, D, E, F, G, H, I]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -2446,7 +3634,6 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I) { return nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -2457,91 +3644,88 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I) { var retH *H var retI *I - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI } // Maps the lambda function across every entity which matched the specified filters. -func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I)) { +func (v *View9[A, B, C, D, E, F, G, H, I]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - + var sliceI *componentSlice[I] var compI []I var retI *I - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -2553,14 +3737,15 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d sliceI, _ = v.storageI.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -2598,7 +3783,6 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d compI = sliceI.comp } - retA = nil retB = nil retC = nil @@ -2609,51 +3793,71 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d retH = nil retI = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -2676,13 +3880,206 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View9[A, B, C, D, E, F, G, H, I]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + var sliceI *componentSlice[I] + var compI []I + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View9[A,B,C,D,E,F,G,H,I]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I)) { +func (v *View9[A, B, C, D, E, F, G, H, I]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -2694,33 +4091,53 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapSlices(lambda func(id []Id, a []A, b []B, sliceListI := make([][]I, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -2734,21 +4151,20 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapSlices(lambda func(id []Id, a []A, b []B, for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], ) } } - // -------------------------------------------------------------------------------- // - View 10 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View10[A,B,C,D,E,F,G,H,I,J any] struct { - world *World +type View10[A, B, C, D, E, F, G, H, I, J any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -2762,14 +4178,13 @@ type View10[A,B,C,D,E,F,G,H,I,J any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View10[A,B,C,D,E,F,G,H,I,J]) initialize(world *World) any { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query10[A,B,C,D,E,F,G,H,I,J](world) + return Query10[A, B, C, D, E, F, G, H, I, J](world) } - // Creates a View for the specified world with the specified component filters. -func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A,B,C,D,E,F,G,H,I,J] { +func Query10[A, B, C, D, E, F, G, H, I, J any](world *World, filters ...Filter) *View10[A, B, C, D, E, F, G, H, I, J] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -2782,7 +4197,6 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A storageI := getStorage[I](world.engine) storageJ := getStorage[J](world.engine) - var AA A var BB B var CC C @@ -2794,7 +4208,7 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A var II I var JJ J - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -2806,13 +4220,12 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A name(HH), name(II), name(JJ), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View10[A,B,C,D,E,F,G,H,I,J]{ - world: world, + v := &View10[A, B, C, D, E, F, G, H, I, J]{ + world: world, filter: filterList, storageA: storageA, @@ -2833,7 +4246,7 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View10[A,B,C,D,E,F,G,H,I,J]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J) { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I, *J) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -2855,7 +4268,6 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -2867,99 +4279,96 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J var retI *I var retJ *J - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - sliceJ, ok := v.storageJ.slice[archId] + sliceJ, ok := v.storageJ.slice[archId] if ok { retJ = &sliceJ.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ } // Maps the lambda function across every entity which matched the specified filters. -func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J)) { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - + var sliceI *componentSlice[I] var compI []I var retI *I - + var sliceJ *componentSlice[J] var compJ []J var retJ *J - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -2972,14 +4381,15 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, sliceJ, _ = v.storageJ.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -3021,7 +4431,6 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, compJ = sliceJ.comp } - retA = nil retB = nil retC = nil @@ -3033,52 +4442,74 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, retI = nil retJ = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } - if compJ != nil { retJ = &compJ[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } + if compJ != nil { + retJ = &compJ[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -3101,13 +4532,219 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View10[A, B, C, D, E, F, G, H, I, J]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + var sliceI *componentSlice[I] + var compI []I + + var sliceJ *componentSlice[J] + var compJ []J + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + j []J + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + var paramJ *J + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + if newWork.j != nil { + paramJ = &newWork.j[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI, paramJ) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + sliceJ, _ = v.storageJ.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + compJ = nil + if sliceJ != nil { + compJ = sliceJ.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J)) { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -3120,35 +4757,57 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapSlices(lambda func(id []Id, a []A, b [] sliceListJ := make([][]J, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceJ, ok := v.storageJ.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -3163,21 +4822,20 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapSlices(lambda func(id []Id, a []A, b [] for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx],sliceListJ[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], sliceListJ[idx], ) } } - // -------------------------------------------------------------------------------- // - View 11 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View11[A,B,C,D,E,F,G,H,I,J,K any] struct { - world *World +type View11[A, B, C, D, E, F, G, H, I, J, K any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -3192,14 +4850,13 @@ type View11[A,B,C,D,E,F,G,H,I,J,K any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) initialize(world *World) any { +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query11[A,B,C,D,E,F,G,H,I,J,K](world) + return Query11[A, B, C, D, E, F, G, H, I, J, K](world) } - // Creates a View for the specified world with the specified component filters. -func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11[A,B,C,D,E,F,G,H,I,J,K] { +func Query11[A, B, C, D, E, F, G, H, I, J, K any](world *World, filters ...Filter) *View11[A, B, C, D, E, F, G, H, I, J, K] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -3213,7 +4870,6 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 storageJ := getStorage[J](world.engine) storageK := getStorage[K](world.engine) - var AA A var BB B var CC C @@ -3226,7 +4882,7 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 var JJ J var KK K - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -3239,13 +4895,12 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 name(II), name(JJ), name(KK), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View11[A,B,C,D,E,F,G,H,I,J,K]{ - world: world, + v := &View11[A, B, C, D, E, F, G, H, I, J, K]{ + world: world, filter: filterList, storageA: storageA, @@ -3267,7 +4922,7 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J,*K) { +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I, *J, *K) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -3289,7 +4944,6 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I, return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -3302,107 +4956,401 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I, var retJ *J var retK *K - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - sliceJ, ok := v.storageJ.slice[archId] + sliceJ, ok := v.storageJ.slice[archId] if ok { retJ = &sliceJ.comp[index] } - sliceK, ok := v.storageK.slice[archId] + sliceK, ok := v.storageK.slice[archId] if ok { retK = &sliceK.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK } -// Maps the lambda function across every entity which matched the specified filters. -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K)) { +// Maps the lambda function across every entity which matched the specified filters. +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + var retA *A + + var sliceB *componentSlice[B] + var compB []B + var retB *B + + var sliceC *componentSlice[C] + var compC []C + var retC *C + + var sliceD *componentSlice[D] + var compD []D + var retD *D + + var sliceE *componentSlice[E] + var compE []E + var retE *E + + var sliceF *componentSlice[F] + var compF []F + var retF *F + + var sliceG *componentSlice[G] + var compG []G + var retG *G + + var sliceH *componentSlice[H] + var compH []H + var retH *H + + var sliceI *componentSlice[I] + var compI []I + var retI *I + + var sliceJ *componentSlice[J] + var compJ []J + var retJ *J + + var sliceK *componentSlice[K] + var compK []K + var retK *K + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + sliceJ, _ = v.storageJ.slice[archId] + sliceK, _ = v.storageK.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + compJ = nil + if sliceJ != nil { + compJ = sliceJ.comp + } + compK = nil + if sliceK != nil { + compK = sliceK.comp + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + retF = nil + retG = nil + retH = nil + retI = nil + retJ = nil + retK = nil + for idx := range ids { + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } + if compJ != nil { + retJ = &compJ[idx] + } + if compK != nil { + retK = &compK[idx] + } + lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK) + } + + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional + // for _, archId := range v.filter.archIds { + // aSlice, ok := v.storageA.slice[archId] + // if !ok { continue } + // bSlice, ok := v.storageB.slice[archId] + // if !ok { continue } + + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + + // ids := lookup.id + // aComp := aSlice.comp + // bComp := bSlice.comp + // if len(ids) != len(aComp) || len(ids) != len(bComp) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &aComp[i], &bComp[i]) + // } + // } +} + +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A - var retA *A - + var sliceB *componentSlice[B] var compB []B - var retB *B - + var sliceC *componentSlice[C] var compC []C - var retC *C - + var sliceD *componentSlice[D] var compD []D - var retD *D - + var sliceE *componentSlice[E] var compE []E - var retE *E - + var sliceF *componentSlice[F] var compF []F - var retF *F - + var sliceG *componentSlice[G] var compG []G - var retG *G - + var sliceH *componentSlice[H] var compH []H - var retH *H - + var sliceI *componentSlice[I] var compI []I - var retI *I - + var sliceJ *componentSlice[J] var compJ []J - var retJ *J - + var sliceK *componentSlice[K] var compK []K - var retK *K - + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + j []J + k []K + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + var paramJ *J + var paramK *K + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + if newWork.j != nil { + paramJ = &newWork.j[i] + } + if newWork.k != nil { + paramK = &newWork.k[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI, paramJ, paramK) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -3416,14 +5364,13 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapId(lambda func(id Id, a *A, b *B, c * sliceK, _ = v.storageK.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - compA = nil if sliceA != nil { compA = sliceA.comp @@ -3469,95 +5416,42 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapId(lambda func(id Id, a *A, b *B, c * compK = sliceK.comp } - - retA = nil - retB = nil - retC = nil - retD = nil - retE = nil - retF = nil - retG = nil - retH = nil - retI = nil - retJ = nil - retK = nil + startWorkRangeIndex := -1 for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } - if compJ != nil { retJ = &compJ[idx] } - if compK != nil { retK = &compK[idx] } - lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK) + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK} + startWorkRangeIndex = -1 + } } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK} + } } - // Original - doesn't handle optional - // for _, archId := range v.filter.archIds { - // aSlice, ok := v.storageA.slice[archId] - // if !ok { continue } - // bSlice, ok := v.storageB.slice[archId] - // if !ok { continue } - - // lookup, ok := v.world.engine.lookup[archId] - // if !ok { panic("LookupList is missing!") } - - // ids := lookup.id - // aComp := aSlice.comp - // bComp := bSlice.comp - // if len(ids) != len(aComp) || len(ids) != len(bComp) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &aComp[i], &bComp[i]) - // } - // } + close(newWorkChanel) + workDone.Wait() } // Deprecated: This API is a tentative alternative way to map -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K)) { +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -3571,37 +5465,61 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapSlices(lambda func(id []Id, a []A, b sliceListK := make([][]K, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceJ, ok := v.storageJ.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceK, ok := v.storageK.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -3617,21 +5535,20 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapSlices(lambda func(id []Id, a []A, b for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx],sliceListJ[idx],sliceListK[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], sliceListJ[idx], sliceListK[idx], ) } } - // -------------------------------------------------------------------------------- // - View 12 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View12[A,B,C,D,E,F,G,H,I,J,K,L any] struct { - world *World +type View12[A, B, C, D, E, F, G, H, I, J, K, L any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -3647,14 +5564,13 @@ type View12[A,B,C,D,E,F,G,H,I,J,K,L any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) initialize(world *World) any { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query12[A,B,C,D,E,F,G,H,I,J,K,L](world) + return Query12[A, B, C, D, E, F, G, H, I, J, K, L](world) } - // Creates a View for the specified world with the specified component filters. -func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View12[A,B,C,D,E,F,G,H,I,J,K,L] { +func Query12[A, B, C, D, E, F, G, H, I, J, K, L any](world *World, filters ...Filter) *View12[A, B, C, D, E, F, G, H, I, J, K, L] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -3669,7 +5585,6 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View storageK := getStorage[K](world.engine) storageL := getStorage[L](world.engine) - var AA A var BB B var CC C @@ -3683,7 +5598,7 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View var KK K var LL L - comps := []componentId{ + comps := []ComponentId{ name(AA), name(BB), @@ -3697,13 +5612,12 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View name(JJ), name(KK), name(LL), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View12[A,B,C,D,E,F,G,H,I,J,K,L]{ - world: world, + v := &View12[A, B, C, D, E, F, G, H, I, J, K, L]{ + world: world, filter: filterList, storageA: storageA, @@ -3726,7 +5640,7 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J,*K,*L) { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I, *J, *K, *L) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -3748,7 +5662,6 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,* return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -3762,115 +5675,112 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,* var retK *K var retL *L - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - sliceJ, ok := v.storageJ.slice[archId] + sliceJ, ok := v.storageJ.slice[archId] if ok { retJ = &sliceJ.comp[index] } - sliceK, ok := v.storageK.slice[archId] + sliceK, ok := v.storageK.slice[archId] if ok { retK = &sliceK.comp[index] } - sliceL, ok := v.storageL.slice[archId] + sliceL, ok := v.storageL.slice[archId] if ok { retL = &sliceL.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK, retL } // Maps the lambda function across every entity which matched the specified filters. -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K, l *L)) { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K, l *L)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - + var sliceI *componentSlice[I] var compI []I var retI *I - + var sliceJ *componentSlice[J] var compJ []J var retJ *J - + var sliceK *componentSlice[K] var compK []K var retK *K - + var sliceL *componentSlice[L] var compL []L var retL *L - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -3885,14 +5795,15 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c sliceL, _ = v.storageL.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -3942,7 +5853,6 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c compL = sliceL.comp } - retA = nil retB = nil retC = nil @@ -3956,54 +5866,80 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c retK = nil retL = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } - if compJ != nil { retJ = &compJ[idx] } - if compK != nil { retK = &compK[idx] } - if compL != nil { retL = &compL[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } + if compJ != nil { + retJ = &compJ[idx] + } + if compK != nil { + retK = &compK[idx] + } + if compL != nil { + retL = &compL[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK, retL) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -4026,13 +5962,245 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K, l *L)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + var sliceI *componentSlice[I] + var compI []I + + var sliceJ *componentSlice[J] + var compJ []J + + var sliceK *componentSlice[K] + var compK []K + + var sliceL *componentSlice[L] + var compL []L + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + j []J + k []K + l []L + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + var paramJ *J + var paramK *K + var paramL *L + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + if newWork.j != nil { + paramJ = &newWork.j[i] + } + if newWork.k != nil { + paramK = &newWork.k[i] + } + if newWork.l != nil { + paramL = &newWork.l[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI, paramJ, paramK, paramL) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + sliceJ, _ = v.storageJ.slice[archId] + sliceK, _ = v.storageK.slice[archId] + sliceL, _ = v.storageL.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + compJ = nil + if sliceJ != nil { + compJ = sliceJ.comp + } + compK = nil + if sliceK != nil { + compK = sliceK.comp + } + compL = nil + if sliceL != nil { + compL = sliceL.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK, l: compL} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK, l: compL} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK, l: compL} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K, l []L)) { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K, l []L)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -4047,39 +6215,65 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapSlices(lambda func(id []Id, a []A, sliceListL := make([][]L, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceJ, ok := v.storageJ.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceK, ok := v.storageK.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceL, ok := v.storageL.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -4096,8 +6290,7 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapSlices(lambda func(id []Id, a []A, for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx],sliceListJ[idx],sliceListK[idx],sliceListL[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], sliceListJ[idx], sliceListK[idx], sliceListL[idx], ) } } -