From 498092e0f322f715021be4dc10b5fbcd40acf286 Mon Sep 17 00:00:00 2001 From: Jacob <2606873+unitoftime@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:55:38 -0400 Subject: [PATCH] Regenerate view_gen.go file with gofmt --- component.go | 4 +- internal/gen/main.go | 22 +- view_gen.go | 6488 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 6509 insertions(+), 5 deletions(-) diff --git a/component.go b/component.go index 8508502..d7a0334 100644 --- a/component.go +++ b/component.go @@ -124,8 +124,8 @@ type componentRegistry struct { func newComponentRegistry() *componentRegistry { r := &componentRegistry{ - archSet: make([][]archetypeId, maxComponentId+1), // TODO: hardcoded to max component - archMask: make(map[archetypeMask]archetypeId), + archSet: make([][]archetypeId, maxComponentId+1), // TODO: hardcoded to max component + archMask: make(map[archetypeMask]archetypeId), revArchMask: make([]archetypeMask, 0), } return r diff --git a/internal/gen/main.go b/internal/gen/main.go index 675e9c3..4d583d7 100644 --- a/internal/gen/main.go +++ b/internal/gen/main.go @@ -1,7 +1,10 @@ package main import ( + "bytes" _ "embed" + "go/format" + "io/fs" "os" "strings" "text/template" @@ -83,11 +86,24 @@ func main() { 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) + buf := bytes.NewBuffer([]byte{}) + + t.Execute(buf, data) + + filename := "view_gen.go" + + // Attempt to write the file as formatted, falling back to writing it normally + formatted, err := format.Source(buf.Bytes()) if err != nil { + err = os.WriteFile(filename, buf.Bytes(), fs.ModePerm) + if err != nil { + panic(err) + } panic(err) } - defer viewFile.Close() - t.Execute(viewFile, data) + err = os.WriteFile(filename, formatted, fs.ModePerm) + if err != nil { + panic(err) + } } diff --git a/view_gen.go b/view_gen.go index e69de29..6793cf8 100755 --- a/view_gen.go +++ b/view_gen.go @@ -0,0 +1,6488 @@ +package ecs + +import ( + "runtime" + "sync" +) + +// Warning: This is an autogenerated file. Do not modify!! + +// -------------------------------------------------------------------------------- +// - View 1 +// -------------------------------------------------------------------------------- + +// 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 + filter filterList + + storageA *componentSliceStorage[A] +} + +// implement the intializer interface so that it can be automatically created and injected into systems +func (v *View1[A]) initialize(world *World) any { + // TODO: filters need to be a part of the query type + 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{ + + name(AA), + } + filterList := newFilterList(comps, filters...) + filterList.regenerate(world) + + v := &View1[A]{ + world: world, + filter: filterList, + + storageA: storageA, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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 { + if id == InvalidEntity { + return nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil + } + + var retA *A + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + + return retA +} + +// Counts the number of entities that match this query +func (v *View1[A]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// Maps the lambda function across every entity which matched the specified filters. +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!") + } + // 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] + } + 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 + // 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 *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 + } + + 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) + } + + for idx := range id { + lambda(id[idx], + sliceListA[idx], + ) + } +} + +// -------------------------------------------------------------------------------- +// - 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 + 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + + var AA A + var BB B + + comps := []componentId{ + + name(AA), + name(BB), + } + filterList := newFilterList(comps, filters...) + filterList.regenerate(world) + + v := &View2[A, B]{ + world: world, + filter: filterList, + + storageA: storageA, + storageB: storageB, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil + } + + var retA *A + var retB *B + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + + return retA, retB +} + +// Counts the number of entities that match this query +func (v *View2[A, B]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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!") + } + // 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 + } + + 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] + } + 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 + // 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 *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)) { + 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 + } + sliceB, ok := v.storageB.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) + } + + for idx := range id { + lambda(id[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 + 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 { + // TODO: filters need to be a part of the query type + 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] { + + 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{ + + name(AA), + name(BB), + name(CC), + } + filterList := newFilterList(comps, filters...) + filterList.regenerate(world) + + v := &View3[A, B, C]{ + world: world, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + + return retA, retB, retC +} + +// Counts the number of entities that match this query +func (v *View3[A, B, C]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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!") + } + // 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 + } + + 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] + } + 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 + // 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 *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)) { + 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 + } + sliceB, ok := v.storageB.slice[archId] + if !ok { + continue + } + sliceC, ok := v.storageC.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) + } + + for idx := range id { + lambda(id[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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] +} + +// 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 { + // TODO: filters need to be a part of the query type + 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] { + + 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{ + + name(AA), + name(BB), + name(CC), + name(DD), + } + filterList := newFilterList(comps, filters...) + filterList.regenerate(world) + + v := &View4[A, B, C, D]{ + world: world, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + + return retA, retB, retC, retD +} + +// Counts the number of entities that match this query +func (v *View4[A, B, C, D]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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!") + } + // 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 + } + + 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] + } + 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 + // 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 *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) + + 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 + + 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) + } + } + } + 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 +// -------------------------------------------------------------------------------- + +// 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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] +} + +// 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + 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{ + + 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, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + 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] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + + return retA, retB, retC, retD, retE +} + +// Counts the number of entities that match this query +func (v *View5[A, B, C, D, E]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + 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 + + // 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 + } + + 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] + } + 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 + // 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 *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)) { + 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) + + 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 + } + + 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) + } + + for idx := range id { + lambda(id[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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] + storageF *componentSliceStorage[F] +} + +// 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + name(FF), + } + filterList := newFilterList(comps, filters...) + filterList.regenerate(world) + + v := &View6[A, B, C, D, E, F]{ + world: world, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + + return retA, retB, retC, retD, retE, retF +} + +// Counts the number of entities that match this query +func (v *View6[A, B, C, D, E, F]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.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 + } + + retA = nil + retB = nil + retC = nil + retD = nil + 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] + } + 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 + // 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 *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) + + 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 + + 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 { + + 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] + + 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + storageG := getStorage[G](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + var GG G + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + name(FF), + name(GG), + } + filterList := newFilterList(comps, filters...) + filterList.regenerate(world) + + v := &View7[A, B, C, D, E, F, G]{ + world: world, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + storageG: storageG, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + var retG *G + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + sliceG, ok := v.storageG.slice[archId] + if ok { + retG = &sliceG.comp[index] + } + + return retA, retB, retC, retD, retE, retF, retG +} + +// Counts the number of entities that match this query +func (v *View7[A, B, C, D, E, F, G]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + 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 + + // 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 + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + 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] + } + 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 + // 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 *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)) { + 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) + + 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 + } + sliceG, ok := v.storageG.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) + sliceListG = append(sliceListG, sliceG.comp) + } + + for idx := range id { + lambda(id[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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] + storageF *componentSliceStorage[F] + storageG *componentSliceStorage[G] + storageH *componentSliceStorage[H] +} + +// 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + storageG := getStorage[G](world.engine) + storageH := getStorage[H](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + var GG G + var HH H + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + 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, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + storageG: storageG, + storageH: storageH, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + var retG *G + var retH *H + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + sliceG, ok := v.storageG.slice[archId] + if ok { + retG = &sliceG.comp[index] + } + sliceH, ok := v.storageH.slice[archId] + if ok { + retH = &sliceH.comp[index] + } + + return retA, retB, retC, retD, retE, retF, retG, retH +} + +// Counts the number of entities that match this query +func (v *View8[A, B, C, D, E, F, G, H]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + 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 + + // 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 + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + retF = nil + 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] + } + 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 + // 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 *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) + + 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 + + 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 + } + 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 + } + sliceG, ok := v.storageG.slice[archId] + if !ok { + continue + } + sliceH, ok := v.storageH.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) + sliceListG = append(sliceListG, sliceG.comp) + sliceListH = append(sliceListH, sliceH.comp) + } + + for idx := range id { + lambda(id[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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] + storageF *componentSliceStorage[F] + storageG *componentSliceStorage[G] + storageH *componentSliceStorage[H] + storageI *componentSliceStorage[I] +} + +// 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + storageG := getStorage[G](world.engine) + storageH := getStorage[H](world.engine) + storageI := getStorage[I](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + var GG G + var HH H + var II I + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + name(FF), + 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, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + storageG: storageG, + storageH: storageH, + storageI: storageI, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + var retG *G + var retH *H + var retI *I + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + sliceG, ok := v.storageG.slice[archId] + if ok { + retG = &sliceG.comp[index] + } + sliceH, ok := v.storageH.slice[archId] + if ok { + retH = &sliceH.comp[index] + } + sliceI, ok := v.storageI.slice[archId] + if ok { + retI = &sliceI.comp[index] + } + + return retA, retB, retC, retD, retE, retF, retG, retH, retI +} + +// Counts the number of entities that match this query +func (v *View9[A, B, C, D, E, F, G, H, I]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + 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 + + // 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 + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + retF = nil + retG = nil + 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] + } + 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 + // 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 *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)) { + 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) + sliceListI := make([][]I, 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 + } + sliceG, ok := v.storageG.slice[archId] + if !ok { + continue + } + sliceH, ok := v.storageH.slice[archId] + if !ok { + continue + } + sliceI, ok := v.storageI.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) + sliceListG = append(sliceListG, sliceG.comp) + sliceListH = append(sliceListH, sliceH.comp) + sliceListI = append(sliceListI, sliceI.comp) + } + + 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], + ) + } +} + +// -------------------------------------------------------------------------------- +// - 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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] + storageF *componentSliceStorage[F] + storageG *componentSliceStorage[G] + storageH *componentSliceStorage[H] + storageI *componentSliceStorage[I] + storageJ *componentSliceStorage[J] +} + +// 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 { + // TODO: filters need to be a part of the query type + 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + storageG := getStorage[G](world.engine) + storageH := getStorage[H](world.engine) + storageI := getStorage[I](world.engine) + storageJ := getStorage[J](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + var GG G + var HH H + var II I + var JJ J + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + name(FF), + name(GG), + 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, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + storageG: storageG, + storageH: storageH, + storageI: storageI, + storageJ: storageJ, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + var retG *G + var retH *H + var retI *I + var retJ *J + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + sliceG, ok := v.storageG.slice[archId] + if ok { + retG = &sliceG.comp[index] + } + sliceH, ok := v.storageH.slice[archId] + if ok { + retH = &sliceH.comp[index] + } + sliceI, ok := v.storageI.slice[archId] + if ok { + retI = &sliceI.comp[index] + } + sliceJ, ok := v.storageJ.slice[archId] + if ok { + retJ = &sliceJ.comp[index] + } + + return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ +} + +// Counts the number of entities that match this query +func (v *View10[A, B, C, D, E, F, G, H, I, J]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + 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 + + // 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 + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + retF = nil + retG = nil + retH = nil + 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] + } + 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 + // 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 *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)) { + 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) + sliceListI := make([][]I, 0) + sliceListJ := make([][]J, 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 + } + sliceG, ok := v.storageG.slice[archId] + if !ok { + continue + } + sliceH, ok := v.storageH.slice[archId] + if !ok { + continue + } + sliceI, ok := v.storageI.slice[archId] + if !ok { + continue + } + sliceJ, ok := v.storageJ.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) + sliceListG = append(sliceListG, sliceG.comp) + sliceListH = append(sliceListH, sliceH.comp) + sliceListI = append(sliceListI, sliceI.comp) + sliceListJ = append(sliceListJ, sliceJ.comp) + } + + 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], + ) + } +} + +// -------------------------------------------------------------------------------- +// - 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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] + storageF *componentSliceStorage[F] + storageG *componentSliceStorage[G] + storageH *componentSliceStorage[H] + storageI *componentSliceStorage[I] + storageJ *componentSliceStorage[J] + storageK *componentSliceStorage[K] +} + +// 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 { + // 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) +} + +// 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + storageG := getStorage[G](world.engine) + storageH := getStorage[H](world.engine) + storageI := getStorage[I](world.engine) + storageJ := getStorage[J](world.engine) + storageK := getStorage[K](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + var GG G + var HH H + var II I + var JJ J + var KK K + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + name(FF), + name(GG), + name(HH), + 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, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + storageG: storageG, + storageH: storageH, + storageI: storageI, + storageJ: storageJ, + storageK: storageK, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + var retG *G + var retH *H + var retI *I + var retJ *J + var retK *K + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + sliceG, ok := v.storageG.slice[archId] + if ok { + retG = &sliceG.comp[index] + } + sliceH, ok := v.storageH.slice[archId] + if ok { + retH = &sliceH.comp[index] + } + sliceI, ok := v.storageI.slice[archId] + if ok { + retI = &sliceI.comp[index] + } + sliceJ, ok := v.storageJ.slice[archId] + if ok { + retJ = &sliceJ.comp[index] + } + sliceK, ok := v.storageK.slice[archId] + if ok { + retK = &sliceK.comp[index] + } + + return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK +} + +// Counts the number of entities that match this query +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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 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 + + 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] + 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 + + 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 + } + + 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} + 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 + } + } + + 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} + } + } + + 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)) { + 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) + sliceListI := make([][]I, 0) + sliceListJ := make([][]J, 0) + sliceListK := make([][]K, 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 + } + sliceG, ok := v.storageG.slice[archId] + if !ok { + continue + } + sliceH, ok := v.storageH.slice[archId] + if !ok { + continue + } + sliceI, ok := v.storageI.slice[archId] + if !ok { + continue + } + sliceJ, ok := v.storageJ.slice[archId] + if !ok { + continue + } + sliceK, ok := v.storageK.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) + sliceListG = append(sliceListG, sliceG.comp) + sliceListH = append(sliceListH, sliceH.comp) + sliceListI = append(sliceListI, sliceI.comp) + sliceListJ = append(sliceListJ, sliceJ.comp) + sliceListK = append(sliceListK, sliceK.comp) + } + + 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], + ) + } +} + +// -------------------------------------------------------------------------------- +// - 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 + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] + storageE *componentSliceStorage[E] + storageF *componentSliceStorage[F] + storageG *componentSliceStorage[G] + storageH *componentSliceStorage[H] + storageI *componentSliceStorage[I] + storageJ *componentSliceStorage[J] + storageK *componentSliceStorage[K] + storageL *componentSliceStorage[L] +} + +// 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 { + // 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) +} + +// 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] { + + storageA := getStorage[A](world.engine) + storageB := getStorage[B](world.engine) + storageC := getStorage[C](world.engine) + storageD := getStorage[D](world.engine) + storageE := getStorage[E](world.engine) + storageF := getStorage[F](world.engine) + storageG := getStorage[G](world.engine) + storageH := getStorage[H](world.engine) + storageI := getStorage[I](world.engine) + storageJ := getStorage[J](world.engine) + storageK := getStorage[K](world.engine) + storageL := getStorage[L](world.engine) + + var AA A + var BB B + var CC C + var DD D + var EE E + var FF F + var GG G + var HH H + var II I + var JJ J + var KK K + var LL L + + comps := []componentId{ + + name(AA), + name(BB), + name(CC), + name(DD), + name(EE), + name(FF), + name(GG), + name(HH), + name(II), + 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, + filter: filterList, + + storageA: storageA, + storageB: storageB, + storageC: storageC, + storageD: storageD, + storageE: storageE, + storageF: storageF, + storageG: storageG, + storageH: storageH, + storageI: storageI, + storageJ: storageJ, + storageK: storageK, + storageL: storageL, + } + return v +} + +// Reads a pointer to the underlying component at the specified id. +// 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) { + if id == InvalidEntity { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + archId, ok := v.world.arch.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + 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!") + // } + index, ok := lookup.index.Get(id) + if !ok { + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil + } + + var retA *A + var retB *B + var retC *C + var retD *D + var retE *E + var retF *F + var retG *G + var retH *H + var retI *I + var retJ *J + var retK *K + var retL *L + + sliceA, ok := v.storageA.slice[archId] + if ok { + retA = &sliceA.comp[index] + } + sliceB, ok := v.storageB.slice[archId] + if ok { + retB = &sliceB.comp[index] + } + sliceC, ok := v.storageC.slice[archId] + if ok { + retC = &sliceC.comp[index] + } + sliceD, ok := v.storageD.slice[archId] + if ok { + retD = &sliceD.comp[index] + } + sliceE, ok := v.storageE.slice[archId] + if ok { + retE = &sliceE.comp[index] + } + sliceF, ok := v.storageF.slice[archId] + if ok { + retF = &sliceF.comp[index] + } + sliceG, ok := v.storageG.slice[archId] + if ok { + retG = &sliceG.comp[index] + } + sliceH, ok := v.storageH.slice[archId] + if ok { + retH = &sliceH.comp[index] + } + sliceI, ok := v.storageI.slice[archId] + if ok { + retI = &sliceI.comp[index] + } + sliceJ, ok := v.storageJ.slice[archId] + if ok { + retJ = &sliceJ.comp[index] + } + sliceK, ok := v.storageK.slice[archId] + if ok { + retK = &sliceK.comp[index] + } + 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 +} + +// Counts the number of entities that match this query +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) Count() int { + v.filter.regenerate(v.world) + + total := 0 + for _, archId := range v.filter.archIds { + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + + total += lookup.Len() + } + return total +} + +// 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)) { + 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] + 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 + + // 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 + } + compL = nil + if sliceL != nil { + compL = sliceL.comp + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + retF = nil + retG = nil + retH = nil + retI = nil + retJ = nil + 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] + } + 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 + // 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 *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)) { + 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) + sliceListI := make([][]I, 0) + sliceListJ := make([][]J, 0) + sliceListK := make([][]K, 0) + sliceListL := make([][]L, 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 + } + sliceG, ok := v.storageG.slice[archId] + if !ok { + continue + } + sliceH, ok := v.storageH.slice[archId] + if !ok { + continue + } + sliceI, ok := v.storageI.slice[archId] + if !ok { + continue + } + sliceJ, ok := v.storageJ.slice[archId] + if !ok { + continue + } + sliceK, ok := v.storageK.slice[archId] + if !ok { + continue + } + sliceL, ok := v.storageL.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) + sliceListG = append(sliceListG, sliceG.comp) + sliceListH = append(sliceListH, sliceH.comp) + sliceListI = append(sliceListI, sliceI.comp) + sliceListJ = append(sliceListJ, sliceJ.comp) + sliceListK = append(sliceListK, sliceK.comp) + sliceListL = append(sliceListL, sliceL.comp) + } + + 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], + ) + } +}