feat(tools): display-disturb — deterministic display-stack disturbance generator
The stall-immunity bench needs both stall classes on demand, without a standby TV or a monitor-tool storm: `ddc` replays the Twinkle-Tray/PowerDisplay-class DDC/CI traffic (VCP reads, optional capabilities-string requests) through the win32k I2C path; `modeset` re-commits the current mode with CDS_RESET — the Level-Two modeset entry that idles the whole adapter with nothing Win32-visible changing. Every op prints an epoch-stamped took_ms line so host.log stall reports correlate line-for-line, and the op duration itself measures the driver's service time per disturbance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -1020,6 +1020,13 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "display-disturb"
|
||||||
|
version = "0.20.0"
|
||||||
|
dependencies = [
|
||||||
|
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ members = [
|
|||||||
"clients/session",
|
"clients/session",
|
||||||
"clients/windows",
|
"clients/windows",
|
||||||
"clients/android/native",
|
"clients/android/native",
|
||||||
|
"tools/display-disturb",
|
||||||
"tools/latency-probe",
|
"tools/latency-probe",
|
||||||
"tools/loss-harness",
|
"tools/loss-harness",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "display-disturb"
|
||||||
|
description = "Deterministic display-stack disturbance generator (vdisplay stall-immunity bench)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
windows = { version = "0.62", features = [
|
||||||
|
"Win32_Devices_Display",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
"Win32_Foundation",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
//! `display-disturb` — deterministic display-stack disturbance generator (Windows).
|
||||||
|
//!
|
||||||
|
//! The vdisplay stall-immunity bench (design: punktfunk-planning
|
||||||
|
//! `design/vdisplay-disturbance-immunity.md`) needs both stall classes reproducible on demand,
|
||||||
|
//! without waiting for a standby TV or a monitor-tool storm:
|
||||||
|
//!
|
||||||
|
//! * `ddc` — Class 2: DDC/CI traffic through the win32k → dxgkrnl → miniport I2C path, exactly
|
||||||
|
//! what Twinkle-Tray/PowerDisplay-class tools emit after every HPD blip (one VCP read ≈ 100 ms,
|
||||||
|
//! a capabilities string up to ~1 s — serialized per physical I2C bus). Requires a physical
|
||||||
|
//! monitor; virtual displays expose no DDC handle.
|
||||||
|
//! * `modeset` — Class 1: a same-mode `ChangeDisplaySettingsExW(CDS_RESET)` re-commit — a
|
||||||
|
//! Level-Two modeset-class DDI entry that idles the whole adapter ("the graphics hardware is
|
||||||
|
//! idle") without changing anything Win32-visible.
|
||||||
|
//!
|
||||||
|
//! Every operation prints `epoch_ms op target duration_ms result` so stalls in a concurrent
|
||||||
|
//! stream's host.log correlate line-for-line. The per-op duration is itself measurement: it is
|
||||||
|
//! the I2C/modeset service time the GPU driver spent, per disturbance.
|
||||||
|
//!
|
||||||
|
//! Usage: `display-disturb ddc [--interval-ms 2000] [--caps] [--vcp 0x10]`
|
||||||
|
//! `display-disturb modeset [--interval-ms 2000]`
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn main() {
|
||||||
|
eprintln!("display-disturb is Windows-only (it exercises the WDDM display stack).");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn main() {
|
||||||
|
win::main()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod win {
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use windows::{
|
||||||
|
core::{BOOL, PCWSTR},
|
||||||
|
Win32::{
|
||||||
|
Devices::Display::{
|
||||||
|
CapabilitiesRequestAndCapabilitiesReply, DestroyPhysicalMonitor,
|
||||||
|
GetCapabilitiesStringLength, GetNumberOfPhysicalMonitorsFromHMONITOR,
|
||||||
|
GetPhysicalMonitorsFromHMONITOR, GetVCPFeatureAndVCPFeatureReply, PHYSICAL_MONITOR,
|
||||||
|
},
|
||||||
|
Foundation::{HANDLE, LPARAM, RECT},
|
||||||
|
Graphics::Gdi::{
|
||||||
|
ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplayMonitors,
|
||||||
|
EnumDisplaySettingsW, CDS_RESET, DEVMODEW, DISPLAY_DEVICEW,
|
||||||
|
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, DISPLAY_DEVICE_MIRRORING_DRIVER,
|
||||||
|
DISP_CHANGE_SUCCESSFUL, ENUM_CURRENT_SETTINGS, HDC, HMONITOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn epoch_ms() -> u128 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_millis())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One timed op: print the correlation line the bench greps for.
|
||||||
|
fn report(op: &str, target: &str, took: Duration, ok: bool) {
|
||||||
|
println!(
|
||||||
|
"{} {op} {target} took_ms={} ok={ok}",
|
||||||
|
epoch_ms(),
|
||||||
|
took.as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
mode: String,
|
||||||
|
interval: Duration,
|
||||||
|
caps: bool,
|
||||||
|
vcp: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> Args {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
let mode = argv.get(1).cloned().unwrap_or_default();
|
||||||
|
if !matches!(mode.as_str(), "ddc" | "modeset") {
|
||||||
|
eprintln!(
|
||||||
|
"usage: display-disturb <ddc|modeset> [--interval-ms N] [--caps] [--vcp 0xNN]"
|
||||||
|
);
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
let mut a = Args {
|
||||||
|
mode,
|
||||||
|
interval: Duration::from_millis(2000),
|
||||||
|
caps: false,
|
||||||
|
vcp: 0x10, // brightness — universally implemented, read-only harmless
|
||||||
|
};
|
||||||
|
let mut i = 2;
|
||||||
|
while i < argv.len() {
|
||||||
|
match argv[i].as_str() {
|
||||||
|
"--interval-ms" => {
|
||||||
|
i += 1;
|
||||||
|
a.interval = Duration::from_millis(
|
||||||
|
argv.get(i).and_then(|s| s.parse().ok()).unwrap_or(2000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--caps" => a.caps = true,
|
||||||
|
"--vcp" => {
|
||||||
|
i += 1;
|
||||||
|
let s = argv.get(i).map(String::as_str).unwrap_or("0x10");
|
||||||
|
a.vcp = u8::from_str_radix(s.trim_start_matches("0x"), 16).unwrap_or(0x10);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
eprintln!("unknown arg: {other}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
a
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let args = parse_args();
|
||||||
|
eprintln!(
|
||||||
|
"display-disturb: mode={} interval={}ms (Ctrl-C to stop) — correlate `took_ms` lines \
|
||||||
|
against the host log's stall reports",
|
||||||
|
args.mode,
|
||||||
|
args.interval.as_millis()
|
||||||
|
);
|
||||||
|
match args.mode.as_str() {
|
||||||
|
"ddc" => ddc_loop(&args),
|
||||||
|
_ => modeset_loop(&args),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Class 2: the DDC hammer ----
|
||||||
|
|
||||||
|
/// Collect the desktop's HMONITORs (the DDC handles hang off them).
|
||||||
|
fn monitors() -> Vec<HMONITOR> {
|
||||||
|
unsafe extern "system" fn cb(mon: HMONITOR, _dc: HDC, _rc: *mut RECT, out: LPARAM) -> BOOL {
|
||||||
|
// SAFETY: `out.0` is the `&mut Vec<HMONITOR>` this enumeration call passed in below,
|
||||||
|
// alive for the whole synchronous enumeration.
|
||||||
|
unsafe { &mut *(out.0 as *mut Vec<HMONITOR>) }.push(mon);
|
||||||
|
BOOL(1)
|
||||||
|
}
|
||||||
|
let mut v: Vec<HMONITOR> = Vec::new();
|
||||||
|
// SAFETY: null dc/clip = enumerate all display monitors; `cb` only touches the Vec whose
|
||||||
|
// address rides in LPARAM for the duration of this synchronous call.
|
||||||
|
let _ =
|
||||||
|
unsafe { EnumDisplayMonitors(None, None, Some(cb), LPARAM(&mut v as *mut _ as isize)) };
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The physical (DDC-capable) monitors behind one HMONITOR, with their descriptions.
|
||||||
|
fn physical_monitors(mon: HMONITOR) -> Vec<(HANDLE, String)> {
|
||||||
|
let mut count = 0u32;
|
||||||
|
// SAFETY: valid HMONITOR from enumeration; `count` is a valid out-param.
|
||||||
|
if unsafe { GetNumberOfPhysicalMonitorsFromHMONITOR(mon, &mut count) }.is_err()
|
||||||
|
|| count == 0
|
||||||
|
{
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut phys = vec![PHYSICAL_MONITOR::default(); count as usize];
|
||||||
|
// SAFETY: `phys` holds exactly `count` entries as the API requires.
|
||||||
|
if unsafe { GetPhysicalMonitorsFromHMONITOR(mon, &mut phys) }.is_err() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
phys.iter()
|
||||||
|
.map(|p| {
|
||||||
|
// Copy the field out: PHYSICAL_MONITOR is packed(1), so referencing the array
|
||||||
|
// in place would be an unaligned reference (E0793).
|
||||||
|
let name = p.szPhysicalMonitorDescription;
|
||||||
|
let desc = String::from_utf16_lossy(
|
||||||
|
&name[..name.iter().position(|&c| c == 0).unwrap_or(0)],
|
||||||
|
);
|
||||||
|
(p.hPhysicalMonitor, desc.trim().to_string())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ddc_loop(args: &Args) -> ! {
|
||||||
|
loop {
|
||||||
|
let mut any = false;
|
||||||
|
for mon in monitors() {
|
||||||
|
for (h, desc) in physical_monitors(mon) {
|
||||||
|
any = true;
|
||||||
|
let label = if desc.is_empty() {
|
||||||
|
"monitor".into()
|
||||||
|
} else {
|
||||||
|
desc.clone()
|
||||||
|
};
|
||||||
|
if args.caps {
|
||||||
|
// The heavy transaction: length + full capabilities string (the
|
||||||
|
// PowerDisplay-discovery load, ~100 ms-1 s of serialized I2C).
|
||||||
|
let t = Instant::now();
|
||||||
|
let mut len = 0u32;
|
||||||
|
// SAFETY: `h` is a live physical-monitor handle; `len` a valid out-param.
|
||||||
|
let mut ok =
|
||||||
|
unsafe { GetCapabilitiesStringLength(h, &mut len) } == 1 && len > 0;
|
||||||
|
if ok {
|
||||||
|
let mut buf = vec![0u8; len as usize];
|
||||||
|
// SAFETY: `buf` is exactly the reported capabilities length.
|
||||||
|
ok = unsafe { CapabilitiesRequestAndCapabilitiesReply(h, &mut buf) }
|
||||||
|
== 1;
|
||||||
|
}
|
||||||
|
report("ddc-caps", &label, t.elapsed(), ok);
|
||||||
|
} else {
|
||||||
|
let t = Instant::now();
|
||||||
|
let (mut cur, mut max) = (0u32, 0u32);
|
||||||
|
// SAFETY: `h` is a live physical-monitor handle; out-params are valid; a
|
||||||
|
// read of a standard VCP code mutates nothing monitor-side.
|
||||||
|
let ok = unsafe {
|
||||||
|
GetVCPFeatureAndVCPFeatureReply(
|
||||||
|
h,
|
||||||
|
args.vcp,
|
||||||
|
None,
|
||||||
|
&mut cur,
|
||||||
|
Some(&mut max),
|
||||||
|
)
|
||||||
|
} == 1;
|
||||||
|
report("ddc-vcp", &label, t.elapsed(), ok);
|
||||||
|
}
|
||||||
|
// SAFETY: closing the handle GetPhysicalMonitorsFromHMONITOR returned.
|
||||||
|
let _ = unsafe { DestroyPhysicalMonitor(h) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !any {
|
||||||
|
eprintln!(
|
||||||
|
"no DDC-capable (physical) monitor found — the ddc mode needs a physical \
|
||||||
|
display; virtual displays expose no DDC handle"
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
std::thread::sleep(args.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Class 1: the no-op modeset tick ----
|
||||||
|
|
||||||
|
/// The first attached, non-mirroring display device (device name, e.g. `\\.\DISPLAY1`).
|
||||||
|
fn first_active_display() -> Option<[u16; 32]> {
|
||||||
|
for i in 0u32.. {
|
||||||
|
let mut dd = DISPLAY_DEVICEW {
|
||||||
|
cb: std::mem::size_of::<DISPLAY_DEVICEW>() as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// SAFETY: null device name = enumerate adapters by index; `dd.cb` is stamped.
|
||||||
|
if unsafe { !EnumDisplayDevicesW(PCWSTR::null(), i, &mut dd, 0).as_bool() } {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if (dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP).0 != 0
|
||||||
|
&& (dd.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER).0 == 0
|
||||||
|
{
|
||||||
|
return Some(dd.DeviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modeset_loop(args: &Args) -> ! {
|
||||||
|
let Some(name) = first_active_display() else {
|
||||||
|
eprintln!("no active display device found");
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
let label = String::from_utf16_lossy(
|
||||||
|
&name[..name.iter().position(|&c| c == 0).unwrap_or(name.len())],
|
||||||
|
);
|
||||||
|
loop {
|
||||||
|
let mut dm = DEVMODEW {
|
||||||
|
dmSize: std::mem::size_of::<DEVMODEW>() as u16,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// SAFETY: `name` is the NUL-terminated device string from enumeration; `dm.dmSize`
|
||||||
|
// is stamped; ENUM_CURRENT_SETTINGS fills the live mode.
|
||||||
|
let got = unsafe {
|
||||||
|
EnumDisplaySettingsW(PCWSTR(name.as_ptr()), ENUM_CURRENT_SETTINGS, &mut dm)
|
||||||
|
}
|
||||||
|
.as_bool();
|
||||||
|
if !got {
|
||||||
|
eprintln!("EnumDisplaySettingsW failed for {label}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let t = Instant::now();
|
||||||
|
// SAFETY: re-applying the CURRENT mode with CDS_RESET — the same-mode re-commit that
|
||||||
|
// forces the modeset path without changing anything user-visible. Null hwnd + null
|
||||||
|
// lparam per the API contract for this flag combination.
|
||||||
|
let rc = unsafe {
|
||||||
|
ChangeDisplaySettingsExW(PCWSTR(name.as_ptr()), Some(&dm), None, CDS_RESET, None)
|
||||||
|
};
|
||||||
|
report(
|
||||||
|
"modeset-reset",
|
||||||
|
&label,
|
||||||
|
t.elapsed(),
|
||||||
|
rc == DISP_CHANGE_SUCCESSFUL,
|
||||||
|
);
|
||||||
|
std::thread::sleep(args.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user