use std::cell::{Cell, RefCell}; use std::rc::Rc; use wasm_bindgen::prelude::wasm_bindgen; use crate::gpu::Wgpu; use crate::gpu_resource::{profiler, GpuPass, ProfileMap}; use crate::graph::{ColorAttachment, DepthAttachment}; use crate::render_data::RenderData; use crate::store::{Loadout, Store}; #[wasm_bindgen(inline_js = r#" const channel = new MessageChannel(); const ready = []; channel.port1.onmessage = () => ready.shift()?.(); export function nextFrame() { return new Promise(resolve => requestAnimationFrame(resolve)); } export function yieldTask() { return new Promise(resolve => { ready.push(resolve); channel.port2.postMessage(0); }); } "#)] extern "C" { #[wasm_bindgen(js_name = nextFrame)] async fn next_frame(); #[wasm_bindgen(js_name = yieldTask)] async fn yield_task(); } pub struct RenderLoop { playing: Cell, fps: Cell, started: Cell, frame: Cell, elapsed: Cell, last: Cell, active_loadout: Loadout, profile_state: RefCell, } impl RenderLoop { pub fn new() -> Self { Self { playing: Cell::new(true), fps: Cell::new(0), started: Cell::new(false), frame: Cell::new(0), elapsed: Cell::new(0.0), last: Cell::new(js_sys::Date::now()), active_loadout: todo!(), profile_state: RefCell::new(profiler::ProfileState::Detached), } } pub fn play(&self) { self.last.set(js_sys::Date::now()); self.playing.set(true); } pub fn pause(&self) { self.playing.set(false); } pub fn set_fps(&self, fps: u32) -> Result<(), &'static str> { if fps > 1000 { return Err("FPS"); } self.fps.set(fps); Ok(()) } pub fn set_profiling(&mut self) { self.profile_state = RefCell::new(profiler::ProfileState::Attached); } pub fn take_profile(&self) -> Option { if let profiler::ProfileState::Ready(data) = self.profile_state.get_mut().take() { Some(data) } else { None } } pub fn start(self: &Rc, gpu: &Wgpu, data: Rc>) { if self.started.replace(true) { return; } let control = self.clone(); wasm_bindgen_futures::spawn_local(async move { loop { next_frame().await; if !control.playing.get() { continue; } let started = js_sys::Date::now(); let fps = control.fps.get(); if fps != 0 && started - control.last.get() < 900.0 / f64::from(fps) { continue; } let delta = (started - control.last.replace(started)) / 1000.0; let elapsed = control.elapsed.get() + delta; control.elapsed.set(elapsed); let frame = control.frame.get().wrapping_add(1); control.frame.set(frame); { let mut data = data.borrow_mut(); data.update_signals(delta as f32, frame, elapsed as f32, control.fps.get()); } // returns false when no render is pending if !data.borrow_mut().begin_render() { continue; } let should_profile = matches!( *control.profile_state.borrow(), profiler::ProfileState::Attached ); gpu.render(&mut self.active_loadout, &data.borrow(), should_profile) .and_then(|profile_metadata| { // we are done with render loop but we got a profile data for this // frame, so we need to process that control.profile_state }); if let Some((profile, adapter, width, height)) = submission { control.profile_pending.set(true); control.profile_after.set(started + 250.0); let control = control.clone(); wasm_bindgen_futures::spawn_local(async move { while profile.state() == 0 { yield_task().await; } if profile.state() == 1 { let passes = profile .read() .into_iter() .map(|(name, milliseconds)| profiler::Pass { name, milliseconds }) .collect::>(); let milliseconds = passes.iter().map(|pass| pass.milliseconds).sum::(); if control.profiling.get() { *control.profile_state.borrow_mut() = Some( profiler::FrameData { frame, milliseconds, readback_milliseconds: js_sys::Date::now() - started, adapter, canvas: profiler::Canvas { width, height }, passes, } .to_string(), ); } } control.profile_pending.set(false); }); } } }); } } impl Wgpu { fn render( &mut self, loadout: &mut Loadout, data: &RenderData, profile: bool, ) -> Result { for buffer in loadout.resources.buffers.values() { if !buffer.sync_each_frame { continue; } self.queue.write_buffer( &buffer.buffer, 0, data.bytes(&buffer.source).ok_or("ROWS_UNKNOWN")?, ); } let output = match self.surface.get_current_texture() { Ok(output) => output, Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { self.surface.configure(&self.device, &self.config); self.surface.get_current_texture().map_err(|_| "SURFACE")? } Err(wgpu::SurfaceError::Timeout) => return Err("SURFACE".into()), Err(_) => return Err("SURFACE".into()), }; let surface_view = output .texture .create_view(&wgpu::TextureViewDescriptor::default()); // profiler is used only when enabled // the presence of a profiler means: // - profiler is available and // - we want to profile this frame let profiler = if profile { loadout.resources.profiler } else { None }; let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("frame"), }); for (pass_index, compiled) in loadout.resources.passes.iter().enumerate() { match compiled { GpuPass::Compute { pass, pipeline, bind_groups, .. } => { let pass = &loadout.graph.passes[*pass]; let timestamp_writes = profiler.and(|p| wgpu::ComputePassTimestampWrites { query_set: &p.query_set, beginning_of_pass_write_index: Some(pass_index as u32 * 2), end_of_pass_write_index: Some(pass_index as u32 + 1), }); let mut command = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(&pass.id), timestamp_writes, }); command.set_pipeline(pipeline); for (group, bind_group) in bind_groups { command.set_bind_group(*group, bind_group, &[]); } command.dispatch_workgroups( pass.dispatch[0], pass.dispatch[1], pass.dispatch[2], ); } GpuPass::Render { first, last, bundle, .. } => { let pass = &loadout.graph.passes[*first]; let last = &loadout.graph.passes[*last]; let colors = pass .color .iter() .zip(&last.color) .map(|(attachment, final_attachment)| { Ok(Some(wgpu::RenderPassColorAttachment { view: view( &loadout.resources, &surface_view, &attachment.resource, )?, depth_slice: None, resolve_target: None, ops: color_ops(attachment, final_attachment)?, })) }) .collect::, String>>()?; let depth = pass .depth .as_ref() .zip(last.depth.as_ref()) .map(|(attachment, final_attachment)| { Ok::<_, String>(wgpu::RenderPassDepthStencilAttachment { view: view( &loadout.resources, &surface_view, &attachment.resource, )?, depth_ops: Some(depth_ops(attachment, final_attachment)?), stencil_ops: None, }) }) .transpose()?; let timestamp_writes = profiler.and(|p| wgpu::RenderPassTimestampWrites { query_set: &p.query_set, beginning_of_pass_write_index: Some(pass_index as u32 * 2), end_of_pass_write_index: Some(pass_index as u32 * 2 + 1), }); let mut command = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some(&pass.id), color_attachments: &colors, depth_stencil_attachment: depth, timestamp_writes, occlusion_query_set: None, }); command.execute_bundles([bundle]); } } } profiler.as_ref().map(|p| p.encode_query_set(&mut encoder)); self.queue.submit([encoder.finish()]); output.present(); profiler .and_then(|p| p.map_profile(self.queue.get_timestamp_period())) .flatten(); } } fn view<'a>( resources: &'a crate::gpu_resource::GpuResources, surface: &'a wgpu::TextureView, id: &str, ) -> Result<&'a wgpu::TextureView, String> { if id == "canvas" { Ok(surface) } else { resources .texture_view(id) .ok_or_else(|| "GRAPH_ATTACHMENT".into()) } } fn color_ops( attachment: &ColorAttachment, final_attachment: &ColorAttachment, ) -> Result, String> { let clear = attachment.clear.as_slice(); Ok(wgpu::Operations { load: match attachment.load.as_str() { "load" => wgpu::LoadOp::Load, "clear" => wgpu::LoadOp::Clear(wgpu::Color { r: f64::from(clear.first().copied().unwrap_or(0.0)), g: f64::from(clear.get(1).copied().unwrap_or(0.0)), b: f64::from(clear.get(2).copied().unwrap_or(0.0)), a: f64::from(clear.get(3).copied().unwrap_or(1.0)), }), _ => return Err("GRAPH_LOAD_OP".into()), }, store: store_op(&final_attachment.store)?, }) } fn depth_ops( attachment: &DepthAttachment, final_attachment: &DepthAttachment, ) -> Result, String> { Ok(wgpu::Operations { load: match attachment.load.as_str() { "load" => wgpu::LoadOp::Load, "clear" => wgpu::LoadOp::Clear(attachment.clear), _ => return Err("GRAPH_LOAD_OP".into()), }, store: store_op(&final_attachment.store)?, }) } fn store_op(value: &str) -> Result { match value { "store" => Ok(wgpu::StoreOp::Store), "discard" => Ok(wgpu::StoreOp::Discard), _ => Err("GRAPH_STORE_OP".into()), } }