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 ///
285 /// The pipeline stays bound to the same [`Device`], so it is only of use
286 /// if that descriptor is still live; after a real session loss nothing
287 /// staged on it would reach the resumed one.
288 #[deprecated(
289 since = "0.1.3",
290 note = "use `pause` and `PausedPipeline::resume`, which rebind to the resumed device"
291 )]
292 pub fn forget_for_session_loss(&mut self) {
293 self.forget_blobs();
294 }
295
296 /// Give up the device for a session pause, as the reference's
297 /// `clear_for_session_loss` does.
298 ///
299 /// Every blob is forgotten, not destroyed: the pause revoked the
300 /// descriptor they were made on, and the kernel reclaimed them then. What
301 /// comes back has no setters: the reference's refuse with `EBADF` until
302 /// it is rebound, and here there is nothing to call.
303 #[must_use]
304 pub fn pause(mut self) -> PausedPipeline {
305 self.forget_blobs();
306 PausedPipeline {
307 crtc_id: self.crtc_id,
308 capabilities: self.capabilities,
309 properties: self.properties,
310 }
311 }
312
313 fn forget_blobs(&mut self) {
314 for slot in [&mut self.degamma, &mut self.ctm, &mut self.gamma] {
315 if let Some(blob) = slot.take() {
316 blob.forget();
317 }
318 }
319 }
320
321 fn require(&self, stage: Stage) -> Result<(), PipelineError> {
322 let present = match stage {
323 Stage::Degamma => self.capabilities.has_degamma_lut,
324 Stage::Ctm => self.capabilities.has_ctm,
325 Stage::Gamma => self.capabilities.has_gamma_lut,
326 };
327 if present {
328 Ok(())
329 } else {
330 Err(PipelineError::NoSuchStage(stage))
331 }
332 }
333
334 /// How many entries the driver reads for a LUT stage.
335 ///
336 /// `None` for the matrix, which does not take one.
337 const fn lut_size(&self, stage: Stage) -> Option<u32> {
338 match stage {
339 Stage::Degamma => Some(self.capabilities.degamma_lut_size),
340 Stage::Gamma => Some(self.capabilities.gamma_lut_size),
341 Stage::Ctm => None,
342 }
343 }
344
345 const fn property_id(&self, stage: Stage) -> Option<u32> {
346 match stage {
347 Stage::Degamma => self.properties.degamma,
348 Stage::Ctm => self.properties.ctm,
349 Stage::Gamma => self.properties.gamma,
350 }
351 }
352
353 const fn slot(&self, stage: Stage) -> Option<&PropertyBlob<'a>> {
354 match stage {
355 Stage::Degamma => self.degamma.as_ref(),
356 Stage::Ctm => self.ctm.as_ref(),
357 Stage::Gamma => self.gamma.as_ref(),
358 }
359 }
360
361 /// Replace a stage's blob, destroying whatever was there.
362 ///
363 /// Safe to do before the commit that switches the property over: the
364 /// kernel holds its own reference to the blob a property currently names,
365 /// so the one being replaced stays alive until nothing points at it.
366 fn replace(&mut self, stage: Stage, blob: PropertyBlob<'a>) {
367 let slot = match stage {
368 Stage::Degamma => &mut self.degamma,
369 Stage::Ctm => &mut self.ctm,
370 Stage::Gamma => &mut self.gamma,
371 };
372 *slot = Some(blob);
373 }
374
375 fn set_lut(&mut self, stage: Stage, build: fn(&mut [ColorLut])) -> Result<(), PipelineError> {
376 let size = self.lut_size(stage).ok_or(PipelineError::NotALut(stage))? as usize;
377 let mut lut = vec![ColorLut::default(); size];
378 build(&mut lut);
379 let blob = self.device.create_property_blob(lut.as_slice())?;
380 self.replace(stage, blob);
381 Ok(())
382 }
383
384 fn set_ctm_blob(&mut self, ctm: ColorCtm) -> Result<(), PipelineError> {
385 let blob = self.device.create_property_blob(&ctm)?;
386 self.replace(Stage::Ctm, blob);
387 Ok(())
388 }
389}
390
391/// A color pipeline whose device has gone away.
392///
393/// What [`CrtcColorPipeline::pause`] leaves: the CRTC, its capabilities and
394/// its property ids, and no blobs. A type rather than a flag, so that nothing
395/// can be staged on a descriptor that no longer exists.
396#[derive(Debug, Clone)]
397pub struct PausedPipeline {
398 crtc_id: u32,
399 capabilities: CrtcCapabilities,
400 properties: StageProperties,
401}
402
403impl PausedPipeline {
404 /// Rebind to the resumed device, as the reference's
405 /// `on_session_resumed` does.
406 ///
407 /// The property ids are kept: the CRTC is the same object on the new
408 /// descriptor. The staged stages are gone, so set them again before the
409 /// next [`apply`](CrtcColorPipeline::apply).
410 #[must_use]
411 pub const fn resume(self, device: &Device) -> CrtcColorPipeline<'_> {
412 CrtcColorPipeline {
413 device,
414 crtc_id: self.crtc_id,
415 capabilities: self.capabilities,
416 properties: self.properties,
417 degamma: None,
418 ctm: None,
419 gamma: None,
420 }
421 }
422
423 /// What this CRTC can do.
424 #[must_use]
425 pub const fn capabilities(&self) -> &CrtcCapabilities {
426 &self.capabilities
427 }
428}