Skip to main content

dsfb_computer_graphics/
host.rs

1use serde::Serialize;
2
3use crate::dsfb::{StateField, StructuralState};
4use crate::frame::{Color, ImageFrame, ScalarField};
5use crate::parameters::{
6    gated_reference_parameters, host_realistic_parameters, motion_augmented_parameters,
7    synthetic_visibility_parameters, HazardMergeMode, HostSupervisionParameters,
8    SmoothstepThreshold, TrustBehavior,
9};
10use crate::scene::{MotionVector, Normal3};
11
12#[derive(Clone, Debug)]
13pub struct HostTemporalInputs<'a> {
14    pub current_color: &'a ImageFrame,
15    pub reprojected_history: &'a ImageFrame,
16    pub motion_vectors: &'a [MotionVector],
17    pub current_depth: &'a [f32],
18    pub reprojected_depth: &'a [f32],
19    pub current_normals: &'a [Normal3],
20    pub reprojected_normals: &'a [Normal3],
21    pub visibility_hint: Option<&'a [bool]>,
22    pub thin_hint: Option<&'a ScalarField>,
23}
24
25#[derive(Clone, Debug, Serialize)]
26pub struct HostSupervisionProfile {
27    pub id: String,
28    pub label: String,
29    pub description: String,
30    pub modulate_alpha: bool,
31    pub use_visibility_hint: bool,
32    pub use_depth_proxy: bool,
33    pub use_normal_proxy: bool,
34    pub use_motion_proxy: bool,
35    pub use_neighborhood_proxy: bool,
36    pub use_thin_proxy: bool,
37    pub use_history_instability: bool,
38    pub use_grammar: bool,
39    pub parameters: HostSupervisionParameters,
40}
41
42#[derive(Clone, Debug)]
43pub struct HostProxyFields {
44    pub residual_proxy: ScalarField,
45    pub visibility_proxy: ScalarField,
46    pub depth_proxy: ScalarField,
47    pub normal_proxy: ScalarField,
48    pub motion_proxy: ScalarField,
49    pub neighborhood_proxy: ScalarField,
50    pub thin_proxy: ScalarField,
51    pub history_instability_proxy: ScalarField,
52}
53
54#[derive(Clone, Debug)]
55pub struct HostSupervisionOutputs {
56    pub residual: ScalarField,
57    pub trust: ScalarField,
58    pub alpha: ScalarField,
59    pub intervention: ScalarField,
60    pub proxies: HostProxyFields,
61    pub state: StateField,
62}
63
64pub fn supervise_temporal_reuse(
65    inputs: &HostTemporalInputs<'_>,
66    profile: &HostSupervisionProfile,
67) -> HostSupervisionOutputs {
68    let width = inputs.current_color.width();
69    let height = inputs.current_color.height();
70    let parameters = profile.parameters;
71
72    let mut residual = ScalarField::new(width, height);
73    let mut trust = ScalarField::new(width, height);
74    let mut alpha = ScalarField::new(width, height);
75    let mut intervention = ScalarField::new(width, height);
76    let mut residual_proxy = ScalarField::new(width, height);
77    let mut visibility_proxy = ScalarField::new(width, height);
78    let mut depth_proxy = ScalarField::new(width, height);
79    let mut normal_proxy = ScalarField::new(width, height);
80    let mut motion_proxy = ScalarField::new(width, height);
81    let mut neighborhood_proxy = ScalarField::new(width, height);
82    let mut thin_proxy = ScalarField::new(width, height);
83    let mut history_instability_proxy = ScalarField::new(width, height);
84    let mut state = StateField::new(width, height);
85
86    for y in 0..height {
87        for x in 0..width {
88            let index = y * width + x;
89            let current = inputs.current_color.get(x, y);
90            let history = inputs.reprojected_history.get(x, y);
91            let residual_value = current.abs_diff(history);
92            let residual_gate =
93                smoothstep_threshold(parameters.thresholds.residual, residual_value);
94            let depth_gate = if profile.use_depth_proxy {
95                smoothstep_threshold(
96                    parameters.thresholds.depth,
97                    (inputs.current_depth[index] - inputs.reprojected_depth[index]).abs(),
98                )
99            } else {
100                0.0
101            };
102            let normal_gate = if profile.use_normal_proxy {
103                let dot = inputs.current_normals[index]
104                    .dot(inputs.reprojected_normals[index])
105                    .clamp(-1.0, 1.0);
106                smoothstep_threshold(parameters.thresholds.normal, 1.0 - dot)
107            } else {
108                0.0
109            };
110            let motion_gate = if profile.use_motion_proxy {
111                motion_disagreement_proxy(
112                    inputs.motion_vectors,
113                    width,
114                    height,
115                    x,
116                    y,
117                    parameters.thresholds.motion,
118                )
119            } else {
120                0.0
121            };
122            let neighborhood_gate = if profile.use_neighborhood_proxy {
123                neighborhood_inconsistency_proxy(
124                    inputs.current_color,
125                    history,
126                    x,
127                    y,
128                    parameters.thresholds.neighborhood,
129                )
130            } else {
131                0.0
132            };
133            let thin_gate = if profile.use_thin_proxy {
134                if let Some(thin_hint) = inputs.thin_hint {
135                    (parameters.thresholds.thin_hint_mix * thin_hint.get(x, y)
136                        + parameters.thresholds.thin_local_contrast_mix
137                            * local_contrast_proxy(
138                                inputs.current_color,
139                                x,
140                                y,
141                                parameters.thresholds.local_contrast,
142                            ))
143                    .clamp(0.0, 1.0)
144                } else {
145                    local_contrast_proxy(
146                        inputs.current_color,
147                        x,
148                        y,
149                        parameters.thresholds.local_contrast,
150                    )
151                }
152            } else {
153                0.0
154            };
155            let visibility_gate: f32 = if profile.use_visibility_hint {
156                inputs
157                    .visibility_hint
158                    .map(|hint| if hint[index] { 1.0 } else { 0.0 })
159                    .unwrap_or(0.0)
160            } else {
161                0.0
162            };
163            let history_instability_gate = if profile.use_history_instability {
164                (parameters.thresholds.history_instability_residual_mix * residual_gate
165                    + parameters.thresholds.history_instability_neighborhood_mix
166                        * neighborhood_gate)
167                    .clamp(0.0, 1.0)
168            } else {
169                0.0
170            };
171
172            let state_value = classify_state(
173                residual_gate,
174                visibility_gate.max(depth_gate).max(normal_gate),
175                motion_gate,
176                thin_gate,
177                neighborhood_gate,
178                parameters,
179            );
180            let grammar_component = if profile.use_grammar {
181                grammar_hazard(state_value)
182            } else {
183                0.0
184            };
185
186            let weighted = parameters.weights.residual * residual_gate
187                + parameters.weights.visibility * visibility_gate
188                + parameters.weights.depth * depth_gate
189                + parameters.weights.normal * normal_gate
190                + parameters.weights.motion * motion_gate
191                + parameters.weights.neighborhood * neighborhood_gate
192                + parameters.weights.thin * thin_gate
193                + parameters.weights.history_instability * history_instability_gate;
194            let grammar_gate = parameters.weights.grammar * grammar_component;
195            let hazard_raw = match parameters.hazard_merge_mode {
196                HazardMergeMode::MaxGate => weighted.max(grammar_gate),
197                HazardMergeMode::WeightedAdd => weighted + grammar_gate,
198            };
199            let hazard = match parameters.trust_behavior {
200                TrustBehavior::GateLike => hazard_raw.clamp(0.0, 1.0),
201                TrustBehavior::Graded => smoothstep_threshold(
202                    parameters.thresholds.hazard_curve,
203                    hazard_raw.clamp(0.0, 1.0),
204                ),
205            };
206            let trust_value = 1.0 - hazard;
207            let alpha_value = if profile.modulate_alpha {
208                parameters.alpha_range.min
209                    + (parameters.alpha_range.max - parameters.alpha_range.min) * hazard
210            } else {
211                parameters.alpha_range.min
212            };
213
214            residual.set(x, y, residual_value);
215            residual_proxy.set(x, y, residual_gate);
216            visibility_proxy.set(x, y, visibility_gate);
217            depth_proxy.set(x, y, depth_gate);
218            normal_proxy.set(x, y, normal_gate);
219            motion_proxy.set(x, y, motion_gate);
220            neighborhood_proxy.set(x, y, neighborhood_gate);
221            thin_proxy.set(x, y, thin_gate);
222            history_instability_proxy.set(x, y, history_instability_gate);
223            trust.set(x, y, trust_value);
224            alpha.set(x, y, alpha_value);
225            intervention.set(x, y, hazard);
226            state.set(x, y, state_value);
227        }
228    }
229
230    HostSupervisionOutputs {
231        residual,
232        trust,
233        alpha,
234        intervention,
235        proxies: HostProxyFields {
236            residual_proxy,
237            visibility_proxy,
238            depth_proxy,
239            normal_proxy,
240            motion_proxy,
241            neighborhood_proxy,
242            thin_proxy,
243            history_instability_proxy,
244        },
245        state,
246    }
247}
248
249pub fn default_host_realistic_profile(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
250    let mut parameters = host_realistic_parameters();
251    parameters.alpha_range.min = alpha_min;
252    parameters.alpha_range.max = alpha_max;
253    HostSupervisionProfile {
254        id: "dsfb_host_realistic".to_string(),
255        label: "DSFB host-realistic minimum".to_string(),
256        description: "Minimum decision-facing path: residual, depth, normal, neighborhood, thin proxy, and grammar supervision without privileged visibility or motion disagreement.".to_string(),
257        modulate_alpha: true,
258        use_visibility_hint: false,
259        use_depth_proxy: true,
260        use_normal_proxy: true,
261        use_motion_proxy: false,
262        use_neighborhood_proxy: true,
263        use_thin_proxy: true,
264        use_history_instability: true,
265        use_grammar: true,
266        parameters,
267    }
268}
269
270pub fn synthetic_visibility_profile(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
271    let mut parameters = synthetic_visibility_parameters();
272    parameters.alpha_range.min = alpha_min;
273    parameters.alpha_range.max = alpha_max;
274    HostSupervisionProfile {
275        id: "dsfb_synthetic_visibility".to_string(),
276        label: "DSFB visibility-assisted".to_string(),
277        description: "Research/debug mode that augments host-realistic cues with a synthetic visibility hint.".to_string(),
278        modulate_alpha: true,
279        use_visibility_hint: true,
280        use_depth_proxy: true,
281        use_normal_proxy: true,
282        use_motion_proxy: true,
283        use_neighborhood_proxy: true,
284        use_thin_proxy: true,
285        use_history_instability: true,
286        use_grammar: true,
287        parameters,
288    }
289}
290
291pub fn motion_augmented_profile(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
292    let mut parameters = motion_augmented_parameters();
293    parameters.alpha_range.min = alpha_min;
294    parameters.alpha_range.max = alpha_max;
295    HostSupervisionProfile {
296        id: "dsfb_motion_augmented".to_string(),
297        label: "DSFB motion-augmented".to_string(),
298        description: "Optional extension that adds motion disagreement to the minimum host-realistic path. It is kept only if scenario evidence shows it matters.".to_string(),
299        modulate_alpha: true,
300        use_visibility_hint: false,
301        use_depth_proxy: true,
302        use_normal_proxy: true,
303        use_motion_proxy: true,
304        use_neighborhood_proxy: true,
305        use_thin_proxy: true,
306        use_history_instability: true,
307        use_grammar: true,
308        parameters,
309    }
310}
311
312pub fn gated_reference_profile(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
313    let mut parameters = gated_reference_parameters();
314    parameters.alpha_range.min = alpha_min;
315    parameters.alpha_range.max = alpha_max;
316    HostSupervisionProfile {
317        id: "dsfb_host_gated_reference".to_string(),
318        label: "DSFB gated reference".to_string(),
319        description: "Reference implementation of the earlier near-binary gate-like supervisory mode, retained for trust diagnostics and comparison.".to_string(),
320        modulate_alpha: true,
321        use_visibility_hint: false,
322        use_depth_proxy: true,
323        use_normal_proxy: true,
324        use_motion_proxy: true,
325        use_neighborhood_proxy: true,
326        use_thin_proxy: true,
327        use_history_instability: true,
328        use_grammar: true,
329        parameters,
330    }
331}
332
333pub fn profile_without_visibility(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
334    let mut profile = synthetic_visibility_profile(alpha_min, alpha_max);
335    profile.id = "dsfb_no_visibility".to_string();
336    profile.label = "DSFB without visibility cue".to_string();
337    profile.description = "Visibility-assisted DSFB ablation with the synthetic visibility cue disabled while keeping the rest of the supervisory structure intact.".to_string();
338    profile.use_visibility_hint = false;
339    profile.parameters.weights.visibility = 0.0;
340    profile
341}
342
343pub fn profile_without_thin(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
344    let mut profile = default_host_realistic_profile(alpha_min, alpha_max);
345    profile.id = "dsfb_no_thin".to_string();
346    profile.label = "DSFB without thin proxy".to_string();
347    profile.use_thin_proxy = false;
348    profile.parameters.weights.thin = 0.0;
349    profile
350}
351
352pub fn profile_without_motion(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
353    let mut profile = motion_augmented_profile(alpha_min, alpha_max);
354    profile.id = "dsfb_no_motion_edge".to_string();
355    profile.label = "DSFB without motion disagreement".to_string();
356    profile.use_motion_proxy = false;
357    profile.parameters.weights.motion = 0.0;
358    profile
359}
360
361pub fn profile_without_grammar(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
362    let mut profile = default_host_realistic_profile(alpha_min, alpha_max);
363    profile.id = "dsfb_no_grammar".to_string();
364    profile.label = "DSFB without grammar".to_string();
365    profile.use_grammar = false;
366    profile.parameters.weights.grammar = 0.0;
367    profile
368}
369
370pub fn profile_residual_only(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
371    let conservative_alpha_max = alpha_min + 0.42 * (alpha_max - alpha_min);
372    let mut parameters = host_realistic_parameters();
373    parameters.alpha_range.min = alpha_min;
374    parameters.alpha_range.max = conservative_alpha_max;
375    parameters.weights.residual = 0.72;
376    parameters.weights.visibility = 0.0;
377    parameters.weights.depth = 0.0;
378    parameters.weights.normal = 0.0;
379    parameters.weights.motion = 0.0;
380    parameters.weights.neighborhood = 0.0;
381    parameters.weights.thin = 0.0;
382    parameters.weights.history_instability = 0.0;
383    parameters.weights.grammar = 0.0;
384    HostSupervisionProfile {
385        id: "dsfb_residual_only".to_string(),
386        label: "DSFB residual-only".to_string(),
387        description: "Residual-only supervisory hazard without auxiliary structure cues. The alpha mapping is intentionally conservative so this remains a true single-cue ablation rather than a near-clone of the stronger residual-threshold baseline.".to_string(),
388        modulate_alpha: true,
389        use_visibility_hint: false,
390        use_depth_proxy: false,
391        use_normal_proxy: false,
392        use_motion_proxy: false,
393        use_neighborhood_proxy: false,
394        use_thin_proxy: false,
395        use_history_instability: false,
396        use_grammar: false,
397        parameters,
398    }
399}
400
401pub fn profile_without_alpha_modulation(alpha_min: f32, alpha_max: f32) -> HostSupervisionProfile {
402    let mut profile = default_host_realistic_profile(alpha_min, alpha_max);
403    profile.id = "dsfb_trust_no_alpha".to_string();
404    profile.label = "DSFB trust without alpha modulation".to_string();
405    profile.modulate_alpha = false;
406    profile
407}
408
409fn local_contrast_proxy(
410    frame: &ImageFrame,
411    x: usize,
412    y: usize,
413    threshold: SmoothstepThreshold,
414) -> f32 {
415    let center = frame.get(x, y).luma();
416    let mut strongest = 0.0f32;
417    for_each_neighbor(x, y, frame.width(), frame.height(), |nx, ny| {
418        strongest = strongest.max((center - frame.get(nx, ny).luma()).abs());
419    });
420    smoothstep_threshold(threshold, strongest)
421}
422
423fn neighborhood_inconsistency_proxy(
424    current_color: &ImageFrame,
425    history: Color,
426    x: usize,
427    y: usize,
428    threshold: SmoothstepThreshold,
429) -> f32 {
430    let mut min_luma = f32::INFINITY;
431    let mut max_luma = f32::NEG_INFINITY;
432    for_each_neighbor(x, y, current_color.width(), current_color.height(), |nx, ny| {
433        let luma = current_color.get(nx, ny).luma();
434        min_luma = min_luma.min(luma);
435        max_luma = max_luma.max(luma);
436    });
437    let current_luma = current_color.get(x, y).luma();
438    min_luma = min_luma.min(current_luma);
439    max_luma = max_luma.max(current_luma);
440    let history_luma = history.luma();
441    let distance = if history_luma < min_luma {
442        min_luma - history_luma
443    } else if history_luma > max_luma {
444        history_luma - max_luma
445    } else {
446        0.0
447    };
448    smoothstep_threshold(threshold, distance)
449}
450
451fn motion_disagreement_proxy(
452    motion_vectors: &[MotionVector],
453    width: usize,
454    height: usize,
455    x: usize,
456    y: usize,
457    threshold: SmoothstepThreshold,
458) -> f32 {
459    let base = motion_vectors[y * width + x];
460    let mut strongest = 0.0f32;
461    for_each_neighbor(x, y, width, height, |nx, ny| {
462        let neighbor = motion_vectors[ny * width + nx];
463        let delta_x = base.to_prev_x - neighbor.to_prev_x;
464        let delta_y = base.to_prev_y - neighbor.to_prev_y;
465        strongest = strongest.max((delta_x * delta_x + delta_y * delta_y).sqrt());
466    });
467    smoothstep_threshold(threshold, strongest)
468}
469
470fn classify_state(
471    residual_gate: f32,
472    structural_disagreement: f32,
473    motion_gate: f32,
474    thin_gate: f32,
475    neighborhood_gate: f32,
476    parameters: HostSupervisionParameters,
477) -> StructuralState {
478    if structural_disagreement >= parameters.structural.disocclusion_like {
479        StructuralState::DisocclusionLike
480    } else if residual_gate >= parameters.structural.unstable_residual
481        && neighborhood_gate >= parameters.structural.unstable_neighborhood
482    {
483        StructuralState::UnstableHistory
484    } else if motion_gate >= parameters.structural.motion_edge
485        || (thin_gate >= parameters.structural.thin_edge
486            && residual_gate >= parameters.structural.thin_residual)
487    {
488        StructuralState::MotionEdge
489    } else {
490        StructuralState::Nominal
491    }
492}
493
494fn grammar_hazard(state: StructuralState) -> f32 {
495    match state {
496        StructuralState::Nominal => 0.0,
497        StructuralState::MotionEdge => 0.32,
498        StructuralState::UnstableHistory => 0.62,
499        StructuralState::DisocclusionLike => 0.88,
500    }
501}
502
503fn smoothstep_threshold(threshold: SmoothstepThreshold, value: f32) -> f32 {
504    let edge_span = (threshold.high - threshold.low).max(f32::EPSILON);
505    let t = ((value - threshold.low) / edge_span).clamp(0.0, 1.0);
506    t * t * (3.0 - 2.0 * t)
507}
508
509/// Calls `f` for each of the (up to 8) 8-connected neighbours of `(x, y)`.
510/// Zero heap allocation. Inlined by the compiler in the per-pixel hot path.
511#[inline(always)]
512fn for_each_neighbor(
513    x: usize,
514    y: usize,
515    width: usize,
516    height: usize,
517    mut f: impl FnMut(usize, usize),
518) {
519    let x = x as i32;
520    let y = y as i32;
521    let w = width as i32;
522    let h = height as i32;
523    for dy in -1i32..=1 {
524        for dx in -1i32..=1 {
525            if dx == 0 && dy == 0 {
526                continue;
527            }
528            let nx = x + dx;
529            let ny = y + dy;
530            if nx >= 0 && nx < w && ny >= 0 && ny < h {
531                f(nx as usize, ny as usize);
532            }
533        }
534    }
535}