From 75d07a7e5df061cadeef9f0a645e1e5658917afe Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 4 Aug 2026 23:30:51 +0200 Subject: [PATCH] =?UTF-8?q?feat(tools/display-disturb):=20adl-emul=20?= =?UTF-8?q?=E2=80=94=20AMD=20connector-emulation=20probe=20(software=20HPD?= =?UTF-8?q?=20dummy)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standby-sink stall program's §3 dead-end list marked ADL EmulationMode 'likely Pro-gated' on field hearsay, with 'probe once, log rc' as the owed falsification — never run. Three RX 9070 XT field cases later (ASUS VG32VQ1B/DP, Odyssey G60SD/DP, LG UltraGear 32GS95UE/HDMI), this is that probe, shippable to reporters: read-only caps/board-layout/connection-state walk by default, --lock pins the live EDID + ADL_EMUL_MODE_ALWAYS on occupied connectors (the software HPD-holding dummy), --unlock restores. Every call prints the bench's epoch_ms correlation line with the decoded ADL rc — ADL_ERR_NOT_SUPPORTED(-8) vs ADL_OK on consumer Adrenalin is the Pro-gating answer, and a --lock run during a stream with the sink asleep is the direct A/B for the metronomic stall class. atiadlxx.dll is bound dynamically (absent = clean exit 2), structs mirror adl_structures.h verbatim, and the probe touches only connectors the board-layout walk enumerated. Gates: check/clippy -D warnings (msvc cross-target) + fmt clean; native stub unaffected. --- tools/display-disturb/Cargo.toml | 1 + tools/display-disturb/src/adl.rs | 545 ++++++++++++++++++++++++++++++ tools/display-disturb/src/main.rs | 27 +- 3 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 tools/display-disturb/src/adl.rs diff --git a/tools/display-disturb/Cargo.toml b/tools/display-disturb/Cargo.toml index 7c85c22e..ae7e2862 100644 --- a/tools/display-disturb/Cargo.toml +++ b/tools/display-disturb/Cargo.toml @@ -13,6 +13,7 @@ windows = { version = "0.62", features = [ "Win32_Devices_Display", "Win32_Graphics_Gdi", "Win32_Foundation", + "Win32_System_LibraryLoader", ] } [lints] diff --git a/tools/display-disturb/src/adl.rs b/tools/display-disturb/src/adl.rs new file mode 100644 index 00000000..23a77fad --- /dev/null +++ b/tools/display-disturb/src/adl.rs @@ -0,0 +1,545 @@ +//! `adl-emul` — the AMD ADL EDID-emulation probe (immunity design doc §3's "probe once, log rc", +//! promoted to a field A/B after the third RX 9070 XT standby-sink case). +//! +//! Three field cases (ASUS VG32VQ1B/DP, Odyssey G60SD/DP, LG UltraGear 32GS95UE/HDMI — all +//! RX 9070 XT hosts) share one mechanism: a connected-but-asleep sink whose standby HPD/DDC/link +//! servicing the KMD performs below every OS lever (CCD deactivation, devnode disable and CRU +//! EDID overrides are confirmed no-ops — design doc §2a/§3). The one software lever that could +//! stop the servicing at its SOURCE is the driver's own connector emulation — the software +//! equivalent of an HPD-holding dummy plug: pin the live EDID with +//! `ADL2_Adapter_ConnectionData_Set`, then `ADL2_Adapter_EmulationMode_Set(ADL_EMUL_MODE_ALWAYS)` +//! so the driver stops caring what the physical pins report. The design doc marked the API +//! "likely Pro-gated" on hearsay; nobody — here or in the community record — ever probed consumer +//! Adrenalin. This subcommand is that probe: +//! +//! * `adl-emul` — read-only: caps, board layout, per-connector state. +//! * `adl-emul --lock [--connector N]` — pin the live EDID + `ADL_EMUL_MODE_ALWAYS` (occupied +//! connectors only unless `--connector` names one). +//! * `adl-emul --unlock [--connector N]` — `ADL_EMUL_MODE_OFF` + remove the pinned EDID. +//! +//! Every call prints the bench's `epoch_ms op target took_ms ok` line plus `rc=` (decoded): the +//! rc IS the deliverable — `ADL2_Adapter_EDIDManagement_Caps` answering `supported=1` with +//! `--lock` returning `ADL_OK` on a consumer RX card kills the Pro-gating assumption, and a +//! locked connector during a stream with the sink asleep is the direct A/B for the metronomic +//! stall. `--unlock` (or a driver reinstall) restores; emulation state can persist across +//! reboots, so a `--lock` run must always be paired with a later `--unlock`. + +// FFI mirrors of ADL's C structs — keep AMD's field names verbatim so the header diff is +// mechanical. +#![allow(non_snake_case)] + +use std::ffi::c_void; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +use windows::core::{s, PCSTR}; +use windows::Win32::Foundation::HMODULE; +use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; + +// ---- ADL constants (adl_defines.h, GPUOpen display-library) ---- + +const ADL_OK: i32 = 0; +const ADL_MAX_PATH: usize = 256; +const ADL_MAX_DISPLAY_EDID_DATA_SIZE: usize = 1024; +const ADL_MAX_RAD_LINK_COUNT: usize = 15; + +const ADL_EMUL_MODE_OFF: i32 = 0; +const ADL_EMUL_MODE_ALWAYS: i32 = 3; + +const ADL_QUERY_REAL_DATA: i32 = 0; +const ADL_QUERY_EMULATED_DATA: i32 = 1; + +const ADL_EMUL_STATUS_REAL_DEVICE_CONNECTED: i32 = 0x1; +const ADL_EMUL_STATUS_EMULATED_DEVICE_PRESENT: i32 = 0x2; +const ADL_EMUL_STATUS_EMULATED_DEVICE_USED: i32 = 0x4; + +const AMD_VENDOR_ID: i32 = 1002; + +/// Decode the rc values a field log will actually contain (adl_defines.h) — `-8` vs `-1` is the +/// whole consumer-vs-Pro question, so spell them out. +fn rc_str(rc: i32) -> &'static str { + match rc { + 0 => "ADL_OK", + 1..=4 => "ADL_OK_(warning-class)", + -1 => "ADL_ERR", + -2 => "ADL_ERR_NOT_INIT", + -3 => "ADL_ERR_INVALID_PARAM", + -5 => "ADL_ERR_INVALID_ADL_IDX", + -8 => "ADL_ERR_NOT_SUPPORTED", + -9 => "ADL_ERR_NULL_POINTER", + -10 => "ADL_ERR_DISABLED_ADAPTER", + -22 => "ADL_ERR_CALL_TO_INCOMPATIABLE_DRIVER", + -23 => "ADL_ERR_NO_ADMINISTRATOR_PRIVILEGES", + _ => "?", + } +} + +fn connector_type_str(t: i32) -> &'static str { + match t { + 1 => "VGA", + 2 => "DVI-D", + 3 => "DVI-I", + 8 => "HDMI-A", + 9 => "HDMI-B", + 10 => "DP", + 11 => "eDP", + 12 => "miniDP", + 13 => "VIRTUAL", + 14 => "USB-C", + _ => "unknown", + } +} + +// ---- ADL structs (adl_structures.h, verbatim layouts) ---- + +#[repr(C)] +struct AdapterInfo { + iSize: i32, + iAdapterIndex: i32, + strUDID: [u8; ADL_MAX_PATH], + iBusNumber: i32, + iDeviceNumber: i32, + iFunctionNumber: i32, + iVendorID: i32, + strAdapterName: [u8; ADL_MAX_PATH], + strDisplayName: [u8; ADL_MAX_PATH], + iPresent: i32, + // _WIN32 tail — this tool only builds for Windows. + iExist: i32, + strDriverPath: [u8; ADL_MAX_PATH], + strDriverPathExt: [u8; ADL_MAX_PATH], + strPNPString: [u8; ADL_MAX_PATH], + iOSDisplayIndex: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ADLMSTRad { + iLinkNumber: i32, + rad: [u8; ADL_MAX_RAD_LINK_COUNT], +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ADLDevicePort { + iConnectorIndex: i32, + aMSTRad: ADLMSTRad, +} + +impl ADLDevicePort { + /// A non-MST port at `connector` (MST RAD all-zero = "DP root / non-DP ignored" per header). + fn root(connector: i32) -> Self { + Self { + iConnectorIndex: connector, + aMSTRad: ADLMSTRad { + iLinkNumber: 0, + rad: [0; ADL_MAX_RAD_LINK_COUNT], + }, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ADLConnectionProperties { + iValidProperties: i32, + iBitrate: i32, + iNumberOfLanes: i32, + iColorDepth: i32, + iStereo3DCaps: i32, + iOutputBandwidth: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ADLConnectionData { + iConnectionType: i32, + aConnectionProperties: ADLConnectionProperties, + iNumberofPorts: i32, + iActiveConnections: i32, + iDataSize: i32, + EdidData: [u8; ADL_MAX_DISPLAY_EDID_DATA_SIZE], +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct ADLConnectionState { + iEmulationStatus: i32, + iEmulationMode: i32, + iDisplayIndex: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ADLConnectorInfo { + iConnectorIndex: i32, + iConnectorId: i32, + iSlotIndex: i32, + iType: i32, + iOffset: i32, + iLength: i32, +} + +// ---- dynamic binding (atiadlxx.dll ships with every AMD driver; absent elsewhere) ---- + +type AdlContext = *mut c_void; +type MallocCb = unsafe extern "C" fn(i32) -> *mut c_void; + +type FnMainCreate = unsafe extern "C" fn(MallocCb, i32, *mut AdlContext) -> i32; +type FnMainDestroy = unsafe extern "C" fn(AdlContext) -> i32; +type FnNumAdapters = unsafe extern "C" fn(AdlContext, *mut i32) -> i32; +type FnAdapterInfoGet = unsafe extern "C" fn(AdlContext, *mut AdapterInfo, i32) -> i32; +type FnEdidMgmtCaps = unsafe extern "C" fn(AdlContext, i32, *mut i32) -> i32; +type FnBoardLayoutGet = unsafe extern "C" fn( + AdlContext, + i32, + *mut i32, + *mut i32, + *mut *mut c_void, + *mut i32, + *mut *mut ADLConnectorInfo, +) -> i32; +type FnConnStateGet = + unsafe extern "C" fn(AdlContext, i32, ADLDevicePort, *mut ADLConnectionState) -> i32; +type FnConnDataGet = + unsafe extern "C" fn(AdlContext, i32, ADLDevicePort, i32, *mut ADLConnectionData) -> i32; +type FnConnDataSet = unsafe extern "C" fn(AdlContext, i32, ADLDevicePort, ADLConnectionData) -> i32; +type FnConnDataRemove = unsafe extern "C" fn(AdlContext, i32, ADLDevicePort) -> i32; +type FnEmulModeSet = unsafe extern "C" fn(AdlContext, i32, ADLDevicePort, i32) -> i32; + +struct Adl { + create: FnMainCreate, + destroy: FnMainDestroy, + num_adapters: FnNumAdapters, + adapter_info: FnAdapterInfoGet, + edid_caps: FnEdidMgmtCaps, + board_layout: FnBoardLayoutGet, + conn_state: FnConnStateGet, + conn_data_get: FnConnDataGet, + conn_data_set: FnConnDataSet, + conn_data_remove: FnConnDataRemove, + emul_mode_set: FnEmulModeSet, +} + +/// ADL's application-provided allocator: it hands buffers (board-layout arrays) back through +/// out-pointers and expects the app to own them. +unsafe extern "C" fn adl_malloc(size: i32) -> *mut c_void { + let size = size.max(1) as usize; + // SAFETY: non-zero size with a fixed valid alignment; the resulting buffers are deliberately + // never freed — ADL's contract wants an ADL_Main_Memory_Free symmetry, and leaking the <1 KiB + // of board-layout arrays in a one-shot probe is simpler than proving allocator parity. + unsafe { + std::alloc::alloc(std::alloc::Layout::from_size_align(size, 16).expect("tiny ADL alloc")) + as *mut c_void + } +} + +impl Adl { + fn load() -> Option { + // SAFETY: plain LoadLibrary of the AMD-driver-installed ADL runtime by its well-known + // name; a foreign-DLL search-path attack would require writing to System32. + let lib: HMODULE = unsafe { LoadLibraryA(s!("atiadlxx.dll")) }.ok()?; + // One unsafe helper: resolve `name` or bail. Every Fn* type above matches the ADL + // header's C signature (x64 has a single calling convention, so `extern "C"` is exact). + unsafe fn sym(lib: HMODULE, name: PCSTR) -> Option { + debug_assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + // SAFETY: caller passes a fn-pointer type T of pointer size (asserted above); + // GetProcAddress yields the export's address or None. + let f = unsafe { GetProcAddress(lib, name) }?; + // SAFETY: reinterpreting one non-null fn pointer as the export's true C signature. + Some(unsafe { std::mem::transmute_copy::<_, T>(&f) }) + } + // SAFETY: `lib` is the live module handle from the successful load above. + unsafe { + Some(Self { + create: sym(lib, s!("ADL2_Main_Control_Create"))?, + destroy: sym(lib, s!("ADL2_Main_Control_Destroy"))?, + num_adapters: sym(lib, s!("ADL2_Adapter_NumberOfAdapters_Get"))?, + adapter_info: sym(lib, s!("ADL2_Adapter_AdapterInfo_Get"))?, + edid_caps: sym(lib, s!("ADL2_Adapter_EDIDManagement_Caps"))?, + board_layout: sym(lib, s!("ADL2_Adapter_BoardLayout_Get"))?, + conn_state: sym(lib, s!("ADL2_Adapter_ConnectionState_Get"))?, + conn_data_get: sym(lib, s!("ADL2_Adapter_ConnectionData_Get"))?, + conn_data_set: sym(lib, s!("ADL2_Adapter_ConnectionData_Set"))?, + conn_data_remove: sym(lib, s!("ADL2_Adapter_ConnectionData_Remove"))?, + emul_mode_set: sym(lib, s!("ADL2_Adapter_EmulationMode_Set"))?, + }) + } + } +} + +// ---- the probe ---- + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum EmulAction { + /// Read-only: caps + layout + per-connector state. Always safe. + Probe, + /// Pin live EDID + `ADL_EMUL_MODE_ALWAYS` on occupied (or named) connectors. + Lock, + /// `ADL_EMUL_MODE_OFF` + remove pinned EDID on all (or named) connectors. + Unlock, +} + +fn epoch_ms() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) +} + +/// The bench's correlation-line convention (`epoch_ms op target took_ms ok`), plus the decoded +/// rc and any op-specific fields — the rc is what a field report gets read for. +fn line(op: &str, target: &str, took_ms: u128, rc: i32, extra: &str) { + let sep = if extra.is_empty() { "" } else { " " }; + println!( + "{} {op} {target} took_ms={took_ms} ok={} rc={rc}({}){sep}{extra}", + epoch_ms(), + rc == ADL_OK, + rc_str(rc), + ); +} + +fn c_str(buf: &[u8]) -> String { + let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); + String::from_utf8_lossy(&buf[..len]).into_owned() +} + +pub fn run(action: EmulAction, connector_filter: Option) -> ! { + let Some(adl) = Adl::load() else { + eprintln!( + "atiadlxx.dll not loadable (or an export is missing) — not an AMD driver install; \ + the ADL emulation lever does not exist on this box" + ); + std::process::exit(2); + }; + + let mut ctx: AdlContext = std::ptr::null_mut(); + let t = Instant::now(); + // SAFETY: documented init call — our allocator callback, iEnumConnectedAdapters=0 (ALL + // adapters: an exclusively-isolated streaming host may report no "connected" display on the + // physical GPU), and a valid out-slot for the context. + let rc = unsafe { (adl.create)(adl_malloc, 0, &mut ctx) }; + line("adl-init", "atiadlxx", t.elapsed().as_millis(), rc, ""); + if rc != ADL_OK { + std::process::exit(1); + } + + let mut count = 0i32; + // SAFETY: live context; valid out-param. + let rc = unsafe { (adl.num_adapters)(ctx, &mut count) }; + if rc != ADL_OK || count <= 0 { + line("adl-num-adapters", "all", 0, rc, &format!("count={count}")); + std::process::exit(1); + } + let mut infos: Vec = (0..count) + .map(|_| { + // SAFETY: AdapterInfo is plain ints + byte arrays — the all-zero pattern is valid, + // and ADL fills the array in place. + let mut a: AdapterInfo = unsafe { std::mem::zeroed() }; + a.iSize = std::mem::size_of::() as i32; + a + }) + .collect(); + let bytes = std::mem::size_of_val(infos.as_slice()) as i32; + // SAFETY: caller-allocated array of exactly `count` stamped entries, byte size passed as the + // API's iInputSize contract requires. + let rc = unsafe { (adl.adapter_info)(ctx, infos.as_mut_ptr(), bytes) }; + line("adl-adapters", "all", 0, rc, &format!("count={count}")); + if rc != ADL_OK { + std::process::exit(1); + } + + // One GPU surfaces as many logical adapters — probe each bus once, AMD-present only. + let mut seen_buses: Vec = Vec::new(); + for info in &infos { + if info.iPresent == 0 + || info.iVendorID != AMD_VENDOR_ID + || seen_buses.contains(&info.iBusNumber) + { + continue; + } + seen_buses.push(info.iBusNumber); + let idx = info.iAdapterIndex; + let name = c_str(&info.strAdapterName); + let target = format!("adapter{idx}[{}]", name.trim()); + + let mut supported = 0i32; + let t = Instant::now(); + // SAFETY: live context, adapter index from this enumeration, valid out-param. + let rc = unsafe { (adl.edid_caps)(ctx, idx, &mut supported) }; + line( + "adl-edid-caps", + &target, + t.elapsed().as_millis(), + rc, + &format!("supported={supported}"), + ); + + let (mut valid, mut n_slots, mut n_conn) = (0i32, 0i32, 0i32); + let mut slots: *mut c_void = std::ptr::null_mut(); + let mut connectors: *mut ADLConnectorInfo = std::ptr::null_mut(); + let t = Instant::now(); + // SAFETY: live context + adapter index; out-pointers valid; ADL allocates the two arrays + // through `adl_malloc` (deliberately leaked, see there). + let rc = unsafe { + (adl.board_layout)( + ctx, + idx, + &mut valid, + &mut n_slots, + &mut slots, + &mut n_conn, + &mut connectors, + ) + }; + line( + "adl-board-layout", + &target, + t.elapsed().as_millis(), + rc, + &format!("connectors={n_conn} valid_flags={valid:#x}"), + ); + let connector_list: &[ADLConnectorInfo] = + if rc == ADL_OK && !connectors.is_null() && n_conn > 0 { + // SAFETY: ADL just filled `connectors` with `n_conn` entries via our allocator; the + // (leaked) buffer outlives this borrow. + unsafe { std::slice::from_raw_parts(connectors, n_conn as usize) } + } else { + &[] + }; + + for c in connector_list { + if connector_filter.is_some_and(|want| want != c.iConnectorIndex) { + continue; + } + let port = ADLDevicePort::root(c.iConnectorIndex); + let ctarget = format!( + "adapter{idx}.connector{}[{}]", + c.iConnectorIndex, + connector_type_str(c.iType) + ); + + let mut state = ADLConnectionState::default(); + let t = Instant::now(); + // SAFETY: live context; port is a by-value POD naming a connector this adapter just + // enumerated; valid out-param. + let rc = unsafe { (adl.conn_state)(ctx, idx, port, &mut state) }; + let real = state.iEmulationStatus & ADL_EMUL_STATUS_REAL_DEVICE_CONNECTED != 0; + line( + "adl-conn-state", + &ctarget, + t.elapsed().as_millis(), + rc, + &format!( + "status={:#x} real_connected={} emulated_present={} emulated_used={} mode={} display={}", + state.iEmulationStatus, + real, + state.iEmulationStatus & ADL_EMUL_STATUS_EMULATED_DEVICE_PRESENT != 0, + state.iEmulationStatus & ADL_EMUL_STATUS_EMULATED_DEVICE_USED != 0, + state.iEmulationMode, + state.iDisplayIndex, + ), + ); + if rc != ADL_OK { + continue; + } + + match action { + EmulAction::Probe => { + // SAFETY: ADLConnectionData is plain ints + a byte array; all-zero is valid + // and ADL overwrites it. + let mut data: ADLConnectionData = unsafe { std::mem::zeroed() }; + let t = Instant::now(); + // SAFETY: live context/port as above; REAL query fills `data` in place. + let rc = unsafe { + (adl.conn_data_get)(ctx, idx, port, ADL_QUERY_REAL_DATA, &mut data) + }; + line( + "adl-conn-data", + &ctarget, + t.elapsed().as_millis(), + rc, + &format!( + "type={} edid_bytes={}", + data.iConnectionType, data.iDataSize + ), + ); + } + EmulAction::Lock => { + if !real && connector_filter.is_none() { + continue; // nothing to pin — and pinning an EMPTY connector is a different experiment + } + // SAFETY: as in Probe — zeroed then driver-filled. + let mut data: ADLConnectionData = unsafe { std::mem::zeroed() }; + let t = Instant::now(); + // SAFETY: live context/port; REAL query first — we pin exactly what the + // sink reports today, so the emulated display IS the user's monitor. + let mut rc = unsafe { + (adl.conn_data_get)(ctx, idx, port, ADL_QUERY_REAL_DATA, &mut data) + }; + if rc != ADL_OK { + // Asleep sinks can refuse a live EDID read — fall back to whatever the + // driver already has as emulation data (Radeon-Pro-UI parity). + // SAFETY: same contract, emulated-data query. + rc = unsafe { + (adl.conn_data_get)(ctx, idx, port, ADL_QUERY_EMULATED_DATA, &mut data) + }; + } + line( + "adl-lock-read", + &ctarget, + t.elapsed().as_millis(), + rc, + &format!( + "type={} edid_bytes={}", + data.iConnectionType, data.iDataSize + ), + ); + if rc != ADL_OK || data.iDataSize <= 0 { + continue; + } + let t = Instant::now(); + // SAFETY: live context/port; `data` passed by value per the ADL signature. + let rc = unsafe { (adl.conn_data_set)(ctx, idx, port, data) }; + line("adl-lock-set", &ctarget, t.elapsed().as_millis(), rc, ""); + let t = Instant::now(); + // SAFETY: live context/port; mode constant from the header. + let rc = unsafe { (adl.emul_mode_set)(ctx, idx, port, ADL_EMUL_MODE_ALWAYS) }; + line( + "adl-lock-mode-always", + &ctarget, + t.elapsed().as_millis(), + rc, + "", + ); + } + EmulAction::Unlock => { + let t = Instant::now(); + // SAFETY: live context/port; mode constant from the header. + let rc = unsafe { (adl.emul_mode_set)(ctx, idx, port, ADL_EMUL_MODE_OFF) }; + line( + "adl-unlock-mode-off", + &ctarget, + t.elapsed().as_millis(), + rc, + "", + ); + let t = Instant::now(); + // SAFETY: live context/port; removes emulation data set earlier (harmless + // where none exists — the rc says so). + let rc = unsafe { (adl.conn_data_remove)(ctx, idx, port) }; + line( + "adl-unlock-remove", + &ctarget, + t.elapsed().as_millis(), + rc, + "", + ); + } + } + } + } + + // SAFETY: destroying the context created above; nothing ADL-owned is used past this point. + let _ = unsafe { (adl.destroy)(ctx) }; + std::process::exit(0); +} diff --git a/tools/display-disturb/src/main.rs b/tools/display-disturb/src/main.rs index 83556808..14ac1e4f 100644 --- a/tools/display-disturb/src/main.rs +++ b/tools/display-disturb/src/main.rs @@ -11,6 +11,9 @@ //! * `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. +//! * `adl-emul` — not a disturbance but the LEVER probe for the standby-sink class: AMD ADL +//! connector-emulation caps/state, and `--lock`/`--unlock` to pin the live EDID + +//! `ADL_EMUL_MODE_ALWAYS` (the software HPD-dummy experiment — see `adl.rs`). //! //! 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 @@ -18,6 +21,7 @@ //! //! Usage: `display-disturb ddc [--interval-ms 2000] [--caps] [--vcp 0x10]` //! `display-disturb modeset [--interval-ms 2000]` +//! `display-disturb adl-emul [--lock|--unlock] [--connector N]` // Unsafe-proof program: every `unsafe {}` in this tool carries a `// SAFETY:` proof. #![deny(clippy::undocumented_unsafe_blocks)] @@ -33,6 +37,9 @@ fn main() { win::main() } +#[cfg(target_os = "windows")] +mod adl; + #[cfg(target_os = "windows")] mod win { use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -77,14 +84,17 @@ mod win { interval: Duration, caps: bool, vcp: u8, + emul: crate::adl::EmulAction, + connector: Option, } fn parse_args() -> Args { let argv: Vec = std::env::args().collect(); let mode = argv.get(1).cloned().unwrap_or_default(); - if !matches!(mode.as_str(), "ddc" | "modeset" | "extend") { + if !matches!(mode.as_str(), "ddc" | "modeset" | "extend" | "adl-emul") { eprintln!( - "usage: display-disturb [--interval-ms N] [--caps] [--vcp 0xNN]" + "usage: display-disturb [--interval-ms N] [--caps] \ + [--vcp 0xNN] [--lock|--unlock] [--connector N]" ); std::process::exit(2); } @@ -93,6 +103,8 @@ mod win { interval: Duration::from_millis(2000), caps: false, vcp: 0x10, // brightness — universally implemented, read-only harmless + emul: crate::adl::EmulAction::Probe, + connector: None, }; let mut i = 2; while i < argv.len() { @@ -109,6 +121,16 @@ mod win { 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); } + "--lock" => a.emul = crate::adl::EmulAction::Lock, + "--unlock" => a.emul = crate::adl::EmulAction::Unlock, + "--connector" => { + i += 1; + a.connector = argv.get(i).and_then(|s| s.parse().ok()); + if a.connector.is_none() { + eprintln!("--connector needs a numeric connector index"); + std::process::exit(2); + } + } other => { eprintln!("unknown arg: {other}"); std::process::exit(2); @@ -130,6 +152,7 @@ mod win { match args.mode.as_str() { "ddc" => ddc_loop(&args), "extend" => extend_once(), + "adl-emul" => crate::adl::run(args.emul, args.connector), _ => modeset_loop(&args), } }