fix(multi-monitor): canonicalize display IDs on GDI enumeration to prevent DXGI/GDI drift #11
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
hex/zeno-rmm#11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptoms
On a multi-monitor host, the remote-desktop video for a given
display_idcan come from one monitor whileSendInput-based clicks/moves addressed to that samedisplay_idland on a different monitor (or land on the right monitor but at the wrong coordinates within the virtual desktop). There is no crash and no error logged — the video stream looks correct, the picker UI looks correct, only pointer placement is wrong, which is the hardest class of bug to notice from logs alone and looks identical to the other, already-fixed multi-monitor input bugs in this repo's history.This has not been observed in production — it's a latent gap found while auditing the existing (fixed) multi-monitor
SendInputissues, not a reported incident.Root cause
display_idis a plainu32that means "position in an enumeration list," but the agent maintains two independently-ordered enumerations on Windows with no cross-referencing between them:DxgiCapture::enumerate_outputs(crates/zeno-agent/src/platform/windows.rs:138-202) walksIDXGIFactory1::EnumAdapters1(outer loop) thenIDXGIAdapter1::EnumOutputs(inner loop), assigningid = out.len() as u32in that walk order. This is the list sent to the SPA (DxgiCapture::displays(),windows.rs:469-471) viaRemoteDesktopDisplayswhenever DXGI is the active backend (the default/common case), and it's whatRemoteDesktopSelectDisplayaddresses.enumerate_gdi_displays(windows.rs:49-92) walksEnumDisplayMonitors, assigningid = displays.len() as u32in that callback order.get_display_rect(display_id)(windows.rs:569-575) — which theSendInputcoordinate-remap path uses to turn a wiredisplay_idback into a virtual-desktop rect — looks the id up in this list, not DXGI's.Microsoft's documented
EnumAdapters1contract only guarantees that the adapter driving the primary output is returned at adapter index 0 (IDXGIFactory1::EnumAdapters1); it says nothing about the order of a given adapter's other outputs, nothing about where a second adapter's outputs land relative to GDI's monitor order, and nothing about output order surviving driver reinitialization or a docked/hybrid-graphics reconfiguration.EnumDisplayMonitors(Multiple Monitor Applications on Different Systems) is a completely separate walk with its own ordering rules. Nothing in the codebase maps one id space onto the other — adisplay_idis produced by whichever enumeration built the list the SPA saw, then consumed by whichever enumeration the code path on the other end happens to call.The primary monitor itself is not at risk (both enumerations independently identify the true primary — DXGI via the
x == 0 && y == 0virtual-desktop-origin heuristic atwindows.rs:180-182, GDI via the authoritativeMONITORINFOF_PRIMARYflag atwindows.rs:67— and Windows defines the primary monitor as the one at the virtual-desktop origin, so these two heuristics can't disagree). The risk is entirely in secondary monitor ids, which have no equivalent cross-check.Why we don't see it today
The user's current hardware (Alonzo-pc) is single-adapter. With exactly one adapter,
enumerate_outputs's outer loop runs once, so its output ordering reduces to a singleEnumOutputswalk — and in practice a lone GPU's own output enumeration and Windows' GDI monitor enumeration tend to agree because both ultimately reflect the same one driver's notion of "which output is which." The two-independent-orderings hazard only becomes observable with more than one adapter contributing outputs (dGPU + iGPU, an added eGPU, a USB/DisplayLink adapter, or a docked laptop) or after a hybrid-graphics/driver reconfiguration reshuffles which physical adapter object backs a given monitor's duplication path.Proposed fix (shape only)
Make GDI's
EnumDisplayMonitorsorder the single canonical id space, and have the DXGI path resolve into it instead of minting its own. Concretely: when DXGI enumerates an output, look up that output's virtual-desktop rect (already available fromIDXGIOutput::GetDesc) against the GDI list and reuse that monitor's GDI-assigned id, rather than assigningid = out.len()from the DXGI walk.select_displayandget_display_rectthen agree by construction because both are keyed off the same GDI enumeration. This is the smallest change (touchesenumerate_outputs's id assignment only, no wire-protocol or helper-process architecture changes) and matches the pattern the codebase already uses elsewhere (get_primary_display_rectdeliberately re-derives from GDI rather than trusting a DXGI-native answer, for the same reason). The alternative of matching by rect/MonitorFromRectatSendInputtime instead of by id was considered but rejected for now — it would require threading the captured frame's rect into the input-remap call sites (run.rs/remote_helper.rs), which is a larger change for the same result.Reproduction recipe (hypothetical — needs a multi-adapter host)
idvalues in theRemoteDesktopDisplaysmessage (DXGI order) againstenumerate_gdi_displays()'s order for the same physical monitors (loggable by temporarily adding a debug line, or inferred from which monitor's video appears under which id in the picker vs. which monitor a click lands on).References
MOUSEEVENTF_ABSOLUTE/MOUSEEVENTF_VIRTUALDESKsemantics theSendInputremap path relies on.crates/zeno-agent/src/platform/windows.rs:138-202—DxgiCapture::enumerate_outputs(DXGI id assignment).crates/zeno-agent/src/platform/windows.rs:49-92—enumerate_gdi_displays(GDI id assignment).crates/zeno-agent/src/platform/windows.rs:566-575—get_display_rect(theSendInputremap path's GDI-order lookup).crates/zeno-agent/src/platform/windows.rs:469-483—DxgiCapture::displays()/select_display(the DXGI-order list sent to the SPA).