Skip to content

Add an Executor #864

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

Open
wants to merge 2 commits into
base: 0.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down
23 changes: 23 additions & 0 deletions examples/other_async_executors.rs
Original file line number Diff line number Diff line change
@@ -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));
}
22 changes: 18 additions & 4 deletions src/compat.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
//! 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<Mutex<Box<miniquad::Context>>>,
frame_wakers: Arc<Mutex<Vec<Waker>>>,
canvas: quad_gl::sprite_batcher::SpriteBatcher,
}

thread_local! {
pub static CTX: RefCell<Option<CompatContext>> = { RefCell::new(None) };
}

fn with_ctx<F: Fn(&mut CompatContext)>(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: Fn(&mut CompatContext) -> 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,
});
});
}

Expand Down
137 changes: 111 additions & 26 deletions src/exec.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<Mutex<Vec<Waker>>>>,
}

impl Future for FrameFuture {
type Output = ();

fn poll(mut self: Pin<&mut Self>, _context: &mut Context) -> Poll<Self::Output> {
if self.done {
fn poll(mut self: Pin<&mut Self>, context: &mut Context) -> Poll<Self::Output> {
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
}
}
}
Expand All @@ -44,33 +47,115 @@ impl Future for FileLoadingFuture {
}
}

fn waker() -> Waker {
fn waker(inner: Arc<Inner<'_>>, 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::<InnerWaker<'_>>());
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::<InnerWaker<'_>>::from_raw(data.cast());
data.wake();
}
unsafe fn wake_by_ref(data: *const ()) {
wake(data)
let data = &*data.cast::<InnerWaker<'_>>();
data.wake();
}
unsafe fn drop(_data: *const ()) {
// Nothing to do
unsafe fn drop(data: *const ()) {
Arc::<InnerWaker<'_>>::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<T>(future: &mut Pin<Box<dyn Future<Output = T>>>) -> Option<T> {
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<Box<dyn Future<Output = ()> + 'a>>;

/// A simple unoptimized, single threaded executor.
pub struct Executor<'a> {
inner: Arc<Inner<'a>>,
pub frame_wakers: Arc<Mutex<Vec<Waker>>>,
}

#[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<VecDeque<LocalFuture<'a>>>,
waiting_futures: Mutex<HashMap<u64, LocalFuture<'a>>>,
next_wait_id: AtomicU64,
}

struct InnerWaker<'a> {
id: u64,
inner: Arc<Inner<'a>>,
}

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<Mutex<Vec<Waker>>>) -> 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<Fut: Future<Output = ()> + 'a>(&self, f: Fut) {
self.inner
.active_futures
.lock()
.unwrap()
.push_back(Box::pin(f));
}
}
5 changes: 1 addition & 4 deletions src/gizmos.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down
45 changes: 34 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -156,7 +158,7 @@ impl MiniquadInputEvent {
}

struct Stage {
main_future: Option<Pin<Box<dyn Future<Output = ()>>>>,
ex: Executor<'static>,
quad_ctx: Arc<Mutex<Box<miniquad::Context>>>,
quad_gl: Arc<Mutex<quad_gl::QuadGl>>,
ui: Arc<Mutex<quad_gl::ui::Ui>>,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -361,6 +370,7 @@ pub struct Context {
pub input: Arc<Mutex<input::InputContext>>,
time: Arc<Mutex<time::Time>>,
ui: Arc<Mutex<quad_gl::ui::Ui>>,
frame_wakers: Arc<Mutex<Vec<Waker>>>,
}

impl Context {
Expand All @@ -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(),
}
}

Expand Down Expand Up @@ -462,6 +473,12 @@ impl Context {
pub fn root_ui<'a>(&'a self) -> impl std::ops::DerefMut<Target = quad_gl::ui::Ui> + 'a {
self.ui.lock().unwrap()
}

pub fn next_frame(&self) -> FrameFuture {
FrameFuture {
frame_wakers: Some(self.frame_wakers.clone()),
}
}
}

pub fn start<F: Fn(Context) -> Fut + 'static, Fut: Future<Output = ()> + 'static>(
Expand All @@ -479,7 +496,13 @@ pub fn start<F: Fn(Context) -> Fut + 'static, Fut: Future<Output = ()> + '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
},
})
});
}
5 changes: 0 additions & 5 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down