use super::{
engine::{
BatchContext, DesignAccum, LaneShared, LiveProgress, Tally, build_items, run_batch_lanes,
},
import_choice::{remember_choice, show_saved_choice},
scan::spawn_missing_preview_scan,
};
use crate::{
BatchModel, LibraryModel, MainWindow, SettingsModel,
bridge::{preview_render, render_thread::RenderContext},
gui::batch::{
batch_queue::{LanePlan, WorkQueue, local_lane_count, remote_lane_count},
preview_cache::PreviewThumbnailCache,
regenerate_all::{
ScopeKind, all_choice_label, chosen_ids, missing_among, missing_choice_label,
next_scope_generation, scope_is_current,
},
remote_lanes_setting::setup_remote_batch_lanes,
},
settings::{ImportPreviewChoice, LiveComputeTarget, SettingsPersister, WorkerSettings},
};
use indicatrix::renderer::gpu_backend::GpuBackend;
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, Model, Weak};
use std::{
cell::RefCell,
collections::{BTreeSet, HashMap},
rc::Rc,
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreviewMode {
Full,
Solid,
}
pub struct PreviewBatchHandle {
cancel: Arc<AtomicBool>,
}
impl PreviewBatchHandle {
pub fn cancel(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PreviewBatchOutcome {
pub generated: u32,
pub failed: u32,
pub angle_table: usize,
pub cancelled: bool,
}
pub struct PreviewBatchSettings {
pub worker: Option<WorkerSettings>,
pub live_compute_target: LiveComputeTarget,
pub preview_size: u32,
pub preview_spp: u32,
pub remote_batch_lanes: u32,
pub mode: PreviewMode,
pub thumbnail_cache: PreviewThumbnailCache,
}
struct BusyGuard {
render_ctx: Arc<Mutex<RenderContext>>,
ui_weak: Weak<MainWindow>,
}
impl Drop for BusyGuard {
fn drop(&mut self) {
RenderContext::lock(&self.render_ctx).export_active_count -= 1;
let ui_weak = self.ui_weak.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
ui.global::<BatchModel>().set_preview_batch_running(false);
});
}
}
fn push_summary(ui_weak: &Weak<MainWindow>, outcome: PreviewBatchOutcome, solid: bool) {
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
ui.global::<BatchModel>().set_preview_summary(
format!(
"Generated {}previews for {} design(s){}{}{}.",
if solid { "solid " } else { "" },
outcome.generated,
if outcome.failed > 0 {
format!(", {} failed", outcome.failed)
} else {
String::new()
},
super::super::angle_table_summary(outcome.angle_table),
if outcome.cancelled {
" (cancelled)"
} else {
""
}
)
.into(),
);
ui.global::<BatchModel>().set_preview_done(true);
});
}
pub fn spawn_preview_batch(
ui_weak: Weak<MainWindow>,
db: Arc<Mutex<Database>>,
render_ctx: Arc<Mutex<RenderContext>>,
settings: PreviewBatchSettings,
entry_ids: Vec<i64>,
batch_id: u64,
current_batch_id: Arc<AtomicU64>,
) -> PreviewBatchHandle {
let cancel = Arc::new(AtomicBool::new(false));
let cancel_thread = Arc::clone(&cancel);
thread::spawn(move || {
RenderContext::lock(&render_ctx).export_active_count += 1;
let _busy_guard = BusyGuard {
render_ctx: Arc::clone(&render_ctx),
ui_weak: ui_weak.clone(),
};
let _ = ui_weak
.upgrade_in_event_loop(|ui| ui.global::<BatchModel>().set_preview_batch_running(true));
let design_total = entry_ids.len() as u32;
let material_candidates = preview_render::ri_candidates();
let solid = settings.mode == PreviewMode::Solid;
let gpu = if solid {
GpuBackend::disabled()
} else {
GpuBackend::acquire()
};
let gpu_retired = AtomicBool::new(false);
let remote_worker = if solid { None } else { settings.worker };
let thumbnail_cache = settings.thumbnail_cache;
let angle_table_entries = Mutex::new(BTreeSet::new());
let ctx = BatchContext {
db: &db,
material_candidates: &material_candidates,
preview_size: settings.preview_size,
preview_spp: settings.preview_spp,
solid,
gpu_retired: &gpu_retired,
angle_table_entries: &angle_table_entries,
};
let queue = WorkQueue::new(build_items(&entry_ids));
let design_state: Mutex<HashMap<i64, DesignAccum>> = Mutex::new(HashMap::new());
let tally = Tally::default();
let progress = Mutex::new(LiveProgress::default());
let target = if solid {
LiveComputeTarget::LocalOnly
} else {
settings.live_compute_target
};
let plan = LanePlan::for_target(target, remote_worker.is_some());
let remote_lane_done = AtomicBool::new(!plan.run_remote);
let local_lane_total = if plan.run_local {
local_lane_count() as u32
} else {
0
};
let remote_lane_total = if plan.run_remote {
remote_lane_count(settings.remote_batch_lanes) as u32
} else {
0
};
let shared = LaneShared {
ctx: &ctx,
queue: &queue,
design_state: &design_state,
tally: &tally,
progress: &progress,
cancel: &cancel_thread,
design_total,
local_lane_total,
remote_lane_total,
sit_out_toasted: AtomicBool::new(false),
thumbnail_cache: &thumbnail_cache,
};
run_batch_lanes(
&shared,
&gpu,
plan,
remote_worker.as_ref(),
local_lane_total,
&ui_weak,
&remote_lane_done,
);
let outcome = PreviewBatchOutcome {
generated: tally.generated.load(Ordering::Relaxed),
failed: tally.failed.load(Ordering::Relaxed),
angle_table: super::super::angle_table_count(&angle_table_entries),
cancelled: cancel_thread.load(Ordering::Relaxed),
};
if current_batch_id.load(Ordering::SeqCst) == batch_id {
push_summary(&ui_weak, outcome, solid);
}
});
PreviewBatchHandle { cancel }
}
struct BatchSlots {
handle_slot: Rc<RefCell<Option<PreviewBatchHandle>>>,
current_batch_id: Arc<AtomicU64>,
thumbnail_cache: PreviewThumbnailCache,
}
fn start_batch(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
slots: &BatchSlots,
entry_ids: Vec<i64>,
mode: PreviewMode,
) {
let BatchSlots {
handle_slot,
current_batch_id,
thumbnail_cache,
} = slots;
if entry_ids.is_empty() {
return;
}
if ui.global::<BatchModel>().get_preview_batch_running() {
crate::gui::show_toast(
ui,
"A preview batch is already running -- wait for it to finish or cancel it \
first.",
"info",
);
return;
}
let snapshot = settings_store.snapshot();
ui.global::<BatchModel>().set_preview_visible(true);
ui.global::<BatchModel>().set_preview_confirming(false);
ui.global::<BatchModel>().set_preview_done(false);
ui.global::<BatchModel>().set_preview_batch_running(true);
ui.global::<BatchModel>().set_preview_design_index(0);
ui.global::<BatchModel>()
.set_preview_eta(String::new().into());
ui.global::<BatchModel>()
.set_preview_design_total(entry_ids.len() as i32);
ui.global::<BatchModel>().set_preview_local_active(0);
ui.global::<BatchModel>().set_preview_local_lane_total(0);
ui.global::<BatchModel>()
.set_preview_remote_title(String::new().into());
ui.global::<BatchModel>().set_preview_remote_active(false);
ui.global::<BatchModel>().set_preview_remote_in_flight(0);
ui.global::<BatchModel>()
.set_preview_summary(String::new().into());
let batch_id = current_batch_id.fetch_add(1, Ordering::SeqCst) + 1;
let handle = spawn_preview_batch(
ui.as_weak(),
Arc::clone(db),
Arc::clone(render_ctx),
PreviewBatchSettings {
worker: snapshot.settings.remote_worker(),
live_compute_target: snapshot.settings.live_compute_target,
preview_size: snapshot.settings.preview_size,
preview_spp: snapshot.settings.preview_spp,
remote_batch_lanes: snapshot.settings.remote_batch_lanes,
mode,
thumbnail_cache: thumbnail_cache.clone(),
},
entry_ids,
batch_id,
Arc::clone(current_batch_id),
);
*handle_slot.borrow_mut() = Some(handle);
}
struct ConfirmEnv {
db: Arc<Mutex<Database>>,
render_ctx: Arc<Mutex<RenderContext>>,
settings_store: Arc<SettingsPersister>,
slots: BatchSlots,
}
fn confirm_offer(ui: &MainWindow, env: &ConfirmEnv, mode: PreviewMode) {
let model = ui.global::<BatchModel>();
if model.get_preview_offer_from_import() && model.get_preview_remember_choice() {
let choice = match mode {
PreviewMode::Full => ImportPreviewChoice::Full,
PreviewMode::Solid => ImportPreviewChoice::Solid,
};
remember_choice(ui, &env.settings_store, choice);
}
let to_ids = |ids: slint::ModelRc<i32>| ids.iter().map(i64::from).collect::<Vec<i64>>();
let ids = chosen_ids(
model.get_preview_offer_regenerate(),
model.get_preview_offer_missing_only(),
to_ids(model.get_preview_offer_ids()),
to_ids(model.get_preview_offer_missing_ids()),
);
start_batch(
ui,
&env.db,
&env.render_ctx,
&env.settings_store,
&env.slots,
ids,
mode,
);
}
pub fn offer_batch_confirmation(ui: &MainWindow, ids: &[i64]) {
open_offer(ui, ids, false);
}
thread_local! {
static SCOPE_ENV: RefCell<Option<(Arc<Mutex<Database>>, Arc<SettingsPersister>)>> =
const { RefCell::new(None) };
}
pub fn offer_regeneration(ui: &MainWindow, ids: &[i64]) {
if ids.is_empty() {
return;
}
open_offer(ui, ids, false);
let model = ui.global::<BatchModel>();
model.set_preview_offer_regenerate(true);
model.set_preview_offer_missing_only(true);
model.set_preview_offer_missing_count(-1);
model.set_preview_offer_missing_ids(slint::ModelRc::default());
model.set_preview_offer_missing_label(missing_choice_label(None).into());
model.set_preview_offer_all_label(all_choice_label(ids.len()).into());
let Some((db, settings_store)) = SCOPE_ENV.with(|env| env.borrow().clone()) else {
return;
};
let generation = next_scope_generation(ScopeKind::Preview);
let settings = settings_store.snapshot().settings;
let all = ids.to_vec();
spawn_missing_preview_scan(
ui.as_weak(),
db,
settings.preview_size,
settings.preview_spp,
move |ui, missing| {
if !scope_is_current(ScopeKind::Preview, generation) {
return;
}
let missing = missing_among(&all, &missing);
let model = ui.global::<BatchModel>();
model.set_preview_offer_missing_count(missing.len() as i32);
model.set_preview_offer_missing_label(missing_choice_label(Some(missing.len())).into());
let missing_i32: Vec<i32> = missing.iter().map(|&id| id as i32).collect();
model.set_preview_offer_missing_ids(slint::ModelRc::new(slint::VecModel::from(
missing_i32,
)));
},
);
}
pub(super) fn open_offer(ui: &MainWindow, ids: &[i64], from_import: bool) {
if ids.is_empty() {
return;
}
let ids_i32: Vec<i32> = ids.iter().map(|&id| id as i32).collect();
ui.global::<BatchModel>()
.set_preview_offer_ids(slint::ModelRc::new(slint::VecModel::from(ids_i32)));
ui.global::<BatchModel>()
.set_preview_offer_count(ids.len() as i32);
ui.global::<BatchModel>()
.set_preview_offer_regenerate(false);
ui.global::<BatchModel>()
.set_preview_offer_from_import(from_import);
ui.global::<BatchModel>().set_preview_remember_choice(false);
ui.global::<BatchModel>().set_preview_confirming(true);
ui.global::<BatchModel>().set_preview_visible(true);
}
pub fn setup_preview_batch_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
thumbnail_cache: &PreviewThumbnailCache,
) {
SCOPE_ENV.with(|env| *env.borrow_mut() = Some((Arc::clone(db), Arc::clone(settings_store))));
let handle: Rc<RefCell<Option<PreviewBatchHandle>>> = Rc::new(RefCell::new(None));
let current_batch_id: Arc<AtomicU64> = Arc::new(AtomicU64::new(0));
let settings_store_size = Arc::clone(settings_store);
ui.global::<SettingsModel>()
.on_preview_size_changed(move |size| {
settings_store_size.update(|s| s.settings.preview_size = size.max(1) as u32);
});
let settings_store_spp = Arc::clone(settings_store);
ui.global::<SettingsModel>()
.on_preview_spp_changed(move |spp| {
settings_store_spp.update(|s| s.settings.preview_spp = spp.max(1) as u32);
});
setup_remote_batch_lanes(ui, settings_store);
let handle_cancel = Rc::clone(&handle);
ui.global::<BatchModel>().on_preview_cancel(move || {
if let Some(h) = handle_cancel.borrow().as_ref() {
h.cancel();
}
});
let ui_weak_close = ui.as_weak();
ui.global::<BatchModel>().on_preview_close(move || {
if let Some(ui) = ui_weak_close.upgrade() {
ui.global::<BatchModel>().set_preview_visible(false);
ui.global::<BatchModel>().set_preview_done(false);
crate::gui::batch::regenerate_all::preview_dialog_closed(&ui);
}
});
let settings_store_dismiss = Arc::clone(settings_store);
let ui_weak_dismiss = ui.as_weak();
ui.global::<BatchModel>().on_preview_dismiss_offer(move || {
if let Some(ui) = ui_weak_dismiss.upgrade() {
let model = ui.global::<BatchModel>();
if model.get_preview_offer_from_import() && model.get_preview_remember_choice() {
remember_choice(&ui, &settings_store_dismiss, ImportPreviewChoice::Skip);
}
model.set_preview_visible(false);
model.set_preview_confirming(false);
crate::gui::batch::regenerate_all::preview_dialog_closed(&ui);
}
});
let env = Rc::new(ConfirmEnv {
db: Arc::clone(db),
render_ctx: Arc::clone(render_ctx),
settings_store: Arc::clone(settings_store),
slots: BatchSlots {
handle_slot: Rc::clone(&handle),
current_batch_id: Arc::clone(¤t_batch_id),
thumbnail_cache: thumbnail_cache.clone(),
},
});
let (env_full, ui_weak_full) = (Rc::clone(&env), ui.as_weak());
ui.global::<BatchModel>()
.on_preview_generate_confirmed(move || {
if let Some(ui) = ui_weak_full.upgrade() {
confirm_offer(&ui, &env_full, PreviewMode::Full);
}
});
let (env_solid, ui_weak_solid) = (env, ui.as_weak());
ui.global::<BatchModel>()
.on_preview_generate_solid_confirmed(move || {
if let Some(ui) = ui_weak_solid.upgrade() {
confirm_offer(&ui, &env_solid, PreviewMode::Solid);
}
});
show_saved_choice(ui, settings_store);
let db_entry = Arc::clone(db);
let render_ctx_entry = Arc::clone(render_ctx);
let settings_store_entry = Arc::clone(settings_store);
let slots_entry = BatchSlots {
handle_slot: Rc::clone(&handle),
current_batch_id: Arc::clone(¤t_batch_id),
thumbnail_cache: thumbnail_cache.clone(),
};
let ui_weak_entry = ui.as_weak();
ui.global::<LibraryModel>()
.on_generate_previews_for_entry(move |id: i32| {
if let Some(ui) = ui_weak_entry.upgrade() {
start_batch(
&ui,
&db_entry,
&render_ctx_entry,
&settings_store_entry,
&slots_entry,
vec![i64::from(id)],
PreviewMode::Full,
);
}
});
let db_scan = Arc::clone(db);
let scan_settings = settings_store.snapshot().settings;
spawn_missing_preview_scan(
ui.as_weak(),
db_scan,
scan_settings.preview_size,
scan_settings.preview_spp,
|ui, ids| {
offer_batch_confirmation(ui, &ids);
},
);
}