From be0a7a10f1ac81c4454f1a02fcd61ebd3574be09 Mon Sep 17 00:00:00 2001 From: Tommy Builds Date: Fri, 4 Oct 2024 08:35:51 -0700 Subject: [PATCH 1/6] Implement desktop effect queue --- .../src/effect_queue.rs | 36 +++++++++++++++++++ runtime/functor-runtime-common/src/lib.rs | 2 ++ runtime/functor-runtime-desktop/src/game.rs | 2 ++ .../src/hot_reload_game.rs | 8 +++++ runtime/functor-runtime-desktop/src/main.rs | 2 ++ .../src/static_game.rs | 7 ++++ src/Functor.Game/EffectQueue.fs | 19 ++++++++++ src/Functor.Game/Functor.Game.fsproj | 1 + src/Functor.Game/Runtime.fs | 16 ++++++++- 9 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 runtime/functor-runtime-common/src/effect_queue.rs create mode 100644 src/Functor.Game/EffectQueue.fs diff --git a/runtime/functor-runtime-common/src/effect_queue.rs b/runtime/functor-runtime-common/src/effect_queue.rs new file mode 100644 index 0000000..a12f205 --- /dev/null +++ b/runtime/functor-runtime-common/src/effect_queue.rs @@ -0,0 +1,36 @@ +use std::{cell::RefCell, collections::VecDeque, fmt, sync::Arc}; + +use crate::Effect; + +#[derive(Clone)] +pub struct EffectQueue { + queue: Arc>>>, +} + +impl EffectQueue { + pub fn new() -> EffectQueue { + EffectQueue { + queue: Arc::new(RefCell::new(VecDeque::new())), + } + } + + pub fn count(effect_queue: &EffectQueue) -> i32 { + effect_queue.queue.borrow().len() as i32 + } + + pub fn enqueue(effect_queue: &EffectQueue, effect: Effect) { + effect_queue.queue.borrow_mut().push_front(effect); + } + + pub fn dequeue(effect_queue: &EffectQueue) -> Option> { + effect_queue.queue.borrow_mut().pop_back() + } +} + +// Implement Debug manually +impl fmt::Debug for EffectQueue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Build the debug representation + f.debug_struct("EffectQueue").finish() + } +} diff --git a/runtime/functor-runtime-common/src/lib.rs b/runtime/functor-runtime-common/src/lib.rs index c90cc0c..53ed73c 100644 --- a/runtime/functor-runtime-common/src/lib.rs +++ b/runtime/functor-runtime-common/src/lib.rs @@ -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; @@ -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::*; diff --git a/runtime/functor-runtime-desktop/src/game.rs b/runtime/functor-runtime-desktop/src/game.rs index 671deff..07077ac 100644 --- a/runtime/functor-runtime-desktop/src/game.rs +++ b/runtime/functor-runtime-desktop/src/game.rs @@ -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); diff --git a/runtime/functor-runtime-desktop/src/hot_reload_game.rs b/runtime/functor-runtime-desktop/src/hot_reload_game.rs index ddde8d1..6c01df8 100644 --- a/runtime/functor-runtime-desktop/src/hot_reload_game.rs +++ b/runtime/functor-runtime-desktop/src/hot_reload_game.rs @@ -44,6 +44,14 @@ impl Game for HotReloadGame { } } + fn tick(&mut self, frame_time: FrameTime) { + unsafe { + let tick_func: Symbol = + 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"); diff --git a/runtime/functor-runtime-desktop/src/main.rs b/runtime/functor-runtime-desktop/src/main.rs index e34b610..f91003b 100644 --- a/runtime/functor-runtime-desktop/src/main.rs +++ b/runtime/functor-runtime-desktop/src/main.rs @@ -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 = Matrix4::look_at_rh( diff --git a/runtime/functor-runtime-desktop/src/static_game.rs b/runtime/functor-runtime-desktop/src/static_game.rs index 4602c68..3f89008 100644 --- a/runtime/functor-runtime-desktop/src/static_game.rs +++ b/runtime/functor-runtime-desktop/src/static_game.rs @@ -21,6 +21,13 @@ impl Game for StaticGame { } } + fn tick(&mut self, frame_time: FrameTime) { + unsafe { + let tick_func: Symbol = self.library.get(b"tick").unwrap(); + tick_func(frame_time) + } + } + fn quit(&mut self) { // Noop - nothing to do yet } diff --git a/src/Functor.Game/EffectQueue.fs b/src/Functor.Game/EffectQueue.fs new file mode 100644 index 0000000..bb28850 --- /dev/null +++ b/src/Functor.Game/EffectQueue.fs @@ -0,0 +1,19 @@ +namespace Functor +open Fable.Core + +[")>] type EffectQueue<'msg> = | Noop + + +module EffectQueue = + + [] + let empty (): EffectQueue<_> = nativeOnly + + [] + let count (effectQueue: EffectQueue<'a>): int = nativeOnly + + [] + let enqueue (eff: effect<'a>) (effectQueue: EffectQueue<'a>) : unit = nativeOnly + + [] + let dequeue (effectQueue: EffectQueue<'a>): Option> = nativeOnly \ No newline at end of file diff --git a/src/Functor.Game/Functor.Game.fsproj b/src/Functor.Game/Functor.Game.fsproj index d7b7d2a..f172751 100644 --- a/src/Functor.Game/Functor.Game.fsproj +++ b/src/Functor.Game/Functor.Game.fsproj @@ -5,6 +5,7 @@ + diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index 3955b2e..746830c 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -14,6 +14,7 @@ module Runtime let mutable currentRunner: Option = None open Fable.Core.Rust + open System.Collections.Generic /////////////////////////////// // WebAssembly API @@ -87,6 +88,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 @@ -98,9 +100,21 @@ 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 (newState, effects) = GameRunner.tick myGame state frameTime + let maybe_effect = EffectQueue.dequeue effectQueue; + + match maybe_effect with + | None -> printfn "No effect" + | Some _e -> printfn "Got effect" + + let (newState, effect) = GameRunner.tick myGame state frameTime + + EffectQueue.enqueue effect effectQueue; + + // effectQueue <- Array.insertAt effectQueue.Length effect effectQueue // Todo: Run tick effects From 193ee9bb54de6a5853250ddc32616d10236b7b97 Mon Sep 17 00:00:00 2001 From: Tommy Builds Date: Fri, 4 Oct 2024 08:38:55 -0700 Subject: [PATCH 2/6] Hook up tick --- src/Functor.Game/Runtime.fs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index 746830c..6b2c794 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -37,6 +37,15 @@ module Runtime let from_js<'a>(obj: JsValue): 'a = nativeOnly + [] + let tick (frameTimeJs: JsValue): unit = + let frameTime = frameTimeJs |> UnsafeJsValue.from_js; + if currentRunner.IsSome then + frameTime + |> currentRunner.Value.tick + else + raise (System.Exception("No runner")) + [] let test_render_wasm (frameTimeJs: JsValue): JsValue = let frameTime = frameTimeJs |> UnsafeJsValue.from_js; From fe17465119d217df7d672c40323009e2fa93c0d9 Mon Sep 17 00:00:00 2001 From: Tommy Builds Date: Fri, 4 Oct 2024 08:39:23 -0700 Subject: [PATCH 3/6] Get working on web --- src/Functor.Game/Runtime.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index 6b2c794..c9cb7f4 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -39,6 +39,7 @@ module Runtime [] let tick (frameTimeJs: JsValue): unit = + FIXME let frameTime = frameTimeJs |> UnsafeJsValue.from_js; if currentRunner.IsSome then frameTime From 71e6a22d4c2f7f64dba95da43b73005d0fe95701 Mon Sep 17 00:00:00 2001 From: Tommy Builds Date: Sun, 6 Oct 2024 13:04:33 -0700 Subject: [PATCH 4/6] Remove FIXME --- src/Functor.Game/Runtime.fs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index c9cb7f4..6b2c794 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -39,7 +39,6 @@ module Runtime [] let tick (frameTimeJs: JsValue): unit = - FIXME let frameTime = frameTimeJs |> UnsafeJsValue.from_js; if currentRunner.IsSome then frameTime From f443cd53777ca3988b93596bf68bdb80b6c84407 Mon Sep 17 00:00:00 2001 From: Tommy Builds Date: Sun, 6 Oct 2024 13:27:52 -0700 Subject: [PATCH 5/6] Hook up basic effect handling --- examples/hello/hello.fs | 5 +++++ runtime/functor-runtime-common/src/effect.rs | 16 +++++++++++++- .../src/effect_queue.rs | 4 +++- src/Functor.Game/Effect.fs | 8 ++++++- src/Functor.Game/Game.fs | 4 ++++ src/Functor.Game/Game.fsi | 1 + src/Functor.Game/Runtime.fs | 22 +++++++++++++------ 7 files changed, 50 insertions(+), 10 deletions(-) diff --git a/examples/hello/hello.fs b/examples/hello/hello.fs index f626a48..c3bfc70 100644 --- a/examples/hello/hello.fs +++ b/examples/hello/hello.fs @@ -43,6 +43,10 @@ type Msg = let game: Game = 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 = @@ -111,5 +115,6 @@ let init (_args: array) = |> Transform.translateZ ((sin (frameTime.tts * 5.0f)) * 1.0f) |]) ) + |> GameBuilder.update update |> GameBuilder.tick tick |> Runtime.runGame \ No newline at end of file diff --git a/runtime/functor-runtime-common/src/effect.rs b/runtime/functor-runtime-common/src/effect.rs index 01e209d..1588d82 100644 --- a/runtime/functor-runtime-common/src/effect.rs +++ b/runtime/functor-runtime-common/src/effect.rs @@ -1,4 +1,4 @@ -use fable_library_rust::Native_::Func1; +use fable_library_rust::{NativeArray_, Native_::Func1}; #[derive(Clone)] pub enum Effect { @@ -7,6 +7,13 @@ pub enum Effect { } impl Effect { + pub fn is_none(effect: &Effect) -> bool { + match effect { + Effect::None => true, + _ => false, + } + } + pub fn none() -> Effect { Effect::None } @@ -21,4 +28,11 @@ impl Effect { Effect::Wrapped(v) => Effect::Wrapped(mapping(v)), } } + + pub fn run(effect: Effect) -> NativeArray_::Array { + match effect { + Effect::None => NativeArray_::array_from(vec![]), + Effect::Wrapped(v) => NativeArray_::array_from(vec![v]), + } + } } diff --git a/runtime/functor-runtime-common/src/effect_queue.rs b/runtime/functor-runtime-common/src/effect_queue.rs index a12f205..c8f9934 100644 --- a/runtime/functor-runtime-common/src/effect_queue.rs +++ b/runtime/functor-runtime-common/src/effect_queue.rs @@ -19,7 +19,9 @@ impl EffectQueue { } pub fn enqueue(effect_queue: &EffectQueue, effect: Effect) { - effect_queue.queue.borrow_mut().push_front(effect); + if !Effect::is_none(&effect) { + effect_queue.queue.borrow_mut().push_front(effect); + } } pub fn dequeue(effect_queue: &EffectQueue) -> Option> { diff --git a/src/Functor.Game/Effect.fs b/src/Functor.Game/Effect.fs index 3819404..2089eb1 100644 --- a/src/Functor.Game/Effect.fs +++ b/src/Functor.Game/Effect.fs @@ -13,4 +13,10 @@ module Effect = let wrapped (a: 'a) : effect<'a> = nativeOnly [] - let map (fn: 'a -> 'b) (eff: effect<'a>) : effect<'b> = nativeOnly \ No newline at end of file + let map (fn: 'a -> 'b) (eff: effect<'a>) : effect<'b> = nativeOnly + + + // TODO: These should live elsewhere because they aren't user space + + [] + let run (eff: effect<'a>) : 'a array = nativeOnly \ No newline at end of file diff --git a/src/Functor.Game/Game.fs b/src/Functor.Game/Game.fs index bec597a..b1b013d 100644 --- a/src/Functor.Game/Game.fs +++ b/src/Functor.Game/Game.fs @@ -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 \ No newline at end of file diff --git a/src/Functor.Game/Game.fsi b/src/Functor.Game/Game.fsi index b70aca6..7da3374 100644 --- a/src/Functor.Game/Game.fsi +++ b/src/Functor.Game/Game.fsi @@ -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 \ No newline at end of file diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index 6b2c794..b27eb48 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -115,16 +115,24 @@ module Runtime let maybe_effect = EffectQueue.dequeue effectQueue; - match maybe_effect with - | None -> printfn "No effect" - | Some _e -> printfn "Got effect" - - let (newState, effect) = GameRunner.tick myGame state frameTime + 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 EffectQueue.enqueue effect effectQueue; - // effectQueue <- Array.insertAt effectQueue.Length effect effectQueue - // Todo: Run tick effects state <- newState From 9cbb959895d44da204807dd401f38f5cc7af1469 Mon Sep 17 00:00:00 2001 From: Tommy Builds Date: Tue, 8 Oct 2024 21:06:11 -0700 Subject: [PATCH 6/6] Get web building --- runtime/functor-runtime-web/index.html | 6 +++++- runtime/functor-runtime-web/src/lib.rs | 5 +++++ src/Functor.Game/Runtime.fs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/runtime/functor-runtime-web/index.html b/runtime/functor-runtime-web/index.html index 024170a..2840968 100644 --- a/runtime/functor-runtime-web/index.html +++ b/runtime/functor-runtime-web/index.html @@ -6,7 +6,7 @@ diff --git a/runtime/functor-runtime-web/src/lib.rs b/runtime/functor-runtime-web/src/lib.rs index 7b9a0e3..7e0dd66 100644 --- a/runtime/functor-runtime-web/src/lib.rs +++ b/runtime/functor-runtime-web/src/lib.rs @@ -41,6 +41,9 @@ fn request_animation_frame(f: &Closure) { 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)] @@ -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); diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index b27eb48..f32179a 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -38,7 +38,7 @@ module Runtime [] - let tick (frameTimeJs: JsValue): unit = + let tick_wasm (frameTimeJs: JsValue): unit = let frameTime = frameTimeJs |> UnsafeJsValue.from_js; if currentRunner.IsSome then frameTime