1use std::collections::HashMap;
2use std::fmt;
3
4use half::f16;
5use wgpu::util::DeviceExt;
6
7use crate::{EffectResourceKind, RenderError, Renderer};
8
9#[derive(Clone, Debug, PartialEq)]
11pub struct Lut3dData {
12 pub edge_length: u32,
14 pub domain_min: [f32; 3],
16 pub domain_max: [f32; 3],
18 pub values: Vec<[f32; 3]>,
20}
21
22impl Lut3dData {
23 pub fn from_cube(text: &str) -> Result<Self, LutParseError> {
32 let mut edge = None;
33 let mut domain_min = [0.0; 3];
34 let mut domain_max = [1.0; 3];
35 let mut values = Vec::new();
36 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
37 for (index, raw) in text.lines().enumerate() {
38 let line_number = index + 1;
39 let line = raw.split('#').next().unwrap_or_default().trim();
40 if line.is_empty() || line.starts_with("TITLE") {
41 continue;
42 }
43 let fields = line.split_whitespace().collect::<Vec<_>>();
44 match fields[0] {
45 "LUT_3D_SIZE" if fields.len() == 2 => {
46 let parsed = fields[1]
47 .parse::<u32>()
48 .map_err(|_| LutParseError::new(line_number, "invalid LUT_3D_SIZE"))?;
49 if parsed < 2 {
50 return Err(LutParseError::new(
51 line_number,
52 "LUT_3D_SIZE must be at least two",
53 ));
54 }
55 edge = Some(parsed);
56 }
57 "LUT_1D_SIZE" => {
58 return Err(LutParseError::new(
59 line_number,
60 "one-dimensional .cube tables are not supported",
61 ));
62 }
63 "DOMAIN_MIN" if fields.len() == 4 => {
64 domain_min = parse_triplet(&fields[1..], line_number)?;
65 }
66 "DOMAIN_MAX" if fields.len() == 4 => {
67 domain_max = parse_triplet(&fields[1..], line_number)?;
68 }
69 "LUT_3D_INPUT_RANGE" if fields.len() == 3 => {
70 let min = parse_component(fields[1], line_number)?;
71 let max = parse_component(fields[2], line_number)?;
72 domain_min = [min; 3];
73 domain_max = [max; 3];
74 }
75 directive if directive.chars().next().is_some_and(char::is_alphabetic) => {
76 return Err(LutParseError::new(
77 line_number,
78 format!("unsupported .cube directive {directive}"),
79 ));
80 }
81 _ if fields.len() == 3 => values.push(parse_triplet(&fields, line_number)?),
82 _ => {
83 return Err(LutParseError::new(
84 line_number,
85 "expected an RGB sample or supported .cube directive",
86 ));
87 }
88 }
89 }
90 let edge = edge.ok_or_else(|| LutParseError::new(0, "missing LUT_3D_SIZE"))?;
91 let expected = usize::try_from(edge)
92 .ok()
93 .and_then(|value| value.checked_pow(3))
94 .ok_or_else(|| LutParseError::new(0, "LUT size is too large"))?;
95 if values.len() != expected {
96 return Err(LutParseError::new(
97 0,
98 format!("expected {expected} RGB samples, found {}", values.len()),
99 ));
100 }
101 if domain_min
102 .iter()
103 .zip(domain_max)
104 .any(|(min, max)| *min >= max)
105 {
106 return Err(LutParseError::new(
107 0,
108 ".cube input domain minimum must be below its maximum",
109 ));
110 }
111 Ok(Self {
112 edge_length: edge,
113 domain_min,
114 domain_max,
115 values,
116 })
117 }
118
119 pub(crate) fn is_valid(&self, maximum: u32) -> bool {
120 self.edge_length >= 2
121 && self.edge_length <= maximum
122 && usize::try_from(self.edge_length)
123 .ok()
124 .and_then(|value| value.checked_pow(3))
125 .is_some_and(|expected| expected == self.values.len())
126 && self.values.iter().flatten().all(|value| value.is_finite())
127 && self
128 .domain_min
129 .iter()
130 .zip(self.domain_max)
131 .all(|(min, max)| min.is_finite() && max.is_finite() && *min < max)
132 }
133}
134
135fn parse_triplet(fields: &[&str], line: usize) -> Result<[f32; 3], LutParseError> {
136 let mut output = [0.0; 3];
137 for (target, field) in output.iter_mut().zip(fields) {
138 *target = parse_component(field, line)?;
139 }
140 Ok(output)
141}
142
143fn parse_component(field: &str, line: usize) -> Result<f32, LutParseError> {
144 field
145 .parse::<f32>()
146 .ok()
147 .filter(|value| value.is_finite())
148 .ok_or_else(|| LutParseError::new(line, "invalid finite RGB component"))
149}
150
151#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct LutParseError {
154 line: usize,
155 message: String,
156}
157
158impl LutParseError {
159 fn new(line: usize, message: impl Into<String>) -> Self {
160 Self {
161 line,
162 message: message.into(),
163 }
164 }
165}
166
167impl fmt::Display for LutParseError {
168 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169 if self.line == 0 {
170 formatter.write_str(&self.message)
171 } else {
172 write!(formatter, ".cube line {}: {}", self.line, self.message)
173 }
174 }
175}
176
177impl std::error::Error for LutParseError {}
178
179#[derive(Debug)]
180pub(crate) struct RegisteredEffectResource {
181 pub(crate) kind: EffectResourceKind,
182 pub(crate) _texture: wgpu::Texture,
183 pub(crate) view: wgpu::TextureView,
184 pub(crate) metadata: wgpu::Buffer,
185}
186
187pub(crate) type EffectResourceRegistry = HashMap<String, RegisteredEffectResource>;
188
189impl Renderer {
190 pub fn register_lut3d(
196 &mut self,
197 id: impl Into<String>,
198 data: &Lut3dData,
199 ) -> Result<(), RenderError> {
200 let id = id.into();
201 if id.is_empty() || !data.is_valid(self.limits.max_texture_dimension_3d) {
202 return Err(RenderError::InvalidEffectResource);
203 }
204 let mut bytes = Vec::with_capacity(data.values.len() * 8);
205 for rgb in &data.values {
206 for value in [rgb[0], rgb[1], rgb[2], 1.0] {
207 bytes.extend_from_slice(&f16::from_f32(value).to_bits().to_le_bytes());
208 }
209 }
210 let (texture, error) = crate::gpu_errors::capture_creation(self.gpu.device(), || {
211 self.gpu.device().create_texture_with_data(
212 self.gpu.queue(),
213 &wgpu::TextureDescriptor {
214 label: Some(&format!("alpha2 LUT resource {id}")),
215 size: wgpu::Extent3d {
216 width: data.edge_length,
217 height: data.edge_length,
218 depth_or_array_layers: data.edge_length,
219 },
220 mip_level_count: 1,
221 sample_count: 1,
222 dimension: wgpu::TextureDimension::D3,
223 format: wgpu::TextureFormat::Rgba16Float,
224 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
225 view_formats: &[],
226 },
227 wgpu::util::TextureDataOrder::LayerMajor,
228 &bytes,
229 )
230 });
231 if let Some(kind) = error {
232 return Err(RenderError::Gpu { kind });
233 }
234 let view = crate::pool::default_view(&texture);
235 let mut metadata_bytes = Vec::with_capacity(32);
236 for value in [
237 data.domain_min[0],
238 data.domain_min[1],
239 data.domain_min[2],
240 0.0,
241 data.domain_max[0],
242 data.domain_max[1],
243 data.domain_max[2],
244 0.0,
245 ] {
246 metadata_bytes.extend_from_slice(&value.to_le_bytes());
247 }
248 let metadata = self
249 .gpu
250 .device()
251 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
252 label: Some(&format!("alpha2 LUT metadata {id}")),
253 contents: &metadata_bytes,
254 usage: wgpu::BufferUsages::UNIFORM,
255 });
256 self.effect_resources.insert(
257 id,
258 RegisteredEffectResource {
259 kind: EffectResourceKind::Lut3d,
260 _texture: texture,
261 view,
262 metadata,
263 },
264 );
265 self.clear_result_cache_for(crate::CacheInvalidation::EffectResources);
267 Ok(())
268 }
269
270 pub fn unregister_effect_resource(&mut self, id: &str) -> bool {
272 let removed = self.effect_resources.remove(id).is_some();
273 if removed {
274 self.clear_result_cache_for(crate::CacheInvalidation::EffectResources);
275 }
276 removed
277 }
278
279 pub fn clear_effect_resources(&mut self) {
281 self.effect_resources.clear();
282 self.clear_result_cache_for(crate::CacheInvalidation::EffectResources);
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn parses_a_three_dimensional_cube() {
292 let data = Lut3dData::from_cube(
293 "TITLE \"identity\"\nLUT_3D_SIZE 2\nDOMAIN_MIN 0 0 0\nDOMAIN_MAX 1 1 1\n\
294 0 0 0\n1 0 0\n0 1 0\n1 1 0\n0 0 1\n1 0 1\n0 1 1\n1 1 1\n",
295 )
296 .expect("valid cube");
297 assert_eq!(data.edge_length, 2);
298 assert_triplet_eq(data.domain_min, [0.0; 3]);
299 assert_eq!(data.values.len(), 8);
300 assert_triplet_eq(data.values[7], [1.0, 1.0, 1.0]);
301 }
302
303 #[test]
304 fn accepts_non_unit_domains_and_rejects_invalid_cubes() {
305 let ranged = Lut3dData::from_cube(
306 "LUT_3D_SIZE 2\nDOMAIN_MIN -1 -0.5 0\nDOMAIN_MAX 1 1.5 2\n\
307 0 0 0\n1 0 0\n0 1 0\n1 1 0\n0 0 1\n1 0 1\n0 1 1\n1 1 1\n",
308 )
309 .expect("per-channel domains are supported");
310 assert_triplet_eq(ranged.domain_min, [-1.0, -0.5, 0.0]);
311 assert_triplet_eq(ranged.domain_max, [1.0, 1.5, 2.0]);
312 assert!(
313 Lut3dData::from_cube("LUT_3D_SIZE 2\nDOMAIN_MIN 2 2 2\nDOMAIN_MAX 1 1 1\n").is_err()
314 );
315 assert!(Lut3dData::from_cube("LUT_3D_SIZE 2\n0 0 0\n").is_err());
316 }
317
318 #[test]
319 fn a_cube_written_with_a_byte_order_mark_or_crlf_parses_as_without() {
320 let body = "LUT_3D_SIZE 2\n0 0 0\n1 0 0\n0 1 0\n1 1 0\n0 0 1\n1 0 1\n0 1 1\n1 1 1\n";
322 let plain = Lut3dData::from_cube(body).expect("valid cube");
323 for text in [
324 format!("\u{feff}{body}"),
325 format!("\u{feff}TITLE \"bom\"\n{body}"),
326 format!("\u{feff}# written by a Windows tool\n{body}"),
327 body.replace('\n', "\r\n"),
328 format!("\u{feff}{}", body.replace('\n', "\r\n")),
329 ] {
330 assert_eq!(Lut3dData::from_cube(&text), Ok(plain.clone()), "{text:?}");
331 }
332 let inner = Lut3dData::from_cube(&body.replace("0 0 1\n", "\u{feff}0 0 1\n"))
334 .expect_err("a mark inside the file is not text the parser knows");
335 assert_eq!(inner.line, 6);
336 }
337
338 #[test]
339 fn a_cube_refuses_one_dimensional_tables_and_unknown_directives_by_line() {
340 let one_dimensional = Lut3dData::from_cube("TITLE \"1d\"\nLUT_1D_SIZE 2\n0 0 0\n1 1 1\n")
341 .expect_err("a 1D table is a different transform");
342 assert_eq!(one_dimensional.line, 2);
343 assert!(one_dimensional.to_string().contains("one-dimensional"));
344 let unknown = Lut3dData::from_cube("LUT_3D_SIZE 2\nLUT_3D_SHAPER 4\n")
345 .expect_err("an unknown directive is refused, not skipped");
346 assert_eq!(unknown.line, 2);
347 assert!(unknown.to_string().contains("LUT_3D_SHAPER"));
348 }
349
350 fn assert_triplet_eq(actual: [f32; 3], expected: [f32; 3]) {
351 assert!(
352 actual
353 .iter()
354 .zip(expected)
355 .all(|(actual, expected)| (*actual - expected).abs() <= f32::EPSILON)
356 );
357 }
358}