openentropy_core/sources/
gpu.rs1use std::io::Write;
8use std::process::Command;
9use std::time::Instant;
10
11use tempfile::NamedTempFile;
12
13use crate::source::{EntropySource, SourceCategory, SourceInfo};
14
15const SIPS_PATH: &str = "/usr/bin/sips";
17
18static GPU_TIMING_INFO: SourceInfo = SourceInfo {
19 name: "gpu_timing",
20 description: "GPU dispatch timing jitter via sips image processing",
21 physics: "Dispatches Metal compute shaders and measures completion time. GPU timing \
22 jitter comes from: shader core occupancy, register file allocation, shared \
23 memory bank conflicts, warp/wavefront scheduling, power throttling, and memory \
24 controller arbitration between GPU cores, CPU, and Neural Engine on Apple \
25 Silicon's unified memory.",
26 category: SourceCategory::Hardware,
27 platform_requirements: &["macos"],
28 entropy_rate_estimate: 300.0,
29 composite: false,
30};
31
32fn create_minimal_tiff() -> Vec<u8> {
35 let mut tiff = Vec::new();
36
37 tiff.extend_from_slice(&[0x49, 0x49]); tiff.extend_from_slice(&42u16.to_le_bytes()); tiff.extend_from_slice(&8u32.to_le_bytes()); let num_entries: u16 = 8;
44 tiff.extend_from_slice(&num_entries.to_le_bytes());
45
46 let write_entry = |tiff: &mut Vec<u8>, tag: u16, typ: u16, count: u32, value: u32| {
49 tiff.extend_from_slice(&tag.to_le_bytes());
50 tiff.extend_from_slice(&typ.to_le_bytes());
51 tiff.extend_from_slice(&count.to_le_bytes());
52 tiff.extend_from_slice(&value.to_le_bytes());
53 };
54
55 write_entry(&mut tiff, 256, 3, 1, 8);
57 write_entry(&mut tiff, 257, 3, 1, 8);
59 write_entry(&mut tiff, 258, 3, 1, 8);
61 write_entry(&mut tiff, 259, 3, 1, 1);
63 write_entry(&mut tiff, 262, 3, 1, 1);
65 let pixel_offset: u32 = 8 + 2 + (num_entries as u32) * 12 + 4;
69 write_entry(&mut tiff, 273, 4, 1, pixel_offset);
70 write_entry(&mut tiff, 278, 3, 1, 8);
72 write_entry(&mut tiff, 279, 4, 1, 64);
74
75 tiff.extend_from_slice(&0u32.to_le_bytes());
77
78 tiff.extend_from_slice(&[128u8; 64]);
80
81 tiff
82}
83
84pub struct GPUTimingSource;
86
87impl EntropySource for GPUTimingSource {
88 fn info(&self) -> &SourceInfo {
89 &GPU_TIMING_INFO
90 }
91
92 fn is_available(&self) -> bool {
93 std::path::Path::new(SIPS_PATH).exists()
94 }
95
96 fn collect(&self, n_samples: usize) -> Vec<u8> {
97 let mut tmpfile = match NamedTempFile::with_suffix(".tiff") {
99 Ok(f) => f,
100 Err(_) => return Vec::new(),
101 };
102
103 let tiff_data = create_minimal_tiff();
104 if tmpfile.write_all(&tiff_data).is_err() {
105 return Vec::new();
106 }
107 if tmpfile.flush().is_err() {
108 return Vec::new();
109 }
110
111 let path = tmpfile.path().to_path_buf();
112 let mut output = Vec::with_capacity(n_samples);
113 let mut prev_ns: u64 = 0;
114
115 let sizes = [16, 32, 24, 48, 12, 36];
118 let iterations = n_samples + 1;
119
120 for i in 0..iterations {
121 let size = sizes[i % sizes.len()];
122
123 let t0 = Instant::now();
124
125 let result = Command::new(SIPS_PATH)
126 .args([
127 "--resampleWidth",
128 &size.to_string(),
129 "--resampleHeight",
130 &size.to_string(),
131 path.to_str().unwrap_or(""),
132 ])
133 .stdout(std::process::Stdio::null())
134 .stderr(std::process::Stdio::null())
135 .status();
136
137 let elapsed_ns = t0.elapsed().as_nanos() as u64;
138
139 if result.is_err() {
140 continue;
141 }
142
143 if i > 0 {
144 let delta = elapsed_ns.wrapping_sub(prev_ns);
145 let mixed = (delta as u8) ^ ((delta >> 8) as u8) ^ ((delta >> 16) as u8);
147 output.push(mixed);
148
149 if output.len() >= n_samples {
150 break;
151 }
152 }
153
154 prev_ns = elapsed_ns;
155 }
156
157 output.truncate(n_samples);
158 output
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn gpu_timing_info() {
168 let src = GPUTimingSource;
169 assert_eq!(src.name(), "gpu_timing");
170 assert_eq!(src.info().category, SourceCategory::Hardware);
171 }
172
173 #[test]
174 fn minimal_tiff_is_valid() {
175 let tiff = create_minimal_tiff();
176 assert_eq!(&tiff[0..2], b"II");
178 assert_eq!(u16::from_le_bytes([tiff[2], tiff[3]]), 42);
179 assert!(tiff.len() > 64);
181 }
182
183 #[test]
184 #[cfg(target_os = "macos")]
185 fn gpu_timing_availability() {
186 let src = GPUTimingSource;
187 assert!(src.is_available());
189 }
190}