From c5930e55f956c45c5efea18110b55f400618decb Mon Sep 17 00:00:00 2001 From: Matthew Gordon Date: Sat, 24 May 2025 18:46:51 -0300 Subject: [PATCH] Web version now has feature parity with native --- Cargo.lock | 10 ---- Cargo.toml | 5 +- src/app/dem_renderer/mod.rs | 35 +++++++++----- src/app/mod.rs | 20 ++++++-- src/mvu/mod.rs | 13 +++++- src/native/mod.rs | 32 ++++++++++++- src/wasm/mod.rs | 91 +++++++++++++++++++++++++------------ web/index.html | 11 ++++- 8 files changed, 158 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f661fe9..0627b85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2707,7 +2707,6 @@ dependencies = [ "thiserror 2.0.12", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", - "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", @@ -2731,15 +2730,6 @@ dependencies = [ "wgpu-hal", ] -[[package]] -name = "wgpu-core-deps-wasm" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca8809ad123f6c7f2c5e01a2c7117c4fdfd02f70bd422ee2533f69dfa98756c" -dependencies = [ - "wgpu-hal", -] - [[package]] name = "wgpu-core-deps-windows-linux-android" version = "25.0.0" diff --git a/Cargo.toml b/Cargo.toml index 0695164..1928c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ env_logger = "0.11.3" futures = { version = "0.3.30", features = ["executor"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wgpu = { version = "25.0.0", features = ["webgl"]} +wgpu = { version = "25.0.0"} wasm-bindgen = "0.2.84" wasm-bindgen-futures = "0.4.42" console_log = "1.0" @@ -30,7 +30,8 @@ web-sys = { version = "0.3", features = [ "Document", "Window", "Element", - "File" + "File", + "Performance" ]} [dev-dependencies] diff --git a/src/app/dem_renderer/mod.rs b/src/app/dem_renderer/mod.rs index 5578cff..cc8cee3 100644 --- a/src/app/dem_renderer/mod.rs +++ b/src/app/dem_renderer/mod.rs @@ -1,6 +1,9 @@ use wgsl_shader_assembler::wgsl_module; -use super::raster::{Dem, DemBvh}; +use super::{ + FrameTimer, + raster::{Dem, DemBvh}, +}; use { bytemuck::{Pod, Zeroable}, std::{borrow::Cow, num::NonZero, rc::Rc}, @@ -16,7 +19,7 @@ pub struct DemRenderer { index_count: usize, camera: Camera, uniforms: UniformBufferManager, - animation_start: std::time::Instant, + animation_phase: f32, } #[repr(C)] @@ -296,24 +299,33 @@ impl DemRenderer { vertex_buffer, index_buffer, index_count, - animation_start: std::time::Instant::now(), + animation_phase: 0.0, camera, uniforms, } } - pub fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue) { + pub fn render( + &mut self, + frame_timer: &dyn FrameTimer, + view: &wgpu::TextureView, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("DemRendererCommandEncoder"), }); - let camera_position = get_animated_camera_position( - self.animation_start.elapsed(), - self.get_max_dem_dimension(), - ); + let radians_per_second = 0.5; + self.animation_phase += frame_timer.get_frame_time_seconds() as f32 * radians_per_second; + self.animation_phase = self.animation_phase % (2.0 * std::f32::consts::PI); + let camera_position = + get_animated_camera_position(self.animation_phase, self.get_max_dem_dimension()); self.camera .set_position(camera_position, self.get_dem_centre()); - self.uniforms.set_camera_to_world_matrix(self.camera.get_camera_to_world_matrix()); - self.uniforms.set_world_to_ndc_matrix(self.camera.get_world_to_ndc_matrix()); + self.uniforms + .set_camera_to_world_matrix(self.camera.get_camera_to_world_matrix()); + self.uniforms + .set_world_to_ndc_matrix(self.camera.get_world_to_ndc_matrix()); self.uniforms .set_camera_position(camera_position.extend(1.0)); self.uniforms.update_buffer(queue); @@ -493,8 +505,7 @@ fn create_dembvh_texture( texture.create_view(&wgpu::TextureViewDescriptor::default()) } -fn get_animated_camera_position(animation_time: std::time::Duration, dem_size: f32) -> glam::Vec3 { - let animation_phase = 2.0 * std::f32::consts::PI * (animation_time.as_secs_f32() % 100.0) / 100.0; +fn get_animated_camera_position(animation_phase: f32, dem_size: f32) -> glam::Vec3 { glam::Vec3::new( dem_size * f32::sin(animation_phase), dem_size * f32::cos(animation_phase), diff --git a/src/app/mod.rs b/src/app/mod.rs index 7ddcd1e..e65cc66 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::mvu::{Event, File, MvuApp, Size2i}; +use crate::mvu::{Event, File, MvuApp, Size2i, FrameTimer}; use { log::info, std::borrow::Cow, @@ -29,6 +29,7 @@ struct Context { render_pipeline: RenderPipeline, queue: Queue, scene_data: Option, + frame_timer: Box, } #[derive(Default)] @@ -43,7 +44,13 @@ impl App { } impl MvuApp for App { - async fn init(&mut self, instance: &Instance, surface: Surface<'static>, size: Size2i) { + async fn init( + &mut self, + instance: &Instance, + surface: Surface<'static>, + size: Size2i, + frame_timer: Box, + ) { let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, @@ -116,6 +123,7 @@ impl MvuApp for App { render_pipeline, queue, scene_data: None, + frame_timer: frame_timer, }); info!("Initialized {}x{}.", size.width, size.height); @@ -146,6 +154,7 @@ impl MvuApp for App { async fn view(&mut self, model: Rc) -> Result<(), Box> { if let Some(context) = &mut self.context { + context.frame_timer.mark_frame_start(); if context.scene_data.is_none() { if let Some(dem) = &model.dem { context.scene_data = Some(DemRenderer::new( @@ -165,7 +174,12 @@ impl MvuApp for App { .texture .create_view(&wgpu::TextureViewDescriptor::default()); if let Some(scene_data) = &mut context.scene_data { - scene_data.render(&view, &context.device, &context.queue); + scene_data.render( + context.frame_timer.as_ref(), + &view, + &context.device, + &context.queue, + ); } else { let mut encoder = context .device diff --git a/src/mvu/mod.rs b/src/mvu/mod.rs index 5bce9e7..b79ad83 100644 --- a/src/mvu/mod.rs +++ b/src/mvu/mod.rs @@ -23,10 +23,21 @@ pub struct Size2i { pub height: u32, } +pub trait FrameTimer { + fn mark_frame_start(&mut self); + fn get_frame_time_seconds(&self) -> f64; +} + #[allow(async_fn_in_trait)] // https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html#where-the-gaps-lie pub trait MvuApp { - async fn init(&mut self, instance: &Instance, surface: Surface<'static>, new_size: Size2i); + async fn init( + &mut self, + instance: &Instance, + surface: Surface<'static>, + new_size: Size2i, + frame_timer: Box, + ); async fn resize(&mut self, size: Size2i); async fn update(&self, model: Rc, event: Event) -> Rc; async fn view(&mut self, model: Rc) -> Result<(), Box>; diff --git a/src/native/mod.rs b/src/native/mod.rs index 846e019..a5e83ee 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -12,6 +12,31 @@ use { }, }; +struct FrameTimer { + last_frame_start: std::time::Instant, + current_frame_start: std::time::Instant, +} + +impl FrameTimer { + fn new() -> Self { + FrameTimer { + last_frame_start: std::time::Instant::now(), + current_frame_start: std::time::Instant::now(), + } + } +} + +impl mvu::FrameTimer for FrameTimer { + fn mark_frame_start(&mut self) { + self.last_frame_start = self.current_frame_start; + self.current_frame_start = std::time::Instant::now(); + } + + fn get_frame_time_seconds(&self) -> f64 { + (self.current_frame_start - self.last_frame_start).as_secs_f64() + } +} + struct AppHost { window: Option>, app: A, @@ -42,7 +67,12 @@ where let surface = instance.create_surface(Arc::clone(&window)).unwrap(); let PhysicalSize { width, height } = window.inner_size(); self.window = Some(window); - block_on(self.app.init(&instance, surface, Size2i { width, height })); + block_on(self.app.init( + &instance, + surface, + Size2i { width, height }, + Box::new(FrameTimer::new()), + )); } // TODO: The idea with these `block_on()` calls is that eventually I'll diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs index 699b297..d16623e 100644 --- a/src/wasm/mod.rs +++ b/src/wasm/mod.rs @@ -8,9 +8,43 @@ use { use crate::{ app::{App, Model}, + mvu, mvu::{Event, File, MvuApp, Size2i}, }; +struct FrameTimer { + performance: web_sys::Performance, + last_frame_start_milliseconds: f64, + current_frame_start_milliseconds: f64, +} + +impl FrameTimer { + fn new() -> Self { + let performance = web_sys::window() + .expect("get window") + .performance() + .expect("window has performance object"); + let last_frame_start_milliseconds = performance.now(); + let current_frame_start_milliseconds = performance.now(); + Self { + performance, + last_frame_start_milliseconds, + current_frame_start_milliseconds, + } + } +} + +impl mvu::FrameTimer for FrameTimer { + fn mark_frame_start(&mut self) { + self.last_frame_start_milliseconds = self.current_frame_start_milliseconds; + self.current_frame_start_milliseconds = self.performance.now(); + } + + fn get_frame_time_seconds(&self) -> f64 { + (self.current_frame_start_milliseconds - self.last_frame_start_milliseconds) * 0.001 + } +} + #[wasm_bindgen] #[derive(Clone)] pub struct PteropusCanvas { @@ -18,38 +52,13 @@ pub struct PteropusCanvas { model: Rc, } -/*struct ArrayFile { - data: Vec, -} - -impl std::io::Read for ArrayFile { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - todo!() - } -} - -impl std::io::Seek for ArrayFile { - fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { - todo!() - } -} - -impl File for ArrayFile {}*/ - #[wasm_bindgen] impl PteropusCanvas { async fn init(&self) { - let window = web_sys::window().expect("get window"); - let document = window.document().expect("get HTML document"); - let canvas_element = document - .get_element_by_id("pteropus-canvas") - .expect("document contains element with id \"pteropus-canvas\""); - let html_canvas: HtmlCanvasElement = canvas_element - .dyn_into() - .expect("pteropus-canvas element is a canvas"); + let html_canvas = self.get_canvas(); let size = Size2i { - width: html_canvas.width(), - height: html_canvas.height(), + width: html_canvas.client_width() as u32, + height: html_canvas.client_height() as u32, }; let instance = wgpu::Instance::default(); let surface_target = SurfaceTarget::Canvas(html_canvas); @@ -60,7 +69,7 @@ impl PteropusCanvas { self.app .lock() .expect("get app mutex") - .init(&instance, surface, size) + .init(&instance, surface, size, Box::new(FrameTimer::new())) .await; } @@ -74,6 +83,19 @@ impl PteropusCanvas { .expect("view succeeds"); } + #[wasm_bindgen] + pub async fn on_resize(&self) { + let html_canvas = self.get_canvas(); + self.app + .lock() + .expect("get app mutex") + .resize(Size2i { + width: html_canvas.client_width() as u32, + height: html_canvas.client_height() as u32, + }) + .await; + } + #[wasm_bindgen] pub async fn load_file(&mut self, file: web_sys::File) { let data = gloo::file::futures::read_as_bytes(&file.into()) @@ -89,6 +111,17 @@ impl PteropusCanvas { ) .await; } + + pub fn get_canvas(&self) -> HtmlCanvasElement { + let window = web_sys::window().expect("get window"); + let document = window.document().expect("get HTML document"); + let canvas_element = document + .get_element_by_id("pteropus-canvas") + .expect("document contains element with id \"pteropus-canvas\""); + canvas_element + .dyn_into() + .expect("pteropus-canvas element is a canvas") + } } pub async fn run(app: App, model: Rc) -> PteropusCanvas { diff --git a/web/index.html b/web/index.html index 5861cdc..4ed02ac 100644 --- a/web/index.html +++ b/web/index.html @@ -24,7 +24,8 @@ let pteropus = await init_pteropus(); - let test_file_data = null + let test_file_data = null; + let needs_resize = true; let mainCanvas = document.getElementById("pteropus-canvas"); mainCanvas.addEventListener("drop", async (event) => { @@ -32,6 +33,10 @@ test_file_data = event.dataTransfer.files[0] }); mainCanvas.addEventListener("dragover", fileDragOverHandler); + const resizeObserver = new ResizeObserver((entries) => { + needs_resize = true; + }); + resizeObserver.observe(mainCanvas) while(true) { @@ -43,6 +48,10 @@ await pteropus.load_file(test_file_data); test_file_data = null } + if(needs_resize) { + await pteropus.on_resize(); + needs_resize = false; + } await new Promise(requestAnimationFrame); } }