use super::{
metrics::{self, SessionMetrics, StripText, TiltEvent, TiltReport},
origins::KeepGuard,
render::{
self, FrameEvent, FrameMsg, LatestWorker, Pixels, SideGeometry, TRACED_SPP, ViewRequest,
},
session::{
self, CompareOrigin, CompareSession, FrameBook, FrameKind, Renderer, TracedProgress,
},
};
use crate::{
CompareMetricRow, CompareModel, CompareWindow, MainWindow,
gui::solid_preview::preview_state::CameraPose,
};
use slint::{ComponentHandle, Image, ModelRc, Timer, TimerMode, VecModel, Weak};
use std::{
cell::RefCell,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
use tracing::warn;
pub(super) const TRACED_DEBOUNCE: Duration = Duration::from_millis(250);
pub(super) const POPPED_OUT_STATUS: &str = "Shown in the Compare window";
const MODE_OVERLAY: i32 = 2;
thread_local! {
static COMPARE: RefCell<Host> = const { RefCell::new(Host::new()) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Surface {
Window,
Embedded,
}
pub(super) enum SurfaceHandle {
Window(CompareWindow),
Embedded(MainWindow),
}
impl SurfaceHandle {
pub(super) fn with_model<R>(&self, f: impl FnOnce(CompareModel<'_>) -> R) -> R {
match self {
Self::Window(window) => f(window.global::<CompareModel>()),
Self::Embedded(ui) => f(ui.global::<CompareModel>()),
}
}
pub(super) fn scale_factor(&self) -> f32 {
match self {
Self::Window(window) => window.window().scale_factor(),
Self::Embedded(ui) => ui.window().scale_factor(),
}
}
}
struct Host {
window: Option<CompareWindow>,
ui: Option<Weak<MainWindow>>,
next_id: u64,
live: Option<LiveSession>,
retained_pose: Option<CameraPose>,
}
impl Host {
const fn new() -> Self {
Self {
window: None,
ui: None,
next_id: 0,
live: None,
retained_pose: None,
}
}
}
pub(super) struct NewLive {
pub(super) surface: Surface,
pub(super) origin: CompareOrigin,
pub(super) guard: KeepGuard,
pub(super) renderer: Renderer,
pub(super) size_px: (u32, u32),
}
pub(super) struct LiveSession {
id: u64,
surface: Surface,
origin: CompareOrigin,
guard: KeepGuard,
renderer: Renderer,
size_px: (u32, u32),
drag_held: bool,
size_settling: bool,
settle_polls: u8,
seed_pose: Option<CameraPose>,
ready: Option<ReadySession>,
}
impl LiveSession {
pub(super) const fn surface(&self) -> Surface {
self.surface
}
pub(super) fn pose(&self) -> Option<CameraPose> {
self.ready
.as_ref()
.map(|ready| ready.session.pose)
.or(self.seed_pose)
}
}
struct ReadySession {
session: CompareSession,
frames: FrameBook,
latest: Arc<AtomicU64>,
solid: LatestWorker<ViewRequest>,
traced: LatestWorker<ViewRequest>,
traced_timer: Timer,
traced_progress: TracedProgress,
traced_spp: [u32; 2],
metrics: SessionMetrics,
}
fn current_status(live: &LiveSession) -> String {
let ready = live.ready.as_ref();
let mut status = session::status_text(
ready.map(|r| &r.session),
live.renderer,
ready.map_or(TracedProgress::Done, |r| r.traced_progress),
TRACED_SPP,
);
if live.renderer == Renderer::Traced
&& let Some(ready) = ready
&& ready.traced_progress == TracedProgress::Done
&& let Some(note) = session::reduced_spp_note(ready.traced_spp, TRACED_SPP)
{
status.push_str(" · ");
status.push_str(¬e);
}
status
}
pub(super) fn solving_status(renderer: Renderer) -> String {
session::status_text(None, renderer, TracedProgress::Done, TRACED_SPP)
}
fn handle_for(host: &Host, surface: Surface) -> Option<SurfaceHandle> {
match surface {
Surface::Window => host
.window
.as_ref()
.map(|window| SurfaceHandle::Window(window.clone_strong())),
Surface::Embedded => host
.ui
.as_ref()
.and_then(Weak::upgrade)
.map(SurfaceHandle::Embedded),
}
}
pub(super) fn handle(surface: Surface) -> Option<SurfaceHandle> {
COMPARE.with(|cell| handle_for(&cell.borrow(), surface))
}
pub(super) fn set_main_window(ui: &MainWindow) {
COMPARE.with(|cell| cell.borrow_mut().ui = Some(ui.as_weak()));
}
pub(super) fn window() -> Option<CompareWindow> {
COMPARE.with(|cell| {
cell.borrow()
.window
.as_ref()
.map(ComponentHandle::clone_strong)
})
}
pub(super) fn store_window(window: &CompareWindow) {
COMPARE.with(|cell| cell.borrow_mut().window = Some(window.clone_strong()));
}
pub(super) fn with_live<T>(f: impl FnOnce(&mut LiveSession) -> T) -> Option<(SurfaceHandle, T)> {
COMPARE.with(|cell| {
let mut host = cell.borrow_mut();
let surface = host.live.as_ref()?.surface;
let handle = handle_for(&host, surface)?;
let live = host.live.as_mut()?;
Some((handle, f(live)))
})
}
pub(super) fn with_live_on<T>(
surface: Surface,
f: impl FnOnce(&mut LiveSession) -> T,
) -> Option<(SurfaceHandle, T)> {
let (handle, result) = with_live(|live| (live.surface == surface).then(|| f(live)))?;
result.map(|result| (handle, result))
}
pub(super) fn live_kind() -> Option<(Surface, CompareOrigin)> {
with_live(|live| (live.surface, live.origin)).map(|(_, kind)| kind)
}
pub(super) fn keep_guard() -> Option<KeepGuard> {
with_live_on(Surface::Window, |live| {
live.ready
.as_ref()
.is_some_and(|ready| ready.session.can_keep())
.then(|| live.guard.clone())
})
.and_then(|(_, guard)| guard)
}
pub(super) fn replace_live(new: NewLive) -> (u64, Option<LiveSession>) {
COMPARE.with(|cell| {
let mut host = cell.borrow_mut();
host.next_id = host.next_id.wrapping_add(1);
let live = LiveSession {
id: host.next_id,
surface: new.surface,
origin: new.origin,
guard: new.guard,
renderer: new.renderer,
size_px: new.size_px,
drag_held: false,
size_settling: false,
settle_polls: 0,
seed_pose: None,
ready: None,
};
(host.next_id, host.live.replace(live))
})
}
pub(super) fn seed_pose(id: u64, pose: CameraPose) {
COMPARE.with(|cell| {
if let Some(live) = cell.borrow_mut().live.as_mut().filter(|live| live.id == id) {
live.seed_pose = Some(pose);
}
});
}
pub(super) fn take_retained_pose() -> Option<CameraPose> {
COMPARE.with(|cell| cell.borrow_mut().retained_pose.take())
}
fn clear_retained_pose() {
COMPARE.with(|cell| cell.borrow_mut().retained_pose = None);
}
pub(super) fn reset_surface(handle: &SurfaceHandle, status: &str) {
handle.with_model(|model| {
model.set_is_open(false);
model.set_embedded_open(false);
model.set_before_image(Image::default());
model.set_after_image(Image::default());
model.set_overlay_image(Image::default());
model.set_status(status.into());
clear_strip(&model);
});
}
fn push_strip(model: &CompareModel<'_>, strip: &StripText) {
let rows: Vec<CompareMetricRow> = strip
.rows
.iter()
.map(|row| CompareMetricRow {
label: row.label.into(),
before: row.before.as_str().into(),
after: row.after.as_str().into(),
change: row.change.as_str().into(),
verdict: row.tone.word().into(),
tone: row.tone.code(),
})
.collect();
model.set_metric_rows(ModelRc::new(VecModel::from(rows)));
model.set_metric_summary(strip.summary.as_str().into());
model.set_metric_note(strip.note.as_str().into());
model.set_tilt_available(strip.tilt_available);
model.set_tilt_running(strip.tilt_running);
model.set_tilt_done(strip.tilt_done);
model.set_tilt_progress(strip.tilt_progress);
}
pub(super) fn clear_strip(model: &CompareModel<'_>) {
push_strip(model, &StripText::default());
}
fn take_live_on(surface: Surface) -> Option<LiveSession> {
COMPARE.with(|cell| {
let mut host = cell.borrow_mut();
if host
.live
.as_ref()
.is_some_and(|live| live.surface == surface)
{
host.live.take()
} else {
None
}
})
}
pub(super) fn close_window(hide: bool) -> Option<CompareOrigin> {
let window = handle(Surface::Window)?;
let old = take_live_on(Surface::Window);
let origin = old.as_ref().map(|live| live.origin);
drop(old);
reset_surface(&window, "");
if hide
&& let SurfaceHandle::Window(window) = &window
&& let Err(error) = window.hide()
{
warn!("Could not hide the compare window: {error}");
}
origin
}
pub(super) fn drop_embedded(status: &str) {
let dropped = take_live_on(Surface::Embedded);
if let Some(pose) = dropped.as_ref().and_then(LiveSession::pose) {
COMPARE.with(|cell| cell.borrow_mut().retained_pose = Some(pose));
}
drop(dropped);
if let Some(pane) = handle(Surface::Embedded) {
reset_surface(&pane, status);
}
}
pub(super) fn close_embedded_session() {
if live_kind() == Some((Surface::Window, CompareOrigin::Retarget)) {
close_window(true);
}
drop_embedded("");
clear_retained_pose();
}
pub(super) fn set_size(surface: Surface, size_px: (u32, u32)) -> bool {
with_live_on(surface, |live| {
live.size_px = size_px;
live.size_settling = false;
live.settle_polls = 0;
})
.is_some()
}
pub(super) fn set_renderer(surface: Surface, renderer: Renderer) -> bool {
with_live_on(surface, |live| live.renderer = renderer).is_some()
}
pub(super) fn layout_switched(surface: Surface, renderer: Renderer) -> bool {
with_live_on(surface, |live| {
live.renderer = renderer;
live.size_settling = true;
live.settle_polls = 0;
})
.is_some()
}
fn post_frame(msg: FrameMsg) {
let _ = slint::invoke_from_event_loop(move || deliver_frame(msg));
}
struct FrameUpdate {
images: Option<(Pixels, Pixels)>,
overlay: Option<Pixels>,
status: String,
}
fn overlay_pixels(overlay: &[u8], size: (u32, u32)) -> Option<Pixels> {
let expected = (size.0 as usize) * (size.1 as usize) * 4;
if size.0 == 0 || size.1 == 0 || overlay.len() != expected {
return None;
}
let mut pixels = Pixels::new(size.0, size.1);
pixels.make_mut_bytes().copy_from_slice(overlay);
Some(pixels)
}
fn worker_failed_text(worker: &str) -> String {
format!(
"The {worker} preview stopped after an internal error. Change the view to try again, or close and reopen the compare window to restart it."
)
}
fn accept_frame(live: &mut LiveSession, msg: FrameMsg) -> Option<FrameUpdate> {
if live.id != msg.session_id {
return None;
}
let renderer = live.renderer;
let ready = live.ready.as_mut()?;
let current = msg.generation == ready.frames.generation();
let mut update = FrameUpdate {
images: None,
overlay: None,
status: String::new(),
};
match msg.event {
FrameEvent::Solid {
before,
after,
overlay,
} => {
if current {
update.overlay = overlay_pixels(&overlay, (before.width(), before.height()));
}
if ready
.frames
.accept(FrameKind::Solid, msg.generation, renderer)
{
update.images = Some((before, after));
}
}
FrameEvent::TracedProgress { side } => {
if current && renderer == Renderer::Traced {
ready.traced_progress = TracedProgress::Rendering { side };
}
}
FrameEvent::Traced { before, after, spp } => {
if ready
.frames
.accept(FrameKind::Traced, msg.generation, renderer)
{
ready.traced_progress = TracedProgress::Done;
ready.traced_spp = spp;
update.images = Some((before, after));
}
}
FrameEvent::WorkerFailed { worker } => {
update.status = worker_failed_text(worker);
return Some(update);
}
}
update.status = current_status(live);
Some(update)
}
fn deliver_frame(msg: FrameMsg) {
let Some((handle, Some(update))) = with_live(|live| accept_frame(live, msg)) else {
return;
};
handle.with_model(|model| {
if let Some((before, after)) = update.images {
model.set_before_image(Image::from_rgba8(before));
model.set_after_image(Image::from_rgba8(after));
}
if let Some(overlay) = update.overlay
&& model.get_mode_index() == MODE_OVERLAY
{
model.set_overlay_image(Image::from_rgba8(overlay));
}
model.set_status(update.status.into());
});
}
pub(super) fn request_frames(traced_delay: Duration) {
let status = with_live(|live| {
let size = live.size_px;
let renderer = live.renderer;
if let Some(ready) = live.ready.as_mut()
&& size.0 > 0
&& size.1 > 0
{
let generation = ready.frames.bump();
ready.latest.store(generation, Ordering::Release);
ready.solid.submit(ViewRequest {
generation,
pose: ready.session.pose,
size,
});
if renderer == Renderer::Traced {
ready.traced_progress = TracedProgress::Waiting;
ready
.traced_timer
.start(TimerMode::SingleShot, traced_delay, submit_traced);
} else {
ready.traced_timer.stop();
}
}
current_status(live)
});
if let Some((handle, status)) = status {
handle.with_model(|model| model.set_status(status.into()));
}
}
fn submit_traced() {
let status = with_live(|live| {
let size = live.size_px;
if live.renderer == Renderer::Traced
&& session::traced_waits_for_layout(live.size_settling, live.settle_polls)
&& let Some(ready) = live.ready.as_mut()
{
live.settle_polls += 1;
ready.traced_timer.start(
TimerMode::SingleShot,
session::SIZE_SETTLE_POLL,
submit_traced,
);
} else if live.renderer == Renderer::Traced
&& session::traced_may_start(live.drag_held)
&& let Some(ready) = live.ready.as_mut()
{
live.size_settling = false;
ready.traced.submit(ViewRequest {
generation: ready.frames.generation(),
pose: ready.session.pose,
size,
});
ready.traced_progress = TracedProgress::Rendering { side: 1 };
}
current_status(live)
});
if let Some((handle, status)) = status {
handle.with_model(|model| model.set_status(status.into()));
}
}
pub(super) fn set_drag(surface: Surface, held: bool) {
with_live_on(surface, |live| {
live.drag_held = held;
let renderer = live.renderer;
if !held
&& renderer == Renderer::Traced
&& let Some(ready) = live.ready.as_mut()
&& ready.traced_progress == TracedProgress::Waiting
{
ready
.traced_timer
.start(TimerMode::SingleShot, TRACED_DEBOUNCE, submit_traced);
}
});
}
pub(super) fn change_pose(surface: Surface, change: impl FnOnce(CameraPose, f64) -> CameraPose) {
let changed = with_live_on(surface, |live| {
live.ready.as_mut().map(|ready| {
ready.session.pose = change(ready.session.pose, ready.session.radius);
})
});
if matches!(changed, Some((_, Some(())))) {
request_frames(TRACED_DEBOUNCE);
}
}
pub(super) fn on_session_built(id: u64, session: CompareSession, metrics: SessionMetrics) {
let sides = || {
(
SideGeometry::from_side(&session.before),
SideGeometry::from_side(&session.after),
)
};
let latest = Arc::new(AtomicU64::new(0));
let solid = render::spawn_solid_worker(id, sides(), Arc::clone(&latest), post_frame);
let traced = render::spawn_traced_worker(id, sides(), Arc::clone(&latest), post_frame);
let can_keep = session.can_keep();
let labels = (session.before.label.clone(), session.after.label.clone());
let strip = metrics.strip();
let stored = with_live(|live| {
if live.id != id {
return false;
}
live.ready = Some(ReadySession {
session,
frames: FrameBook::default(),
latest,
solid,
traced,
traced_timer: Timer::default(),
traced_progress: TracedProgress::Done,
traced_spp: [TRACED_SPP; 2],
metrics,
});
true
});
let Some((handle, true)) = stored else {
return;
};
handle.with_model(|model| {
model.set_can_keep(can_keep);
model.set_before_label(labels.0.into());
model.set_after_label(labels.1.into());
push_strip(&model, &strip);
});
request_frames(Duration::ZERO);
}
pub(super) fn start_tilt(surface: Surface) {
let started = with_live_on(surface, |live| {
let id = live.id;
let ready = live.ready.as_mut()?;
let request = ready.metrics.begin_tilt(&ready.session)?;
Some((id, request, ready.metrics.strip()))
});
let Some((handle, Some((id, request, strip)))) = started else {
return;
};
let run = request.run;
handle.with_model(|model| push_strip(&model, &strip));
if let Err(reason) = metrics::spawn_tilt(id, request, post_tilt) {
warn!("{reason}");
let failed = with_live(|live| {
let ready = live.ready.as_mut().filter(|_| live.id == id)?;
ready
.metrics
.tilt_failed_to_start(run, reason)
.then(|| ready.metrics.strip())
});
if let Some((handle, Some(strip))) = failed {
handle.with_model(|model| push_strip(&model, &strip));
}
}
}
pub(super) fn cancel_tilt(surface: Surface) {
let cancelled = with_live_on(surface, |live| {
let ready = live.ready.as_mut()?;
ready.metrics.cancel_tilt().then(|| ready.metrics.strip())
});
if let Some((handle, Some(strip))) = cancelled {
handle.with_model(|model| push_strip(&model, &strip));
}
}
fn post_tilt(report: TiltReport) {
let _ = slint::invoke_from_event_loop(move || deliver_tilt(report));
}
enum TiltUpdate {
Progress(f32),
Strip(Box<StripText>),
}
fn deliver_tilt(report: TiltReport) {
let TiltReport {
session_id,
run,
event,
} = report;
let update = with_live(|live| {
if live.id != session_id {
return None;
}
let ready = live.ready.as_mut()?;
match event {
TiltEvent::Progress { done } => ready
.metrics
.tilt_progress(run, done)
.then(|| TiltUpdate::Progress(ready.metrics.progress_fraction())),
TiltEvent::Finished(outcome) => ready
.metrics
.tilt_finished(run, &outcome)
.then(|| TiltUpdate::Strip(Box::new(ready.metrics.strip()))),
}
});
let Some((handle, Some(update))) = update else {
return;
};
handle.with_model(|model| match update {
TiltUpdate::Progress(fraction) => model.set_tilt_progress(fraction),
TiltUpdate::Strip(strip) => push_strip(&model, &strip),
});
}
#[cfg(test)]
mod tests {
use super::worker_failed_text;
#[test]
fn the_failure_text_names_the_preview_and_how_to_restart_it() {
for worker in ["solid", "traced"] {
let text = worker_failed_text(worker);
assert!(text.starts_with(&format!("The {worker} preview stopped")));
assert!(text.contains("close and reopen the compare window"));
}
}
}