use super::*;
use crate::bridge::{
render_thread::RenderContext,
sample_cursor::tests::{apply_frame, assert_partition},
};
use glam::Vec3;
use indicatrix_net::{
client::ApplyOutcome,
messages::{FrameHeader, StreamEvent},
radiance,
};
use std::time::Duration;
fn t(ms: u64) -> Instant {
static ORIGIN: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
*ORIGIN.get_or_init(Instant::now) + Duration::from_millis(ms)
}
fn complete_chunk(lane: &mut LiveLane, chunk: &ChunkRequest, done_at: Instant) -> ChunkEnd {
apply_frame(
&chunk.accumulator,
chunk.request_id,
chunk.first_sample,
chunk.samples,
1.0,
);
lane.chunk_done(chunk.request_id, done_at)
.expect("the chunk is in flight")
}
#[test]
fn chunk_sizing_starts_small_and_then_follows_the_measured_rate() {
assert_eq!(live_chunk_samples(None), LIVE_FIRST_CHUNK_SAMPLES);
assert_eq!(live_chunk_samples(Some(100.0)), 150, "1.5 s at 100 spp/s");
assert_eq!(live_chunk_samples(Some(0.1)), LIVE_CHUNK_MIN_SAMPLES);
assert_eq!(live_chunk_samples(Some(f64::NAN)), LIVE_CHUNK_MIN_SAMPLES);
assert_eq!(live_chunk_samples(Some(1e12)), LIVE_CHUNK_MAX_SAMPLES);
}
#[test]
fn a_finished_chunk_updates_the_rate_and_the_next_chunk_is_sized_from_it() {
let epoch = Arc::new(LiveEpoch::new(1, 1, 10_000));
let mut lane = LiveLane::new(Arc::clone(&epoch), true);
let first = lane.next_chunk(1, t(0)).unwrap();
assert_eq!(
(first.first_sample, first.samples),
(0, LIVE_FIRST_CHUNK_SAMPLES)
);
assert!(
lane.next_chunk(2, t(0)).is_none(),
"one chunk in flight at a time"
);
complete_chunk(&mut lane, &first, t(100));
let rate = lane.rate().unwrap();
assert!((rate - 80.0).abs() < 1e-6, "rate {rate}");
let second = lane.next_chunk(2, t(100)).unwrap();
assert_eq!((second.first_sample, second.samples), (8, 120));
}
#[test]
fn the_lane_never_claims_past_the_target_and_then_reports_finished() {
let epoch = Arc::new(LiveEpoch::new(1, 1, 12));
let mut lane = LiveLane::new(Arc::clone(&epoch), true);
let a = lane.next_chunk(1, t(0)).unwrap();
complete_chunk(&mut lane, &a, t(10));
let b = lane.next_chunk(2, t(10)).unwrap();
assert_eq!((b.first_sample, b.samples), (8, 4), "clamped to the target");
complete_chunk(&mut lane, &b, t(20));
assert!(lane.next_chunk(3, t(20)).is_none());
assert!(lane.is_finished());
assert_eq!(epoch.remote_done(), 12);
}
#[test]
fn a_drag_mid_chunk_cancels_and_nothing_leaks_into_the_next_epoch() {
let ctx = Mutex::new(RenderContext::default());
let epoch1 = Arc::new(LiveEpoch::new(2, 2, 64));
{
let mut c = ctx.lock().unwrap();
c.remote_active = true;
c.live_epoch = Some(Arc::clone(&epoch1));
}
let mut lane1 = LiveLane::new(Arc::clone(&epoch1), true);
let chunk1 = lane1.next_chunk(1, t(0)).unwrap();
apply_frame(&chunk1.accumulator, 1, chunk1.first_sample, 3, 5.0);
assert_eq!(epoch1.remote_done(), 3);
assert_eq!(
lane1.abandon(),
Some(1),
"the in-flight request must be cancelled"
);
ctx.lock().unwrap().release_remote();
assert_eq!(
epoch1.remote_done(),
0,
"an abandoned chunk is never merged"
);
{
let c = ctx.lock().unwrap();
let (remote_active, has_epoch, dirty) = (c.remote_active, c.live_epoch.is_some(), c.dirty);
drop(c);
assert!(!remote_active);
assert!(!has_epoch);
assert!(dirty, "local restarts from a clean buffer");
}
assert!(lane1.chunk_done(1, t(5)).is_none());
assert!(lane1.chunk_failed(1).is_none());
let epoch2 = Arc::new(LiveEpoch::new(2, 2, 64));
let mut lane2 = LiveLane::new(Arc::clone(&epoch2), true);
let chunk2 = lane2.next_chunk(2, t(10)).unwrap();
let buf = vec![Vec3::splat(99.0); 4];
let bytes = radiance::encode(&buf);
let stale = StreamEvent::Frame(FrameHeader::for_payload(1, 0, 3, &bytes));
let outcome = chunk2
.accumulator
.lock()
.unwrap()
.apply(&stale, Some(&bytes))
.unwrap();
assert_eq!(outcome, ApplyOutcome::StaleDropped);
apply_frame(&chunk1.accumulator, 1, chunk1.first_sample + 3, 2, 7.0);
assert_eq!(epoch2.remote_done(), 0);
assert_eq!(epoch2.remote_snapshot(), (vec![Vec3::ZERO; 4], 0));
}
fn play_epoch(
target: u32,
local_lane: bool,
mut fail_after: impl FnMut(u32) -> Option<u32>,
) -> (Vec<(u32, u32)>, u32, u32, Vec<ChunkVerdict>) {
let epoch = Arc::new(LiveEpoch::new(1, 1, target));
let mut lane = LiveLane::new(Arc::clone(&epoch), local_lane);
let mut traced = Vec::new();
let mut local_count = 0;
let mut verdicts = Vec::new();
let mut request_id = 0;
let mut clock = 0;
loop {
request_id += 1;
clock += 50;
if let Some(chunk) = lane.next_chunk(request_id, t(clock)) {
if let Some(done) = fail_after(request_id) {
let done = done.min(chunk.samples);
if done > 0 {
apply_frame(
&chunk.accumulator,
request_id,
chunk.first_sample,
done,
1.0,
);
traced.push((chunk.first_sample, done));
}
verdicts.push(lane.chunk_failed(request_id).unwrap());
} else {
complete_chunk(&mut lane, &chunk, t(clock + 40));
traced.push((chunk.first_sample, chunk.samples));
}
}
if local_lane && let Some((start, count)) = epoch.claim_local(2) {
traced.push((start, count));
local_count += count;
}
if lane.is_finished() && local_count + epoch.remote_done() >= target {
break;
}
assert!(request_id < 10_000, "the epoch never finished");
}
assert!(
epoch.claim_local(2).is_none(),
"nothing may be left to trace once the count reached the target"
);
traced.sort_unstable();
(traced, local_count, epoch.remote_done(), verdicts)
}
#[test]
fn a_failure_mid_chunk_returns_the_remainder_and_the_final_count_is_exact() {
let target = 64;
let (traced, local, remote, verdicts) = play_epoch(target, true, |id| (id == 1).then_some(3));
assert_eq!(verdicts, vec![ChunkVerdict::Continue]);
assert_eq!(local + remote, target);
assert_partition(&traced, 0, target);
}
#[test]
fn two_consecutive_failures_give_up_and_local_finishes_exactly() {
let target = 40;
let (traced, local, remote, verdicts) = play_epoch(target, true, |id| (id <= 2).then_some(1));
assert_eq!(verdicts, vec![ChunkVerdict::Continue, ChunkVerdict::GaveUp]);
assert_eq!(remote, 2, "each failed chunk kept its one-sample prefix");
assert_eq!(local + remote, target);
assert_partition(&traced, 0, target);
}
#[test]
fn remote_only_retries_its_own_remainder() {
let target = 20;
let (traced, local, remote, verdicts) = play_epoch(target, false, |id| (id == 1).then_some(5));
assert_eq!(verdicts, vec![ChunkVerdict::Continue]);
assert_eq!(local, 0);
assert_eq!(remote, target);
assert_partition(&traced, 0, target);
}
#[test]
fn a_display_only_lane_claims_the_whole_budget_once_and_merges_nothing() {
let epoch = Arc::new(LiveEpoch::new(2, 2, 256));
let mut lane = LiveLane::display_only(Arc::clone(&epoch));
assert!(lane.is_display_only());
let chunk = lane.next_chunk(7, t(0)).expect("one chunk");
assert_eq!((chunk.first_sample, chunk.samples), (0, 256));
let end = lane.chunk_done(7, t(500)).expect("in flight");
assert_eq!((end.start, end.count, end.done), (0, 256, 256));
assert!(lane.is_finished());
assert_eq!(lane.next_chunk(8, t(600)).map(|c| c.samples), None);
assert_eq!(epoch.remote_done(), 0, "no radiance was ever merged");
}
#[test]
fn a_failed_display_only_request_is_retried_whole() {
let epoch = Arc::new(LiveEpoch::new(1, 1, 64));
let mut lane = LiveLane::display_only(Arc::clone(&epoch));
let first = lane.next_chunk(1, t(0)).unwrap();
assert_eq!(
lane.chunk_failed(first.request_id),
Some(ChunkVerdict::Continue)
);
let retry = lane.next_chunk(2, t(10)).expect("retried");
assert_eq!((retry.first_sample, retry.samples), (0, 64));
assert_eq!(
lane.chunk_failed(retry.request_id),
Some(ChunkVerdict::GaveUp)
);
}