diff --git a/Cargo.toml b/Cargo.toml index 86bf1906..453f0f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ quad-gl = { git = "https://github.com/not-fl3/quad-gl", branch = "v0.4" } [dev-dependencies] dolly = "*" +tokio = { version = "1.41.1", features = ["fs", "rt"] } #[patch."https://github.com/not-fl3/quad-gl"] #quad-gl = { path = '../quad-gl' } diff --git a/examples/other_async_executors.rs b/examples/other_async_executors.rs new file mode 100644 index 00000000..c54d83ad --- /dev/null +++ b/examples/other_async_executors.rs @@ -0,0 +1,23 @@ +use macroquad::color::*; + +async fn game(ctx: macroquad::Context) { + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + let _guard = rt.enter(); + for _ in 0..3 { + ctx.clear_screen(WHITE); + + eprintln!("now for some tokio business"); + + let file = tokio::fs::File::open("examples/ferris.png").await.unwrap(); + + eprintln!("tokio file loaded"); + + ctx.next_frame().await; + } +} + +fn main() { + macroquad::start(Default::default(), |ctx| game(ctx)); +} diff --git a/src/compat.rs b/src/compat.rs index a41ff291..847eb4f6 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -1,15 +1,18 @@ //! miniquad-0.4 emulation -pub use crate::window::next_frame; pub use quad_gl::color::*; use std::{ cell::RefCell, sync::{Arc, Mutex}, + task::Waker, }; +use crate::exec::FrameFuture; + pub struct CompatContext { quad_ctx: Arc>>, + frame_wakers: Arc>>, canvas: quad_gl::sprite_batcher::SpriteBatcher, } @@ -17,15 +20,26 @@ thread_local! { pub static CTX: RefCell> = { RefCell::new(None) }; } -fn with_ctx(f: F) { - CTX.with_borrow_mut(|v| f(v.as_mut().unwrap())); +pub fn next_frame() -> FrameFuture { + with_ctx(|ctx| FrameFuture { + frame_wakers: Some(ctx.frame_wakers.clone()), + }) +} + +fn with_ctx T>(f: F) -> T { + CTX.with_borrow_mut(|v| f(v.as_mut().unwrap())) } pub fn init_compat_mode(ctx: &crate::Context) { let canvas = ctx.new_canvas(); let quad_ctx = ctx.quad_ctx.clone(); + let frame_wakers = ctx.frame_wakers.clone(); CTX.with_borrow_mut(|v| { - *v = Some(CompatContext { quad_ctx, canvas }); + *v = Some(CompatContext { + quad_ctx, + canvas, + frame_wakers, + }); }); } diff --git a/src/exec.rs b/src/exec.rs index aed90fc9..931a1989 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -1,28 +1,31 @@ +use std::collections::{HashMap, VecDeque}; use std::future::Future; use std::pin::Pin; +use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; +use std::task::{Context, Poll, RawWaker, RawWakerVTable, Wake, Waker}; use crate::Error; -// Returns Pending as long as its inner bool is false. -#[derive(Default)] +// Returns Pending once and finishes immediately once woken. pub struct FrameFuture { - done: bool, + pub frame_wakers: Option>>>, } impl Future for FrameFuture { type Output = (); - fn poll(mut self: Pin<&mut Self>, _context: &mut Context) -> Poll { - if self.done { + fn poll(mut self: Pin<&mut Self>, context: &mut Context) -> Poll { + if let Some(wakers) = self.frame_wakers.take() { + wakers.lock().unwrap().push(context.waker().clone()); + eprintln!("frame future pend"); + Poll::Pending + } else { // We were told to step, meaning this future gets destroyed and we run // the main future until we call next_frame again and end up in this poll // function again. + eprintln!("frame future ready"); Poll::Ready(()) - } else { - self.done = true; - Poll::Pending } } } @@ -44,33 +47,115 @@ impl Future for FileLoadingFuture { } } -fn waker() -> Waker { +fn waker(inner: Arc>, id: u64) -> Waker { + // Cannot use the `Wake` trait, because `Inner` has a lifetime that + // cannot be used to generate a raw waker. unsafe fn clone(data: *const ()) -> RawWaker { + Arc::increment_strong_count(data.cast::>()); RawWaker::new(data, &VTABLE) } - unsafe fn wake(_data: *const ()) { - panic!( - "macroquad does not support waking futures, please use coroutines, \ - otherwise your pending future will block until the next frame" - ) + unsafe fn wake(data: *const ()) { + // Even though we generate an unconstrained lifetime here, that's no issue, + // as we just move data that has a lifetime from one collection to another. + let data = Arc::>::from_raw(data.cast()); + data.wake(); } unsafe fn wake_by_ref(data: *const ()) { - wake(data) + let data = &*data.cast::>(); + data.wake(); } - unsafe fn drop(_data: *const ()) { - // Nothing to do + unsafe fn drop(data: *const ()) { + Arc::>::from_raw(data.cast()); } const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); - let raw_waker = RawWaker::new(std::ptr::null(), &VTABLE); + let raw_waker = RawWaker::new( + Arc::into_raw(Arc::new(InnerWaker { inner, id })).cast(), + &VTABLE, + ); unsafe { Waker::from_raw(raw_waker) } } -/// returns Some(T) if future is done, None if it would block -pub(crate) fn resume(future: &mut Pin>>) -> Option { - let waker = waker(); - let mut futures_context = std::task::Context::from_waker(&waker); - match future.as_mut().poll(&mut futures_context) { - Poll::Ready(v) => Some(v), - Poll::Pending => None, +type LocalFuture<'a> = Pin + 'a>>; + +/// A simple unoptimized, single threaded executor. +pub struct Executor<'a> { + inner: Arc>, + pub frame_wakers: Arc>>, +} + +#[derive(Default)] +struct Inner<'a> { + // FIXME: use a thread safe queue that doesn't need to lock the read end to + // push to the write end. + active_futures: Mutex>>, + waiting_futures: Mutex>>, + next_wait_id: AtomicU64, +} + +struct InnerWaker<'a> { + id: u64, + inner: Arc>, +} + +impl InnerWaker<'_> { + fn wake(&self) { + let future = self + .inner + .waiting_futures + .lock() + .unwrap() + .remove(&self.id) + .expect("already woken"); + self.inner.active_futures.lock().unwrap().push_back(future); + } +} + +impl<'a> Executor<'a> { + pub fn new(frame_wakers: Arc>>) -> Self { + Self { + frame_wakers, + inner: Default::default(), + } + } + + /// Returns `true` if all futures have finished. + pub fn is_empty(&self) -> bool { + self.inner.active_futures.lock().unwrap().is_empty() + && self.inner.waiting_futures.lock().unwrap().is_empty() + } + + /// Runs one future until it returns `Pending`. + /// Returns `true` if anything was run. + pub fn tick(&self) -> bool { + let Some(mut future) = self.inner.active_futures.lock().unwrap().pop_front() else { + return false; + }; + let id = self + .inner + .next_wait_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let waker = waker(self.inner.clone(), id); + let mut futures_context = std::task::Context::from_waker(&waker); + match future.as_mut().poll(&mut futures_context) { + Poll::Ready(()) => {} + Poll::Pending => { + let prev = self + .inner + .waiting_futures + .lock() + .unwrap() + .insert(id, future); + assert!(prev.is_none()); + } + } + true + } + + pub fn spawn + 'a>(&self, f: Fut) { + self.inner + .active_futures + .lock() + .unwrap() + .push_back(Box::pin(f)); } } diff --git a/src/gizmos.rs b/src/gizmos.rs index a3dea009..c77d0906 100644 --- a/src/gizmos.rs +++ b/src/gizmos.rs @@ -1,7 +1,4 @@ -pub use crate::{ - math::{vec2, vec3, Vec3}, - window::next_frame, -}; +pub use crate::math::{vec2, vec3, Vec3}; pub use quad_gl::{ color::*, draw_calls_batcher::{DrawCallsBatcher, DrawMode, Vertex}, diff --git a/src/lib.rs b/src/lib.rs index 80c04fee..7fbb8424 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,11 +36,13 @@ //! } //!``` #![allow(warnings)] +use exec::{Executor, FrameFuture}; use miniquad::*; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; +use std::task::Waker; mod exec; mod time; @@ -156,7 +158,7 @@ impl MiniquadInputEvent { } struct Stage { - main_future: Option>>>, + ex: Executor<'static>, quad_ctx: Arc>>, quad_gl: Arc>, ui: Arc>, @@ -321,18 +323,25 @@ impl EventHandler for Stage { fn draw(&mut self) { self.time.lock().unwrap().update(); //let result = maybe_unwind(get_context().unwind, || { - if let Some(future) = self.main_future.as_mut() { - let _z = telemetry::ZoneGuard::new("user code"); + let _z = telemetry::ZoneGuard::new("user code"); - if exec::resume(future).is_some() { - self.main_future = None; - miniquad::window::quit(); - return; - } + eprintln!("frame start"); - self.input.lock().unwrap().end_frame(); - compat::end_frame(); + for waker in self.ex.frame_wakers.lock().unwrap().drain(..) { + waker.wake(); } + + while self.ex.tick() {} + + eprintln!("frame end"); + + if self.ex.is_empty() { + miniquad::window::quit(); + return; + } + + self.input.lock().unwrap().end_frame(); + compat::end_frame(); } fn window_restored_event(&mut self) { @@ -361,6 +370,7 @@ pub struct Context { pub input: Arc>, time: Arc>, ui: Arc>, + frame_wakers: Arc>>, } impl Context { @@ -379,6 +389,7 @@ impl Context { input: Arc::new(Mutex::new(input::InputContext::new())), ui: Arc::new(Mutex::new(ui)), time: Arc::new(Mutex::new(time)), + frame_wakers: Default::default(), } } @@ -462,6 +473,12 @@ impl Context { pub fn root_ui<'a>(&'a self) -> impl std::ops::DerefMut + 'a { self.ui.lock().unwrap() } + + pub fn next_frame(&self) -> FrameFuture { + FrameFuture { + frame_wakers: Some(self.frame_wakers.clone()), + } + } } pub fn start Fut + 'static, Fut: Future + 'static>( @@ -479,7 +496,13 @@ pub fn start Fut + 'static, Fut: Future + 'static quad_gl: ctx.quad_gl.clone(), ui: ctx.ui.clone(), time: ctx.time.clone(), - main_future: Some(Box::pin(future(ctx))), + ex: { + let ex = Executor::new(ctx.frame_wakers.clone()); + // TODO: should we wait for coroutines to finish even if the main task finished? + // Right now we do, but this is not how macroquad worked previously. + ex.spawn(future(ctx)); + ex + }, }) }); } diff --git a/src/window.rs b/src/window.rs index 9c49836e..5b681439 100644 --- a/src/window.rs +++ b/src/window.rs @@ -7,11 +7,6 @@ pub use miniquad; pub use miniquad::conf::Conf; -/// Block execution until the next frame. -pub fn next_frame() -> crate::exec::FrameFuture { - crate::exec::FrameFuture::default() -} - #[doc(hidden)] pub fn gl_set_drawcall_buffer_capacity(_max_vertices: usize, _max_indices: usize) { // let context = get_context();