Skip to content
Merged
106 changes: 106 additions & 0 deletions docs/specs/runtime/plugin-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,112 @@ fall through on miss/failure, sticky for the process" semantics:
Android exposes only the env-var read (the writable override and `dp list`
are not shipped there in v1; see Non-goals above).

### 3.4 Desktop Linux: monitor enumeration and display claims (multi-screen M0)

The per-monitor registry (§1.1, #69 / ADR-015) is populated on desktop Linux
as on Windows. Two pieces make that possible.

**Monitor enumeration** (`os_display_edid_enumerate`,
`src/xrt/auxiliary/os/os_display_edid_linux.c`). No single source is a monitor
record: RandR knows where each monitor sits on the desktop and which one is
primary, but XWayland publishes no EDID property; DRM sysfs
(`/sys/class/drm/card*-*/{status,enabled,edid,modes}`) has the EDID and the
connector name but no desktop position. The enumerator takes the RandR
monitors from `os_display_desktop_enumerate` and ties each one to a connected
DRM connector, first rule that fires:

0. **randr-edid** — the X server publishes the output's own `EDID` property
(native X does; XWayland does not). That is the monitor's identity. The
connector is the one enabled connector carrying the same EDID (vendor,
product, serial), or, among several identical ones, the one whose name
agrees.
1. **name** — RandR output name equals the connector name with the card prefix
and the kernel's subtype letter dropped (`card1-HDMI-A-1` → `HDMI-1`), and
the connector agrees physically: its modes hold the monitor's device mode,
or its EDID size is within 10 mm of RandR's. A bare name is not enough: the
NVIDIA X driver numbers outputs from 0 (`DP-0`) while nvidia-drm numbers
connectors from 1, and two GPUs can each have an `HDMI-A-1`. This is the
normal case on Mutter's XWayland.
2. **mm** — exactly one unused, enabled connector whose EDID physical size is
within 10 mm of RandR's (EDID stores cm in the base block and mm in the
detailed timing; 340 vs 344 mm is the same panel).
3. **mode** — exactly one unused, enabled connector with the monitor's device
mode among its modes.

"Device mode" is the compositor's current mode when Mutter reports it, else
the RandR rect. It is never the DRM-derived mode, which was itself found by
connector name.

Ambiguity is never guessed: a monitor no rule ties uniquely is listed with
its placement and no EDID identity. A connector is used at most once, and a
connected-but-disabled connector never joins by mm or mode. The record keeps
the card prefix (`card1-HDMI-A-1`). Connectors are read in name order, so
DRM-only records come out in the same order on every boot. From
the EDID the enumerator reads the manufacturer and product id (bytes 8–11),
the serial (12–15), the size in cm (21/22) and the first detailed timing's
pixels, mm and refresh. With **no reachable X server** (pure Wayland,
headless) every connected, enabled connector becomes a DRM-only record flagged
`origin_unknown` at (0, 0). The join method per monitor is logged once at INFO
(`plugin loader: monitor N … join=randr-edid|name|mm|mode|drm-only|none`), and
`displayxr-cli displays` prints it.

The plug-in-facing `xrt_display_descriptor` is unchanged (no ABI change). The
connector name, mm and device mode stay runtime-side (`os_display_edid_monitor`
plus a loader side table keyed by `monitor_id`). The `monitor_id` hash adds the
connector name (with its card prefix) where the platform has one, so DRM-only
records at (0, 0) stay distinct. Windows ids are unchanged.

**Claims from every plug-in.** `target_plugin_resolve_displays` now loads every
manifest plug-in on POSIX as a claim source (`collect_display_sources_platform`,
the twin of the Windows one): same roots and order as discovery,
`DXR_PLUGIN_EXCLUSIVE` honoured, the active plug-in reused rather than loaded
twice. The other plug-ins are claim sources only. No device is created from
them, and active-plug-in selection is unchanged.

