use super::{display_thread::FrameMetricsSnapshot, redraw_gate::RedrawGate};
use crate::{ActivityModel, settings::model::LiveComputeTarget};
use glam::Vec3;
use slint::{ComponentHandle, Weak};
use std::{cell::RefCell, sync::Arc, time::Duration};
thread_local! {
static TRACE_ACTIVITY_ID: RefCell<Option<i32>> = const { RefCell::new(None) };
}
#[derive(Debug, Clone, Copy)]
pub(super) struct FrameActivityFlags {
pub(super) camera_moving: bool,
pub(super) converged: bool,
}
pub trait TraceActivitySink: ComponentHandle {
fn start_trace_activity(&self) -> i32;
fn finish_trace_activity(&self, id: i32);
}
impl TraceActivitySink for crate::MainWindow {
fn start_trace_activity(&self) -> i32 {
self.global::<ActivityModel>().invoke_start_external(
"trace".into(),
"Path tracing".into(),
false,
)
}
fn finish_trace_activity(&self, id: i32) {
self.global::<ActivityModel>().invoke_finish_external(id);
}
}
pub(super) struct FramePayload {
image: slint::SharedPixelBuffer<slint::Rgba8Pixel>,
metrics_snapshot: FrameMetricsSnapshot,
activity_flags: FrameActivityFlags,
}
pub(super) fn push_frame_to_ui<T, F, M>(
ui_weak: &Weak<T>,
update_image: &F,
update_metrics: &M,
redraw_gate: &Arc<RedrawGate<FramePayload>>,
image: slint::SharedPixelBuffer<slint::Rgba8Pixel>,
metrics_snapshot: FrameMetricsSnapshot,
activity_flags: FrameActivityFlags,
) 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,
{
if redraw_gate
.submit(FramePayload {
image,
metrics_snapshot,
activity_flags,
})
.is_none()
{
return;
}
let redraw_gate = Arc::clone(redraw_gate);
let _ = ui_weak.upgrade_in_event_loop({
let update_image = update_image.clone();
let update_metrics = update_metrics.clone();
move |ui| {
let Some(FramePayload {
image,
metrics_snapshot,
activity_flags,
}) = redraw_gate.take()
else {
return;
};
update_image(&ui, image);
let metrics = metrics_snapshot.metrics;
update_metrics(
&ui,
metrics.brilliance_pct,
metrics.fire_index,
metrics.scintillation_pct,
metrics.windowing_pct,
metrics.extinction_pct,
metrics_snapshot.graph_brilliance,
metrics_snapshot.graph_extinction,
metrics_snapshot.graph_windowing,
metrics_snapshot.cam_pitch_deg,
);
track_trace_activity(&ui, activity_flags);
}
});
}
fn track_trace_activity<T: TraceActivitySink>(ui: &T, flags: FrameActivityFlags) {
if !flags.camera_moving {
let already_running = TRACE_ACTIVITY_ID.with(|cell| cell.borrow().is_some());
if !already_running && !flags.converged {
let id = ui.start_trace_activity();
TRACE_ACTIVITY_ID.with(|cell| *cell.borrow_mut() = Some(id));
}
}
if flags.converged
&& let Some(id) = TRACE_ACTIVITY_ID.with(|cell| cell.borrow_mut().take())
{
ui.finish_trace_activity(id);
}
}
pub(super) fn push_metrics_to_ui<T, M>(
ui_weak: &Weak<T>,
update_metrics: &M,
metrics_snapshot: FrameMetricsSnapshot,
) where
T: ComponentHandle + 'static,
M: Fn(&T, f32, f32, f32, f32, f32, [f32; 19], [f32; 19], [f32; 19], f32)
+ Send
+ 'static
+ Clone,
{
let update_metrics = update_metrics.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
let metrics = metrics_snapshot.metrics;
update_metrics(
&ui,
metrics.brilliance_pct,
metrics.fire_index,
metrics.scintillation_pct,
metrics.windowing_pct,
metrics.extinction_pct,
metrics_snapshot.graph_brilliance,
metrics_snapshot.graph_extinction,
metrics_snapshot.graph_windowing,
metrics_snapshot.cam_pitch_deg,
);
});
}
pub(super) struct AccumulationBuffers<'a> {
pub(super) accum: &'a mut Vec<Vec3>,
pub(super) first_hit_depth: &'a mut Vec<f32>,
pub(super) first_hit_normal: &'a mut Vec<Vec3>,
pub(super) first_hit_facet_id: &'a mut Vec<i32>,
}
pub(super) fn update_accumulation_state(
width: u32,
height: u32,
dirty: bool,
buffers: &mut AccumulationBuffers<'_>,
accum_samples: &mut u32,
last_width: &mut u32,
last_height: &mut u32,
) {
if width != *last_width || height != *last_height {
let pixel_count = (width * height) as usize;
*buffers.accum = vec![Vec3::ZERO; pixel_count];
*buffers.first_hit_depth = vec![1.0e6; pixel_count];
*buffers.first_hit_normal = vec![Vec3::ZERO; pixel_count];
*buffers.first_hit_facet_id = vec![-1; pixel_count];
*accum_samples = 0;
*last_width = width;
*last_height = height;
}
if dirty {
buffers.accum.fill(Vec3::ZERO);
*accum_samples = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct SuspensionFlags {
pub(super) paused: bool,
pub(super) tab_visible: bool,
pub(super) remote_suspends: bool,
pub(super) export_active: bool,
pub(super) material_unresolved: bool,
}
impl SuspensionFlags {
pub(super) const fn tracing_suspended(self) -> bool {
self.paused
|| !self.tab_visible
|| self.remote_suspends
|| self.export_active
|| self.material_unresolved
}
pub(super) const fn metrics_suspended(self) -> bool {
self.paused || self.remote_suspends || self.export_active || self.material_unresolved
}
}
#[must_use]
pub(super) const fn remote_suspends_local(
remote_active: bool,
live_compute_target: LiveComputeTarget,
) -> bool {
remote_active && matches!(live_compute_target, LiveComputeTarget::RemoteOnly)
}
#[must_use]
pub(super) const fn should_combine_remote(
remote_active: bool,
live_compute_target: LiveComputeTarget,
) -> bool {
remote_active && matches!(live_compute_target, LiveComputeTarget::Both)
}
pub(super) const fn resolve_remote_ownership(
dirty: bool,
remote_active: bool,
was_remote_active_last_frame: bool,
) -> bool {
if dirty && remote_active && was_remote_active_last_frame {
false
} else {
remote_active
}
}
#[must_use]
pub(super) fn pacing_sleep(paced: bool, elapsed: Duration, frame: Duration) -> Option<Duration> {
if !paced {
return None;
}
frame.checked_sub(elapsed).filter(|rest| !rest.is_zero())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_paced_iteration_sleeps_only_the_unused_remainder() {
let frame = Duration::from_millis(16);
assert_eq!(
pacing_sleep(true, Duration::from_millis(5), frame),
Some(Duration::from_millis(11))
);
}
#[test]
fn a_paced_iteration_that_ran_long_does_not_sleep() {
let frame = Duration::from_millis(16);
assert_eq!(pacing_sleep(true, Duration::from_millis(16), frame), None);
assert_eq!(pacing_sleep(true, Duration::from_millis(40), frame), None);
}
#[test]
fn an_unpaced_iteration_never_sleeps() {
assert_eq!(
pacing_sleep(false, Duration::ZERO, Duration::from_millis(16)),
None
);
}
#[test]
fn tracing_runs_only_when_all_four_flags_allow_it() {
assert!(
!SuspensionFlags {
paused: false,
tab_visible: true,
remote_suspends: false,
export_active: false,
material_unresolved: false
}
.tracing_suspended()
);
}
#[test]
fn each_flag_alone_suspends_tracing() {
assert!(
SuspensionFlags {
paused: true,
tab_visible: true,
remote_suspends: false,
export_active: false,
material_unresolved: false
}
.tracing_suspended(),
"paused"
);
assert!(
SuspensionFlags {
paused: false,
tab_visible: false,
remote_suspends: false,
export_active: false,
material_unresolved: false
}
.tracing_suspended(),
"!tab_visible"
);
assert!(
SuspensionFlags {
paused: false,
tab_visible: true,
remote_suspends: true,
export_active: false,
material_unresolved: false
}
.tracing_suspended(),
"remote_suspends"
);
assert!(
SuspensionFlags {
paused: false,
tab_visible: true,
remote_suspends: false,
export_active: true,
material_unresolved: false
}
.tracing_suspended(),
"export_active"
);
}
#[test]
fn every_flag_combination_still_suspends_if_any_one_is_set() {
for paused in [false, true] {
for tab_visible in [false, true] {
for remote_suspends in [false, true] {
for export_active in [false, true] {
let expected = paused || !tab_visible || remote_suspends || export_active;
assert_eq!(
SuspensionFlags {
paused,
tab_visible,
remote_suspends,
export_active,
material_unresolved: false,
}
.tracing_suspended(),
expected,
"paused={paused} tab_visible={tab_visible} \
remote_suspends={remote_suspends} export_active={export_active}"
);
}
}
}
}
}
#[test]
fn an_invisible_tab_alone_does_not_suspend_metrics() {
assert!(
!SuspensionFlags {
paused: false,
tab_visible: false,
remote_suspends: false,
export_active: false,
material_unresolved: false,
}
.metrics_suspended(),
"an invisible 3D tab must not freeze the HUD/tilt-dialog metrics -- \
only tracing_suspended (the full trace) should see !tab_visible"
);
}
#[test]
fn every_hard_suspend_reason_still_suspends_metrics() {
assert!(
SuspensionFlags {
paused: true,
tab_visible: true,
remote_suspends: false,
export_active: false,
material_unresolved: false,
}
.metrics_suspended(),
"paused"
);
assert!(
SuspensionFlags {
paused: false,
tab_visible: true,
remote_suspends: true,
export_active: false,
material_unresolved: false,
}
.metrics_suspended(),
"remote_suspends"
);
assert!(
SuspensionFlags {
paused: false,
tab_visible: true,
remote_suspends: false,
export_active: true,
material_unresolved: false,
}
.metrics_suspended(),
"export_active"
);
}
#[test]
fn metrics_suspended_is_never_stricter_than_tracing_suspended() {
for paused in [false, true] {
for tab_visible in [false, true] {
for remote_suspends in [false, true] {
for export_active in [false, true] {
let flags = SuspensionFlags {
paused,
tab_visible,
remote_suspends,
export_active,
material_unresolved: false,
};
if flags.metrics_suspended() {
assert!(
flags.tracing_suspended(),
"metrics_suspended implies tracing_suspended: {flags:?}"
);
}
}
}
}
}
}
#[test]
fn remote_only_is_the_sole_mode_where_remote_activity_suspends_local_tracing() {
for mode in [
LiveComputeTarget::LocalOnly,
LiveComputeTarget::RemoteOnly,
LiveComputeTarget::Both,
] {
assert!(
!remote_suspends_local(false, mode),
"remote_active == false must never suspend, regardless of mode"
);
}
assert!(
!remote_suspends_local(true, LiveComputeTarget::LocalOnly),
"LocalOnly never dispatches remote, but even if remote_active were \
somehow true, LocalOnly must not suspend local tracing"
);
assert!(
remote_suspends_local(true, LiveComputeTarget::RemoteOnly),
"RemoteOnly must reproduce the pre-existing suspend-while-remote-active \
behaviour exactly"
);
assert!(
!remote_suspends_local(true, LiveComputeTarget::Both),
"Both must never suspend local tracing -- it keeps contributing, it never \
yields the buffer to remote"
);
}
#[test]
fn only_both_mode_combines_and_only_while_remote_is_still_active() {
for mode in [
LiveComputeTarget::LocalOnly,
LiveComputeTarget::RemoteOnly,
LiveComputeTarget::Both,
] {
assert!(
!should_combine_remote(false, mode),
"remote_active == false must never combine, regardless of mode"
);
}
assert!(!should_combine_remote(true, LiveComputeTarget::LocalOnly));
assert!(
!should_combine_remote(true, LiveComputeTarget::RemoteOnly),
"RemoteOnly's local tracing is suspended whenever this could matter -- it \
must never itself decide to combine"
);
assert!(should_combine_remote(true, LiveComputeTarget::Both));
}
#[test]
fn combining_persists_past_a_successful_remote_completion() {
assert!(should_combine_remote(true, LiveComputeTarget::Both));
}
#[test]
fn a_settled_combined_render_keeps_combining_rather_than_being_overwritten() {
for _quiet_frame in 0..5 {
let resolved = resolve_remote_ownership(false, true, true);
assert!(resolved);
assert!(should_combine_remote(resolved, LiveComputeTarget::Both));
}
}
#[test]
fn a_scene_change_during_a_combined_render_stops_combining_a_now_stale_contribution() {
let resolved = resolve_remote_ownership(true, true, true);
assert!(
!resolved,
"a genuine scene invalidation must release ownership even mid-combine"
);
assert!(
!should_combine_remote(resolved, LiveComputeTarget::Both),
"combining must stop the instant ownership is released, so local's freshly \
reset accum_buffer (see update_accumulation_state's own dirty handling) is \
never summed with a now-stale-scene remote contribution"
);
}
#[test]
fn nothing_active_and_no_dirty_stays_inactive() {
assert!(!resolve_remote_ownership(false, false, false));
}
#[test]
fn the_handoffs_own_settling_entry_does_not_release_ownership() {
assert!(
resolve_remote_ownership(true, true, false),
"the hand-off starting must not immediately cancel itself"
);
}
#[test]
fn a_completed_remote_render_is_not_overwritten_by_local_tracing() {
assert!(resolve_remote_ownership(false, true, true));
}
#[test]
fn a_scene_change_after_a_completed_remote_render_resumes_local_tracing() {
assert!(!resolve_remote_ownership(true, true, true));
}
#[test]
fn a_dirty_frame_while_already_inactive_stays_inactive() {
assert!(!resolve_remote_ownership(true, false, false));
assert!(!resolve_remote_ownership(true, false, true));
}
#[test]
fn remote_rendering_survives_quiet_frames_with_no_dirty() {
assert!(resolve_remote_ownership(false, true, true));
assert!(resolve_remote_ownership(false, true, false));
}
#[test]
fn a_quiet_frame_with_unchanged_dimensions_preserves_accumulated_samples() {
let mut accum = vec![Vec3::new(1.0, 2.0, 3.0); 4];
let mut first_hit_depth = vec![0.5; 4];
let mut first_hit_normal = vec![Vec3::X; 4];
let mut first_hit_facet_id = vec![7; 4];
let mut accum_samples = 42;
let mut last_width = 2;
let mut last_height = 2;
update_accumulation_state(
2,
2,
false,
&mut AccumulationBuffers {
accum: &mut accum,
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,
);
assert_eq!(
accum_samples, 42,
"a suspended frame must not reset progress"
);
assert_eq!(
accum,
vec![Vec3::new(1.0, 2.0, 3.0); 4],
"the running sum must survive"
);
}
#[test]
fn a_dirty_frame_still_resets_accumulation_regardless_of_remote_ownership() {
let mut accum = vec![Vec3::new(1.0, 2.0, 3.0); 4];
let mut first_hit_depth = vec![0.5; 4];
let mut first_hit_normal = vec![Vec3::X; 4];
let mut first_hit_facet_id = vec![7; 4];
let mut accum_samples = 42;
let mut last_width = 2;
let mut last_height = 2;
update_accumulation_state(
2,
2,
true,
&mut AccumulationBuffers {
accum: &mut accum,
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,
);
assert_eq!(accum_samples, 0);
assert_eq!(accum, vec![Vec3::ZERO; 4]);
}
}