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#[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}