Unlike Windows, POSIX loads the others **only when they could matter**. The
active plug-in wins every monitor it claims (#1521), and only a different
pinned plug-in outranks it. So when the active plug-in claims every monitor
and no other plug-in is preferred, the source set is the active plug-in alone,
and nothing else is `dlopen`ed or probed. That covers
`XRT_PREFERRED_PLUGIN_ID=sim-display` (sim-display claims every monitor), so
pinning sim-display does not load the Leia plug-in or touch the SR service.
The check is repeated on every resolve.

Plug-in instances loaded only as claim sources are released with the
`xrt_instance` (`target_plugin_release_claim_sources`, from
`t_instance_destroy`): `destroy()` is called on each, the source cache is
dropped, and the next resolve re-collects. The `dlopen` handles stay loaded,
as for every plug-in. Windows is unchanged and keeps its claim sources for
the process lifetime. `DXR_PLUGIN_EXCLUSIVE` (§2.2) still keeps every other
plug-in out of the process entirely.

**Back-compat claim for a plug-in without `probe_displays`.** Such a plug-in
gets one synthesized `EDID`-confidence claim. On Windows that claim stays on
the primary monitor. Off-Windows, the **active** plug-in's claim goes to the
monitor its panel matches once the runtime has read `get_display_info`
(`target_plugin_note_active_panel`, called by the builder at system create and
on every display-info apply). The matching uses the ADR-033 resolver's rules:
the plug-in's origin when non-zero, then the connector's device mode, then the
pixel size, with ties broken on physical size. Without this, a laptop with the
3D panel on an external connector would route the laptop screen (the primary)
to the vendor DP and the panel to the fallback, because the active plug-in
wins every monitor it claims (#1521). A plug-in that implements
`probe_displays` never reaches the synthesized path, so its own claims always
decide. `displayxr-cli displays --claims` brings the system up headlessly
first, so it reports the same placement the runtime uses.

Example from a laptop (eDP-1, primary) with an Acer DS1 on HDMI, with
`leia-sr` active and no `probe_displays` in the plug-in yet:

```
monitor … 3456x2160 @ (0,0) SDC 423F 300x190 mm output=eDP-1 → sim-display FALLBACK
monitor … 3840x2160 @ (3456,0) ACR 0001 344x193 mm output=HDMI-1 → leia-sr EDID
```

On Linux nothing in the compositors reads the registry yet. The Vulkan
compositor uses the scalar `dp_factory_*`, so this milestone changes no
weaving. macOS and Android still enumerate no monitors.

---

## 4. Plug-in DLL contract
Expand Down
5 changes: 5 additions & 0 deletions src/xrt/auxiliary/os/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ target_sources(aux_os PRIVATE os_display_edid.h)
if(WIN32)
target_sources(aux_os PRIVATE os_display_edid_win32.c)
target_link_libraries(aux_os PRIVATE setupapi)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT ANDROID)
# Desktop Linux (multi-screen M0): RandR monitors joined to DRM sysfs
# connectors. Needs os_display_desktop_x11.c + os_display_connector_linux.c,
# added below under the same condition.
target_sources(aux_os PRIVATE os_display_edid_linux.h os_display_edid_linux.c)
else()
target_sources(aux_os PRIVATE os_display_edid_stubs.c)
endif()
Expand Down
7 changes: 6 additions & 1 deletion src/xrt/auxiliary/os/os_display_connector_linux.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ struct connector_mode
{
char name[64]; // normalised
uint32_t w, h;
double scale; // 0 = unknown
double scale; // 0 = unknown
uint32_t refresh_mhz; // current mode's refresh, 0 = unknown
};

// Local declarations of the libdbus ABI used here (stable since dbus 1.0).
Expand Down Expand Up @@ -297,6 +298,7 @@ query_mutter(struct connector_mode *out, uint32_t max)
if (read_spec_connector(&f, &mon, &connector) && connector != NULL) {
f.message_iter_next(&mon); // past the spec, to the modes array
uint32_t cw = 0, ch = 0;
double crefresh = 0.0;
if (f.message_iter_get_arg_type(&mon) == OS_DBUS_TYPE_ARRAY) {
os_dbus_iter modes;
f.message_iter_recurse(&mon, &modes);
Expand All @@ -315,6 +317,7 @@ query_mutter(struct connector_mode *out, uint32_t max)
if (dict_bool(&f, &m, "is-current") && w > 0 && h > 0) {
cw = (uint32_t)w;
ch = (uint32_t)h;
crefresh = refresh;
}
}
f.message_iter_next(&modes);
Expand All @@ -326,6 +329,7 @@ query_mutter(struct connector_mode *out, uint32_t max)
os_display_connector_normalise(connector, c->name, sizeof(c->name));
c->w = cw;
c->h = ch;
c->refresh_mhz = crefresh > 0.0 ? (uint32_t)(crefresh * 1000.0 + 0.5) : 0u;
}
}
f.message_iter_next(&mons);
Expand Down Expand Up @@ -493,6 +497,7 @@ os_display_connector_annotate(struct os_display_desktop_info *mons, uint32_t cou
m->native_width = mc->w;
m->native_height = mc->h;
m->scale = mc->scale;
m->native_refresh_mhz = mc->refresh_mhz;
m->native_source = OS_DISPLAY_NATIVE_SOURCE_COMPOSITOR;
continue;
}
Expand Down
4 changes: 4 additions & 0 deletions src/xrt/auxiliary/os/os_display_desktop.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ struct os_display_desktop_info

//! Where @ref native_width / @ref native_height came from.
enum os_display_native_source native_source;

//! Refresh of that device mode in milli-Hz, when the source reports it
//! (Mutter DisplayConfig does; DRM sysfs does not). 0 = unknown.
uint32_t native_refresh_mhz;
/*! @} */
};

Expand Down
84 changes: 84 additions & 0 deletions src/xrt/auxiliary/os/os_display_desktop_x11.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

#include "os_display_desktop.h"
#include "os_display_connector_linux.h"
#include "os_display_edid_linux.h"

#include <string.h>
#include <stdio.h>
Expand Down Expand Up @@ -88,6 +89,23 @@ struct x11_fns

struct os_xrr_monitor_info *(*XRRGetMonitors)(os_x_display *, os_x_window, int, int *);
void (*XRRFreeMonitors)(struct os_xrr_monitor_info *);

// Optional: only the EDID reader uses these, and a missing one just means
// "no EDID from the X server".
os_x_atom (*XInternAtom)(os_x_display *, const char *, int);
int (*XRRGetOutputProperty)(os_x_display *,
unsigned long,
os_x_atom,
long,
long,
int,
int,
os_x_atom,
os_x_atom *,
int *,
unsigned long *,
unsigned long *,
unsigned char **);
};

static bool
Expand Down Expand Up @@ -126,6 +144,9 @@ x11_fns_load(struct x11_fns *f)

#undef LOAD

*(void **)(&f->XInternAtom) = dlsym(f->lib_x11, "XInternAtom");
*(void **)(&f->XRRGetOutputProperty) = dlsym(f->lib_xrandr, "XRRGetOutputProperty");

return true;

fail:
Expand Down Expand Up @@ -255,6 +276,69 @@ query_monitors(struct os_display_desktop_info *out_infos, uint32_t max_infos)
return written;
}

uint32_t
os_display_x11_read_monitor_edids(struct os_display_randr_edid *out, uint32_t max)
{
if (out == NULL || max == 0) {
return 0;
}
memset(out, 0, sizeof(*out) * max);

struct x11_fns f;
if (!x11_fns_load(&f)) {
return 0;
}
uint32_t written = 0;
os_x_display *dpy = (f.XInternAtom != NULL && f.XRRGetOutputProperty != NULL) ? f.XOpenDisplay(NULL) : NULL;
if (dpy != NULL) {
// only_if_exists: a server that never published an EDID property
// has no such atom, and creating one would be a side effect.
const os_x_atom edid_atom = f.XInternAtom(dpy, "EDID", 1);
int count = 0;
struct os_xrr_monitor_info *mons =
edid_atom != 0 ? f.XRRGetMonitors(dpy, f.XDefaultRootWindow(dpy), 1, &count) : NULL;
for (int i = 0; mons != NULL && i < count && written < max; i++) {
const struct os_xrr_monitor_info *m = &mons[i];
if (m->width <= 0 || m->height <= 0) {
continue; // same filter as query_monitors, so names line up
}
struct os_display_randr_edid *o = &out[written++];
char *name = f.XGetAtomName(dpy, m->name);
if (name != NULL) {
(void)snprintf(o->name, sizeof(o->name), "%s", name);
f.XFree(name);
}
if (m->noutput < 1 || m->outputs == NULL) {
continue;
}
const unsigned long output = ((const unsigned long *)m->outputs)[0];
os_x_atom actual_type = 0;
int actual_format = 0;
unsigned long nitems = 0, bytes_after = 0;
unsigned char *prop = NULL;
// Length is in 32-bit units: 64 = 256 bytes.
if (f.XRRGetOutputProperty(dpy, output, edid_atom, 0, OS_DISPLAY_RANDR_EDID_MAX / 4, 0, 0,
0 /* AnyPropertyType */, &actual_type, &actual_format, &nitems,
&bytes_after, &prop) == 0 /* Success */
&& prop != NULL && actual_format == 8 && nitems >= 128) {
const uint32_t n =
nitems > OS_DISPLAY_RANDR_EDID_MAX ? OS_DISPLAY_RANDR_EDID_MAX : (uint32_t)nitems;
memcpy(o->edid, prop, n);
o->len = n;
}
if (prop != NULL) {
f.XFree(prop);
}
}
if (mons != NULL) {
f.XRRFreeMonitors(mons);
}
f.XCloseDisplay(dpy);
}
x11_fns_unload(&f);
return written;
}

