Skip to content

Instantly share code, notes, and snippets.

@zakarumych
Created June 27, 2020 18:13
Show Gist options
  • Save zakarumych/d2c8f6324227168278deba53def24bbe to your computer and use it in GitHub Desktop.
Save zakarumych/d2c8f6324227168278deba53def24bbe to your computer and use it in GitHub Desktop.
use {
crate::{
config::{AssetSource, Config},
renderer::Renderer,
},
alex::{Accessor, Schedule, World, WorldAccess},
cfg_if::cfg_if,
color_eyre::Report,
goods::Cache,
lazy_static::lazy_static,
std::{
cell::Cell,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
sync::atomic::{AtomicBool, AtomicPtr, Ordering},
task::{Context, Poll},
},
tokio::runtime::Runtime,
winit::{
dpi::{PhysicalPosition, PhysicalSize},
event::{Event as WinitEvent, WindowEvent as WinitWindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
window::{Theme, Window, WindowBuilder, WindowId},
},
};
pub use winit::event::{
AxisId, DeviceEvent, DeviceId, ElementState, KeyboardInput, ModifiersState, MouseButton,
MouseScrollDelta, Touch, TouchPhase,
};
pub enum WindowEvent {
Resized(PhysicalSize<u32>),
Moved(PhysicalPosition<i32>),
CloseRequested,
Destroyed,
Focused(bool),
KeyboardInput {
device_id: DeviceId,
input: KeyboardInput,
is_synthetic: bool,
},
ModifiersChanged(ModifiersState),
CursorMoved {
device_id: DeviceId,
position: PhysicalPosition<f64>,
modifiers: ModifiersState,
},
CursorEntered {
device_id: DeviceId,
},
CursorLeft {
device_id: DeviceId,
},
MouseWheel {
device_id: DeviceId,
delta: MouseScrollDelta,
phase: TouchPhase,
modifiers: ModifiersState,
},
MouseInput {
device_id: DeviceId,
state: ElementState,
button: MouseButton,
modifiers: ModifiersState,
},
TouchpadPressure {
device_id: DeviceId,
pressure: f32,
stage: i64,
},
AxisMotion {
device_id: DeviceId,
axis: AxisId,
value: f64,
},
Touch(Touch),
ScaleFactorChanged {
scale_factor: f64,
},
ThemeChanged(Theme),
}
pub enum Event {
WindowEvent {
window_id: WindowId,
event: WindowEvent,
},
DeviceEvent {
device_id: DeviceId,
event: DeviceEvent,
},
Suspended,
Resumed,
RedrawRequested(WindowId),
RedrawEventsCleared,
LoopDestroyed,
}
struct Shared {
event_loop_ptr: Cell<*const EventLoopWindowTarget<()>>,
waiting_for_event: Cell<bool>,
}
/// Root data structure for the game engine.
pub struct Engine {
world: World,
schedule: Schedule,
assets: Cache<String>,
events: flume::Receiver<Event>,
shared: Rc<Shared>,
}
impl Engine {
pub fn build_window(&mut self, builder: WindowBuilder) -> Result<Window, Report> {
let elwt = self.shared.event_loop_ptr.get();
if elwt.is_null() {
unreachable!()
}
let elwt = unsafe {
// This block can be executed only within winit's event loop callback.
// when closure provided to `Engine::run` is polled, or
// on initial call to that closure.
// Because it takes `&mut self` and `Engine is not `Send` it cannot be
// sent to another thread.
// This function is not async so it is not possible that reference we creating
// here will be preserved over yielding.
&*elwt
};
let window = builder.build(elwt)?;
Ok(window)
}
/// Adds a system to this engine.
pub fn add_system<A, S>(&mut self, accessor: A, system: S) -> &mut Self
where
A: for<'a> Accessor<'a> + Send + 'static,
S: FnMut(WorldAccess<'_>) + Send + 'static,
{
self.schedule.add_system(accessor, system);
self
}
/// Asynchronously wait for next event.
pub async fn next(&mut self) -> Event {
self.shared.waiting_for_event.set(true);
let event = self.events.recv_async().await;
self.shared.waiting_for_event.set(false);
event.unwrap()
}
/// Runs an instance of an engine.
/// This function neven returns on success.
/// Instead it calls provided closure with create engine instance
/// and drive it to completion.
/// Along with polling winit's event-loop for window events.
pub fn run<F, A>(closure: F) -> Result<(), Report>
where
F: FnOnce(Self) -> A,
A: Future<Output = Result<(), Report>> + 'static,
{
let mut runtime = Runtime::new()?;
// Setup basic logging first.
runtime.block_on(Self::init_logger())?;
let config = runtime.block_on(Self::load_config())?;
let registry = config
.sources
.iter()
.fold(goods::RegistryBuilder::new(), |builder, source| match source {
AssetSource::FileSystem { path } => {
cfg_if! {
if #[cfg(feature = "goods-fs")] {
builder.with(goods::FileSource::new(path.clone()))
} else {
tracing::error!("FileSystem asset source ignored w/o `goods-fs` feature");
builder
}
}
}
});
let assets = Cache::new(registry.build(), goods::Tokio(runtime.handle().clone()));
let (sender, receiver) = flume::unbounded();
let shared = Rc::new(Shared {
event_loop_ptr: Cell::new(std::ptr::null()),
waiting_for_event: Cell::new(false),
});
let engine = Engine {
assets,
schedule: Schedule::new(),
world: World::new(),
events: receiver,
shared: shared.clone(),
};
let event_loop = EventLoop::new();
shared.event_loop_ptr.set(&*event_loop);
let mut app = Box::pin(closure(engine));
shared.event_loop_ptr.set(std::ptr::null());
// Here goes magic
event_loop.run(move |event, el, flow| match event {
WinitEvent::MainEventsCleared => {
runtime.block_on(async {
// Set pointer. We ensure it is always valid while non-null.
shared.event_loop_ptr.set(el);
// Poll closure only once.
if let Poll::Ready(result) = (AppEventWaitFuture { app: app.as_mut() }.await) {
// No place where we could return this error.
// log and panic are only options.
if let Err(err) = result {
tracing::error!("Error: {}", err);
}
// Exit when closure resolves.
*flow = ControlFlow::Exit;
} else {
if shared.waiting_for_event.get() {
*flow = ControlFlow::Wait;
} else {
*flow = ControlFlow::Poll;
}
}
// Unset event loop before it is invalidated.
shared.event_loop_ptr.set(std::ptr::null());
});
}
rest => {
match rest {
WinitEvent::WindowEvent { window_id, event } => convert_window_event(event)
.map(|event| Event::WindowEvent { window_id, event }),
WinitEvent::DeviceEvent { device_id, event } => {
Event::DeviceEvent { device_id, event }.into()
}
WinitEvent::Suspended => Event::Suspended.into(),
WinitEvent::Resumed => Event::Resumed.into(),
WinitEvent::RedrawRequested(window) => Event::RedrawRequested(window).into(),
WinitEvent::RedrawEventsCleared => Event::RedrawEventsCleared.into(),
WinitEvent::LoopDestroyed => Event::LoopDestroyed.into(),
_ => None,
}
.map(|event| {
let _ = sender.send(event);
});
}
})
}
async fn init_logger() -> Result<(), Report> {
env_logger::try_init()?;
Ok(())
}
async fn load_config() -> Result<Config, Report> {
tracing::info!("Running at {}", std::env::current_dir()?.display());
// Now load config.
let config = Config::load_default().await?;
tracing::info!("Config loaded: {:?}", config);
Ok(config)
}
}
struct AppEventWaitFuture<'a, A> {
app: Pin<&'a mut A>,
}
impl<'a, A> Future for AppEventWaitFuture<'a, A>
where
A: Future<Output = Result<(), Report>>,
{
type Output = Poll<Result<(), Report>>;
fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Poll<Result<(), Report>>> {
let poll = Future::poll(self.get_mut().app.as_mut(), ctx);
Poll::Ready(poll)
}
}
fn convert_window_event(event: WinitWindowEvent<'_>) -> Option<WindowEvent> {
match event {
WinitWindowEvent::Resized(size) => WindowEvent::Resized(size).into(),
WinitWindowEvent::Moved(position) => WindowEvent::Moved(position).into(),
WinitWindowEvent::CloseRequested => WindowEvent::CloseRequested.into(),
WinitWindowEvent::Destroyed => WindowEvent::Destroyed.into(),
WinitWindowEvent::Focused(focused) => WindowEvent::Focused(focused).into(),
WinitWindowEvent::KeyboardInput {
device_id,
input,
is_synthetic,
} => WindowEvent::KeyboardInput {
device_id,
input,
is_synthetic,
}
.into(),
WinitWindowEvent::ModifiersChanged(state) => WindowEvent::ModifiersChanged(state).into(),
WinitWindowEvent::CursorMoved {
device_id,
position,
modifiers,
} => WindowEvent::CursorMoved {
device_id,
position,
modifiers,
}
.into(),
WinitWindowEvent::CursorEntered { device_id } => {
WindowEvent::CursorEntered { device_id }.into()
}
WinitWindowEvent::CursorLeft { device_id } => WindowEvent::CursorLeft { device_id }.into(),
WinitWindowEvent::MouseWheel {
device_id,
delta,
phase,
modifiers,
} => WindowEvent::MouseWheel {
device_id,
delta,
phase,
modifiers,
}
.into(),
WinitWindowEvent::MouseInput {
device_id,
state,
button,
modifiers,
} => WindowEvent::MouseInput {
device_id,
state,
button,
modifiers,
}
.into(),
WinitWindowEvent::TouchpadPressure {
device_id,
pressure,
stage,
} => WindowEvent::TouchpadPressure {
device_id,
pressure,
stage,
}
.into(),
WinitWindowEvent::AxisMotion {
device_id,
axis,
value,
} => WindowEvent::AxisMotion {
device_id,
axis,
value,
}
.into(),
WinitWindowEvent::Touch(touch) => WindowEvent::Touch(touch).into(),
WinitWindowEvent::ScaleFactorChanged { scale_factor, .. } => {
WindowEvent::ScaleFactorChanged { scale_factor }.into()
}
WinitWindowEvent::ThemeChanged(theme) => WindowEvent::ThemeChanged(theme).into(),
_ => None,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment