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, Platform, 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::GPU,
27 platform: Platform::MacOS,
28 requirements: &[],
29 entropy_rate_estimate: 300.0,
30 composite: false,
31};
32
33fn create_minimal_tiff() -> Vec<u8> {
36 let mut tiff = Vec::new();
37
38 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;
45 tiff.extend_from_slice(&num_entries.to_le_bytes());
46
47 let write_entry = |tiff: &mut Vec<u8>, tag: u16, typ: u16, count: u32, value: u32| {
50 tiff.extend_from_slice(&tag.to_le_bytes());
51 tiff.extend_from_slice(&typ.to_le_bytes());
52 tiff.extend_from_slice(&count.to_le_bytes());
53 tiff.extend_from_slice(&value.to_le_bytes());
54 };
55
56 write_entry(&mut tiff, 256, 3, 1, 8);
58 write_entry(&mut tiff, 257, 3, 1, 8);
60 write_entry(&mut tiff, 258, 3, 1, 8);
62 write_entry(&mut tiff, 259, 3, 1, 1);
64 write_entry(&mut tiff, 262, 3, 1, 1);
66 let pixel_offset: u32 = 8 + 2 + (num_entries as u32) * 12 + 4;
70 write_entry(&mut tiff, 273, 4, 1, pixel_offset);
71 write_entry(&mut tiff, 278, 3, 1, 8);
73 write_entry(&mut tiff, 279, 4, 1, 64);
75
76 tiff.extend_from_slice(&0u32.to_le_bytes());
78
79 tiff.extend_from_slice(&[128u8; 64]);
81
82 tiff
83}
84
85pub struct GPUTimingSource;
87
88impl EntropySource for GPUTimingSource {
89 fn info(&self) -> &SourceInfo {
90 &GPU_TIMING_INFO
91 }
92
93 fn is_available(&self) -> bool {
94 std::path::Path::new(SIPS_PATH).exists()
95 }
96
97 fn collect(&self, n_samples: usize) -> Vec<u8> {
98 let mut tmpfile = match NamedTempFile::with_suffix(".tiff") {
100 Ok(f) => f,
101 Err(_) => return Vec::new(),
102 };
103
104 let tiff_data = create_minimal_tiff();
105 if tmpfile.write_all(&tiff_data).is_err() {
106 return Vec::new();
107 }
108 if tmpfile.flush().is_err() {
109 return Vec::new();
110 }
111
112 let path = tmpfile.path().to_path_buf();
113 let mut output = Vec::with_capacity(n_samples);
114 let mut prev_ns: u64 = 0;
115
116 let sizes = [16, 32, 24, 48, 12, 36];
119 let iterations = n_samples + 1;
120
121 for i in 0..iterations {
122 let size = sizes[i % sizes.len()];
123
124 let t0 = Instant::now();
125
126 let result = Command::new(SIPS_PATH)
127 .args([
128 "--resampleWidth",
129 &size.to_string(),
130 "--resampleHeight",
131 &size.to_string(),
132 path.to_str().unwrap_or(""),
133 ])
134 .stdout(std::process::Stdio::null())
135 .stderr(std::process::Stdio::null())
136 .status();
137
138 let elapsed_ns = t0.elapsed().as_nanos() as u64;
139
140 if result.is_err() {
141 continue;
142 }
143
144 if i > 0 {
145 let delta = elapsed_ns.wrapping_sub(prev_ns);
146 let mixed = (delta as u8) ^ ((delta >> 8) as u8) ^ ((delta >> 16) as u8);
148 output.push(mixed);
149
150 if output.len() >= n_samples {
151 break;
152 }
153 }
154
155 prev_ns = elapsed_ns;
156 }
157
158 output.truncate(n_samples);
159 output
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn gpu_timing_info() {
169 let src = GPUTimingSource;
170 assert_eq!(src.name(), "gpu_timing");
171 assert_eq!(src.info().category, SourceCategory::GPU);
172 }
173
174 #[test]
175 fn minimal_tiff_is_valid() {
176 let tiff = create_minimal_tiff();
177 assert_eq!(&tiff[0..2], b"II");
179 assert_eq!(u16::from_le_bytes([tiff[2], tiff[3]]), 42);
180 assert!(tiff.len() > 64);
182 }
183
184 #[test]
185 #[cfg(target_os = "macos")]
186 fn gpu_timing_availability() {
187 let src = GPUTimingSource;
188 assert!(src.is_available());
190 }
191
192 #[test]
193 #[cfg(target_os = "macos")]
194 #[ignore] fn gpu_timing_collects_bytes() {
196 let src = GPUTimingSource;
197 if src.is_available() {
198 let data = src.collect(32);
199 assert!(!data.is_empty());
200 assert!(data.len() <= 32);
201 }
202 }
203}