From 839568fb5fd8b779dc2e7a8aa5541a9b0264080f Mon Sep 17 00:00:00 2001 From: Mykola Zhyhallo Date: Wed, 18 Oct 2023 22:56:33 +0200 Subject: [PATCH] Added MapIdParallel to the Views --- internal/gen/main.go | 26 +- internal/gen/view.tgo | 91 + view_gen.go | 4505 ++++++++++++++++++++++++++++++----------- 3 files changed, 3461 insertions(+), 1161 deletions(-) diff --git a/internal/gen/main.go b/internal/gen/main.go index ba2146d..675e9c3 100644 --- a/internal/gen/main.go +++ b/internal/gen/main.go @@ -1,13 +1,12 @@ package main import ( + _ "embed" "os" "strings" - "text/template" - _ "embed" + "text/template" ) - //go:embed view.tgo var viewTemplate string @@ -35,6 +34,9 @@ func main() { } funcs := template.FuncMap{ "join": strings.Join, + "lower": func(val string) string { + return strings.ToLower(val) + }, "nils": func(n int) string { val := make([]string, 0) for i := 0; i < n; i++ { @@ -63,9 +65,23 @@ func main() { } return strings.Join(ret, ", ") }, + "parallelLambdaStructArgs": func(val []string) string { + ret := make([]string, len(val)) + for i := range val { + ret[i] = strings.ToLower(val[i]) + " []" + val[i] + } + return strings.Join(ret, "; ") + }, + "parallelLambdaArgsFromStruct": func(val []string) string { + ret := make([]string, len(val)) + for i := range val { + ret[i] = "param" + val[i] + } + return strings.Join(ret, ", ") + }, } - t := template.Must(template.New("ViewTemplate").Funcs(funcs).Parse(viewTemplate)) + t := template.Must(template.New("ViewTemplate").Funcs(funcs).Parse(viewTemplate)) viewFile, err := os.OpenFile("view_gen.go", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { @@ -73,5 +89,5 @@ func main() { } defer viewFile.Close() - t.Execute(viewFile, data) + t.Execute(viewFile, data) } diff --git a/internal/gen/view.tgo b/internal/gen/view.tgo index 9e7cbbb..b8bc223 100644 --- a/internal/gen/view.tgo +++ b/internal/gen/view.tgo @@ -1,5 +1,10 @@ package ecs +import ( + "sync" + "runtime" +) + // Warning: This is an autogenerated file. Do not modify!! {{range $i, $element := .Views}} @@ -176,6 +181,92 @@ func (v *View{{len $element}}[{{join $element ","}}]) MapId(lambda func(id Id, { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View{{len $element}}[{{join $element ","}}]) MapIdParallel(chunkSize int, lambda func(id Id, {{lambdaArgs $element}})) { + v.filter.regenerate(v.world) + + {{range $ii, $arg := $element}} + var slice{{$arg}} *componentSlice[{{$arg}}] + var comp{{$arg}} []{{$arg}} + {{end}} + + + workDone := &sync.WaitGroup{} + type workPackage struct{start int; end int; ids []Id; {{parallelLambdaStructArgs $element}}} + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + {{range $ii, $arg := $element}} + var param{{$arg}} *{{$arg}}{{end}} + + for i := newWork.start; i < newWork.end; i++ { + {{range $ii, $arg := $element}} + if newWork.{{lower $arg}} != nil { param{{$arg}} = &newWork.{{lower $arg}}[i]}{{end}} + + lambda(newWork.ids[i], {{parallelLambdaArgsFromStruct $element}}) + } + } + } + parallelLevel := runtime.NumCPU()*2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + + for _, archId := range v.filter.archIds { + {{range $ii, $arg := $element}} + slice{{$arg}}, _ = v.storage{{$arg}}.slice[archId]{{end}} + + lookup := v.world.engine.lookup[archId] + if lookup == nil { panic("LookupList is missing!") } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + {{range $ii, $arg := $element}} + comp{{$arg}} = nil + if slice{{$arg}} != nil { + comp{{$arg}} = slice{{$arg}}.comp + }{{end}} + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx - startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx+1, ids: ids, {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map func (v *View{{len $element}}[{{join $element ","}}]) MapSlices(lambda func(id []Id, {{sliceLambdaArgs $element}})) { v.filter.regenerate(v.world) diff --git a/view_gen.go b/view_gen.go index 22a6e47..83a243a 100755 --- a/view_gen.go +++ b/view_gen.go @@ -1,8 +1,11 @@ package ecs -// Warning: This is an autogenerated file. Do not modify!! - +import ( + "runtime" + "sync" +) +// Warning: This is an autogenerated file. Do not modify!! // -------------------------------------------------------------------------------- // - View 1 @@ -10,9 +13,9 @@ package ecs // Represents a view of data in a specific world. Provides access to the components specified in the generic block type View1[A any] struct { - world *World + world *World filter filterList - + storageA *componentSliceStorage[A] } @@ -22,25 +25,22 @@ func (v *View1[A]) initialize(world *World) any { return Query1[A](world) } - // Creates a View for the specified world with the specified component filters. func Query1[A any](world *World, filters ...Filter) *View1[A] { storageA := getStorage[A](world.engine) - var AA A comps := []componentId{ name(AA), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) v := &View1[A]{ - world: world, + world: world, filter: filterList, storageA: storageA, @@ -52,7 +52,7 @@ func Query1[A any](world *World, filters ...Filter) *View1[A] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View1[A]) Read(id Id) (*A) { +func (v *View1[A]) Read(id Id) *A { if id == InvalidEntity { return nil } @@ -74,14 +74,12 @@ func (v *View1[A]) Read(id Id) (*A) { return nil } - var retA *A - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - return retA } @@ -90,70 +88,72 @@ func (v *View1[A]) Read(id Id) (*A) { func (v *View1[A]) MapId(lambda func(id Id, a *A)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp } - retA = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } lambda(ids[idx], retA) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -176,27 +176,120 @@ func (v *View1[A]) MapId(lambda func(id Id, a *A)) { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View1[A]) MapIdParallel(chunkSize int, lambda func(id Id, a *A)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + + lambda(newWork.ids[i], paramA) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map func (v *View1[A]) MapSlices(lambda func(id []Id, a []A)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) } @@ -207,34 +300,31 @@ func (v *View1[A]) MapSlices(lambda func(id []Id, a []A)) { } } - // -------------------------------------------------------------------------------- // - View 2 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View2[A,B any] struct { - world *World +type View2[A, B any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View2[A,B]) initialize(world *World) any { +func (v *View2[A, B]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query2[A,B](world) + return Query2[A, B](world) } - // Creates a View for the specified world with the specified component filters. -func Query2[A,B any](world *World, filters ...Filter) *View2[A,B] { +func Query2[A, B any](world *World, filters ...Filter) *View2[A, B] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) - var AA A var BB B @@ -242,13 +332,12 @@ func Query2[A,B any](world *World, filters ...Filter) *View2[A,B] { name(AA), name(BB), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View2[A,B]{ - world: world, + v := &View2[A, B]{ + world: world, filter: filterList, storageA: storageA, @@ -261,7 +350,7 @@ func Query2[A,B any](world *World, filters ...Filter) *View2[A,B] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View2[A,B]) Read(id Id) (*A,*B) { +func (v *View2[A, B]) Read(id Id) (*A, *B) { if id == InvalidEntity { return nil, nil } @@ -283,51 +372,48 @@ func (v *View2[A,B]) Read(id Id) (*A,*B) { return nil, nil } - var retA *A var retB *B - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - return retA, retB } // Maps the lambda function across every entity which matched the specified filters. -func (v *View2[A,B]) MapId(lambda func(id Id, a *A, b *B)) { +func (v *View2[A, B]) MapId(lambda func(id Id, a *A, b *B)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -337,48 +423,53 @@ func (v *View2[A,B]) MapId(lambda func(id Id, a *A, b *B)) { compB = sliceB.comp } - retA = nil retB = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } lambda(ids[idx], retA, retB) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -401,71 +492,176 @@ func (v *View2[A,B]) MapId(lambda func(id Id, a *A, b *B)) { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View2[A, B]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + + lambda(newWork.ids[i], paramA, paramB) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View2[A,B]) MapSlices(lambda func(id []Id, a []A, b []B)) { +func (v *View2[A, B]) MapSlices(lambda func(id []Id, a []A, b []B)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) } for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx], + sliceListA[idx], sliceListB[idx], ) } } - // -------------------------------------------------------------------------------- // - View 3 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View3[A,B,C any] struct { - world *World +type View3[A, B, C any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View3[A,B,C]) initialize(world *World) any { +func (v *View3[A, B, C]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query3[A,B,C](world) + return Query3[A, B, C](world) } - // Creates a View for the specified world with the specified component filters. -func Query3[A,B,C any](world *World, filters ...Filter) *View3[A,B,C] { +func Query3[A, B, C any](world *World, filters ...Filter) *View3[A, B, C] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) storageC := getStorage[C](world.engine) - var AA A var BB B var CC C @@ -475,13 +671,12 @@ func Query3[A,B,C any](world *World, filters ...Filter) *View3[A,B,C] { name(AA), name(BB), name(CC), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View3[A,B,C]{ - world: world, + v := &View3[A, B, C]{ + world: world, filter: filterList, storageA: storageA, @@ -495,7 +690,7 @@ func Query3[A,B,C any](world *World, filters ...Filter) *View3[A,B,C] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View3[A,B,C]) Read(id Id) (*A,*B,*C) { +func (v *View3[A, B, C]) Read(id Id) (*A, *B, *C) { if id == InvalidEntity { return nil, nil, nil } @@ -517,61 +712,58 @@ func (v *View3[A,B,C]) Read(id Id) (*A,*B,*C) { return nil, nil, nil } - var retA *A var retB *B var retC *C - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - return retA, retB, retC } // Maps the lambda function across every entity which matched the specified filters. -func (v *View3[A,B,C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { +func (v *View3[A, B, C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -585,50 +777,57 @@ func (v *View3[A,B,C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { compC = sliceC.comp } - retA = nil retB = nil retC = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } lambda(ids[idx], retA, retB, retC) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -651,33 +850,156 @@ func (v *View3[A,B,C]) MapId(lambda func(id Id, a *A, b *B, c *C)) { // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View3[A, B, C]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View3[A,B,C]) MapSlices(lambda func(id []Id, a []A, b []B, c []C)) { +func (v *View3[A, B, C]) MapSlices(lambda func(id []Id, a []A, b []B, c []C)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -685,21 +1007,20 @@ func (v *View3[A,B,C]) MapSlices(lambda func(id []Id, a []A, b []B, c []C)) { for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], ) } } - // -------------------------------------------------------------------------------- // - View 4 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View4[A,B,C,D any] struct { - world *World +type View4[A, B, C, D any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -707,21 +1028,19 @@ type View4[A,B,C,D any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View4[A,B,C,D]) initialize(world *World) any { +func (v *View4[A, B, C, D]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query4[A,B,C,D](world) + return Query4[A, B, C, D](world) } - // Creates a View for the specified world with the specified component filters. -func Query4[A,B,C,D any](world *World, filters ...Filter) *View4[A,B,C,D] { +func Query4[A, B, C, D any](world *World, filters ...Filter) *View4[A, B, C, D] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) storageC := getStorage[C](world.engine) storageD := getStorage[D](world.engine) - var AA A var BB B var CC C @@ -733,13 +1052,12 @@ func Query4[A,B,C,D any](world *World, filters ...Filter) *View4[A,B,C,D] { name(BB), name(CC), name(DD), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View4[A,B,C,D]{ - world: world, + v := &View4[A, B, C, D]{ + world: world, filter: filterList, storageA: storageA, @@ -754,7 +1072,7 @@ func Query4[A,B,C,D any](world *World, filters ...Filter) *View4[A,B,C,D] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View4[A,B,C,D]) Read(id Id) (*A,*B,*C,*D) { +func (v *View4[A, B, C, D]) Read(id Id) (*A, *B, *C, *D) { if id == InvalidEntity { return nil, nil, nil, nil } @@ -776,71 +1094,68 @@ func (v *View4[A,B,C,D]) Read(id Id) (*A,*B,*C,*D) { return nil, nil, nil, nil } - var retA *A var retB *B var retC *C var retD *D - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - return retA, retB, retC, retD } // Maps the lambda function across every entity which matched the specified filters. -func (v *View4[A,B,C,D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { +func (v *View4[A, B, C, D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] sliceD, _ = v.storageD.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -858,52 +1173,61 @@ func (v *View4[A,B,C,D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { compD = sliceD.comp } - retA = nil retB = nil retC = nil retD = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } lambda(ids[idx], retA, retB, retC, retD) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -926,59 +1250,196 @@ func (v *View4[A,B,C,D]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D)) { // } } -// Deprecated: This API is a tentative alternative way to map -func (v *View4[A,B,C,D]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D)) { +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View4[A, B, C, D]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D)) { v.filter.regenerate(v.world) - id := make([][]Id, 0) + var sliceA *componentSlice[A] + var compA []A - - sliceListA := make([][]A, 0) - sliceListB := make([][]B, 0) - sliceListC := make([][]C, 0) - sliceListD := make([][]D, 0) + var sliceB *componentSlice[B] + var compB []B - for _, archId := range v.filter.archIds { - - sliceA, ok := v.storageA.slice[archId] - if !ok { continue } - sliceB, ok := v.storageB.slice[archId] - if !ok { continue } - sliceC, ok := v.storageC.slice[archId] - if !ok { continue } - sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + var sliceC *componentSlice[C] + var compC []C - lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } - // lookup, ok := v.world.engine.lookup[archId] - // if !ok { panic("LookupList is missing!") } + var sliceD *componentSlice[D] + var compD []D - id = append(id, lookup.id) - - sliceListA = append(sliceListA, sliceA.comp) - sliceListB = append(sliceListB, sliceB.comp) - sliceListC = append(sliceListC, sliceC.comp) - sliceListD = append(sliceListD, sliceD.comp) + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD) + } + } } - - for idx := range id { - lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx], - ) + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() } -} + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD} + } + } + + close(newWorkChanel) + workDone.Wait() +} + +// Deprecated: This API is a tentative alternative way to map +func (v *View4[A, B, C, D]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D)) { + v.filter.regenerate(v.world) + + id := make([][]Id, 0) + + sliceListA := make([][]A, 0) + sliceListB := make([][]B, 0) + sliceListC := make([][]C, 0) + sliceListD := make([][]D, 0) + + for _, archId := range v.filter.archIds { + + sliceA, ok := v.storageA.slice[archId] + if !ok { + continue + } + sliceB, ok := v.storageB.slice[archId] + if !ok { + continue + } + sliceC, ok := v.storageC.slice[archId] + if !ok { + continue + } + sliceD, ok := v.storageD.slice[archId] + if !ok { + continue + } + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + + id = append(id, lookup.id) + + sliceListA = append(sliceListA, sliceA.comp) + sliceListB = append(sliceListB, sliceB.comp) + sliceListC = append(sliceListC, sliceC.comp) + sliceListD = append(sliceListD, sliceD.comp) + } + + for idx := range id { + lambda(id[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], + ) + } +} // -------------------------------------------------------------------------------- -// - View 5 -// -------------------------------------------------------------------------------- +// - View 5 +// -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View5[A,B,C,D,E any] struct { - world *World +type View5[A, B, C, D, E any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -987,14 +1448,13 @@ type View5[A,B,C,D,E any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View5[A,B,C,D,E]) initialize(world *World) any { +func (v *View5[A, B, C, D, E]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query5[A,B,C,D,E](world) + return Query5[A, B, C, D, E](world) } - // Creates a View for the specified world with the specified component filters. -func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { +func Query5[A, B, C, D, E any](world *World, filters ...Filter) *View5[A, B, C, D, E] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1002,7 +1462,6 @@ func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { storageD := getStorage[D](world.engine) storageE := getStorage[E](world.engine) - var AA A var BB B var CC C @@ -1016,13 +1475,12 @@ func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { name(CC), name(DD), name(EE), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View5[A,B,C,D,E]{ - world: world, + v := &View5[A, B, C, D, E]{ + world: world, filter: filterList, storageA: storageA, @@ -1038,7 +1496,7 @@ func Query5[A,B,C,D,E any](world *World, filters ...Filter) *View5[A,B,C,D,E] { // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View5[A,B,C,D,E]) Read(id Id) (*A,*B,*C,*D,*E) { +func (v *View5[A, B, C, D, E]) Read(id Id) (*A, *B, *C, *D, *E) { if id == InvalidEntity { return nil, nil, nil, nil, nil } @@ -1060,66 +1518,62 @@ func (v *View5[A,B,C,D,E]) Read(id Id) (*A,*B,*C,*D,*E) { return nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C var retD *D var retE *E - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - return retA, retB, retC, retD, retE } // Maps the lambda function across every entity which matched the specified filters. -func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E)) { +func (v *View5[A, B, C, D, E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -1127,14 +1581,15 @@ func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E sliceE, _ = v.storageE.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -1156,54 +1611,65 @@ func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E compE = sliceE.comp } - retA = nil retB = nil retC = nil retD = nil retE = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -1226,13 +1692,154 @@ func (v *View5[A,B,C,D,E]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View5[A, B, C, D, E]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View5[A,B,C,D,E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E)) { +func (v *View5[A, B, C, D, E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -1240,25 +1847,37 @@ func (v *View5[A,B,C,D,E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d sliceListE := make([][]E, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -1268,21 +1887,20 @@ func (v *View5[A,B,C,D,E]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], ) } } - // -------------------------------------------------------------------------------- // - View 6 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View6[A,B,C,D,E,F any] struct { - world *World +type View6[A, B, C, D, E, F any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -1292,14 +1910,13 @@ type View6[A,B,C,D,E,F any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View6[A,B,C,D,E,F]) initialize(world *World) any { +func (v *View6[A, B, C, D, E, F]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query6[A,B,C,D,E,F](world) + return Query6[A, B, C, D, E, F](world) } - // Creates a View for the specified world with the specified component filters. -func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F] { +func Query6[A, B, C, D, E, F any](world *World, filters ...Filter) *View6[A, B, C, D, E, F] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1308,7 +1925,6 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F storageE := getStorage[E](world.engine) storageF := getStorage[F](world.engine) - var AA A var BB B var CC C @@ -1324,13 +1940,12 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F name(DD), name(EE), name(FF), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View6[A,B,C,D,E,F]{ - world: world, + v := &View6[A, B, C, D, E, F]{ + world: world, filter: filterList, storageA: storageA, @@ -1347,7 +1962,7 @@ func Query6[A,B,C,D,E,F any](world *World, filters ...Filter) *View6[A,B,C,D,E,F // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View6[A,B,C,D,E,F]) Read(id Id) (*A,*B,*C,*D,*E,*F) { +func (v *View6[A, B, C, D, E, F]) Read(id Id) (*A, *B, *C, *D, *E, *F) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil } @@ -1369,7 +1984,6 @@ func (v *View6[A,B,C,D,E,F]) Read(id Id) (*A,*B,*C,*D,*E,*F) { return nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -1377,67 +1991,64 @@ func (v *View6[A,B,C,D,E,F]) Read(id Id) (*A,*B,*C,*D,*E,*F) { var retE *E var retF *F - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - return retA, retB, retC, retD, retE, retF } // Maps the lambda function across every entity which matched the specified filters. -func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F)) { +func (v *View6[A, B, C, D, E, F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -1446,14 +2057,15 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e sliceF, _ = v.storageF.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -1479,7 +2091,6 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e compF = sliceF.comp } - retA = nil retB = nil retC = nil @@ -1487,48 +2098,62 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e retE = nil retF = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -1551,85 +2176,251 @@ func (v *View6[A,B,C,D,E,F]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e // } } -// Deprecated: This API is a tentative alternative way to map -func (v *View6[A,B,C,D,E,F]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F)) { +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View6[A, B, C, D, E, F]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F)) { v.filter.regenerate(v.world) - id := make([][]Id, 0) + var sliceA *componentSlice[A] + var compA []A - - sliceListA := make([][]A, 0) - sliceListB := make([][]B, 0) - sliceListC := make([][]C, 0) - sliceListD := make([][]D, 0) - sliceListE := make([][]E, 0) - sliceListF := make([][]F, 0) + var sliceB *componentSlice[B] + var compB []B - for _, archId := range v.filter.archIds { - - sliceA, ok := v.storageA.slice[archId] - if !ok { continue } - sliceB, ok := v.storageB.slice[archId] - if !ok { continue } - sliceC, ok := v.storageC.slice[archId] - if !ok { continue } - sliceD, ok := v.storageD.slice[archId] - if !ok { continue } - sliceE, ok := v.storageE.slice[archId] - if !ok { continue } - sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + var sliceC *componentSlice[C] + var compC []C - lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } - // lookup, ok := v.world.engine.lookup[archId] - // if !ok { panic("LookupList is missing!") } + var sliceD *componentSlice[D] + var compD []D - id = append(id, lookup.id) - - sliceListA = append(sliceListA, sliceA.comp) - sliceListB = append(sliceListB, sliceB.comp) - sliceListC = append(sliceListC, sliceC.comp) - sliceListD = append(sliceListD, sliceD.comp) - sliceListE = append(sliceListE, sliceE.comp) - sliceListF = append(sliceListF, sliceF.comp) - } + var sliceE *componentSlice[E] + var compE []E - for idx := range id { - lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx], - ) + var sliceF *componentSlice[F] + var compF []F + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() } -} + for _, archId := range v.filter.archIds { -// -------------------------------------------------------------------------------- -// - View 7 -// -------------------------------------------------------------------------------- + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] -// Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View7[A,B,C,D,E,F,G any] struct { - world *World - filter filterList - - storageA *componentSliceStorage[A] - storageB *componentSliceStorage[B] - storageC *componentSliceStorage[C] - storageD *componentSliceStorage[D] + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF} + } + } + + close(newWorkChanel) + workDone.Wait() +} + +// Deprecated: This API is a tentative alternative way to map +func (v *View6[A, B, C, D, E, F]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F)) { + v.filter.regenerate(v.world) + + id := make([][]Id, 0) + + sliceListA := make([][]A, 0) + sliceListB := make([][]B, 0) + sliceListC := make([][]C, 0) + sliceListD := make([][]D, 0) + sliceListE := make([][]E, 0) + sliceListF := make([][]F, 0) + + for _, archId := range v.filter.archIds { + + sliceA, ok := v.storageA.slice[archId] + if !ok { + continue + } + sliceB, ok := v.storageB.slice[archId] + if !ok { + continue + } + sliceC, ok := v.storageC.slice[archId] + if !ok { + continue + } + sliceD, ok := v.storageD.slice[archId] + if !ok { + continue + } + sliceE, ok := v.storageE.slice[archId] + if !ok { + continue + } + sliceF, ok := v.storageF.slice[archId] + if !ok { + continue + } + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + + id = append(id, lookup.id) + + sliceListA = append(sliceListA, sliceA.comp) + sliceListB = append(sliceListB, sliceB.comp) + sliceListC = append(sliceListC, sliceC.comp) + sliceListD = append(sliceListD, sliceD.comp) + sliceListE = append(sliceListE, sliceE.comp) + sliceListF = append(sliceListF, sliceF.comp) + } + + for idx := range id { + lambda(id[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], + ) + } +} + +// -------------------------------------------------------------------------------- +// - View 7 +// -------------------------------------------------------------------------------- + +// Represents a view of data in a specific world. Provides access to the components specified in the generic block +type View7[A, B, C, D, E, F, G any] struct { + world *World + filter filterList + + storageA *componentSliceStorage[A] + storageB *componentSliceStorage[B] + storageC *componentSliceStorage[C] + storageD *componentSliceStorage[D] storageE *componentSliceStorage[E] storageF *componentSliceStorage[F] storageG *componentSliceStorage[G] } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View7[A,B,C,D,E,F,G]) initialize(world *World) any { +func (v *View7[A, B, C, D, E, F, G]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query7[A,B,C,D,E,F,G](world) + return Query7[A, B, C, D, E, F, G](world) } - // Creates a View for the specified world with the specified component filters. -func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E,F,G] { +func Query7[A, B, C, D, E, F, G any](world *World, filters ...Filter) *View7[A, B, C, D, E, F, G] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1639,7 +2430,6 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E storageF := getStorage[F](world.engine) storageG := getStorage[G](world.engine) - var AA A var BB B var CC C @@ -1657,13 +2447,12 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E name(EE), name(FF), name(GG), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View7[A,B,C,D,E,F,G]{ - world: world, + v := &View7[A, B, C, D, E, F, G]{ + world: world, filter: filterList, storageA: storageA, @@ -1681,7 +2470,7 @@ func Query7[A,B,C,D,E,F,G any](world *World, filters ...Filter) *View7[A,B,C,D,E // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View7[A,B,C,D,E,F,G]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G) { +func (v *View7[A, B, C, D, E, F, G]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil } @@ -1703,7 +2492,6 @@ func (v *View7[A,B,C,D,E,F,G]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G) { return nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -1712,75 +2500,72 @@ func (v *View7[A,B,C,D,E,F,G]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G) { var retF *F var retG *G - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - return retA, retB, retC, retD, retE, retF, retG } // Maps the lambda function across every entity which matched the specified filters. -func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G)) { +func (v *View7[A, B, C, D, E, F, G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -1790,14 +2575,15 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, sliceG, _ = v.storageG.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -1827,7 +2613,6 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, compG = sliceG.comp } - retA = nil retB = nil retC = nil @@ -1836,49 +2621,65 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, retF = nil retG = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -1901,13 +2702,180 @@ func (v *View7[A,B,C,D,E,F,G]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View7[A, B, C, D, E, F, G]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View7[A,B,C,D,E,F,G]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G)) { +func (v *View7[A, B, C, D, E, F, G]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -1917,29 +2885,45 @@ func (v *View7[A,B,C,D,E,F,G]) MapSlices(lambda func(id []Id, a []A, b []B, c [] sliceListG := make([][]G, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -1951,21 +2935,20 @@ func (v *View7[A,B,C,D,E,F,G]) MapSlices(lambda func(id []Id, a []A, b []B, c [] for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], ) } } - // -------------------------------------------------------------------------------- // - View 8 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View8[A,B,C,D,E,F,G,H any] struct { - world *World +type View8[A, B, C, D, E, F, G, H any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -1977,14 +2960,13 @@ type View8[A,B,C,D,E,F,G,H any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View8[A,B,C,D,E,F,G,H]) initialize(world *World) any { +func (v *View8[A, B, C, D, E, F, G, H]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query8[A,B,C,D,E,F,G,H](world) + return Query8[A, B, C, D, E, F, G, H](world) } - // Creates a View for the specified world with the specified component filters. -func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D,E,F,G,H] { +func Query8[A, B, C, D, E, F, G, H any](world *World, filters ...Filter) *View8[A, B, C, D, E, F, G, H] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -1995,7 +2977,6 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D storageG := getStorage[G](world.engine) storageH := getStorage[H](world.engine) - var AA A var BB B var CC C @@ -2015,13 +2996,12 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D name(FF), name(GG), name(HH), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View8[A,B,C,D,E,F,G,H]{ - world: world, + v := &View8[A, B, C, D, E, F, G, H]{ + world: world, filter: filterList, storageA: storageA, @@ -2040,7 +3020,7 @@ func Query8[A,B,C,D,E,F,G,H any](world *World, filters ...Filter) *View8[A,B,C,D // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View8[A,B,C,D,E,F,G,H]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H) { +func (v *View8[A, B, C, D, E, F, G, H]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil } @@ -2062,7 +3042,6 @@ func (v *View8[A,B,C,D,E,F,G,H]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H) { return nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -2072,83 +3051,80 @@ func (v *View8[A,B,C,D,E,F,G,H]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H) { var retG *G var retH *H - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH } // Maps the lambda function across every entity which matched the specified filters. -func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H)) { +func (v *View8[A, B, C, D, E, F, G, H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -2159,14 +3135,15 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D sliceH, _ = v.storageH.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -2200,7 +3177,6 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D compH = sliceH.comp } - retA = nil retB = nil retC = nil @@ -2210,50 +3186,68 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D retG = nil retH = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -2276,48 +3270,246 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D // } } -// Deprecated: This API is a tentative alternative way to map -func (v *View8[A,B,C,D,E,F,G,H]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H)) { +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View8[A, B, C, D, E, F, G, H]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H)) { v.filter.regenerate(v.world) - id := make([][]Id, 0) + var sliceA *componentSlice[A] + var compA []A - - sliceListA := make([][]A, 0) - sliceListB := make([][]B, 0) - sliceListC := make([][]C, 0) - sliceListD := make([][]D, 0) - sliceListE := make([][]E, 0) - sliceListF := make([][]F, 0) - sliceListG := make([][]G, 0) - sliceListH := make([][]H, 0) + var sliceB *componentSlice[B] + var compB []B - for _, archId := range v.filter.archIds { - - sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH} + } + } + + close(newWorkChanel) + workDone.Wait() +} + +// Deprecated: This API is a tentative alternative way to map +func (v *View8[A, B, C, D, E, F, G, H]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H)) { + v.filter.regenerate(v.world) + + id := make([][]Id, 0) + + sliceListA := make([][]A, 0) + sliceListB := make([][]B, 0) + sliceListC := make([][]C, 0) + sliceListD := make([][]D, 0) + sliceListE := make([][]E, 0) + sliceListF := make([][]F, 0) + sliceListG := make([][]G, 0) + sliceListH := make([][]H, 0) + + for _, archId := range v.filter.archIds { + + sliceA, ok := v.storageA.slice[archId] + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -2330,21 +3522,20 @@ func (v *View8[A,B,C,D,E,F,G,H]) MapSlices(lambda func(id []Id, a []A, b []B, c for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], ) } } - // -------------------------------------------------------------------------------- // - View 9 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View9[A,B,C,D,E,F,G,H,I any] struct { - world *World +type View9[A, B, C, D, E, F, G, H, I any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -2357,14 +3548,13 @@ type View9[A,B,C,D,E,F,G,H,I any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View9[A,B,C,D,E,F,G,H,I]) initialize(world *World) any { +func (v *View9[A, B, C, D, E, F, G, H, I]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query9[A,B,C,D,E,F,G,H,I](world) + return Query9[A, B, C, D, E, F, G, H, I](world) } - // Creates a View for the specified world with the specified component filters. -func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C,D,E,F,G,H,I] { +func Query9[A, B, C, D, E, F, G, H, I any](world *World, filters ...Filter) *View9[A, B, C, D, E, F, G, H, I] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -2376,7 +3566,6 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C storageH := getStorage[H](world.engine) storageI := getStorage[I](world.engine) - var AA A var BB B var CC C @@ -2398,13 +3587,12 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C name(GG), name(HH), name(II), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View9[A,B,C,D,E,F,G,H,I]{ - world: world, + v := &View9[A, B, C, D, E, F, G, H, I]{ + world: world, filter: filterList, storageA: storageA, @@ -2424,7 +3612,7 @@ func Query9[A,B,C,D,E,F,G,H,I any](world *World, filters ...Filter) *View9[A,B,C // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View9[A,B,C,D,E,F,G,H,I]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I) { +func (v *View9[A, B, C, D, E, F, G, H, I]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -2446,7 +3634,6 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I) { return nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -2457,91 +3644,88 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I) { var retH *H var retI *I - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI } // Maps the lambda function across every entity which matched the specified filters. -func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I)) { +func (v *View9[A, B, C, D, E, F, G, H, I]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - + var sliceI *componentSlice[I] var compI []I var retI *I - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -2553,14 +3737,15 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d sliceI, _ = v.storageI.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -2598,7 +3783,6 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d compI = sliceI.comp } - retA = nil retB = nil retC = nil @@ -2609,51 +3793,71 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d retH = nil retI = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -2676,13 +3880,206 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapId(lambda func(id Id, a *A, b *B, c *C, d // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View9[A, B, C, D, E, F, G, H, I]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + var sliceI *componentSlice[I] + var compI []I + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View9[A,B,C,D,E,F,G,H,I]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I)) { +func (v *View9[A, B, C, D, E, F, G, H, I]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -2694,33 +4091,53 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapSlices(lambda func(id []Id, a []A, b []B, sliceListI := make([][]I, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -2734,21 +4151,20 @@ func (v *View9[A,B,C,D,E,F,G,H,I]) MapSlices(lambda func(id []Id, a []A, b []B, for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], ) } } - // -------------------------------------------------------------------------------- // - View 10 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View10[A,B,C,D,E,F,G,H,I,J any] struct { - world *World +type View10[A, B, C, D, E, F, G, H, I, J any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -2762,14 +4178,13 @@ type View10[A,B,C,D,E,F,G,H,I,J any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View10[A,B,C,D,E,F,G,H,I,J]) initialize(world *World) any { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query10[A,B,C,D,E,F,G,H,I,J](world) + return Query10[A, B, C, D, E, F, G, H, I, J](world) } - // Creates a View for the specified world with the specified component filters. -func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A,B,C,D,E,F,G,H,I,J] { +func Query10[A, B, C, D, E, F, G, H, I, J any](world *World, filters ...Filter) *View10[A, B, C, D, E, F, G, H, I, J] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -2782,7 +4197,6 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A storageI := getStorage[I](world.engine) storageJ := getStorage[J](world.engine) - var AA A var BB B var CC C @@ -2806,13 +4220,12 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A name(HH), name(II), name(JJ), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View10[A,B,C,D,E,F,G,H,I,J]{ - world: world, + v := &View10[A, B, C, D, E, F, G, H, I, J]{ + world: world, filter: filterList, storageA: storageA, @@ -2833,7 +4246,7 @@ func Query10[A,B,C,D,E,F,G,H,I,J any](world *World, filters ...Filter) *View10[A // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View10[A,B,C,D,E,F,G,H,I,J]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J) { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I, *J) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -2855,7 +4268,6 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -2867,99 +4279,96 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J var retI *I var retJ *J - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - sliceJ, ok := v.storageJ.slice[archId] + sliceJ, ok := v.storageJ.slice[archId] if ok { retJ = &sliceJ.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ } // Maps the lambda function across every entity which matched the specified filters. -func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J)) { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - + var sliceI *componentSlice[I] var compI []I var retI *I - + var sliceJ *componentSlice[J] var compJ []J var retJ *J - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -2972,14 +4381,15 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, sliceJ, _ = v.storageJ.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -3021,7 +4431,6 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, compJ = sliceJ.comp } - retA = nil retB = nil retC = nil @@ -3033,52 +4442,74 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, retI = nil retJ = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } - if compJ != nil { retJ = &compJ[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } + if compJ != nil { + retJ = &compJ[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -3101,13 +4532,219 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapId(lambda func(id Id, a *A, b *B, c *C, // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View10[A, B, C, D, E, F, G, H, I, J]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + var sliceI *componentSlice[I] + var compI []I + + var sliceJ *componentSlice[J] + var compJ []J + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + j []J + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + var paramJ *J + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + if newWork.j != nil { + paramJ = &newWork.j[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI, paramJ) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + sliceJ, _ = v.storageJ.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + compJ = nil + if sliceJ != nil { + compJ = sliceJ.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J)) { +func (v *View10[A, B, C, D, E, F, G, H, I, J]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -3120,35 +4757,57 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapSlices(lambda func(id []Id, a []A, b [] sliceListJ := make([][]J, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceJ, ok := v.storageJ.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -3163,21 +4822,20 @@ func (v *View10[A,B,C,D,E,F,G,H,I,J]) MapSlices(lambda func(id []Id, a []A, b [] for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx],sliceListJ[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], sliceListJ[idx], ) } } - // -------------------------------------------------------------------------------- // - View 11 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View11[A,B,C,D,E,F,G,H,I,J,K any] struct { - world *World +type View11[A, B, C, D, E, F, G, H, I, J, K any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -3192,14 +4850,13 @@ type View11[A,B,C,D,E,F,G,H,I,J,K any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) initialize(world *World) any { +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query11[A,B,C,D,E,F,G,H,I,J,K](world) + return Query11[A, B, C, D, E, F, G, H, I, J, K](world) } - // Creates a View for the specified world with the specified component filters. -func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11[A,B,C,D,E,F,G,H,I,J,K] { +func Query11[A, B, C, D, E, F, G, H, I, J, K any](world *World, filters ...Filter) *View11[A, B, C, D, E, F, G, H, I, J, K] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -3213,7 +4870,6 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 storageJ := getStorage[J](world.engine) storageK := getStorage[K](world.engine) - var AA A var BB B var CC C @@ -3239,13 +4895,12 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 name(II), name(JJ), name(KK), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View11[A,B,C,D,E,F,G,H,I,J,K]{ - world: world, + v := &View11[A, B, C, D, E, F, G, H, I, J, K]{ + world: world, filter: filterList, storageA: storageA, @@ -3267,7 +4922,7 @@ func Query11[A,B,C,D,E,F,G,H,I,J,K any](world *World, filters ...Filter) *View11 // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J,*K) { +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I, *J, *K) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -3289,7 +4944,6 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I, return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -3302,107 +4956,401 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I, var retJ *J var retK *K - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - sliceJ, ok := v.storageJ.slice[archId] + sliceJ, ok := v.storageJ.slice[archId] if ok { retJ = &sliceJ.comp[index] } - sliceK, ok := v.storageK.slice[archId] + sliceK, ok := v.storageK.slice[archId] if ok { retK = &sliceK.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK } -// Maps the lambda function across every entity which matched the specified filters. -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K)) { +// Maps the lambda function across every entity which matched the specified filters. +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + var retA *A + + var sliceB *componentSlice[B] + var compB []B + var retB *B + + var sliceC *componentSlice[C] + var compC []C + var retC *C + + var sliceD *componentSlice[D] + var compD []D + var retD *D + + var sliceE *componentSlice[E] + var compE []E + var retE *E + + var sliceF *componentSlice[F] + var compF []F + var retF *F + + var sliceG *componentSlice[G] + var compG []G + var retG *G + + var sliceH *componentSlice[H] + var compH []H + var retH *H + + var sliceI *componentSlice[I] + var compI []I + var retI *I + + var sliceJ *componentSlice[J] + var compJ []J + var retJ *J + + var sliceK *componentSlice[K] + var compK []K + var retK *K + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + sliceJ, _ = v.storageJ.slice[archId] + sliceK, _ = v.storageK.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + compJ = nil + if sliceJ != nil { + compJ = sliceJ.comp + } + compK = nil + if sliceK != nil { + compK = sliceK.comp + } + + retA = nil + retB = nil + retC = nil + retD = nil + retE = nil + retF = nil + retG = nil + retH = nil + retI = nil + retJ = nil + retK = nil + for idx := range ids { + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } + if compJ != nil { + retJ = &compJ[idx] + } + if compK != nil { + retK = &compK[idx] + } + lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK) + } + + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional + // for _, archId := range v.filter.archIds { + // aSlice, ok := v.storageA.slice[archId] + // if !ok { continue } + // bSlice, ok := v.storageB.slice[archId] + // if !ok { continue } + + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + + // ids := lookup.id + // aComp := aSlice.comp + // bComp := bSlice.comp + // if len(ids) != len(aComp) || len(ids) != len(bComp) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &aComp[i], &bComp[i]) + // } + // } +} + +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A - var retA *A - + var sliceB *componentSlice[B] var compB []B - var retB *B - + var sliceC *componentSlice[C] var compC []C - var retC *C - + var sliceD *componentSlice[D] var compD []D - var retD *D - + var sliceE *componentSlice[E] var compE []E - var retE *E - + var sliceF *componentSlice[F] var compF []F - var retF *F - + var sliceG *componentSlice[G] var compG []G - var retG *G - + var sliceH *componentSlice[H] var compH []H - var retH *H - + var sliceI *componentSlice[I] var compI []I - var retI *I - + var sliceJ *componentSlice[J] var compJ []J - var retJ *J - + var sliceK *componentSlice[K] var compK []K - var retK *K - + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + j []J + k []K + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + var paramJ *J + var paramK *K + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + if newWork.j != nil { + paramJ = &newWork.j[i] + } + if newWork.k != nil { + paramK = &newWork.k[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI, paramJ, paramK) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -3416,14 +5364,13 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapId(lambda func(id Id, a *A, b *B, c * sliceK, _ = v.storageK.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - compA = nil if sliceA != nil { compA = sliceA.comp @@ -3469,95 +5416,42 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapId(lambda func(id Id, a *A, b *B, c * compK = sliceK.comp } - - retA = nil - retB = nil - retC = nil - retD = nil - retE = nil - retF = nil - retG = nil - retH = nil - retI = nil - retJ = nil - retK = nil + startWorkRangeIndex := -1 for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } - if compJ != nil { retJ = &compJ[idx] } - if compK != nil { retK = &compK[idx] } - lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK) + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK} + startWorkRangeIndex = -1 + } } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK} + } } - // Original - doesn't handle optional - // for _, archId := range v.filter.archIds { - // aSlice, ok := v.storageA.slice[archId] - // if !ok { continue } - // bSlice, ok := v.storageB.slice[archId] - // if !ok { continue } - - // lookup, ok := v.world.engine.lookup[archId] - // if !ok { panic("LookupList is missing!") } - - // ids := lookup.id - // aComp := aSlice.comp - // bComp := bSlice.comp - // if len(ids) != len(aComp) || len(ids) != len(bComp) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &aComp[i], &bComp[i]) - // } - // } + close(newWorkChanel) + workDone.Wait() } // Deprecated: This API is a tentative alternative way to map -func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K)) { +func (v *View11[A, B, C, D, E, F, G, H, I, J, K]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -3571,37 +5465,61 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapSlices(lambda func(id []Id, a []A, b sliceListK := make([][]K, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceJ, ok := v.storageJ.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceK, ok := v.storageK.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -3617,21 +5535,20 @@ func (v *View11[A,B,C,D,E,F,G,H,I,J,K]) MapSlices(lambda func(id []Id, a []A, b for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx],sliceListJ[idx],sliceListK[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], sliceListJ[idx], sliceListK[idx], ) } } - // -------------------------------------------------------------------------------- // - View 12 // -------------------------------------------------------------------------------- // Represents a view of data in a specific world. Provides access to the components specified in the generic block -type View12[A,B,C,D,E,F,G,H,I,J,K,L any] struct { - world *World +type View12[A, B, C, D, E, F, G, H, I, J, K, L any] struct { + world *World filter filterList - + storageA *componentSliceStorage[A] storageB *componentSliceStorage[B] storageC *componentSliceStorage[C] @@ -3647,14 +5564,13 @@ type View12[A,B,C,D,E,F,G,H,I,J,K,L any] struct { } // implement the intializer interface so that it can be automatically created and injected into systems -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) initialize(world *World) any { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) initialize(world *World) any { // TODO: filters need to be a part of the query type - return Query12[A,B,C,D,E,F,G,H,I,J,K,L](world) + return Query12[A, B, C, D, E, F, G, H, I, J, K, L](world) } - // Creates a View for the specified world with the specified component filters. -func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View12[A,B,C,D,E,F,G,H,I,J,K,L] { +func Query12[A, B, C, D, E, F, G, H, I, J, K, L any](world *World, filters ...Filter) *View12[A, B, C, D, E, F, G, H, I, J, K, L] { storageA := getStorage[A](world.engine) storageB := getStorage[B](world.engine) @@ -3669,7 +5585,6 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View storageK := getStorage[K](world.engine) storageL := getStorage[L](world.engine) - var AA A var BB B var CC C @@ -3697,13 +5612,12 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View name(JJ), name(KK), name(LL), - } filterList := newFilterList(comps, filters...) filterList.regenerate(world) - v := &View12[A,B,C,D,E,F,G,H,I,J,K,L]{ - world: world, + v := &View12[A, B, C, D, E, F, G, H, I, J, K, L]{ + world: world, filter: filterList, storageA: storageA, @@ -3726,7 +5640,7 @@ func Query12[A,B,C,D,E,F,G,H,I,J,K,L any](world *World, filters ...Filter) *View // Read will return even if the specified id doesn't match the filter list // Read will return the value if it exists, else returns nil. // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,*I,*J,*K,*L) { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) Read(id Id) (*A, *B, *C, *D, *E, *F, *G, *H, *I, *J, *K, *L) { if id == InvalidEntity { return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } @@ -3748,7 +5662,6 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,* return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil } - var retA *A var retB *B var retC *C @@ -3762,115 +5675,112 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) Read(id Id) (*A,*B,*C,*D,*E,*F,*G,*H,* var retK *K var retL *L - sliceA, ok := v.storageA.slice[archId] + sliceA, ok := v.storageA.slice[archId] if ok { retA = &sliceA.comp[index] } - sliceB, ok := v.storageB.slice[archId] + sliceB, ok := v.storageB.slice[archId] if ok { retB = &sliceB.comp[index] } - sliceC, ok := v.storageC.slice[archId] + sliceC, ok := v.storageC.slice[archId] if ok { retC = &sliceC.comp[index] } - sliceD, ok := v.storageD.slice[archId] + sliceD, ok := v.storageD.slice[archId] if ok { retD = &sliceD.comp[index] } - sliceE, ok := v.storageE.slice[archId] + sliceE, ok := v.storageE.slice[archId] if ok { retE = &sliceE.comp[index] } - sliceF, ok := v.storageF.slice[archId] + sliceF, ok := v.storageF.slice[archId] if ok { retF = &sliceF.comp[index] } - sliceG, ok := v.storageG.slice[archId] + sliceG, ok := v.storageG.slice[archId] if ok { retG = &sliceG.comp[index] } - sliceH, ok := v.storageH.slice[archId] + sliceH, ok := v.storageH.slice[archId] if ok { retH = &sliceH.comp[index] } - sliceI, ok := v.storageI.slice[archId] + sliceI, ok := v.storageI.slice[archId] if ok { retI = &sliceI.comp[index] } - sliceJ, ok := v.storageJ.slice[archId] + sliceJ, ok := v.storageJ.slice[archId] if ok { retJ = &sliceJ.comp[index] } - sliceK, ok := v.storageK.slice[archId] + sliceK, ok := v.storageK.slice[archId] if ok { retK = &sliceK.comp[index] } - sliceL, ok := v.storageL.slice[archId] + sliceL, ok := v.storageL.slice[archId] if ok { retL = &sliceL.comp[index] } - return retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK, retL } // Maps the lambda function across every entity which matched the specified filters. -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K, l *L)) { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) MapId(lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K, l *L)) { v.filter.regenerate(v.world) - var sliceA *componentSlice[A] var compA []A var retA *A - + var sliceB *componentSlice[B] var compB []B var retB *B - + var sliceC *componentSlice[C] var compC []C var retC *C - + var sliceD *componentSlice[D] var compD []D var retD *D - + var sliceE *componentSlice[E] var compE []E var retE *E - + var sliceF *componentSlice[F] var compF []F var retF *F - + var sliceG *componentSlice[G] var compG []G var retG *G - + var sliceH *componentSlice[H] var compH []H var retH *H - + var sliceI *componentSlice[I] var compI []I var retI *I - + var sliceJ *componentSlice[J] var compJ []J var retJ *J - + var sliceK *componentSlice[K] var compK []K var retK *K - + var sliceL *componentSlice[L] var compL []L var retL *L - for _, archId := range v.filter.archIds { - + sliceA, _ = v.storageA.slice[archId] sliceB, _ = v.storageB.slice[archId] sliceC, _ = v.storageC.slice[archId] @@ -3885,14 +5795,15 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c sliceL, _ = v.storageL.slice[archId] lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } ids := lookup.id - // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. - + compA = nil if sliceA != nil { compA = sliceA.comp @@ -3942,7 +5853,6 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c compL = sliceL.comp } - retA = nil retB = nil retC = nil @@ -3956,54 +5866,80 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c retK = nil retL = nil for idx := range ids { - if ids[idx] == InvalidEntity { continue } // Skip if its a hole - - if compA != nil { retA = &compA[idx] } - if compB != nil { retB = &compB[idx] } - if compC != nil { retC = &compC[idx] } - if compD != nil { retD = &compD[idx] } - if compE != nil { retE = &compE[idx] } - if compF != nil { retF = &compF[idx] } - if compG != nil { retG = &compG[idx] } - if compH != nil { retH = &compH[idx] } - if compI != nil { retI = &compI[idx] } - if compJ != nil { retJ = &compJ[idx] } - if compK != nil { retK = &compK[idx] } - if compL != nil { retL = &compL[idx] } + if ids[idx] == InvalidEntity { + continue + } // Skip if its a hole + + if compA != nil { + retA = &compA[idx] + } + if compB != nil { + retB = &compB[idx] + } + if compC != nil { + retC = &compC[idx] + } + if compD != nil { + retD = &compD[idx] + } + if compE != nil { + retE = &compE[idx] + } + if compF != nil { + retF = &compF[idx] + } + if compG != nil { + retG = &compG[idx] + } + if compH != nil { + retH = &compH[idx] + } + if compI != nil { + retI = &compI[idx] + } + if compJ != nil { + retJ = &compJ[idx] + } + if compK != nil { + retK = &compK[idx] + } + if compL != nil { + retL = &compL[idx] + } lambda(ids[idx], retA, retB, retC, retD, retE, retF, retG, retH, retI, retJ, retK, retL) } - // // Option 2 - This is faster but has a combinatorial explosion problem - // if compA == nil && compB == nil { - // return - // } else if compA != nil && compB == nil { - // if len(ids) != len(compA) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], nil) - // } - // } else if compA == nil && compB != nil { - // if len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], nil, &compB[i]) - // } - // } else if compA != nil && compB != nil { - // if len(ids) != len(compA) || len(ids) != len(compB) { - // panic("ERROR - Bounds don't match") - // } - // for i := range ids { - // if ids[i] == InvalidEntity { continue } - // lambda(ids[i], &compA[i], &compB[i]) - // } - // } - } - - // Original - doesn't handle optional + // // Option 2 - This is faster but has a combinatorial explosion problem + // if compA == nil && compB == nil { + // return + // } else if compA != nil && compB == nil { + // if len(ids) != len(compA) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], nil) + // } + // } else if compA == nil && compB != nil { + // if len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], nil, &compB[i]) + // } + // } else if compA != nil && compB != nil { + // if len(ids) != len(compA) || len(ids) != len(compB) { + // panic("ERROR - Bounds don't match") + // } + // for i := range ids { + // if ids[i] == InvalidEntity { continue } + // lambda(ids[i], &compA[i], &compB[i]) + // } + // } + } + + // Original - doesn't handle optional // for _, archId := range v.filter.archIds { // aSlice, ok := v.storageA.slice[archId] // if !ok { continue } @@ -4026,13 +5962,245 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapId(lambda func(id Id, a *A, b *B, c // } } +// Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) MapIdParallel(chunkSize int, lambda func(id Id, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H, i *I, j *J, k *K, l *L)) { + v.filter.regenerate(v.world) + + var sliceA *componentSlice[A] + var compA []A + + var sliceB *componentSlice[B] + var compB []B + + var sliceC *componentSlice[C] + var compC []C + + var sliceD *componentSlice[D] + var compD []D + + var sliceE *componentSlice[E] + var compE []E + + var sliceF *componentSlice[F] + var compF []F + + var sliceG *componentSlice[G] + var compG []G + + var sliceH *componentSlice[H] + var compH []H + + var sliceI *componentSlice[I] + var compI []I + + var sliceJ *componentSlice[J] + var compJ []J + + var sliceK *componentSlice[K] + var compK []K + + var sliceL *componentSlice[L] + var compL []L + + workDone := &sync.WaitGroup{} + type workPackage struct { + start int + end int + ids []Id + a []A + b []B + c []C + d []D + e []E + f []F + g []G + h []H + i []I + j []J + k []K + l []L + } + newWorkChanel := make(chan workPackage) + mapWorker := func() { + defer workDone.Done() + + for { + newWork, ok := <-newWorkChanel + if !ok { + return + } + + // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? + + var paramA *A + var paramB *B + var paramC *C + var paramD *D + var paramE *E + var paramF *F + var paramG *G + var paramH *H + var paramI *I + var paramJ *J + var paramK *K + var paramL *L + + for i := newWork.start; i < newWork.end; i++ { + + if newWork.a != nil { + paramA = &newWork.a[i] + } + if newWork.b != nil { + paramB = &newWork.b[i] + } + if newWork.c != nil { + paramC = &newWork.c[i] + } + if newWork.d != nil { + paramD = &newWork.d[i] + } + if newWork.e != nil { + paramE = &newWork.e[i] + } + if newWork.f != nil { + paramF = &newWork.f[i] + } + if newWork.g != nil { + paramG = &newWork.g[i] + } + if newWork.h != nil { + paramH = &newWork.h[i] + } + if newWork.i != nil { + paramI = &newWork.i[i] + } + if newWork.j != nil { + paramJ = &newWork.j[i] + } + if newWork.k != nil { + paramK = &newWork.k[i] + } + if newWork.l != nil { + paramL = &newWork.l[i] + } + + lambda(newWork.ids[i], paramA, paramB, paramC, paramD, paramE, paramF, paramG, paramH, paramI, paramJ, paramK, paramL) + } + } + } + parallelLevel := runtime.NumCPU() * 2 + for i := 0; i < parallelLevel; i++ { + go mapWorker() + } + + for _, archId := range v.filter.archIds { + + sliceA, _ = v.storageA.slice[archId] + sliceB, _ = v.storageB.slice[archId] + sliceC, _ = v.storageC.slice[archId] + sliceD, _ = v.storageD.slice[archId] + sliceE, _ = v.storageE.slice[archId] + sliceF, _ = v.storageF.slice[archId] + sliceG, _ = v.storageG.slice[archId] + sliceH, _ = v.storageH.slice[archId] + sliceI, _ = v.storageI.slice[archId] + sliceJ, _ = v.storageJ.slice[archId] + sliceK, _ = v.storageK.slice[archId] + sliceL, _ = v.storageL.slice[archId] + + lookup := v.world.engine.lookup[archId] + if lookup == nil { + panic("LookupList is missing!") + } + // lookup, ok := v.world.engine.lookup[archId] + // if !ok { panic("LookupList is missing!") } + ids := lookup.id + + compA = nil + if sliceA != nil { + compA = sliceA.comp + } + compB = nil + if sliceB != nil { + compB = sliceB.comp + } + compC = nil + if sliceC != nil { + compC = sliceC.comp + } + compD = nil + if sliceD != nil { + compD = sliceD.comp + } + compE = nil + if sliceE != nil { + compE = sliceE.comp + } + compF = nil + if sliceF != nil { + compF = sliceF.comp + } + compG = nil + if sliceG != nil { + compG = sliceG.comp + } + compH = nil + if sliceH != nil { + compH = sliceH.comp + } + compI = nil + if sliceI != nil { + compI = sliceI.comp + } + compJ = nil + if sliceJ != nil { + compJ = sliceJ.comp + } + compK = nil + if sliceK != nil { + compK = sliceK.comp + } + compL = nil + if sliceL != nil { + compL = sliceL.comp + } + + startWorkRangeIndex := -1 + for idx := range ids { + //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. + if ids[idx] == InvalidEntity { + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK, l: compL} + startWorkRangeIndex = -1 + } + continue + } // Skip if its a hole + + if startWorkRangeIndex == -1 { + startWorkRangeIndex = idx + } + + if idx-startWorkRangeIndex >= chunkSize { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx + 1, ids: ids, a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK, l: compL} + startWorkRangeIndex = -1 + } + } + + if startWorkRangeIndex != -1 { + newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), a: compA, b: compB, c: compC, d: compD, e: compE, f: compF, g: compG, h: compH, i: compI, j: compJ, k: compK, l: compL} + } + } + + close(newWorkChanel) + workDone.Wait() +} + // Deprecated: This API is a tentative alternative way to map -func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K, l []L)) { +func (v *View12[A, B, C, D, E, F, G, H, I, J, K, L]) MapSlices(lambda func(id []Id, a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I, j []J, k []K, l []L)) { v.filter.regenerate(v.world) id := make([][]Id, 0) - sliceListA := make([][]A, 0) sliceListB := make([][]B, 0) sliceListC := make([][]C, 0) @@ -4047,39 +6215,65 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapSlices(lambda func(id []Id, a []A, sliceListL := make([][]L, 0) for _, archId := range v.filter.archIds { - + sliceA, ok := v.storageA.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceB, ok := v.storageB.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceC, ok := v.storageC.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceD, ok := v.storageD.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceE, ok := v.storageE.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceF, ok := v.storageF.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceG, ok := v.storageG.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceH, ok := v.storageH.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceI, ok := v.storageI.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceJ, ok := v.storageJ.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceK, ok := v.storageK.slice[archId] - if !ok { continue } + if !ok { + continue + } sliceL, ok := v.storageL.slice[archId] - if !ok { continue } + if !ok { + continue + } lookup := v.world.engine.lookup[archId] - if lookup == nil { panic("LookupList is missing!") } + if lookup == nil { + panic("LookupList is missing!") + } // lookup, ok := v.world.engine.lookup[archId] // if !ok { panic("LookupList is missing!") } id = append(id, lookup.id) - + sliceListA = append(sliceListA, sliceA.comp) sliceListB = append(sliceListB, sliceB.comp) sliceListC = append(sliceListC, sliceC.comp) @@ -4096,8 +6290,7 @@ func (v *View12[A,B,C,D,E,F,G,H,I,J,K,L]) MapSlices(lambda func(id []Id, a []A, for idx := range id { lambda(id[idx], - sliceListA[idx],sliceListB[idx],sliceListC[idx],sliceListD[idx],sliceListE[idx],sliceListF[idx],sliceListG[idx],sliceListH[idx],sliceListI[idx],sliceListJ[idx],sliceListK[idx],sliceListL[idx], + sliceListA[idx], sliceListB[idx], sliceListC[idx], sliceListD[idx], sliceListE[idx], sliceListF[idx], sliceListG[idx], sliceListH[idx], sliceListI[idx], sliceListJ[idx], sliceListK[idx], sliceListL[idx], ) } } -