Skip to main content

primase_core/atmosphere/
status.rs

1//! Portable explanations of unresolved or approximate display processing.
2use serde::Serialize;
3
4use crate::{JsonError, Resource, World};
5
6use super::ToneMap;
7
8/// A display-processing operation, independent of a source's settings vocabulary.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum PostStage {
12    /// The mapping from exposed scene colour to the display curve's output.
13    Curve,
14    /// A colour transform before or after the curve.
15    ColourMatrix,
16    /// A screen-position-dependent attenuation.
17    Vignette,
18    /// A three-dimensional colour lookup.
19    ColourLookup,
20    /// The power applied before the colour lookup.
21    InputPower,
22}
23
24/// Whether a source could not establish an operation or supplied a stated approximation.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum PostStageStatusKind {
28    /// The operation is not provided; a later pass providing it resolves this status.
29    Unresolved,
30    /// A source-stated approximation or provenance limitation that remains relevant independently of stage presence.
31    Approximate,
32}
33
34/// One source-provided explanation. Consumers show the explanation without interpreting source records.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
36pub struct PostStageStatus {
37    /// The affected operation.
38    pub stage: PostStage,
39    /// An unresolved operation or a stated approximation.
40    pub kind: PostStageStatusKind,
41    /// A stable source-provided report key.
42    pub key: String,
43    /// The source pass's explanation, including any provenance limits.
44    pub reason: String,
45}
46
47impl PostStageStatus {
48    /// Whether this explanation still applies to the current display processing.
49    /// Providing a previously unresolved operation suppresses that omission; approximation limits remain visible.
50    pub fn is_relevant(&self, tone: Option<&ToneMap>) -> bool {
51        if self.kind == PostStageStatusKind::Approximate {
52            return true;
53        }
54        match self.stage {
55            PostStage::Curve => tone.is_none(),
56            PostStage::ColourMatrix => tone.is_none_or(|tone| tone.colour_matrix.is_none()),
57            PostStage::Vignette => tone.is_none_or(|tone| tone.vignette.is_none()),
58            PostStage::ColourLookup | PostStage::InputPower => tone.is_none_or(|tone| tone.lut.is_none()),
59        }
60    }
61}
62
63/// Source-pass status for display processing, separate from the numerical operations in [`super::Atmosphere`].
64/// An empty or absent resource makes no claim about an unread source's support.
65#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
66pub struct PostProcessingStatus(pub Vec<PostStageStatus>);
67
68impl Resource for PostProcessingStatus {
69    const NAME: &'static str = "core.post_processing_status";
70
71    fn to_json(&self) -> Result<Option<serde_json::Value>, JsonError> {
72        crate::json::to_value(self).map(Some)
73    }
74
75    fn verify(&self, _world: &World) -> Result<(), String> {
76        let mut keys = std::collections::BTreeSet::new();
77        for status in &self.0 {
78            if status.key.trim().is_empty() || status.reason.trim().is_empty() {
79                return Err("post stage status needs a nonempty key and explanation".into());
80            }
81            if !keys.insert(&status.key) {
82                return Err(format!("post stage status repeats report key {}", status.key));
83            }
84        }
85        Ok(())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::{ColourLut, ColourMatrix, ReinhardCurve};
93
94    fn status(stage: PostStage, kind: PostStageStatusKind) -> PostStageStatus {
95        PostStageStatus {
96            stage,
97            kind,
98            key: "post.stage_unresolved".into(),
99            reason: "source operation unresolved".into(),
100        }
101    }
102
103    #[test]
104    fn provided_stages_resolve_only_their_own_omissions_and_keep_approximation_limits() {
105        let mut tone = ToneMap {
106            curve: ReinhardCurve {
107                contrast: 1.,
108                white_balance: 1.,
109                gamma: 1.,
110            },
111            colour_matrix: None,
112            vignette: None,
113            lut: None,
114        };
115        let curve = status(PostStage::Curve, PostStageStatusKind::Unresolved);
116        let matrix = status(PostStage::ColourMatrix, PostStageStatusKind::Unresolved);
117        let vignette = status(PostStage::Vignette, PostStageStatusKind::Unresolved);
118        let lookup = status(PostStage::ColourLookup, PostStageStatusKind::Unresolved);
119        let power = status(PostStage::InputPower, PostStageStatusKind::Approximate);
120        assert!(curve.is_relevant(None));
121        assert!(!curve.is_relevant(Some(&tone)));
122        assert!(
123            matrix.is_relevant(Some(&tone))
124                && vignette.is_relevant(Some(&tone))
125                && lookup.is_relevant(Some(&tone))
126        );
127        tone.colour_matrix = Some(ColourMatrix::AfterCurve([
128            [1., 0., 0., 0.],
129            [0., 1., 0., 0.],
130            [0., 0., 1., 0.],
131        ]));
132        assert!(!matrix.is_relevant(Some(&tone)));
133        assert!(vignette.is_relevant(Some(&tone)) && lookup.is_relevant(Some(&tone)));
134        tone.vignette = Some([0.; 4]);
135        tone.lut = Some(ColourLut {
136            side: 2,
137            values: vec![[0.; 3]; 8],
138            input_power: 1.,
139        });
140        assert!(!vignette.is_relevant(Some(&tone)) && !lookup.is_relevant(Some(&tone)));
141        assert!(power.is_relevant(Some(&tone)));
142    }
143
144    #[test]
145    fn status_is_portable_data_with_unique_nonempty_report_keys() {
146        let world = World::new();
147        let issue = status(PostStage::ColourMatrix, PostStageStatusKind::Unresolved);
148        let mut statuses = PostProcessingStatus(vec![issue.clone()]);
149        assert!(statuses.verify(&world).is_ok());
150        let json = statuses.to_json().unwrap().unwrap();
151        assert_eq!(json[0]["stage"], "colour_matrix");
152        assert_eq!(json[0]["kind"], "unresolved");
153        assert_eq!(json[0]["reason"], "source operation unresolved");
154        statuses.0.push(issue);
155        assert!(
156            statuses
157                .verify(&world)
158                .unwrap_err()
159                .contains("repeats report key")
160        );
161        statuses.0.pop();
162        statuses.0[0].reason.clear();
163        assert!(statuses.verify(&world).unwrap_err().contains("nonempty"));
164    }
165}