Web version now has feature parity with native

This commit is contained in:
Matthew Gordon 2025-05-24 18:46:51 -03:00
parent a15eedcd1b
commit c5930e55f9
8 changed files with 158 additions and 59 deletions

10
Cargo.lock generated
View File

@ -2707,7 +2707,6 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"wgpu-core-deps-apple", "wgpu-core-deps-apple",
"wgpu-core-deps-emscripten", "wgpu-core-deps-emscripten",
"wgpu-core-deps-wasm",
"wgpu-core-deps-windows-linux-android", "wgpu-core-deps-windows-linux-android",
"wgpu-hal", "wgpu-hal",
"wgpu-types", "wgpu-types",
@ -2731,15 +2730,6 @@ dependencies = [
"wgpu-hal", "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]] [[package]]
name = "wgpu-core-deps-windows-linux-android" name = "wgpu-core-deps-windows-linux-android"
version = "25.0.0" version = "25.0.0"

View File

@ -20,7 +20,7 @@ env_logger = "0.11.3"
futures = { version = "0.3.30", features = ["executor"] } futures = { version = "0.3.30", features = ["executor"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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 = "0.2.84"
wasm-bindgen-futures = "0.4.42" wasm-bindgen-futures = "0.4.42"
console_log = "1.0" console_log = "1.0"
@ -30,7 +30,8 @@ web-sys = { version = "0.3", features = [
"Document", "Document",
"Window", "Window",
"Element", "Element",
"File" "File",
"Performance"
]} ]}
[dev-dependencies] [dev-dependencies]

View File

@ -1,6 +1,9 @@
use wgsl_shader_assembler::wgsl_module; use wgsl_shader_assembler::wgsl_module;
use super::raster::{Dem, DemBvh}; use super::{
FrameTimer,
raster::{Dem, DemBvh},
};
use { use {
bytemuck::{Pod, Zeroable}, bytemuck::{Pod, Zeroable},
std::{borrow::Cow, num::NonZero, rc::Rc}, std::{borrow::Cow, num::NonZero, rc::Rc},
@ -16,7 +19,7 @@ pub struct DemRenderer {
index_count: usize, index_count: usize,
camera: Camera, camera: Camera,
uniforms: UniformBufferManager, uniforms: UniformBufferManager,
animation_start: std::time::Instant, animation_phase: f32,
} }
#[repr(C)] #[repr(C)]
@ -296,24 +299,33 @@ impl DemRenderer {
vertex_buffer, vertex_buffer,
index_buffer, index_buffer,
index_count, index_count,
animation_start: std::time::Instant::now(), animation_phase: 0.0,
camera, camera,
uniforms, 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 { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("DemRendererCommandEncoder"), label: Some("DemRendererCommandEncoder"),
}); });
let camera_position = get_animated_camera_position( let radians_per_second = 0.5;
self.animation_start.elapsed(), self.animation_phase += frame_timer.get_frame_time_seconds() as f32 * radians_per_second;
self.get_max_dem_dimension(), 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 self.camera
.set_position(camera_position, self.get_dem_centre()); .set_position(camera_position, self.get_dem_centre());
self.uniforms.set_camera_to_world_matrix(self.camera.get_camera_to_world_matrix()); self.uniforms
self.uniforms.set_world_to_ndc_matrix(self.camera.get_world_to_ndc_matrix()); .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 self.uniforms
.set_camera_position(camera_position.extend(1.0)); .set_camera_position(camera_position.extend(1.0));
self.uniforms.update_buffer(queue); self.uniforms.update_buffer(queue);
@ -493,8 +505,7 @@ fn create_dembvh_texture(
texture.create_view(&wgpu::TextureViewDescriptor::default()) texture.create_view(&wgpu::TextureViewDescriptor::default())
} }
fn get_animated_camera_position(animation_time: std::time::Duration, dem_size: f32) -> glam::Vec3 { fn get_animated_camera_position(animation_phase: f32, dem_size: f32) -> glam::Vec3 {
let animation_phase = 2.0 * std::f32::consts::PI * (animation_time.as_secs_f32() % 100.0) / 100.0;
glam::Vec3::new( glam::Vec3::new(
dem_size * f32::sin(animation_phase), dem_size * f32::sin(animation_phase),
dem_size * f32::cos(animation_phase), dem_size * f32::cos(animation_phase),

View File

@ -1,6 +1,6 @@
use std::rc::Rc; use std::rc::Rc;
use crate::mvu::{Event, File, MvuApp, Size2i}; use crate::mvu::{Event, File, MvuApp, Size2i, FrameTimer};
use { use {
log::info, log::info,
std::borrow::Cow, std::borrow::Cow,
@ -29,6 +29,7 @@ struct Context {
render_pipeline: RenderPipeline, render_pipeline: RenderPipeline,
queue: Queue, queue: Queue,
scene_data: Option<DemRenderer>, scene_data: Option<DemRenderer>,
frame_timer: Box<dyn FrameTimer>,
} }
#[derive(Default)] #[derive(Default)]
@ -43,7 +44,13 @@ impl App {
} }
impl MvuApp<Model> for App { impl MvuApp<Model> 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<dyn FrameTimer>,
) {
let adapter = instance let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions { .request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance, power_preference: wgpu::PowerPreference::HighPerformance,
@ -116,6 +123,7 @@ impl MvuApp<Model> for App {
render_pipeline, render_pipeline,
queue, queue,
scene_data: None, scene_data: None,
frame_timer: frame_timer,
}); });
info!("Initialized {}x{}.", size.width, size.height); info!("Initialized {}x{}.", size.width, size.height);
@ -146,6 +154,7 @@ impl MvuApp<Model> for App {
async fn view(&mut self, model: Rc<Model>) -> Result<(), Box<dyn std::error::Error>> { async fn view(&mut self, model: Rc<Model>) -> Result<(), Box<dyn std::error::Error>> {
if let Some(context) = &mut self.context { if let Some(context) = &mut self.context {
context.frame_timer.mark_frame_start();
if context.scene_data.is_none() { if context.scene_data.is_none() {
if let Some(dem) = &model.dem { if let Some(dem) = &model.dem {
context.scene_data = Some(DemRenderer::new( context.scene_data = Some(DemRenderer::new(
@ -165,7 +174,12 @@ impl MvuApp<Model> for App {
.texture .texture
.create_view(&wgpu::TextureViewDescriptor::default()); .create_view(&wgpu::TextureViewDescriptor::default());
if let Some(scene_data) = &mut context.scene_data { 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 { } else {
let mut encoder = context let mut encoder = context
.device .device

View File

@ -23,10 +23,21 @@ pub struct Size2i {
pub height: u32, pub height: u32,
} }
pub trait FrameTimer {
fn mark_frame_start(&mut self);
fn get_frame_time_seconds(&self) -> f64;
}
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
// https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html#where-the-gaps-lie // https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html#where-the-gaps-lie
pub trait MvuApp<M> { pub trait MvuApp<M> {
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<dyn FrameTimer>,
);
async fn resize(&mut self, size: Size2i); async fn resize(&mut self, size: Size2i);
async fn update(&self, model: Rc<M>, event: Event) -> Rc<M>; async fn update(&self, model: Rc<M>, event: Event) -> Rc<M>;
async fn view(&mut self, model: Rc<M>) -> Result<(), Box<dyn std::error::Error>>; async fn view(&mut self, model: Rc<M>) -> Result<(), Box<dyn std::error::Error>>;

View File

@ -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<A, M> { struct AppHost<A, M> {
window: Option<Arc<Window>>, window: Option<Arc<Window>>,
app: A, app: A,
@ -42,7 +67,12 @@ where
let surface = instance.create_surface(Arc::clone(&window)).unwrap(); let surface = instance.create_surface(Arc::clone(&window)).unwrap();
let PhysicalSize { width, height } = window.inner_size(); let PhysicalSize { width, height } = window.inner_size();
self.window = Some(window); 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 // TODO: The idea with these `block_on()` calls is that eventually I'll

View File

@ -8,9 +8,43 @@ use {
use crate::{ use crate::{
app::{App, Model}, app::{App, Model},
mvu,
mvu::{Event, File, MvuApp, Size2i}, 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] #[wasm_bindgen]
#[derive(Clone)] #[derive(Clone)]
pub struct PteropusCanvas { pub struct PteropusCanvas {
@ -18,38 +52,13 @@ pub struct PteropusCanvas {
model: Rc<Model>, model: Rc<Model>,
} }
/*struct ArrayFile {
data: Vec<u8>,
}
impl std::io::Read for ArrayFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
todo!()
}
}
impl std::io::Seek for ArrayFile {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
todo!()
}
}
impl File for ArrayFile {}*/
#[wasm_bindgen] #[wasm_bindgen]
impl PteropusCanvas { impl PteropusCanvas {
async fn init(&self) { async fn init(&self) {
let window = web_sys::window().expect("get window"); let html_canvas = self.get_canvas();
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 size = Size2i { let size = Size2i {
width: html_canvas.width(), width: html_canvas.client_width() as u32,
height: html_canvas.height(), height: html_canvas.client_height() as u32,
}; };
let instance = wgpu::Instance::default(); let instance = wgpu::Instance::default();
let surface_target = SurfaceTarget::Canvas(html_canvas); let surface_target = SurfaceTarget::Canvas(html_canvas);
@ -60,7 +69,7 @@ impl PteropusCanvas {
self.app self.app
.lock() .lock()
.expect("get app mutex") .expect("get app mutex")
.init(&instance, surface, size) .init(&instance, surface, size, Box::new(FrameTimer::new()))
.await; .await;
} }
@ -74,6 +83,19 @@ impl PteropusCanvas {
.expect("view succeeds"); .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] #[wasm_bindgen]
pub async fn load_file(&mut self, file: web_sys::File) { pub async fn load_file(&mut self, file: web_sys::File) {
let data = gloo::file::futures::read_as_bytes(&file.into()) let data = gloo::file::futures::read_as_bytes(&file.into())
@ -89,6 +111,17 @@ impl PteropusCanvas {
) )
.await; .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<Model>) -> PteropusCanvas { pub async fn run(app: App, model: Rc<Model>) -> PteropusCanvas {

View File

@ -24,7 +24,8 @@
let pteropus = await init_pteropus(); 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"); let mainCanvas = document.getElementById("pteropus-canvas");
mainCanvas.addEventListener("drop", async (event) => { mainCanvas.addEventListener("drop", async (event) => {
@ -32,6 +33,10 @@
test_file_data = event.dataTransfer.files[0] test_file_data = event.dataTransfer.files[0]
}); });
mainCanvas.addEventListener("dragover", fileDragOverHandler); mainCanvas.addEventListener("dragover", fileDragOverHandler);
const resizeObserver = new ResizeObserver((entries) => {
needs_resize = true;
});
resizeObserver.observe(mainCanvas)
while(true) while(true)
{ {
@ -43,6 +48,10 @@
await pteropus.load_file(test_file_data); await pteropus.load_file(test_file_data);
test_file_data = null test_file_data = null
} }
if(needs_resize) {
await pteropus.on_resize();
needs_resize = false;
}
await new Promise(requestAnimationFrame); await new Promise(requestAnimationFrame);
} }
} }