use std::sync::atomic::{AtomicU64, Ordering};
use super::accumulate::CHUNK_TIMING_EMA_ALPHA;
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::Instant;
#[cfg(target_arch = "wasm32")]
pub use web_time::Instant;
pub const MIN_CHUNK_DEADLINE_MS: f64 = 2000.0;
pub const CHUNK_DEADLINE_EMA_FACTOR: f64 = 20.0;
pub const FIRST_CHUNK_DEADLINE_MS: f64 = 10_000.0;
#[must_use]
pub fn chunk_deadline_ms(ema_ns_per_tuple: Option<f64>, chunk_rays: usize) -> f64 {
ema_ns_per_tuple
.filter(|v| v.is_finite() && *v > 0.0)
.map_or(FIRST_CHUNK_DEADLINE_MS, |ns| {
(ns * chunk_rays as f64 * 1.0e-6 * CHUNK_DEADLINE_EMA_FACTOR).max(MIN_CHUNK_DEADLINE_MS)
})
}
#[must_use]
pub fn blend_ema(previous: Option<f64>, ns_per_tuple: f64) -> f64 {
previous.map_or(ns_per_tuple, |ema| {
CHUNK_TIMING_EMA_ALPHA.mul_add(ns_per_tuple, (1.0 - CHUNK_TIMING_EMA_ALPHA) * ema)
})
}
static READBACK_DEADLINE_MS_BITS: AtomicU64 = AtomicU64::new(FIRST_CHUNK_DEADLINE_MS.to_bits());
pub fn set_readback_deadline_ms(ms: f64) {
READBACK_DEADLINE_MS_BITS.store(ms.to_bits(), Ordering::Relaxed);
}
#[must_use]
pub fn readback_deadline_ms() -> f64 {
f64::from_bits(READBACK_DEADLINE_MS_BITS.load(Ordering::Relaxed))
}
pub enum Raced<T> {
Done(T),
TimedOut,
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn race_deadline<F: std::future::Future>(fut: F, _deadline_ms: f64) -> Raced<F::Output> {
Raced::Done(fut.await)
}
#[cfg(target_arch = "wasm32")]
#[allow(
clippy::future_not_send,
reason = "wgpu web backend futures are !Send; wasm32 is single-threaded"
)]
pub async fn race_deadline<F: std::future::Future>(fut: F, deadline_ms: f64) -> Raced<F::Output> {
use std::{future::Future, pin::Pin, task::Poll};
let (tx, mut rx) = futures_channel::oneshot::channel::<()>();
let Some(timer_id) = schedule_timeout(deadline_ms, tx) else {
return Raced::Done(fut.await);
};
let mut fut = std::pin::pin!(fut);
let out = std::future::poll_fn(|cx| {
if let Poll::Ready(value) = fut.as_mut().poll(cx) {
return Poll::Ready(Raced::Done(value));
}
if Pin::new(&mut rx).poll(cx).is_ready() {
return Poll::Ready(Raced::TimedOut);
}
Poll::Pending
})
.await;
clear_timeout(&timer_id);
out
}
#[cfg(target_arch = "wasm32")]
fn schedule_timeout(
deadline_ms: f64,
tx: futures_channel::oneshot::Sender<()>,
) -> Option<wasm_bindgen::JsValue> {
use wasm_bindgen::{JsCast, JsValue, closure::Closure};
let global = js_sys::global();
let set_timeout = js_sys::Reflect::get(&global, &JsValue::from_str("setTimeout"))
.ok()?
.dyn_into::<js_sys::Function>()
.ok()?;
let callback = Closure::once_into_js(move || {
let _ = tx.send(());
});
set_timeout
.call2(&global, &callback, &JsValue::from_f64(deadline_ms.max(1.0)))
.ok()
}
#[cfg(target_arch = "wasm32")]
fn clear_timeout(timer_id: &wasm_bindgen::JsValue) {
use wasm_bindgen::{JsCast, JsValue};
let global = js_sys::global();
if let Some(clear) = js_sys::Reflect::get(&global, &JsValue::from_str("clearTimeout"))
.ok()
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
{
let _ = clear.call1(&global, timer_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deadline_floors_at_two_seconds_and_scales_with_ema() {
assert!((chunk_deadline_ms(None, 1000) - FIRST_CHUNK_DEADLINE_MS).abs() < 1e-9);
assert!((chunk_deadline_ms(Some(100.0), 1000) - MIN_CHUNK_DEADLINE_MS).abs() < 1e-9);
let big = chunk_deadline_ms(Some(1000.0), 1_000_000);
assert!((big - 20_000.0).abs() < 1e-6, "got {big}");
assert!((chunk_deadline_ms(Some(f64::NAN), 10) - FIRST_CHUNK_DEADLINE_MS).abs() < 1e-9);
assert!((chunk_deadline_ms(Some(-1.0), 10) - FIRST_CHUNK_DEADLINE_MS).abs() < 1e-9);
}
#[test]
fn ema_blend_seeds_then_smooths() {
assert!((blend_ema(None, 50.0) - 50.0).abs() < 1e-12);
let next = blend_ema(Some(100.0), 200.0);
let expected =
CHUNK_TIMING_EMA_ALPHA.mul_add(200.0, (1.0 - CHUNK_TIMING_EMA_ALPHA) * 100.0);
assert!((next - expected).abs() < 1e-12);
}
#[test]
fn readback_deadline_round_trips() {
let before = readback_deadline_ms();
set_readback_deadline_ms(1234.5);
assert!((readback_deadline_ms() - 1234.5).abs() < 1e-12);
set_readback_deadline_ms(before);
}
}