From a3d3da0abeee0d8e203ba2476b177c30bf229724 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Sat, 13 Aug 2022 16:59:55 -0500 Subject: [PATCH] WIP Browser Runtime --- .gitignore | 1 + Cargo.lock | 12 +++ Cargo.toml | 12 ++- run-example-web.sh | 32 +++++++ src/runtime/js/ecs.js | 61 ++++++++---- src/runtime/js/log.js | 20 ++-- src/runtime/native/ecs.rs | 2 +- src/runtime/native/js/ecs.js | 81 ---------------- src/runtime/native/js/index.d.ts | 52 ----------- src/runtime/native/js/log.js | 19 ---- src/runtime/native/log.rs | 2 +- src/runtime/native/mod.rs | 12 ++- src/runtime/native/native_setup.js | 6 ++ src/runtime/wasm/mod.rs | 117 +++++++++++++++++++++++ src/runtime/wasm/wasm_setup.js | 9 ++ wasm_resources/index.html | 143 +++++++++++++++++++++++++++++ 16 files changed, 396 insertions(+), 185 deletions(-) create mode 100755 run-example-web.sh delete mode 100644 src/runtime/native/js/ecs.js delete mode 100644 src/runtime/native/js/index.d.ts delete mode 100644 src/runtime/native/js/log.js create mode 100644 src/runtime/native/native_setup.js create mode 100644 src/runtime/wasm/mod.rs create mode 100644 src/runtime/wasm/wasm_setup.js create mode 100644 wasm_resources/index.html diff --git a/.gitignore b/.gitignore index ea8c4bf..fac7122 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/web-target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7e0aeb0..ffaa444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -488,6 +488,7 @@ dependencies = [ "bevy_reflect_fns", "deno_core", "fixedbitset", + "js-sys", "serde", "serde_json", "swc_atoms", @@ -497,6 +498,8 @@ dependencies = [ "swc_ecma_transforms_base", "swc_ecma_transforms_typescript", "swc_ecma_visit", + "wasm-bindgen", + "wasm_mutex", ] [[package]] @@ -3539,6 +3542,15 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +[[package]] +name = "wasm_mutex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbdddc3b163fc2d639800b3411a5428d1e151ba2a400a560b1545e39f1e68cd" +dependencies = [ + "serde", +] + [[package]] name = "web-sys" version = "0.3.59" diff --git a/Cargo.toml b/Cargo.toml index 4f39b36..5501eb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ bevy = { version = "0.8.0", default-features = false, features = ["bevy_asset"] bevy_reflect = "0.8.0-dev" fixedbitset = "0.4" -deno_core = "0.146.0" serde = "1.0" serde_json = "1.0" @@ -24,5 +23,16 @@ swc_ecma_transforms_typescript = { version = "0.145.1" } swc_ecma_visit = { version = "0.76.3" } swc_atoms = { version = "0.4.3" } +[profile.dev] +opt-level = 1 + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +deno_core = "0.146.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm_mutex = "0.1.4" +js-sys = "0.3.59" +wasm-bindgen = { version = "0.2.82", features = ["enable-interning"] } + [dev-dependencies] bevy = { version = "0.8.0", default-features = false, features = ["render", "bevy_winit", "x11", "filesystem_watcher"] } diff --git a/run-example-web.sh b/run-example-web.sh new file mode 100755 index 0000000..8c2e494 --- /dev/null +++ b/run-example-web.sh @@ -0,0 +1,32 @@ +#!/bin/env bash + +# +# This script is usually run by the justfile +# + +example="breakout" +target=wasm32-unknown-unknown +target_dir="web-target" + +release_arg="" +build_kind="debug" +dist_dir="$target_dir/wasm-debug" + +if [ "$is_release" == "release" ]; then + release_arg="--release" + build_kind="release" + dist_dir="$target_dir/wasm-release" +fi + +export CARGO_TARGET_DIR=$target_dir + +set -ex + +cargo build --target $target --example $example $release_arg +rm -rf $dist_dir +mkdir -p $dist_dir +wasm-bindgen --out-dir $dist_dir --target web --no-typescript $target_dir/$target/$build_kind/examples/$example.wasm +cp wasm_resources/index.html $dist_dir/index.html +cp -r assets $dist_dir + +basic-http-server -x $dist_dir diff --git a/src/runtime/js/ecs.js b/src/runtime/js/ecs.js index f2fadfc..0aef618 100644 --- a/src/runtime/js/ecs.js +++ b/src/runtime/js/ecs.js @@ -1,7 +1,6 @@ "use strict"; ((window) => { - class ComponentId { index; } @@ -13,56 +12,82 @@ } class World { - get #rid() { return 0 } + get #rid() { + return 0; + } toString() { - return Deno.core.opSync("op_world_tostring", this.rid); + return bevyModJsScriptingOpSync("op_world_tostring", this.rid); } get components() { - return Deno.core.opSync("op_world_components", this.rid); + return bevyModJsScriptingOpSync("op_world_components", this.rid); } get resources() { - return Deno.core.opSync("op_world_resources", this.rid); + return bevyModJsScriptingOpSync("op_world_resources", this.rid); } get entities() { - return Deno.core.opSync("op_world_entities", this.rid); + return bevyModJsScriptingOpSync("op_world_entities", this.rid); } query(descriptor) { - return Deno.core.opSync("op_world_query", this.rid, descriptor) - .map(({ entity, components }) => ({ - entity, - components: components.map(wrapValueRef), - })); + return bevyModJsScriptingOpSync( + "op_world_query", + this.rid, + descriptor + ).map(({ entity, components }) => ({ + entity, + components: components.map(wrapValueRef), + })); } } - const VALUE_REF_GET_INNER = Symbol("value_ref_get_inner"); function wrapValueRef(valueRef) { // leaf primitives - if (typeof valueRef !== "object") { return valueRef }; + if (typeof valueRef !== "object") { + return valueRef; + } const proxy = new Proxy(valueRef, { ownKeys: (target) => { - return Deno.core.opSync("op_value_ref_keys", world.rid, target); + return bevyModJsScriptingOpSync( + "op_value_ref_keys", + world.rid, + target + ); }, get: (target, p, receiver) => { switch (p) { case VALUE_REF_GET_INNER: return target; case "toString": - return () => Deno.core.opSync("op_value_ref_to_string", world.rid, target); + return () => + bevyModJsScriptingOpSync( + "op_value_ref_to_string", + world.rid, + target + ); default: - let valueRef = Deno.core.opSync("op_value_ref_get", world.rid, target, "." + p); + let valueRef = bevyModJsScriptingOpSync( + "op_value_ref_get", + world.rid, + target, + "." + p + ); return wrapValueRef(valueRef); } }, set: (target, p, value) => { - Deno.core.opSync("op_value_ref_set", world.rid, target, "." + p, value); - } + bevyModJsScriptingOpSync( + "op_value_ref_set", + world.rid, + target, + "." + p, + value + ); + }, }); return proxy; } diff --git a/src/runtime/js/log.js b/src/runtime/js/log.js index 108519e..a1b78fb 100644 --- a/src/runtime/js/log.js +++ b/src/runtime/js/log.js @@ -2,18 +2,18 @@ ((window) => { window.trace = function (val) { - Deno.core.opSync("op_log", "trace", val); - } + bevyModJsScriptingOpSync("op_log", "trace", val); + }; window.debug = function (val) { - Deno.core.opSync("op_log", "debug", val) - } + bevyModJsScriptingOpSync("op_log", "debug", val); + }; window.info = function (val) { - Deno.core.opSync("op_log", "info", val) - } + bevyModJsScriptingOpSync("op_log", "info", val); + }; window.warn = function (val) { - Deno.core.opSync("op_log", "warn", val) - } + bevyModJsScriptingOpSync("op_log", "warn", val); + }; window.error = function (val) { - Deno.core.opSync("op_log", "error", val) - } + bevyModJsScriptingOpSync("op_log", "error", val); + }; })(globalThis); diff --git a/src/runtime/native/ecs.rs b/src/runtime/native/ecs.rs index 72a3260..9420edc 100644 --- a/src/runtime/native/ecs.rs +++ b/src/runtime/native/ecs.rs @@ -606,6 +606,6 @@ pub fn extension() -> Extension { op_value_ref_set::decl(), op_value_ref_call::decl(), ]) - .js(include_js_files!(prefix "bevy", "js/ecs.js",)) + .js(include_js_files!(prefix "bevy", "../js/ecs.js",)) .build() } diff --git a/src/runtime/native/js/ecs.js b/src/runtime/native/js/ecs.js deleted file mode 100644 index e7281c0..0000000 --- a/src/runtime/native/js/ecs.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; - -((window) => { - - class ComponentId { - index; - } - - class Entity { - #bits; - id; - generation; - } - - class World { - get #rid() { return 0 } - - toString() { - return Deno.core.opSync("op_world_tostring", this.rid); - } - - get components() { - return Deno.core.opSync("op_world_components", this.rid); - } - - get resources() { - return Deno.core.opSync("op_world_resources", this.rid); - } - - get entities() { - return Deno.core.opSync("op_world_entities", this.rid); - } - - query(descriptor) { - return Deno.core.opSync("op_world_query", this.rid, descriptor) - .map(({ entity, components }) => ({ - entity, - components: components.map(wrapValueRef), - })); - } - } - - - const VALUE_REF_GET_INNER = Symbol("value_ref_get_inner"); - function wrapValueRef(valueRef) { - // leaf primitives - if (typeof valueRef !== "object") { return valueRef }; - let target = () => { }; - target.valueRef = valueRef; - const proxy = new Proxy(target, { - ownKeys: (target) => { - return [...Deno.core.opSync("op_value_ref_keys", world.rid, target.valueRef), VALUE_REF_GET_INNER]; - }, - get: (target, p, receiver) => { - switch (p) { - case VALUE_REF_GET_INNER: - return target; - case "toString": - return () => Deno.core.opSync("op_value_ref_to_string", world.rid, target.valueRef); - default: - let valueRef = Deno.core.opSync("op_value_ref_get", world.rid, target.valueRef, "." + p); - return wrapValueRef(valueRef); - } - }, - set: (target, p, value) => { - Deno.core.opSync("op_value_ref_set", world.rid, target.valueRef, "." + p, value); - }, - apply: (target, thisArg, args) => { - let ret = Deno.core.opSync("op_value_ref_call", world.rid, target.valueRef, args.map(arg => { - let valueRef = arg[VALUE_REF_GET_INNER]?.valueRef; - return (valueRef !== undefined) ? valueRef : arg; - })); - return wrapValueRef(ret); - } - }); - return proxy; - } - - const world = new World(); - window.world = world; -})(globalThis); diff --git a/src/runtime/native/js/index.d.ts b/src/runtime/native/js/index.d.ts deleted file mode 100644 index 3f33c0b..0000000 --- a/src/runtime/native/js/index.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -declare namespace Deno { - namespace core { - function opSync(op: string, ...args: any[]): any; - - } -} - -// log.s -declare function trace(val: any): void; -declare function debug(val: any): void; -declare function info(val: any): void; -declare function warn(val: any): void; -declare function error(val: any): void; - -// ecs.js -declare class ComponentId { - index: number; -} -declare class Entity { - id: number; - generation: number; -} - -type ComponentInfo = { - id: ComponentId, - name: string, - size: number; -}; - -type QueryDescriptor = { - components: ComponentId[], -}; - -type QueryItem = { - entity: Entity, - components: any[], -}; - -type Primitive = number | string | boolean; -interface Value { - [path: string | number]: Value | Primitive | undefined, -} - -declare class World { - get components(): ComponentInfo[]; - get resources(): ComponentInfo[]; - get entities(): Entity[]; - - query(descriptor: QueryDescriptor): QueryItem[]; -} - -declare let world: World; diff --git a/src/runtime/native/js/log.js b/src/runtime/native/js/log.js deleted file mode 100644 index 108519e..0000000 --- a/src/runtime/native/js/log.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; - -((window) => { - window.trace = function (val) { - Deno.core.opSync("op_log", "trace", val); - } - window.debug = function (val) { - Deno.core.opSync("op_log", "debug", val) - } - window.info = function (val) { - Deno.core.opSync("op_log", "info", val) - } - window.warn = function (val) { - Deno.core.opSync("op_log", "warn", val) - } - window.error = function (val) { - Deno.core.opSync("op_log", "error", val) - } -})(globalThis); diff --git a/src/runtime/native/log.rs b/src/runtime/native/log.rs index 7cad1c3..16554c4 100644 --- a/src/runtime/native/log.rs +++ b/src/runtime/native/log.rs @@ -35,6 +35,6 @@ fn op_log(state: &mut OpState, kind: String, text: serde_json::Value) -> Result< pub fn extension() -> Extension { Extension::builder() .ops(vec![op_log::decl()]) - .js(include_js_files!(prefix "bevy", "js/log.js",)) + .js(include_js_files!(prefix "bevy", "../js/log.js",)) .build() } diff --git a/src/runtime/native/mod.rs b/src/runtime/native/mod.rs index 6748b58..db91707 100644 --- a/src/runtime/native/mod.rs +++ b/src/runtime/native/mod.rs @@ -4,7 +4,9 @@ mod log; use std::{cell::RefCell, path::PathBuf}; use bevy::{prelude::*, utils::HashMap}; -use deno_core::{v8, JsRuntime as DenoJsRuntime, ResourceId, RuntimeOptions}; +use deno_core::{ + include_js_files, v8, Extension, JsRuntime as DenoJsRuntime, ResourceId, RuntimeOptions, +}; use super::JsRuntimeApi; use crate::asset::JsScript; @@ -41,7 +43,13 @@ struct LoadedScriptData { impl Default for JsRuntime { fn default() -> Self { let mut runtime = DenoJsRuntime::new(RuntimeOptions { - extensions: vec![ecs::extension(), log::extension()], + extensions: vec![ + ecs::extension(), + log::extension(), + Extension::builder() + .js(include_js_files!(prefix "bevy", "./native_setup.js",)) + .build(), + ], ..Default::default() }); diff --git a/src/runtime/native/native_setup.js b/src/runtime/native/native_setup.js new file mode 100644 index 0000000..6b252b5 --- /dev/null +++ b/src/runtime/native/native_setup.js @@ -0,0 +1,6 @@ +"use_strict"; + +((window) => { + // Set the bevy scripting op function to Deno's opSync function + window.bevyModJsScriptingOpSync = Deno.core.opSync; +})(globalThis); diff --git a/src/runtime/wasm/mod.rs b/src/runtime/wasm/mod.rs new file mode 100644 index 0000000..a0e24ba --- /dev/null +++ b/src/runtime/wasm/mod.rs @@ -0,0 +1,117 @@ +use bevy::prelude::*; +use bevy::utils::HashMap; +use wasm_bindgen::{prelude::*, JsCast}; +use wasm_mutex::Mutex; + +use crate::asset::JsScript; + +use super::JsRuntimeApi; + +const LOCK_SHOULD_NOT_FAIL: &str = + "Mutex lock should not fail because there should be no concurrent access"; + +// #[wasm_bindgen] +// struct Punchy; + +// #[wasm_bindgen] +// impl Punchy { +// pub fn log(&self, message: &str, level: &str) { +// let script = current_script_path(); +// match level { +// "error" => error!(script, "{}", message), +// "warn" => warn!(script, "{}", message), +// "debug" => debug!(script, "{}", message), +// "trace" => trace!(script, "{}", message), +// // Default to info +// _ => info!(script, "{}", message), +// }; +// } +// } + +#[wasm_bindgen] +extern "C" { + + #[wasm_bindgen(js_name = "Object")] + type ScriptObject; + + #[wasm_bindgen(method, catch)] + fn update(this: &ScriptObject) -> Result<(), JsValue>; + + #[wasm_bindgen(method, catch)] + fn update(this: &ScriptObject) -> Result<(), JsValue>; +} + +pub struct JsRuntime { + scripts: Mutex, wasm_bindgen::JsValue>>, +} + +impl FromWorld for JsRuntime { + fn from_world(_: &mut World) -> Self { + js_sys::eval(include_str!("./wasm_setup.js")).expect("Eval Init JS"); + js_sys::eval(include_str!("../js/ecs.js")).expect("Eval Init JS"); + js_sys::eval(include_str!("../js/log.js")).expect("Eval Init JS"); + + Self { + scripts: Default::default(), + } + } +} + +impl JsRuntimeApi for JsRuntime { + fn load_script(&self, handle: &Handle, script: &JsScript, _reload: bool) { + let function = js_sys::Function::new_no_args(&format!( + r#"return ((window) => {{ + {code} + }})(globalThis);"#, + code = script.source + )); + + let output = match function.call0(&JsValue::UNDEFINED) { + Ok(output) => output, + Err(e) => { + error!(?script.path, "Error executing script: {:?}", e); + return; + } + }; + + self.scripts + .try_lock() + .expect(LOCK_SHOULD_NOT_FAIL) + .insert(handle.clone_weak(), output); + } + + fn has_loaded(&self, handle: &Handle) -> bool { + self.scripts + .try_lock() + .expect(LOCK_SHOULD_NOT_FAIL) + .contains_key(handle) + } + + fn run_script(&self, handle: &Handle, stage: &CoreStage, _world: &mut World) { + let try_run = || { + let scripts = self.scripts.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + let output = scripts + .get(handle) + .ok_or_else(|| anyhow::format_err!("Script not loaded yet"))?; + + let output: &ScriptObject = output.dyn_ref().ok_or_else(|| { + anyhow::format_err!( + "Script must export an object with an object with an `update` function." + ) + })?; + + match stage { + CoreStage::Update => output.update(), + _ => return Ok(()), + } + .map_err(|e| anyhow::format_err!("Error executing script function: {e:?}"))?; + + Ok::<_, anyhow::Error>(()) + }; + + if let Err(e) = try_run() { + // TODO: add script path to error + error!("Error running script: {}", e); + } + } +} diff --git a/src/runtime/wasm/wasm_setup.js b/src/runtime/wasm/wasm_setup.js new file mode 100644 index 0000000..6ea2d94 --- /dev/null +++ b/src/runtime/wasm/wasm_setup.js @@ -0,0 +1,9 @@ +"use_strict"; + +((window) => { + function bevyModJsScriptingOpSync(...args) { + console.log("bevyModJsScriptingOpSync", args); + } + + window.bevyModJsScriptingOpSync = bevyModJsScriptingOpSync; +})(globalThis); diff --git a/wasm_resources/index.html b/wasm_resources/index.html new file mode 100644 index 0000000..478e9f2 --- /dev/null +++ b/wasm_resources/index.html @@ -0,0 +1,143 @@ + + + + + + + + +
+
+
+
Loading game...
+
+
+ + + + \ No newline at end of file