From 839568fb5fd8b779dc2e7a8aa5541a9b0264080f Mon Sep 17 00:00:00 2001 From: Mykola Zhyhallo Date: Wed, 18 Oct 2023 22:56:33 +0200 Subject: [PATCH 1/6] 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], ) } } - From f0935be0fe9fe7e10eace068f40a9cf5426d3ebd Mon Sep 17 00:00:00 2001 From: Mykola Zhyhallo Date: Wed, 25 Oct 2023 01:08:01 +0200 Subject: [PATCH 2/6] Added systems groups and better systems managing --- README.md | 63 +++++- arch.go | 34 +-- bundle.go | 34 ++- component.go | 26 +-- entity.go | 17 +- filter.go | 18 +- go.mod | 5 +- go.sum | 2 + name.go | 10 +- system.go | 393 --------------------------------- system/group/componentGuard.go | 160 ++++++++++++++ system/group/error.go | 30 +++ system/group/fixed.go | 188 ++++++++++++++++ system/group/group.go | 146 ++++++++++++ system/group/orderGuard.go | 84 +++++++ system/group/realtime.go | 175 +++++++++++++++ system/group/statistics.go | 74 +++++++ system/group/step.go | 192 ++++++++++++++++ system/system.go | 61 +++++ system_test.go | 74 ------- view_gen.go | 24 +- 21 files changed, 1259 insertions(+), 551 deletions(-) delete mode 100644 system.go create mode 100644 system/group/componentGuard.go create mode 100644 system/group/error.go create mode 100644 system/group/fixed.go create mode 100644 system/group/group.go create mode 100644 system/group/orderGuard.go create mode 100644 system/group/realtime.go create mode 100644 system/group/statistics.go create mode 100644 system/group/step.go create mode 100644 system/system.go delete mode 100644 system_test.go diff --git a/README.md b/README.md index 264572d..1979cde 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ ecs.MapN(world, func(id ecs.Id, a *ComponentA, /*... */, n *ComponentN) { }) ``` -### Advanced queries +#### Advanced queries You can also filter your queries for more advanced usage: ``` // Returns a view of Position and Velocity, but only if the entity also has the `Rotation` component. @@ -78,6 +78,67 @@ query := ecs.Query2[Position, Velocity](world, ecs.With(Rotation)) query := ecs.Query2[Position, Velocity](world, ecs.Optional(Velocity)) ``` +### Systems +There are several types of systems: + 1. Realtime - should be runned as fast as possible (without any deplays). Usefull for Rendering or collecting inputs from user. + 2. Fixed - should be runned ones a specified period of time. Usefull for physics. + 3. Step - can only be runned by manually triggering. Usefull for simulations, like stable updates in P2P games. + +To create system, implement one of the interfaces `RealtimeSystem`, `FixedSystem`, `StepSystem`: +``` +type Position struct { + X, Y float64 +} +type Velocity struct { + X, Y float64 +} + +// --- world initialization here --- + +type movementSystem struct {} + +func (s *movementSystem) GetName() { + return "physics::movement" +} + +func (s *movementSystem) RunFixed(delta time.Duration) { + query := ecs.Query2[Position, Velocity](world) + query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) { + // No need to adjust here to delta time between updates, because + // physics steps should always be the same. Delta time is still available to systems, where + // it can be important (f.e. if we want to detect "throttling") + pos.X += vel.X + pos.Y += vel.Y + + // Add some drag + vel.X *= 0.9 + vel.Y *= 0.9 + }) +} +``` + +To shedule the system, we need to have a system group of one of the types `RealtimeGroup`, `FixedGroup`, `StepGroup`. There are several rules regarding system groups: +1. One system cant belong to multiple groups. +2. Groups are running in parallel and there is no synchronization between them. +3. All the systems in the group can be automatically runned in parallel. +4. Groups share lock to the components on the systems level, so there will be no race conditions while accessing components data. +5. Group can only contain systems with theirs type. + +``` +componentsGuard = group.NewComponentsGuard() +physics = group.NewFixedGroup("physics", 50*time.Millisecond, componentsGuard) +physics.AddSystem(&movementSystem{}) +physics.StartFixed() +defer physics.StopFixed() +``` + +#### Order +You can specify systems order by implementing `system.RunBeforeSystem` or/and `system.RunAfterSystem` interface for the system. You cant implement order between systems from multiple groups. + +#### Automatic parallelism +In order for sheduler to understand which systems can be runed in parallel, you have to specify components used by systems. By default, if component will not be specified, system will be marked as `exclusive` and will only be runned by locking entire ECS world. +You can do this by implementing `system.ReadComponentsSystem` or/and `system.WriteComponentsSystem` interface for the system. Make sure to use `system.ReadComponentsSystem` as much as possible, because with read access, multiple systems can be runned at the same time. + ### Commands Commands will eventually replace `ecs.Write(...)` once I figure out how their usage will work. Commands essentially buffer some work on the ECS so that the work can be executed later on. You can use them in loop safe ways by calling `Execute()` after your loop has completed. Right now they work like this: diff --git a/arch.go b/arch.go index 00726fd..ef7b37b 100644 --- a/arch.go +++ b/arch.go @@ -5,6 +5,7 @@ import ( ) // This is the identifier for entities in the world +// //cod:struct type Id uint32 @@ -28,17 +29,17 @@ func (s *componentSlice[T]) Write(index int, val T) { // TODO: Rename, this is kind of like an archetype header type lookupList struct { - index *internalMap[Id,int] // A mapping from entity ids to array indices - id []Id // An array of every id in the arch list (essentially a reverse mapping from index to Id) - holes []int // List of indexes that have ben deleted - mask archetypeMask + index *internalMap[Id, int] // A mapping from entity ids to array indices + id []Id // An array of every id in the arch list (essentially a reverse mapping from index to Id) + holes []int // List of indexes that have ben deleted + mask archetypeMask } // Adds ourselves to the last available hole, else appends // Returns the index func (l *lookupList) addToEasiestHole(id Id) int { if len(l.holes) > 0 { - lastHoleIndex := len(l.holes)-1 + lastHoleIndex := len(l.holes) - 1 index := l.holes[lastHoleIndex] l.id[index] = id l.index.Put(id, index) @@ -54,7 +55,6 @@ func (l *lookupList) addToEasiestHole(id Id) int { } } - type storage interface { ReadToEntity(*Entity, archetypeId, int) bool ReadToRawEntity(*RawEntity, archetypeId, int) bool @@ -105,12 +105,12 @@ func (s *componentSliceStorage[T]) print(amount int) { // Provides generic storage for all archetypes type archEngine struct { - generation int + generation int // archCounter archetypeId - lookup []*lookupList // Indexed by archetypeId - compSliceStorage []storage // Indexed by componentId - dcr *componentRegistry + lookup []*lookupList // Indexed by archetypeId + compSliceStorage []storage // Indexed by componentId + dcr *componentRegistry // TODO - using this makes things not thread safe inside the engine archCount map[archetypeId]int @@ -118,10 +118,10 @@ type archEngine struct { func newArchEngine() *archEngine { return &archEngine{ - generation: 1, // Start at 1 so that anyone with the default int value will always realize they are in the wrong generation + generation: 1, // Start at 1 so that anyone with the default int value will always realize they are in the wrong generation lookup: make([]*lookupList, 0, DefaultAllocation), - compSliceStorage: make([]storage, maxComponentId + 1), + compSliceStorage: make([]storage, maxComponentId+1), dcr: newComponentRegistry(), archCount: make(map[archetypeId]int), } @@ -133,10 +133,10 @@ func (e *archEngine) newArchetypeId(archMask archetypeMask) archetypeId { archId := archetypeId(len(e.lookup)) e.lookup = append(e.lookup, &lookupList{ - index: newMap[Id,int](0), + index: newMap[Id, int](0), id: make([]Id, 0, DefaultAllocation), holes: make([]int, 0, DefaultAllocation), - mask: archMask, + mask: archMask, }, ) @@ -163,7 +163,7 @@ func (e *archEngine) getGeneration() int { // } func (e *archEngine) count(anything ...any) int { - comps := make([]componentId, len(anything)) + comps := make([]ComponentId, len(anything)) for i, c := range anything { comps[i] = name(c) } @@ -190,7 +190,7 @@ func (e *archEngine) getArchetypeId(comp ...Component) archetypeId { } // TODO - map might be slower than just having an array. I could probably do a big bitmask and then just do a logical OR -func (e *archEngine) FilterList(archIds []archetypeId, comp []componentId) []archetypeId { +func (e *archEngine) FilterList(archIds []archetypeId, comp []ComponentId) []archetypeId { // TODO: could I maybe do something more optimal with archetypeMask? // New way: With archSets that are just slices // Logic: Go thorugh and keep track of how many times we see each archetype. Then only keep the archetypes that we've seen an amount of times equal to the number of components. If we have 5 components and see 5 for a specific archId, it means that each component has that archId @@ -231,7 +231,7 @@ func getStorage[T any](e *archEngine) *componentSliceStorage[T] { } // Note: This will panic if the wrong compId doesn't match the generic type -func getStorageByCompId[T any](e *archEngine, compId componentId) *componentSliceStorage[T] { +func getStorageByCompId[T any](e *archEngine, compId ComponentId) *componentSliceStorage[T] { ss := e.compSliceStorage[compId] if ss == nil { ss = &componentSliceStorage[T]{ diff --git a/bundle.go b/bundle.go index f868b47..5befb11 100644 --- a/bundle.go +++ b/bundle.go @@ -1,7 +1,7 @@ package ecs type Bundle[T any] struct { - compId componentId + compId ComponentId storage *componentSliceStorage[T] // world *ecs.World //Needed? } @@ -11,14 +11,14 @@ func NewBundle[T any](world *World) Bundle[T] { var t T compId := name(t) return Bundle[T]{ - compId: compId, + compId: compId, storage: getStorageByCompId[T](world.engine, compId), } } func (c Bundle[T]) New(comp T) Box[T] { return Box[T]{ - Comp: comp, + Comp: comp, compId: c.compId, // storage: c.storage, } @@ -28,17 +28,17 @@ func (b Bundle[T]) write(engine *archEngine, archId archetypeId, index int, comp writeArch[T](engine, archId, index, b.storage, comp) } -func (b Bundle[T]) id() componentId { +func (b Bundle[T]) id() ComponentId { return b.compId } type Bundle4[A, B, C, D any] struct { - compId componentId - boxA *Box[A] - boxB *Box[B] - boxC *Box[C] - boxD *Box[D] - comps []Component + compId ComponentId + boxA *Box[A] + boxB *Box[B] + boxC *Box[C] + boxD *Box[D] + comps []Component } // Createst the boxed component type @@ -56,16 +56,15 @@ func NewBundle4[A, B, C, D any]() Bundle4[A, B, C, D] { } return Bundle4[A, B, C, D]{ - boxA: boxA, - boxB: boxB, - boxC: boxC, - boxD: boxD, + boxA: boxA, + boxB: boxB, + boxC: boxC, + boxD: boxD, comps: comps, } } - -func (bun Bundle4[A,B,C,D]) Write(world *World, id Id, a A, b B, c C, d D) { +func (bun Bundle4[A, B, C, D]) Write(world *World, id Id, a A, b B, c C, d D) { // bun.boxA.Comp = a // bun.boxB.Comp = b // bun.boxC.Comp = c @@ -86,7 +85,6 @@ func (bun Bundle4[A,B,C,D]) Write(world *World, id Id, a A, b B, c C, d D) { bun.boxD.Comp = d Write(world, id, - bun.comps... + bun.comps..., ) } - diff --git a/component.go b/component.go index 3a0377d..3ed1f77 100644 --- a/component.go +++ b/component.go @@ -5,17 +5,17 @@ import ( // "sort" ) -type componentId uint16 +type ComponentId uint16 type Component interface { write(*archEngine, archetypeId, int) - id() componentId + Id() ComponentId } // This type is used to box a component with all of its type info so that it implements the component interface. I would like to get rid of this and simplify the APIs type Box[T any] struct { Comp T - compId componentId + compId ComponentId } // Createst the boxed component type @@ -26,10 +26,10 @@ func C[T any](comp T) Box[T] { } } func (c Box[T]) write(engine *archEngine, archId archetypeId, index int) { - store := getStorageByCompId[T](engine, c.id()) + store := getStorageByCompId[T](engine, c.Id()) writeArch[T](engine, archId, index, store, c.Comp) } -func (c Box[T]) id() componentId { +func (c Box[T]) Id() ComponentId { if c.compId == invalidComponentId { c.compId = name(c.Comp) } @@ -40,20 +40,20 @@ func (c Box[T]) Get() T { return c.Comp } - // Note: you can increase max component size by increasing maxComponentId and archetypeMask // TODO: I should have some kind of panic if you go over maximum component size const maxComponentId = 255 + // Supports maximum 256 unique component types type archetypeMask [4]uint64 // TODO: can/should I make this configurable? func buildArchMask(comps ...Component) archetypeMask { var mask archetypeMask for _, comp := range comps { // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) - c := comp.id() + c := comp.Id() idx := c / 64 offset := c - (64 * idx) - mask[idx] |= (1<