Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Double buffering for easier multithreading ? #610

Closed
genaray opened this issue Aug 19, 2020 · 4 comments
Closed

[FEATURE] Double buffering for easier multithreading ? #610

genaray opened this issue Aug 19, 2020 · 4 comments

Comments

@genaray
Copy link

genaray commented Aug 19, 2020

So lets talk about double buffering... its an other little tool used by some ECS to make multithreading a lot easier.

Its "pretty" simple... you have two game states

  • Previous
  • Current

So right now say, your velocity integration system might look like...
position[ID] += velocity[ID] * deltaTime

Using double buffering it may look like this instead.
newGameState.position[ID] = oldGameState.position[ID] + oldGameState.velocity[ID] * deltaTime

newGameState.position could have been completely empty before this moment. The act of updating the state populates the state.

You just need to make sure every piece of meaningful state data gets a chance to be pushed through like that.
Or is in a separate read-only collection that can be shared, if it's not something that can change frame to frame.

This can also be good for other multithreading in our games.
Now even jobs that read data A can run at the same time as jobs that write data A, because the readable and writable versions are separate.
So, you have much less work relating to scheduling dependencies, locks, etc.

The cost is that it can take longer for a change to propagate through multiple passes of updates. But you still have the power to read from the writable data if you know the last set of writes have finished and need to propagate the latest changes faster :)

@exuvo
Copy link

exuvo commented Sep 9, 2020

I have implemented "double buffering" in my game for multithreading reasons with some modifications to artemis-odb to speed it up and the performance cost is not too heavy as long as i use many small components instead of large ones.

I have one world that is always the live one. During system processing i mark component adds/changes/removals in BitVectors. To get good performance i do multiple processings one after another if game speed is high. At the end i go through the adds/changes/removals BitVectors and delete entities, copy changed components, add entities to the world copy.

To avoid locks i have 2st world copies. One that is being updated and another that is read only for this time step. It complicates the copy update a bit but not too much. I gained more performance by not needing locks for each world than i lose by applying changes twice.

During my parallel world updates for each StarSystem i can safely read the world copies of other StarSystems. Same for UI.

See classes Galaxy StarSystem and ShadowStarSystem in https://github.com/exuvo/Aurora-J .

Artemis-odb modifications include a CustomComponentMapper that automatically marks component adds/removals in my BitVectors. The other change is to the EntityMangager to allow creating an entity with a specific ID for the world copies. This is a mostly unsafe operation but is required to maintain same entityIDs in the main world as well as the copies.

The tedious parts are that any time i change a component i have to mark it as changed, and each component i want to be propagated to the world copies needs to have a copy method. Most of my component copy methods store an hash of something so it can quickly decide if this a new component that needs a full copy or only a partial faster copy.

@genaray
Copy link
Author

genaray commented Sep 15, 2020

I have implemented "double buffering" in my game for multithreading reasons with some modifications to artemis-odb to speed it up and the performance cost is not too heavy as long as i use many small components instead of large ones.

I have one world that is always the live one. During system processing i mark component adds/changes/removals in BitVectors. To get good performance i do multiple processings one after another if game speed is high. At the end i go through the adds/changes/removals BitVectors and delete entities, copy changed components, add entities to the world copy.

To avoid locks i have 2st world copies. One that is being updated and another that is read only for this time step. It complicates the copy update a bit but not too much. I gained more performance by not needing locks for each world than i lose by applying changes twice.

During my parallel world updates for each StarSystem i can safely read the world copies of other StarSystems. Same for UI.

See classes Galaxy StarSystem and ShadowStarSystem in https://github.com/exuvo/Aurora-J .

Artemis-odb modifications include a CustomComponentMapper that automatically marks component adds/removals in my BitVectors. The other change is to the EntityMangager to allow creating an entity with a specific ID for the world copies. This is a mostly unsafe operation but is required to maintain same entityIDs in the main world as well as the copies.

The tedious parts are that any time i change a component i have to mark it as changed, and each component i want to be propagated to the world copies needs to have a copy method. Most of my component copy methods store an hash of something so it can quickly decide if this a new component that needs a full copy or only a partial faster copy.

Thanks a lot for your answer ! Im gonna look at it once im home :) Great that some people are still active in here... i just hope that artemis ODB adds double buffering on their own in the future ^^

@DaanVanYperen
Copy link
Collaborator

Implementing double buffering would be a major effort and such a fundamental change that it is unlikely to be implemented at this time.

Multithreading is something we'd like to solve, but @junkdog has said in the past that artemis-odb (for multiple reasons) will most likely never be multithreaded.

@exuvo
Copy link

exuvo commented Jul 12, 2021

From the changes i did for double buffering it is not a major code effort to implement but it is very bug prone if you somewhere forget to mark a component as changed. I could not alleviate it by always using setters as they never had references to the world being used so it was always up to me to remember to mark it as changed whenever i did anything with a component. I would put it on par with remembering to free in C.

At best you could have it in its own branch with hefty warnings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants