Skip to content

Commit

Permalink
feat(runtime): Implement effect queue (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy-xr authored Oct 9, 2024
1 parent 0abfe6e commit 3c18b35
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 4 deletions.
5 changes: 5 additions & 0 deletions examples/hello/hello.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ type Msg =

let game: Game<Model, Msg> = GameBuilder.local Model.initial

let update model msg =
printfn "Running update"
(model, Effect.none())

let tick model (tick: Time.FrameTime) =

let applyVelocity (tick: Time.FrameTime) ball =
Expand Down Expand Up @@ -111,5 +115,6 @@ let init (_args: array<string>) =
|> Transform.translateZ ((sin (frameTime.tts * 5.0f)) * 1.0f)
|])
)
|> GameBuilder.update update
|> GameBuilder.tick tick
|> Runtime.runGame
16 changes: 15 additions & 1 deletion runtime/functor-runtime-common/src/effect.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use fable_library_rust::Native_::Func1;
use fable_library_rust::{NativeArray_, Native_::Func1};

#[derive(Clone)]
pub enum Effect<T: Clone + 'static> {
Expand All @@ -7,6 +7,13 @@ pub enum Effect<T: Clone + 'static> {
}

impl<T: Clone + 'static> Effect<T> {
pub fn is_none(effect: &Effect<T>) -> bool {
match effect {
Effect::None => true,
_ => false,
}
}

pub fn none() -> Effect<T> {
Effect::None
}
Expand All @@ -21,4 +28,11 @@ impl<T: Clone + 'static> Effect<T> {
Effect::Wrapped(v) => Effect::Wrapped(mapping(v)),
}
}

pub fn run(effect: Effect<T>) -> NativeArray_::Array<T> {
match effect {
Effect::None => NativeArray_::array_from(vec![]),
Effect::Wrapped(v) => NativeArray_::array_from(vec![v]),
}
}
}
38 changes: 38 additions & 0 deletions runtime/functor-runtime-common/src/effect_queue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::{cell::RefCell, collections::VecDeque, fmt, sync::Arc};

use crate::Effect;

#[derive(Clone)]
pub struct EffectQueue<T: Clone + 'static> {
queue: Arc<RefCell<VecDeque<Effect<T>>>>,
}

impl<T: Clone + 'static> EffectQueue<T> {
pub fn new() -> EffectQueue<T> {
EffectQueue {
queue: Arc::new(RefCell::new(VecDeque::new())),
}
}

pub fn count(effect_queue: &EffectQueue<T>) -> i32 {
effect_queue.queue.borrow().len() as i32
}

pub fn enqueue(effect_queue: &EffectQueue<T>, effect: Effect<T>) {
if !Effect::is_none(&effect) {
effect_queue.queue.borrow_mut().push_front(effect);
}
}

pub fn dequeue(effect_queue: &EffectQueue<T>) -> Option<Effect<T>> {
effect_queue.queue.borrow_mut().pop_back()
}
}

