use super::{
dispatch::{connection_is_stale, start_remote_render},
state::{Orchestrator, current_pose, lock},
};
use crate::{
MainWindow, RemoteWorkerModel,
bridge::{
frame_cache::guide_pass::GuideCache,
remote::{
handoff::{HandoffAction, HandoffEvent, HandoffMachine, HandoffState},
remote_render::RemoteConnectionHandle,
},
render_thread::{RedrawGate, RenderContext},
},
settings::{LiveComputeTarget, SettingsPersister},
};
use slint::ComponentHandle;
use std::{
rc::Rc,
sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant},
};
const SETTLE_DEBOUNCE: Duration = Duration::from_millis(600);
const POLL_INTERVAL_MS: u64 = 100;
#[must_use]
pub fn setup_remote_rendering(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
) -> slint::Timer {
let state = Arc::new(Mutex::new(Orchestrator {
handoff: HandoffMachine::new(),
remote_handle: None,
accumulator: None,
current_request_id: None,
remote_connection: None,
last_pose: current_pose(render_ctx),
last_change_at: Instant::now(),
guide_cache: GuideCache::new(),
pending_guide_gen: None,
pending_denoise_gen: None,
last_denoised: None,
redraw_gate: RedrawGate::new(),
last_redraw_at: None,
}));
let next_request_id = Rc::new(AtomicU32::new(1));
let timer = slint::Timer::default();
let ui_weak = ui.as_weak();
let render_ctx_poll = render_ctx.clone();
let settings_store_poll = settings_store.clone();
timer.start(
slint::TimerMode::Repeated,
Duration::from_millis(POLL_INTERVAL_MS),
move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
poll_tick(
&ui,
&render_ctx_poll,
&settings_store_poll,
&state,
&next_request_id,
);
},
);
timer
}
fn poll_tick(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
state: &Arc<Mutex<Orchestrator>>,
next_request_id: &Rc<AtomicU32>,
) {
let pose = current_pose(render_ctx);
let changed = pose != lock(state).last_pose;
if changed {
lock(state).last_pose = pose;
lock(state).last_change_at = Instant::now();
let actions = lock(state).handoff.handle(HandoffEvent::OrientationChanged);
apply_actions(&actions, render_ctx, state);
sync_served_by_to_ui(ui, state);
sync_camera_moving_to_ctx(render_ctx, state);
return;
}
let (should_check_settle, elapsed_enough) = {
let s = lock(state);
(
matches!(s.handoff.state(), HandoffState::Previewing),
s.last_change_at.elapsed() >= SETTLE_DEBOUNCE,
)
};
if should_check_settle && elapsed_enough {
let live_compute_target = render_ctx
.lock()
.unwrap_or_else(PoisonError::into_inner)
.live_compute_target;
let worker = if matches!(live_compute_target, LiveComputeTarget::LocalOnly) {
None
} else {
settings_store
.snapshot()
.settings
.remote_workers
.first()
.cloned()
};
if connection_is_stale(
lock(state)
.remote_connection
.as_ref()
.map(RemoteConnectionHandle::worker),
worker.as_ref(),
) {
lock(state).remote_connection = None;
}
let actions = lock(state).handoff.handle(HandoffEvent::SettleElapsed {
worker_available: worker.is_some(),
});
apply_actions(&actions, render_ctx, state);
if let Some(worker) = worker {
start_remote_render(ui, render_ctx, worker, next_request_id, state);
}
}
sync_camera_moving_to_ctx(render_ctx, state);
reconcile_served_by_after_release(ui, render_ctx, state);
}
fn reconcile_served_by_after_release(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &Arc<Mutex<Orchestrator>>,
) {
let still_remote_active = render_ctx
.lock()
.unwrap_or_else(PoisonError::into_inner)
.remote_active;
if still_remote_active {
return;
}
let served_by_remote = matches!(
lock(state).handoff.served_by(),
crate::bridge::remote::handoff::ImageSource::Remote
);
if served_by_remote {
lock(state).handoff.handle(HandoffEvent::SceneInvalidated);
sync_served_by_to_ui(ui, state);
}
}
fn sync_camera_moving_to_ctx(
render_ctx: &Arc<Mutex<RenderContext>>,
state: &Arc<Mutex<Orchestrator>>,
) {
let moving = matches!(lock(state).handoff.state(), HandoffState::Previewing);
render_ctx
.lock()
.unwrap_or_else(PoisonError::into_inner)
.camera_moving = moving;
}
pub(super) fn sync_served_by_to_ui(ui: &MainWindow, state: &Arc<Mutex<Orchestrator>>) {
let served_by_remote = matches!(
lock(state).handoff.served_by(),
crate::bridge::remote::handoff::ImageSource::Remote
);
ui.global::<RemoteWorkerModel>()
.set_served_by_remote(served_by_remote);
}
pub(super) fn apply_actions(
actions: &[HandoffAction],
render_ctx: &Arc<Mutex<RenderContext>>,
state: &Arc<Mutex<Orchestrator>>,
) {
for action in actions {
match action {
HandoffAction::DiscardLocalPreview | HandoffAction::SendRenderRequestToWorker => {}
HandoffAction::SendCancelToWorker => {
if let Some(handle) = &lock(state).remote_handle {
handle.cancel();
}
}
HandoffAction::DiscardRemotePartial => {
let mut s = lock(state);
s.remote_handle = None;
s.accumulator = None;
if let Some(pending) = s.pending_guide_gen.take() {
pending.cancel.store(true, Ordering::Relaxed);
}
s.pending_denoise_gen = None;
s.last_denoised = None;
drop(s);
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;
}
}
}
}