use super::{
decisions::wants_display_only,
state::{Orchestrator, lock},
update::handle_remote_update,
};
use crate::{
MainWindow,
bridge::{
export_thread::SceneSnapshot,
frame_cache::guide_pass::GuideCache,
remote::{
live_lane::LiveLane,
remote_render::{self, RemoteRenderRequest, RemoteUpdate},
},
render_thread::RenderContext,
sample_cursor::LiveEpoch,
},
settings::{LiveComputeTarget, RemoteEndpoint, WorkerSettings},
};
use indicatrix::{
geometry::tool::StoneGeometry,
optics::raytracer::{Camera, DEFAULT_FOV_DEG},
};
use indicatrix_net::SceneState;
use slint::{ComponentHandle, Weak};
use std::{
sync::{Arc, Mutex, PoisonError, atomic::Ordering},
time::Instant,
};
pub(super) fn start_remote_render(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
endpoint: RemoteEndpoint,
state: &Arc<Mutex<Orchestrator>>,
) {
let (width, height, target_samples, live_compute_target, scene_generation) = {
let mut ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
(
ctx.width,
ctx.height,
ctx.target_samples,
ctx.live_compute_target,
ctx.scene_generation(),
)
};
if width == 0 || height == 0 || target_samples == 0 {
return;
}
let display_only = wants_display_only(
live_compute_target,
endpoint.live_transfer,
lock(state).display_only_refused,
);
let combining = matches!(live_compute_target, LiveComputeTarget::Both) && !display_only;
let Ok(snapshot) = SceneSnapshot::capture(render_ctx) else {
return;
};
let scene = scene_state_from_snapshot(&snapshot, width, height);
if !combining && !display_only {
let n_d = snapshot.material.dispersion.n_d();
let guide_key = GuideCache::key_for_geom(
width,
height,
snapshot.yaw,
snapshot.pitch,
snapshot.distance,
StoneGeometry {
planes: &snapshot.active_planes,
tools: &snapshot.tools,
},
n_d,
);
let guide_camera = Camera::new(
snapshot.yaw,
snapshot.pitch,
snapshot.distance,
DEFAULT_FOV_DEG,
);
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, snapshot.tools,
width,
height,
n_d,
));
}
let epoch = Arc::new(LiveEpoch::new(width, height, target_samples).for_scene(scene_generation));
{
let mut ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
ctx.remote_active = true;
ctx.dirty = true;
ctx.live_epoch = Some(Arc::clone(&epoch));
ctx.live_display_only = display_only;
}
{
let mut s = lock(state);
if s.remote_connection
.as_ref()
.is_none_or(|h| h.worker() != &endpoint.connection)
{
s.remote_connection = Some(remote_render::spawn_remote_connection(endpoint.connection));
s.display_only_refused = false;
s.remote_hdr = None;
}
s.live_lane = Some(if display_only {
LiveLane::display_only(epoch)
} else {
LiveLane::new(epoch, combining)
});
s.lane_scene = Some(scene);
}
dispatch_next_chunk(&ui.as_weak(), render_ctx, state);
}
pub(super) fn dispatch_next_chunk(
ui_weak: &Weak<MainWindow>,
render_ctx: &Arc<Mutex<RenderContext>>,
state: &Arc<Mutex<Orchestrator>>,
) -> bool {
let mut s = lock(state);
let request_id = s.alloc_request_id();
let Orchestrator {
live_lane,
lane_scene,
remote_connection,
..
} = &mut *s;
let (Some(lane), Some(scene), Some(connection)) = (
live_lane.as_mut(),
lane_scene.as_ref(),
remote_connection.as_ref(),
) else {
return false;
};
let Some(chunk) = lane.next_chunk(request_id, Instant::now()) else {
return false;
};
let (width, height) = lane.epoch().dimensions();
let request = RemoteRenderRequest {
worker: connection.worker().clone(),
request_id: chunk.request_id,
scene: scene.clone(),
first_sample: chunk.first_sample,
samples: chunk.samples,
width,
height,
intent: indicatrix_net::messages::RequestIntent::Interactive,
display_only: lane.is_display_only(),
};
let ui_weak = ui_weak.clone();
let render_ctx = Arc::clone(render_ctx);
let state_for_updates = Arc::clone(state);
let chunk_accumulator = Arc::clone(&chunk.accumulator);
let handle = connection.render(request, chunk.accumulator, move |update: RemoteUpdate| {
handle_remote_update(
&ui_weak,
&render_ctx,
&state_for_updates,
(width, height, &chunk_accumulator),
update,
);
});
s.current_request_id = Some(request_id);
s.remote_handle = Some(handle);
true
}
#[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(),
backdrop: snapshot.backdrop,
surface_glare: snapshot.surface_glare,
tools: snapshot.tools.clone(),
environment: crate::bridge::remote::hdr_asset::scene_environment(snapshot.env_map.as_ref()),
fluorescence: snapshot.fluorescence.as_ref().clone(),
head_shadow_deg: snapshot.head_shadow_deg,
}
}
#[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 the_live_dispatch_scene_carries_the_viewports_surface_glare() {
let ctx = Mutex::new(RenderContext {
surface_glare: 0.4,
..Default::default()
});
let snapshot = SceneSnapshot::capture(&ctx).expect("default resolves");
let state = scene_state_from_snapshot(&snapshot, 64, 64);
assert_eq!(state.surface_glare.to_bits(), 0.4f32.to_bits());
}
#[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"))
));
}
}