openentropy_core/sources/frontier/
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: 2500.0,
51 composite: false,
52};
53
54pub struct DisplayPllSource;
56
57#[cfg(target_os = "macos")]
59mod coregraphics {
60 use std::ffi::c_void;
61
62 type CGDirectDisplayID = u32;
64 type CGDisplayModeRef = *const c_void;
66
67 #[link(name = "CoreGraphics", kind = "framework")]
68 unsafe extern "C" {
69 fn CGMainDisplayID() -> CGDirectDisplayID;
70 fn CGDisplayCopyDisplayMode(display: CGDirectDisplayID) -> CGDisplayModeRef;
71 fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64;
72 fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize;
73 fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize;
74 fn CGDisplayModeRelease(mode: CGDisplayModeRef);
75 fn CGDisplayPixelsWide(display: CGDirectDisplayID) -> usize;
76 fn CGDisplayPixelsHigh(display: CGDirectDisplayID) -> usize;
77 }
78
79 pub fn has_display() -> bool {
81 unsafe { CGMainDisplayID() != 0 }
84 }
85
86 pub fn query_display_property(query_type: usize) -> u64 {
90 unsafe {
93 let display = CGMainDisplayID();
94 if display == 0 {
95 return 0;
96 }
97
98 match query_type % 4 {
99 0 => {
100 let mode = CGDisplayCopyDisplayMode(display);
102 if mode.is_null() {
103 return 0;
104 }
105 let rate = CGDisplayModeGetRefreshRate(mode);
106 CGDisplayModeRelease(mode);
107 rate.to_bits()
108 }
109 1 => {
110 let mode = CGDisplayCopyDisplayMode(display);
112 if mode.is_null() {
113 return 0;
114 }
115 let w = CGDisplayModeGetPixelWidth(mode);
116 let h = CGDisplayModeGetPixelHeight(mode);
117 CGDisplayModeRelease(mode);
118 (w as u64) ^ (h as u64).rotate_left(32)
119 }
120 2 => {
121 let w = CGDisplayPixelsWide(display);
123 let h = CGDisplayPixelsHigh(display);
124 (w as u64).wrapping_mul(h as u64)
125 }
126 _ => {
127 let mode = CGDisplayCopyDisplayMode(display);
129 if mode.is_null() {
130 return 0;
131 }
132 let rate = CGDisplayModeGetRefreshRate(mode);
133 let w = CGDisplayModeGetPixelWidth(mode);
134 CGDisplayModeRelease(mode);
135 rate.to_bits() ^ (w as u64)
136 }
137 }
138 }
139 }
140}
141
142impl EntropySource for DisplayPllSource {
143 fn info(&self) -> &SourceInfo {
144 &DISPLAY_PLL_INFO
145 }
146
147 fn is_available(&self) -> bool {
148 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
149 {
150 coregraphics::has_display()
151 }
152 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
153 {
154 false
155 }
156 }
157
158 fn collect(&self, n_samples: usize) -> Vec<u8> {
159 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
160 {
161 let _ = n_samples;
162 Vec::new()
163 }
164
165 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
166 {
167 let raw_count = n_samples * 4 + 64;
168 let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
169
170 for i in 0..raw_count {
171 let counter_before = read_cntvct();
173
174 let display_val = coregraphics::query_display_property(i);
176 std::hint::black_box(display_val);
177
178 let counter_after = read_cntvct();
180
181 let duration = counter_after.wrapping_sub(counter_before);
185 beats.push(duration);
186 }
187
188 extract_timing_entropy(&beats, n_samples)
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn info() {
199 let src = DisplayPllSource;
200 assert_eq!(src.name(), "display_pll");
201 assert_eq!(src.info().category, SourceCategory::Thermal);
202 assert!(!src.info().composite);
203 }
204
205 #[test]
206 fn physics_mentions_display() {
207 let src = DisplayPllSource;
208 assert!(src.info().physics.contains("display PLL"));
209 assert!(src.info().physics.contains("533 MHz"));
210 assert!(src.info().physics.contains("CNTVCT_EL0"));
211 }
212
213 #[test]
214 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
215 fn collects_bytes() {
216 let src = DisplayPllSource;
217 if src.is_available() {
218 let data = src.collect(64);
219 assert!(!data.is_empty());
220 assert!(data.len() <= 64);
221 }
222 }
223}