uint32_t
os_display_desktop_enumerate(struct os_display_desktop_info *out_infos, uint32_t max_infos)
{
Expand Down
60 changes: 60 additions & 0 deletions src/xrt/auxiliary/os/os_display_edid.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
* Windows: reads EDID from the registry via SetupAPI, correlates with
* EnumDisplayMonitors for HMONITOR handles and screen coordinates.
*
* Desktop Linux: joins the RandR monitors (`os_display_desktop_enumerate`:
* placement rect, primary flag) to the DRM connectors in sysfs
* (`/sys/class/drm/card*-*`: EDID blob, status), see
* `os_display_edid_linux.h` for the join rules. With no reachable X server
* (pure Wayland) the records come from DRM alone and carry
* @ref os_display_edid_monitor::origin_unknown.
*
* Other platforms: stubs that return zero results.
*/

Expand Down Expand Up @@ -36,6 +43,20 @@ enum os_edid_diag_error
OS_EDID_DIAG_NO_CORRELATION = 4, //!< Got EDID data but couldn't match to GDI monitors
};

/*!
* How a monitor's placement record was tied to its EDID (desktop Linux; the
* other platforms leave @ref OS_EDID_JOIN_NONE).
*/
enum os_display_edid_join
{
OS_EDID_JOIN_NONE = 0, //!< No EDID source tied to this monitor.
OS_EDID_JOIN_NAME = 1, //!< RandR output name == normalised DRM connector name.
OS_EDID_JOIN_MM = 2, //!< Unique match on physical size (mm, with tolerance).
OS_EDID_JOIN_MODE = 3, //!< Unique match on pixel mode.
OS_EDID_JOIN_DRM_ONLY = 4, //!< No placement source (no X server): DRM record alone.
OS_EDID_JOIN_RANDR_EDID = 5, //!< The X server's own EDID output property.
};

/*!
* EDID-derived identity for a connected monitor.
*/
Expand All @@ -50,6 +71,26 @@ struct os_display_edid_monitor
uint32_t refresh_hz; //!< Current refresh rate in Hz
bool is_primary; //!< True if this is the primary monitor
void *hmonitor; //!< HMONITOR on Windows, NULL elsewhere

/*!
* @name Runtime-private extras (desktop Linux today; zero elsewhere)
*
* Not plug-in ABI: @ref xrt_display_descriptor is built from the fields
* above and is unchanged. These feed `displayxr-cli displays`, the
* monitor id, and the runtime's own panel matching.
* @{
*/
uint32_t serial_number; //!< EDID bytes 12-15 (little-endian); 0 = none.
uint32_t physical_width_mm; //!< Detailed-timing mm, else bytes 21 x 10, else RandR; 0 = unknown.
uint32_t physical_height_mm; //!< As above, bytes 22 x 10.
uint32_t native_width; //!< The connector's device mode (may differ from pixel_width
uint32_t native_height; //!< under a scaled X screen); 0 = unknown.
char connector[32]; //!< DRM connector, e.g. "card1-HDMI-A-1"; "" = unknown.
char output_name[32]; //!< RandR output name, e.g. "HDMI-1"; "" = none.
bool origin_unknown; //!< screen_left/top are NOT a desktop position (DRM-only record).
enum os_display_edid_join join; //!< How the EDID was tied to the placement record.

/*! @} */
};

/*!
Expand All @@ -75,6 +116,7 @@ struct os_display_edid_list
*
* On Windows, uses SetupAPI to read EDID from the registry and
* correlates with EnumDisplayMonitors for HMONITOR handles.
* On desktop Linux, joins RandR monitors to DRM sysfs connectors.
* On other platforms, sets count to 0.
*
* @param[out] out_list Receives the enumerated monitors.
Expand All @@ -98,6 +140,24 @@ os_display_edid_find_in_table(const struct os_display_edid_list *list,
const uint16_t table[][2],
uint32_t table_len);

/*!
* Short name of a join method ("name", "mm", "mode", "drm-only", "none"), for
* logs and `displayxr-cli displays`. Never NULL.
*/
static inline const char *
os_display_edid_join_str(enum os_display_edid_join join)
{
switch (join) {
case OS_EDID_JOIN_NAME: return "name";
case OS_EDID_JOIN_MM: return "mm";
case OS_EDID_JOIN_MODE: return "mode";
case OS_EDID_JOIN_DRM_ONLY: return "drm-only";
case OS_EDID_JOIN_RANDR_EDID: return "randr-edid";
case OS_EDID_JOIN_NONE:
default: return "none";
}
}

#ifdef __cplusplus
}
#endif
Loading
Loading