Skip to content

Commit

Permalink
Rewrite archEngine filter function for performance and multithreading
Browse files Browse the repository at this point in the history
  • Loading branch information
unitoftime committed Oct 14, 2024
1 parent 7da6e1a commit 9cb278e
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 45 deletions.
92 changes: 60 additions & 32 deletions arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,10 @@ func (s *componentSliceStorage[T]) print(amount int) {
// Provides generic storage for all archetypes
type archEngine struct {
generation int
// archCounter archetypeId

lookup []*lookupList // Indexed by archetypeId
compSliceStorage []storage // Indexed by componentId
dcr *componentRegistry

// TODO - using this makes things not thread safe inside the engine
archCount map[archetypeId]int
}

func newArchEngine() *archEngine {
Expand All @@ -127,7 +123,6 @@ func newArchEngine() *archEngine {
lookup: make([]*lookupList, 0, DefaultAllocation),
compSliceStorage: make([]storage, maxComponentId+1),
dcr: newComponentRegistry(),
archCount: make(map[archetypeId]int),
}
}

Expand Down Expand Up @@ -193,39 +188,72 @@ func (e *archEngine) getArchetypeId(comp ...Component) archetypeId {
return e.dcr.getArchetypeId(e, comp...)
}

// TODO - map might be slower than just having an array. I could probably do a big bitmask and then just do a logical OR
// Returns replaces archIds with a list of archids that match the compId list
func (e *archEngine) FilterList(archIds []archetypeId, comp []componentId) []archetypeId {
// TODO: could I maybe do something more optimal with archetypeMask?
// New way: With archSets that are just slices
// Logic: Go thorugh and keep track of how many times we see each archetype. Then only keep the archetypes that we've seen an amount of times equal to the number of components. If we have 5 components and see 5 for a specific archId, it means that each component has that archId
// TODO: this may be more efficient to use a slice?

// Clearing Optimization: https://go.dev/doc/go1.11#performance-compiler
for k := range e.archCount {
delete(e.archCount, k)
}

for _, compId := range comp {
for _, archId := range e.dcr.archSet[compId] {
e.archCount[archId] = e.archCount[archId] + 1
}
}

numComponents := len(comp)
// Idea 3: Loop through every registered archMask to see if it matches
// Problem - Forces you to check every arch mask, even if the
// The good side is that you dont need to deduplicate your list, and you dont need to allocate
requiredArchMask := buildArchMaskFromId(comp...)

archIds = archIds[:0]
for archId, count := range e.archCount {
if count >= numComponents {
archIds = append(archIds, archId)

// // TODO: How tight do I want my tolerances?
// if count > numComponents {
// panic("AAAA")
// }
for archId := range e.dcr.revArchMask {
if requiredArchMask.contains(e.dcr.revArchMask[archId]) {
archIds = append(archIds, archetypeId(archId))
}
}

return archIds

//--------------------------------------------------------------------------------
// Idea 2: Loop through every archMask that every componentId points to
// // TODO: could I maybe do something more optimal with archetypeMask? Something like this could work.
// requiredArchMask := buildArchMaskFromId(comp...)

// archCount := make(map[archetypeId]struct{})

// archIds = archIds[:0]
// for _, compId := range comp {
// for _, archId := range e.dcr.archSet[compId] {
// archMask, ok := e.dcr.revArchMask[archId]
// if !ok {
// panic("AAA")
// continue
// } // TODO: This shouldn't happen?
// if requiredArchMask.contains(archMask) {
// archCount[archId] = struct{}{}
// }
// }
// }

// for archId := range archCount {
// archIds = append(archIds, archId)
// }
// return archIds

// --------------------------------------------------------------------------------
// // Old 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

// // Clearing Optimization: https://go.dev/doc/go1.11#performance-compiler
// for k := range e.archCount {
// delete(e.archCount, k)
// }

// for _, compId := range comp {
// for _, archId := range e.dcr.archSet[compId] {
// e.archCount[archId] = e.archCount[archId] + 1
// }
// }

// numComponents := len(comp)

// archIds = archIds[:0]
// for archId, count := range e.archCount {
// if count >= numComponents {
// archIds = append(archIds, archId)
// }
// }

// return archIds
}

func getStorage[T any](e *archEngine) *componentSliceStorage[T] {
Expand Down
53 changes: 40 additions & 13 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ func buildArchMaskFromAny(comps ...any) archetypeMask {
}
return mask
}
func buildArchMaskFromId(compIds ...componentId) archetypeMask {
var mask archetypeMask
for _, c := range compIds {
// Ranges: [0, 64), [64, 128), [128, 192), [192, 256)
idx := c / 64
offset := c - (64 * idx)
mask[idx] |= (1 << offset)
}
return mask
}

// Performs a bitwise OR on the base mask `m` with the added mask `a`
func (m archetypeMask) bitwiseOr(a archetypeMask) archetypeMask {
Expand All @@ -87,19 +97,36 @@ func (m archetypeMask) bitwiseAnd(a archetypeMask) archetypeMask {
return m
}

// Checks to ensure archetype m contains archetype a
// Returns true if every bit in m is also set in a
// Returns false if at least one set bit in m is not set in a
func (m archetypeMask) contains(a archetypeMask) bool {
// Logic: Bitwise AND on every segment, if the 'check' result doesn't match m[i] for that segment
// then we know there was a bit in a[i] that was not set
var check uint64
for i := range m {
check = m[i] & a[i]
if check != m[i] {
return false
}
}
return true
}

// TODO: You should move to this (ie archetype graph (or bitmask?). maintain the current archetype node, then traverse to nodes (and add new ones) based on which components are added): https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9
// Dynamic component Registry
type componentRegistry struct {
archSet [][]archetypeId // Contains the set of archetypeIds that have this component
archMask map[archetypeMask]archetypeId // Contains a mapping of archetype bitmasks to archetypeIds
revArchMask map[archetypeId]archetypeMask // Contains the reverse mapping of archetypeIds to archetype masks
archSet [][]archetypeId // Contains the set of archetypeIds that have this component
archMask map[archetypeMask]archetypeId // Contains a mapping of archetype bitmasks to archetypeIds

revArchMask []archetypeMask // Contains the reverse mapping of archetypeIds to archetype masks. Indexed by archetypeId
}

func newComponentRegistry() *componentRegistry {
r := &componentRegistry{
archSet: make([][]archetypeId, maxComponentId+1), // TODO: hardcoded to max component
archMask: make(map[archetypeMask]archetypeId),
revArchMask: make(map[archetypeId]archetypeMask),
archSet: make([][]archetypeId, maxComponentId+1), // TODO: hardcoded to max component
archMask: make(map[archetypeMask]archetypeId),
revArchMask: make([]archetypeMask, 0),
}
return r
}
Expand All @@ -122,7 +149,11 @@ func (r *componentRegistry) getArchetypeId(engine *archEngine, comps ...Componen
if !ok {
archId = engine.newArchetypeId(mask)
r.archMask[mask] = archId
r.revArchMask[archId] = mask

if int(archId) != len(r.revArchMask) {
panic(fmt.Sprintf("ecs: archId must increment. Expected: %d, Got: %d", len(r.revArchMask), archId))
}
r.revArchMask = append(r.revArchMask, mask)

// Add this archetypeId to every component's archList
for _, comp := range comps {
Expand All @@ -135,12 +166,8 @@ func (r *componentRegistry) getArchetypeId(engine *archEngine, comps ...Componen

// This is mostly for the without filter
func (r *componentRegistry) archIdOverlapsMask(archId archetypeId, compArchMask archetypeMask) bool {
// compArchMask := buildArchMask(comps...)
archMaskToCheck, ok := r.revArchMask[archId]
if !ok {
// TODO: I'm not sure what the best thing to do here is. If we get here it means that an archId was passed in which hasn't been created yet. I think that indicates a programmer bug, so I'm going to panic
panic("Bug: Invalid ArchId used")
}
archMaskToCheck := r.revArchMask[archId]

resultArchMask := archMaskToCheck.bitwiseAnd(compArchMask)
if resultArchMask != blankArchMask {
// If the resulting arch mask is nonzero, it means that both the component mask and the base mask had the same bit set, which means the arch had one of the components
Expand Down

0 comments on commit 9cb278e

Please sign in to comment.