use super::{
capability::{REMOTE_MIN_SPP, RemoteCapability},
dispatch::run_remote_batch,
};
use glam::Vec3;
use indicatrix_net::{SceneState, client::Accumulator};
use std::{
sync::{Arc, Mutex, PoisonError, atomic::AtomicBool},
time::{Duration, Instant},
};
pub(in crate::bridge::export_thread) const REMOTE_CALIBRATION_SAMPLES: u32 = 8;
const REMOTE_CHUNK_TARGET_SECS: f64 = 22.0;
const REMOTE_CHUNK_MIN_SPP: u32 = REMOTE_MIN_SPP;
#[must_use]
pub(in crate::bridge::export_thread) fn remote_chunk_samples(rate_samples_per_sec: f64) -> u32 {
let target = rate_samples_per_sec * REMOTE_CHUNK_TARGET_SECS;
if target.is_finite() && target >= f64::from(REMOTE_CHUNK_MIN_SPP) {
target.round() as u32
} else {
REMOTE_CHUNK_MIN_SPP
}
}
#[must_use]
pub(in crate::bridge::export_thread) const fn shortfall(assigned: u32, completed: u32) -> u32 {
assigned.saturating_sub(completed)
}
#[must_use]
pub(in crate::bridge::export_thread) fn remote_marginal_rate(
delta_samples: u32,
elapsed: Duration,
) -> Option<f64> {
if delta_samples == 0 {
return None;
}
let secs = elapsed.as_secs_f64();
if secs <= 0.0 {
return None;
}
Some(f64::from(delta_samples) / secs)
}
#[derive(Debug)]
pub(in crate::bridge::export_thread) enum RemoteCalibration {
Ready(f64),
Cancelled,
Failed(String),
Short { done: u32, expected: u32 },
}
pub(in crate::bridge::export_thread) fn calibrate_remote_rate(
capability: &RemoteCapability,
scene_state: &SceneState,
width: u32,
height: u32,
samples_done: &mut u32,
accum: &mut [Vec3],
cancel: &AtomicBool,
) -> RemoteCalibration {
let probe = REMOTE_CALIBRATION_SAMPLES;
let remote_accumulator = Arc::new(Mutex::new(Accumulator::new(width, height)));
let start_sample = *samples_done;
let timer = Instant::now();
let (done, cancelled, error, measured_rate) = run_remote_batch(
capability,
scene_state.clone(),
start_sample,
probe,
width,
height,
&remote_accumulator,
cancel,
);
let elapsed = timer.elapsed().as_secs_f64().max(1e-9);
{
let acc = remote_accumulator
.lock()
.unwrap_or_else(PoisonError::into_inner);
for (dst, src) in accum.iter_mut().zip(acc.buffer()) {
*dst += *src;
}
}
*samples_done += done;
if cancelled {
return RemoteCalibration::Cancelled;
}
if let Some(message) = error {
return RemoteCalibration::Failed(message);
}
if done < probe {
return RemoteCalibration::Short {
done,
expected: probe,
};
}
let rate = measured_rate.unwrap_or_else(|| f64::from(probe) / elapsed);
RemoteCalibration::Ready(rate)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::export_thread::sample_cursor::SampleCursor;
#[test]
fn remote_chunk_samples_targets_rate_times_the_target_duration() {
assert_eq!(remote_chunk_samples(1000.0), 22_000);
}
#[test]
fn remote_chunk_samples_never_drops_below_the_floor_for_a_tiny_rate() {
assert_eq!(remote_chunk_samples(0.01), REMOTE_CHUNK_MIN_SPP);
assert_eq!(remote_chunk_samples(0.0), REMOTE_CHUNK_MIN_SPP);
}
#[test]
fn remote_chunk_samples_falls_back_to_the_floor_for_non_finite_rates() {
assert_eq!(remote_chunk_samples(f64::NAN), REMOTE_CHUNK_MIN_SPP);
assert_eq!(remote_chunk_samples(f64::INFINITY), REMOTE_CHUNK_MIN_SPP);
assert_eq!(remote_chunk_samples(-100.0), REMOTE_CHUNK_MIN_SPP);
}
#[test]
fn remote_chunk_samples_scales_up_for_a_faster_measured_rate() {
let slow = remote_chunk_samples(500.0);
let fast = remote_chunk_samples(1000.0);
assert!(fast > slow);
assert!((2.0f64.mul_add(-f64::from(slow), f64::from(fast))).abs() < 2.0);
}
#[test]
fn remote_chunk_samples_output_is_always_a_usable_positive_claim_size() {
for total in [32u32, 100, 1000, 32768] {
for rate in [0.0, 0.01, 50.0, 1000.0, 1_000_000.0] {
let cursor = SampleCursor::new(0, total);
let want = remote_chunk_samples(rate);
assert!(want > 0, "a chunk size of 0 would never advance the cursor");
if let Some((start, count)) = cursor.claim(want) {
assert_eq!(start, 0);
assert!(count <= total);
}
}
}
}
#[test]
fn shortfall_is_zero_when_remote_finished_everything_it_was_assigned() {
assert_eq!(shortfall(1000, 1000), 0);
}
#[test]
fn shortfall_is_the_unfinished_remainder_of_a_partial_completion() {
assert_eq!(shortfall(1000, 400), 600);
}
#[test]
fn shortfall_is_the_full_range_when_remote_completed_nothing() {
assert_eq!(shortfall(1000, 0), 1000);
}
#[test]
fn shortfall_never_underflows_even_if_completed_somehow_exceeded_assigned() {
assert_eq!(shortfall(100, 150), 0);
}
#[test]
fn remote_marginal_rate_divides_samples_by_elapsed_seconds() {
assert_eq!(
remote_marginal_rate(100, Duration::from_secs(2)),
Some(50.0)
);
}
#[test]
fn remote_marginal_rate_is_none_when_nothing_was_measured() {
assert_eq!(remote_marginal_rate(0, Duration::from_secs(1)), None);
}
#[test]
fn remote_marginal_rate_is_none_for_a_non_positive_elapsed_duration() {
assert_eq!(remote_marginal_rate(100, Duration::ZERO), None);
}
}