use super::{
poll::{apply_actions, sync_served_by_to_ui},
state::{Orchestrator, lock},
};
use crate::{
MainWindow, RemoteWorkerModel, ViewportModel,
bridge::{
frame_cache::guide_pass::GuideCache,
remote::{handoff::HandoffEvent, remote_render::RemoteUpdate},
render_thread::{RenderContext, tonemap_running_average},
},
gui::{remote::worker_callbacks::backend_label, show_toast},
settings::LiveComputeTarget,
};
use indicatrix_net::client::Accumulator;
use slint::{ComponentHandle, Weak};
use std::{
sync::{Arc, Mutex, PoisonError},
time::{Duration, Instant},
};
use super::super::generation::{
DenoiseGenerationJob, adopt_ready_denoise, adopt_ready_guides, spawn_denoise_generation,
};
const REMOTE_REDRAW_MIN_INTERVAL: Duration = Duration::from_millis(33);
#[must_use]
fn redraw_is_due(last_redraw_at: Option<Instant>, min_interval: Duration, now: Instant) -> bool {
last_redraw_at.is_none_or(|t| now.duration_since(t) >= min_interval)
}
fn is_combining(render_ctx: &Arc<Mutex<RenderContext>>) -> bool {
matches!(
render_ctx
.lock()
.unwrap_or_else(PoisonError::into_inner)
.live_compute_target,
LiveComputeTarget::Both
)
}
pub(super) fn handle_remote_update(
ui_weak: &Weak<MainWindow>,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &Arc<Mutex<Orchestrator>>,
accumulator: &Arc<Mutex<Accumulator>>,
width: u32,
height: u32,
update: RemoteUpdate,
) {
let render_ctx = render_ctx.clone();
let state = Arc::clone(state);
let accumulator = Arc::clone(accumulator);
let wants_gated_redraw = matches!(
update,
RemoteUpdate::Frame { .. } | RemoteUpdate::Preview { .. }
);
if wants_gated_redraw {
if is_combining(&render_ctx) {
return;
}
let orch = lock(&state);
let due = redraw_is_due(
orch.last_redraw_at,
REMOTE_REDRAW_MIN_INTERVAL,
Instant::now(),
);
if !due || orch.redraw_gate.submit(()).is_none() {
return;
}
drop(orch);
}
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
if wants_gated_redraw {
lock(&state).redraw_gate.take();
}
if lock(&state).current_request_id != Some(update.request_id()) {
return;
}
match update {
RemoteUpdate::Connected { info, .. } => {
let actions = lock(&state)
.handoff
.handle(HandoffEvent::RemoteStreamStarted);
apply_actions(&actions, &render_ctx, &state);
ui.global::<RemoteWorkerModel>()
.set_served_by_worker_name(backend_label(info.render.as_ref()).into());
}
RemoteUpdate::Frame { samples_done, .. } => {
tracing::trace!("remote render: {samples_done} samples done");
if !is_combining(&render_ctx) {
redraw_from_accumulator(&ui, &accumulator, &render_ctx, &state, width, height);
}
}
RemoteUpdate::Preview { .. } => {
if !is_combining(&render_ctx) {
redraw_from_accumulator(&ui, &accumulator, &render_ctx, &state, width, height);
}
}
RemoteUpdate::Progress { samples_done, .. } => {
tracing::trace!("remote render progress: {samples_done} samples done");
}
RemoteUpdate::Done { cancelled, .. } => {
if cancelled {
return;
}
if !is_combining(&render_ctx) {
redraw_from_accumulator(&ui, &accumulator, &render_ctx, &state, width, height);
}
let actions = lock(&state).handoff.handle(HandoffEvent::RemoteDone);
apply_actions(&actions, &render_ctx, &state);
sync_served_by_to_ui(&ui, &state);
let mut s = lock(&state);
s.remote_handle = None;
s.accumulator = None;
}
RemoteUpdate::Failed { message, .. } => {
let actions = lock(&state).handoff.handle(HandoffEvent::RemoteFailed);
apply_actions(&actions, &render_ctx, &state);
{
let mut ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
ctx.remote_active = false;
ctx.dirty = true;
ctx.remote_accumulator = None;
ctx.remote_reserved_samples = 0;
}
let mut s = lock(&state);
s.remote_handle = None;
s.accumulator = None;
drop(s);
show_toast(&ui, &format!("Remote render failed: {message}"), "error");
}
}
});
}
fn redraw_from_accumulator(
ui: &MainWindow,
accumulator: &Arc<Mutex<Accumulator>>,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &Arc<Mutex<Orchestrator>>,
width: u32,
height: u32,
) {
let (buffer, samples_done) = {
let acc = accumulator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(acc.buffer().to_vec(), acc.samples_done().max(1))
};
let (yaw, pitch, distance, planes, denoise_enabled) = {
let ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
(
ctx.yaw,
ctx.pitch,
ctx.distance,
ctx.active_planes.clone(),
ctx.denoise_enabled,
)
};
let desired_key = GuideCache::key_for(width, height, yaw, pitch, distance, &planes);
let mut orch = lock(state);
let bytes = if denoise_enabled {
if let Some(fresh) = adopt_ready_denoise(&desired_key, orch.pending_denoise_gen.as_ref()) {
orch.pending_denoise_gen = None;
orch.last_denoised = Some((desired_key.clone(), fresh));
}
match orch.last_denoised.as_ref() {
Some((key, bytes)) if *key == desired_key => bytes.clone(),
_ => {
orch.last_denoised = None;
tonemap_running_average(width, height, samples_done, &buffer)
}
}
} else {
orch.pending_denoise_gen = None;
orch.last_denoised = None;
tonemap_running_average(width, height, samples_done, &buffer)
};
if denoise_enabled {
let pending_matches = orch
.pending_denoise_gen
.as_ref()
.is_some_and(|p| p.key == desired_key);
if !pending_matches {
let Orchestrator {
guide_cache,
pending_guide_gen,
..
} = &mut *orch;
if adopt_ready_guides(&desired_key, guide_cache, pending_guide_gen.as_ref()) {
let guides = guide_cache
.ensure(width, height, yaw, pitch, distance, &planes)
.clone();
orch.pending_denoise_gen = Some(spawn_denoise_generation(
desired_key,
DenoiseGenerationJob {
width,
height,
samples_done,
buffer,
guides,
yaw,
pitch,
distance,
planes: planes.as_ref().clone(),
},
));
}
}
}
orch.last_redraw_at = Some(Instant::now());
drop(orch);
let mut fb = crate::bridge::pixel_buffer::FramebufferTransfer::new(width, height);
let image = fb.copy_from_gpu_slice(&bytes);
ui.global::<ViewportModel>()
.set_render_image(slint::Image::from_rgba8(image));
ui.global::<ViewportModel>().set_has_render(true);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_prior_redraw_is_always_due() {
assert!(redraw_is_due(
None,
REMOTE_REDRAW_MIN_INTERVAL,
Instant::now()
));
}
#[test]
fn a_redraw_within_the_interval_is_not_due() {
let last = Instant::now();
let now = last + Duration::from_millis(10);
assert!(
!redraw_is_due(Some(last), REMOTE_REDRAW_MIN_INTERVAL, now),
"10ms after the last redraw is well inside the ~33ms interval"
);
}
#[test]
fn a_redraw_exactly_at_the_interval_boundary_is_due() {
let last = Instant::now();
let now = last + REMOTE_REDRAW_MIN_INTERVAL;
assert!(
redraw_is_due(Some(last), REMOTE_REDRAW_MIN_INTERVAL, now),
"the boundary itself (elapsed >= min_interval) must count as due"
);
}
#[test]
fn a_redraw_well_past_the_interval_is_due() {
let last = Instant::now();
let now = last + REMOTE_REDRAW_MIN_INTERVAL + Duration::from_secs(1);
assert!(redraw_is_due(Some(last), REMOTE_REDRAW_MIN_INTERVAL, now));
}
#[test]
fn a_burst_of_updates_within_one_interval_finds_at_most_one_due() {
let start = Instant::now();
let mut last_redraw_at: Option<Instant> = None;
let mut due_count = 0;
for ms in [0u64, 5, 10, 15, 20, 25, 30] {
let now = start + Duration::from_millis(ms);
if redraw_is_due(last_redraw_at, REMOTE_REDRAW_MIN_INTERVAL, now) {
due_count += 1;
last_redraw_at = Some(now);
}
}
assert_eq!(
due_count, 1,
"only the FIRST update of a burst inside one ~33ms interval may redraw"
);
let now = start + REMOTE_REDRAW_MIN_INTERVAL + Duration::from_millis(1);
assert!(redraw_is_due(
last_redraw_at,
REMOTE_REDRAW_MIN_INTERVAL,
now
));
}
}