use super::dispatch::{CANCEL_WAIT_TIMEOUT, liveness_deadline};
use crate::{
bridge::{
export_thread::params::ComputeTarget,
remote::remote_render::{self, RemoteFinalImageRequest, RemoteUpdate},
},
settings::{ExportTransfer, WorkerSettings},
};
use indicatrix::color::ColorSpace;
use indicatrix_net::{SceneState, client::Accumulator};
use std::{
collections::BTreeSet,
sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError},
},
time::{Duration, Instant},
};
const REQUEST_ID: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::bridge::export_thread) enum TransferPlan {
FinalPicture,
FullData,
FullDataRefusedBefore,
}
#[must_use]
pub(in crate::bridge::export_thread) const fn plan_export_transfer(
requested: ExportTransfer,
compute_target: ComputeTarget,
remote_configured: bool,
hdr_refused: bool,
refused_before: bool,
) -> TransferPlan {
let wanted = matches!(requested, ExportTransfer::FinalPicture)
&& !matches!(compute_target, ComputeTarget::LocalOnly)
&& remote_configured
&& !hdr_refused;
match (wanted, refused_before) {
(false, _) => TransferPlan::FullData,
(true, true) => TransferPlan::FullDataRefusedBefore,
(true, false) => TransferPlan::FinalPicture,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::bridge::export_thread) enum FinalPictureOutcome {
Completed(Vec<u8>),
Cancelled,
Unsupported(String),
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::bridge::export_thread) enum FinalPictureFollowUp {
Use(Vec<u8>),
Cancelled,
FallBackToFullData { note: String, remember: bool },
Fail(String),
}
#[must_use]
pub(in crate::bridge::export_thread) fn final_picture_follow_up(
outcome: FinalPictureOutcome,
compute_target: ComputeTarget,
) -> FinalPictureFollowUp {
match outcome {
FinalPictureOutcome::Completed(rgba) => FinalPictureFollowUp::Use(rgba),
FinalPictureOutcome::Cancelled => FinalPictureFollowUp::Cancelled,
FinalPictureOutcome::Unsupported(message) => FinalPictureFollowUp::FallBackToFullData {
note: format!(
"The remote does not support final-picture transfer ({message}); \
using full data instead."
),
remember: true,
},
FinalPictureOutcome::Failed(message) => {
if matches!(compute_target, ComputeTarget::RemoteOnly) {
FinalPictureFollowUp::Fail(format!("Remote final-picture render failed: {message}"))
} else {
FinalPictureFollowUp::FallBackToFullData {
note: format!(
"Remote final-picture render failed ({message}); continuing with \
full data."
),
remember: false,
}
}
}
}
}
static FINAL_PICTURE_REFUSED: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
pub(in crate::bridge::export_thread) fn remember_final_picture_refused(worker: &WorkerSettings) {
FINAL_PICTURE_REFUSED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(worker.address.clone());
}
#[must_use]
pub(in crate::bridge::export_thread) fn final_picture_refused(worker: &WorkerSettings) -> bool {
FINAL_PICTURE_REFUSED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.contains(&worker.address)
}
pub fn forget_final_picture_refusals() {
FINAL_PICTURE_REFUSED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clear();
}
pub(in crate::bridge::export_thread) fn decode_final_picture(
accumulator: &Accumulator,
width: u32,
height: u32,
) -> Result<Vec<u8>, String> {
let picture = accumulator
.final_image()
.ok_or_else(|| "the remote finished without sending a picture".to_string())?;
if (picture.width, picture.height) != (width, height) {
return Err(format!(
"the remote sent a {}x{} picture for a {width}x{height} request",
picture.width, picture.height
));
}
indicatrix_net::display::decode_rgba8(picture.encoding, width, height, &picture.bytes)
.map_err(|e| format!("the remote's picture did not decode: {e}"))
}
pub(in crate::bridge::export_thread) fn run_final_image_request(
worker: &WorkerSettings,
scene: SceneState,
samples: u32,
color_space: ColorSpace,
cancel: &AtomicBool,
mut on_progress: impl FnMut(u32),
) -> FinalPictureOutcome {
let (width, height) = (scene.width, scene.height);
let accumulator = Arc::new(Mutex::new(Accumulator::new(width, height)));
let (tx, rx) = mpsc::channel::<RemoteUpdate>();
let handle = remote_render::spawn_final_image_request(
RemoteFinalImageRequest {
worker: worker.clone(),
request_id: REQUEST_ID,
scene,
first_sample: 0,
samples,
color_space,
},
Arc::clone(&accumulator),
move |update| {
let _ = tx.send(update);
},
);
let picture_bytes = u64::from(width) * u64::from(height) * 4;
let mut cancel_sent_at: Option<Instant> = None;
let mut last_update = Instant::now();
let mut seen_first = false;
loop {
if cancel_sent_at.is_none() && cancel.load(Ordering::Relaxed) {
handle.cancel();
cancel_sent_at = Some(Instant::now());
}
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(update) => {
last_update = Instant::now();
seen_first = true;
if let Some(outcome) =
on_update(update, &accumulator, width, height, &mut on_progress)
{
return outcome;
}
}
Err(RecvTimeoutError::Timeout) => match cancel_sent_at {
Some(sent_at) if sent_at.elapsed() > CANCEL_WAIT_TIMEOUT => {
tracing::warn!("final picture: the remote never confirmed the cancel");
return FinalPictureOutcome::Cancelled;
}
None if last_update.elapsed() > liveness_deadline(seen_first, picture_bytes) => {
return FinalPictureOutcome::Failed(format!(
"remote silent for {:.0?}",
last_update.elapsed()
));
}
Some(_) | None => {}
},
Err(RecvTimeoutError::Disconnected) => {
return FinalPictureOutcome::Failed(
"the remote connection thread ended unexpectedly".to_string(),
);
}
}
}
}
fn on_update(
update: RemoteUpdate,
accumulator: &Mutex<Accumulator>,
width: u32,
height: u32,
on_progress: &mut impl FnMut(u32),
) -> Option<FinalPictureOutcome> {
match update {
RemoteUpdate::Progress { samples_done, .. } => {
on_progress(samples_done);
None
}
RemoteUpdate::Done {
cancelled: true, ..
} => Some(FinalPictureOutcome::Cancelled),
RemoteUpdate::Done {
cancelled: false, ..
} => {
let decoded = decode_final_picture(
&accumulator.lock().unwrap_or_else(PoisonError::into_inner),
width,
height,
);
Some(match decoded {
Ok(rgba) => FinalPictureOutcome::Completed(rgba),
Err(message) => FinalPictureOutcome::Failed(message),
})
}
RemoteUpdate::Unsupported { message, .. } => {
Some(FinalPictureOutcome::Unsupported(message))
}
RemoteUpdate::Failed { message, .. } => Some(FinalPictureOutcome::Failed(message)),
RemoteUpdate::Connected { .. }
| RemoteUpdate::FinalImage { .. }
| RemoteUpdate::Frame { .. }
| RemoteUpdate::Preview { .. }
| RemoteUpdate::DisplayFrame { .. }
| RemoteUpdate::CapabilityChanged { .. } => None,
}
}
#[cfg(test)]
mod tests;