use crate::{
BatchModel,
bridge::{
preview_render::{BATCH_TILT_LIGHTING_PRESET, PREVIEW_LIGHT_PITCH, PREVIEW_LIGHT_YAW},
remote::remote_render,
},
gui::{
batch::{
batch_queue::WorkQueue,
material_choice::ensure_balanced_material,
preview::{RI_MATCH_TOLERANCE, target_ri_for_design},
remote_dispatch::{DispatcherGroup, RemoteStatus},
},
progress_eta::{EtaEstimator, batch_eta_label},
},
settings::WorkerSettings,
};
use indicatrix::{
color::metrics::{
PROFILE_AZIMUTHS_DEG, SweepProgress, evaluate_all_axes_profiles_stepped,
evaluate_full_axis_profile_at_azimuth,
},
geometry::plane::GpuFacetPlane,
optics::{
fluorescence::Fluorescence,
materials::GemMaterial,
raytracer::{DEFAULT_MAX_BOUNCES, EnvironmentSource},
},
};
use indicatrix_net::{
SceneState,
messages::{AxisTiltCurves as WireAxisTiltCurves, TiltCurvesRequest, TiltCurvesResponse},
};
use indicatrix_vault::model::tilt_curves::{
AxisTiltCurves as StorageAxisTiltCurves, TILT_CURVE_AXIS_COUNT, TILT_CURVE_POINTS_PER_AXIS,
TiltPerformanceCurves,
};
use slint::{ComponentHandle, Weak};
use std::{
any::Any,
collections::BTreeSet,
sync::{
Mutex, PoisonError,
atomic::{AtomicBool, AtomicU32, Ordering},
},
thread,
time::Duration,
};
use tracing::warn;
mod save;
use super::remote_lane::run_remote_lane;
use save::save_curves;
pub use save::save_tilt_curves_for_entry;
const REMOTE_SCENE_MAX_BOUNCES: u32 = DEFAULT_MAX_BOUNCES;
const fn batch_environment() -> EnvironmentSource<'static> {
BATCH_TILT_LIGHTING_PRESET.studio(1.0, PREVIEW_LIGHT_YAW, PREVIEW_LIGHT_PITCH)
}
const TILT_REQUEST_ID: u32 = 1;
const LOCAL_IDLE_POLL: Duration = Duration::from_millis(15);
pub(super) struct BatchContext<'a> {
pub(super) db: &'a Mutex<indicatrix_vault::db::sqlite::Database>,
pub(super) material_candidates:
&'a [indicatrix_vault::model::material_match::RiPresetCandidate],
pub(super) angle_table_entries: &'a Mutex<BTreeSet<i64>>,
}
struct ResolvedDesign {
title: String,
planes: Vec<GpuFacetPlane>,
material: GemMaterial,
material_name: String,
updated_at: Option<i64>,
}
pub(super) fn record_planes(
ctx: &BatchContext<'_>,
full: &indicatrix_vault::model::entry::FullDiagramRecord,
) -> Option<Vec<GpuFacetPlane>> {
super::super::record_planes_for_batch(full, ctx.angle_table_entries)
}
fn resolve_design(ctx: &BatchContext<'_>, entry_id: i64) -> Option<ResolvedDesign> {
let (full, updated_at) = {
let guard = ctx.db.lock().unwrap_or_else(PoisonError::into_inner);
(
guard.get_diagram_full(entry_id),
guard.entry_updated_at(entry_id),
)
};
let Ok(Some(full)) = full else {
return None;
};
let Ok(updated_at) = updated_at else {
return None;
};
let planes = record_planes(ctx, &full)?;
let target_ri = target_ri_for_design(&full);
let material_name = {
let guard = ctx.db.lock().unwrap_or_else(PoisonError::into_inner);
ensure_balanced_material(
&guard,
entry_id,
target_ri,
ctx.material_candidates,
RI_MATCH_TOLERANCE,
&planes,
)
};
let Ok(Some(material_name)) = material_name else {
return None;
};
let material = GemMaterial::by_name(&material_name)?;
Some(ResolvedDesign {
title: full.title,
planes,
material,
material_name,
updated_at,
})
}
fn wire_axis_to_storage(axis: WireAxisTiltCurves) -> Result<StorageAxisTiltCurves, String> {
let to_array = |v: Vec<f32>, name: &str| -> Result<[f32; TILT_CURVE_POINTS_PER_AXIS], String> {
v.try_into().map_err(|v: Vec<f32>| {
format!(
"{name} has {} points, expected {TILT_CURVE_POINTS_PER_AXIS}",
v.len()
)
})
};
Ok(StorageAxisTiltCurves {
brilliance_pct: to_array(axis.brilliance_pct, "brilliance_pct")?,
extinction_pct: to_array(axis.extinction_pct, "extinction_pct")?,
windowing_pct: to_array(axis.windowing_pct, "windowing_pct")?,
})
}
fn fetch_tilt_curves_remote(
worker: &WorkerSettings,
planes: &[GpuFacetPlane],
material: &GemMaterial,
cancel: &AtomicBool,
) -> Option<TiltPerformanceCurves> {
if cancel.load(Ordering::Relaxed) {
return None;
}
let scene = SceneState {
width: 1,
height: 1,
yaw: 0.0,
pitch: 0.0,
distance: 1.0,
light_yaw: PREVIEW_LIGHT_YAW,
light_pitch: PREVIEW_LIGHT_PITCH,
exposure: 1.0,
max_bounces: REMOTE_SCENE_MAX_BOUNCES,
lighting_preset: BATCH_TILT_LIGHTING_PRESET,
material: material.clone(),
planes: planes.to_vec(),
girdle_frosted: false,
backdrop: 0.0,
environment: indicatrix_net::scene::SceneEnvironment::Studio,
surface_glare: 1.0,
tools: Vec::new(),
fluorescence: Fluorescence::default(),
head_shadow_deg: 16.0,
};
let (mut stream, welcome) = remote_render::connect_and_handshake(worker).ok()?;
if !welcome.tilt_curves {
return None;
}
let request = TiltCurvesRequest {
request_id: TILT_REQUEST_ID,
scene,
};
indicatrix_net::client::send_tilt_curves_request(&mut stream, &request).ok()?;
match indicatrix_net::client::recv_tilt_curves_response(&mut stream) {
Ok(TiltCurvesResponse::Curves(result)) => {
let mut axes_vec = Vec::with_capacity(result.axes.len());
for axis in result.axes {
match wire_axis_to_storage(axis) {
Ok(axis) => axes_vec.push(axis),
Err(e) => {
warn!("Remote tilt-curve reply had a malformed axis: {e}");
return None;
}
}
}
let axes: [StorageAxisTiltCurves; TILT_CURVE_AXIS_COUNT] = axes_vec.try_into().ok()?;
Some(TiltPerformanceCurves { axes })
}
Ok(TiltCurvesResponse::Cancelled { .. } | TiltCurvesResponse::Error(_)) | Err(_) => None,
}
}
fn compute_tilt_curves_locally(
planes: &[GpuFacetPlane],
material: &GemMaterial,
cancel: &AtomicBool,
) -> Option<TiltPerformanceCurves> {
let mut axes = Vec::with_capacity(PROFILE_AZIMUTHS_DEG.len());
for &azimuth_deg in &PROFILE_AZIMUTHS_DEG {
if cancel.load(Ordering::Relaxed) {
return None;
}
let (brilliance_pct, extinction_pct, windowing_pct) = evaluate_full_axis_profile_at_azimuth(
planes,
material,
azimuth_deg,
batch_environment(),
);
axes.push(StorageAxisTiltCurves {
brilliance_pct,
extinction_pct,
windowing_pct,
});
}
let axes: [StorageAxisTiltCurves; TILT_CURVE_AXIS_COUNT] = axes.try_into().ok()?;
Some(TiltPerformanceCurves { axes })
}
fn compute_tilt_curves_locally_stepped(
planes: &[GpuFacetPlane],
material: &GemMaterial,
cancel: &AtomicBool,
on_step: &mut dyn FnMut(SweepProgress),
) -> Option<TiltPerformanceCurves> {
let profiles = evaluate_all_axes_profiles_stepped(
planes,
material,
batch_environment(),
&mut |progress| {
on_step(progress);
!cancel.load(Ordering::Relaxed)
},
)?;
let axes: Vec<StorageAxisTiltCurves> = profiles
.into_iter()
.map(|profile| StorageAxisTiltCurves {
brilliance_pct: profile.brilliance,
extinction_pct: profile.extinction,
windowing_pct: profile.windowing,
})
.collect();
let axes: [StorageAxisTiltCurves; TILT_CURVE_AXIS_COUNT] = axes.try_into().ok()?;
Some(TiltPerformanceCurves { axes })
}
fn process_local_entry(ctx: &BatchContext<'_>, entry_id: i64, cancel: &AtomicBool) -> bool {
let Some(resolved) = resolve_design(ctx, entry_id) else {
return false;
};
let Some(curves) = compute_tilt_curves_locally(&resolved.planes, &resolved.material, cancel)
else {
return false;
};
save_curves(
ctx.db,
entry_id,
&curves,
Some(&resolved.material_name),
resolved.updated_at,
)
}
pub(super) fn process_remote_entry(
ctx: &BatchContext<'_>,
worker: Option<&WorkerSettings>,
entry_id: i64,
cancel: &AtomicBool,
mut on_title: impl FnMut(&str),
) -> bool {
let Some(resolved) = resolve_design(ctx, entry_id) else {
return false;
};
on_title(&resolved.title);
let Some(worker) = worker else {
return false;
};
let Some(curves) =
fetch_tilt_curves_remote(worker, &resolved.planes, &resolved.material, cancel)
else {
return false;
};
save_curves(
ctx.db,
entry_id,
&curves,
Some(&resolved.material_name),
resolved.updated_at,
)
}
pub(super) fn panic_message(payload: &(dyn Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string())
}
#[derive(Default)]
pub(super) struct Tally {
pub(super) computed: AtomicU32,
pub(super) failed: AtomicU32,
}
#[derive(Default, Clone)]
pub(super) struct LiveProgress {
completed: u32,
local_active: u32,
remote: RemoteStatus,
eta: EtaEstimator,
}
pub(super) fn push_progress(
ui_weak: &Weak<crate::MainWindow>,
design_total: u32,
local_lane_total: u32,
progress: &Mutex<LiveProgress>,
) {
let (snapshot, eta_text) = {
let mut p = progress.lock().unwrap_or_else(PoisonError::into_inner);
let now = std::time::Instant::now();
if design_total > 0 {
let fraction = f64::from(p.completed) / f64::from(design_total);
p.eta.observe(now, fraction);
}
let eta_text = batch_eta_label(p.completed, design_total, p.eta.eta(now));
(p.clone(), eta_text)
};
let ui_weak = ui_weak.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
ui.global::<BatchModel>().set_tilt_eta(eta_text.into());
ui.global::<BatchModel>()
.set_tilt_design_index(snapshot.completed as i32);
ui.global::<BatchModel>()
.set_tilt_design_total(design_total as i32);
ui.global::<BatchModel>()
.set_tilt_local_active(snapshot.local_active as i32);
ui.global::<BatchModel>()
.set_tilt_local_lane_total(local_lane_total as i32);
ui.global::<BatchModel>()
.set_tilt_remote_title(snapshot.remote.title().into());
ui.global::<BatchModel>()
.set_tilt_remote_active(snapshot.remote.is_active());
ui.global::<BatchModel>()
.set_tilt_remote_in_flight(snapshot.remote.in_flight() as i32);
});
}
pub(super) fn update_remote(
progress: &Mutex<LiveProgress>,
change: impl FnOnce(&mut RemoteStatus),
) {
change(
&mut progress
.lock()
.unwrap_or_else(PoisonError::into_inner)
.remote,
);
}
pub(super) fn increment_completed(progress: &Mutex<LiveProgress>) {
progress
.lock()
.unwrap_or_else(PoisonError::into_inner)
.completed += 1;
}
fn adjust_local_active(progress: &Mutex<LiveProgress>, delta: i32) {
let mut p = progress.lock().unwrap_or_else(PoisonError::into_inner);
p.local_active = p.local_active.saturating_add_signed(delta);
}
pub(super) struct LaneShared<'a> {
pub(super) ctx: &'a BatchContext<'a>,
pub(super) queue: &'a WorkQueue<i64>,
pub(super) tally: &'a Tally,
pub(super) progress: &'a Mutex<LiveProgress>,
pub(super) cancel: &'a AtomicBool,
pub(super) design_total: u32,
pub(super) local_lane_total: u32,
pub(super) remote_lane_total: u32,
}
pub(super) fn run_local_lane(
shared: &LaneShared<'_>,
ui_weak: &Weak<crate::MainWindow>,
remote_lane_done: &AtomicBool,
) {
loop {
if shared.cancel.load(Ordering::Relaxed) {
break;
}
let remote_finished = remote_lane_done.load(Ordering::Acquire);
let Some(entry_id) = shared.queue.claim_local() else {
if remote_finished {
break;
}
thread::sleep(LOCAL_IDLE_POLL);
continue;
};
adjust_local_active(shared.progress, 1);
push_progress(
ui_weak,
shared.design_total,
shared.local_lane_total,
shared.progress,
);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
process_local_entry(shared.ctx, entry_id, shared.cancel)
}));
let saved = match result {
Ok(saved) => saved,
Err(payload) => {
warn!(
"Tilt-curve computation panicked for entry {entry_id}: {}",
panic_message(&*payload)
);
false
}
};
if saved {
shared.tally.computed.fetch_add(1, Ordering::Relaxed);
} else {
shared.tally.failed.fetch_add(1, Ordering::Relaxed);
}
adjust_local_active(shared.progress, -1);
increment_completed(shared.progress);
push_progress(
ui_weak,
shared.design_total,
shared.local_lane_total,
shared.progress,
);
}
}
#[must_use]
pub fn tilt_curves_for_planes(
planes: &[GpuFacetPlane],
material: &GemMaterial,
worker: Option<&WorkerSettings>,
cancel: &AtomicBool,
on_step: &mut dyn FnMut(SweepProgress),
) -> Option<TiltPerformanceCurves> {
if let Some(worker) = worker
&& let Some(curves) = fetch_tilt_curves_remote(worker, planes, material, cancel)
{
return Some(curves);
}
compute_tilt_curves_locally_stepped(planes, material, cancel, on_step)
}
pub(super) fn run_batch_lanes(
shared: &LaneShared<'_>,
plan: super::super::batch_queue::LanePlan,
remote_worker: Option<&WorkerSettings>,
local_lane_total: u32,
ui_weak: &Weak<crate::MainWindow>,
remote_lane_done: &AtomicBool,
) {
let dispatchers = DispatcherGroup::new(shared.remote_lane_total as usize, remote_lane_done);
let dispatchers = &dispatchers;
std::thread::scope(|scope| {
if plan.run_remote {
for _ in 0..shared.remote_lane_total {
let remote_ui_weak = ui_weak.clone();
scope.spawn(move || {
run_remote_lane(
shared,
&remote_ui_weak,
remote_worker,
plan.fallback_to_local,
dispatchers,
);
});
}
}
if plan.run_local {
for _ in 0..local_lane_total {
let local_ui_weak = ui_weak.clone();
scope.spawn(move || {
run_local_lane(shared, &local_ui_weak, remote_lane_done);
});
}
}
});
}