1use crate::render::gpu_colour_kernel::{cpu_batch_emf_to_display_gamut_mapped, diff_cpu_gpu};
24use crate::render::spectral_kernel::{emf_to_spd, spd_to_xyz, Xyz};
25
26#[derive(Debug, Clone, Copy)]
32pub struct GoldenVector {
33 pub alpha: f32,
34 pub mu: f32,
35 pub sigma: f32,
36 pub expected_xyz: Xyz,
37}
38
39pub fn golden_vectors() -> Vec<GoldenVector> {
42 (0..=10)
43 .map(|i| {
44 let sigma = i as f32 / 10.0;
45 let spd = emf_to_spd(1.0, 0.0, sigma);
46 let xyz = spd_to_xyz(&spd);
47 GoldenVector {
48 alpha: 1.0,
49 mu: 0.0,
50 sigma,
51 expected_xyz: xyz,
52 }
53 })
54 .collect()
55}
56
57pub fn verify_golden_vectors() -> usize {
60 let vectors = golden_vectors();
61 let mut mismatches = 0;
62
63 for v in &vectors {
64 let spd = emf_to_spd(v.alpha, v.mu, v.sigma);
65 let xyz = spd_to_xyz(&spd);
66 let dx = (xyz.x - v.expected_xyz.x).abs();
67 let dy = (xyz.y - v.expected_xyz.y).abs();
68 let dz = (xyz.z - v.expected_xyz.z).abs();
69 if dx > 1e-6 || dy > 1e-6 || dz > 1e-6 {
70 mismatches += 1;
71 }
72 }
73
74 mismatches
75}
76
77pub fn determinism_check_xyz(alpha: f32, mu: f32, sigma: f32, runs: usize) -> bool {
84 if runs == 0 {
85 return true;
86 }
87 let spd = emf_to_spd(alpha, mu, sigma);
88 let first = spd_to_xyz(&spd);
89
90 for _ in 1..runs {
91 let spd = emf_to_spd(alpha, mu, sigma);
92 let xyz = spd_to_xyz(&spd);
93 if xyz != first {
94 return false;
95 }
96 }
97 true
98}
99
100pub fn determinism_check_batch(emf: &[f32], runs: usize) -> bool {
103 if runs == 0 || emf.is_empty() {
104 return true;
105 }
106 let n = emf.len() / 3;
107 let mut first = vec![0u8; n * 3];
108 cpu_batch_emf_to_display_gamut_mapped(emf, &mut first);
109
110 for _ in 1..runs {
111 let mut out = vec![0u8; n * 3];
112 cpu_batch_emf_to_display_gamut_mapped(emf, &mut out);
113 if out != first {
114 return false;
115 }
116 }
117 true
118}
119
120pub fn fnv1a_hash(data: &[u8]) -> u32 {
127 let mut hash: u32 = 0x811c9dc5;
128 for &b in data {
129 hash ^= b as u32;
130 hash = hash.wrapping_mul(0x01000193);
131 }
132 hash
133}
134
135pub fn batch_display_rgb_hash(emf: &[f32]) -> u32 {
138 let n = emf.len() / 3;
139 let mut out = vec![0u8; n * 3];
140 cpu_batch_emf_to_display_gamut_mapped(emf, &mut out);
141 fnv1a_hash(&out)
142}
143
144pub fn cpu_gpu_differential(emf: &[f32]) -> (Vec<u8>, usize) {
155 let n = emf.len() / 3;
156 let mut cpu_out = vec![0u8; n * 3];
157 cpu_batch_emf_to_display_gamut_mapped(emf, &mut cpu_out);
158
159 let mismatches = diff_cpu_gpu(&cpu_out, &cpu_out);
161 (cpu_out, mismatches)
162}
163
164#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::render::spectral_kernel::emf_to_linear_rgb;
172
173 #[test]
174 fn golden_vectors_all_pass() {
175 let mismatches = verify_golden_vectors();
176 assert_eq!(
177 mismatches, 0,
178 "all golden vectors must match current implementation"
179 );
180 }
181
182 #[test]
183 fn golden_vectors_count() {
184 let vectors = golden_vectors();
185 assert_eq!(vectors.len(), 11, "should have 11 golden vectors (σ=0..1)");
186 }
187
188 #[test]
189 fn golden_vectors_sigma_sweep() {
190 let vectors = golden_vectors();
191 for (i, v) in vectors.iter().enumerate() {
192 let expected_sigma = i as f32 / 10.0;
193 assert!(
194 (v.sigma - expected_sigma).abs() < 1e-6,
195 "vector {} should have σ={}",
196 i,
197 expected_sigma
198 );
199 }
200 }
201
202 #[test]
203 fn golden_vectors_xyz_finite() {
204 let vectors = golden_vectors();
205 for v in &vectors {
206 assert!(v.expected_xyz.x.is_finite(), "X must be finite");
207 assert!(v.expected_xyz.y.is_finite(), "Y must be finite");
208 assert!(v.expected_xyz.z.is_finite(), "Z must be finite");
209 }
210 }
211
212 #[test]
213 fn determinism_xyz_100_runs() {
214 assert!(
215 determinism_check_xyz(1.0, 0.3, 0.5, 100),
216 "EMF→XYZ must be deterministic over 100 runs"
217 );
218 }
219
220 #[test]
221 fn determinism_batch_50_runs() {
222 let emf = [1.0, 0.0, 0.0, 1.0, 0.5, 0.5, 1.0, 0.0, 1.0, 0.8, 0.2, 0.6];
223 assert!(
224 determinism_check_batch(&emf, 50),
225 "batch EMF→RGB must be deterministic over 50 runs"
226 );
227 }
228
229 #[test]
230 fn fnv1a_hash_deterministic() {
231 let data = [1u8, 2, 3, 4, 5];
232 let h1 = fnv1a_hash(&data);
233 let h2 = fnv1a_hash(&data);
234 assert_eq!(h1, h2, "FNV-1a must be deterministic");
235 }
236
237 #[test]
238 fn fnv1a_hash_known_vector() {
239 assert_eq!(
241 fnv1a_hash(&[]),
242 0x811c9dc5,
243 "FNV-1a of empty = offset basis"
244 );
245 }
246
247 #[test]
248 fn fnv1a_hash_differs_on_input() {
249 let a = [0u8, 0, 0];
250 let b = [0u8, 0, 1];
251 assert_ne!(
252 fnv1a_hash(&a),
253 fnv1a_hash(&b),
254 "different inputs must hash differently"
255 );
256 }
257
258 #[test]
259 fn batch_display_rgb_hash_deterministic() {
260 let emf = [1.0, 0.3, 0.5, 0.8, 0.2, 0.7];
261 let h1 = batch_display_rgb_hash(&emf);
262 let h2 = batch_display_rgb_hash(&emf);
263 assert_eq!(h1, h2, "batch hash must be deterministic");
264 }
265
266 #[test]
267 fn batch_display_rgb_hash_differs_on_input() {
268 let emf_a = [1.0, 0.0, 0.0];
269 let emf_b = [1.0, 0.0, 1.0];
270 assert_ne!(
271 batch_display_rgb_hash(&emf_a),
272 batch_display_rgb_hash(&emf_b),
273 "different EMF inputs must produce different hashes"
274 );
275 }
276
277 #[test]
278 fn cpu_gpu_differential_self_zero_mismatches() {
279 let emf = [1.0, 0.3, 0.5, 0.8, 0.2, 0.7, 1.0, 0.0, 0.0];
280 let (_, mismatches) = cpu_gpu_differential(&emf);
281 assert_eq!(mismatches, 0, "self-comparison should have 0 mismatches");
282 }
283
284 #[test]
285 fn cpu_gpu_differential_valid_output() {
286 let emf = [1.0, 0.3, 0.5, 0.8, 0.2, 0.7, 1.0, 0.0, 0.0];
287 let (_out, _) = cpu_gpu_differential(&emf);
288 }
290
291 #[test]
292 fn full_pipeline_sweep_no_nans() {
293 for i in 0..=100 {
294 let sigma = i as f32 / 100.0;
295 for j in 0..=10 {
296 let mu = j as f32 / 10.0;
297 let rgb = emf_to_linear_rgb(1.0, mu, sigma);
298 assert!(rgb.r.is_finite(), "R NaN at σ={}, μ={}", sigma, mu);
299 assert!(rgb.g.is_finite(), "G NaN at σ={}, μ={}", sigma, mu);
300 assert!(rgb.b.is_finite(), "B NaN at σ={}, μ={}", sigma, mu);
301 }
302 }
303 }
304
305 #[test]
306 fn golden_vectors_monotone_y_at_mid_sigma() {
307 let vectors = golden_vectors();
309 let y_values: Vec<f32> = vectors.iter().map(|v| v.expected_xyz.y).collect();
310 let max_idx = y_values
311 .iter()
312 .enumerate()
313 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
314 .unwrap()
315 .0;
316 assert!(
318 max_idx >= 3 && max_idx <= 7,
319 "Y peak should be near green (mid σ), got index {}",
320 max_idx
321 );
322 }
323}