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::{ComponentHandle, Weak};
use std::{
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
};
const DENOISE_MIN_INTERVAL: Duration = Duration::from_millis(120);
use frame_helpers::{
AccumulationBuffers, SuspensionFlags, 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>(
ui_weak: Weak<T>,
ctx: Arc<Mutex<RenderContext>>,
update_image: F,
update_metrics: M,
) where
T: ComponentHandle + '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,
{
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 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 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 accumulation_reset = dirty || width != last_width || height != last_height;
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 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,
);
if accum_samples + remote_samples_done_now >= target_samples && !dirty {
thread::sleep(std::time::Duration::from_millis(60));
continue;
}
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 {
&[]
};
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,
);
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 {
while display.busy() {
thread::sleep(display_thread::CONVERGENCE_WAIT_POLL);
}
true
} 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);
combined_scratch.clear();
combined_scratch.extend(
accum_buffer.iter().zip(acc.buffer()).map(|(a, b)| *a + *b),
);
(&combined_scratch, current_sample_count + acc.samples_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,
},
);
display.send(work);
last_denoise_at = Some(Instant::now());
}
}
thread::sleep(std::time::Duration::from_millis(16));
}
});
}