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