use channels for comms (#9)

this  refactors the app setup and messaging modules
This commit is contained in:
Akash Shakdwipeea
2025-08-20 21:58:09 +05:30
committed by GitHub
parent ba2d1c064b
commit 3b81926ae1
22 changed files with 981 additions and 900 deletions
+3
View File
@@ -0,0 +1,3 @@
[build]
target = "wasm32-unknown-unknown"
rustflags = '-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals'
-1
View File
@@ -42,4 +42,3 @@ target/
/wasm-pack.log
.vscode/
.idea/
.cargo
Generated
+229 -13
View File
@@ -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",
]
+14 -1
View File
@@ -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
+2 -2
View File
@@ -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",
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("hello world!");
}
-135
View File
@@ -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::<web_sys::HtmlCanvasElement>().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::WindowHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{RawWindowHandle, WebCanvasWindowHandle, WindowHandle};
let value: &JsValue = &self.element;
let obj: NonNull<std::ffi::c_void> = 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::DisplayHandle<'_>, 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::WindowHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{RawWindowHandle, WebOffscreenCanvasWindowHandle, WindowHandle};
let value: &JsValue = &self.inner;
let obj: NonNull<std::ffi::c_void> = 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::DisplayHandle<'_>, 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)) }
}
}
+93 -525
View File
@@ -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<RefCell<MainWorker>>,
_worker: platform::web::worker::MainWorker,
worker_chan: Sender<WindowEvent>,
// Store closures to keep them alive
resize_listener: Option<Closure<dyn FnMut()>>,
mousemove_listener: Option<Closure<dyn FnMut(web_sys::MouseEvent)>>,
}
#[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<Self, JsValue> {
let (sender, receiver) = mpsc::channel::<WindowEvent>();
// 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));
// 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::<dyn Fn()>::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();
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;
});
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.
})?;
// 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::<dyn Fn(_)>::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();
});
canvas
.add_event_listener_with_callback("mousemove", listener.as_ref().unchecked_ref())
.unwrap();
listener.forget(); // Leak, but just once.
_worker.transfer_ownership(&canvas);
// 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::<dyn Fn(_)>::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.
Self { canvas, worker }
}
}
thread_local! {
/// Main render state.
static STATE: RefCell<State> = panic!();
}
/// 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::<dyn FnMut(f32)>::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();
});
// 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<dyn FnMut(f32)>,
}
impl State {
pub async fn new(canvas: OffscreenCanvas) -> Self {
let id = wgpu::InstanceDescriptor {
backends: wgpu::Backends::BROWSER_WEBGPU,
..Default::default()
let mut app = App {
_worker,
worker_chan: sender,
resize_listener: None,
mousemove_listener: None,
};
// wgpu instance
let instance = wgpu::Instance::new(&id);
// wgpu surface
let surface = instance
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(
web_sys::OffscreenCanvas::clone(&canvas),
))
app.setup_event_listeners();
return Ok(app);
}
#[cfg(target_arch = "wasm32")]
pub fn setup_event_listeners(&mut self) {
let window = web_sys::window().unwrap();
let resize_worker_chan = self.worker_chan.clone();
let resize_listener: Closure<dyn FnMut()> = Closure::new(move || {
use crate::message::ResizeMessage;
let window = web_sys::window().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();
// 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("resize", resize_listener.as_ref().unchecked_ref());
let mousemove_worker_chan = self.worker_chan.clone();
let mousemove_listener: Closure<dyn FnMut(web_sys::MouseEvent)> =
Closure::new(move |event: web_sys::MouseEvent| {
use crate::message::MouseMessage;
let mouse_event_data = MouseMessage::from_evt(event.clone());
let mut event_data = WindowEvent::PointerMove(mouse_event_data.clone());
if event.type_() == "click" {
event_data = WindowEvent::PointerClick(mouse_event_data.clone());
}
mousemove_worker_chan.clone().send(event_data).unwrap();
});
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::<dyn FnMut(f32)>::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::<web_sys::DedicatedWorkerGlobalScope>();
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
);
self.resize_listener = Some(resize_listener);
self.mousemove_listener = Some(mousemove_listener);
}
}
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];
}
}
/// 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::<Vertex>() 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<dyn FnOnce()>) };
(*work)();
}
/// Utility
pub fn console_log(s: String) {
web_sys::console::log_1(&s.into());
}
+27 -112
View File
@@ -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)
#[derive(Debug)]
pub enum WindowEvent {
Resize(ResizeMessage),
PointerMove(MouseMessage),
PointerClick(MouseMessage),
}
/// 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())
// 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 JsResizeMessage {
pub struct ResizeMessage {
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(),
}
}
}
}
#[derive(Debug, Clone)]
pub struct JsMouseMessage {
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 {
impl MouseMessage {
pub fn from_evt(event: web_sys::MouseEvent) -> Self {
let window = web_sys::window().unwrap();
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(),
}
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,
}
}
}
+5
View File
@@ -0,0 +1,5 @@
#[cfg(target_arch = "wasm32")]
pub mod web;
#[cfg(not(target_arch = "wasm32"))]
mod native;
+1
View File
@@ -0,0 +1 @@
+16
View File
@@ -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::<web_sys::HtmlCanvasElement>().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
}
+17
View File
@@ -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]);
};
+149
View File
@@ -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<dyn FnMut(web_sys::Event)>,
}
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<Self, JsValue> {
// Creates a new worker.
let handle = create_worker("main", name);
// Double-boxing because `dyn FnOnce` is unsized and so `Box<dyn FnOnce()>` 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<dyn FnOnce()>));
// 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<WindowEvent>) {
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::<web_sys::DedicatedWorkerGlobalScope>();
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::<web_sys::OffscreenCanvas>() {
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
}
+392
View File
@@ -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<WindowEvent>,
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<WindowEvent>) -> 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<RefCell<Renderer>>) {
let render_frame: Closure<dyn FnMut(f32)> = 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::<DedicatedWorkerGlobalScope>();
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::<Vertex>() 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,
}
-24
View File
@@ -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;
}
}
});
}
-79
View File
@@ -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<dyn FnMut(web_sys::Event)>,
}
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<Self, JsValue> {
// 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
}
}
+2 -3
View File
@@ -14,8 +14,8 @@
height: 100%;
}
canvas {
width: 90%;
height: 300px;
width: 100%;
height: 100vh;
background-color: yellowgreen;
}
</style>
@@ -26,7 +26,6 @@
<ul>
</ul>
</nav>
<h1 class="title">Enjoy WebGPU + WASM + Webpack with 🍰☕</h1>
</header>
<main>
<section>
+6 -3
View File
@@ -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();
};
// Wait for DOM to be ready before starting
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
+18
View File
@@ -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',
},
},
});