diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..4cd1f14 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +target = "wasm32-unknown-unknown" +rustflags = '-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals' \ No newline at end of file diff --git a/.gitignore b/.gitignore index e01a103..ecc6d51 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,3 @@ target/ /wasm-pack.log .vscode/ .idea/ -.cargo diff --git a/Cargo.lock b/Cargo.lock index c26acc5..cbd035f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -49,15 +55,23 @@ dependencies = [ "cgmath", "console_error_panic_hook", "console_log", + "gltf", "js-sys", "log", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-logger", "web-sys", "wgpu", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bit-set" version = "0.8.0" @@ -73,6 +87,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.1" @@ -114,6 +134,18 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cfg-if" version = "1.0.1" @@ -189,11 +221,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags", + "bitflags 2.9.1", "core-foundation", "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -215,6 +256,25 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -271,6 +331,45 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gltf" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" +dependencies = [ + "base64", + "byteorder", + "gltf-json", + "image", + "lazy_static", + "serde_json", + "urlencoding", +] + +[[package]] +name = "gltf-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" +dependencies = [ + "inflections", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gltf-json" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" +dependencies = [ + "gltf-derive", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "glutin_wgl_sys" version = "0.6.1" @@ -286,7 +385,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags", + "bitflags 2.9.1", "gpu-alloc-types", ] @@ -296,7 +395,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] @@ -317,7 +416,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags", + "bitflags 2.9.1", "gpu-descriptor-types", "hashbrown", ] @@ -328,7 +427,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] @@ -357,6 +456,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -367,6 +480,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jni-sys" version = "0.3.0" @@ -400,6 +525,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.174" @@ -453,13 +584,19 @@ dependencies = [ "libc", ] +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + [[package]] name = "metal" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags", + "bitflags 2.9.1", "block", "core-graphics-types", "foreign-types", @@ -468,6 +605,16 @@ dependencies = [ "paste", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "naga" version = "26.0.0" @@ -476,7 +623,7 @@ checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" dependencies = [ "arrayvec", "bit-set", - "bitflags", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -572,6 +719,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -635,7 +795,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] @@ -656,6 +816,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -682,6 +848,24 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slotmap" version = "1.0.7" @@ -703,7 +887,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] @@ -784,6 +968,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "version_check" version = "0.9.5" @@ -861,6 +1051,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -878,7 +1079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" dependencies = [ "arrayvec", - "bitflags", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "document-features", @@ -909,7 +1110,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags", + "bitflags 2.9.1", "cfg_aliases", "document-features", "hashbrown", @@ -968,7 +1169,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags", + "bitflags 2.9.1", "block", "bytemuck", "cfg-if", @@ -1012,7 +1213,7 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" dependencies = [ - "bitflags", + "bitflags 2.9.1", "bytemuck", "js-sys", "log", @@ -1242,3 +1443,18 @@ name = "xml-rs" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index d9c57d0..40be4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,16 +7,22 @@ edition = "2021" [lib] crate-type = ["cdylib"] +[features] +default = [] +atomics = [] +bulk-memory = [] + [profile.release] opt-level = "z" lto = true [dependencies] -wasm-bindgen = "0.2.100" +wasm-bindgen = { version = "0.2.100", features = ["enable-interning"] } wasm-bindgen-futures = "0.4.50" console_error_panic_hook = "0.1.7" console_log = "1.0.0" log = "0.4.27" +wasm-logger = "0.2.0" web-sys = { version = "0.3.77", features = [ "Window", "Document", @@ -39,3 +45,10 @@ bytemuck = { version = "1.23.1", features = [ cgmath = "0.18" raw-window-handle = "0.6.2" wgpu = "26.0.1" + +[dependencies.gltf] +version = "1.4" +features = ["extras", "names"] + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false \ No newline at end of file diff --git a/package.json b/package.json index 4032c64..0cb19b4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "name": "basic", "version": "0.1.0", "scripts": { - "wasm-dev": "wasm-pack build --dev --out-name wasm-index --target web .", - "wasm-release": "wasm-pack build --release --out-name wasm-index --target web .", + "wasm-dev": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' wasm-pack build --dev --out-name wasm-index --target web . -- --features atomics,bulk-memory -Z build-std=panic_abort,std", + "wasm-release": "RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' wasm-pack build --release --out-name wasm-index --target web . -- --features atomics,bulk-memory -Z build-std=panic_abort,std", "bundle-dev": "vite build --mode development", "bundle-release": "vite build", "build": "run-s clean wasm-dev bundle-dev", diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/bin/app.rs b/src/bin/app.rs new file mode 100644 index 0000000..2fe40a1 --- /dev/null +++ b/src/bin/app.rs @@ -0,0 +1,3 @@ +fn main() { + println!("hello world!"); +} diff --git a/src/canvas.rs b/src/canvas.rs deleted file mode 100644 index df9b1e0..0000000 --- a/src/canvas.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::{ops::Deref, ptr::NonNull}; -use wasm_bindgen::{JsCast, JsValue}; - -#[derive(Debug, Clone)] -pub struct Canvas { - element: web_sys::HtmlCanvasElement, - handle: u32, -} - -impl Canvas { - pub fn new(selectors: &str, handle: u32) -> Self { - // 0 is reserved for window itself. - assert!(handle > 0); - - // Injects `data-raw-handle` attribute into the canvas element. - // This is required by `wgpu::Surface` and `raw-window-handle`. - let element = Self::get_canvas_element(selectors); - element - .set_attribute("data-raw-handle", handle.to_string().as_str()) - .unwrap(); - - Self { element, handle } - } - - pub fn get_canvas_element(selectors: &str) -> web_sys::HtmlCanvasElement { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let element = document.query_selector(selectors).unwrap().unwrap(); - let canvas = element.dyn_into::().unwrap(); - let scale_factor = window.device_pixel_ratio(); - let width = (canvas.client_width() as f64 * scale_factor) as u32; - let height = (canvas.client_height() as f64 * scale_factor) as u32; - canvas.set_width(width); - canvas.set_height(height); - canvas - } - - #[inline] - pub fn handle(&self) -> u32 { - self.handle - } -} - -impl Deref for Canvas { - type Target = web_sys::HtmlCanvasElement; - - fn deref(&self) -> &Self::Target { - &self.element - } -} - -impl raw_window_handle::HasWindowHandle for Canvas { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - use raw_window_handle::{RawWindowHandle, WebCanvasWindowHandle, WindowHandle}; - - let value: &JsValue = &self.element; - let obj: NonNull = NonNull::from(value).cast(); - let handle = WebCanvasWindowHandle::new(obj); - let raw = RawWindowHandle::WebCanvas(handle); - unsafe { Ok(WindowHandle::borrow_raw(raw)) } - } -} - -impl raw_window_handle::HasDisplayHandle for Canvas { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - use raw_window_handle::{DisplayHandle, RawDisplayHandle, WebDisplayHandle}; - let handle = WebDisplayHandle::new(); - let raw = RawDisplayHandle::Web(handle); - unsafe { Ok(DisplayHandle::borrow_raw(raw)) } - } -} - -#[derive(Debug, Clone)] -pub struct OffscreenCanvas { - inner: web_sys::OffscreenCanvas, - handle: u32, -} - -impl OffscreenCanvas { - pub const fn new(canvas: web_sys::OffscreenCanvas, handle: u32) -> Self { - Self { - inner: canvas, - handle, - } - } - - pub fn each(self) -> (web_sys::OffscreenCanvas, u32) { - (self.inner, self.handle) - } -} - -impl Deref for OffscreenCanvas { - type Target = web_sys::OffscreenCanvas; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From<&Canvas> for OffscreenCanvas { - fn from(value: &Canvas) -> Self { - let offscreen = value.element.transfer_control_to_offscreen().unwrap(); - let handle = value.handle; - Self::new(offscreen, handle) - } -} - -impl raw_window_handle::HasWindowHandle for OffscreenCanvas { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - use raw_window_handle::{RawWindowHandle, WebOffscreenCanvasWindowHandle, WindowHandle}; - - let value: &JsValue = &self.inner; - let obj: NonNull = NonNull::from(value).cast(); - let handle = WebOffscreenCanvasWindowHandle::new(obj); - let raw = RawWindowHandle::WebOffscreenCanvas(handle); - unsafe { Ok(WindowHandle::borrow_raw(raw)) } - } -} - -impl raw_window_handle::HasDisplayHandle for OffscreenCanvas { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - use raw_window_handle::{DisplayHandle, RawDisplayHandle, WebDisplayHandle}; - let handle = WebDisplayHandle::new(); - let raw = RawDisplayHandle::Web(handle); - unsafe { Ok(DisplayHandle::borrow_raw(raw)) } - } -} diff --git a/src/lib.rs b/src/lib.rs index 1385438..f40e592 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,552 +1,120 @@ -use std::{cell::RefCell, mem, rc::Rc}; +use std::sync::mpsc::{self, Sender}; + +use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; -use wgpu::util::DeviceExt; -mod worker; -use worker::*; -mod canvas; -use canvas::*; + +use crate::{message::WindowEvent, platform::web, platform::web::worker::MainWorker}; + mod message; -use message::*; - -/// `App` is responsible for accessing window elements. -/// Also, it creates main worker and passes window events to the worker. -/// Main worker, on the other hand, does all works such as drawing. -#[allow(dead_code)] -#[wasm_bindgen] +mod platform; +mod renderer; +#[cfg(target_arch = "wasm32")] pub struct App { - canvas: Canvas, - worker: Rc>, + _worker: platform::web::worker::MainWorker, + worker_chan: Sender, + + // Store closures to keep them alive + resize_listener: Option>, + mousemove_listener: Option>, } -#[wasm_bindgen] impl App { - /// `index.js` creates `App` using this constructor. - #[allow(clippy::new_without_default)] - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - // Gets the canvas. - let canvas = Canvas::new("#canvas0", 1); - // Spawns main worker. - let worker = MainWorker::spawn("main-worker", 1).unwrap(); + pub async fn new() -> Result { + let (sender, receiver) = mpsc::channel::(); - // Transfers offscreen canvas to the worker. - let offscreen = OffscreenCanvas::from(&canvas); - let (offscreen, handle) = offscreen.each(); - let msg = js_sys::Array::new_with_length(3); - msg.set(0, JsMessage::INIT.into_jsvalue()); - msg.set(1, JsValue::from(offscreen.clone())); - msg.set(2, JsValue::from(handle)); - let t = js_sys::Array::new_with_length(1); - t.set(0, JsValue::from(offscreen)); - worker.post_message_with_transfer(&msg, &t).unwrap(); - let worker = Rc::new(RefCell::new(worker)); + let canvas = web::get_canvas_element("#canvas0"); + let _worker = MainWorker::spawn("main-worker", 1, move || { + wasm_bindgen_futures::spawn_local(async move { + MainWorker::run_render_loop(receiver).await; + }); + })?; - // Registers "resize" event proxy. - let worker_cloned = Rc::clone(&worker); - let msg = js_sys::Array::new_with_length(JsResizeMessage::field_num() + 1); - msg.set(0, JsMessage::WINDOW_RESIZE.into_jsvalue()); - let canvas_cloned = canvas.clone(); - let listener = Closure::::new(move || { - let window = web_sys::window().unwrap(); - let scale_factor = window.device_pixel_ratio(); - JsResizeMessage::set_js_array(&msg, &canvas_cloned, scale_factor, 1); - worker_cloned.borrow_mut().post_message(&msg).unwrap(); - }); + _worker.transfer_ownership(&canvas); + + let mut app = App { + _worker, + worker_chan: sender, + resize_listener: None, + mousemove_listener: None, + }; + + app.setup_event_listeners(); + return Ok(app); + } + + #[cfg(target_arch = "wasm32")] + pub fn setup_event_listeners(&mut self) { let window = web_sys::window().unwrap(); - window - .add_event_listener_with_callback("resize", listener.as_ref().unchecked_ref()) - .unwrap(); - listener.forget(); // Leak, but just once. + let resize_worker_chan = self.worker_chan.clone(); + + let resize_listener: Closure = Closure::new(move || { + use crate::message::ResizeMessage; - // Registers "mousemove" event proxy. - let worker_cloned = Rc::clone(&worker); - let msg = js_sys::Array::new_with_length(JsMouseMessage::field_num() + 1); - let listener = Closure::::new(move |event: web_sys::MouseEvent| { let window = web_sys::window().unwrap(); - let scale_factor = window.device_pixel_ratio(); - msg.set(0, JsMessage::MOUSE_MOVE.into_jsvalue()); - JsMouseMessage::set_js_array(&msg, event, scale_factor, 1); - worker_cloned.borrow_mut().post_message(&msg).unwrap(); + let width = window.inner_width().ok().unwrap().as_f64().unwrap(); + let height = window.inner_height().ok().unwrap().as_f64().unwrap(); + + resize_worker_chan + .send(WindowEvent::Resize(ResizeMessage { + width, + height, + scale_factor: window.device_pixel_ratio(), + })) + .unwrap(); }); - canvas - .add_event_listener_with_callback("mousemove", listener.as_ref().unchecked_ref()) - .unwrap(); - listener.forget(); // Leak, but just once. - // Registers "click" event proxy. - let worker_cloned = Rc::clone(&worker); - let msg = js_sys::Array::new_with_length(JsMouseMessage::field_num() + 1); - let listener = Closure::::new(move |event: web_sys::MouseEvent| { - let window = web_sys::window().unwrap(); - let scale_factor = window.device_pixel_ratio(); - msg.set(0, JsMessage::MOUSE_CLICK.into_jsvalue()); - JsMouseMessage::set_js_array(&msg, event, scale_factor, 1); - worker_cloned.borrow_mut().post_message(&msg).unwrap(); - }); - canvas - .add_event_listener_with_callback("click", listener.as_ref().unchecked_ref()) - .unwrap(); - listener.forget(); // Leak, but just once. + let _ = window + .add_event_listener_with_callback("resize", resize_listener.as_ref().unchecked_ref()); - Self { canvas, worker } - } -} + let mousemove_worker_chan = self.worker_chan.clone(); + let mousemove_listener: Closure = + Closure::new(move |event: web_sys::MouseEvent| { + use crate::message::MouseMessage; + let mouse_event_data = MouseMessage::from_evt(event.clone()); -thread_local! { - /// Main render state. - static STATE: RefCell = panic!(); -} + let mut event_data = WindowEvent::PointerMove(mouse_event_data.clone()); + if event.type_() == "click" { + event_data = WindowEvent::PointerClick(mouse_event_data.clone()); + } -/// Initializes [`STATE`] from worker side, not in window context. -/// That's because window and worker don't share memory (We can use shared memory with some restrictions). -/// When initialization is over, JS replaces this event handler with [`main_onmessage`]. -#[wasm_bindgen] -pub async fn main_onmessage_init(event: web_sys::MessageEvent) -> bool { - let data = event.data(); - debug_assert!(data.is_array()); - let data: js_sys::Array = data.unchecked_into(); - match JsMessage::from_f64(data.get(0)).0 { - JsMessage::INIT_INNER => { - // Initializes State. - let canvas: web_sys::OffscreenCanvas = data.get(1).unchecked_into(); - let handle = data.get(2).as_f64().unwrap() as u32; - let canvas = OffscreenCanvas::new(canvas, handle); - let state = State::new(canvas).await; - STATE.set(state); - - // Registers animation callback to the State and activate it. - let animation_cb = Closure::::new(move |time: f32| { - STATE.with_borrow_mut(|state| { - state.render(time); - state.request_animation_frame(); - }) - }); - STATE.with_borrow_mut(move |state| { - state.animation_cb = animation_cb; - state.request_animation_frame(); + mousemove_worker_chan.clone().send(event_data).unwrap(); }); - // Ready. - true - } - _ => false, // Not ready yet. - } -} - -/// Main worker's message handler for various events. -#[wasm_bindgen] -pub fn main_onmessage(event: web_sys::MessageEvent) { - let data = event.data(); - debug_assert!(data.is_array()); - let data: js_sys::Array = data.unchecked_into(); - match JsMessage::from_f64(data.get(0)).0 { - JsMessage::WINDOW_RESIZE_INNER => { - STATE.with_borrow_mut(|state| { - state.resize(JsResizeMessage::from_js_array(data, 1)); - }); - } - JsMessage::MOUSE_MOVE_INNER => { - STATE.with_borrow_mut(|state| { - state.mouse_move(JsMouseMessage::from_js_array(data, 1)); - }); - } - JsMessage::MOUSE_CLICK_INNER => { - STATE.with_borrow_mut(|state| { - state.mouse_click(JsMouseMessage::from_js_array(data, 1)); - }); - } - other => { - crate::log!("unsupported message: {:?}", other); - } - } -} - -/// Drawing relative data. -/// Note that this belongs to main worker. -struct State { - canvas: OffscreenCanvas, - surface: wgpu::Surface<'static>, - device: wgpu::Device, - queue: wgpu::Queue, - surface_config: wgpu::SurfaceConfiguration, - vertex_buffer: wgpu::Buffer, - index_buffer: wgpu::Buffer, - index_num: u32, - uniform_data: UniformData, - uniform_buffer: wgpu::Buffer, - uniform_bind_group: wgpu::BindGroup, - render_pipeline: wgpu::RenderPipeline, - animation_cb: Closure, -} - -impl State { - pub async fn new(canvas: OffscreenCanvas) -> Self { - let id = wgpu::InstanceDescriptor { - backends: wgpu::Backends::BROWSER_WEBGPU, - ..Default::default() - }; - - // wgpu instance - let instance = wgpu::Instance::new(&id); - // wgpu surface - let surface = instance - .create_surface(wgpu::SurfaceTarget::OffscreenCanvas( - web_sys::OffscreenCanvas::clone(&canvas), - )) - .unwrap(); - // wgpu adapter - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - compatible_surface: Some(&surface), - force_fallback_adapter: false, - ..Default::default() - }) - .await + let _ = window + .add_event_listener_with_callback( + "mousemove", + mousemove_listener.as_ref().unchecked_ref(), + ) .unwrap(); - log!("Adapter info: {:?}", adapter.get_info()); - log!("Adapter features: {:?}", adapter.features()); - log!("Adapter limits: {:?}", adapter.limits()); - - // wgpu device and queue - let descriptor = wgpu::DeviceDescriptor { - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::default(), - label: None, - memory_hints: wgpu::MemoryHints::default(), - trace: wgpu::Trace::default(), - }; - - let (device, queue) = adapter.request_device(&descriptor).await.unwrap(); - log!("after"); - // wgpu surface configuration - let surface_caps = surface.get_capabilities(&adapter); - let surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_caps.formats[0], - width: canvas.width(), - height: canvas.height(), - present_mode: surface_caps.present_modes[0], - alpha_mode: surface_caps.alpha_modes[0], - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - log!( - "suface size: {} x {}", - surface_config.width, - surface_config.height - ); - surface.configure(&device, &surface_config); - // wgpu vertex buffer - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Vertex buffer"), - contents: bytemuck::cast_slice(VERTICES), - usage: wgpu::BufferUsages::VERTEX, - }); - // wgpu index buffer - let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Index buffer"), - contents: bytemuck::cast_slice(INDICES), - usage: wgpu::BufferUsages::INDEX, - }); - // wgpu uniform buffer - let uniform_data = UniformData { - resolution: [canvas.width() as f32, canvas.height() as f32], - mouse_move: [std::f32::MIN, std::f32::MIN], - mouse_click: [std::f32::MIN, std::f32::MIN], - ..Default::default() - }; - let (uniform_buffer, uniform_layout, uniform_bind_group) = - State::create_uniform_buffer(&device, bytemuck::cast_slice(&[uniform_data][..])); - // wgpu shader module - let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader module"), - source: wgpu::ShaderSource::Wgsl(include_str!("example.wgsl").into()), - }); - // wgpu render pipeline - let render_pipeline = State::create_render_pipeline( - &device, - &[&uniform_layout], - &shader_module, - &surface_config, - ); - // dummy animation callback. - let animation_cb = Closure::::new(|_| {}); - - Self { - canvas, - surface, - device, - queue, - surface_config, - vertex_buffer, - index_buffer, - index_num: INDICES.len() as u32, - uniform_data, - uniform_buffer, - uniform_bind_group, - render_pipeline, - animation_cb, - } - } - - fn create_uniform_buffer( - device: &wgpu::Device, - contents: &[u8], - ) -> (wgpu::Buffer, wgpu::BindGroupLayout, wgpu::BindGroup) { - let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Uniform buffer"), - contents, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Uniform bind group layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Uniform bind group"), - layout: &bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - (buffer, bind_group_layout, bind_group) - } - - fn create_render_pipeline( - device: &wgpu::Device, - bind_group_layouts: &[&wgpu::BindGroupLayout], - shader_module: &wgpu::ShaderModule, - surface_config: &wgpu::SurfaceConfiguration, - ) -> wgpu::RenderPipeline { - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render pipeline layout"), - bind_group_layouts, - push_constant_ranges: &[], - }); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - cache: None, - label: Some("Render pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - compilation_options: wgpu::PipelineCompilationOptions::default(), - module: shader_module, - entry_point: Some("v_main"), - buffers: &[Vertex::layout()], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - fragment: Some(wgpu::FragmentState { - compilation_options: wgpu::PipelineCompilationOptions::default(), - module: shader_module, - entry_point: Some("f_main"), - targets: &[Some(wgpu::ColorTargetState { - format: surface_config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }) - } - - fn render(&mut self, time: f32) { - // Write uniform data to its buffer - self.uniform_data.time = time * 0.001; - self.queue.write_buffer( - &self.uniform_buffer, - 0, - bytemuck::cast_slice(&[self.uniform_data][..]), - ); - - let surface_texture = self.surface.get_current_texture().unwrap(); - let texture_view = surface_texture.texture.create_view(&Default::default()); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Render command encoder"), - }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - depth_slice: None, - view: &texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); - render_pass.draw_indexed(0..self.index_num, 0, 0..1); - } - self.queue.submit(std::iter::once(encoder.finish())); - surface_texture.present(); - } - - pub fn request_animation_frame(&self) { - let global = js_sys::global().unchecked_into::(); - global - .request_animation_frame(self.animation_cb.as_ref().unchecked_ref()) + let _ = window + .add_event_listener_with_callback("click", mousemove_listener.as_ref().unchecked_ref()) .unwrap(); - } - fn resize(&mut self, msg: JsResizeMessage) { - let new_width = (msg.width * msg.scale_factor) as u32; - let new_height = (msg.height * msg.scale_factor) as u32; - if new_width != self.canvas.width() || new_height != self.canvas.height() { - self.surface_config.width = new_width; - self.surface_config.height = new_height; - self.surface.configure(&self.device, &self.surface_config); - - // Update uniform data - self.uniform_data.resolution = [new_width as f32, new_height as f32]; - - log!( - "Resized: ({}, {}), scale: {}", - new_width, - new_height, - msg.scale_factor - ); - } - } - - pub fn mouse_move(&mut self, msg: JsMouseMessage) { - // Update uniform data - let x = (msg.offset_x * msg.scale_factor) as f32; - let y = (msg.offset_y * msg.scale_factor) as f32; - self.uniform_data.mouse_move = [x, y]; - } - - pub fn mouse_click(&mut self, msg: JsMouseMessage) { - // Update uniform data - let x = (msg.offset_x * msg.scale_factor) as f32; - let y = (msg.offset_y * msg.scale_factor) as f32; - self.uniform_data.mouse_click = [x, y]; + self.resize_listener = Some(resize_listener); + self.mousemove_listener = Some(mousemove_listener); } } -/// Simple vertex format. -#[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - pos: [f32; 3], - color: [f32; 3], -} - -impl Vertex { - fn layout() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - // pos - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - // color - offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x3, - }, - ], - } - } -} - -/// Vertex example. -const VERTICES: &[Vertex] = &[ - Vertex { - pos: [-1.0, 1.0, 0.0], // Top-left - color: [1.0, 0.0, 1.0], // Magenta - }, - Vertex { - pos: [-1.0, -1.0, 0.0], // Bottom-left - color: [0.0, 0.0, 1.0], // Blue - }, - Vertex { - pos: [1.0, 1.0, 0.0], // Top-right - color: [1.0, 1.0, 0.0], // Yellow - }, - Vertex { - pos: [1.0, -1.0, 0.0], // Bottom-right - color: [0.0, 1.0, 0.0], // Green - }, -]; - -const INDICES: &[u32] = &[0, 1, 2, 2, 1, 3]; // CCW, quad - -/// Simple uniform data. -#[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)] -struct UniformData { - mouse_move: [f32; 2], - mouse_click: [f32; 2], - resolution: [f32; 2], - time: f32, - _padding: f32, -} - -/// Boilerplate initialization for wasm debugging. +/// Entrypoint for the main thread #[wasm_bindgen] pub fn main() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - console_log::init_with_level(log::Level::Warn).unwrap(); + console_log::init_with_level(log::Level::Info).unwrap(); + wasm_logger::init(wasm_logger::Config::default()); + + wasm_bindgen_futures::spawn_local(async { + let app = App::new().await.unwrap(); + // keep the app running, and prevent drops of the objects App owns + Box::leak(Box::new(app)); + }); } -/// Utility -#[macro_export] -macro_rules! log { - ($($t:tt)*) => { - #[cfg(debug_assertions)] - { - $crate::console_log(format!($($t)*)); - } - } +// executes the closure it is spawned with +#[wasm_bindgen] +pub fn worker_entrypoint(ptr: u32) { + let work = unsafe { Box::from_raw(ptr as *mut Box) }; + (*work)(); } -/// Utility -pub fn console_log(s: String) { - web_sys::console::log_1(&s.into()); -} + diff --git a/src/message.rs b/src/message.rs index 91f5380..b1a5cea 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,90 +1,32 @@ -use super::canvas::Canvas; -use std::mem; -use wasm_bindgen::JsValue; +use core::fmt; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct JsMessage(pub u64); - -impl JsMessage { - /// Common message group. - const COMMON: u64 = 0; - /// Window message group. - const WINDOW: u64 = 1 << 32; - /// Mouse message group. - const MOUSE: u64 = 2 << 32; - - /// A common message requesting initialization of main object. - pub const INIT_INNER: u64 = Self::COMMON | 1; - pub const INIT: Self = Self(Self::INIT_INNER); - - /// Window resize message. - pub const WINDOW_RESIZE_INNER: u64 = Self::WINDOW | 1; - pub const WINDOW_RESIZE: Self = Self(Self::WINDOW_RESIZE_INNER); - - /// Mouse move message. - pub const MOUSE_MOVE_INNER: u64 = Self::MOUSE | 1; - pub const MOUSE_MOVE: Self = Self(Self::MOUSE_MOVE_INNER); - /// Mouse click message. - pub const MOUSE_CLICK_INNER: u64 = Self::MOUSE | 2; - pub const MOUSE_CLICK: Self = Self(Self::MOUSE_CLICK_INNER); - - /// Reinterprets value to f64 in bit level. - /// Then convert it into JsValue. - /// Use [`Self::from_f64()`] to recover. - #[inline] - pub fn into_jsvalue(self) -> JsValue { - let f: f64 = unsafe { mem::transmute(self) }; - JsValue::from(f) - } - - /// Converts message given from JS side into original type. - #[inline] - pub fn from_f64(value: JsValue) -> Self { - let f = value.as_f64().unwrap(); - Self(f.to_bits()) - } +#[derive(Debug)] +pub enum WindowEvent { + Resize(ResizeMessage), + PointerMove(MouseMessage), + PointerClick(MouseMessage), } -#[derive(Debug, Clone)] -pub struct JsResizeMessage { - pub scale_factor: f64, - pub handle: f64, - pub width: f64, - pub height: f64, -} - -impl JsResizeMessage { - /// Returns number of fileds. - /// Use this to prepare [`js_sys::Array`] buffer. - #[inline] - pub const fn field_num() -> u32 { - 4 - } - - /// Helper - pub fn set_js_array(arr: &js_sys::Array, canvas: &Canvas, scale_factor: f64, offset: u32) { - arr.set(offset, JsValue::from(scale_factor)); - arr.set(offset + 1, JsValue::from(canvas.handle())); - arr.set(offset + 2, JsValue::from(canvas.client_width())); - arr.set(offset + 3, JsValue::from(canvas.client_height())); - } - - /// Helper - pub fn from_js_array(arr: js_sys::Array, offset: u32) -> Self { - // Safety: Infallible. - unsafe { - Self { - scale_factor: arr.get(offset).as_f64().unwrap_unchecked(), - handle: arr.get(offset + 1).as_f64().unwrap_unchecked(), - width: arr.get(offset + 2).as_f64().unwrap_unchecked(), - height: arr.get(offset + 3).as_f64().unwrap_unchecked(), - } +// Display for WindowEvent +impl fmt::Display for WindowEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WindowEvent::Resize(msg) => write!(f, "Resize: {:?}", msg), + WindowEvent::PointerMove(msg) => write!(f, "PointerMove: {:?}", msg), + WindowEvent::PointerClick(msg) => write!(f, "PointerClick: {:?}", msg), } } } #[derive(Debug, Clone)] -pub struct JsMouseMessage { +pub struct ResizeMessage { + pub scale_factor: f64, + pub width: f64, + pub height: f64, +} + +#[derive(Debug, Clone)] +pub struct MouseMessage { pub scale_factor: f64, pub button: f64, pub client_x: f64, @@ -95,45 +37,18 @@ pub struct JsMouseMessage { pub offset_y: f64, } -impl JsMouseMessage { - /// Returns number of fileds. - /// Use this to prepare [`js_sys::Array`] buffer. - #[inline] - pub const fn field_num() -> u32 { - 8 - } - - /// Helper - pub fn set_js_array( - arr: &js_sys::Array, - event: web_sys::MouseEvent, - scale_factor: f64, - offset: u32, - ) { - arr.set(offset, JsValue::from(scale_factor)); - arr.set(offset + 1, JsValue::from(event.button())); - arr.set(offset + 2, JsValue::from(event.client_x())); - arr.set(offset + 3, JsValue::from(event.client_y())); - arr.set(offset + 4, JsValue::from(event.movement_x())); - arr.set(offset + 5, JsValue::from(event.movement_y())); - arr.set(offset + 6, JsValue::from(event.offset_x())); - arr.set(offset + 7, JsValue::from(event.offset_y())); - } - - /// Helper - pub fn from_js_array(arr: js_sys::Array, offset: u32) -> Self { - // Safety: Infallible. - unsafe { - Self { - scale_factor: arr.get(offset).as_f64().unwrap_unchecked(), - button: arr.get(offset + 1).as_f64().unwrap_unchecked(), - client_x: arr.get(offset + 2).as_f64().unwrap_unchecked(), - client_y: arr.get(offset + 3).as_f64().unwrap_unchecked(), - movement_x: arr.get(offset + 4).as_f64().unwrap_unchecked(), - movement_y: arr.get(offset + 5).as_f64().unwrap_unchecked(), - offset_x: arr.get(offset + 6).as_f64().unwrap_unchecked(), - offset_y: arr.get(offset + 7).as_f64().unwrap_unchecked(), - } +impl MouseMessage { + pub fn from_evt(event: web_sys::MouseEvent) -> Self { + let window = web_sys::window().unwrap(); + Self { + scale_factor: window.device_pixel_ratio(), + button: event.button() as f64, + client_x: event.client_x() as f64, + client_y: event.client_y() as f64, + movement_x: event.movement_x() as f64, + movement_y: event.movement_y() as f64, + offset_x: event.offset_x() as f64, + offset_y: event.offset_y() as f64, } } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..e9db6b3 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,5 @@ +#[cfg(target_arch = "wasm32")] +pub mod web; + +#[cfg(not(target_arch = "wasm32"))] +mod native; diff --git a/src/platform/native/mod.rs b/src/platform/native/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/platform/native/mod.rs @@ -0,0 +1 @@ + diff --git a/src/platform/web/mod.rs b/src/platform/web/mod.rs new file mode 100644 index 0000000..d8cc65d --- /dev/null +++ b/src/platform/web/mod.rs @@ -0,0 +1,16 @@ +use wasm_bindgen::JsCast; + +pub mod worker; + +pub fn get_canvas_element(selectors: &str) -> web_sys::HtmlCanvasElement { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let element = document.query_selector(selectors).unwrap().unwrap(); + let canvas = element.dyn_into::().unwrap(); + let scale_factor = window.device_pixel_ratio(); + let width = (canvas.client_width() as f64 * scale_factor) as u32; + let height = (canvas.client_height() as f64 * scale_factor) as u32; + canvas.set_width(width); + canvas.set_height(height); + canvas +} diff --git a/src/platform/web/worker/mainWorker.js b/src/platform/web/worker/mainWorker.js new file mode 100644 index 0000000..69476e3 --- /dev/null +++ b/src/platform/web/worker/mainWorker.js @@ -0,0 +1,17 @@ +// Imports wasm statically. +// Reasonable because worker can't do anything without wasm. +// And worker's loading can still be determined from external dynamically. +import wbg_init, { worker_entrypoint } from "pkg/wasm-index.js"; + +export function attachMain() {} + +let isReady = false; + +onmessage = async (event) => { + console.log("event", event); + if (isReady) return; + + isReady = true; + await wbg_init({ module_or_path: event.data[0], memory: event.data[2] }); + worker_entrypoint(event.data[3]); +}; diff --git a/src/platform/web/worker/mod.rs b/src/platform/web/worker/mod.rs new file mode 100644 index 0000000..4f690e8 --- /dev/null +++ b/src/platform/web/worker/mod.rs @@ -0,0 +1,149 @@ +use crate::message::WindowEvent; +use std::sync::mpsc::Receiver; +use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; +use wasm_bindgen::{prelude::*, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::MessageEvent; +use log::info; + +/// Binds JS. +#[wasm_bindgen(module = "/src/platform/web/worker/workerGen.js")] +extern "C" { + /// Spawn new worker in JS side in order to make bundler know about dependency. + #[wasm_bindgen(js_name = "createWorker")] + fn create_worker(kind: &str, name: &str) -> web_sys::Worker; +} + +/// Binds JS. +/// This makes wasm-bindgen bring `mainWorker.js` to the `pkg` directory. +/// So that bundler can bundle it together. +#[wasm_bindgen(module = "/src/platform/web/worker/mainWorker.js")] +extern "C" { + /// Nothing to do. + #[wasm_bindgen] + fn attachMain(); +} + +pub struct MainWorker { + handle: web_sys::Worker, + name: String, + _callback: Closure, +} + +impl Drop for MainWorker { + /// Terminates web worker *immediately*. + fn drop(&mut self) { + self.handle.terminate(); + info!("Worker({}) was terminated", &self.name); + } +} + +impl Debug for MainWorker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MainWorker") + .field("handle", &self.handle) + .field("name", &self.name) + .finish() + } +} + + + +impl MainWorker { + /// Spawns main worker from the window context. + pub fn spawn( + name: &str, + id: usize, + f: impl FnOnce() + Send + 'static, + ) -> Result { + // Creates a new worker. + let handle = create_worker("main", name); + + // Double-boxing because `dyn FnOnce` is unsized and so `Box` has + // an undefined layout (although I think in practice its a pointer and a length?). + let ptr = Box::into_raw(Box::new(Box::new(f) as Box)); + + // Sets default callback. + let callback = Closure::new(|_ev| { + info!("got a message..canvas?"); + }); + handle.set_onmessage(Some(callback.as_ref().unchecked_ref())); + + let msg: js_sys::Array = [ + &wasm_bindgen::module(), + &id.into(), + &wasm_bindgen::memory(), + &JsValue::from(ptr as u32), + ] + .into_iter() + .collect(); + + info!("posting message"); + handle.post_message(&msg)?; + + Ok(Self { + handle, + name: name.to_owned(), + _callback: callback, + }) + } + + pub fn transfer_ownership(&self, canvas: &web_sys::HtmlCanvasElement) { + let offscreen_canvas = canvas.transfer_control_to_offscreen().unwrap(); + let transfer_list = js_sys::Array::new(); + transfer_list.push(&offscreen_canvas); + + info!("posting canvas (is_undefined: {})", canvas.is_undefined()); + self.handle + .post_message_with_transfer(&offscreen_canvas, &transfer_list) + .unwrap(); + } + + pub async fn run_render_loop(events_chan: Receiver) { + use crate::renderer::Renderer; + + let canvas = wait_for_canvas_transfer().await; + + let renderer = Rc::new(RefCell::new(Renderer::new(canvas, events_chan).await)); + Renderer::run_render_loop(renderer); + } +} + +impl Deref for MainWorker { + type Target = web_sys::Worker; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +pub async fn wait_for_canvas_transfer() -> web_sys::OffscreenCanvas { + let global = js_sys::global().unchecked_into::(); + + let promise = js_sys::Promise::new(&mut |resolve, _reject| { + let handler = Closure::once(move |event: MessageEvent| { + let data = event.data(); + + info!("data received: {:?}", data); + + // Check if the received data is an OffscreenCanvas directly + if data.is_instance_of::() { + resolve + .call1(&JsValue::NULL, &data) + .expect("resolve failed"); + } + }); + + global.set_onmessage(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + }); + + let canvas: web_sys::OffscreenCanvas = JsFuture::from(promise) + .await + .expect("promise rejected") + .unchecked_into(); + + info!("received canvas: {:?}", canvas); + canvas +} diff --git a/src/worker/workerGen.js b/src/platform/web/worker/workerGen.js similarity index 100% rename from src/worker/workerGen.js rename to src/platform/web/worker/workerGen.js diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs new file mode 100644 index 0000000..efe78d1 --- /dev/null +++ b/src/renderer/mod.rs @@ -0,0 +1,392 @@ +use std::{cell::RefCell, rc::Rc, sync::mpsc::Receiver}; + +use log::info; +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::DedicatedWorkerGlobalScope; +use wgpu::util::DeviceExt; + +use crate::message::{MouseMessage, ResizeMessage, WindowEvent}; + +/// Drawing relative data. +/// Note that this belongs to main worker. +pub struct Renderer { + canvas: web_sys::OffscreenCanvas, + events_chan: Receiver, + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + surface_config: wgpu::SurfaceConfiguration, + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, + index_num: u32, + uniform_data: UniformData, + uniform_buffer: wgpu::Buffer, + uniform_bind_group: wgpu::BindGroup, + render_pipeline: wgpu::RenderPipeline, +} + +impl Renderer { + pub async fn new(canvas: web_sys::OffscreenCanvas, events_chan: Receiver) -> Self { + let id = wgpu::InstanceDescriptor { + backends: wgpu::Backends::BROWSER_WEBGPU, + ..Default::default() + }; + + // wgpu instance + let instance = wgpu::Instance::new(&id); + // wgpu surface + let surface = instance + .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone())) + .unwrap(); + // wgpu adapter + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + compatible_surface: Some(&surface), + force_fallback_adapter: false, + ..Default::default() + }) + .await + .unwrap(); + + info!("Adapter info: {:?}", adapter.get_info()); + info!("Adapter features: {:?}", adapter.features()); + info!("Adapter limits: {:?}", adapter.limits()); + + // wgpu device and queue + let descriptor = wgpu::DeviceDescriptor { + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + label: None, + memory_hints: wgpu::MemoryHints::default(), + trace: wgpu::Trace::default(), + }; + + let (device, queue) = adapter.request_device(&descriptor).await.unwrap(); + info!("after"); + // wgpu surface configuration + let surface_caps = surface.get_capabilities(&adapter); + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_caps.formats[0], + width: canvas.clone().width(), + height: canvas.clone().height(), + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + info!( + "suface size: {} x {}", + surface_config.width, surface_config.height + ); + surface.configure(&device, &surface_config); + // wgpu vertex buffer + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex buffer"), + contents: bytemuck::cast_slice(VERTICES), + usage: wgpu::BufferUsages::VERTEX, + }); + // wgpu index buffer + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index buffer"), + contents: bytemuck::cast_slice(INDICES), + usage: wgpu::BufferUsages::INDEX, + }); + // wgpu uniform buffer + let uniform_data = UniformData { + resolution: [canvas.width() as f32, canvas.height() as f32], + mouse_move: [std::f32::MIN, std::f32::MIN], + mouse_click: [std::f32::MIN, std::f32::MIN], + ..Default::default() + }; + let (uniform_buffer, uniform_layout, uniform_bind_group) = + Renderer::create_uniform_buffer(&device, bytemuck::cast_slice(&[uniform_data][..])); + // wgpu shader module + let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shader module"), + source: wgpu::ShaderSource::Wgsl(include_str!("../example.wgsl").into()), + }); + // wgpu render pipeline + let render_pipeline = Renderer::create_render_pipeline( + &device, + &[&uniform_layout], + &shader_module, + &surface_config, + ); + + Self { + canvas, + events_chan, + surface, + device, + queue, + surface_config, + vertex_buffer, + index_buffer, + index_num: INDICES.len() as u32, + uniform_data, + uniform_buffer, + uniform_bind_group, + render_pipeline, + } + } + + fn create_uniform_buffer( + device: &wgpu::Device, + contents: &[u8], + ) -> (wgpu::Buffer, wgpu::BindGroupLayout, wgpu::BindGroup) { + let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Uniform buffer"), + contents, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Uniform bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Uniform bind group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + (buffer, bind_group_layout, bind_group) + } + + fn create_render_pipeline( + device: &wgpu::Device, + bind_group_layouts: &[&wgpu::BindGroupLayout], + shader_module: &wgpu::ShaderModule, + surface_config: &wgpu::SurfaceConfiguration, + ) -> wgpu::RenderPipeline { + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render pipeline layout"), + bind_group_layouts, + push_constant_ranges: &[], + }); + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + cache: None, + label: Some("Render pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + compilation_options: wgpu::PipelineCompilationOptions::default(), + module: shader_module, + entry_point: Some("v_main"), + buffers: &[Vertex::layout()], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + compilation_options: wgpu::PipelineCompilationOptions::default(), + module: shader_module, + entry_point: Some("f_main"), + targets: &[Some(wgpu::ColorTargetState { + format: surface_config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }) + } + + fn render(&mut self, time: f32) { + // Write uniform data to its buffer + self.uniform_data.time = time * 0.001; + self.queue.write_buffer( + &self.uniform_buffer, + 0, + bytemuck::cast_slice(&[self.uniform_data][..]), + ); + + let surface_texture = self.surface.get_current_texture().unwrap(); + let texture_view = surface_texture.texture.create_view(&Default::default()); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render command encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + depth_slice: None, + view: &texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); + render_pass.draw_indexed(0..self.index_num, 0, 0..1); + } + self.queue.submit(std::iter::once(encoder.finish())); + surface_texture.present(); + } + + pub fn handle_event(&mut self, event: WindowEvent) { + match event { + WindowEvent::PointerMove(msg) => self.mouse_move(msg), + WindowEvent::Resize(msg) => self.resize(msg), + WindowEvent::PointerClick(msg) => self.mouse_click(msg), + } + } + + pub fn run_render_loop(renderer: Rc>) { + let render_frame: Closure = Closure::new(move |time: f32| { + { + let mut r = renderer.borrow_mut(); + + if let Ok(event) = r.events_chan.try_recv() { + r.handle_event(event); + } + + r.render(time); + } + + Self::run_render_loop(renderer.clone()); + }); + + let global = js_sys::global().unchecked_into::(); + + global + .request_animation_frame(render_frame.as_ref().unchecked_ref()) + .unwrap(); + + render_frame.forget(); + } + + fn resize(&mut self, msg: ResizeMessage) { + let new_width = (msg.width * msg.scale_factor) as u32; + let new_height = (msg.height * msg.scale_factor) as u32; + if new_width != self.canvas.width() || new_height != self.canvas.height() { + self.surface_config.width = new_width; + self.surface_config.height = new_height; + self.surface.configure(&self.device, &self.surface_config); + + // Update uniform data + self.uniform_data.resolution = [new_width as f32, new_height as f32]; + + info!( + "Resized: ({}, {}), scale: {}", + new_width, new_height, msg.scale_factor + ); + } + } + + pub fn mouse_move(&mut self, msg: MouseMessage) { + // Update uniform data + let x = (msg.offset_x * msg.scale_factor) as f32; + let y = (msg.offset_y * msg.scale_factor) as f32; + self.uniform_data.mouse_move = [x, y]; + } + + pub fn mouse_click(&mut self, msg: MouseMessage) { + info!("clicked"); + // Update uniform data + let x = (msg.offset_x * msg.scale_factor) as f32; + let y = (msg.offset_y * msg.scale_factor) as f32; + self.uniform_data.mouse_click = [x, y]; + } +} + +/// Simple vertex format. +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct Vertex { + pos: [f32; 3], + color: [f32; 3], +} + +impl Vertex { + fn layout() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + // pos + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + // color + offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x3, + }, + ], + } + } +} + +/// Vertex example. +const VERTICES: &[Vertex] = &[ + Vertex { + pos: [0.0, 0.5, 0.0], // Top-left + color: [1.0, 0.0, 1.0], // Magenta + }, + Vertex { + pos: [-0.5, -0.5, 0.0], // Bottom-left + color: [0.0, 0.0, 1.0], // Blue + }, + Vertex { + pos: [0.5, -0.5, 0.0], // Top-right + color: [1.0, 1.0, 0.0], // Yellow + }, +]; + +const INDICES: &[u32] = &[0, 1, 2]; // CCW, quad + +/// Simple uniform data. +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)] +struct UniformData { + mouse_move: [f32; 2], + mouse_click: [f32; 2], + resolution: [f32; 2], + time: f32, + _padding: f32, +} + + diff --git a/src/worker/mainWorker.js b/src/worker/mainWorker.js deleted file mode 100644 index 695138d..0000000 --- a/src/worker/mainWorker.js +++ /dev/null @@ -1,24 +0,0 @@ -// Imports wasm statically. -// Reasonable because worker can't do anything without wasm. -// And worker's loading can still be determined from external dynamically. -import * as wasm from '../../../..'; - -export function attachMain() {} - -onmessage = event => { - // Initailzes wasm. - const { default: wbg_init } = wasm; - wbg_init(event.data[0]).then(() => { - - // Initializes our main worker. - onmessage = async (event) => { - const ready = await wasm.main_onmessage_init(event); - if (ready) { - - // Now, worker is ready for work. - onmessage = wasm.main_onmessage; - } - } - }); -} - diff --git a/src/worker/mod.rs b/src/worker/mod.rs deleted file mode 100644 index f5298b3..0000000 --- a/src/worker/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{fmt::Debug, ops::Deref}; -use wasm_bindgen::prelude::*; - -/// Binds JS. -#[wasm_bindgen(module = "/src/worker/workerGen.js")] -extern "C" { - /// Spawn new worker in JS side in order to make bundler know about dependency. - #[wasm_bindgen(js_name = "createWorker")] - fn create_worker(kind: &str, name: &str) -> web_sys::Worker; -} - -/// Binds JS. -/// This makes wasm-bindgen bring `mainWorker.js` to the `pkg` directory. -/// So that bundler can bundle it together. -#[wasm_bindgen(module = "/src/worker/mainWorker.js")] -extern "C" { - /// Nothing to do. - #[wasm_bindgen] - fn attachMain(); -} - -pub struct MainWorker { - handle: web_sys::Worker, - name: String, - _callback: Closure, -} - -impl Drop for MainWorker { - /// Terminates web worker *immediately*. - fn drop(&mut self) { - self.handle.terminate(); - crate::log!("Worker({}) was terminated", &self.name); - } -} - -impl Debug for MainWorker { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MainWorker") - .field("handle", &self.handle) - .field("name", &self.name) - .finish() - } -} - -impl MainWorker { - /// Spawns main worker from the window context. - pub fn spawn(name: &str, id: usize) -> Result { - // Creates a new worker. - let handle = create_worker("main", name); - - // Sets default callback. - let callback = Closure::new(|_ev| { - // Implement if you want. - unimplemented!() - }); - handle.set_onmessage(Some(callback.as_ref().unchecked_ref())); - - // Initializes the worker. - let msg = js_sys::Array::new_with_length(2); - msg.set(0, wasm_bindgen::module()); - msg.set(1, id.into()); - handle.post_message(&msg)?; - - Ok(Self { - handle, - name: name.to_owned(), - _callback: callback, - }) - } -} - -impl Deref for MainWorker { - type Target = web_sys::Worker; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.handle - } -} diff --git a/static/index.html b/static/index.html index 84d6803..ae21bbe 100644 --- a/static/index.html +++ b/static/index.html @@ -14,8 +14,8 @@ height: 100%; } canvas { - width: 90%; - height: 300px; + width: 100%; + height: 100vh; background-color: yellowgreen; } @@ -26,7 +26,6 @@
-

Enjoy WebGPU + WASM + Webpack with 🍰☕

diff --git a/static/index.js b/static/index.js index 6258dc3..de12766 100644 --- a/static/index.js +++ b/static/index.js @@ -1,10 +1,13 @@ -import wbg_init, { main, App } from "../pkg/wasm-index.js"; +import wbg_init, { main } from "../pkg/wasm-index.js"; const start = async () => { await wbg_init(); - main(); - const app = new App(); }; -start(); +// Wait for DOM to be ready before starting +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', start); +} else { + start(); +} diff --git a/vite.config.js b/vite.config.js index 320eb4d..359bf03 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,12 +10,19 @@ export default defineConfig({ }, // Relative to 'root'. outDir: "../dist", + copyPublicDir: true, }, // For getting out of index.html from dist/static directory. root: "static", + publicDir: ".", worker: { format: "es", }, + resolve: { + alias: { + 'pkg': 'pkg' + } + }, plugins: [ // Makes us be able to use top level await for wasm. // Otherwise, we can restrict build.target to 'es2022', which allows top level await. @@ -23,8 +30,19 @@ export default defineConfig({ ], server: { port: 8080, + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }, + fs: { + strict: false, + }, }, preview: { port: 8080, + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }, }, });