SecondHalfGames / yakui

yakui is a declarative Rust UI library for games
Apache License 2.0
237 stars 21 forks source link

Multithreading issues #190

Open johann2 opened 1 month ago

johann2 commented 1 month ago

I'm not sure how important multi-threaded use is for this project, but I thought, I'd write down the issues I've run into while attempting to integrate yakui with my game. This post doesn't have much of actionable feedback, but hopefully it can serve as a data point for future API design decisions. Also, if I overlooked an approach that would be much easier to implement, I'm most interested in learning more about it.

My game runs on 2 threads (actually more, but these 2 are relevant): one is for the window event loop and rendering and the other is for gameplay. I've considered multiple options, but so far, each of them has some significant drawbacks.

1) Arc<Mutex>. This didn't work at all, because Yakui isn't Send or Sync. If it did, it would've solved all my problems, (until the first deadlock)

2) Send all the relevant info (list of items to display, the current UI state, etc) from the game thread to the main thread. Send gameplay events, such as move item from container A to B, item used, etc back to the game thread. This approach seems kind of unergonomic and also has a risk of weird synchronization bugs. (For example: an item in container got used by some other system, but the UI has outdated data and can generate another use event for an already used item)

3) Have the Yakui entry point in the game thread and send input events from the render thread to the game thread. Also send generated polygons and textures from the game thread to render thread every frame.

Option 3 is the approach I finally decided to pursue. However, I feel that with some API changes, the integration time could be brought down considerably.

I've solved the rendering part, after less than a day of mucking about with the code. I took the yakui-wgpu crate as a template and implemented a struct without any reference to the Yakui or RenderDom objects, containing only the geometry and texture updates, so it could be sent between the threads. It's probably a bit slower, because it does copy a bit more data and it's a bit more complex than just passing the PaintDom pointer to the renderer. I could do a pull request with the changes, if you're interested.

The second issue is getting the input to the game thread. I had to add the dependency to winit and yakui-winit to the game thread, something I've managed to avoid so far. What makes things complicated is that in addition to sending the input events to the game thread, I also need to check if the event gets sunk by the UI or not. Listening to another channel for responses seems to be the most obvious way, but it has the potential to block the render thread unnecessarily.

In my use case, I can get away with letting the events to always bubble, but this may be important for some other use cases.

In conclusion, if multi threaded use is something you're interested in supporting out of the box, my main ideas are: 1) A data structure that has all the info for painting the UI that's Send and Sync,maybe Clone too. 2) Instead of the handle_window_event function, consider having a function that converts from winit events to yakui events that could be sent to another thread and handled there. 3) I'm not sure how to address the problem with bubbling/sinking feedback. Maybe having the part of yakui that receives events somehow accessible to other threads?

Thanks for reading this long post and also thank you for writing this library, it's the first UI library I've actually enjoyed using.

LPGhatguy commented 1 month ago

Thank you for all of the wonderful feedback and ideas! I'll be mulling this over and see if I come up with anything helpful.