Fix render pacing and profiler readback

Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-20 15:42:31 +00:00
co-authored by heaust
parent abfa428464
commit 481d105f19
8 changed files with 100 additions and 99 deletions
Generated
-22
View File
@@ -117,15 +117,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 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]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.34" version = "0.3.34"
@@ -150,18 +141,6 @@ dependencies = [
"slab", "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]] [[package]]
name = "half" name = "half"
version = "2.7.1" version = "2.7.1"
@@ -738,7 +717,6 @@ dependencies = [
name = "yawn-core" name = "yawn-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gloo-timers",
"js-sys", "js-sys",
"serde", "serde",
"serde_json", "serde_json",
+3 -3
View File
@@ -353,10 +353,10 @@ export class Scene {
arenaBytes: options.arenaBytes, arenaBytes: options.arenaBytes,
debug: options.debug, 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; await this.core.ready;
for (const [name, stride, format] of rows) for (const [name, stride, format] of rows)
await this.core.createRows({ name, rows: 1, stride, format }); await this.core.createRows({ name, rows: 1, stride, format });
@@ -373,7 +373,7 @@ export class Scene {
this.core this.core
.array("materials") .array("materials")
.write(material, [1, 1, 1, 1, 0, 0.7, 0, 0, 0, 0, 1, 0.5]); .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(); await this.#compileRenderGraph();
return this; return this;
} }
-1
View File
@@ -9,7 +9,6 @@ crate-type = ["cdylib"]
path = "lib.rs" path = "lib.rs"
[dependencies] [dependencies]
gloo-timers = { version = "0.3", features = ["futures"] }
js-sys = "0.3" js-sys = "0.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
+58 -34
View File
@@ -1,9 +1,7 @@
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::rc::Rc; 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::Wgpu;
use crate::gpu_resource::{GpuPass, ProfileMap}; use crate::gpu_resource::{GpuPass, ProfileMap};
@@ -11,6 +9,29 @@ use crate::graph::{ColorAttachment, DepthAttachment};
use crate::render_data::RenderData; use crate::render_data::RenderData;
use crate::store::{Loadout, Store}; 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 { pub struct RenderLoop {
playing: Cell<bool>, playing: Cell<bool>,
fps: Cell<u32>, fps: Cell<u32>,
@@ -19,24 +40,23 @@ pub struct RenderLoop {
elapsed: Cell<f64>, elapsed: Cell<f64>,
last: Cell<f64>, last: Cell<f64>,
profiling: Cell<bool>, profiling: Cell<bool>,
profile_pending: Cell<bool>,
profile_after: Cell<f64>,
profile: RefCell<Option<String>>, profile: RefCell<Option<String>>,
} }
struct Submission {
completed: Arc<AtomicBool>,
profile: Option<ProfileMap>,
}
impl RenderLoop { impl RenderLoop {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
playing: Cell::new(true), playing: Cell::new(true),
fps: Cell::new(60), fps: Cell::new(0),
started: Cell::new(false), started: Cell::new(false),
frame: Cell::new(0), frame: Cell::new(0),
elapsed: Cell::new(0.0), elapsed: Cell::new(0.0),
last: Cell::new(js_sys::Date::now()), last: Cell::new(js_sys::Date::now()),
profiling: Cell::new(false), profiling: Cell::new(false),
profile_pending: Cell::new(false),
profile_after: Cell::new(0.0),
profile: RefCell::new(None), profile: RefCell::new(None),
} }
} }
@@ -51,7 +71,7 @@ impl RenderLoop {
} }
pub fn set_fps(&self, fps: u32) -> Result<(), &'static str> { pub fn set_fps(&self, fps: u32) -> Result<(), &'static str> {
if !(1..=1000).contains(&fps) { if fps > 1000 {
return Err("FPS"); return Err("FPS");
} }
self.fps.set(fps); self.fps.set(fps);
@@ -60,6 +80,7 @@ impl RenderLoop {
pub fn set_profiling(&self, enabled: bool) { pub fn set_profiling(&self, enabled: bool) {
self.profiling.set(enabled); self.profiling.set(enabled);
self.profile_after.set(0.0);
if !enabled { if !enabled {
self.profile.borrow_mut().take(); self.profile.borrow_mut().take();
} }
@@ -81,8 +102,15 @@ impl RenderLoop {
let control = self.clone(); let control = self.clone();
wasm_bindgen_futures::spawn_local(async move { wasm_bindgen_futures::spawn_local(async move {
loop { loop {
next_frame().await;
if !control.playing.get() {
continue;
}
let started = js_sys::Date::now(); let started = js_sys::Date::now();
if control.playing.get() { 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 delta = (started - control.last.replace(started)) / 1000.0;
let elapsed = control.elapsed.get() + delta; let elapsed = control.elapsed.get() + delta;
control.elapsed.set(elapsed); control.elapsed.set(elapsed);
@@ -97,23 +125,26 @@ impl RenderLoop {
if let (Some(gpu), Some(loadout)) = if let (Some(gpu), Some(loadout)) =
(gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut()) (gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut())
{ {
gpu.render(loadout, &data.borrow(), control.profiling.get()) let profile = control.profiling.get()
&& !control.profile_pending.get()
&& started >= control.profile_after.get();
gpu.render(loadout, &data.borrow(), profile)
.ok() .ok()
.flatten() .flatten()
.map(|profile| (profile, gpu.adapter.clone(), gpu.width, gpu.height))
} else { } else {
None None
} }
} else { } else {
None None
}; };
if let Some(submission) = submission { if let Some((profile, adapter, width, height)) = submission {
while !submission.completed.load(Ordering::Acquire) { control.profile_pending.set(true);
TimeoutFuture::new(0).await; control.profile_after.set(started + 250.0);
} let control = control.clone();
let wall_milliseconds = js_sys::Date::now() - started; wasm_bindgen_futures::spawn_local(async move {
if let Some(profile) = submission.profile {
while profile.state() == 0 { while profile.state() == 0 {
TimeoutFuture::new(0).await; yield_task().await;
} }
if profile.state() == 1 { if profile.state() == 1 {
let passes = profile let passes = profile
@@ -130,27 +161,24 @@ impl RenderLoop {
.iter() .iter()
.filter_map(|pass| pass["milliseconds"].as_f64()) .filter_map(|pass| pass["milliseconds"].as_f64())
.sum::<f64>(); .sum::<f64>();
let gpu = gpu.borrow(); if control.profiling.get() {
let gpu = gpu.as_ref().unwrap();
*control.profile.borrow_mut() = Some( *control.profile.borrow_mut() = Some(
serde_json::json!({ serde_json::json!({
"frame": frame, "frame": frame,
"milliseconds": milliseconds, "milliseconds": milliseconds,
"wallMilliseconds": wall_milliseconds, "readbackMilliseconds": js_sys::Date::now() - started,
"adapter": gpu.adapter, "adapter": adapter,
"canvas": { "width": gpu.width, "height": gpu.height }, "canvas": { "width": width, "height": height },
"passes": passes, "passes": passes,
}) })
.to_string(), .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, loadout: &mut Loadout,
data: &RenderData, data: &RenderData,
profile: bool, profile: bool,
) -> Result<Option<Submission>, String> { ) -> Result<Option<ProfileMap>, String> {
for buffer in loadout.resources.buffers.values() { for buffer in loadout.resources.buffers.values() {
if !buffer.sync_each_frame { if !buffer.sync_each_frame {
continue; continue;
@@ -282,11 +310,7 @@ impl Wgpu {
}) })
.flatten(); .flatten();
output.present(); output.present();
let completed = Arc::new(AtomicBool::new(false)); Ok(profile)
let callback = completed.clone();
self.queue
.on_submitted_work_done(move || callback.store(true, Ordering::Release));
Ok(Some(Submission { completed, profile }))
} }
} }
+1 -1
View File
@@ -292,7 +292,7 @@ onUnmounted(() => {
<span>{{ adapterInfo || profile.adapter }}</span> <span>{{ adapterInfo || profile.adapter }}</span>
<span> <span>
{{ profile.canvas.width }}×{{ profile.canvas.height }} · {{ profile.canvas.width }}×{{ profile.canvas.height }} ·
{{ profile.wallMilliseconds.toFixed(2) }} ms wall {{ profile.readbackMilliseconds.toFixed(2) }} ms readback latency
</span> </span>
</div> </div>
<p v-if="profilerSupported === false"> <p v-if="profilerSupported === false">
+1 -1
View File
@@ -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; await scene.ready;
log("Benchmark core ready; preparing geometry…"); log("Benchmark core ready; preparing geometry…");
const camera = new ArcRotateCamera(scene, { const camera = new ArcRotateCamera(scene, {
+1 -1
View File
@@ -56,7 +56,7 @@ await core.setProfiler(false);
stop(); 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. 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.
+1 -1
View File
@@ -24,7 +24,7 @@ const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready; 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 ## 3. Add a triangle