diff --git a/Cargo.lock b/Cargo.lock index d9bf8e4..98d3a49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,15 +117,6 @@ 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" @@ -150,18 +141,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "half" version = "2.7.1" @@ -738,7 +717,6 @@ dependencies = [ name = "yawn-core" version = "0.1.0" dependencies = [ - "gloo-timers", "js-sys", "serde", "serde_json", diff --git a/addons/handles/src/Scene.ts b/addons/handles/src/Scene.ts index efbccbd..9c3c913 100644 --- a/addons/handles/src/Scene.ts +++ b/addons/handles/src/Scene.ts @@ -353,10 +353,10 @@ export class Scene { arenaBytes: options.arenaBytes, debug: options.debug, }); - this.ready = this.#initialize(options.fps ?? 60); + this.ready = this.#initialize(options.fps); } - async #initialize(fps: number) { + async #initialize(fps?: number) { await this.core.ready; for (const [name, stride, format] of rows) await this.core.createRows({ name, rows: 1, stride, format }); @@ -373,7 +373,7 @@ export class Scene { this.core .array("materials") .write(material, [1, 1, 1, 1, 0, 0.7, 0, 0, 0, 0, 1, 0.5]); - await this.core.setFps(fps); + if (fps !== undefined) await this.core.setFps(fps); await this.#compileRenderGraph(); return this; } diff --git a/core/Cargo.toml b/core/Cargo.toml index e073b10..25ed2d5 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,7 +9,6 @@ crate-type = ["cdylib"] path = "lib.rs" [dependencies] -gloo-timers = { version = "0.3", features = ["futures"] } js-sys = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/core/renderer/render.rs b/core/renderer/render.rs index 72c5013..ce09fd3 100644 --- a/core/renderer/render.rs +++ b/core/renderer/render.rs @@ -1,9 +1,7 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use gloo_timers::future::TimeoutFuture; +use wasm_bindgen::prelude::wasm_bindgen; use crate::gpu::Wgpu; use crate::gpu_resource::{GpuPass, ProfileMap}; @@ -11,6 +9,29 @@ 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, @@ -19,24 +40,23 @@ pub struct RenderLoop { elapsed: Cell, last: Cell, profiling: Cell, + profile_pending: Cell, + profile_after: Cell, profile: RefCell>, } -struct Submission { - completed: Arc, - profile: Option, -} - impl RenderLoop { pub fn new() -> Self { Self { playing: Cell::new(true), - fps: Cell::new(60), + fps: Cell::new(0), started: Cell::new(false), 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), } } @@ -51,7 +71,7 @@ impl RenderLoop { } pub fn set_fps(&self, fps: u32) -> Result<(), &'static str> { - if !(1..=1000).contains(&fps) { + if fps > 1000 { return Err("FPS"); } self.fps.set(fps); @@ -60,6 +80,7 @@ impl RenderLoop { pub fn set_profiling(&self, enabled: bool) { self.profiling.set(enabled); + self.profile_after.set(0.0); if !enabled { self.profile.borrow_mut().take(); } @@ -81,75 +102,82 @@ impl RenderLoop { 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(); - if control.playing.get() { - 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 skip = { - let mut data = data.borrow_mut(); - data.update_info(delta as f32, frame, elapsed as f32, control.fps.get()); - data.skip_render() - }; - let submission = if !skip { - if let (Some(gpu), Some(loadout)) = - (gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut()) - { - gpu.render(loadout, &data.borrow(), control.profiling.get()) - .ok() - .flatten() - } else { - None - } + 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 skip = { + let mut data = data.borrow_mut(); + data.update_info(delta as f32, frame, elapsed as f32, control.fps.get()); + data.skip_render() + }; + let submission = if !skip { + if let (Some(gpu), Some(loadout)) = + (gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut()) + { + let profile = control.profiling.get() + && !control.profile_pending.get() + && started >= control.profile_after.get(); + gpu.render(loadout, &data.borrow(), profile) + .ok() + .flatten() + .map(|profile| (profile, gpu.adapter.clone(), gpu.width, gpu.height)) } else { None - }; - if let Some(submission) = submission { - while !submission.completed.load(Ordering::Acquire) { - TimeoutFuture::new(0).await; + } + } else { + None + }; + 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; } - let wall_milliseconds = js_sys::Date::now() - started; - if let Some(profile) = submission.profile { - while profile.state() == 0 { - TimeoutFuture::new(0).await; - } - if profile.state() == 1 { - let passes = profile - .read() - .into_iter() - .map(|(name, milliseconds)| { - serde_json::json!({ - "name": name, - "milliseconds": milliseconds, - }) + if profile.state() == 1 { + let passes = profile + .read() + .into_iter() + .map(|(name, milliseconds)| { + serde_json::json!({ + "name": name, + "milliseconds": milliseconds, }) - .collect::>(); - let milliseconds = passes - .iter() - .filter_map(|pass| pass["milliseconds"].as_f64()) - .sum::(); - let gpu = gpu.borrow(); - let gpu = gpu.as_ref().unwrap(); + }) + .collect::>(); + let milliseconds = passes + .iter() + .filter_map(|pass| pass["milliseconds"].as_f64()) + .sum::(); + if control.profiling.get() { *control.profile.borrow_mut() = Some( serde_json::json!({ "frame": frame, "milliseconds": milliseconds, - "wallMilliseconds": wall_milliseconds, - "adapter": gpu.adapter, - "canvas": { "width": gpu.width, "height": gpu.height }, + "readbackMilliseconds": js_sys::Date::now() - started, + "adapter": adapter, + "canvas": { "width": width, "height": height }, "passes": passes, }) .to_string(), ); } } - } + control.profile_pending.set(false); + }); } - let target = 1000.0 / f64::from(control.fps.get()); - let wait = (target - (js_sys::Date::now() - started)).max(0.0) as u32; - TimeoutFuture::new(if control.playing.get() { wait } else { 50 }).await; } }); } @@ -161,7 +189,7 @@ impl Wgpu { loadout: &mut Loadout, data: &RenderData, profile: bool, - ) -> Result, String> { + ) -> Result, String> { for buffer in loadout.resources.buffers.values() { if !buffer.sync_each_frame { continue; @@ -282,11 +310,7 @@ impl Wgpu { }) .flatten(); output.present(); - let completed = Arc::new(AtomicBool::new(false)); - let callback = completed.clone(); - self.queue - .on_submitted_work_done(move || callback.store(true, Ordering::Release)); - Ok(Some(Submission { completed, profile })) + Ok(profile) } } diff --git a/docs/.vitepress/Playground.vue b/docs/.vitepress/Playground.vue index 6fb9a20..f6420b7 100644 --- a/docs/.vitepress/Playground.vue +++ b/docs/.vitepress/Playground.vue @@ -292,7 +292,7 @@ onUnmounted(() => { {{ adapterInfo || profile.adapter }} {{ profile.canvas.width }}×{{ profile.canvas.height }} · - {{ profile.wallMilliseconds.toFixed(2) }} ms wall + {{ profile.readbackMilliseconds.toFixed(2) }} ms readback latency

