From f1fd4df54022f4af7c15403f6d9f39f0c580c97b Mon Sep 17 00:00:00 2001 From: Akash Shakdwipeea Date: Mon, 24 Aug 2026 00:27:03 +0530 Subject: [PATCH] WIP: cross-platform --- Cargo.lock | 23 ++++ core/Cargo.toml | 2 + core/lib.rs | 35 ++++--- core/render_graph/gpu_resource.rs | 98 ++++------------- core/render_graph/profiler.rs | 127 ++++++++++++++++++++++ core/render_graph/store.rs | 1 - core/renderer/render.rs | 168 ++++++++++++++---------------- 7 files changed, 267 insertions(+), 187 deletions(-) create mode 100644 core/render_graph/profiler.rs diff --git a/Cargo.lock b/Cargo.lock index 98d3a49..a3fe2d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,12 +117,32 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "futures-task" version = "0.3.34" @@ -136,6 +156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -717,6 +738,8 @@ dependencies = [ name = "yawn-core" version = "0.1.0" dependencies = [ + "futures-channel", + "futures-util", "js-sys", "serde", "serde_json", diff --git a/core/Cargo.toml b/core/Cargo.toml index 25ed2d5..06e47e6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,6 +9,8 @@ crate-type = ["cdylib"] path = "lib.rs" [dependencies] +futures-channel = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["async-await-macro", "std"] } js-sys = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/core/lib.rs b/core/lib.rs index 913e1f9..5cf812a 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -34,7 +34,7 @@ struct RowRequest { #[wasm_bindgen] pub struct Core { data: Rc>, - gpu: Rc>>, + gpu: Wgpu, store: Rc>, render: Rc, } @@ -42,25 +42,26 @@ pub struct Core { #[wasm_bindgen] impl Core { #[wasm_bindgen(constructor)] - pub fn new(arena_bytes: u32) -> Result { - Ok(Self { - data: Rc::new(RefCell::new( - RenderData::new(arena_bytes).map_err(JsError::new)?, - )), - gpu: Rc::new(RefCell::new(None)), - store: Rc::new(RefCell::new(Store::default())), - render: Rc::new(RenderLoop::new()), - }) - } - - pub async fn initialize(&self, canvas: web_sys::OffscreenCanvas) -> Result<(), JsError> { + pub async fn new(arena_bytes: u32, canvas: web_sys::OffscreenCanvas) -> Result { let gpu = Wgpu::new(canvas) .await .map_err(|error| JsError::new(&error))?; - *self.gpu.borrow_mut() = Some(gpu); - self.render - .start(self.gpu.clone(), self.store.clone(), self.data.clone()); - Ok(()) + + let render = Rc::new(RenderLoop::new()); + + let data = Rc::new(RefCell::new( + RenderData::new(arena_bytes).map_err(JsError::new)?, + )); + + // what does clone mean here + render.start(&gpu, data.clone()); + + Ok(Self { + data, + gpu, + store: Rc::new(RefCell::new(Store::default())), + render, + }) } pub fn rows(&self) -> String { diff --git a/core/render_graph/gpu_resource.rs b/core/render_graph/gpu_resource.rs index c311ce7..44c160c 100644 --- a/core/render_graph/gpu_resource.rs +++ b/core/render_graph/gpu_resource.rs @@ -7,9 +7,11 @@ use serde::de::DeserializeOwned; use serde_json::Value; use crate::gpu::Wgpu; -use crate::graph::{Binding, Execution, Extent, Pass, RenderGraph, RenderPipeline, Texture}; +use crate::graph::{self, Binding, Execution, Extent, Pass, RenderGraph, RenderPipeline, Texture}; use crate::render_data::RenderData; +pub mod profiler; + pub struct GpuResources { pub buffers: HashMap, pub textures: Vec, @@ -18,21 +20,7 @@ pub struct GpuResources { pub render_pipelines: HashMap, pub compute_pipelines: HashMap, pub passes: Vec, - pub profiler: Option, -} - -pub struct GpuProfiler { - query_set: wgpu::QuerySet, - resolve: wgpu::Buffer, - readback: Arc, - query_count: u32, -} - -pub struct ProfileMap { - readback: Arc, - state: Arc, - labels: Vec, - timestamp_period: f32, + pub profiler: Option, } pub struct GpuBuffer { @@ -175,30 +163,6 @@ impl GpuResources { }) .collect::, _>>()?; - let profiler = (gpu.timestamp_queries && !graph.executions.is_empty()).then(|| { - let query_count = graph.executions.len() as u32 * 2; - let bytes = u64::from(query_count) * 8; - GpuProfiler { - query_set: gpu.device.create_query_set(&wgpu::QuerySetDescriptor { - label: Some("frame-profile"), - ty: wgpu::QueryType::Timestamp, - count: query_count, - }), - resolve: gpu.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("frame-profile-resolve"), - size: bytes, - usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC, - mapped_at_creation: false, - }), - readback: Arc::new(gpu.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("frame-profile-readback"), - size: bytes, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - })), - query_count, - } - }); let mut resources = Self { buffers, textures, @@ -207,7 +171,7 @@ impl GpuResources { render_pipelines, compute_pipelines, passes: Vec::new(), - profiler, + profiler: None, }; for execution in &graph.executions { let compiled = match execution { @@ -256,6 +220,17 @@ impl GpuResources { }; resources.passes.push(compiled); } + + let pass_labels = resources + .passes + .iter() + .map(|pass| pass.label().to_owned()) + .collect::>(); + + resources.profiler = (gpu.timestamp_queries && !graph.executions.is_empty()).then(|| { + profiler::Profiler::new(&gpu.device, graph.executions.len() as u32, pass_labels) + }); + Ok(resources) } @@ -308,45 +283,6 @@ impl GpuResources { .is_some_and(|texture| !texture.uploaded_mips.contains(&mip_level)) } - pub fn render_timestamps(&self, pass: usize) -> Option> { - self.profiler - .as_ref() - .map(|profiler| wgpu::RenderPassTimestampWrites { - query_set: &profiler.query_set, - beginning_of_pass_write_index: Some(pass as u32 * 2), - end_of_pass_write_index: Some(pass as u32 * 2 + 1), - }) - } - - pub fn compute_timestamps(&self, pass: usize) -> Option> { - self.profiler - .as_ref() - .map(|profiler| wgpu::ComputePassTimestampWrites { - query_set: &profiler.query_set, - beginning_of_pass_write_index: Some(pass as u32 * 2), - end_of_pass_write_index: Some(pass as u32 * 2 + 1), - }) - } - - pub fn resolve_profile(&self, encoder: &mut wgpu::CommandEncoder) { - let Some(profiler) = &self.profiler else { - return; - }; - encoder.resolve_query_set( - &profiler.query_set, - 0..profiler.query_count, - &profiler.resolve, - 0, - ); - encoder.copy_buffer_to_buffer( - &profiler.resolve, - 0, - &profiler.readback, - 0, - u64::from(profiler.query_count) * 8, - ); - } - pub fn map_profile(&self, timestamp_period: f32) -> Option { let profiler = self.profiler.as_ref()?; let state = Arc::new(AtomicU8::new(0)); @@ -448,11 +384,13 @@ impl GpuResources { .render_pipelines .get(&pass.pipeline) .ok_or("GRAPH_PIPELINE")?; + let pipeline_changed = previous_pipeline != Some(pass.pipeline.as_str()); if pipeline_changed { encoder.set_pipeline(pipeline); previous_pipeline = Some(pass.pipeline.as_str()); } + if pipeline_changed || previous_bindings != Some(pass.bindings.as_slice()) { for (group, bind_group) in self.bind_groups(pass, |group| pipeline.get_bind_group_layout(group), gpu)? diff --git a/core/render_graph/profiler.rs b/core/render_graph/profiler.rs new file mode 100644 index 0000000..a7c561d --- /dev/null +++ b/core/render_graph/profiler.rs @@ -0,0 +1,127 @@ +use serde::Serialize; + +pub struct Profiler { + pub query_set: wgpu::QuerySet, + pub resolve: wgpu::Buffer, + pub readback: wgpu::Buffer, + pub query_count: u32, + pub pass_labels: Vec, +} + +impl Profiler { + pub fn new(device: &wgpu::Device, render_pass_count: u32, pass_labels: Vec) -> Self { + let query_count = render_pass_count * 2; + let size = u64::from(query_count) * 8; + + Self { + query_set: device.create_query_set(&wgpu::QuerySetDescriptor { + label: Some("frame-profile"), + ty: wgpu::QueryType::Timestamp, + count: query_count, + }), + resolve: device.create_buffer(&wgpu::BufferDescriptor { + label: Some("frame-profile-resolve"), + size, + usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC, + mapped_at_creation: false, + }), + readback: device.create_buffer(&wgpu::BufferDescriptor { + label: Some("frame-profile-readback"), + size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }), + query_count, + pass_labels, + } + } + + pub fn encode_query_set(&self, encoder: &mut wgpu::CommandEncoder) { + encoder.resolve_query_set(&self.query_set, 0..self.query_count, &self.resolve, 0); + encoder.copy_buffer_to_buffer( + &self.resolve, + 0, + &self.readback, + 0, + u64::from(self.query_count) * 8, + ); + } + + pub fn read_passes(self) -> Vec { + let bytes = self.readback.get_mapped_range(..); + let values = bytes + .chunks_exact(8) + .map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap())) + .collect::>(); + + let profile = self + .pass_labels + .into_iter() + .zip(values.chunks_exact(2)) + .map(|(label, timestamps)| Pass { + name: label, + milliseconds: timestamps[1].saturating_sub(timestamps[0]) as f64 + * f64::from(self.timestamp_period) + / 1_000_000.0, + }) + .collect(); + + drop(bytes); + self.readback.unmap(); + + profile + } + + pub async fn read_render_pass_data(&self) -> Result, wgpu::BufferMapError> { + self.readback + .map_async(wgpu::MapMode::Read, .., |result| { + result.and(self.read_passes()); + }) + .await?; + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FrameData { + pub frame: u32, + pub milliseconds: f64, + pub readback_milliseconds: f64, + pub adapter: String, + pub canvas: Canvas, + pub passes: Vec, +} + +impl std::fmt::Display for FrameData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .fmt(f) + } +} + +#[derive(Serialize)] +pub struct Canvas { + pub width: u32, + pub height: u32, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Pass { + pub name: String, + pub milliseconds: f64, +} + +pub enum ProfileState { + Detached, + Attached, + Ready(FrameData), +} + +// pub struct ProfileMap { +// readback: Arc, +// state: Arc, +// labels: Vec, +// timestamp_period: f32, +// } diff --git a/core/render_graph/store.rs b/core/render_graph/store.rs index 50419c9..7374c85 100644 --- a/core/render_graph/store.rs +++ b/core/render_graph/store.rs @@ -14,7 +14,6 @@ pub struct Loadout { pub struct Store { graphs: HashMap, texture_sources: HashMap>, - active: Option, } impl Store { diff --git a/core/renderer/render.rs b/core/renderer/render.rs index bb4a683..9d1cda1 100644 --- a/core/renderer/render.rs +++ b/core/renderer/render.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use wasm_bindgen::prelude::wasm_bindgen; use crate::gpu::Wgpu; -use crate::gpu_resource::{GpuPass, ProfileMap}; +use crate::gpu_resource::{profiler, GpuPass, ProfileMap}; use crate::graph::{ColorAttachment, DepthAttachment}; use crate::render_data::RenderData; use crate::store::{Loadout, Store}; @@ -39,10 +39,8 @@ pub struct RenderLoop { frame: Cell, elapsed: Cell, last: Cell, - profiling: Cell, - profile_pending: Cell, - profile_after: Cell, - profile: RefCell>, + active_loadout: Loadout, + profile_state: RefCell, } impl RenderLoop { @@ -54,10 +52,8 @@ impl RenderLoop { frame: Cell::new(0), elapsed: Cell::new(0.0), last: Cell::new(js_sys::Date::now()), - profiling: Cell::new(false), - profile_pending: Cell::new(false), - profile_after: Cell::new(0.0), - profile: RefCell::new(None), + active_loadout: todo!(), + profile_state: RefCell::new(profiler::ProfileState::Detached), } } @@ -78,24 +74,19 @@ impl RenderLoop { Ok(()) } - pub fn set_profiling(&self, enabled: bool) { - self.profiling.set(enabled); - self.profile_after.set(0.0); - if !enabled { - self.profile.borrow_mut().take(); - } + pub fn set_profiling(&mut self) { + self.profile_state = RefCell::new(profiler::ProfileState::Attached); } pub fn take_profile(&self) -> Option { - self.profile.borrow_mut().take() + if let profiler::ProfileState::Ready(data) = self.profile_state.get_mut().take() { + Some(data) + } else { + None + } } - pub fn start( - self: &Rc, - gpu: Rc>>, - store: Rc>, - data: Rc>, - ) { + pub fn start(self: &Rc, gpu: &Wgpu, data: Rc>) { if self.started.replace(true) { return; } @@ -106,6 +97,7 @@ impl RenderLoop { 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) { @@ -120,36 +112,25 @@ impl RenderLoop { let mut data = data.borrow_mut(); data.update_signals(delta as f32, frame, elapsed as f32, control.fps.get()); } - let submission = if data.borrow().render_pending() { - if let (Some(gpu), Some(loadout)) = - (gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut()) - { - if data.borrow_mut().begin_render() { - let profile = control.profiling.get() - && !control.profile_pending.get() - && started >= control.profile_after.get(); - let result = { - let data = data.borrow(); - gpu.render(loadout, &data, profile) - }; - match result { - Ok(profile) => profile.map(|profile| { - (profile, gpu.adapter.clone(), gpu.width, gpu.height) - }), - Err(_) => { - data.borrow_mut().mark_dirty(); - None - } - } - } else { - None - } - } else { - None - } - } else { - None - }; + + // 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); @@ -162,27 +143,20 @@ impl RenderLoop { let passes = profile .read() .into_iter() - .map(|(name, milliseconds)| { - serde_json::json!({ - "name": name, - "milliseconds": milliseconds, - }) - }) + .map(|(name, milliseconds)| profiler::Pass { name, milliseconds }) .collect::>(); - let milliseconds = passes - .iter() - .filter_map(|pass| pass["milliseconds"].as_f64()) - .sum::(); + let milliseconds = + passes.iter().map(|pass| pass.milliseconds).sum::(); if control.profiling.get() { - *control.profile.borrow_mut() = Some( - serde_json::json!({ - "frame": frame, - "milliseconds": milliseconds, - "readbackMilliseconds": js_sys::Date::now() - started, - "adapter": adapter, - "canvas": { "width": width, "height": height }, - "passes": passes, - }) + *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(), ); } @@ -201,7 +175,7 @@ impl Wgpu { loadout: &mut Loadout, data: &RenderData, profile: bool, - ) -> Result, String> { + ) -> Result { for buffer in loadout.resources.buffers.values() { if !buffer.sync_each_frame { continue; @@ -212,6 +186,7 @@ impl Wgpu { 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) => { @@ -221,9 +196,21 @@ impl Wgpu { 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 { @@ -238,9 +225,12 @@ impl Wgpu { .. } => { let pass = &loadout.graph.passes[*pass]; - let timestamp_writes = profile - .then(|| loadout.resources.compute_timestamps(pass_index)) - .flatten(); + 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, @@ -296,9 +286,13 @@ impl Wgpu { }) }) .transpose()?; - let timestamp_writes = profile - .then(|| loadout.resources.render_timestamps(pass_index)) - .flatten(); + + 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, @@ -310,19 +304,15 @@ impl Wgpu { } } } - if profile { - loadout.resources.resolve_profile(&mut encoder); - } + + profiler.as_ref().map(|p| p.encode_query_set(&mut encoder)); self.queue.submit([encoder.finish()]); - let profile = profile - .then(|| { - loadout - .resources - .map_profile(self.queue.get_timestamp_period()) - }) - .flatten(); + output.present(); - Ok(profile) + + profiler + .and_then(|p| p.map_profile(self.queue.get_timestamp_period())) + .flatten(); } }