Skip to main content

drmkit_display/
pipeline.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The CRTC's color pipeline: degamma, matrix, gamma.
5//!
6//! Three blob properties in a fixed order. Content arrives encoded, degamma
7//! takes it to linear light, the matrix moves it between primaries -- which is
8//! only meaningful in linear light -- and gamma encodes it for the sink.
9
10use drmkit_core::{AtomicRequest, CoreError, Device, PropertyBlob, PropertyStore};
11
12use crate::capabilities::CrtcCapabilities;
13use crate::curves::{
14    ColorCtm, ColorLut, build_bt2020_to_bt709_ctm, build_hlg_oetf_inverse_lut, build_identity_ctm,
15    build_identity_lut, build_pq_eotf_lut, build_pq_oetf_lut,
16};
17
18/// Which stage a call was about.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Stage {
22    /// `DEGAMMA_LUT`: encoded to linear.
23    Degamma,
24    /// `CTM`: the 3x3 matrix.
25    Ctm,
26    /// `GAMMA_LUT`: linear to encoded.
27    Gamma,
28}
29
30impl Stage {
31    /// The kernel's property name.
32    #[must_use]
33    pub const fn property(self) -> &'static str {
34        match self {
35            Self::Degamma => "DEGAMMA_LUT",
36            Self::Ctm => "CTM",
37            Self::Gamma => "GAMMA_LUT",
38        }
39    }
40}
41
42/// What can go wrong programming the pipeline.
43#[derive(Debug, thiserror::Error)]
44#[non_exhaustive]
45pub enum PipelineError {
46    /// The CRTC exposes none of the three stages, so there is nothing to
47    /// program. Older and simpler display controllers.
48    #[error("this CRTC has no color pipeline at all")]
49    NoPipeline,
50
51    /// A LUT was asked for on the matrix stage, which does not take one.
52    #[error("{} is a matrix, not a LUT", .0.property())]
53    NotALut(Stage),
54
55    /// The kernel refused.
56    #[error(transparent)]
57    Core(#[from] CoreError),
58
59    /// The CRTC does not have this stage.
60    ///
61    /// Common and not a defect: plenty of drivers expose one or two of the
62    /// three. A caller checks
63    /// [`capabilities`](CrtcColorPipeline::capabilities) and works with what
64    /// is there.
65    #[error("this CRTC has no {} stage", .0.property())]
66    NoSuchStage(Stage),
67
68    /// A custom LUT was the wrong length.
69    ///
70    /// The driver states exactly how many entries it reads, and a blob of any
71    /// other length is rejected -- which fails the whole atomic commit, not
72    /// just this property.
73    #[error("{} wants {expected} entries, not {given}", .stage.property())]
74    WrongLutSize {
75        /// Which stage.
76        stage: Stage,
77        /// What the driver asks for.
78        expected: u32,
79        /// What was supplied.
80        given: usize,
81    },
82}
83
84/// The color pipeline of one CRTC.
85///
86/// Holds the blobs it creates and destroys them on drop, and borrows the
87/// device so a blob cannot outlive the descriptor that has to free it.
88#[derive(Debug)]
89pub struct CrtcColorPipeline<'a> {
90    device: &'a Device,
91    crtc_id: u32,
92    capabilities: CrtcCapabilities,
93    /// The property ids, resolved once.
94    ///
95    /// As the reference does: re-reading the CRTC's property set on every
96    /// apply cost eight to ten ioctls per commit for three numbers that never
97    /// change.
98    properties: StageProperties,
99    degamma: Option<PropertyBlob<'a>>,
100    ctm: Option<PropertyBlob<'a>>,
101    gamma: Option<PropertyBlob<'a>>,
102}
103
104/// The property ids for the three stages, where they exist.
105#[derive(Debug, Clone, Copy, Default)]
106struct StageProperties {
107    degamma: Option<u32>,
108    ctm: Option<u32>,
109    gamma: Option<u32>,
110}
111
112impl<'a> CrtcColorPipeline<'a> {
113    /// Read a CRTC's color capabilities and prepare to program them.
114    ///
115    /// A driver exposing only part of the pipeline -- vkms has gamma and
116    /// nothing else -- succeeds; the stages it lacks refuse.
117    ///
118    /// # Errors
119    ///
120    /// - [`PipelineError::NoPipeline`] if the CRTC has none of the three.
121    /// - [`PipelineError::Core`] if its properties cannot be read.
122    pub fn new(device: &'a Device, crtc_id: u32) -> Result<Self, PipelineError> {
123        let mut properties = PropertyStore::new();
124        properties.cache_properties(device, crtc_id, drmkit_core::ObjectType::Crtc)?;
125        let capabilities = CrtcCapabilities::from_properties(&properties, crtc_id);
126        if !capabilities.has_degamma_lut && !capabilities.has_ctm && !capabilities.has_gamma_lut {
127            return Err(PipelineError::NoPipeline);
128        }
129        let id = |stage: Stage| properties.property_id(crtc_id, stage.property()).ok();
130        Ok(Self {
131            device,
132            crtc_id,
133            capabilities,
134            properties: StageProperties {
135                degamma: id(Stage::Degamma),
136                ctm: id(Stage::Ctm),
137                gamma: id(Stage::Gamma),
138            },
139            degamma: None,
140            ctm: None,
141            gamma: None,
142        })
143    }
144
145    /// What this CRTC can do.
146    #[must_use]
147    pub const fn capabilities(&self) -> &CrtcCapabilities {
148        &self.capabilities
149    }
150
151    /// The blob currently staged for a stage, if any.
152    #[must_use]
153    pub fn blob_id(&self, stage: Stage) -> Option<u64> {
154        self.slot(stage).map(PropertyBlob::id)
155    }
156
157    /// Set every stage this CRTC has to identity.
158    ///
159    /// For taking over a display from something that left a curve on it: the
160    /// kernel keeps whatever the last client set, so a compositor that never
161    /// writes these inherits them.
162    ///
163    /// # Errors
164    ///
165    /// [`PipelineError::Core`] if a blob cannot be created.
166    pub fn set_identity(&mut self) -> Result<(), PipelineError> {
167        if self.capabilities.has_degamma_lut {
168            self.set_lut(Stage::Degamma, build_identity_lut)?;
169        }
170        if self.capabilities.has_ctm {
171            self.set_ctm_blob(build_identity_ctm())?;
172        }
173        if self.capabilities.has_gamma_lut {
174            self.set_lut(Stage::Gamma, build_identity_lut)?;
175        }
176        Ok(())
177    }
178
179    /// Degamma: PQ encoded to linear, for HDR10 content.
180    ///
181    /// # Errors
182    ///
183    /// - [`PipelineError::NoSuchStage`] if the CRTC has no degamma stage.
184    /// - [`PipelineError::Core`] if the blob cannot be created.
185    pub fn set_pq_to_linear(&mut self) -> Result<(), PipelineError> {
186        self.require(Stage::Degamma)?;
187        self.set_lut(Stage::Degamma, build_pq_eotf_lut)
188    }
189
190    /// Degamma: HLG encoded to scene-linear.
191    ///
192    /// # Errors
193    ///
194    /// As [`CrtcColorPipeline::set_pq_to_linear`].
195    pub fn set_hlg_to_linear(&mut self) -> Result<(), PipelineError> {
196        self.require(Stage::Degamma)?;
197        self.set_lut(Stage::Degamma, build_hlg_oetf_inverse_lut)
198    }
199
200    /// Matrix: BT.2020 to BT.709, in linear light.
201    ///
202    /// Needs a degamma stage in front of it that produces linear values.
203    ///
204    /// # Errors
205    ///
206    /// - [`PipelineError::NoSuchStage`] if the CRTC has no matrix stage.
207    /// - [`PipelineError::Core`] if the blob cannot be created.
208    pub fn set_bt2020_to_bt709(&mut self) -> Result<(), PipelineError> {
209        self.require(Stage::Ctm)?;
210        self.set_ctm_blob(build_bt2020_to_bt709_ctm())
211    }
212
213    /// Gamma: linear to PQ encoded, for HDR10 output.
214    ///
215    /// # Errors
216    ///
217    /// - [`PipelineError::NoSuchStage`] if the CRTC has no gamma stage.
218    /// - [`PipelineError::Core`] if the blob cannot be created.
219    pub fn set_linear_to_pq(&mut self) -> Result<(), PipelineError> {
220        self.require(Stage::Gamma)?;
221        self.set_lut(Stage::Gamma, build_pq_oetf_lut)
222    }
223
224    /// Stage a LUT of the caller's own.
225    ///
226    /// # Errors
227    ///
228    /// - [`PipelineError::NoSuchStage`] if the CRTC has no such stage,
229    ///   [`PipelineError::NotALut`] for the matrix, or
230    ///   [`PipelineError::WrongLutSize`] if `lut` is not the length the driver
231    ///   asks for.
232    /// - [`PipelineError::Core`] if the blob cannot be created.
233    pub fn set_custom_lut(&mut self, stage: Stage, lut: &[ColorLut]) -> Result<(), PipelineError> {
234        self.require(stage)?;
235        let expected = self.lut_size(stage).ok_or(PipelineError::NotALut(stage))?;
236        if lut.len() != expected as usize {
237            return Err(PipelineError::WrongLutSize {
238                stage,
239                expected,
240                given: lut.len(),
241            });
242        }
243        self.replace(stage, self.device.create_property_blob(lut)?);
244        Ok(())
245    }
246
247    /// Stage a matrix of the caller's own.
248    ///
249    /// # Errors
250    ///
251    /// As [`CrtcColorPipeline::set_bt2020_to_bt709`].
252    pub fn set_custom_ctm(&mut self, ctm: ColorCtm) -> Result<(), PipelineError> {
253        self.require(Stage::Ctm)?;
254        self.set_ctm_blob(ctm)
255    }
256
257    /// Write the staged blob ids into `request`.
258    ///
259    /// A stage that has not been set is left out. The kernel keeps whatever
260    /// the property already holds, which is what a caller programming only
261    /// gamma wants -- and is also why [`set_identity`](Self::set_identity)
262    /// exists for a caller that does not.
263    ///
264    /// # Errors
265    ///
266    /// [`PipelineError::Core`] if a property cannot be added to the request.
267    pub fn apply(&self, request: &mut AtomicRequest) -> Result<(), PipelineError> {
268        for stage in [Stage::Degamma, Stage::Ctm, Stage::Gamma] {
269            let (Some(blob), Some(property)) = (self.blob_id(stage), self.property_id(stage))
270            else {
271                continue;
272            };
273            request.add_property(self.crtc_id, property, blob)?;
274        }
275        Ok(())
276    }
277
278    /// Forget every blob without destroying it.
279    ///
280    /// For a session pause, which revokes the descriptor the blobs were made
281    /// on -- the kernel reclaimed them at that moment, and destroying through
282    /// the replacement descriptor would at best fail and at worst free a
283    /// different blob that happens to have the same id there.
284    pub fn forget_for_session_loss(&mut self) {
285        for slot in [&mut self.degamma, &mut self.ctm, &mut self.gamma] {
286            if let Some(blob) = slot.take() {
287                blob.forget();
288            }
289        }
290    }
291
292    fn require(&self, stage: Stage) -> Result<(), PipelineError> {
293        let present = match stage {
294            Stage::Degamma => self.capabilities.has_degamma_lut,
295            Stage::Ctm => self.capabilities.has_ctm,
296            Stage::Gamma => self.capabilities.has_gamma_lut,
297        };
298        if present {
299            Ok(())
300        } else {
301            Err(PipelineError::NoSuchStage(stage))
302        }
303    }
304
305    /// How many entries the driver reads for a LUT stage.
306    ///
307    /// `None` for the matrix, which does not take one.
308    const fn lut_size(&self, stage: Stage) -> Option<u32> {
309        match stage {
310            Stage::Degamma => Some(self.capabilities.degamma_lut_size),
311            Stage::Gamma => Some(self.capabilities.gamma_lut_size),
312            Stage::Ctm => None,
313        }
314    }
315
316    const fn property_id(&self, stage: Stage) -> Option<u32> {
317        match stage {
318            Stage::Degamma => self.properties.degamma,
319            Stage::Ctm => self.properties.ctm,
320            Stage::Gamma => self.properties.gamma,
321        }
322    }
323
324    const fn slot(&self, stage: Stage) -> Option<&PropertyBlob<'a>> {
325        match stage {
326            Stage::Degamma => self.degamma.as_ref(),
327            Stage::Ctm => self.ctm.as_ref(),
328            Stage::Gamma => self.gamma.as_ref(),
329        }
330    }
331
332    /// Replace a stage's blob, destroying whatever was there.
333    ///
334    /// Safe to do before the commit that switches the property over: the
335    /// kernel holds its own reference to the blob a property currently names,
336    /// so the one being replaced stays alive until nothing points at it.
337    fn replace(&mut self, stage: Stage, blob: PropertyBlob<'a>) {
338        let slot = match stage {
339            Stage::Degamma => &mut self.degamma,
340            Stage::Ctm => &mut self.ctm,
341            Stage::Gamma => &mut self.gamma,
342        };
343        *slot = Some(blob);
344    }
345
346    fn set_lut(&mut self, stage: Stage, build: fn(&mut [ColorLut])) -> Result<(), PipelineError> {
347        let size = self.lut_size(stage).ok_or(PipelineError::NotALut(stage))? as usize;
348        let mut lut = vec![ColorLut::default(); size];
349        build(&mut lut);
350        let blob = self.device.create_property_blob(lut.as_slice())?;
351        self.replace(stage, blob);
352        Ok(())
353    }
354
355    fn set_ctm_blob(&mut self, ctm: ColorCtm) -> Result<(), PipelineError> {
356        let blob = self.device.create_property_blob(&ctm)?;
357        self.replace(Stage::Ctm, blob);
358        Ok(())
359    }
360}