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