bevyengine / bevyengine/bevy

🐢 Create inter-thread executor API

Open
#18,172 0 comments 1 reaction 0 assignees View on GitHub
A-ECS A-Tasks C-Feature D-Complex S-Ready-For-Implementation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

This is a sub-issue of #17667. See that issue for more high-level details.

Borrows ideas from [maniwani's PR](https://github.com/bevyengine/bevy/pull/9122) to remove `!Send` resources

### Overall plan

We will spawn a new thread which holds an event loop proxy and holds a sender and receiver for winit. `World` will hold another send/recv for the proxy. A system param will be created for use in systems and will come with a method that takes a callback function and executes the function on the main thread. It does so by communicating with the proxy, which wakes the event loop and sends the callback to the event loop, which will then block until execution is finished.

### Details

@maniwani provided the following process:
> - Leave an executor in the main thread with winit.
> - Store a handle/channel—that you can use to send tasks/callbacks to the executor—as a resource in the world. Note that you also have to use winit's EventLoopProxy to actually wake up the event loop.
> - Create a system param that borrows the handle and the proxy and exposes a method that sends a task/callback, wakes the loop, and blocks on the task's completion.
> - Have winit run all submitted tasks/callbacks in its own ApplicationHandler::proxy_wake_up callback (or ApplicationHandler::user_event, idk what version of winit we're on).

and provided this rough example:
```rust
pub struct BevyWinitAppHandler {
/* ... */
send: Sender,
recv: Receiver,
}

impl BevyWinitAppHandler {
pub fn new(send: Sender, recv: Receiver) {
/* ... */
}
}

impl ApplicationHandler for BevyWinitAppHandler {
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
/* ... */
while let Ok(callback) = self.recv.recv() {
// execute task we've received from some system
}
}
/* ... */
}

fn example_runner_function() {
let mut event_loop_builder = EventLoop::builder();

/* ... */

let event_loop = event_loop_builder
.build()
.expect("`winit` event loop can only be created once");

let waker = event_loop.create_proxy();

// winit can send directly to world
let (winit_send, world_recv) = std::sync::mpsc::channel::();

// world must send through proxy (to wake up the loop)
let (world_send, proxy_recv) = std::sync::mpsc::channel::();
let (proxy_send, winit_recv) = std::sync::mpsc::channel::();

// spawn a thread to manage the `EventLoopProxy`
// (this way `bevy_ecs` can avoid depending on `winit` or having to define a trait)
std::thread::Builder::new()
.name("main-event-loop-proxy".to_string())
.spawn(move || {
while let Ok(callback) = proxy_recv.recv() {
waker.wake_up();
proxy_send.send(callback).unwrap();
}
})
.unwrap();

// spawn a thread to manage the app
std::thread::Builder::new()
.name("app".to_string())
.spawn(move || {
let result = catch_unwind(AssertUnwindSafe(|| {
/* move the main sub-app/world in here */

// you'll actually have to newtype these channel halves,
// but hopefully you see the point...
world.insert_resource(world_recv);
world.insert_resource(world_send);

/* ... */
}));

if let Some(_) = result.err() {
// TODO: log panic
}
})
.unwrap();

// start the event loop
event_loop.run_app(BevyWinitAppHandler::new(winit_send, winit_recv));
}
```

**NOTE:** the example includes separating the world from the winit thread, but that is out of the scope of this issue and will be completed as work for a future issue. The scope of this issue is only to create this functionality, along with a few tests, but not to apply it anywhere (other than the tests).

This would allow code that needs to be run on main thread to stay on main thread without depending on `NonSendRes`.

Contributor guide

Open the contributing guide

Research direction

Start with the runner function and ApplicationHandler proxy_wake_up flow described in the issue, then trace how the executor communicates through the channels and wakes the event loop. Done means the inter-thread callback functionality exists with the requested tests, without applying it elsewhere or separating the world from the winit thread.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend-api-design, operating-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.