mlange-42 / arche

Arche is an archetype-based Entity Component System (ECS) for Go.
https://mlange-42.github.io/arche/
MIT License
144 stars 8 forks source link

panic on `filter.compile` with more than 16 components #415

Closed hanchon closed 6 months ago

hanchon commented 6 months ago

Hi, I was playing with the lib and Ebitengine to create a simple word game. Everything was working as expected with no issues until I tried to add the 17th component. I was not able to have a minimal reproducible example, but I'll try to make one next week. I am opening the issue to start talking about the problem.

If I have 16 components everything is working great, no issues there. As soon as I add a new component (I created a simple boolean component in case the error was related to the pointer to the sprite):

Component:

package component

type BgSprite struct {
    Active bool
}

Entity:

package entity

import (
    "github.com/hanchon/wordgame/pkg/games/cruzadas/component"
    "github.com/mlange-42/arche/ecs"
    "github.com/mlange-42/arche/generic"
)

func NewBackground(w *ecs.World) {
    mapper := generic.NewMap1[component.BgSprite](w)
    entity := mapper.New()
    a := mapper.Get(entity)
    a.Active = false
}

Scene loader:

func LoadScene(world *ecs.World) *types.SceneFunctions {
    sf := types.NewSceneFunctions()
    // Entities
    currentUserInput := controller.Mouse{}
    entity.NewController(world, &currentUserInput)
    entity.NewCircle(world)
    entity.NewBackground(world)

    letters := []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
    entity.NewLettersInCircle(world, letters)
    entity.NewWordsToGuest(world, letters)

    // Systems
    sf.AddSystems(
        []types.System{
            system.NewDrawCircleSystem(world),
            system.NewDrawActiveLineSystem(world),
            system.NewDrawLettersInCircleSystem(world),
            system.NewDrawActiveWordSystem(world),
            system.NewUserInputSystem(world),
            system.NewCollideWithLettersSystem(world),
            system.NewCheckInputedWordSystem(world),
            system.NewDrawGridSystem(world),
            system.NewDrawDiscoveredLettersSystem(world),
        },
    )

    return sf
}

After adding the line entity.NewBackground(world), my draw function started to panic. Without that line, everything works as expected.

Panic error:

2024-04-28 13:53:24.581 main[12782:10271293] [CAMetalLayer nextDrawable] returning nil because allocation failed.
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x101143af4]

goroutine 67 [running]:
github.com/mlange-42/arche/ecs.(*archetype).ExtendLayouts(...)
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/ecs/archetype.go:270
github.com/mlange-42/arche/ecs.(*archNode).ExtendArchetypeLayouts(0x14000228c08?, 0x0?)
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/ecs/archetype_node.go:140 +0x44
github.com/mlange-42/arche/ecs.(*World).extendArchetypeLayouts(0x140003ce488, 0x20)
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/ecs/world_internal.go:1081 +0x44
github.com/mlange-42/arche/ecs.(*World).componentID(0x140003ce488, {0x1013e6420, 0x101379500})
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/ecs/world_internal.go:1094 +0xc8
github.com/mlange-42/arche/ecs.TypeID(...)
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/ecs/functions.go:37
github.com/mlange-42/arche/generic.toIds(0x140003ce488, {0x14000219a40, 0x3, 0x0?})
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/generic/util.go:37 +0x5c
github.com/mlange-42/arche/generic.(*compiledQuery).Compile(0x14000001930, 0x140003ce488, {0x14000219a40?, 0x14000035828?, 0x10117ce80?}, {0x0, 0x0, 0x0}, {0x0, 0x0, ...}, ...)
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/generic/compiled.go:37 +0x84
github.com/mlange-42/arche/generic.(*Filter3[...]).Filter(0x101147e7c, 0x140000358b8?, {0x0?, 0x0, 0x0})
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/generic/query_generated.go:712 +0x84
github.com/mlange-42/arche/generic.(*Filter3[...]).Query(0x3fd0000000000000, 0x140003ce488?, {0x0, 0x1013dd5c0?, 0x1400048e140?})
        /Users/hanchon/go/pkg/mod/github.com/mlange-42/arche@v0.11.1-0.20240422140523-1f3a73586dbf/generic/query_generated.go:739 +0x50
github.com/hanchon/wordgame/pkg/games/cruzadas/system.(*DrawActiveWord).Draw(0x14000538698?, 0x14000148cb0)
        /Users/hanchon/devel/hanchon/wordgame/pkg/games/cruzadas/system/draw_active_word.go:28 +0x48
github.com/hanchon/wordgame/pkg/scenes/types.(*SceneFunctions).Draw(...)
        /Users/hanchon/devel/hanchon/wordgame/pkg/scenes/types/functions.go:20
github.com/hanchon/wordgame/pkg/games/cruzadas.(*CruzadasScene).Draw(0x140000da180?, 0x14000148cb0)
        /Users/hanchon/devel/hanchon/wordgame/pkg/games/cruzadas/scene.go:26 +0x4c
github.com/joelschutz/stagehand.(*SceneManager[...]).Draw(...)

Draw function:

type DrawActiveWord struct {
    world  *ecs.World
    filter *generic.Filter3[component.ActiveWord, component.Position, component.Color]
}

func NewDrawActiveWordSystem(world *ecs.World) *DrawActiveWord {
    return &DrawActiveWord{
        world:  world,
        filter: generic.NewFilter3[component.ActiveWord, component.Position, component.Color](),
    }
}

