use super::{
state::{Orchestrator, lock},
update::handle_remote_update,
};
use crate::{
MainWindow,
bridge::{
export_thread::SceneSnapshot,
frame_cache::guide_pass::GuideCache,
remote::remote_render::{self, RemoteUpdate},
render_thread::RenderContext,
},
settings::{LiveComputeTarget, WorkerSettings},
};
use indicatrix::optics::raytracer::Camera;
use indicatrix_net::{SceneState, client::Accumulator};
use slint::ComponentHandle;
use std::{
rc::Rc,
sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicU32, Ordering},
},
};
pub(super) fn start_remote_render(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
worker: WorkerSettings,
next_request_id: &Rc<AtomicU32>,
state: &Arc<Mutex<Orchestrator>>,
) {
let (width, height, samples, live_compute_target) = {
let ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
(
ctx.width,
ctx.height,
ctx.remote_render_samples,
ctx.live_compute_target,
)
};
if width == 0 || height == 0 {
return;
}
let combining = matches!(live_compute_target, LiveComputeTarget::Both);
let snapshot = SceneSnapshot::capture(render_ctx);
let scene = scene_state_from_snapshot(&snapshot, width, height);
if !combining {
let guide_key = GuideCache::key_for(
width,
height,
snapshot.yaw,
snapshot.pitch,
snapshot.distance,
&snapshot.active_planes,
);
let guide_camera = Camera::new(snapshot.yaw, snapshot.pitch, snapshot.distance, 42.0);
let mut s = lock(state);
if let Some(previous) = s.pending_guide_gen.take() {
previous.cancel.store(true, Ordering::Relaxed);
}
s.pending_guide_gen = Some(super::super::generation::spawn_guide_generation(
guide_key,
guide_camera,
snapshot.active_planes, width,
height,
));
}
let accumulator = Arc::new(Mutex::new(Accumulator::new(width, height)));
lock(state).accumulator = Some(Arc::clone(&accumulator));
{
let mut ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
ctx.remote_active = true;
ctx.dirty = true;
ctx.remote_accumulator = combining.then(|| Arc::clone(&accumulator));
ctx.remote_reserved_samples = if combining { samples } else { 0 };
}
let request_id = next_request_id.fetch_add(1, Ordering::Relaxed);
lock(state).current_request_id = Some(request_id);
let ui_weak = ui.as_weak();
let state_for_updates = Arc::clone(state);
let render_ctx_for_updates = Arc::clone(render_ctx);
let accumulator_for_redraw = Arc::clone(&accumulator);
let mut s = lock(state);
if s.remote_connection
.as_ref()
.is_none_or(|h| h.worker() != &worker)
{
s.remote_connection = Some(remote_render::spawn_remote_connection(worker.clone()));
}
let handle = s
.remote_connection
.as_ref()
.expect("just ensured Some immediately above")
.render(
remote_render::RemoteRenderRequest {
worker,
request_id,
scene,
first_sample: 0,
samples,
width,
height,
},
accumulator,
move |update: RemoteUpdate| {
handle_remote_update(
&ui_weak,
&render_ctx_for_updates,
&state_for_updates,
&accumulator_for_redraw,
width,
height,
update,
);
},
);
s.remote_handle = Some(handle);
}
#[must_use]
pub(super) fn connection_is_stale(
cached: Option<&WorkerSettings>,
wanted: Option<&WorkerSettings>,
) -> bool {
match (cached, wanted) {
(Some(c), Some(w)) => c != w,
(Some(_), None) => true,
(None, _) => false,
}
}
fn scene_state_from_snapshot(snapshot: &SceneSnapshot, width: u32, height: u32) -> SceneState {
SceneState {
width,
height,
yaw: snapshot.yaw,
pitch: snapshot.pitch,
distance: snapshot.distance,
light_yaw: snapshot.light_yaw,
light_pitch: snapshot.light_pitch,
exposure: snapshot.exposure,
max_bounces: snapshot.max_bounces,
lighting_preset: snapshot.lighting_preset,
material: snapshot.material.clone(),
planes: snapshot.active_planes.clone(),
girdle_frosted: !snapshot.facet_finishes.is_empty(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn worker_named(name: &str) -> WorkerSettings {
WorkerSettings {
name: name.to_string(),
address: format!("{name}.local:9443"),
cert_dir: format!("/certs/{name}"),
..WorkerSettings::default()
}
}
#[test]
fn connection_is_stale_is_false_when_nothing_is_cached_yet() {
assert!(!connection_is_stale(None, None));
assert!(!connection_is_stale(None, Some(&worker_named("a"))));
}
#[test]
fn connection_is_stale_is_false_when_the_cached_worker_is_unchanged() {
let w = worker_named("a");
assert!(!connection_is_stale(Some(&w), Some(&w)));
}
#[test]
fn connection_is_stale_is_true_when_the_address_or_cert_dir_differs() {
let cached = worker_named("a");
let mut edited_address = cached.clone();
edited_address.address = "somewhere-else.local:9443".to_string();
assert!(
connection_is_stale(Some(&cached), Some(&edited_address)),
"an edited address must force a reconnect"
);
let mut edited_cert_dir = cached.clone();
edited_cert_dir.cert_dir = "/certs/somewhere-else".to_string();
assert!(
connection_is_stale(Some(&cached), Some(&edited_cert_dir)),
"an edited cert_dir must force a reconnect -- the whole point of mutual \
TLS here is that a connection authenticated under the OLD certificates \
must never be reused as if the new ones had been verified"
);
}
#[test]
fn connection_is_stale_is_true_when_the_configured_worker_was_removed() {
assert!(
connection_is_stale(Some(&worker_named("a")), None),
"no worker configured any more (removed from the list, or remote compute \
switched off) must tear down a cached connection rather than leave it \
dangling"
);
}
#[test]
fn connection_is_stale_is_true_when_a_different_worker_is_now_first() {
assert!(connection_is_stale(
Some(&worker_named("a")),
Some(&worker_named("b"))
));
}
}