mod context;
mod denoise;
mod display_thread;
mod frame_helpers;
mod gpu_backend;
mod local_preview;
mod metrics;
mod redraw_gate;
mod scanline;
pub use context::{
MaterialOverrides, MaterialSources, PlanesOwner, RenderContext, apply_material_overrides,
load_env_map, resolve_material, resolve_material_with_override,
};
pub use denoise::{
DenoiseScratch, FirstHitSnapshot, denoise_and_tonemap_frame, tonemap_running_average,
};
pub use metrics::hash_planes;
pub use redraw_gate::RedrawGate;
use crate::bridge::frame_cache::{girdle_finish::GirdleFinishCache, stone_width::StoneWidthCache};
use context::{FrameInputs, resolve_material_and_quality, snapshot_frame_inputs};
use display_thread::{FrameMetricsSnapshot, spawn_display_thread};
use glam::Vec3;
use gpu_backend::{
BackendFrame, FrameOutputs, HybridPacing, ViewportGpu, accumulate_frame_samples,
};
use indicatrix::optics::{
materials::GemMaterial,
raytracer::{Camera, EnvironmentSource, FacetFinish},
};
use metrics::{MetricsCache, compute_or_reuse_metrics};
use slint::Weak;
use std::{
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
};
const DENOISE_MIN_INTERVAL: Duration = Duration::from_millis(120);
const DISPLAY_BUSY_WAIT_TIMEOUT: Duration = Duration::from_secs(5);
use frame_helpers::{
AccumulationBuffers, FrameActivityFlags, SuspensionFlags, TraceActivitySink,
combined_sample_offset, push_metrics_to_ui, remote_suspends_local, resolve_remote_ownership,
should_combine_remote, update_accumulation_state,
};
#[expect(
clippy::too_many_lines,
reason = "the render worker thread's full loop (resize/dirty handling, sampling, \
progressive accumulation, UI callbacks); splitting it apart risks changing \
GUI render-thread behaviour verifiable only by launching the app"
)]
pub fn spawn_render_thread<T, F, M, S>(
ui_weak: Weak<T>,
ctx: Arc<Mutex<RenderContext>>,
update_image: F,
update_metrics: M,
update_gpu_status: S,
) where
T: TraceActivitySink + 'static,
F: Fn(&T, slint::SharedPixelBuffer<slint::Rgba8Pixel>) + Send + 'static + Clone,
M: Fn(&T, f32, f32, f32, f32, f32, [f32; 19], [f32; 19], [f32; 19], f32)
+ Send
+ 'static
+ Clone,
S: Fn(&T, slint::SharedString) + Send + 'static + Clone,
{
thread::spawn(move || {
let mut last_width = 0;
let mut last_height = 0;
let materials = GemMaterial::all_materials();
let mut accum_buffer: Vec<Vec3> = Vec::new();
let mut first_hit_depth: Vec<f32> = Vec::new();
let mut first_hit_normal: Vec<Vec3> = Vec::new();
let mut first_hit_facet_id: Vec<i32> = Vec::new();
let mut combined_scratch: Vec<Vec3> = Vec::new();
let mut last_displayed_remote_samples: u32 = 0;
let mut accum_samples: u32 = 0;
let mut last_denoise_at: Option<Instant> = None;
let mut metrics_cache: Option<MetricsCache> = None;
let mut gpu_backend = ViewportGpu::acquire();
let mut last_gpu_status: Option<String> = None;
let mut hybrid_pacing = HybridPacing::new();
let mut girdle_cache = GirdleFinishCache::new();
let mut stone_width_cache = StoneWidthCache::new();
let ui_weak_metrics_only = ui_weak.clone();
let update_metrics_metrics_only = update_metrics.clone();
let display = spawn_display_thread(ui_weak, update_image, update_metrics);
let mut prev_remote_active = false;
loop {
let FrameInputs {
width,
height,
yaw,
pitch,
distance,
light_yaw,
light_pitch,
material_name,
material_override,
material_unresolved,
lighting_preset,
backdrop,
target_samples,
max_bounces,
exposure,
inclusion_sigma_s,
c_axis_override,
girdle_frosted,
edge_rounding_radius,
stone_width_mm,
active_planes,
custom_materials,
running,
dirty,
paused,
tab_visible,
denoise_enabled,
remote_active: remote_active_snapshot,
remote_accumulator,
remote_reserved_samples,
export_active,
live_compute_target,
local_compute_target,
local_preview_scale,
camera_moving,
env_map,
} = snapshot_frame_inputs(&ctx);
if !running {
break;
}
let remote_active =
resolve_remote_ownership(dirty, remote_active_snapshot, prev_remote_active);
if remote_active != remote_active_snapshot {
ctx.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remote_active = false;
}
prev_remote_active = remote_active;
let remote_samples_done_now: u32 =
if should_combine_remote(remote_active, live_compute_target) {
remote_accumulator.as_ref().map_or(0, |acc| {
acc.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.samples_done()
})
} else {
0
};
if width == 0 || height == 0 {
thread::sleep(std::time::Duration::from_millis(16));
continue;
}
let (width, height) = local_preview::effective_dimensions(
width,
height,
local_preview_scale,
camera_moving && !remote_active,
);
let dimensions_changed = width != last_width || height != last_height;
let accumulation_reset = dirty || dimensions_changed;
update_accumulation_state(
width,
height,
dirty,
&mut AccumulationBuffers {
accum: &mut accum_buffer,
first_hit_depth: &mut first_hit_depth,
first_hit_normal: &mut first_hit_normal,
first_hit_facet_id: &mut first_hit_facet_id,
},
&mut accum_samples,
&mut last_width,
&mut last_height,
);
if dimensions_changed {
gpu_backend.invalidate_guide_cache();
}
if accumulation_reset {
last_denoise_at = None;
display.bump_generation();
}
let suspension = SuspensionFlags {
paused,
tab_visible,
remote_suspends: remote_suspends_local(remote_active, live_compute_target),
export_active,
material_unresolved: material_unresolved.is_some(),
};
if suspension.tracing_suspended() {
if !suspension.metrics_suspended() {
let (current_mat, _spp) = resolve_material_and_quality(
&MaterialSources {
materials: &materials,
custom_materials: &custom_materials,
material_override: material_override.as_ref(),
material_name: &material_name,
},
target_samples,
&MaterialOverrides {
inclusion_sigma_s,
c_axis_override,
edge_rounding_radius,
stone_width_mm,
},
&active_planes,
&mut stone_width_cache,
);
let (metrics, graph_brilliance, graph_extinction, graph_windowing) =
compute_or_reuse_metrics(
&mut metrics_cache,
&active_planes,
¤t_mat,
yaw,
pitch,
light_yaw,
light_pitch,
);
push_metrics_to_ui(
&ui_weak_metrics_only,
&update_metrics_metrics_only,
FrameMetricsSnapshot {
metrics,
graph_brilliance,
graph_extinction,
graph_windowing,
cam_pitch_deg: pitch.to_degrees(),
},
);
}
thread::sleep(std::time::Duration::from_millis(100));
continue;
}
let (current_mat, spp) = resolve_material_and_quality(
&MaterialSources {
materials: &materials,
custom_materials: &custom_materials,
material_override: material_override.as_ref(),
material_name: &material_name,
},
target_samples,
&MaterialOverrides {
inclusion_sigma_s,
c_axis_override,
edge_rounding_radius,
stone_width_mm,
},
&active_planes,
&mut stone_width_cache,
);
let remote_advanced = should_combine_remote(remote_active, live_compute_target)
&& remote_samples_done_now > last_displayed_remote_samples;
let local_converged = accum_samples + remote_samples_done_now >= target_samples;
if local_converged && !dirty && !remote_advanced {
thread::sleep(std::time::Duration::from_millis(60));
continue;
}
let skip_local_tracing = local_converged && !dirty;
if !skip_local_tracing {
accum_samples += spp;
}
let (metrics, graph_brilliance, graph_extinction, graph_windowing) =
compute_or_reuse_metrics(
&mut metrics_cache,
&active_planes,
¤t_mat,
yaw,
pitch,
light_yaw,
light_pitch,
);
let cam_pitch_deg = pitch.to_degrees();
let camera = Camera::new(yaw, pitch, distance, 42.0);
let current_sample_count = accum_samples;
let environment = env_map.as_deref().map_or_else(
|| {
lighting_preset
.studio(exposure, light_yaw, light_pitch)
.with_backdrop(backdrop.level())
},
EnvironmentSource::HdrMap,
);
let facet_finishes: &[FacetFinish] = if girdle_frosted {
girdle_cache.ensure(&active_planes)
} else {
&[]
};
if !skip_local_tracing {
accumulate_frame_samples(
&mut gpu_backend,
&BackendFrame {
width,
height,
yaw,
pitch,
distance,
camera: &camera,
planes: &active_planes,
facet_finishes,
material: ¤t_mat,
max_bounces,
environment,
spp,
sample_offset: combined_sample_offset(
remote_reserved_samples,
current_sample_count - spp,
),
},
&mut FrameOutputs {
accum: &mut accum_buffer,
depth: &mut first_hit_depth,
normal: &mut first_hit_normal,
facet_id: &mut first_hit_facet_id,
},
&mut hybrid_pacing,
local_compute_target,
);
}
if gpu_backend.status_message() != last_gpu_status.as_deref() {
last_gpu_status = gpu_backend.status_message().map(str::to_string);
let text: slint::SharedString = last_gpu_status.as_deref().unwrap_or("").into();
let update_gpu_status = update_gpu_status.clone();
let _ = ui_weak_metrics_only.upgrade_in_event_loop(move |ui| {
update_gpu_status(&ui, text);
});
}
let converged_now = accum_samples + remote_samples_done_now >= target_samples;
let denoise_due = !denoise_enabled
|| accumulation_reset
|| converged_now
|| last_denoise_at.is_none_or(|t| t.elapsed() >= DENOISE_MIN_INTERVAL);
if denoise_due {
let can_send = if converged_now {
let wait_start = Instant::now();
let mut timed_out = false;
while display.busy() {
if wait_start.elapsed() >= DISPLAY_BUSY_WAIT_TIMEOUT {
timed_out = true;
break;
}
thread::sleep(display_thread::CONVERGENCE_WAIT_POLL);
}
if timed_out {
tracing::warn!(
timeout_secs = DISPLAY_BUSY_WAIT_TIMEOUT.as_secs(),
"display thread still busy after the convergence wait \
timeout; skipping this display cycle instead of blocking \
the render thread forever"
);
}
!timed_out
} else {
!display.busy()
};
if can_send {
let (display_accum, display_sample_count): (&[Vec3], u32) =
if should_combine_remote(remote_active, live_compute_target)
&& let Some(remote_acc) = &remote_accumulator
{
let acc = remote_acc
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let remote_done = acc.samples_done();
combined_scratch.clear();
combined_scratch.extend(
accum_buffer.iter().zip(acc.buffer()).map(|(a, b)| *a + *b),
);
drop(acc);
last_displayed_remote_samples = remote_done;
(&combined_scratch, current_sample_count + remote_done)
} else {
(&accum_buffer, current_sample_count)
};
let mut work = display.reclaim();
work.fill(
display.current_generation(),
denoise_enabled,
FirstHitSnapshot {
width,
height,
current_sample_count: display_sample_count,
accum_buffer: display_accum,
first_hit_depth: &first_hit_depth,
first_hit_normal: &first_hit_normal,
first_hit_facet_id: &first_hit_facet_id,
},
FrameMetricsSnapshot {
metrics,
graph_brilliance,
graph_extinction,
graph_windowing,
cam_pitch_deg,
},
FrameActivityFlags {
camera_moving,
converged: converged_now,
},
);
display.send(work);
last_denoise_at = Some(Instant::now());
}
}
thread::sleep(std::time::Duration::from_millis(16));
}
});
}