diff --git a/docs/.vitepress/playgrounds.js b/docs/.vitepress/playgrounds.js index 6fcd92d..5b2894d 100644 --- a/docs/.vitepress/playgrounds.js +++ b/docs/.vitepress/playgrounds.js @@ -402,7 +402,7 @@ for (let y = 0; y < 64; y++) { } } -const scene = new Scene(canvas, { fps: 1000, hdr: true }); +const scene = new Scene(canvas, { hdr: true }); await scene.ready; log("Benchmark core ready; preparing geometry…"); const camera = new ArcRotateCamera(scene, { diff --git a/docs/guide/core.md b/docs/guide/core.md index 80a1dd2..1eb589f 100644 --- a/docs/guide/core.md +++ b/docs/guide/core.md @@ -56,7 +56,7 @@ await core.setProfiler(false); stop(); ``` -`new YawnCore(canvas, { debug: true })` enables the same timestamp-query mode at startup. Timings describe physical GPU passes and actual compiled draw counts; compatible indexed draws over consecutive instances collapse into one command. The sidebar also reports canvas size and wall-clock completion time. +`new YawnCore(canvas, { debug: true })` enables the same timestamp-query mode at startup. Samples are read back asynchronously, so profiling does not serialize the GPU queue. Timings describe physical GPU passes and actual compiled draw counts; compatible indexed draws over consecutive instances collapse into one command. The sidebar also reports canvas size and wall-clock readback latency. The saved **Forward benchmark** playground renders 138 logical objects and 4.5 million triangles. Add `&grid=32` to lower geometry density or `&overdraw=1` to stack the objects while diagnosing depth and fragment cost. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 8df1ae7..398be77 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -24,7 +24,7 @@ const scene = new Scene(canvas, { hdr: true, fps: 60 }); await scene.ready; ``` -`Scene` initializes conventional SOA rows and loads one clustered-forward HDR render graph. The core itself still starts with only its eight-float `info` row. +Omit `fps` to render as fast as the browser and GPU allow. `Scene` initializes conventional SOA rows and loads one clustered-forward HDR render graph. The core itself still starts with only its eight-float `info` row. ## 3. Add a triangle