openentropy_core/sources/thermal/
display_pll.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
34#[cfg(target_os = "macos")]
35use crate::sources::helpers::{extract_timing_entropy, read_cntvct};
36
37static DISPLAY_PLL_INFO: SourceInfo = SourceInfo {
38 name: "display_pll",
39 description: "Display PLL phase noise from pixel clock domain crossing",
40 physics: "Queries CoreGraphics display properties (mode, refresh rate, color space) \
41 that cross into the display PLL\u{2019}s clock domain (~533 MHz pixel clock). \
42 The display PLL is an independent oscillator from both the CPU crystal \
43 (24 MHz) and audio PLL (48 kHz). Phase noise arises from VCO transistor \
44 Johnson-Nyquist noise and charge pump shot noise in the display PLL. \
45 Reading CNTVCT_EL0 before and after each query captures the beat between \
46 CPU crystal and display PLL.",
47 category: SourceCategory::Thermal,
48 platform: Platform::MacOS,
49 requirements: &[Requirement::AppleSilicon],
50 entropy_rate_estimate: 4.0,
51 composite: false,
52 is_fast: true,
53};
54
55pub struct DisplayPllSource;
57
58#[cfg(target_os = "macos")]
60mod coregraphics {
61 use std::ffi::c_void;
62
63 type CGDirectDisplayID = u32;
65 type CGDisplayModeRef = *const c_void;
67
68 #[link(name = "CoreGraphics", kind = "framework")]
69 unsafe extern "C" {
70 fn CGMainDisplayID() -> CGDirectDisplayID;
71 fn CGDisplayCopyDisplayMode(display: CGDirectDisplayID) -> CGDisplayModeRef;
72 fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64;
73 fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize;
74 fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize;
75 fn CGDisplayModeRelease(mode: CGDisplayModeRef);
76 fn CGDisplayPixelsWide(display: CGDirectDisplayID) -> usize;
77 fn CGDisplayPixelsHigh(display: CGDirectDisplayID) -> usize;
78 }
79
80 pub fn has_display() -> bool {
82 unsafe { CGMainDisplayID() != 0 }
85 }
86
87 pub fn query_display_property(query_type: usize) -> u64 {
91 unsafe {
94 let display = CGMainDisplayID();
95 if display == 0 {
96 return 0;
97 }
98
99 match query_type % 4 {
100 0 => {
101 let mode = CGDisplayCopyDisplayMode(display);
103 if mode.is_null() {
104 return 0;
105 }
106 let rate = CGDisplayModeGetRefreshRate(mode);
107 CGDisplayModeRelease(mode);
108 rate.to_bits()
109 }
110 1 => {
111 let mode = CGDisplayCopyDisplayMode(display);
113 if mode.is_null() {
114 return 0;
115 }
116 let w = CGDisplayModeGetPixelWidth(mode);
117 let h = CGDisplayModeGetPixelHeight(mode);
118 CGDisplayModeRelease(mode);
119 (w as u64) ^ (h as u64).rotate_left(32)
120 }
121 2 => {
122 let w = CGDisplayPixelsWide(display);
124 let h = CGDisplayPixelsHigh(display);
125 (w as u64).wrapping_mul(h as u64)
126 }
127 _ => {
128 let mode = CGDisplayCopyDisplayMode(display);
130 if mode.is_null() {
131 return 0;
132 }
133 let rate = CGDisplayModeGetRefreshRate(mode);
134 let w = CGDisplayModeGetPixelWidth(mode);
135 CGDisplayModeRelease(mode);
136 rate.to_bits() ^ (w as u64)
137 }
138 }
139 }
140 }
141}
142
143impl EntropySource for DisplayPllSource {
144 fn info(&self) -> &SourceInfo {
145 &DISPLAY_PLL_INFO
146 }
147
148 fn is_available(&self) -> bool {
149 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
150 {
151 coregraphics::has_display()
152 }
153 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
154 {
155 false
156 }
157 }
158
159 fn collect(&self, n_samples: usize) -> Vec<u8> {
160 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
161 {
162 let _ = n_samples;
163 Vec::new()
164 }
165
166 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
167 {
168 let raw_count = n_samples * 4 + 64;
169 let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
170
171 for i in 0..raw_count {
172 let counter_before = read_cntvct();
174
175 let display_val = coregraphics::query_display_property(i);
177 std::hint::black_box(display_val);
178
179 let counter_after = read_cntvct();
181
182 let duration = counter_after.wrapping_sub(counter_before);
186 beats.push(duration);
187 }
188
189 extract_timing_entropy(&beats, n_samples)
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn info() {
200 let src = DisplayPllSource;
201 assert_eq!(src.name(), "display_pll");
202 assert_eq!(src.info().category, SourceCategory::Thermal);
203 assert!(!src.info().composite);
204 }
205
206 #[test]
207 fn physics_mentions_display() {
208 let src = DisplayPllSource;
209 assert!(src.info().physics.contains("display PLL"));
210 assert!(src.info().physics.contains("533 MHz"));
211 assert!(src.info().physics.contains("CNTVCT_EL0"));
212 }
213
214 #[test]
215 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
216 fn collects_bytes() {
217 let src = DisplayPllSource;
218 if src.is_available() {
219 let data = src.collect(64);
220 assert!(!data.is_empty());
221 assert!(data.len() <= 64);
222 }
223 }
224}