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, 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::Hardware,
27    platform_requirements: &["macos"],
28    entropy_rate_estimate: 300.0,
29    composite: false,
30};
31
32/// Minimal valid TIFF file (8x8 grayscale, uncompressed).
33/// TIFF header + IFD + 64 bytes of pixel data.
34fn create_minimal_tiff() -> Vec<u8> {
35    let mut tiff = Vec::new();
36
37    // TIFF Header: little-endian, magic 42, IFD offset 8
38    tiff.extend_from_slice(&[0x49, 0x49]); // "II" = little-endian
39    tiff.extend_from_slice(&42u16.to_le_bytes()); // TIFF magic
40    tiff.extend_from_slice(&8u32.to_le_bytes()); // offset to first IFD
41
42    // IFD at offset 8
43    let num_entries: u16 = 8;
44    tiff.extend_from_slice(&num_entries.to_le_bytes());
45
46    // Helper: write a 12-byte IFD entry (tag, type, count, value)
47    // Type 3 = SHORT (2 bytes), Type 4 = LONG (4 bytes)
48    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    // ImageWidth = 8
56    write_entry(&mut tiff, 256, 3, 1, 8);
57    // ImageLength = 8
58    write_entry(&mut tiff, 257, 3, 1, 8);
59    // BitsPerSample = 8
60    write_entry(&mut tiff, 258, 3, 1, 8);
61    // Compression = 1 (no compression)
62    write_entry(&mut tiff, 259, 3, 1, 1);
63    // PhotometricInterpretation = 1 (min-is-black)
64    write_entry(&mut tiff, 262, 3, 1, 1);
65    // StripOffsets — pixel data starts after IFD
66    // IFD: 2 (count) + 8*12 (entries) + 4 (next IFD) = 102 bytes from offset 8
67    // Total header = 8 + 102 = 110
68    let pixel_offset: u32 = 8 + 2 + (num_entries as u32) * 12 + 4;
69    write_entry(&mut tiff, 273, 4, 1, pixel_offset);
70    // RowsPerStrip = 8
71    write_entry(&mut tiff, 278, 3, 1, 8);
72    // StripByteCounts = 64 (8x8 pixels, 1 byte each)
73    write_entry(&mut tiff, 279, 4, 1, 64);
74
75    // Next IFD offset = 0 (no more IFDs)
76    tiff.extend_from_slice(&0u32.to_le_bytes());
77
78    // Pixel data: 64 bytes of gray values
79    tiff.extend_from_slice(&[128u8; 64]);
80
81    tiff
82}
83
84/// Entropy source that harvests timing jitter from GPU image processing operations.
85pub 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        // Create a temporary TIFF file.
98        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        // Each iteration: resize the image via sips and measure timing.
116        // Alternate between sizes to force actual GPU work each time.
117        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                // XOR low bytes for mixing.
146                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        // TIFF starts with "II" (little-endian) and magic 42
177        assert_eq!(&tiff[0..2], b"II");
178        assert_eq!(u16::from_le_bytes([tiff[2], tiff[3]]), 42);
179        // Should have pixel data at the end
180        assert!(tiff.len() > 64);
181    }
182
183    #[test]
184    #[cfg(target_os = "macos")]
185    fn gpu_timing_availability() {
186        let src = GPUTimingSource;
187        // On macOS, sips should always be present.
188        assert!(src.is_available());
189    }
190}