func (c *DrawActiveWord) Draw(screen *ebiten.Image) {
    res := c.filter.Query(c.world)
    if !res.Next() {
        return
    }

    word, pos, color := res.Get()
    res.Close()

    scale := 0.20
    spriteSize := constants.SpriteSize * scale

    rectLenght := float32(len(word.Word) * int(spriteSize))

    radius := spriteSize / 2

    startX := pos.X - rectLenght/2
    startY := pos.Y - 10

    rectStartX := startX
    rectStartY := startY - float32(radius)

    vector.DrawFilledCircle(screen, startX, startY, float32(radius), color.Color, true)
    vector.DrawFilledRect(screen, rectStartX, rectStartY, rectLenght, float32(spriteSize), color.Color, true)
    vector.DrawFilledCircle(screen, rectStartX+rectLenght, startY, float32(radius), color.Color, true)

    for i, v := range word.Word {
        letters.WhiteLetters[v].Draw(
            screen,
            ganim8.DrawOpts(float64(rectStartX)+spriteSize*float64(i), float64(rectStartY), 0, scale, scale),
        )
    }

}

I went deep into the panic error and I found that the issue in related to the compile function for the filter. Used in the line res := c.filter.Query(c.world). The problem is that the *ecs.archetype is nil but I can not figure it out why. The file is arche/ecs/archetype.go and here a is nil, so a.layouts is the reason of the panic.

func (a *archetype) ExtendLayouts(count uint8) {
    if len(a.layouts) >= int(count) {
        return
    }
    temp := a.layouts
    a.layouts = make([]layout, count)
    copy(a.layouts, temp)
    a.archetypeAccess.basePointer = unsafe.Pointer(&a.layouts[0])
}

If I create new entities using any other component the program works without any issue, but as soon as I try to use the BgSprite component I get the panic.

Sorry for the wall of text, maybe you can find my issue with this information, but I'll try to make a smaller example to replicate my issue so it's easier to debug.

Thanks

PD: I am using the current main (1f3a73586dbf70c153a535bac5666fa72f1b02f1) but I also tried the v0.11.0 tag and I found the same issue. PD2: I tried also to recreate the filter each time inside the draw function, but I got the same panic. PD3: If i keep adding more components different filters calls start to panic, I think that is related to the pages that are changing in the file arche/ecs/utils.go

// Get returns the value at the given index.
func (p *pagedSlice[T]) Get(index int32) *T {
    return &p.pages[index/pageSize][index%pageSize]
}

Debugger screenshots included: Screenshot 2024-04-28 at 14 07 19 Screenshot 2024-04-28 at 14 07 07

hanchon commented 6 months ago

Update: Creating the minimal example was simpler than expected, you can run it locally:

_ = filterThatFails.Query(&w) always panic for me, without that line it works fine. I used the v0.11.0 version of the lib for this example

package main

import (
    "github.com/mlange-42/arche/ecs"
    "github.com/mlange-42/arche/generic"
)

type Component1 struct {
}

type Component2 struct {
}

type Component3 struct {
}

type Component4 struct {
}

type Component5 struct {
}

type Component6 struct {
}

type Component7 struct {
}

type Component8 struct {
}

type Component9 struct {
}

type Component10 struct {
}

type Component11 struct {
}

type Component12 struct {
}

type Component13 struct {
}

type Component14 struct {
}

type Component15 struct {
}

type Component16 struct {
}

type Component17 struct {
}

type Component18 struct {
}

func main() {
    w := ecs.NewWorld()

    builderFirst10 := generic.NewMap10[
        Component1,
        Component2,
        Component3,
        Component4,
        Component5,
        Component6,
        Component7,
        Component8,
        Component9,
        Component10,
    ](&w)
    _ = builderFirst10.New()

    builderNext6 := generic.NewMap6[
        Component12,
        Component13,
        Component14,
        Component15,
        Component16,
        Component17,
    ](&w)
    _ = builderNext6.New()

    filter := generic.NewFilter1[Component1]()
    res := filter.Query(&w)
    for res.Next() {
        continue
    }

    builderWithError := generic.NewMap1[Component18](&w)
    _ = builderWithError.New()

    filterThatFails := generic.NewFilter1[Component18]()
    _ = filterThatFails.Query(&w)

}
hanchon commented 6 months ago
go get github.com/mlange-42/arche@2ae05f972ba46b3668cdf40b7d1d864d8d775ca5
go run main.go

Run with no issues

go get github.com/mlange-42/arche@55baf5abc1995fd48317cae77d4f5f3a3ac4e78d
go run main.go

Panics

The pr that broke it is #327 , the initial value is working fine (16) but the size is not increasing when needed.

mlange-42 commented 6 months ago

Hi @hanchon, many thanks for reporting! I will look into it.

mlange-42 commented 6 months ago

Solved. The problem was a missing check whether an archetype (more exactly, an archetype node) is active, before extending the layouts.

"Inactive" archetypes nodes are those that do not actually contain entities (or even archetypes), as they are just traversed when traversing the archetype graph. See Architecture # Archetype graph in the user guide for more information on the archetype graph, in case you are interested.

Will merge and make a new release tomorrow or later today.

Many thanks again for spotting this, and for the very detailed bug report! Cheers!

hanchon commented 6 months ago

Awesome! Tomorrow I'll build locally the pr #416 to try it out. Thank you for the quick fix!

hanchon commented 6 months ago

I just tried #416 -> commit:4685db2374b629f22a9dfc32f4e14284cd40a060, and it's working great!

Thanks for solving the problem, i'll keep using my local build of arche until there is a new release.

mlange-42 commented 6 months ago

@hanchon FYI: the fix was just released with v0.12.0