Skip to main content

openentropy_core/sources/
gpu.rs

1//! GPUTimingSource — GPU dispatch timing via the `sips` command.
2//!
3//! Creates a small temporary TIFF image and uses macOS's `sips` command to
4//! resize it, measuring per-operation timing. The `sips` tool dispatches
5//! Metal/CoreImage compute work, and the timing jitter reflects GPU scheduling.
6
7use std::io::Write;
8use std::process::Command;
9use std::time::Instant;
10
11use tempfile::NamedTempFile;
12
13use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
14
15/// Path to the sips binary on macOS.
16const 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
33/// Minimal valid TIFF file (8x8 grayscale, uncompressed).
34/// TIFF header + IFD + 64 bytes of pixel data.
35fn create_minimal_tiff() -> Vec<u8> {
36    let mut tiff = Vec::new();
37
38    // TIFF Header: little-endian, magic 42, IFD offset 8
39    tiff.extend_from_slice(&[0x49, 0x49]); // "II" = little-endian
40    tiff.extend_from_slice(&42u16.to_le_bytes()); // TIFF magic
41    tiff.extend_from_slice(&8u32.to_le_bytes()); // offset to first IFD
42
43    // IFD at offset 8
44    let num_entries: u16 = 8;
45    tiff.extend_from_slice(&num_entries.to_le_bytes());
46
47    // Helper: write a 12-byte IFD entry (tag, type, count, value)
48    // Type 3 = SHORT (2 bytes), Type 4 = LONG (4 bytes)
49    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    // ImageWidth = 8
57    write_entry(&mut tiff, 256, 3, 1, 8);
58    // ImageLength = 8
59    write_entry(&mut tiff, 257, 3, 1, 8);
60    // BitsPerSample = 8
61    write_entry(&mut tiff, 258, 3, 1, 8);
62    // Compression = 1 (no compression)
63    write_entry(&mut tiff, 259, 3, 1, 1);
64    // PhotometricInterpretation = 1 (min-is-black)
65    write_entry(&mut tiff, 262, 3, 1, 1);
66    // StripOffsets — pixel data starts after IFD
67    // IFD: 2 (count) + 8*12 (entries) + 4 (next IFD) = 102 bytes from offset 8
68    // Total header = 8 + 102 = 110
69    let pixel_offset: u32 = 8 + 2 + (num_entries as u32) * 12 + 4;
70    write_entry(&mut tiff, 273, 4, 1, pixel_offset);
71    // RowsPerStrip = 8
72    write_entry(&mut tiff, 278, 3, 1, 8);
73    // StripByteCounts = 64 (8x8 pixels, 1 byte each)
74    write_entry(&mut tiff, 279, 4, 1, 64);
75
76    // Next IFD offset = 0 (no more IFDs)
77    tiff.extend_from_slice(&0u32.to_le_bytes());
78
79    // Pixel data: 64 bytes of gray values
80    tiff.extend_from_slice(&[128u8; 64]);
81
82    tiff
83}
84
85/// Entropy source that harvests timing jitter from GPU image processing operations.
86pub 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        // Create a temporary TIFF file.
99        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        // Each iteration: resize the image via sips and measure timing.
117        // Alternate between sizes to force actual GPU work each time.
118        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                // XOR low bytes for mixing.
147                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        // TIFF starts with "II" (little-endian) and magic 42
178        assert_eq!(&tiff[0..2], b"II");
179        assert_eq!(u16::from_le_bytes([tiff[2], tiff[3]]), 42);
180        // Should have pixel data at the end
181        assert!(tiff.len() > 64);
182    }
183
184    #[test]
185    #[cfg(target_os = "macos")]
186    fn gpu_timing_availability() {
187        let src = GPUTimingSource;
188        // On macOS, sips should always be present.
189        assert!(src.is_available());
190    }
191
192    #[test]
193    #[cfg(target_os = "macos")]
194    #[ignore] // Requires sips binary and GPU
195    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}