ten3roberts / hecs-schedule

Pararell execution framwork for hecs
MIT License
8 stars 2 forks source link

Clear world before spawning a scene #8

Open konceptosociala opened 1 year ago

konceptosociala commented 1 year ago

I have implemented dynamic scene loading for CommandBuffer, where the whole world must be cleared before scene is loaded

pub trait SpawnSceneExt {
    fn spawn_scene(&mut self, scene: Scene, asset_manager: &mut AssetManager);
}

impl SpawnSceneExt for CommandBuffer {
    fn spawn_scene(&mut self, scene: Scene, asset_manager: &mut AssetManager) {
        cmd.write(|world|{
            world.clear();
        });

        for entity in scene.entities {
            let mut entity_builder = EntityBuilder::new();

            for component in entity.components {
                component.add_into(&mut entity_builder);
            }

            self.spawn(entity_builder.build());
        }

        *asset_manager = scene.assets;
    }
}

However, as custom CommandBuffer writes are implemented deferred, world is cleared after scene spawning, and it's not what it takes. I've tried to remove clearing from spawn_scene method and do smth like this:

fn spawn_scene(
    mut cmd: Write<CommandBuffer>,
    mut asset_manager: Write<AssetManager>,
    mut world_mut: MaybeWrite<World>,
){
    let scene = Scene::load("assets/scenes/my_scene.ron").expect("Cannot load the scene");

    cmd.write(|world|{
        world.clear();
    });

    if let Some(world) = world_mut.option_mut() {
        cmd.execute(world);
    }

    cmd.spawn_scene(scene, &mut asset_manager);
}

Is this the only option? And if so, will World be always accessible in this system, or one day I hit the load scene button and nothing happens?

ten3roberts commented 1 year ago

Good catch.

As you mentioned the actions in a command buffer actions are aggregated to perform one larger write to the world and to deduplicated writes if the same component is set multiple times.

Though this quickly conflicts with order dependent actions such as write.

Given your use case, would sequential (but potentially more boxing and looking up the entity in the world) application of the commandbuffer be preferable. Due to the increase of allocations it may not keep the same performance if that is a concern of yours.

Let me know what you think :relaxed:

konceptosociala commented 1 year ago

Given your use case, would sequential (but potentially more boxing and looking up the entity in the world) application of the commandbuffer be preferable. Due to the increase of allocations it may not keep the same performance if that is a concern of yours.

Do you mean two cmd.writes with clearing and spawning? If so borrow checker makes the problem with the lifetime of &mut AssetManager. I am not sure, sorry

konceptosociala commented 1 year ago

Sorry for being off-topic, but I couldn't find anyone as good at hecs and hecs-schedule as you are.

There is one fork of hecs, which ported change tracking from bevy. And I made separate forks of hecs and hecs-schedule to make it compatible with the latest (some time ago) version of hecs (0.9.1). But it turns out that in the hecs example, both tracking of additions and changes of components work, and in hecs-schedule - only additions tracking. I searched through the entire code of both crates, but could not figure out what was going wrong, since the same querys are performed in both cases and the mutated state of all mutable components is checked.

Here is a repo with 2 examples to reproduce the problem I really ask you for help 🥺🙏 If you have some free time... I need at least a little hint of what is wrong

ten3roberts commented 1 year ago

No worries at all :relaxed:

I do have accrued some knowledge of Entity Component Systems.

I am still planning to keep supporting hecs-schedule, but I am currently writing my own ECS https://github.com/ten3roberts/flax to solve the shortcomings I've come to discover while using hecs.

It has built in support for hiearchies and relations between entities, systems and automatic parallelization like this library, as well as cheap change, insert, and remove tracking, and a whole lot more features.

Since the scheduling is built in rather than a separate crate it can use lower level primitives and know more about the systems which run, which was not possible with hecs-schedule. This allows better scheduling across threads.

One concrete example of this is that in flax, the scheduler knows about which archetypes a query will access. A query of mut A, B will not parallelize with mut A, C even if there is not entity with A, B, C. flax is able to see this little detail, and conclude that these can correctly run in parallel, until an entity which mathes both queries is inserted.

The integrated CommandBuffer has the same features, and also solves this issue.

flax also solves my biggest nuisance with hecs, which is the newtype wrappers to create new components, that require delegating lots of trait implementations, and type conversions or dereferencing sprinkled about when using different components together in maths, like position and velocity.

If you've ever used the position.0 + velocity.0 or lots of **v you know what I am talking about.

Please do let me know if flax would fit your use case. I will be happy to add anything that is missing :heart:

ten3roberts commented 1 year ago

I managed to fix your issue with the change tracking:

The schedule setup should be:

    let mut schedule = Schedule::builder()
        .add_system(check)
        // Clear the change trackers after they have been checked and not after
        .add_system(clear)
        // Make sure to flush the CommandBuffer. This will be done automatically at the end of the
        // schedule, but we want the "changes" in the commandbuffer to be visible in the `change` system so that they
        // don't run in the same change tick and then are cleared as well at the end.
        .flush()
        .add_system(change)
        .build();

The issue was that the ordering of check => change => clear caused the changes to be cleared in the next system before the check system had the slightest chance of seeing them.

The plain hecs example is not free of errors either, as the 3 spawned threads will run in a different order each loop, as the 3 individual loops are not coordinated apart from who won the fight of the lock (with cooperative fairness yielding which depends on platform). In some cases, the clear thread will run after change but before check so a change will now and then be missed, and it is platform dependent, just something to keep in mind, though it hardly matters in this case as it is only an example :stuck_out_tongue_closed_eyes:, but it you have similar worker threads in your other project you know what may cause issues similar to this one.

To bounce back to flax there is no need to clear the trackers. Each query is created in the beginning of each system, and they will each know which changes to look for. For example if you have a query which only runs once every second it will still pick up all changes since the last time it ran, while another query which runs every frame will only see the changes from the last frame it ran. So there are no errors relating to missed changes if a query runs conditionally or just not often enough.