// Implement Debug manually
impl<T: Clone + 'static> fmt::Debug for EffectQueue<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Build the debug representation
f.debug_struct("EffectQueue").finish()
}
}
2 changes: 2 additions & 0 deletions runtime/functor-runtime-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl OpaqueState {
pub mod animation;
pub mod asset;
mod effect;
mod effect_queue;
mod frame_time;
pub mod geometry;
pub mod io;
Expand All @@ -65,6 +66,7 @@ mod shader_program;
pub mod texture;

pub use effect::*;
pub use effect_queue::*;
pub use frame_time::*;
pub use render_context::*;
pub use scene3d::*;
Expand Down
2 changes: 2 additions & 0 deletions runtime/functor-runtime-desktop/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use functor_runtime_common::{FrameTime, Scene3D};
pub trait Game {
fn check_hot_reload(&mut self, frame_time: FrameTime);

fn tick(&mut self, frame_time: FrameTime);

fn render(&mut self, frame_time: FrameTime) -> Scene3D;

fn quit(&mut self);
Expand Down
8 changes: 8 additions & 0 deletions runtime/functor-runtime-desktop/src/hot_reload_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ impl Game for HotReloadGame {
}
}

fn tick(&mut self, frame_time: FrameTime) {
unsafe {
let tick_func: Symbol<fn(FrameTime)> =
self.library.as_ref().unwrap().get(b"tick").unwrap();
tick_func(frame_time)
}
}

fn quit(&mut self) {
if let Some(handle) = self.watcher_thread.take() {
handle.join().expect("Failed to join watcher thread");
Expand Down
2 changes: 2 additions & 0 deletions runtime/functor-runtime-desktop/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ pub async fn main() {
}
}

game.tick(time.clone());

gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
let radius = 5.0;
let view_matrix: Matrix4<f32> = Matrix4::look_at_rh(
Expand Down
7 changes: 7 additions & 0 deletions runtime/functor-runtime-desktop/src/static_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ impl Game for StaticGame {
}
}

fn tick(&mut self, frame_time: FrameTime) {
unsafe {
let tick_func: Symbol<fn(FrameTime)> = self.library.get(b"tick").unwrap();
tick_func(frame_time)
}
}

fn quit(&mut self) {
// Noop - nothing to do yet
}
Expand Down
6 changes: 5 additions & 1 deletion runtime/functor-runtime-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<canvas id="canvas" width="640" height="480"></canvas>
<script type="module">

import init1, { test_render_wasm } from "./build-wasm/pkg/game_wasm.js";
import init1, { test_render_wasm, tick_wasm } from "./build-wasm/pkg/game_wasm.js";
import startRuntime from "./pkg/functor_runtime_web.js";

const initOutput = init1();
Expand All @@ -19,6 +19,10 @@
return test_render_wasm(frameTime);
};

window.game.tick = (frameTime) => {
return tick_wasm(frameTime);
}

startRuntime();
});
</script>
Expand Down
5 changes: 5 additions & 0 deletions runtime/functor-runtime-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ fn request_animation_frame(f: &Closure<dyn FnMut()>) {
extern "C" {
#[wasm_bindgen(js_namespace = game, js_name = render)]
fn game_render(frameTimeJs: JsValue) -> JsValue;

#[wasm_bindgen(js_namespace = game, js_name = tick)]
fn game_tick(frameTimeJs: JsValue);
}

#[wasm_bindgen(start)]
Expand Down Expand Up @@ -206,6 +209,8 @@ async fn run_async() -> Result<(), JsValue> {

// let scene = Scene3D::cube();

game_tick(functor_runtime_common::to_js_value(&frame_time));

let val = game_render(functor_runtime_common::to_js_value(&frame_time));
web_sys::console::log_2(&JsValue::from_str("calling render"), &val);

Expand Down
8 changes: 7 additions & 1 deletion src/Functor.Game/Effect.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ module Effect =
let wrapped (a: 'a) : effect<'a> = nativeOnly

[<Emit("functor_runtime_common::Effect::map($0, $1)")>]
let map (fn: 'a -> 'b) (eff: effect<'a>) : effect<'b> = nativeOnly
let map (fn: 'a -> 'b) (eff: effect<'a>) : effect<'b> = nativeOnly


// TODO: These should live elsewhere because they aren't user space

[<Emit("functor_runtime_common::Effect::run($0)")>]
let run (eff: effect<'a>) : 'a array = nativeOnly
19 changes: 19 additions & 0 deletions src/Functor.Game/EffectQueue.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Functor
open Fable.Core

[<Erase; Emit("functor_runtime_common::EffectQueue<$0>")>] type EffectQueue<'msg> = | Noop


module EffectQueue =

[<Emit("functor_runtime_common::EffectQueue::new()")>]
let empty (): EffectQueue<_> = nativeOnly

[<Emit("functor_runtime_common::EffectQueue::count(&$0)")>]
let count (effectQueue: EffectQueue<'a>): int = nativeOnly

[<Emit("functor_runtime_common::EffectQueue::enqueue(&$1, $0)")>]
let enqueue (eff: effect<'a>) (effectQueue: EffectQueue<'a>) : unit = nativeOnly

[<Emit("functor_runtime_common::EffectQueue::dequeue(&$0)")>]
let dequeue (effectQueue: EffectQueue<'a>): Option<effect<'a>> = nativeOnly
1 change: 1 addition & 0 deletions src/Functor.Game/Functor.Game.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Effect.fs" />
<Compile Include="EffectQueue.fs" />
<Compile Include="Math/Angle.fs" />
<Compile Include="Math/Vector2.fs" />
<Compile Include="Math/Point2.fs" />
Expand Down
4 changes: 4 additions & 0 deletions src/Functor.Game/Game.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,9 @@ module GameRunner =
let (newModel, effect) = game.tick model tick
(newModel, effect)

let update<'model, 'msg> (game: Game<'model, 'msg>) (model: 'model) (msg: 'msg) =
let (newModel, effect) = game.update model msg
(newModel, effect)

let draw3d<'model, 'msg> (game: Game<'model, 'msg>) (model: 'model) (tick: Time.FrameTime) =
game.draw3d model tick
1 change: 1 addition & 0 deletions src/Functor.Game/Game.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ module GameBuilder =
module GameRunner =
val initialState: Game<'model, 'msg> -> 'model
val tick: Game<'model, 'msg> -> 'model -> Time.FrameTime -> ('model * effect<'msg>)
val update: Game<'model, 'msg> -> 'model -> 'msg -> ('model * effect<'msg>)
val draw3d: Game<'model, 'msg> -> 'model -> Time.FrameTime -> Graphics.Scene3D
33 changes: 32 additions & 1 deletion src/Functor.Game/Runtime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Runtime
let mutable currentRunner: Option<IRunner> = None

open Fable.Core.Rust
open System.Collections.Generic

///////////////////////////////
// WebAssembly API
Expand All @@ -36,6 +37,15 @@ module Runtime
let from_js<'a>(obj: JsValue): 'a = nativeOnly


[<OuterAttr("wasm_bindgen")>]
let tick_wasm (frameTimeJs: JsValue): unit =
let frameTime = frameTimeJs |> UnsafeJsValue.from_js<Time.FrameTime>;
if currentRunner.IsSome then
frameTime
|> currentRunner.Value.tick
else
raise (System.Exception("No runner"))

[<OuterAttr("wasm_bindgen")>]
let test_render_wasm (frameTimeJs: JsValue): JsValue =
let frameTime = frameTimeJs |> UnsafeJsValue.from_js<Time.FrameTime>;
Expand Down Expand Up @@ -87,6 +97,7 @@ module Runtime
type GameExecutor<'Msg, 'Model>(game: Game<'Model, 'Msg>, initialState: 'Model) =
let myGame = game
let mutable state: 'Model = initialState
let mutable effectQueue: EffectQueue<'Msg> = EffectQueue.empty()
do
printfn "Hello from GameRunner!"
interface IRunner with
Expand All @@ -98,9 +109,29 @@ module Runtime

// Todo: If first frame, run 'init'

printfn "Effect count: %d" (EffectQueue.count effectQueue)
// Todo: Run any pending effects
// Print the number of pending effects in the queue

let maybe_effect = EffectQueue.dequeue effectQueue;

let finalState =
match maybe_effect with
| None ->
printfn "No effect"
state
| Some _e ->
let arr: 'Msg array = Effect.run _e

Array.fold (fun (currentState) msg ->
let (newState, effect) = GameRunner.update myGame state msg
EffectQueue.enqueue effect effectQueue
newState
) (state) arr

let (newState, effect) = GameRunner.tick myGame finalState frameTime

let (newState, effects) = GameRunner.tick myGame state frameTime
EffectQueue.enqueue effect effectQueue;

// Todo: Run tick effects

Expand Down

0 comments on commit 3c18b35

Please sign in to comment.