From cbacf4ed1c5dceac99a28fb9394bfeca3edaa159 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 10:58:11 -0700 Subject: [PATCH 01/10] feat(plugin): per-monitor display info slot + per-screen info derivation (multi-screen M1) Appends xrt_plugin_iface::get_display_info_for_monitor (ADR-020: append-only, struct_size-gated, no XRT_PLUGIN_API_VERSION_CURRENT bump), announced by XRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITOR. EDID mm + the connector's device mode travel in a new by-pointer struct xrt_display_physical instead of growing xrt_display_descriptor: probe_displays receives descriptors as an ARRAY, so growing that struct would change the stride under every existing plug-in. xrt_screen.h carries the per-screen record (xrt_screen / xrt_screen_info / xrt_screen_list); util/u_screen_info.h resolves one monitor's info: the system-default screen reports the system info verbatim, else the owning plug-in's slot, else EDID-derived defaults at the system panel's vertical FOV. sim_display implements the slot. Unit-tested in tests_aux_screen_info. Co-Authored-By: Claude Fable 5.1 --- src/xrt/auxiliary/util/u_screen_info.h | 196 ++++++++++++++++ .../drivers/sim_display/sim_display_device.c | 22 ++ .../sim_display/sim_display_interface.h | 12 + .../drivers/sim_display/sim_display_plugin.c | 82 ++++++- src/xrt/include/xrt/xrt_plugin.h | 94 +++++++- src/xrt/include/xrt/xrt_screen.h | 185 +++++++++++++++ tests/CMakeLists.txt | 6 + tests/tests_aux_screen_info.cpp | 216 ++++++++++++++++++ 8 files changed, 804 insertions(+), 9 deletions(-) create mode 100644 src/xrt/auxiliary/util/u_screen_info.h create mode 100644 src/xrt/include/xrt/xrt_screen.h create mode 100644 tests/tests_aux_screen_info.cpp diff --git a/src/xrt/auxiliary/util/u_screen_info.h b/src/xrt/auxiliary/util/u_screen_info.h new file mode 100644 index 0000000000..8bd52719fb --- /dev/null +++ b/src/xrt/auxiliary/util/u_screen_info.h @@ -0,0 +1,196 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen display-info derivation (multi-screen M1). + * + * Turns one registry monitor into its @ref xrt_screen_info. Header-only and + * free of loader state so the rules are unit-testable + * (`tests/tests_aux_screen_info.cpp`): + * + * 1. The system-default screen reports the system-level info verbatim — it + * IS what `xrGetSystemProperties` reports, so `xrEnumerateDisplaysDXR` + * and `XrDisplayInfoDXR` can never disagree about it. + * 2. Otherwise the owning plug-in's `get_display_info_for_monitor` slot, + * when it implements one and answers. + * 3. Otherwise derived from EDID: physical size from the mm, native pixels, + * view scale 1, no eye tracking, and the nominal viewer centred at the + * distance that gives this screen the SAME vertical FOV the system panel's + * nominal viewer has (z = h * z_ref / h_ref). With no reference panel the + * viewer sits at @ref U_SCREEN_INFO_DEFAULT_VIEWER_Z_M. + * + * @ingroup aux_util + */ + +#pragma once + +#include "xrt/xrt_plugin.h" +#include "xrt/xrt_screen.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//! Viewer distance used when neither the screen nor a reference panel gives one. +#define U_SCREEN_INFO_DEFAULT_VIEWER_Z_M 0.6f + +/*! + * Everything @ref u_screen_info_resolve looks at for one screen. + */ +struct u_screen_info_inputs +{ + //! The registry descriptor of the screen (for the plug-in slot); may be NULL. + const struct xrt_display_descriptor *desc; + + //! EDID mm + the connector's device mode (0 = unknown); also handed to the slot. + struct xrt_display_physical physical; + + //! Current desktop size, used when the device mode is unknown. + uint32_t desktop_width; + uint32_t desktop_height; + + //! The plug-in that won the screen, and its instance (both may be NULL). + const struct xrt_plugin_iface *iface; + struct xrt_plugin_instance *inst; + + //! This is the system-default screen. + bool is_system_default; + + //! The system-level info (what XrSystemProperties reports), or NULL if + //! the active plug-in has not described its panel. Copied for the + //! system-default screen; the vertical-FOV reference for derived ones. + const struct xrt_screen_info *system_info; +}; + +/*! + * Build a @ref xrt_screen_info from the plug-in display-info struct (the + * shape both `get_display_info` and `get_display_info_for_monitor` return). + * A zero recommended view scale (the plug-in's "let the runtime derive") is + * left 0 for the caller to resolve. + */ +static inline void +u_screen_info_from_plugin(const struct xrt_plugin_display_info *pdi, uint32_t source, struct xrt_screen_info *out) +{ + memset(out, 0, sizeof(*out)); + out->width_m = pdi->display_width_m; + out->height_m = pdi->display_height_m; + out->nominal_viewer_x_m = pdi->nominal_viewer_x_m; + out->nominal_viewer_y_m = pdi->nominal_viewer_y_m; + out->nominal_viewer_z_m = pdi->nominal_viewer_z_m; + out->recommended_view_scale_x = pdi->recommended_view_scale_x; + out->recommended_view_scale_y = pdi->recommended_view_scale_y; + out->pixel_width = pdi->display_pixel_width; + out->pixel_height = pdi->display_pixel_height; + out->supported_eye_tracking_modes = pdi->supported_eye_tracking_modes; + out->default_eye_tracking_mode = pdi->default_eye_tracking_mode; + out->source = source; +} + +/*! + * Rule 3: derive a screen's info from EDID + the reference panel. + */ +static inline void +u_screen_info_derive(const struct u_screen_info_inputs *in, struct xrt_screen_info *out) +{ + memset(out, 0, sizeof(*out)); + out->source = XRT_SCREEN_INFO_SOURCE_DERIVED; + + out->width_m = (float)in->physical.physical_width_mm / 1000.0f; + out->height_m = (float)in->physical.physical_height_mm / 1000.0f; + + if (in->physical.native_pixel_width > 0 && in->physical.native_pixel_height > 0) { + out->pixel_width = in->physical.native_pixel_width; + out->pixel_height = in->physical.native_pixel_height; + } else { + out->pixel_width = in->desktop_width; + out->pixel_height = in->desktop_height; + } + + out->recommended_view_scale_x = 1.0f; + out->recommended_view_scale_y = 1.0f; + + // Same vertical FOV as the reference panel's nominal viewer: the viewer + // distance scales with the screen height. x/y stay 0 (centred). + const struct xrt_screen_info *ref = in->system_info; + float z = U_SCREEN_INFO_DEFAULT_VIEWER_Z_M; + if (ref != NULL && ref->nominal_viewer_z_m > 0.0f) { + z = ref->nominal_viewer_z_m; + if (ref->height_m > 0.0f && out->height_m > 0.0f) { + z = out->height_m * (ref->nominal_viewer_z_m / ref->height_m); + } + } + out->nominal_viewer_z_m = z; + + // No eye tracking: the runtime knows of no tracker for this screen. + out->supported_eye_tracking_modes = 0; + out->default_eye_tracking_mode = 0; +} + +/*! + * Resolve one screen's info by the rules in the file comment. + * + * @return the @ref xrt_screen_info_source that answered (also in out->source). + */ +static inline enum xrt_screen_info_source +u_screen_info_resolve(const struct u_screen_info_inputs *in, struct xrt_screen_info *out) +{ + // Rule 1: the system-default screen is the system info. + if (in->is_system_default && in->system_info != NULL) { + *out = *in->system_info; + out->source = XRT_SCREEN_INFO_SOURCE_SYSTEM; + return XRT_SCREEN_INFO_SOURCE_SYSTEM; + } + + // Rule 2: the owning plug-in describes the monitor. + if (in->desc != NULL && xrt_plugin_iface_has_display_info_for_monitor(in->iface)) { + struct xrt_plugin_display_info pdi; + memset(&pdi, 0, sizeof(pdi)); + pdi.struct_size = (uint32_t)sizeof(pdi); + struct xrt_display_physical phys = in->physical; + phys.struct_size = (uint32_t)sizeof(phys); + if (in->iface->get_display_info_for_monitor(in->inst, in->desc, &phys, &pdi)) { + u_screen_info_from_plugin(&pdi, XRT_SCREEN_INFO_SOURCE_PLUGIN_MONITOR, out); + // "0 = let the runtime derive". The system derives its baseline + // from the head device's mode table, which describes the system + // panel only; per-screen mode tables do not exist yet, so a + // non-default screen renders at native (1.0). + if (!(out->recommended_view_scale_x > 0.0f) || !(out->recommended_view_scale_y > 0.0f)) { + out->recommended_view_scale_x = 1.0f; + out->recommended_view_scale_y = 1.0f; + } + if (out->pixel_width == 0 || out->pixel_height == 0) { + struct xrt_screen_info d; + u_screen_info_derive(in, &d); + out->pixel_width = d.pixel_width; + out->pixel_height = d.pixel_height; + } + return XRT_SCREEN_INFO_SOURCE_PLUGIN_MONITOR; + } + } + + // Rule 3: EDID-derived defaults. + u_screen_info_derive(in, out); + if (!(out->width_m > 0.0f) && out->pixel_width == 0) { + out->source = XRT_SCREEN_INFO_SOURCE_NONE; + } + return (enum xrt_screen_info_source)out->source; +} + +//! Short name of a @ref xrt_screen_info_source, for logs and the CLI. Never NULL. +static inline const char * +u_screen_info_source_str(uint32_t source) +{ + switch (source) { + case XRT_SCREEN_INFO_SOURCE_SYSTEM: return "system"; + case XRT_SCREEN_INFO_SOURCE_PLUGIN_MONITOR: return "plugin-monitor"; + case XRT_SCREEN_INFO_SOURCE_DERIVED: return "derived"; + case XRT_SCREEN_INFO_SOURCE_NONE: + default: return "none"; + } +} + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/drivers/sim_display/sim_display_device.c b/src/xrt/drivers/sim_display/sim_display_device.c index 59c97a6413..d70a84b5a1 100644 --- a/src/xrt/drivers/sim_display/sim_display_device.c +++ b/src/xrt/drivers/sim_display/sim_display_device.c @@ -928,3 +928,25 @@ sim_display_hmd_create(void) return &hmd->base; } + + +/* + * + * Per-monitor defaults (multi-screen M1). + * + */ + +void +sim_display_get_default_viewer(float *out_w_m, float *out_h_m, float *out_nominal_y_m, float *out_nominal_z_m) +{ + // The same env-configurable defaults sim_display_hmd_create() starts from, + // and the same 10 cm eye height it gives its static pose. + if (out_w_m != NULL) + *out_w_m = debug_get_float_option_sim_display_width_m(); + if (out_h_m != NULL) + *out_h_m = debug_get_float_option_sim_display_height_m(); + if (out_nominal_y_m != NULL) + *out_nominal_y_m = 0.1f; + if (out_nominal_z_m != NULL) + *out_nominal_z_m = debug_get_float_option_sim_display_nominal_z_m(); +} diff --git a/src/xrt/drivers/sim_display/sim_display_interface.h b/src/xrt/drivers/sim_display/sim_display_interface.h index 080844962d..2bb00592c1 100644 --- a/src/xrt/drivers/sim_display/sim_display_interface.h +++ b/src/xrt/drivers/sim_display/sim_display_interface.h @@ -195,6 +195,18 @@ sim_display_get_display_info(struct xrt_device *xdev, struct sim_display_info *o void sim_display_get_panel_metrics(float *out_w_m, float *out_h_m, uint32_t *out_px_w, uint32_t *out_px_h); +/*! + * The simulated panel's DEFAULT geometry — physical size (SIM_DISPLAY_WIDTH_M / + * SIM_DISPLAY_HEIGHT_M), nominal eye height and viewing distance + * (SIM_DISPLAY_NOMINAL_Z_M) — without a device. Used to describe a monitor + * whose EDID gives no physical size (multi-screen M1, + * get_display_info_for_monitor). Any out-param may be NULL. + * + * @ingroup drv_sim_display + */ +void +sim_display_get_default_viewer(float *out_w_m, float *out_h_m, float *out_nominal_y_m, float *out_nominal_z_m); + /*! * Create a simulated 3D display HMD device. * diff --git a/src/xrt/drivers/sim_display/sim_display_plugin.c b/src/xrt/drivers/sim_display/sim_display_plugin.c index 906ff323dc..d1b4fd2d9f 100644 --- a/src/xrt/drivers/sim_display/sim_display_plugin.c +++ b/src/xrt/drivers/sim_display/sim_display_plugin.c @@ -145,6 +145,71 @@ sim_display_plugin_get_display_info(struct xrt_plugin_instance *inst, } +/*! + * Multi-screen M1: describe ONE monitor sim_display won (FALLBACK claim) — + * the per-monitor twin of @ref sim_display_plugin_get_display_info. Physical + * size from the monitor's EDID mm (the env/default size when EDID has none), + * pixels from the connector's device mode (else the desktop mode), sim's + * usual nominal viewer, and the same eye-tracking advertisement as the head + * (none, or MANUAL under SIM_DISPLAY_FAKE_TRACKING=1). + */ +static bool +sim_display_plugin_get_display_info_for_monitor(struct xrt_plugin_instance *inst, + const struct xrt_display_descriptor *display, + const struct xrt_display_physical *physical, + struct xrt_plugin_display_info *out_info) +{ + (void)inst; + if (display == NULL || out_info == NULL || + out_info->struct_size < + offsetof(struct xrt_plugin_display_info, supported_eye_tracking_modes) + 2 * sizeof(uint32_t)) { + return false; + } + + float w_m = 0.0f, h_m = 0.0f, ny = 0.0f, nz = 0.0f; + sim_display_get_default_viewer(&w_m, &h_m, &ny, &nz); + + uint32_t mm_w = 0, mm_h = 0, px_w = 0, px_h = 0; + if (physical != NULL && + physical->struct_size >= offsetof(struct xrt_display_physical, native_pixel_height) + sizeof(uint32_t)) { + mm_w = physical->physical_width_mm; + mm_h = physical->physical_height_mm; + px_w = physical->native_pixel_width; + px_h = physical->native_pixel_height; + } + if (mm_w > 0 && mm_h > 0) { + w_m = (float)mm_w / 1000.0f; + h_m = (float)mm_h / 1000.0f; + } + if (px_w == 0 || px_h == 0) { + px_w = display->pixel_width; + px_h = display->pixel_height; + } + + out_info->display_width_m = w_m; + out_info->display_height_m = h_m; + out_info->nominal_viewer_x_m = 0.0f; + out_info->nominal_viewer_y_m = ny; + out_info->nominal_viewer_z_m = nz; + out_info->display_pixel_width = px_w; + out_info->display_pixel_height = px_h; + out_info->recommended_view_scale_x = 0.0f; // runtime derives + out_info->recommended_view_scale_y = 0.0f; + out_info->display_screen_left = display->screen_left; + out_info->display_screen_top = display->screen_top; + if (sim_display_fake_tracking_enabled()) { + out_info->supported_eye_tracking_modes = 2u; /* MANUAL_BIT */ + out_info->default_eye_tracking_mode = 1u; /* MANUAL */ + } else { + out_info->supported_eye_tracking_modes = 0u; + out_info->default_eye_tracking_mode = 0u; + } + if (out_info->struct_size >= offsetof(struct xrt_plugin_display_info, refresh_mhz) + sizeof(uint32_t)) { + out_info->refresh_mhz = display->refresh_mhz; + } + return true; +} + static uint32_t sim_display_plugin_probe_displays(struct xrt_plugin_instance *inst, const struct xrt_display_descriptor *displays, @@ -224,13 +289,13 @@ static struct xrt_plugin_iface g_sim_display_iface = { .probe = sim_display_plugin_probe, .create_device = sim_display_plugin_create_device, - /* - * Per-graphics-API DP factories. sim_display ships factories for - * every API the platform supports; the runtime picks one at session - * creation based on the app's graphics binding. Each function - * pointer is the existing factory from sim_display_interface.h — - * the signatures already match the xrt_dp_factory_*_fn_t typedefs. - */ +/* + * Per-graphics-API DP factories. sim_display ships factories for + * every API the platform supports; the runtime picks one at session + * creation based on the app's graphics binding. Each function + * pointer is the existing factory from sim_display_interface.h — + * the signatures already match the xrt_dp_factory_*_fn_t typedefs. + */ #if defined(XRT_HAVE_VULKAN) || !defined(_WIN32) .create_dp_vk = sim_display_dp_factory_vk, /* #1243: vk_bundle ABI fingerprint (size + table offset) — see xrt_plugin_iface. */ @@ -296,6 +361,9 @@ static struct xrt_plugin_iface g_sim_display_iface = { .stereo_camera_wait_frame = sim_display_stereo_camera_wait_frame, .stereo_camera_release_frame = sim_display_stereo_camera_release_frame, .stereo_camera_close = sim_display_stereo_camera_close, + + /* Multi-screen M1: per-monitor display info for every monitor sim wins. */ + .get_display_info_for_monitor = sim_display_plugin_get_display_info_for_monitor, }; diff --git a/src/xrt/include/xrt/xrt_plugin.h b/src/xrt/include/xrt/xrt_plugin.h index 7804744823..d62d91e70f 100644 --- a/src/xrt/include/xrt/xrt_plugin.h +++ b/src/xrt/include/xrt/xrt_plugin.h @@ -158,8 +158,14 @@ struct xrt_plugin_display_info * * Forward-compat: the runtime sets `struct_size` to its own * `sizeof(struct xrt_display_descriptor)` before the call; plug-ins MUST - * NOT read past that offset. Field additions append at the end with no - * API version bump. + * NOT read past that offset. + * + * **Growth caveat (multi-screen M1):** `probe_displays` receives these as an + * ARRAY, and a plug-in indexes it with its own compile-time `sizeof`, so + * appending a field changes the array stride under every existing plug-in — + * that is a layout break (an XRT_PLUGIN_API_VERSION_CURRENT bump), not the + * free append `struct_size` suggests. New per-monitor facts go in a separate + * struct passed by pointer instead (see @ref xrt_display_physical). */ struct xrt_display_descriptor { @@ -200,6 +206,34 @@ struct xrt_display_descriptor uint32_t flags; }; +/*! + * Physical facts about one monitor that @ref xrt_display_descriptor does not + * carry (multi-screen M1), handed by pointer — never as an array — to + * `xrt_plugin_iface::get_display_info_for_monitor`, so it can grow by + * appending under `struct_size` with no ABI bump. + * + * The runtime sets `struct_size` to its own `sizeof`; a plug-in MUST NOT + * read past it. + */ +struct xrt_display_physical +{ + /*! `sizeof(struct xrt_display_physical)` at the runtime's compile time. */ + uint32_t struct_size; + + /*! Reserved for alignment. Must be 0. */ + uint32_t reserved_0; + + /*! Physical size from EDID (detailed timing, else the basic block), + * millimetres; 0 = unknown. */ + uint32_t physical_width_mm; + uint32_t physical_height_mm; + + /*! The connector's device (native) mode; may differ from the + * descriptor's pixel size under a scaled desktop. 0 = unknown. */ + uint32_t native_pixel_width; + uint32_t native_pixel_height; +}; + /*! * How sure a plug-in is that a monitor is its hardware. The runtime * resolves competing claims for the same monitor by highest confidence @@ -1114,6 +1148,40 @@ struct xrt_plugin_iface void (*stereo_camera_close)(struct xrt_plugin_stereo_camera *cam); /*! @} */ + + /*! + * Report the display info of ONE monitor this plug-in won in the + * per-monitor registry (multi-screen M1, `XR_DXR_display_info` v22 + * `xrEnumerateDisplaysDXR`). Same out struct and the same field meanings + * as @ref get_display_info, but keyed by the monitor instead of the head + * device: physical size, nominal viewer (display-centred, metres), native + * pixels, recommended view scale (0 = let the runtime derive), desktop + * origin, eye-tracking modes supported/default for THAT monitor. + * + * Contract: + * - @p display is the descriptor the runtime built for this monitor + * (the one `probe_displays` saw). @p physical carries what the + * descriptor does not (EDID millimetres, the connector's device + * mode); it may be NULL, and its fields are 0 when unknown. + * - The runtime sets `out_info->struct_size` and zero-fills the rest; + * the plug-in MUST NOT write past `struct_size`. + * - Cheap and non-blocking (it runs on `xrEnumerateDisplaysDXR` and in + * diagnostics), callable from any thread, with @p inst NULL for a + * plug-in that has no instance state. + * - Return false for a monitor the plug-in cannot describe; the runtime + * then derives defaults from EDID (no eye tracking, view scale 1). + * + * Optional. NULL (or a plug-in whose `struct_size` predates this field) + * ⟹ the runtime uses @ref get_display_info for the active plug-in's + * panel and EDID-derived defaults for every other monitor. Appended per + * ADR-020 (append-only within a major; gated by @ref struct_size; no + * XRT_PLUGIN_API_VERSION_CURRENT bump) AFTER the stereo camera block. + * Announced by @ref XRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITOR. + */ + bool (*get_display_info_for_monitor)(struct xrt_plugin_instance *inst, + const struct xrt_display_descriptor *display, + const struct xrt_display_physical *physical, + struct xrt_plugin_display_info *out_info); }; /*! @@ -1139,6 +1207,28 @@ struct xrt_plugin_iface */ #define XRT_PLUGIN_IFACE_HAS_STEREO_CAMERA 1 +/*! + * Defined when @ref xrt_plugin_iface carries @ref + * xrt_plugin_iface::get_display_info_for_monitor and this header defines + * @ref xrt_display_physical (multi-screen M1), so a plug-in built against an older runtime header can + * #ifdef-guard implementing it. + */ +#define XRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITOR 1 + +/*! + * True when @p iface implements @ref + * xrt_plugin_iface::get_display_info_for_monitor (and its struct_size covers + * the slot). + */ +static inline bool +xrt_plugin_iface_has_display_info_for_monitor(const struct xrt_plugin_iface *iface) +{ + return iface != NULL && + iface->struct_size >= offsetof(struct xrt_plugin_iface, get_display_info_for_monitor) + + sizeof(iface->get_display_info_for_monitor) && + iface->get_display_info_for_monitor != NULL; +} + /*! * True when @p iface fills the whole stereo camera slot set (and its * struct_size covers it). diff --git a/src/xrt/include/xrt/xrt_screen.h b/src/xrt/include/xrt/xrt_screen.h new file mode 100644 index 0000000000..9816dfa207 --- /dev/null +++ b/src/xrt/include/xrt/xrt_screen.h @@ -0,0 +1,185 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen (per-monitor) display information (multi-screen M1). + * + * One @ref xrt_screen per monitor in the per-monitor DP registry + * (`xrt_dp_factory_registry`), carrying the display info that until M1 only + * existed once, for the active plug-in's panel, in `xrt_system_compositor_info` + * (physical size, nominal viewer, recommended view scale, eye tracking). It is + * what `XR_DXR_display_info` v22 `xrEnumerateDisplaysDXR` reports, and what a + * session bound with `XrSessionDisplayBindingDXR` reads its display-scoped + * Kooima inputs from. + * + * Deliberately NOT part of `xrt_system_compositor_info`: that struct crosses + * IPC by value, so the list travels in its own message + * (`system_enumerate_displays`) and is produced on demand by + * `xrt_instance::enumerate_displays`. + * + * Plain data, fixed size, no pointers: safe to copy across IPC. + * + * @ingroup xrt_iface + */ + +#pragma once + +#include "xrt/xrt_compiler.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//! Max screens in a @ref xrt_screen_list (= XRT_DP_REGISTRY_MAX_ENTRIES). +#define XRT_SCREEN_LIST_MAX 16 + +//! Size of @ref xrt_screen::plugin_id and @ref xrt_screen::device_name, incl. the NUL. +#define XRT_SCREEN_NAME_MAX 64 + +/*! + * The id a screen gets when the runtime could not enumerate monitors at all + * (empty registry: macOS, Android, a Windows box whose EDID read failed) and + * so synthesizes the single system-default screen. Non-zero, because 0 means + * "runtime decides" in `XrSessionDisplayBindingDXR`. + */ +#define XRT_SCREEN_ID_SYNTHETIC_DEFAULT ((uint64_t)1) + +/*! + * Where a screen's @ref xrt_screen_info came from (diagnostics + tests). + */ +enum xrt_screen_info_source +{ + //! Nothing could be said about the screen (no size, no reference). + XRT_SCREEN_INFO_SOURCE_NONE = 0, + //! The system-level info: this is the system-default screen, so its block + //! IS what `xrGetSystemProperties` reports (the active plug-in's + //! `get_display_info`, as applied to the system). + XRT_SCREEN_INFO_SOURCE_SYSTEM = 1, + //! The owning plug-in's `get_display_info_for_monitor` slot. + XRT_SCREEN_INFO_SOURCE_PLUGIN_MONITOR = 2, + //! Derived by the runtime from EDID mm + pixel size (no eye tracking, + //! view scale 1, nominal viewer at the system panel's vertical FOV). + XRT_SCREEN_INFO_SOURCE_DERIVED = 3, +}; + +/*! + * Bits for @ref xrt_screen::flags. Values match `XrDisplayFlagsDXR`. + * @{ + */ +//! The OS desktop's primary monitor. +#define XRT_SCREEN_FLAG_PRIMARY (1u << 0) +//! The screen has live eye tracking (`info.supported_eye_tracking_modes != 0`). +#define XRT_SCREEN_FLAG_TRACKED (1u << 1) +//! The system-default screen: the one `xrt_system_compositor_info` describes. +#define XRT_SCREEN_FLAG_SYSTEM_DEFAULT (1u << 2) +/*! @} */ + +/*! + * The display-info block of one screen — the per-screen counterpart of the + * `display_*` / `nominal_viewer_*` / `recommended_view_scale_*` / + * `*_eye_tracking_mode*` fields of `xrt_system_compositor_info`. + */ +struct xrt_screen_info +{ + //! Physical size of the visible area, metres; 0 = unknown. + float width_m; + float height_m; + + //! Nominal viewer relative to the screen centre (+X right, +Y up, +Z toward the viewer), metres. + float nominal_viewer_x_m; + float nominal_viewer_y_m; + float nominal_viewer_z_m; + + //! Recommended per-view scale (render px = window px * scale). + float recommended_view_scale_x; + float recommended_view_scale_y; + + //! Native panel resolution, pixels; 0 = unknown. + uint32_t pixel_width; + uint32_t pixel_height; + + //! Eye-tracking modes (bit 0 MANAGED, bit 1 MANUAL; 0 = none) and the default (0 MANAGED, 1 MANUAL). + uint32_t supported_eye_tracking_modes; + uint32_t default_eye_tracking_mode; + + //! @ref xrt_screen_info_source. + uint32_t source; +}; + +/*! + * One screen (monitor) as the runtime knows it. + */ +struct xrt_screen +{ + //! Registry monitor id (EDID-derived; see `xrt_display_descriptor::monitor_id`), or + //! @ref XRT_SCREEN_ID_SYNTHETIC_DEFAULT. Never 0. + uint64_t id; + + //! `XRT_SCREEN_FLAG_*`. + uint32_t flags; + + //! Winning claim confidence (`enum xrt_display_claim_confidence`); 0 when synthesized. + uint32_t confidence; + + //! Current desktop rect, in the space the OS places windows in. + int32_t desktop_left; + int32_t desktop_top; + uint32_t desktop_width; + uint32_t desktop_height; + + //! The connector's device mode; 0 = unknown. + uint32_t native_width; + uint32_t native_height; + + //! Desktop compositor scale for this output; 0 = unknown. + float desktop_scale; + + //! EDID physical size, mm; 0 = unknown. + uint32_t physical_width_mm; + uint32_t physical_height_mm; + + //! Discovery id of the plug-in that won the screen ("" = none). + char plugin_id[XRT_SCREEN_NAME_MAX]; + + //! OS output name (X11: RandR output, else the DRM connector); "" = unknown. + char device_name[XRT_SCREEN_NAME_MAX]; + + //! Display info of this screen. + struct xrt_screen_info info; +}; + +/*! + * Fixed-capacity screen list — the payload of the `system_enumerate_displays` + * IPC message and the out-param of `xrt_instance::enumerate_displays`. The + * system-default screen, when there is one, is `screens[0]`. + */ +struct xrt_screen_list +{ + uint32_t count; + uint32_t _pad; + struct xrt_screen screens[XRT_SCREEN_LIST_MAX]; +}; + +/*! + * Find a screen by id; NULL when absent (or @p id is 0). + */ +static inline const struct xrt_screen * +xrt_screen_list_find(const struct xrt_screen_list *list, uint64_t id) +{ + if (list == NULL || id == 0) { + return NULL; + } + for (uint32_t i = 0; i < list->count && i < XRT_SCREEN_LIST_MAX; i++) { + if (list->screens[i].id == id) { + return &list->screens[i]; + } + } + return NULL; +} + +#ifdef __cplusplus +} +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 834d9cec36..a888781f95 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,7 @@ set(tests tests_aux_mini_window_tell tests_aux_zone_canvas tests_aux_tiling_zero_copy + tests_aux_screen_info tests_aux_tiling_row_origin tests_aux_wayland_geom tests_comp_wl_move_sync_tag @@ -261,6 +262,11 @@ endif() # arithmetic only; it reads xrt_rendering_mode, hence xrt-interfaces. target_link_libraries(tests_aux_tiling_zero_copy PRIVATE xrt-interfaces) +# Multi-screen M1 -- per-screen display-info derivation (u_screen_info.h): +# system-default / plug-in per-monitor slot / EDID-derived rules. Header only; +# it reads the plug-in ABI structs, hence xrt-interfaces. +target_link_libraries(tests_aux_screen_info PRIVATE xrt-interfaces) + # #1625 -- the atlas tile ROW ORIGIN: view 0 is the top-left tile as displayed, # on every backend. The GL-vs-everyone-else disagreement was two lines of # integer arithmetic, so it is pinned on the host rather than on five GPUs. diff --git a/tests/tests_aux_screen_info.cpp b/tests/tests_aux_screen_info.cpp new file mode 100644 index 0000000000..a2f35575d7 --- /dev/null +++ b/tests/tests_aux_screen_info.cpp @@ -0,0 +1,216 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Multi-screen M1: one registry monitor -> its xrt_screen_info. + * + * Pins the three rules of u_screen_info_resolve() (util/u_screen_info.h): + * 1. the system-default screen reports the system info verbatim; + * 2. otherwise the owning plug-in's get_display_info_for_monitor slot; + * 3. otherwise EDID-derived defaults at the system panel's vertical FOV. + * Plus the ABI gate: a plug-in whose struct_size predates the slot is never + * called through it. + */ + +#include "catch_amalgamated.hpp" + +#include "util/u_screen_info.h" + +#include +#include + +namespace { + +struct fake_slot_state +{ + int calls = 0; + bool answer = true; + uint32_t seen_mm_w = 0; + uint64_t seen_monitor = 0; +} g_slot; + +bool +fake_get_display_info_for_monitor(struct xrt_plugin_instance * /*inst*/, + const struct xrt_display_descriptor *display, + const struct xrt_display_physical *physical, + struct xrt_plugin_display_info *out) +{ + g_slot.calls++; + g_slot.seen_monitor = display->monitor_id; + g_slot.seen_mm_w = physical != nullptr ? physical->physical_width_mm : 0; + if (!g_slot.answer) { + return false; + } + out->display_width_m = 0.5f; + out->display_height_m = 0.3f; + out->nominal_viewer_y_m = 0.1f; + out->nominal_viewer_z_m = 0.9f; + out->display_pixel_width = 2560; + out->display_pixel_height = 1440; + out->supported_eye_tracking_modes = 2u; // MANUAL + out->default_eye_tracking_mode = 1u; + // recommended_view_scale left 0: "let the runtime derive". + return true; +} + +struct fixture +{ + xrt_plugin_iface iface{}; + xrt_display_descriptor desc{}; + xrt_screen_info system{}; + u_screen_info_inputs in{}; + + fixture() + { + g_slot = fake_slot_state{}; + + iface.struct_size = (uint32_t)sizeof(iface); + iface.get_display_info_for_monitor = fake_get_display_info_for_monitor; + + desc.struct_size = (uint32_t)sizeof(desc); + desc.monitor_id = 0x1234; + desc.pixel_width = 3456; + desc.pixel_height = 2160; + + // A 0.344 x 0.193 m panel viewed from 0.6 m — the reference FOV. + system.width_m = 0.344f; + system.height_m = 0.193f; + system.nominal_viewer_z_m = 0.6f; + system.recommended_view_scale_x = 0.5f; + system.recommended_view_scale_y = 0.5f; + system.pixel_width = 3840; + system.pixel_height = 2160; + system.supported_eye_tracking_modes = 1u; + system.source = XRT_SCREEN_INFO_SOURCE_SYSTEM; + + in.desc = &desc; + in.physical.struct_size = (uint32_t)sizeof(in.physical); + in.physical.physical_width_mm = 300; + in.physical.physical_height_mm = 190; + in.physical.native_pixel_width = 2880; + in.physical.native_pixel_height = 1800; + in.desktop_width = 3456; + in.desktop_height = 2160; + in.iface = &iface; + in.system_info = &system; + } +}; + +} // namespace + +TEST_CASE("system-default screen reports the system info verbatim, never the slot") +{ + fixture f; + f.in.is_system_default = true; + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_SYSTEM); + CHECK(g_slot.calls == 0); + CHECK(out.width_m == f.system.width_m); + CHECK(out.nominal_viewer_z_m == f.system.nominal_viewer_z_m); + CHECK(out.recommended_view_scale_x == 0.5f); + CHECK(out.supported_eye_tracking_modes == 1u); +} + +TEST_CASE("non-default screen with the plug-in slot: the plug-in answers") +{ + fixture f; + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_PLUGIN_MONITOR); + CHECK(g_slot.calls == 1); + CHECK(g_slot.seen_monitor == 0x1234); + CHECK(g_slot.seen_mm_w == 300); // the EDID mm reach the plug-in + CHECK(out.width_m == 0.5f); + CHECK(out.height_m == 0.3f); + CHECK(out.nominal_viewer_y_m == 0.1f); + CHECK(out.nominal_viewer_z_m == 0.9f); + CHECK(out.pixel_width == 2560); + CHECK(out.supported_eye_tracking_modes == 2u); + CHECK(out.default_eye_tracking_mode == 1u); + // "0 = let the runtime derive" resolves to native for a non-default screen. + CHECK(out.recommended_view_scale_x == 1.0f); + CHECK(out.recommended_view_scale_y == 1.0f); +} + +TEST_CASE("non-default screen without the slot: EDID-derived at the reference vertical FOV") +{ + fixture f; + f.iface.get_display_info_for_monitor = nullptr; + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_DERIVED); + CHECK(out.width_m == Catch::Approx(0.300f)); + CHECK(out.height_m == Catch::Approx(0.190f)); + CHECK(out.pixel_width == 2880); // the connector's device mode, not the desktop's + CHECK(out.pixel_height == 1800); + CHECK(out.recommended_view_scale_x == 1.0f); + CHECK(out.supported_eye_tracking_modes == 0u); + CHECK(out.nominal_viewer_x_m == 0.0f); + CHECK(out.nominal_viewer_y_m == 0.0f); + // Same vertical FOV: z / h equal on both screens. + CHECK(out.nominal_viewer_z_m == Catch::Approx(0.190f * 0.6f / 0.193f)); + CHECK(out.nominal_viewer_z_m / out.height_m == Catch::Approx(f.system.nominal_viewer_z_m / f.system.height_m)); +} + +TEST_CASE("slot that declines falls back to the EDID derivation") +{ + fixture f; + g_slot.answer = false; + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_DERIVED); + CHECK(g_slot.calls == 1); + CHECK(out.width_m == Catch::Approx(0.300f)); +} + +TEST_CASE("a plug-in built before the slot existed is never called through it") +{ + fixture f; + f.iface.struct_size = (uint32_t)offsetof(xrt_plugin_iface, get_display_info_for_monitor); + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_DERIVED); + CHECK(g_slot.calls == 0); +} + +TEST_CASE("unknown mm and no device mode: desktop pixels, reference viewer distance") +{ + fixture f; + f.iface.get_display_info_for_monitor = nullptr; + f.in.physical.physical_width_mm = 0; + f.in.physical.physical_height_mm = 0; + f.in.physical.native_pixel_width = 0; + f.in.physical.native_pixel_height = 0; + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_DERIVED); + CHECK(out.width_m == 0.0f); + CHECK(out.pixel_width == 3456); + CHECK(out.pixel_height == 2160); + CHECK(out.nominal_viewer_z_m == Catch::Approx(0.6f)); +} + +TEST_CASE("no system info at all: default viewer distance, system-default flag cannot copy") +{ + fixture f; + f.iface.get_display_info_for_monitor = nullptr; + f.in.system_info = nullptr; + f.in.is_system_default = true; + + xrt_screen_info out{}; + CHECK(u_screen_info_resolve(&f.in, &out) == XRT_SCREEN_INFO_SOURCE_DERIVED); + CHECK(out.nominal_viewer_z_m == Catch::Approx(U_SCREEN_INFO_DEFAULT_VIEWER_Z_M)); +} + +TEST_CASE("xrt_screen_list_find: by id, never id 0") +{ + xrt_screen_list list{}; + list.count = 2; + list.screens[0].id = 7; + list.screens[1].id = 9; + CHECK(xrt_screen_list_find(&list, 9) == &list.screens[1]); + CHECK(xrt_screen_list_find(&list, 8) == nullptr); + CHECK(xrt_screen_list_find(&list, 0) == nullptr); + CHECK(xrt_screen_list_find(nullptr, 7) == nullptr); +} From daa78f440536763035dac775fc0d2c92502a2f3c Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 10:58:11 -0700 Subject: [PATCH 02/10] feat(runtime): per-screen list from the DP registry, in-process and over IPC (multi-screen M1) xrt_instance::enumerate_displays (optional slot) returns the system's screens: target_screens_build turns each per-monitor registry entry into an xrt_screen (desktop rect, device mode, compositor scale, EDID mm, output name, winning plug-in, display info), system-default first. The native instance answers from the system it created; the IPC client asks the service through the new system_enumerate_displays message, so xrt_system_compositor_info keeps its by-value layout. The loader exposes its monitor side table (target_plugin_get_monitor_record) for this. Co-Authored-By: Claude Fable 5.1 --- src/xrt/include/xrt/xrt_instance.h | 30 +++ src/xrt/ipc/client/ipc_client_instance.c | 14 ++ src/xrt/ipc/server/ipc_server_handler.c | 20 ++ src/xrt/ipc/shared/ipc_protocol.h | 1 + src/xrt/ipc/shared/proto.json | 6 + src/xrt/targets/common/CMakeLists.txt | 1 + src/xrt/targets/common/target_instance.c | 39 ++++ src/xrt/targets/common/target_plugin_loader.c | 30 +++ src/xrt/targets/common/target_plugin_loader.h | 14 ++ src/xrt/targets/common/target_screens.c | 210 ++++++++++++++++++ src/xrt/targets/common/target_screens.h | 74 ++++++ 11 files changed, 439 insertions(+) create mode 100644 src/xrt/targets/common/target_screens.c create mode 100644 src/xrt/targets/common/target_screens.h diff --git a/src/xrt/include/xrt/xrt_instance.h b/src/xrt/include/xrt/xrt_instance.h index 4fc5d6be42..126bb61c59 100644 --- a/src/xrt/include/xrt/xrt_instance.h +++ b/src/xrt/include/xrt/xrt_instance.h @@ -26,6 +26,7 @@ struct xrt_instance_android; struct xrt_stereo_camera_client; struct xrt_plugin_iface; struct xrt_plugin_instance; +struct xrt_screen_list; struct xrt_space_overseer; struct xrt_system; struct xrt_system_devices; @@ -279,8 +280,37 @@ struct xrt_instance bool (*get_active_plugin)(struct xrt_instance *xinst, const struct xrt_plugin_iface **out_iface, struct xrt_plugin_instance **out_inst); + + /*! + * Optional (multi-screen M1, `XR_DXR_display_info` v22): the system's + * screens — one per monitor in the per-monitor DP registry, the + * system-default one first — with each screen's display info. The native + * instance builds it from the registry of the system it created; the IPC + * client instance asks the service (`system_enumerate_displays`), so both + * paths return the same list. Call after `create_system`; an empty list + * (count 0) is a valid answer. NULL on instances that cannot answer + * (the no-compositor instance). + */ + xrt_result_t (*enumerate_displays)(struct xrt_instance *xinst, struct xrt_screen_list *out_list); }; +/*! + * @copydoc xrt_instance::enumerate_displays + * + * Returns XRT_ERROR_FEATURE_NOT_SUPPORTED when the instance has no + * implementation. + * + * @public @memberof xrt_instance + */ +static inline xrt_result_t +xrt_instance_enumerate_displays(struct xrt_instance *xinst, struct xrt_screen_list *out_list) +{ + if (xinst == NULL || xinst->enumerate_displays == NULL) { + return XRT_ERROR_FEATURE_NOT_SUPPORTED; + } + return xinst->enumerate_displays(xinst, out_list); +} + /*! * @copydoc xrt_instance::create_system * diff --git a/src/xrt/ipc/client/ipc_client_instance.c b/src/xrt/ipc/client/ipc_client_instance.c index 9cce353915..f879583321 100644 --- a/src/xrt/ipc/client/ipc_client_instance.c +++ b/src/xrt/ipc/client/ipc_client_instance.c @@ -217,6 +217,19 @@ ipc_client_instance_create_system(struct xrt_instance *xinst, return xret; } +/*! + * xrt_instance::enumerate_displays (multi-screen M1): the service's screen + * list, so `xrEnumerateDisplaysDXR` under IPC matches the in-process answer. + */ +static xrt_result_t +ipc_client_instance_enumerate_displays(struct xrt_instance *xinst, struct xrt_screen_list *out_list) +{ + struct ipc_client_instance *ii = ipc_client_instance(xinst); + + xrt_result_t xret = ipc_call_system_enumerate_displays(&ii->ipc_c, out_list); + IPC_CHK_ALWAYS_RET(&ii->ipc_c, xret, "ipc_call_system_enumerate_displays"); +} + static xrt_result_t ipc_client_instance_get_prober(struct xrt_instance *xinst, struct xrt_prober **out_xp) { @@ -276,6 +289,7 @@ ipc_instance_create(const struct xrt_instance_info *i_info, struct xrt_instance ii->base.create_system = ipc_client_instance_create_system; ii->base.get_prober = ipc_client_instance_get_prober; ii->base.destroy = ipc_client_instance_destroy; + ii->base.enumerate_displays = ipc_client_instance_enumerate_displays; #ifdef XRT_OS_WINDOWS timeBeginPeriod(1); diff --git a/src/xrt/ipc/server/ipc_server_handler.c b/src/xrt/ipc/server/ipc_server_handler.c index bb1d8ab29e..f18494d856 100644 --- a/src/xrt/ipc/server/ipc_server_handler.c +++ b/src/xrt/ipc/server/ipc_server_handler.c @@ -2242,6 +2242,26 @@ ipc_handle_system_compositor_get_info(volatile struct ipc_client_state *ics, return XRT_SUCCESS; } +/*! + * Multi-screen M1 (`XR_DXR_display_info` v22): the service's screen list, so an + * IPC client's `xrEnumerateDisplaysDXR` returns exactly what an in-process app + * would. A separate message on purpose — `xrt_system_compositor_info` crosses + * the wire by value and is not grown for this. + */ +xrt_result_t +ipc_handle_system_enumerate_displays(volatile struct ipc_client_state *ics, struct xrt_screen_list *out_list) +{ + IPC_TRACE_MARKER(); + + xrt_result_t xret = xrt_instance_enumerate_displays(ics->server->xinst, out_list); + if (xret == XRT_ERROR_FEATURE_NOT_SUPPORTED) { + // An instance that cannot enumerate has no screens to report. + memset(out_list, 0, sizeof(*out_list)); + return XRT_SUCCESS; + } + return xret; +} + xrt_result_t ipc_handle_session_create(volatile struct ipc_client_state *ics, const struct xrt_session_info *xsi, diff --git a/src/xrt/ipc/shared/ipc_protocol.h b/src/xrt/ipc/shared/ipc_protocol.h index 799fd74733..68e69483fa 100644 --- a/src/xrt/ipc/shared/ipc_protocol.h +++ b/src/xrt/ipc/shared/ipc_protocol.h @@ -26,6 +26,7 @@ #include "xrt/xrt_tracking.h" #include "xrt/xrt_display_metrics.h" #include "xrt/xrt_stereo_camera.h" +#include "xrt/xrt_screen.h" #include "xrt/xrt_config_build.h" #include diff --git a/src/xrt/ipc/shared/proto.json b/src/xrt/ipc/shared/proto.json index 9084763834..21f4e76dc7 100644 --- a/src/xrt/ipc/shared/proto.json +++ b/src/xrt/ipc/shared/proto.json @@ -320,6 +320,12 @@ ] }, + "system_enumerate_displays": { + "out": [ + {"name": "list", "type": "struct xrt_screen_list"} + ] + }, + "session_create": { "in": [ {"name": "xsi", "type": "struct xrt_session_info"}, diff --git a/src/xrt/targets/common/CMakeLists.txt b/src/xrt/targets/common/CMakeLists.txt index 565ef40f28..e9e8731a60 100644 --- a/src/xrt/targets/common/CMakeLists.txt +++ b/src/xrt/targets/common/CMakeLists.txt @@ -13,6 +13,7 @@ add_library( target_input_arbiter.c target_input_arbiter.h target_input_plugin_loader.c target_input_plugin_loader.h target_plugin_loader.c target_plugin_loader.h + target_screens.c target_screens.h target_plugin_preload_sanitize.c target_plugin_preload_sanitize.h target_plugin_path_guard.c target_plugin_path_guard.h target_rig_composer.c target_rig_composer.h diff --git a/src/xrt/targets/common/target_instance.c b/src/xrt/targets/common/target_instance.c index 8919f265f5..fd789854f8 100644 --- a/src/xrt/targets/common/target_instance.c +++ b/src/xrt/targets/common/target_instance.c @@ -12,6 +12,7 @@ #include "xrt/xrt_space.h" #include "xrt/xrt_system.h" #include "xrt/xrt_compositor.h" +#include "xrt/xrt_screen.h" #include "xrt/xrt_config_build.h" #include "xrt/xrt_config_os.h" @@ -42,6 +43,7 @@ // longer link-includes any vendor drv_* code (ADR-019 / #256 / #263). #include "target_plugin_loader.h" +#include "target_screens.h" #include "target_instance_parts.h" @@ -204,6 +206,10 @@ fill_display_desktop_info(struct xrt_system_compositor_info *info); //! The head device the display info describes; NULL until a system exists. static struct xrt_device *g_display_info_head = NULL; +//! The system compositor info the per-monitor registry and the display info +//! live in — what `xrt_instance::enumerate_displays` builds the screen list +//! from (multi-screen M1). NULL until a system with a compositor exists. +static const struct xrt_system_compositor_info *g_screens_info = NULL; //! The plug-in display info that was last APPLIED to `xsysc->info` — what the //! refresh compares against, so it is idempotent regardless of what //! `fill_display_desktop_info` later overrides on the info struct itself. @@ -908,6 +914,7 @@ t_instance_create_system(struct xrt_instance *xinst, } g_display_info_head = head; g_display_info_applied_valid = false; + g_screens_info = &xsysc->info; xsysc->info.refresh_display_processors = refresh_display_processors_cb; // Vendor-neutral display-info population through the plug-in @@ -988,6 +995,37 @@ t_instance_get_active_plugin(struct xrt_instance *xinst, return iface != NULL; } +/*! + * xrt_instance::enumerate_displays (multi-screen M1) — the screen list of the + * system this instance created, built from its per-monitor registry and the + * display info applied to it. The service answers `system_enumerate_displays` + * through this too. Serialised with the refresh callback, which rewrites + * both on a client connect. + */ +static xrt_result_t +t_instance_enumerate_displays(struct xrt_instance *xinst, struct xrt_screen_list *out_list) +{ + (void)xinst; + if (out_list == NULL) { + return XRT_ERROR_ALLOCATION; + } + memset(out_list, 0, sizeof(*out_list)); + if (g_screens_info == NULL) { + return XRT_SUCCESS; // no system (or no compositor) yet: no screens + } + + if (g_display_info_mutex_initialized) { + os_mutex_lock(&g_display_info_mutex); + } + struct target_screens_system sys; + target_screens_system_from_info(g_screens_info, &sys); + target_screens_build(&g_screens_info->dp_registry, g_screens_info->active_plugin_id, &sys, out_list); + if (g_display_info_mutex_initialized) { + os_mutex_unlock(&g_display_info_mutex); + } + return XRT_SUCCESS; +} + #ifdef XRT_FEATURE_HYBRID_MODE // In hybrid mode, export as native_instance_create to avoid symbol conflict // with ipc_instance_create from the IPC client library @@ -1014,6 +1052,7 @@ xrt_instance_create(struct xrt_instance_info *ii, struct xrt_instance **out_xins tinst->base.get_prober = t_instance_get_prober; tinst->base.destroy = t_instance_destroy; tinst->base.get_active_plugin = t_instance_get_active_plugin; + tinst->base.enumerate_displays = t_instance_enumerate_displays; tinst->xp = xp; tinst->base.startup_timestamp = os_monotonic_get_ns(); diff --git a/src/xrt/targets/common/target_plugin_loader.c b/src/xrt/targets/common/target_plugin_loader.c index 57de1f070f..f9b6e85f4d 100644 --- a/src/xrt/targets/common/target_plugin_loader.c +++ b/src/xrt/targets/common/target_plugin_loader.c @@ -3205,6 +3205,7 @@ static struct { uint64_t monitor_id; struct os_display_edid_monitor mon; + struct xrt_display_descriptor desc; } g_monitor_side[XRT_DP_REGISTRY_MAX_ENTRIES]; static uint32_t g_monitor_side_count = 0; @@ -3319,6 +3320,7 @@ target_plugin_build_descriptors(const struct os_display_edid_list *list, for (uint32_t i = 0; i < g_monitor_side_count; i++) { g_monitor_side[i].monitor_id = out[i].monitor_id; g_monitor_side[i].mon = list->monitors[i]; + g_monitor_side[i].desc = out[i]; } if (g_refresh_mutex_initialized) { os_mutex_unlock(&g_refresh_mutex); @@ -3326,6 +3328,34 @@ target_plugin_build_descriptors(const struct os_display_edid_list *list, return n; } +bool +target_plugin_get_monitor_record(uint64_t monitor_id, + struct xrt_display_descriptor *out_desc, + struct os_display_edid_monitor *out_mon) +{ + bool found = false; + if (g_refresh_mutex_initialized) { + os_mutex_lock(&g_refresh_mutex); + } + for (uint32_t i = 0; i < g_monitor_side_count; i++) { + if (g_monitor_side[i].monitor_id != monitor_id) { + continue; + } + if (out_desc != NULL) { + *out_desc = g_monitor_side[i].desc; + } + if (out_mon != NULL) { + *out_mon = g_monitor_side[i].mon; + } + found = true; + break; + } + if (g_refresh_mutex_initialized) { + os_mutex_unlock(&g_refresh_mutex); + } + return found; +} + #ifndef XRT_OS_WINDOWS static const struct os_display_edid_monitor * monitor_side_find(uint64_t monitor_id) diff --git a/src/xrt/targets/common/target_plugin_loader.h b/src/xrt/targets/common/target_plugin_loader.h index 9011d6bcbb..517fa37114 100644 --- a/src/xrt/targets/common/target_plugin_loader.h +++ b/src/xrt/targets/common/target_plugin_loader.h @@ -340,6 +340,7 @@ target_plugin_build_discovery_roots(char roots[][PATH_MAX], int max_roots); * os/os_display_edid.h; the .c includes those, the header stays lean. */ struct xrt_dp_factory_registry; struct os_display_edid_list; +struct os_display_edid_monitor; /*! * Map an enumerated EDID monitor list (`os_display_edid_enumerate`) into the @@ -387,6 +388,19 @@ target_plugin_resolve_displays(const struct xrt_display_descriptor *descriptors, uint32_t descriptor_count, struct xrt_dp_factory_registry *out_registry); +/*! + * The runtime-private record behind a monitor id from the last + * @ref target_plugin_build_descriptors: the descriptor the plug-ins saw and + * the full EDID/placement record (mm, connector, output name, device mode). + * Either out-param may be NULL. False when the id is unknown (or the + * platform enumerates no monitors). Multi-screen M1: feeds the per-screen + * info that `xrEnumerateDisplaysDXR` reports. + */ +bool +target_plugin_get_monitor_record(uint64_t monitor_id, + struct xrt_display_descriptor *out_desc, + struct os_display_edid_monitor *out_mon); + struct xrt_plugin_display_info; struct os_display_edid_monitor; struct os_display_panel_hint; diff --git a/src/xrt/targets/common/target_screens.c b/src/xrt/targets/common/target_screens.c new file mode 100644 index 0000000000..9f725b60cb --- /dev/null +++ b/src/xrt/targets/common/target_screens.c @@ -0,0 +1,210 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Per-screen list from the per-monitor DP registry (multi-screen M1). + * @ingroup targets_common + */ + +#include "target_screens.h" +#include "target_plugin_loader.h" + +#include "xrt/xrt_plugin.h" + +#include "os/os_display_edid.h" +#include "os/os_display_desktop.h" +#include "util/u_screen_info.h" + +#include +#include + + +void +target_screens_system_from_info(const struct xrt_system_compositor_info *info, struct target_screens_system *out) +{ + memset(out, 0, sizeof(*out)); + if (info == NULL) { + return; + } + out->info_valid = info->display_width_m > 0.0f && info->display_height_m > 0.0f; + out->info.width_m = info->display_width_m; + out->info.height_m = info->display_height_m; + out->info.nominal_viewer_x_m = info->nominal_viewer_x_m; + out->info.nominal_viewer_y_m = info->nominal_viewer_y_m; + out->info.nominal_viewer_z_m = info->nominal_viewer_z_m; + out->info.recommended_view_scale_x = info->recommended_view_scale_x; + out->info.recommended_view_scale_y = info->recommended_view_scale_y; + out->info.pixel_width = info->display_pixel_width; + out->info.pixel_height = info->display_pixel_height; + out->info.supported_eye_tracking_modes = info->supported_eye_tracking_modes; + out->info.default_eye_tracking_mode = info->default_eye_tracking_mode; + out->info.source = XRT_SCREEN_INFO_SOURCE_SYSTEM; + + out->desktop_left = info->display_screen_left; + out->desktop_top = info->display_screen_top; + out->desktop_width = info->display_desktop_width; + out->desktop_height = info->display_desktop_height; + out->native_width = info->display_desktop_native_width; + out->native_height = info->display_desktop_native_height; + out->desktop_scale = info->display_desktop_scale; + out->is_primary = info->display_is_primary; + (void)snprintf(out->device_name, sizeof(out->device_name), "%.*s", (int)(sizeof(out->device_name) - 1), + info->display_device_name); +} + +//! The compositor scale of the desktop monitor named @p name (0 = unknown). +static float +desktop_scale_for(const struct os_display_desktop_info *mons, uint32_t count, const char *name) +{ + if (name == NULL || name[0] == '\0') { + return 0.0f; + } + for (uint32_t i = 0; i < count; i++) { + if (strcmp(mons[i].device_name, name) == 0) { + return (float)mons[i].scale; + } + } + return 0.0f; +} + +static void +finish_flags(struct xrt_screen *s, bool is_primary, bool is_default) +{ + s->flags = 0; + if (is_primary) { + s->flags |= XRT_SCREEN_FLAG_PRIMARY; + } + if (s->info.supported_eye_tracking_modes != 0) { + s->flags |= XRT_SCREEN_FLAG_TRACKED; + } + if (is_default) { + s->flags |= XRT_SCREEN_FLAG_SYSTEM_DEFAULT; + } +} + +static void +build_from_entry(const struct xrt_dp_registry_entry *e, + bool is_default, + const struct target_screens_system *sys, + const struct os_display_desktop_info *desk, + uint32_t desk_count, + struct xrt_screen *s) +{ + memset(s, 0, sizeof(*s)); + s->id = e->monitor_id; + s->confidence = e->confidence; + s->desktop_left = e->screen_left; + s->desktop_top = e->screen_top; + s->desktop_width = e->pixel_width; + s->desktop_height = e->pixel_height; + (void)snprintf(s->plugin_id, sizeof(s->plugin_id), "%s", e->plugin_id); + + struct xrt_display_descriptor desc; + struct os_display_edid_monitor mon; + memset(&desc, 0, sizeof(desc)); + memset(&mon, 0, sizeof(mon)); + const bool have_record = target_plugin_get_monitor_record(e->monitor_id, &desc, &mon); + bool is_primary = false; + if (have_record) { + s->native_width = mon.native_width; + s->native_height = mon.native_height; + s->physical_width_mm = mon.physical_width_mm; + s->physical_height_mm = mon.physical_height_mm; + (void)snprintf(s->device_name, sizeof(s->device_name), "%s", + mon.output_name[0] != '\0' ? mon.output_name : mon.connector); + is_primary = mon.is_primary; + } + s->desktop_scale = desktop_scale_for(desk, desk_count, s->device_name); + + struct u_screen_info_inputs in; + memset(&in, 0, sizeof(in)); + in.desc = have_record ? &desc : NULL; + in.physical.struct_size = (uint32_t)sizeof(in.physical); + in.physical.physical_width_mm = s->physical_width_mm; + in.physical.physical_height_mm = s->physical_height_mm; + in.physical.native_pixel_width = s->native_width; + in.physical.native_pixel_height = s->native_height; + in.desktop_width = s->desktop_width; + in.desktop_height = s->desktop_height; + in.iface = (const struct xrt_plugin_iface *)e->owning_iface; + in.inst = (struct xrt_plugin_instance *)e->owning_instance; + in.is_system_default = is_default; + in.system_info = (sys != NULL && sys->info_valid) ? &sys->info : NULL; + u_screen_info_resolve(&in, &s->info); + + // The system-default screen's placement is the one the runtime resolved + // for the panel (#1301) — what XrDisplayDesktopInfoDXR reports — when + // that resolution succeeded. + if (is_default && sys != NULL && sys->desktop_width > 0 && sys->desktop_height > 0) { + s->desktop_left = sys->desktop_left; + s->desktop_top = sys->desktop_top; + s->desktop_width = sys->desktop_width; + s->desktop_height = sys->desktop_height; + if (sys->native_width > 0 && sys->native_height > 0) { + s->native_width = sys->native_width; + s->native_height = sys->native_height; + } + if (sys->desktop_scale > 0.0f) { + s->desktop_scale = sys->desktop_scale; + } + if (sys->device_name[0] != '\0') { + (void)snprintf(s->device_name, sizeof(s->device_name), "%s", sys->device_name); + } + is_primary = sys->is_primary; + } + + finish_flags(s, is_primary, is_default); +} + +void +target_screens_build(const struct xrt_dp_factory_registry *reg, + const char *active_plugin_id, + const struct target_screens_system *sys, + struct xrt_screen_list *out) +{ + memset(out, 0, sizeof(*out)); + + if (reg == NULL || reg->entry_count == 0) { + // No monitor enumeration on this platform (or it failed): one + // synthesized screen, the system's, when the system knows it. + if (sys == NULL || !sys->info_valid) { + return; + } + struct xrt_screen *s = &out->screens[0]; + s->id = XRT_SCREEN_ID_SYNTHETIC_DEFAULT; + s->desktop_left = sys->desktop_left; + s->desktop_top = sys->desktop_top; + s->desktop_width = sys->desktop_width; + s->desktop_height = sys->desktop_height; + s->native_width = sys->native_width; + s->native_height = sys->native_height; + s->desktop_scale = sys->desktop_scale; + (void)snprintf(s->plugin_id, sizeof(s->plugin_id), "%s", + active_plugin_id != NULL ? active_plugin_id : ""); + (void)snprintf(s->device_name, sizeof(s->device_name), "%s", sys->device_name); + s->info = sys->info; + s->info.source = XRT_SCREEN_INFO_SOURCE_SYSTEM; + finish_flags(s, sys->is_primary, true); + out->count = 1; + return; + } + + struct os_display_desktop_info desk[OS_DISPLAY_DESKTOP_MAX_MONITORS]; + memset(desk, 0, sizeof(desk)); + const uint32_t desk_count = os_display_desktop_enumerate(desk, OS_DISPLAY_DESKTOP_MAX_MONITORS); + + const struct xrt_dp_registry_entry *def = xrt_dp_registry_primary_entry(reg, active_plugin_id); + + uint32_t n = 0; + if (def != NULL) { + build_from_entry(def, true, sys, desk, desk_count, &out->screens[n++]); + } + for (uint32_t i = 0; i < reg->entry_count && n < XRT_SCREEN_LIST_MAX; i++) { + const struct xrt_dp_registry_entry *e = ®->entries[i]; + if (e == def) { + continue; + } + build_from_entry(e, false, sys, desk, desk_count, &out->screens[n++]); + } + out->count = n; +} diff --git a/src/xrt/targets/common/target_screens.h b/src/xrt/targets/common/target_screens.h new file mode 100644 index 0000000000..310c36d20a --- /dev/null +++ b/src/xrt/targets/common/target_screens.h @@ -0,0 +1,74 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Build the per-screen list (`xrt_screen_list`) from the per-monitor + * DP registry (multi-screen M1). + * + * Shared by the native instance (`xrt_instance::enumerate_displays`, which the + * service also answers `system_enumerate_displays` with) and `displayxr-cli + * info`, so the CLI prints exactly what `xrEnumerateDisplaysDXR` reports. + * + * @ingroup targets_common + */ + +#pragma once + +#include "xrt/xrt_screen.h" +#include "xrt/xrt_compositor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/*! + * What the system says about its default screen — the inputs that become + * the SYSTEM_DEFAULT screen, verbatim. + */ +struct target_screens_system +{ + //! The active plug-in described its panel (@ref info is meaningful). + bool info_valid; + struct xrt_screen_info info; + + //! The resolved panel desktop rect (#1301); zero = unknown. + int32_t desktop_left; + int32_t desktop_top; + uint32_t desktop_width; + uint32_t desktop_height; + uint32_t native_width; + uint32_t native_height; + float desktop_scale; + bool is_primary; + char device_name[XRT_SCREEN_NAME_MAX]; +}; + +/*! + * Fill @p out from the system compositor info the runtime applied (the same + * fields `xrGetSystemProperties` reads). `info_valid` follows the display + * size being known. + */ +void +target_screens_system_from_info(const struct xrt_system_compositor_info *info, struct target_screens_system *out); + +/*! + * Build the screen list: one @ref xrt_screen per registry entry, the + * system-default one (`xrt_dp_registry_primary_entry(reg, active_plugin_id)`) + * first and flagged SYSTEM_DEFAULT, each screen's info resolved by + * `u_screen_info_resolve` (system info / the owning plug-in's + * `get_display_info_for_monitor` / EDID-derived). An empty registry yields + * one synthesized screen (@ref XRT_SCREEN_ID_SYNTHETIC_DEFAULT) from @p sys, + * or an empty list when @p sys knows nothing either. + * + * Reads the loader's monitor side table, so it must run after the registry + * was built in this process. + */ +void +target_screens_build(const struct xrt_dp_factory_registry *reg, + const char *active_plugin_id, + const struct target_screens_system *sys, + struct xrt_screen_list *out); + +#ifdef __cplusplus +} +#endif From 718366288e60c61365e59379ede4973433d2b490 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 10:58:11 -0700 Subject: [PATCH 03/10] feat(display_info): v22 - xrEnumerateDisplaysDXR, DISPLAY spaces, session display binding XR_DXR_display_info SPEC_VERSION 22 (append-only): XrDisplayDXR + xrEnumerateDisplaysDXR (two-call), XrDisplaySpaceCreateInfoDXR + xrCreateDisplaySpaceDXR, XrSessionDisplayBindingDXR on XrSessionCreateInfo. Type values 1004999214-216; 217 reserved for XrViewDisplayBindingsDXR (M3). oxr fills XrDisplayDXR from the instance's screen list (the SYSTEM_DEFAULT entry reports exactly what XrSystemProperties does, active-mode view scale included). A DISPLAY space resolves to the session's single display plane (head pose for runtime-window sessions, head tracking origin for external-window / bridge sessions). A session bound to a non-default display uses that display's physical size + nominal viewer for its display-scoped Kooima; the weaving DP is unchanged until segmentation (M2/M3). Co-Authored-By: Claude Fable 5.1 --- src/external/openxr_includes/openxr/README.md | 2 +- .../openxr/XR_DXR_display_info.h | 168 +++++++++++++++++- src/xrt/state_trackers/oxr/oxr_api_funcs.h | 10 ++ .../state_trackers/oxr/oxr_api_negotiate.c | 2 + src/xrt/state_trackers/oxr/oxr_api_space.c | 27 +++ src/xrt/state_trackers/oxr/oxr_api_system.c | 21 +++ src/xrt/state_trackers/oxr/oxr_objects.h | 49 +++++ src/xrt/state_trackers/oxr/oxr_session.c | 103 ++++++++++- src/xrt/state_trackers/oxr/oxr_space.c | 75 ++++++++ src/xrt/state_trackers/oxr/oxr_system.c | 154 ++++++++++++++++ 10 files changed, 600 insertions(+), 11 deletions(-) diff --git a/src/external/openxr_includes/openxr/README.md b/src/external/openxr_includes/openxr/README.md index a76375e72f..fef24dd590 100644 --- a/src/external/openxr_includes/openxr/README.md +++ b/src/external/openxr_includes/openxr/README.md @@ -40,7 +40,7 @@ Rules: | 1004999180 | `XR_DXR_macos_gl_binding` | relocated from 1004999010 (collided with display_info) — spec v2 | | 1004999190–199 | `XR_DXR_weave` | 190 = `XR_TYPE_WEAVE_SUBMIT_INFO_DXR`, 191 = `XR_TYPE_WEAVE_OUTPUT_DXR` (#625), 192 = `XR_TYPE_WEAVE_SUBMIT_RECTS_DXR` (batch, spec v3), 193 = `XR_TYPE_WEAVE_SUBMIT_OVERLAYS_DXR` (2D overlay atlas, spec v4, browser#18), 194 = `XR_TYPE_WEAVE_SUBMIT_LAYOUT_DXR` (N-view worst-case atlas, spec v6, #774), 195 = `XR_TYPE_WEAVE_BIND_WINDOW_INFO_DXR`, 196 = `XR_TYPE_WEAVE_WINDOW_GEOMETRY_DXR`, 197 = `XR_TYPE_WEAVE_SUBMIT_HANDLES_DXR` (Android / platform-neutral handles + geometry, spec v7, #1036), 198 = `XR_TYPE_WEAVE_SUBMIT_FLAT_REGIONS_DXR` (per-region hardware wish, spec v8, browser#88). **199 is RESERVED** for a spec v9 per-submit wish MASK (R8 texture instead of a rect list, for non-rectangular flat geometry) — do not assign it elsewhere | | 1004999200–209 | `XR_DXR_xlib_window_binding` | 200 = `XR_TYPE_XLIB_WINDOW_BINDING_CREATE_INFO_DXR` (#660 Phase 3) | -| 1004999210–219 | `XR_DXR_display_info` (v16+ additions) | 210 = `XR_TYPE_DISPLAY_DESKTOP_POSITION_DXR` (#715); fresh decade rather than reusing the 005/009 gaps in the original block. 211 = `XR_TYPE_DISPLAY_DESKTOP_INFO_DXR` (spec v18, #1301) — full panel desktop rect + GDI device name, superseding 210. 212 = `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (spec v19, #1486) — an `XrViewConfigurationType`, not an `XrStructureType`: the block is shared across every extended core enum. 213 = `XR_TYPE_VIEW_ACTIVITY_STATE_DXR` (spec v21, ADR-041) — per-frame `activeViewCount` chained on `XrViewState`, the other half of the fixed-view-count submission contract | +| 1004999210–219 | `XR_DXR_display_info` (v16+ additions) | 210 = `XR_TYPE_DISPLAY_DESKTOP_POSITION_DXR` (#715); fresh decade rather than reusing the 005/009 gaps in the original block. 211 = `XR_TYPE_DISPLAY_DESKTOP_INFO_DXR` (spec v18, #1301) — full panel desktop rect + GDI device name, superseding 210. 212 = `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (spec v19, #1486) — an `XrViewConfigurationType`, not an `XrStructureType`: the block is shared across every extended core enum. 213 = `XR_TYPE_VIEW_ACTIVITY_STATE_DXR` (spec v21, ADR-041) — per-frame `activeViewCount` chained on `XrViewState`, the other half of the fixed-view-count submission contract. 214 = `XR_TYPE_DISPLAY_DXR`, 215 = `XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR`, 216 = `XR_TYPE_SESSION_DISPLAY_BINDING_DXR` (spec v22, multi-screen M1 — per-display enumeration, DISPLAY spaces, session display binding). **217 is RESERVED** for `XrViewDisplayBindingsDXR` (per-view display binding on `XrViewState`, multi-screen M3); 218–219 free | | 1004999220–229 | `XR_DXR_android_surface_binding` | 220 = `XR_TYPE_ANDROID_WINDOW_GEOMETRY_DXR` (#1037), 221 = `XR_TYPE_EVENT_DATA_ANDROID_WINDOW_LAYOUT_HINT_DXR` (spec v2, #1396). The extension's `XR_TYPE_ANDROID_SURFACE_BINDING_CREATE_INFO_DXR` keeps **1004999005** — one of the two unused gaps in the `XR_DXR_display_info` decade — because that value was already published in the (then unimplemented) sketch in `docs/specs/extensions/XR_DXR_display_info.md` §4 | | 1004999230–239 | `XR_DXR_result_codes` | Not an extension — runtime-wide `XrResult` values returned from core entry points, where no extension can have been enabled. 230 = `XR_ERROR_RUNTIME_VERSION_SKEW_DXR` (browser#103), an **error**-class result, so it appears in the header negated as `-1004999230` | | 1004999240–249 | `XR_DXR_weave` (v9+ additions) | 240 = `XR_TYPE_WEAVE_IPC_CONNECTION_DXR` (`xrWeaveExportIpcConnectionDXR`, spec v9, browser#103); 241 = `XR_TYPE_WEAVE_DMABUF_DESC_DXR`, 242 = `XR_TYPE_WEAVE_OVERLAY_DMABUF_DESC_DXR`, 243 = `XR_TYPE_WEAVE_OUTPUT_DMABUF_DXR`, 244 = `XR_TYPE_WEAVE_SUBMIT_SYNC_DXR`, 245 = `XR_TYPE_WEAVE_OUTPUT_SYNC_DXR` (desktop-Linux dma-buf transport + sync_file fences, spec v10, #1699); 246 = `XR_TYPE_WEAVE_SNAP_GRID_INFO_DXR` (`xrWeaveSnapWindowGridDXR`, bulk grid snap, spec v11, #1723); 247 = `XR_TYPE_WEAVE_OUTPUT_ORIGIN_DXR`, 248 = `XR_TYPE_WEAVE_WINDOW_LOGICAL_ORIGIN_DXR` (woven origin per output for present-owner move sync, spec v12, browser-pvt#180); 249 = `XR_TYPE_WEAVE_SUBMIT_MONO_IN_2D_DXR` (one flat view instead of the weave while the panel is in hardware 2D, spec v13). The decade is now full. A fresh decade because the original 190–199 block is exhausted (199 is reserved for the wish mask) | diff --git a/src/external/openxr_includes/openxr/XR_DXR_display_info.h b/src/external/openxr_includes/openxr/XR_DXR_display_info.h index ec5ee5d8a5..d8231c936e 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_display_info.h +++ b/src/external/openxr_includes/openxr/XR_DXR_display_info.h @@ -32,7 +32,7 @@ extern "C" { #endif #define XR_DXR_display_info 1 -#define XR_DXR_display_info_SPEC_VERSION 21 +#define XR_DXR_display_info_SPEC_VERSION 22 #define XR_DXR_DISPLAY_INFO_EXTENSION_NAME "XR_DXR_display_info" // Reuse the type value from the deleted XR_EXT_dynamic_render_resolution @@ -662,6 +662,172 @@ typedef struct XrViewActivityStateDXR { uint32_t activeViewCount; //!< Views [0, activeViewCount) are live; the rest are inactive aliases } XrViewActivityStateDXR; +// ---- v22: Per-display enumeration, display spaces, session display binding (multi-screen M1) ---- + +// Type values 1004999214-1004999216 (display_info's 210-219 decade, see +// README.md). 1004999217 is RESERVED for XrViewDisplayBindingsDXR (per-view +// display binding chained on XrViewState, multi-screen M3); 218-219 are free. +#define XR_TYPE_DISPLAY_DXR ((XrStructureType)1004999214) +#define XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR ((XrStructureType)1004999215) +#define XR_TYPE_SESSION_DISPLAY_BINDING_DXR ((XrStructureType)1004999216) + +//! Size of XrDisplayDXR::vendorPluginId, in bytes, including the NUL. +#define XR_MAX_DISPLAY_VENDOR_PLUGIN_ID_SIZE_DXR 64 +//! Size of XrDisplayDXR::deviceName, in bytes, including the NUL. +#define XR_MAX_DISPLAY_NAME_SIZE_DXR 64 + +/*! + * @brief Per-display flags reported in XrDisplayDXR::flags (v22). + */ +typedef XrFlags64 XrDisplayFlagsDXR; +//! The OS desktop's primary monitor. +static const XrDisplayFlagsDXR XR_DISPLAY_PRIMARY_BIT_DXR = 0x00000001; +//! The display has live eye tracking (eyeTracking.supportedModes != 0). +static const XrDisplayFlagsDXR XR_DISPLAY_TRACKED_BIT_DXR = 0x00000002; +/*! + * The SYSTEM DEFAULT display: the one whose information XrDisplayInfoDXR, + * XrDisplayDesktopInfoDXR and XrEyeTrackingModeCapabilitiesDXR on + * XrSystemProperties report. Exactly one enumerated display carries it. + */ +static const XrDisplayFlagsDXR XR_DISPLAY_SYSTEM_DEFAULT_BIT_DXR = 0x00000004; + +/*! + * @brief One physical display (monitor) the runtime knows about, returned by + * xrEnumerateDisplaysDXR (v22). + * + * The list contains every connected monitor the runtime identified — the 3D + * panel(s) a vendor display processor claims AND ordinary monitors served by + * the vendor-neutral fallback — so an app can tell which screen is 3D + * (TRACKED, a vendor plug-in id) and place itself accordingly. + * + * @ref info is EMBEDDED BY VALUE (its `type`/`next` are written as + * XR_TYPE_DISPLAY_INFO_DXR / NULL by the runtime; the app need not initialise + * them), so the struct is one fixed-size record per display. For the + * SYSTEM_DEFAULT display, @ref info and @ref eyeTracking carry exactly what + * XrSystemProperties reports. + * + * Coordinate contract for @ref desktopRect: identical to + * XrDisplayDesktopInfoDXR::desktopRect (the space the OS places windows in; + * Linux/X11: root-window coordinates). An all-zero extent means unknown. + */ +typedef struct XrDisplayDXR { + XrStructureType type; //!< Must be XR_TYPE_DISPLAY_DXR + void* XR_MAY_ALIAS next; + /*! + * Opaque, non-zero identifier of this display, stable for the life of the + * instance (and, in practice, across processes on the same boot: it is + * derived from the monitor's EDID identity and connector). Pass it to + * XrDisplaySpaceCreateInfoDXR and XrSessionDisplayBindingDXR. + */ + uint64_t displayId; + //! Physical size, nominal viewer, recommended view scale and native pixels of THIS display. + XrDisplayInfoDXR info; + //! The monitor's current desktop rect (offset = signed top-left, extent = current mode). + XrRect2Di desktopRect; + //! The connector's device (native) mode; 0 = unknown. + uint32_t nativePixelWidth; + uint32_t nativePixelHeight; + //! The desktop compositor's scale for this output (e.g. 1.5); 0 = unknown. + float desktopScale; + //! Eye-tracking capabilities of THIS display (`type`/`next` written by the runtime). + XrEyeTrackingModeCapabilitiesDXR eyeTracking; + //! Bitmask of XR_DISPLAY_*_BIT_DXR. + XrDisplayFlagsDXR flags; + //! Discovery id of the vendor plug-in that drives this display (e.g. "leia-sr", "sim-display"), NUL-terminated. + char vendorPluginId[XR_MAX_DISPLAY_VENDOR_PLUGIN_ID_SIZE_DXR]; + //! OS output name (Linux/X11: RandR output, e.g. "HDMI-1"; Windows: GDI name), NUL-terminated; "" = unknown. + char deviceName[XR_MAX_DISPLAY_NAME_SIZE_DXR]; +} XrDisplayDXR; + +/*! + * @brief Enumerate the system's displays (v22). Standard two-call idiom. + * + * The list is snapshotted when the call is made; ids are stable for the + * instance. Order: the SYSTEM_DEFAULT display first, then the rest in the + * runtime's monitor-enumeration order. + * + * @param instance A valid XrInstance with XR_DXR_display_info enabled. + * @param systemId The XrSystemId from xrGetSystem. + * @param displayCapacityInput Capacity of @p displays, or 0 to query the count. + * @param displayCountOutput Receives the number of displays (or the required capacity). + * @param displays Array of XrDisplayDXR with `type` set to XR_TYPE_DISPLAY_DXR; may be NULL when capacity is 0. + * @return XR_SUCCESS, XR_ERROR_SIZE_INSUFFICIENT, XR_ERROR_SYSTEM_INVALID, XR_ERROR_VALIDATION_FAILURE. + */ +typedef XrResult (XRAPI_PTR *PFN_xrEnumerateDisplaysDXR)( + XrInstance instance, + XrSystemId systemId, + uint32_t displayCapacityInput, + uint32_t* displayCountOutput, + XrDisplayDXR* displays); + +/*! + * @brief Create info for a DISPLAY space (v22). + * + * A DISPLAY space is a reference-space-like XrSpace whose origin is the + * centre of @ref displayId's visible area: +X right, +Y up, +Z toward the + * viewer (the display space of docs/architecture/kooima-projection.md), with + * @ref poseInDisplaySpace applied on top exactly like + * XrReferenceSpaceCreateInfo::poseInReferenceSpace. + * + * Spec v22 limit: every DISPLAY space resolves to the session's single + * display plane — the same pose XrViewDisplayRawDXR::displayPlanePose + * reports — whichever display it names. Per-display poses (and therefore + * meaningful relationships BETWEEN two displays' spaces) arrive with + * per-screen DISPLAY poses in a later revision; there is no shared room frame. + */ +typedef struct XrDisplaySpaceCreateInfoDXR { + XrStructureType type; //!< Must be XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR + const void* XR_MAY_ALIAS next; + uint64_t displayId; //!< An id from xrEnumerateDisplaysDXR + XrPosef poseInDisplaySpace; +} XrDisplaySpaceCreateInfoDXR; + +/*! + * @brief Create a DISPLAY space (v22). + * @return XR_SUCCESS, XR_ERROR_VALIDATION_FAILURE (unknown displayId), + * XR_ERROR_POSE_INVALID, XR_ERROR_SESSION_LOST, ... + */ +typedef XrResult (XRAPI_PTR *PFN_xrCreateDisplaySpaceDXR)( + XrSession session, + const XrDisplaySpaceCreateInfoDXR* createInfo, + XrSpace* space); + +/*! + * @brief Pin a session to one display (v22). Chained on XrSessionCreateInfo. + * + * @ref displayId 0 means "runtime decides" (identical to not chaining the + * struct). Any other value must be an id from xrEnumerateDisplaysDXR, or + * xrCreateSession fails with XR_ERROR_VALIDATION_FAILURE. + * + * Spec v22 effect: the bound display's physical size and nominal viewer + * become the session's untracked-Kooima inputs (the display-scoped projection + * an app gets before window metrics are known). Which display processor + * weaves the session is unchanged — honouring the binding in the compositor + * arrives with multi-display segmentation; XrSystemProperties keeps reporting + * the SYSTEM_DEFAULT display. + * + * @extends XrSessionCreateInfo + */ +typedef struct XrSessionDisplayBindingDXR { + XrStructureType type; //!< Must be XR_TYPE_SESSION_DISPLAY_BINDING_DXR + const void* XR_MAY_ALIAS next; + uint64_t displayId; //!< 0 = runtime decides +} XrSessionDisplayBindingDXR; + +#ifndef XR_NO_PROTOTYPES +XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateDisplaysDXR( + XrInstance instance, + XrSystemId systemId, + uint32_t displayCapacityInput, + uint32_t* displayCountOutput, + XrDisplayDXR* displays); + +XRAPI_ATTR XrResult XRAPI_CALL xrCreateDisplaySpaceDXR( + XrSession session, + const XrDisplaySpaceCreateInfoDXR* createInfo, + XrSpace* space); +#endif + #ifdef __cplusplus } #endif diff --git a/src/xrt/state_trackers/oxr/oxr_api_funcs.h b/src/xrt/state_trackers/oxr/oxr_api_funcs.h index 051679369f..5b25a1f68e 100644 --- a/src/xrt/state_trackers/oxr/oxr_api_funcs.h +++ b/src/xrt/state_trackers/oxr/oxr_api_funcs.h @@ -790,6 +790,16 @@ oxr_xrEnumerateDisplayRenderingModesDXR(XrSession session, uint32_t modeCapacityInput, uint32_t *modeCountOutput, XrDisplayRenderingModeInfoDXR *modes); +//! OpenXR API function @ep{xrEnumerateDisplaysDXR} (spec v22, multi-screen M1) +XRAPI_ATTR XrResult XRAPI_CALL +oxr_xrEnumerateDisplaysDXR(XrInstance instance, + XrSystemId systemId, + uint32_t displayCapacityInput, + uint32_t *displayCountOutput, + XrDisplayDXR *displays); +//! OpenXR API function @ep{xrCreateDisplaySpaceDXR} (spec v22, multi-screen M1) +XRAPI_ATTR XrResult XRAPI_CALL +oxr_xrCreateDisplaySpaceDXR(XrSession session, const XrDisplaySpaceCreateInfoDXR *createInfo, XrSpace *space); //! OpenXR API function @ep{xrSetWorkspaceViewRigDXR} XRAPI_ATTR XrResult XRAPI_CALL oxr_xrSetWorkspaceViewRigDXR(XrSession session, const void *rig); diff --git a/src/xrt/state_trackers/oxr/oxr_api_negotiate.c b/src/xrt/state_trackers/oxr/oxr_api_negotiate.c index 9481242809..8731b28d20 100644 --- a/src/xrt/state_trackers/oxr/oxr_api_negotiate.c +++ b/src/xrt/state_trackers/oxr/oxr_api_negotiate.c @@ -437,6 +437,8 @@ handle_non_null(struct oxr_instance *inst, struct oxr_logger *log, const char *n ENTRY_IF_EXT(xrRequestEyeTrackingModeDXR, DXR_display_info); ENTRY_IF_EXT(xrRequestDisplayRenderingModeDXR, DXR_display_info); ENTRY_IF_EXT(xrEnumerateDisplayRenderingModesDXR, DXR_display_info); + ENTRY_IF_EXT(xrEnumerateDisplaysDXR, DXR_display_info); + ENTRY_IF_EXT(xrCreateDisplaySpaceDXR, DXR_display_info); #endif #ifdef OXR_HAVE_DXR_view_rig diff --git a/src/xrt/state_trackers/oxr/oxr_api_space.c b/src/xrt/state_trackers/oxr/oxr_api_space.c index c627c4072f..1b407339a8 100644 --- a/src/xrt/state_trackers/oxr/oxr_api_space.c +++ b/src/xrt/state_trackers/oxr/oxr_api_space.c @@ -231,6 +231,33 @@ oxr_xrCreateReferenceSpace(XrSession session, const XrReferenceSpaceCreateInfo * return oxr_session_success_result(sess); } +#ifdef OXR_HAVE_DXR_display_info +XRAPI_ATTR XrResult XRAPI_CALL +oxr_xrCreateDisplaySpaceDXR(XrSession session, const XrDisplaySpaceCreateInfoDXR *createInfo, XrSpace *out_space) +{ + OXR_TRACE_MARKER(); + + struct oxr_session *sess; + struct oxr_space *spc = NULL; + struct oxr_logger log; + OXR_VERIFY_SESSION_AND_INIT_LOG(&log, session, sess, "xrCreateDisplaySpaceDXR"); + OXR_VERIFY_SESSION_NOT_LOST(&log, sess); + OXR_VERIFY_EXTENSION(&log, sess->sys->inst, DXR_display_info); + OXR_VERIFY_ARG_TYPE_AND_NOT_NULL(&log, createInfo, XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR); + OXR_VERIFY_ARG_NOT_NULL(&log, out_space); + OXR_VERIFY_POSE(&log, createInfo->poseInDisplaySpace); + + XrResult ret = oxr_space_display_create(&log, sess, createInfo, &spc); + if (ret != XR_SUCCESS) { + return ret; + } + + *out_space = oxr_space_to_openxr(spc); + + return oxr_session_success_result(sess); +} +#endif // OXR_HAVE_DXR_display_info + XRAPI_ATTR XrResult XRAPI_CALL oxr_xrLocateSpace(XrSpace space, XrSpace baseSpace, XrTime time, XrSpaceLocation *location) { diff --git a/src/xrt/state_trackers/oxr/oxr_api_system.c b/src/xrt/state_trackers/oxr/oxr_api_system.c index 865de2c06e..14522baa62 100644 --- a/src/xrt/state_trackers/oxr/oxr_api_system.c +++ b/src/xrt/state_trackers/oxr/oxr_api_system.c @@ -104,6 +104,27 @@ oxr_xrGetSystemProperties(XrInstance instance, XrSystemId systemId, XrSystemProp return ret; } +#ifdef OXR_HAVE_DXR_display_info +XRAPI_ATTR XrResult XRAPI_CALL +oxr_xrEnumerateDisplaysDXR(XrInstance instance, + XrSystemId systemId, + uint32_t displayCapacityInput, + uint32_t *displayCountOutput, + XrDisplayDXR *displays) +{ + OXR_TRACE_MARKER(); + + struct oxr_instance *inst; + struct oxr_logger log; + OXR_VERIFY_INSTANCE_AND_INIT_LOG(&log, instance, inst, "xrEnumerateDisplaysDXR"); + OXR_VERIFY_EXTENSION(&log, inst, DXR_display_info); + OXR_VERIFY_SYSTEM_AND_GET(&log, inst, systemId, sys); + OXR_VERIFY_ARG_NOT_NULL(&log, displayCountOutput); + + return oxr_system_enumerate_displays(&log, sys, displayCapacityInput, displayCountOutput, displays); +} +#endif // OXR_HAVE_DXR_display_info + XRAPI_ATTR XrResult XRAPI_CALL oxr_xrEnumerateViewConfigurations(XrInstance instance, XrSystemId systemId, diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h index e8e6a99159..496ed90277 100644 --- a/src/xrt/state_trackers/oxr/oxr_objects.h +++ b/src/xrt/state_trackers/oxr/oxr_objects.h @@ -18,6 +18,7 @@ #include "xrt/xrt_tracking.h" #include "xrt/xrt_compositor.h" #include "xrt/xrt_display_metrics.h" +#include "xrt/xrt_screen.h" #include "xrt/xrt_vulkan_includes.h" #include "xrt/xrt_openxr_includes.h" #include "xrt/xrt_config_os.h" @@ -1211,6 +1212,20 @@ oxr_space_xdev_pose_create(struct oxr_logger *log, const struct xrt_pose *pose, struct oxr_space **out_space); +#ifdef OXR_HAVE_DXR_display_info +/*! + * XR_DXR_display_info v22: a DISPLAY space for one enumerated display. In v22 + * every display space resolves to the session's single display plane (the + * pose XrViewDisplayRawDXR::displayPlanePose reports); per-display poses + * arrive with per-screen DISPLAY poses (multi-screen M3). + */ +XrResult +oxr_space_display_create(struct oxr_logger *log, + struct oxr_session *sess, + const XrDisplaySpaceCreateInfoDXR *createInfo, + struct oxr_space **out_space); +#endif + XrResult oxr_space_locate( struct oxr_logger *log, struct oxr_space *spc, struct oxr_space *baseSpc, XrTime time, XrSpaceLocation *location); @@ -1343,6 +1358,25 @@ oxr_system_get_by_id(struct oxr_logger *log, XrResult oxr_system_get_properties(struct oxr_logger *log, struct oxr_system *sys, XrSystemProperties *properties); +/*! + * Multi-screen M1: the system's screens (`xrt_instance::enumerate_displays`, + * in-process or over IPC). Never empty while the system has display info: an + * instance that cannot enumerate yields one screen synthesized from + * `xsysc->info`, flagged SYSTEM_DEFAULT. + */ +void +oxr_system_get_screens(struct oxr_system *sys, struct xrt_screen_list *out_list); + +#ifdef OXR_HAVE_DXR_display_info +//! xrEnumerateDisplaysDXR (XR_DXR_display_info v22). +XrResult +oxr_system_enumerate_displays(struct oxr_logger *log, + struct oxr_system *sys, + uint32_t displayCapacityInput, + uint32_t *displayCountOutput, + XrDisplayDXR *displays); +#endif + /*! * #1486: is @p type one of the primary view configurations this system * advertises? Writes the view count that type reports to @p out_view_count @@ -2793,6 +2827,21 @@ struct oxr_session //! True if session was created with an external window handle (XR_DXR_win32_window_binding). bool has_external_window; + /*! + * XrSessionDisplayBindingDXR (XR_DXR_display_info v22, multi-screen M1): + * the display the app pinned the session to; 0 = runtime decides. + */ + uint64_t bound_display_id; + + /*! + * True when the session is bound to a display OTHER than the system + * default and that display's size is known: @ref bound_display_info then + * backs the session's display-scoped Kooima inputs (physical size, + * nominal viewer). Which DP weaves is unchanged in M1. + */ + bool bound_display_overrides; + struct xrt_screen_info bound_display_info; + #ifdef XRT_OS_ANDROID //! The last ANativeWindow this session published into `android_globals` //! from XR_DXR_android_surface_binding (#1037). A MARKER, not an owned diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c index 692569dcaa..f33ded58b4 100644 --- a/src/xrt/state_trackers/oxr/oxr_session.c +++ b/src/xrt/state_trackers/oxr/oxr_session.c @@ -381,6 +381,15 @@ oxr_session_get_display_dimensions(struct oxr_session *sess, float *out_width_m, return false; } + // XrSessionDisplayBindingDXR (multi-screen M1): a session pinned to a + // non-default display uses THAT display's physical size, ahead of the + // compositor's (whose DP describes the system default panel). + if (sess->bound_display_overrides) { + *out_width_m = sess->bound_display_info.width_m; + *out_height_m = sess->bound_display_info.height_m; + return true; + } + #ifdef XRT_HAVE_D3D11_NATIVE_COMPOSITOR // D3D11 native compositor path (has its own display dimension query) if (sess->xcn != NULL && sess->is_d3d11_native_compositor) { @@ -432,6 +441,32 @@ oxr_session_get_display_dimensions(struct oxr_session *sess, float *out_width_m, return true; } +/*! + * The nominal viewer's height and distance for the session's untracked Kooima + * (display space): the system panel's, or — for a session pinned to another + * display with XrSessionDisplayBindingDXR (multi-screen M1) — that display's. + * Either out-param may be NULL. + */ +static void +oxr_session_get_nominal_viewer(struct oxr_session *sess, float *out_y_m, float *out_z_m) +{ + float y = 0.0f; + float z = 0.0f; + if (sess->bound_display_overrides) { + y = sess->bound_display_info.nominal_viewer_y_m; + z = sess->bound_display_info.nominal_viewer_z_m; + } else if (sess->sys->xsysc != NULL) { + y = sess->sys->xsysc->info.nominal_viewer_y_m; + z = sess->sys->xsysc->info.nominal_viewer_z_m; + } + if (out_y_m != NULL) { + *out_y_m = y; + } + if (out_z_m != NULL) { + *out_z_m = z; + } +} + /*! * Get window metrics for adaptive FOV and eye position adjustment. * @@ -2861,11 +2896,12 @@ oxr_session_locate_views(struct oxr_logger *log, } have_eye_positions = true; } else { - // Nominal viewer from system compositor info (sim_display, etc.) - const struct xrt_system_compositor_info *sinfo = &sess->sys->xsysc->info; - U_LOG_I("Kooima: nominal_y=%.3f nominal_z=%.3f", - sinfo->nominal_viewer_y_m, sinfo->nominal_viewer_z_m); - if (sinfo->nominal_viewer_z_m > 0.0f) { + // Nominal viewer from system compositor info (sim_display, etc.), + // or from the bound display (XrSessionDisplayBindingDXR, M1). + float nominal_z_m = 0.0f; + oxr_session_get_nominal_viewer(sess, NULL, &nominal_z_m); + U_LOG_I("Kooima: nominal_z=%.3f", nominal_z_m); + if (nominal_z_m > 0.0f) { float ipd_m = sess->ipd_meters; eye_count = 2; // Untracked → the eye sits at the REFERENCE: directly in front @@ -2877,8 +2913,8 @@ oxr_session_locate_views(struct oxr_logger *log, // reproduce, so the camera/display rigs would disagree. // nominal_y belongs only to the parallax / zero-parallax pivot // (the `nominal` vector below), never to the eye position. - adj_eyes[0] = (struct xrt_eye_position){-ipd_m / 2.0f, 0.0f, sinfo->nominal_viewer_z_m}; - adj_eyes[1] = (struct xrt_eye_position){ipd_m / 2.0f, 0.0f, sinfo->nominal_viewer_z_m}; + adj_eyes[0] = (struct xrt_eye_position){-ipd_m / 2.0f, 0.0f, nominal_z_m}; + adj_eyes[1] = (struct xrt_eye_position){ipd_m / 2.0f, 0.0f, nominal_z_m}; have_eye_positions = true; if (should_log) { U_LOG_I("Nominal eyes: [0]=(%.4f,%.4f,%.4f) [1]=(%.4f,%.4f,%.4f), IPD=%.1fmm", @@ -3079,8 +3115,8 @@ oxr_session_locate_views(struct oxr_logger *log, if (screen_width_m > 0.0f && screen_height_m > 0.0f) { // Nominal viewer for view math (parallax lerp target) - const struct xrt_system_compositor_info *si = &sess->sys->xsysc->info; - struct xrt_vec3 nominal = {0, si->nominal_viewer_y_m, si->nominal_viewer_z_m}; + struct xrt_vec3 nominal = {0, 0, 0}; + oxr_session_get_nominal_viewer(sess, &nominal.y, &nominal.z); struct xrt_vec3 raw_eyes[XRT_MAX_VIEWS]; // The DP owns multi-view eye fill: it must report one eye @@ -5446,6 +5482,29 @@ oxr_session_create(struct oxr_logger *log, } #endif +#ifdef OXR_HAVE_DXR_display_info + // XrSessionDisplayBindingDXR (spec v22, multi-screen M1): validate before + // anything is created so a bad id fails cleanly. 0 = runtime decides. + uint64_t bound_display_id = 0; + struct xrt_screen bound_display; + memset(&bound_display, 0, sizeof(bound_display)); + const XrSessionDisplayBindingDXR *display_binding = + OXR_GET_INPUT_FROM_CHAIN(createInfo, XR_TYPE_SESSION_DISPLAY_BINDING_DXR, XrSessionDisplayBindingDXR); + if (display_binding != NULL && sys->inst->extensions.DXR_display_info && display_binding->displayId != 0) { + struct xrt_screen_list screens; + oxr_system_get_screens(sys, &screens); + const struct xrt_screen *found = xrt_screen_list_find(&screens, display_binding->displayId); + if (found == NULL) { + return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, + "(XrSessionDisplayBindingDXR::displayId == 0x%016llx) is not a display " + "xrEnumerateDisplaysDXR reports", + (unsigned long long)display_binding->displayId); + } + bound_display_id = display_binding->displayId; + bound_display = *found; + } +#endif + /* Try allocating and populating. */ XrResult ret = oxr_session_create_impl(log, sys, createInfo, &xsi, &sess); if (ret != XR_SUCCESS) { @@ -5539,6 +5598,32 @@ oxr_session_create(struct oxr_logger *log, #endif sess->is_bridge_relay = xsi.is_bridge_relay; +#ifdef OXR_HAVE_DXR_display_info + if (bound_display_id != 0) { + sess->bound_display_id = bound_display_id; + const bool is_default = (bound_display.flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0; + // Bound to the system default = today's behaviour exactly. Bound + // elsewhere: that display's size and nominal viewer drive the + // session's display-scoped Kooima (M1); the DP that weaves is + // unchanged until multi-display segmentation (M2/M3). + sess->bound_display_overrides = + !is_default && bound_display.info.width_m > 0.0f && bound_display.info.height_m > 0.0f; + sess->bound_display_info = bound_display.info; + U_LOG_W( + "xrCreateSession: XrSessionDisplayBindingDXR -> display 0x%016llx ('%s', plug-in '%s', %.4f x %.4f " + "m, " + "nominal z %.3f m)%s", + (unsigned long long)bound_display_id, bound_display.device_name, bound_display.plugin_id, + bound_display.info.width_m, bound_display.info.height_m, bound_display.info.nominal_viewer_z_m, + is_default ? " — the system default display, nothing changes" + : sess->bound_display_overrides + ? " — its size + nominal viewer back this session's display-scoped Kooima; weaving still " + "follows the system default display (multi-screen M1)" + : " — its physical size is unknown, so the session keeps the system default display's " + "Kooima inputs"); + } +#endif + #ifdef OXR_HAVE_DXR_depth_budget // XR_DXR_depth_budget: the create-info chain is gone by locate time, and // the rear-depth DEFAULT is a function of transparency, so remember it. diff --git a/src/xrt/state_trackers/oxr/oxr_space.c b/src/xrt/state_trackers/oxr/oxr_space.c index 1f66c5d8fe..edebd64178 100644 --- a/src/xrt/state_trackers/oxr/oxr_space.c +++ b/src/xrt/state_trackers/oxr/oxr_space.c @@ -335,6 +335,81 @@ oxr_space_xdev_pose_create(struct oxr_logger *log, return XR_SUCCESS; } +#ifdef OXR_HAVE_DXR_display_info +XrResult +oxr_space_display_create(struct oxr_logger *log, + struct oxr_session *sess, + const XrDisplaySpaceCreateInfoDXR *createInfo, + struct oxr_space **out_space) +{ + struct xrt_screen_list screens; + oxr_system_get_screens(sess->sys, &screens); + if (createInfo->displayId == 0 || xrt_screen_list_find(&screens, createInfo->displayId) == NULL) { + return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, + "(createInfo->displayId == 0x%016llx) is not a display xrEnumerateDisplaysDXR reports", + (unsigned long long)createInfo->displayId); + } + + struct xrt_pose pose; + memcpy(&pose, &createInfo->poseInDisplaySpace, sizeof(pose)); + if (!math_pose_validate(&pose)) { + return oxr_error(log, XR_ERROR_POSE_INVALID, "(createInfo->poseInDisplaySpace)"); + } + + struct xrt_device *head = GET_XDEV_BY_ROLE(sess->sys, head); + if (head == NULL) { + return oxr_error(log, XR_ERROR_RUNTIME_FAILURE, "No head device for the display plane"); + } + + /* + * Spec v22: every DISPLAY space is the session's single display plane — + * where oxr_session_locate_views puts it (and XrViewDisplayRawDXR:: + * displayPlanePose reports it): + * + * - runtime-window sessions: the head device's pose (on a 3D display + * the head device IS the display; the qwerty rig moves it). That is a + * pose space on the head's GENERIC_HEAD_POSE. + * - external-window and bridge-relay sessions: the identity of the + * head's tracking-origin space (the app owns its camera). That is an + * offset space from the root by the tracking-origin offset. + * + * Either way the overseer locates it, in-process and over IPC. An app + * driving XR_DXR_view_rig owns the plane pose itself and does not need + * this space. Per-display poses arrive in multi-screen M3. + */ + struct xrt_space *xspace = NULL; + xrt_result_t xret; + if (!sess->has_external_window && !sess->is_bridge_relay) { + xret = xrt_space_overseer_create_pose_space(sess->sys->xso, head, XRT_INPUT_GENERIC_HEAD_POSE, &xspace); + OXR_CHECK_XRET(log, sess, xret, xrt_space_overseer_create_pose_space); + } else { + struct xrt_pose origin_offset = XRT_POSE_IDENTITY; + if (head->tracking_origin != NULL) { + xret = xrt_space_overseer_get_tracking_origin_offset(sess->sys->xso, head->tracking_origin, + &origin_offset); + if (xret != XRT_SUCCESS) { + origin_offset = (struct xrt_pose)XRT_POSE_IDENTITY; + } + } + xret = xrt_space_overseer_create_offset_space(sess->sys->xso, sess->sys->xso->semantic.root, + &origin_offset, &xspace); + OXR_CHECK_XRET(log, sess, xret, xrt_space_overseer_create_offset_space); + } + + struct oxr_space *spc = NULL; + OXR_ALLOCATE_HANDLE_OR_RETURN(log, spc, OXR_XR_DEBUG_SPACE, oxr_space_destroy, &sess->handle); + spc->sess = sess; + spc->pose = pose; + spc->space_type = OXR_SPACE_TYPE_XDEV_POSE; + xrt_space_reference(&spc->xdev_pose.xs, xspace); + xrt_space_reference(&xspace, NULL); + + *out_space = spc; + + return XR_SUCCESS; +} +#endif // OXR_HAVE_DXR_display_info + /* * diff --git a/src/xrt/state_trackers/oxr/oxr_system.c b/src/xrt/state_trackers/oxr/oxr_system.c index b6b2a4b526..99658bf51b 100644 --- a/src/xrt/state_trackers/oxr/oxr_system.c +++ b/src/xrt/state_trackers/oxr/oxr_system.c @@ -15,6 +15,8 @@ #include #include "xrt/xrt_device.h" +#include "xrt/xrt_instance.h" +#include "xrt/xrt_screen.h" #include "util/u_debug.h" #include "util/u_verify.h" @@ -1006,6 +1008,158 @@ oxr_system_get_properties(struct oxr_logger *log, struct oxr_system *sys, XrSyst return XR_SUCCESS; } +/* + * + * Multi-screen M1: per-display enumeration (XR_DXR_display_info v22). + * + */ + +void +oxr_system_get_screens(struct oxr_system *sys, struct xrt_screen_list *out_list) +{ + memset(out_list, 0, sizeof(*out_list)); + + struct xrt_instance *xinst = (sys != NULL && sys->inst != NULL) ? sys->inst->xinst : NULL; + if (xrt_instance_enumerate_displays(xinst, out_list) != XRT_SUCCESS) { + memset(out_list, 0, sizeof(*out_list)); + } + if (out_list->count > 0 || sys == NULL || sys->xsysc == NULL) { + return; + } + + // The instance could not enumerate (or knows no monitor): the system's + // own display is still a display. Synthesize it from the system info so + // the list always carries exactly one SYSTEM_DEFAULT entry. + const struct xrt_system_compositor_info *info = &sys->xsysc->info; + if (!(info->display_width_m > 0.0f) || !(info->display_height_m > 0.0f)) { + return; + } + struct xrt_screen *scr = &out_list->screens[0]; + scr->id = XRT_SCREEN_ID_SYNTHETIC_DEFAULT; + scr->flags = XRT_SCREEN_FLAG_SYSTEM_DEFAULT; + if (info->display_is_primary) { + scr->flags |= XRT_SCREEN_FLAG_PRIMARY; + } + if (info->supported_eye_tracking_modes != 0) { + scr->flags |= XRT_SCREEN_FLAG_TRACKED; + } + scr->desktop_left = info->display_screen_left; + scr->desktop_top = info->display_screen_top; + scr->desktop_width = info->display_desktop_width; + scr->desktop_height = info->display_desktop_height; + scr->native_width = info->display_desktop_native_width; + scr->native_height = info->display_desktop_native_height; + scr->desktop_scale = info->display_desktop_scale; + (void)snprintf(scr->plugin_id, sizeof(scr->plugin_id), "%s", info->active_plugin_id); + (void)snprintf(scr->device_name, sizeof(scr->device_name), "%.*s", (int)(sizeof(scr->device_name) - 1), + info->display_device_name); + scr->info.width_m = info->display_width_m; + scr->info.height_m = info->display_height_m; + scr->info.nominal_viewer_x_m = info->nominal_viewer_x_m; + scr->info.nominal_viewer_y_m = info->nominal_viewer_y_m; + scr->info.nominal_viewer_z_m = info->nominal_viewer_z_m; + scr->info.recommended_view_scale_x = info->recommended_view_scale_x; + scr->info.recommended_view_scale_y = info->recommended_view_scale_y; + scr->info.pixel_width = info->display_pixel_width; + scr->info.pixel_height = info->display_pixel_height; + scr->info.supported_eye_tracking_modes = info->supported_eye_tracking_modes; + scr->info.default_eye_tracking_mode = info->default_eye_tracking_mode; + scr->info.source = XRT_SCREEN_INFO_SOURCE_SYSTEM; + out_list->count = 1; +} + +#ifdef OXR_HAVE_DXR_display_info +static void +fill_display_dxr(struct oxr_system *sys, const struct xrt_screen *scr, XrDisplayDXR *d) +{ + const struct xrt_screen_info *si = &scr->info; + + d->type = XR_TYPE_DISPLAY_DXR; + // d->next is the app's; nothing is chained on XrDisplayDXR in v22. + d->displayId = scr->id; + + d->info.type = XR_TYPE_DISPLAY_INFO_DXR; + d->info.next = NULL; + d->info.displaySizeMeters.width = si->width_m; + d->info.displaySizeMeters.height = si->height_m; + d->info.nominalViewerPositionInDisplaySpace.x = si->nominal_viewer_x_m; + d->info.nominalViewerPositionInDisplaySpace.y = si->nominal_viewer_y_m; + d->info.nominalViewerPositionInDisplaySpace.z = si->nominal_viewer_z_m; + float scale_x = si->recommended_view_scale_x; + float scale_y = si->recommended_view_scale_y; + if ((scr->flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0) { + // Exactly what XrDisplayInfoDXR on XrSystemProperties reports: the + // ACTIVE mode's per-view scale at query time (see + // oxr_system_get_properties). + xrt_device_get_active_mode_view_scale(GET_XDEV_BY_ROLE(sys, head), &scale_x, &scale_y); + } + d->info.recommendedViewScaleX = scale_x; + d->info.recommendedViewScaleY = scale_y; + d->info.displayPixelWidth = si->pixel_width; + d->info.displayPixelHeight = si->pixel_height; + + d->desktopRect.offset.x = scr->desktop_left; + d->desktopRect.offset.y = scr->desktop_top; + d->desktopRect.extent.width = (int32_t)scr->desktop_width; + d->desktopRect.extent.height = (int32_t)scr->desktop_height; + d->nativePixelWidth = scr->native_width; + d->nativePixelHeight = scr->native_height; + d->desktopScale = scr->desktop_scale; + + d->eyeTracking.type = XR_TYPE_EYE_TRACKING_MODE_CAPABILITIES_DXR; + d->eyeTracking.next = NULL; + d->eyeTracking.supportedModes = (XrEyeTrackingModeCapabilityFlagsDXR)si->supported_eye_tracking_modes; + d->eyeTracking.defaultMode = (XrEyeTrackingModeDXR)si->default_eye_tracking_mode; + + d->flags = 0; + if ((scr->flags & XRT_SCREEN_FLAG_PRIMARY) != 0) { + d->flags |= XR_DISPLAY_PRIMARY_BIT_DXR; + } + if ((scr->flags & XRT_SCREEN_FLAG_TRACKED) != 0) { + d->flags |= XR_DISPLAY_TRACKED_BIT_DXR; + } + if ((scr->flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0) { + d->flags |= XR_DISPLAY_SYSTEM_DEFAULT_BIT_DXR; + } + + (void)snprintf(d->vendorPluginId, sizeof(d->vendorPluginId), "%s", scr->plugin_id); + (void)snprintf(d->deviceName, sizeof(d->deviceName), "%s", scr->device_name); +} + +XrResult +oxr_system_enumerate_displays(struct oxr_logger *log, + struct oxr_system *sys, + uint32_t displayCapacityInput, + uint32_t *displayCountOutput, + XrDisplayDXR *displays) +{ + struct xrt_screen_list list; + oxr_system_get_screens(sys, &list); + + *displayCountOutput = list.count; + if (displayCapacityInput == 0) { + return XR_SUCCESS; + } + if (displays == NULL) { + return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, "(displays == NULL) with displayCapacityInput > 0"); + } + if (displayCapacityInput < list.count) { + return oxr_error(log, XR_ERROR_SIZE_INSUFFICIENT, "(displayCapacityInput == %u) < %u", + displayCapacityInput, list.count); + } + for (uint32_t i = 0; i < list.count; i++) { + if (displays[i].type != XR_TYPE_DISPLAY_DXR) { + return oxr_error(log, XR_ERROR_VALIDATION_FAILURE, "(displays[%u].type != XR_TYPE_DISPLAY_DXR)", + i); + } + } + for (uint32_t i = 0; i < list.count; i++) { + fill_display_dxr(sys, &list.screens[i], &displays[i]); + } + return XR_SUCCESS; +} +#endif // OXR_HAVE_DXR_display_info + bool oxr_system_lookup_view_config(const struct oxr_system *sys, XrViewConfigurationType type, uint32_t *out_view_count) { From c76b073486d630099803aaa96f4b32a4668054d8 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 10:58:11 -0700 Subject: [PATCH 04/10] feat(cli,test_apps): print the per-screen list (multi-screen M1) displayxr-cli info gains a ':: Displays' block (and a 'displays' JSON array) built by the same target_screens_build the runtime uses, so M1 is verifiable headless. cube_handle_vk_linux logs xrEnumerateDisplaysDXR at startup, one line per display. Co-Authored-By: Claude Fable 5.1 --- src/xrt/targets/cli/cli_query.c | 118 ++++++++++++++++++++++++ src/xrt/targets/cli/cli_query.h | 9 ++ test_apps/cube_handle_vk_linux/main.cpp | 36 ++++++++ 3 files changed, 163 insertions(+) diff --git a/src/xrt/targets/cli/cli_query.c b/src/xrt/targets/cli/cli_query.c index 503c8eb627..551d2fbd22 100644 --- a/src/xrt/targets/cli/cli_query.c +++ b/src/xrt/targets/cli/cli_query.c @@ -30,11 +30,13 @@ #include "os/os_time.h" // #1380 rig-role sampling window #include "util/u_git_tag.h" #include "util/u_setting.h" // #1252 settings chain (env > per-user > machine) +#include "util/u_screen_info.h" // multi-screen M1 per-screen info #ifdef XRT_OS_WINDOWS #include "util/u_windows.h" // #1201 DPI awareness reporting #endif #include "target_plugin_loader.h" +#include "target_screens.h" #include "target_input_plugin_loader.h" #include "target_builder_input_provider.h" #include "target_input_arbiter.h" @@ -782,6 +784,10 @@ dp_confidence_label(uint32_t c) * the registry is empty and the service path falls back to the scalar — reported * as agreement, never a false mismatch. */ +//! The registry @ref probe_dp_selection resolved — reused for the per-screen +//! list so the plug-ins' probe_displays run once per CLI invocation. +static struct xrt_dp_factory_registry g_cli_dp_registry; + static void probe_dp_selection(struct cli_query_result *r, const struct xrt_plugin_iface *active) { @@ -799,6 +805,7 @@ probe_dp_selection(struct cli_query_result *r, const struct xrt_plugin_iface *ac struct xrt_dp_factory_registry reg = {0}; target_plugin_resolve_displays(descs, dn, ®); r->dp_sel_claim_count = reg.entry_count; + g_cli_dp_registry = reg; if (reg.entry_count > 0) { // The compositor passes COMP_DP_PRIMARY_MONITOR, which selects via @@ -1669,6 +1676,45 @@ cli_query_fill(struct cli_query_result *r, struct cli_query_handles *h, const st r->desktop_info.width_in_caller_dpi == info.display_pixel_width && r->desktop_info.height_in_caller_dpi == info.display_pixel_height; + // Multi-screen M1 — the per-screen list xrEnumerateDisplaysDXR reports. The + // system-default screen gets the active plug-in's panel info, with the + // view-scale baseline derived from the mode table exactly as + // apply_plugin_display_info does when the plug-in leaves it 0. + { + struct target_screens_system sys; + memset(&sys, 0, sizeof(sys)); + u_screen_info_from_plugin(&info, XRT_SCREEN_INFO_SOURCE_SYSTEM, &sys.info); + if (!(sys.info.recommended_view_scale_x > 0.0f) || !(sys.info.recommended_view_scale_y > 0.0f)) { + float min_x = 1.0f, min_y = 1.0f; + for (uint32_t mi = 0; mi < head->rendering_mode_count; mi++) { + const struct xrt_rendering_mode *rm = &head->rendering_modes[mi]; + if (rm->view_scale_x > 0.0f && rm->view_scale_x < min_x) { + min_x = rm->view_scale_x; + } + if (rm->view_scale_y > 0.0f && rm->view_scale_y < min_y) { + min_y = rm->view_scale_y; + } + } + sys.info.recommended_view_scale_x = min_x; + sys.info.recommended_view_scale_y = min_y; + } + sys.info_valid = info.display_width_m > 0.0f && info.display_height_m > 0.0f; + if (r->desktop_info_ok) { + sys.desktop_left = r->desktop_info.left; + sys.desktop_top = r->desktop_info.top; + sys.desktop_width = r->desktop_info.width; + sys.desktop_height = r->desktop_info.height; + sys.native_width = r->desktop_info.native_width; + sys.native_height = r->desktop_info.native_height; + sys.desktop_scale = (float)r->desktop_info.scale; + sys.is_primary = r->desktop_info.is_primary; + snprintf(sys.device_name, sizeof(sys.device_name), "%.*s", (int)(sizeof(sys.device_name) - 1), + r->desktop_info.device_name); + } + target_screens_build(&g_cli_dp_registry, iface->id, &sys, &r->screens); + r->screens_probed = true; + } + // #1252 — the allow-listed performance settings, resolved through the same // chain the runtime uses. Platform-independent, and deliberately BEFORE the // GPU probe so it is reported even where that probe does not run. @@ -2118,6 +2164,39 @@ cli_query_print_info_text(const struct cli_query_result *r) } } + P(" :: Displays (XR_DXR_display_info v22 — what xrEnumerateDisplaysDXR reports)\n"); + if (!r->screens_probed) { + PT("not evaluated\n"); + } else if (r->screens.count == 0) { + PT("none (no monitor enumerated and no display info)\n"); + } else { + for (uint32_t i = 0; i < r->screens.count; i++) { + const struct xrt_screen *sc = &r->screens.screens[i]; + const struct xrt_screen_info *si = &sc->info; + char et[64]; + PT("[%u] 0x%016llx '%s' plug-in='%s'%s%s%s\n", i, (unsigned long long)sc->id, + or_q(sc->device_name), or_q(sc->plugin_id), + (sc->flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) ? " SYSTEM_DEFAULT" : "", + (sc->flags & XRT_SCREEN_FLAG_PRIMARY) ? " PRIMARY" : "", + (sc->flags & XRT_SCREEN_FLAG_TRACKED) ? " TRACKED" : ""); + PT(" desktop %ux%u @ (%d,%d) native %ux%u scale %.2f edid %ux%u mm\n", sc->desktop_width, + sc->desktop_height, (int)sc->desktop_left, (int)sc->desktop_top, sc->native_width, + sc->native_height, (double)sc->desktop_scale, sc->physical_width_mm, sc->physical_height_mm); + PT(" info[%s]: %.4f x %.4f m %ux%u px nominal=(%.3f, %.3f, %.3f) m view scale %.3f x " + "%.3f " + "eye tracking %s%s%s%s\n", + u_screen_info_source_str(si->source), (double)si->width_m, (double)si->height_m, + si->pixel_width, si->pixel_height, (double)si->nominal_viewer_x_m, + (double)si->nominal_viewer_y_m, (double)si->nominal_viewer_z_m, + (double)si->recommended_view_scale_x, (double)si->recommended_view_scale_y, + eye_modes_label(si->supported_eye_tracking_modes, et, sizeof(et)), + si->supported_eye_tracking_modes != 0 ? " (default " : "", + si->supported_eye_tracking_modes != 0 ? eye_default_label(si->default_eye_tracking_mode) + : "", + si->supported_eye_tracking_modes != 0 ? ")" : ""); + } + } + P(" :: GPU topology (#918 — does the weave cross adapters to reach the panel?)\n"); if (!r->gpu_probed) { PT("%s\n", r->gpu_verdict[0] != '\0' ? r->gpu_verdict : "not evaluated"); @@ -2371,6 +2450,45 @@ cli_query_info_to_cjson(const struct cli_query_result *r) cJSON_AddNumberToObject(ds, "claim_count", (double)r->dp_sel_claim_count); } + // Multi-screen M1 — the per-screen list (xrEnumerateDisplaysDXR). + { + cJSON *arr = cJSON_AddArrayToObject(root, "displays"); + for (uint32_t i = 0; r->screens_probed && i < r->screens.count; i++) { + const struct xrt_screen *sc = &r->screens.screens[i]; + const struct xrt_screen_info *si = &sc->info; + cJSON *d = cJSON_CreateObject(); + char idbuf[32]; + snprintf(idbuf, sizeof(idbuf), "0x%016llx", (unsigned long long)sc->id); + cJSON_AddStringToObject(d, "id", idbuf); + cJSON_AddStringToObject(d, "device_name", sc->device_name); + cJSON_AddStringToObject(d, "plugin_id", sc->plugin_id); + cJSON_AddBoolToObject(d, "system_default", (sc->flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0); + cJSON_AddBoolToObject(d, "primary", (sc->flags & XRT_SCREEN_FLAG_PRIMARY) != 0); + cJSON_AddBoolToObject(d, "tracked", (sc->flags & XRT_SCREEN_FLAG_TRACKED) != 0); + cJSON_AddNumberToObject(d, "desktop_left", (double)sc->desktop_left); + cJSON_AddNumberToObject(d, "desktop_top", (double)sc->desktop_top); + cJSON_AddNumberToObject(d, "desktop_width", (double)sc->desktop_width); + cJSON_AddNumberToObject(d, "desktop_height", (double)sc->desktop_height); + cJSON_AddNumberToObject(d, "native_width", (double)sc->native_width); + cJSON_AddNumberToObject(d, "native_height", (double)sc->native_height); + cJSON_AddNumberToObject(d, "desktop_scale", (double)sc->desktop_scale); + cJSON_AddStringToObject(d, "info_source", u_screen_info_source_str(si->source)); + cJSON_AddNumberToObject(d, "width_m", (double)si->width_m); + cJSON_AddNumberToObject(d, "height_m", (double)si->height_m); + cJSON_AddNumberToObject(d, "pixel_width", (double)si->pixel_width); + cJSON_AddNumberToObject(d, "pixel_height", (double)si->pixel_height); + cJSON_AddNumberToObject(d, "nominal_viewer_x_m", (double)si->nominal_viewer_x_m); + cJSON_AddNumberToObject(d, "nominal_viewer_y_m", (double)si->nominal_viewer_y_m); + cJSON_AddNumberToObject(d, "nominal_viewer_z_m", (double)si->nominal_viewer_z_m); + cJSON_AddNumberToObject(d, "recommended_view_scale_x", (double)si->recommended_view_scale_x); + cJSON_AddNumberToObject(d, "recommended_view_scale_y", (double)si->recommended_view_scale_y); + cJSON_AddNumberToObject(d, "supported_eye_tracking_modes", + (double)si->supported_eye_tracking_modes); + cJSON_AddNumberToObject(d, "default_eye_tracking_mode", (double)si->default_eye_tracking_mode); + cJSON_AddItemToArray(arr, d); + } + } + // #1234 / #902 - VK late-weave repaint reachability (informational). { cJSON *vl = cJSON_AddObjectToObject(root, "vk_repaint_tier"); diff --git a/src/xrt/targets/cli/cli_query.h b/src/xrt/targets/cli/cli_query.h index 2a5c1f7524..79c849e11f 100644 --- a/src/xrt/targets/cli/cli_query.h +++ b/src/xrt/targets/cli/cli_query.h @@ -25,6 +25,7 @@ #include "xrt/xrt_device.h" #include "xrt/xrt_display_zones.h" #include "xrt/xrt_dp_lift.h" +#include "xrt/xrt_screen.h" #include #include @@ -395,6 +396,14 @@ struct cli_query_result char dp_sel_service_id[64]; //!< service path plug-in id (registry primary; == in-proc on empty registry). char dp_sel_service_conf[24]; //!< service claim confidence label (FALLBACK/EDID/VERIFIED/scalar-fallback). + /* Multi-screen M1 — the per-screen list `xrEnumerateDisplaysDXR` reports, + * built from the same registry + display info the runtime uses + * (target_screens_build), so M1 is verifiable without a window. + * screens_probed — the list was built (the active plug-in described its + * panel; an empty registry still yields the synthesized default). */ + bool screens_probed; + struct xrt_screen_list screens; + /* #918 GPU topology — does this box pay a cross-adapter present to get * the woven frame onto the panel? Windows-only: `gpu_probed` stays false * everywhere else and the serializers print nothing but an "n/a" line. diff --git a/test_apps/cube_handle_vk_linux/main.cpp b/test_apps/cube_handle_vk_linux/main.cpp index ccda5b65f4..3145eaa87b 100644 --- a/test_apps/cube_handle_vk_linux/main.cpp +++ b/test_apps/cube_handle_vk_linux/main.cpp @@ -2189,6 +2189,42 @@ static bool InitializeOpenXR(AppXrSession& xr, DxrWindowBackend requestedBackend } } + // XR_DXR_display_info v22 (multi-screen M1): every display the runtime + // knows, one line each. Informational only — the app still renders to the + // system-default display reported above. Absent on runtimes older than + // v22 (the proc lookup then fails and nothing is logged). + if (hasDisplayInfo) { + PFN_xrEnumerateDisplaysDXR pfnEnumerateDisplays = nullptr; + if (XR_SUCCEEDED(xrGetInstanceProcAddr(xr.instance, "xrEnumerateDisplaysDXR", + (PFN_xrVoidFunction*)&pfnEnumerateDisplays)) && + pfnEnumerateDisplays != nullptr) { + uint32_t displayCount = 0; + if (XR_SUCCEEDED(pfnEnumerateDisplays(xr.instance, xr.systemId, 0, &displayCount, nullptr))) { + std::vector displays(displayCount, XrDisplayDXR{XR_TYPE_DISPLAY_DXR}); + if (displayCount > 0 && + XR_SUCCEEDED(pfnEnumerateDisplays(xr.instance, xr.systemId, displayCount, &displayCount, + displays.data()))) { + for (uint32_t i = 0; i < displayCount; i++) { + const XrDisplayDXR& d = displays[i]; + LOG_INFO("Display %u: id=0x%016llx '%s' plug-in='%s'%s%s%s %dx%d @ (%d,%d), " + "%.4f x %.4f m, %ux%u px, nominal z %.3f m, scale %.3fx%.3f, eye modes 0x%llx", + i, (unsigned long long)d.displayId, d.deviceName, d.vendorPluginId, + (d.flags & XR_DISPLAY_SYSTEM_DEFAULT_BIT_DXR) ? " [system default]" : "", + (d.flags & XR_DISPLAY_PRIMARY_BIT_DXR) ? " [primary]" : "", + (d.flags & XR_DISPLAY_TRACKED_BIT_DXR) ? " [tracked]" : "", + d.desktopRect.extent.width, d.desktopRect.extent.height, + d.desktopRect.offset.x, d.desktopRect.offset.y, + d.info.displaySizeMeters.width, d.info.displaySizeMeters.height, + d.info.displayPixelWidth, d.info.displayPixelHeight, + d.info.nominalViewerPositionInDisplaySpace.z, + d.info.recommendedViewScaleX, d.info.recommendedViewScaleY, + (unsigned long long)d.eyeTracking.supportedModes); + } + } + } + } + } + uint32_t viewCount = 0; XR_CHECK(xrEnumerateViewConfigurationViews(xr.instance, xr.systemId, xr.viewConfigType, 0, &viewCount, nullptr)); xr.configViews.resize(viewCount, {XR_TYPE_VIEW_CONFIGURATION_VIEW}); From 1d6bed167539d56acd32278e429ba002b8b8bea8 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 10:59:20 -0700 Subject: [PATCH 05/10] docs(display_info): v22 per-display enumeration, DISPLAY spaces, session binding; plug-in slot reference XR_DXR_display_info spec: v22 section, revision row, OPEN 1 rewritten as partially resolved (what M2/M3/M5 still owe). view-configuration-model: what a session display binding does and does not change. xrt_plugin_iface reference: get_display_info_for_monitor, its source order and why xrt_display_physical is a separate by-pointer struct. Co-Authored-By: Claude Fable 5.1 --- docs/reference/view-configuration-model.md | 14 +++ docs/reference/xrt_plugin_iface.md | 44 +++++++- docs/specs/extensions/XR_DXR_display_info.md | 112 +++++++++++++++++-- 3 files changed, 161 insertions(+), 9 deletions(-) diff --git a/docs/reference/view-configuration-model.md b/docs/reference/view-configuration-model.md index 66ab245bfd..f4103b7e6f 100644 --- a/docs/reference/view-configuration-model.md +++ b/docs/reference/view-configuration-model.md @@ -513,6 +513,20 @@ and declares two modes, 2D (1 view) and LeiaSR (2 views). So `PRIMARY_MULTIVIEW_DXR` is future-proofing plus sim-display CI conformance — it is not preserving a shipping N-view product today. +## Session display binding (`XR_DXR_display_info` v22) + +An app can pin its session to one display by chaining `XrSessionDisplayBindingDXR` +(a `displayId` from `xrEnumerateDisplaysDXR`) on `XrSessionCreateInfo`. It does **not** +change anything on this page: both view configurations report the same view counts and +the same recommended sizes whatever the binding, because `xrEnumerateViewConfigurationViews` +is a system query (it runs before any session exists) and swapchains stay sized for the +system's worst case (ADR-010). What the binding changes, in v22, is the session's +display-scoped Kooima inputs — a session bound to a non-default display takes that +display's physical size and nominal viewer — while the system default display's DP still +weaves it. Per-view display bindings, where views are assigned to displays, arrive in +multi-screen M3 (ADR-047); until then binding to the system default (or not binding) is +exactly today's behaviour. + ## History — what the deviation was Before this change the runtime modelled **one** view configuration per system diff --git a/docs/reference/xrt_plugin_iface.md b/docs/reference/xrt_plugin_iface.md index d6f00c39e5..94a534cb3c 100644 --- a/docs/reference/xrt_plugin_iface.md +++ b/docs/reference/xrt_plugin_iface.md @@ -260,6 +260,47 @@ a disagreeing value does not survive the first mode switch anyway. **Called again after startup — keep it cheap.** A long-lived service can start before the vendor backend has identified the panel (an unattended reboot with the panel asleep; even a healthy boot needs a few seconds after the vendor platform service is up). The runtime therefore calls `get_display_info` not only at instance create but again on every client connect / per-client compositor create (`xrt_system_compositor_info::refresh_display_processors`), and — while the answer is still unknown — about once a second from the service's main loop (#1721), so identification is picked up even when no client connects afterwards; it re-applies the answer whenever the reported geometry (physical size, pixel size, nominal viewer, screen origin) differs from what it last applied — logging one `display info refreshed from plug-in after startup` WARN. IPC clients that connected *before* the first real answer keep the startup snapshot of the head's mode table (`rendering_modes[]` tiling, per-view pixel dims) they were created with — the client copies it out of shared memory once at device create — until they reconnect; their head poses/geometry are already right because you update your own `xrt_device` in place. The contract for the repeat calls: return `false` **fast** while the panel is still unknown (no blocking retry — spend any startup budget once, in `create_device`), and `true` with the real numbers once it is; keep your own head `xrt_device` (views, FOV, `hmd->views[].display`) correct in place, because the runtime re-derives only its own tiling / atlas / `xsysc->info` from the struct. Return values must be stable between calls for the same panel so the refresh stays a no-op. +### `get_display_info_for_monitor` (multi-screen M1) + +```c +bool (*get_display_info_for_monitor)(struct xrt_plugin_instance *inst, + const struct xrt_display_descriptor *display, + const struct xrt_display_physical *physical, + struct xrt_plugin_display_info *out_info); +``` + +The per-monitor twin of `get_display_info`: describe ONE monitor your plug-in won in the +per-monitor registry (your `probe_displays` claim beat everyone else's). Same out struct, +same field meanings — physical size, nominal viewer (display-centred, metres), native +pixels, recommended view scale (`0` = let the runtime decide), desktop origin, eye-tracking +modes supported/default — but keyed by the monitor, with no head device. It feeds +`XR_DXR_display_info` v22 `xrEnumerateDisplaysDXR` (`docs/specs/extensions/XR_DXR_display_info.md`) +and `displayxr-cli info`'s `:: Displays` block. + +- `display` is the descriptor `probe_displays` saw for the monitor. `physical` carries what the + descriptor does not — EDID millimetres and the connector's device mode — and may be NULL; + its fields are 0 when unknown. Clamp reads to `physical->struct_size`. +- The runtime sets `out_info->struct_size` and zero-fills the rest; never write past it. +- Cheap, non-blocking, any thread: it runs on every `xrEnumerateDisplaysDXR` and in diagnostics. +- Return `false` for a monitor you cannot describe. + +**Which monitor gets which source.** The system-default monitor (the one the scalar +`get_display_info` describes, i.e. `XrSystemProperties`) always reports the system info, so +the two can never disagree — this slot is asked only about your *other* monitors. With the +slot absent (NULL, or a `struct_size` that predates it) those monitors get runtime defaults +from EDID: physical size from the millimetres, view scale 1, no eye tracking, nominal viewer +centred at the distance that matches the system panel's vertical FOV. + +**ABI.** Appended after `stereo_camera_close` per ADR-020 (append-only, `struct_size`-gated, +no `XRT_PLUGIN_API_VERSION_CURRENT` bump). Guard an implementation with +`#ifdef XRT_PLUGIN_IFACE_HAS_DISPLAY_INFO_FOR_MONITOR` to keep building against older +headers. `xrt_display_physical` is a separate struct passed by pointer **on purpose**: +`probe_displays` receives descriptors as an array, so appending to `xrt_display_descriptor` +would change the array stride under every existing plug-in — a layout break, not a free +append. Reference implementation: `sim_display_plugin.c` (EDID mm, else its +`SIM_DISPLAY_WIDTH_M/HEIGHT_M` defaults; its usual nominal viewer; `SIM_DISPLAY_FAKE_TRACKING` +semantics unchanged). + ### `set_pose_source` ```c @@ -642,7 +683,8 @@ Flags (`XRT_PLUGIN_STEREO_CAMERA_*`) mirror the XR bits: `SHARED_WITH_EYE_TRACKI **Slot order.** The six slots are the LAST members of the iface, appended (ADR-020) after everything that was on `main` before them: `… vk_bundle_fn_table_offset` → `create_dp_d3d11_lift` -(ADR-042) → `get_platform_state` (ADR-045) → `stereo_camera_enumerate … stereo_camera_close`. +(ADR-042) → `get_platform_state` (ADR-045) → `stereo_camera_enumerate … stereo_camera_close` +→ `get_display_info_for_monitor` (multi-screen M1, now the last member). `tests_stereo_camera` pins that order (lift right after the vk fingerprint, platform-state right after lift, the camera block right after platform-state and ending the struct), so a reorder or a slot squeezed in between fails on the host, not on a vendor box. diff --git a/docs/specs/extensions/XR_DXR_display_info.md b/docs/specs/extensions/XR_DXR_display_info.md index b66b9e37d2..9bfca3a532 100644 --- a/docs/specs/extensions/XR_DXR_display_info.md +++ b/docs/specs/extensions/XR_DXR_display_info.md @@ -1139,6 +1139,96 @@ projLayer.viewCount = located; `test_apps/common/dxr_view_config.h` carries `DxrAliasInactiveViews()`, a header-only implementation of that tail loop, and every in-tree test app uses it. +### Per-display enumeration, DISPLAY spaces and session display binding (v22) + +Multi-screen milestone M1 (plan: `docs/roadmap/multi-screen.md`, ADR-047 — PR #1849). A system +can drive more than one monitor — on a laptop with an external 3D panel the runtime +already knows both (the per-monitor registry, ADR-015), but until v22 an app could +only see the one `XrDisplayInfoDXR` describes. v22 adds three things, all +append-only: no existing struct, value or behaviour changes. + +```c +#define XR_TYPE_DISPLAY_DXR ((XrStructureType)1004999214) +#define XR_TYPE_DISPLAY_SPACE_CREATE_INFO_DXR ((XrStructureType)1004999215) +#define XR_TYPE_SESSION_DISPLAY_BINDING_DXR ((XrStructureType)1004999216) +// 1004999217 is RESERVED for XrViewDisplayBindingsDXR (per-view display binding, M3). + +typedef XrFlags64 XrDisplayFlagsDXR; +XR_DISPLAY_PRIMARY_BIT_DXR = 0x1 // the OS desktop's primary monitor +XR_DISPLAY_TRACKED_BIT_DXR = 0x2 // eyeTracking.supportedModes != 0 +XR_DISPLAY_SYSTEM_DEFAULT_BIT_DXR = 0x4 // the display XrSystemProperties describes + +typedef struct XrDisplayDXR { + XrStructureType type; // XR_TYPE_DISPLAY_DXR + void* XR_MAY_ALIAS next; + uint64_t displayId; // opaque, non-zero + XrDisplayInfoDXR info; // embedded by value + XrRect2Di desktopRect; // same contract as XrDisplayDesktopInfoDXR + uint32_t nativePixelWidth, nativePixelHeight; + float desktopScale; // compositor scale, 0 = unknown + XrEyeTrackingModeCapabilitiesDXR eyeTracking; // embedded by value + XrDisplayFlagsDXR flags; + char vendorPluginId[64]; + char deviceName[64]; +} XrDisplayDXR; + +XrResult xrEnumerateDisplaysDXR(XrInstance, XrSystemId, uint32_t capacityInput, + uint32_t* countOutput, XrDisplayDXR* displays); + +typedef struct XrDisplaySpaceCreateInfoDXR { + XrStructureType type; const void* next; + uint64_t displayId; + XrPosef poseInDisplaySpace; +} XrDisplaySpaceCreateInfoDXR; + +XrResult xrCreateDisplaySpaceDXR(XrSession, const XrDisplaySpaceCreateInfoDXR*, XrSpace*); + +typedef struct XrSessionDisplayBindingDXR { // chained on XrSessionCreateInfo + XrStructureType type; const void* next; + uint64_t displayId; // 0 = runtime decides +} XrSessionDisplayBindingDXR; +``` + +**Enumeration.** `xrEnumerateDisplaysDXR` uses the standard two-call idiom and lists +every monitor the runtime identified — the 3D panel(s) a vendor plug-in claims and the +ordinary monitors the vendor-neutral fallback serves — one `XrDisplayDXR` each. The +SYSTEM_DEFAULT display comes first; exactly one entry carries the bit, and its `info` +and `eyeTracking` are exactly what `XrDisplayInfoDXR` / `XrEyeTrackingModeCapabilitiesDXR` +on `XrSystemProperties` report (active-mode view scale included). `XrSystemProperties` +keeps that meaning: v22 does not change which display it describes. `displayId` is +derived from the monitor's EDID identity and connector, so it is stable for the +instance and, in practice, across processes on the same boot; an app must still treat +it as opaque. Each display's `info` comes from, in order: the system info (for the +system default); the owning vendor plug-in's per-monitor answer +(`get_display_info_for_monitor`, `docs/reference/xrt_plugin_iface.md`); otherwise +runtime defaults from EDID — physical size from the EDID millimetres, native pixels +from the connector's mode, view scale 1, no eye tracking, and a nominal viewer centred +at the distance that gives the display the same vertical FOV as the system default's +nominal viewer. The list is identical in-process and over IPC (the service answers a +dedicated message). + +**DISPLAY spaces.** `xrCreateDisplaySpaceDXR` creates a reference-space-like `XrSpace` +for one display: origin at the display's centre, +X right, +Y up, +Z toward the viewer +(the display space of `docs/architecture/kooima-projection.md`), with +`poseInDisplaySpace` applied like `poseInReferenceSpace`. An unknown `displayId` fails +with `XR_ERROR_VALIDATION_FAILURE`. **v22 limit:** every DISPLAY space resolves to the +session's single display plane — the pose `XrViewDisplayRawDXR::displayPlanePose` +reports (the head device's pose for a runtime-window session; the head's tracking +origin for an app-window session) — whichever display it names. Spatial relationships +*between* displays arrive with per-screen DISPLAY poses in M3; there is no shared room +frame. + +**Session display binding.** Chaining `XrSessionDisplayBindingDXR` with a non-zero +`displayId` pins the session to that display (an unknown id fails `xrCreateSession` +with `XR_ERROR_VALIDATION_FAILURE`; `0` is the same as not chaining it). **v22 effect:** +when the bound display is not the system default and its physical size is known, its +physical size and nominal viewer become the session's display-scoped Kooima inputs +(the projection used before window metrics are known); binding to the system default +changes nothing. Swapchain sizing stays the system's worst case (ADR-010) and the +display processor that weaves the session is still the system default's — honouring +the binding in the compositor (segments, per-view display bindings) is M2/M3. +`XrSystemProperties` is not affected by a session's binding. + ### Example Code: Querying Display Mode Support and Requesting 2D > ⚠️ **These inline examples predate the multiview model and the v13 header — read with care.** @@ -1903,15 +1993,20 @@ this capability, and if so, how? ### Open Issues -**OPEN 1: Multi-display scenarios.** +**OPEN 1: Multi-display scenarios — PARTIALLY RESOLVED in v22.** -The current design assumes a single tracked 3D display per system. Multi-display scenarios -(e.g., multiple tracked monitors in a workstation) would require: -- A way to enumerate multiple displays per system. -- Per-display `XrDisplayInfoDXR` queries. -- Possibly per-display DISPLAY spaces. +Up to v21 the extension assumed a single tracked 3D display per system. v22 (multi-screen +M1, ADR-047) adds what an app needs to *see* every display: `xrEnumerateDisplaysDXR` +(per-display `XrDisplayInfoDXR`, desktop rect, eye-tracking capabilities, vendor +plug-in id, PRIMARY / TRACKED / SYSTEM_DEFAULT flags), per-display DISPLAY spaces +(`xrCreateDisplaySpaceDXR`) and a session display binding (`XrSessionDisplayBindingDXR`). +See *Per-display enumeration, DISPLAY spaces and session display binding (v22)*. -This is out of scope for the initial extension but should be considered in future revisions. +Still open, and planned in `docs/roadmap/multi-screen.md`: a window that spans two +displays woven per segment (M2), per-view display bindings on `XrViewState` and +per-display DISPLAY poses — i.e. meaningful relationships between two displays' spaces +(M3, `1004999217` reserved), and a second tracked panel with its own tracker (M5). +There is deliberately no shared room frame between displays. --- @@ -2381,7 +2476,8 @@ the property) silently ignore the call — graceful degradation. | 17 | 2026-08-16 | David Fattal | **Panel lease** (ADR-035 D2, #961): added `XrEventDataDisplayModeRequestDeniedDXR` (`1004999014`, additive) + `XrDisplayModeDenialReasonDXR` + `XR_DISPLAY_MODE_INDEX_NONE_DXR`. Requests from non-lease-holders are denied with a reason event, never queued; `XrEventDataHardwareDisplayStateChangedDXR` fires only after the display processor confirmed; a mode-change event whose transition does not land is reverted by a second event (#761); service-mode sessions apply nothing locally. | | 18 | 2026-09-01 | David Fattal | Added `XrDisplayDesktopInfoDXR` (`1004999211`, new chained struct — additive, no ABI change to existing structs): the panel monitor's **full desktop rect** plus a **stable device name** (`\\.\DISPLAY1`), superseding the origin-only `XrDisplayDesktopPositionDXR`, so a client can place its window on the 3D panel and re-resolve the monitor after a topology change (#1301, unblocking displayxr-unity#266). Adds `isPrimary` and `isPanelConfirmed`. The rect is resolved under a pinned per-monitor-v2 DPI context so it is physical even when the host process is DPI-unaware. | | 19 | 2026-09-17 | David Fattal | **N-view view configuration** (#1486 option B / #80): added `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (`1004999212`) — the extension now extends the core `XrViewConfigurationType` enum. `xrEnumerateViewConfigurations` returns a list: a conformant 2-view `PRIMARY_STEREO` plus, when this extension is enabled, the DXR type reporting the device's max view count across modes. `xrEndFrame` rejects `viewCount > 2` under `PRIMARY_STEREO` and names the DXR type as the opt-in. Additive: no struct, wire or ABI change. Also carries the earlier RAW-mode wording clarification (#1370): eye positions are **relative to the display plane**, not "regardless of the reference space" — `XrViewDisplayRawDXR::displayPlanePose` reports that plane in the locate space, and render-ready views (legacy and rig-chained) honour `XrViewLocateInfo::space` on both legs. | -| 20 | 2026-09-18 | David Fattal | **The mode floor** (#1499, follow-up to #1486): added `XR_DISPLAY_MODE_DENIAL_REASON_VIEW_CONFIG_CANNOT_FILL_DXR` (`6`) to `XrDisplayModeDenialReasonDXR`. A session never runs in, or requests, a rendering mode whose `viewCount` exceeds what its primary view configuration reports: `xrBeginSession` moves the display to a fillable mode before the first frame (announced with `XrEventDataRenderingModeChangedDXR`) and `xrRequestDisplayRenderingModeDXR` denies such a request with the new reason, locally, before it reaches the panel-lease holder. A `PRIMARY_MULTIVIEW_DXR` session is never floored or denied; a device that pins its mode and service mode both outrank the floor, and the runtime logs rather than silently clamps. **Enumerant only** — no struct, wire or ABI change. **Current header version (`XR_DXR_display_info_SPEC_VERSION == 20`).** | +| 20 | 2026-09-18 | David Fattal | **The mode floor** (#1499, follow-up to #1486): added `XR_DISPLAY_MODE_DENIAL_REASON_VIEW_CONFIG_CANNOT_FILL_DXR` (`6`) to `XrDisplayModeDenialReasonDXR`. A session never runs in, or requests, a rendering mode whose `viewCount` exceeds what its primary view configuration reports: `xrBeginSession` moves the display to a fillable mode before the first frame (announced with `XrEventDataRenderingModeChangedDXR`) and `xrRequestDisplayRenderingModeDXR` denies such a request with the new reason, locally, before it reaches the panel-lease holder. A `PRIMARY_MULTIVIEW_DXR` session is never floored or denied; a device that pins its mode and service mode both outrank the floor, and the runtime logs rather than silently clamps. **Enumerant only** — no struct, wire or ABI change. | +| 22 | 2026-10-07 | David Fattal | **Per-display enumeration** (multi-screen M1, ADR-047): added `XrDisplayDXR` (`1004999214`) + `xrEnumerateDisplaysDXR`, `XrDisplaySpaceCreateInfoDXR` (`1004999215`) + `xrCreateDisplaySpaceDXR`, `XrSessionDisplayBindingDXR` (`1004999216`, chained on `XrSessionCreateInfo`), `XrDisplayFlagsDXR`; `1004999217` reserved for `XrViewDisplayBindingsDXR` (M3). Append-only: `XrSystemProperties` keeps describing the system-default display. v22 limits: every DISPLAY space is the session's single display plane; a binding to a non-default display changes the session's display-scoped Kooima inputs only, not which DP weaves. **Current header version (`XR_DXR_display_info_SPEC_VERSION == 22`).** | > The `XR_DXR_display_info_SPEC_VERSION` define in the header is the authoritative current > revision. Earlier revision numbers in this table reflect the proposal's editing history and do From ff4dc01a1fd9b6f07f8c479b948f98c292cea539 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 11:00:58 -0700 Subject: [PATCH 06/10] test(stereo_camera): the iface now ends with get_display_info_for_monitor The ADR-020 slot-order pin asserted the camera block ends xrt_plugin_iface; multi-screen M1 appends one slot after it. Pin that slot's position too. Co-Authored-By: Claude Fable 5.1 --- tests/tests_stereo_camera.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/tests_stereo_camera.cpp b/tests/tests_stereo_camera.cpp index 1829067169..49f30997da 100644 --- a/tests/tests_stereo_camera.cpp +++ b/tests/tests_stereo_camera.cpp @@ -312,7 +312,11 @@ TEST_CASE("plug-in iface: camera slots are appended after lift and platform-stat offsetof(xrt_plugin_iface, vk_bundle_fn_table_offset) + sizeof(uint32_t)); CHECK(offsetof(xrt_plugin_iface, get_platform_state) == lift_end); CHECK(offsetof(xrt_plugin_iface, stereo_camera_enumerate) == platform_state_end); - CHECK(offsetof(xrt_plugin_iface, stereo_camera_close) + sizeof(void *) == sizeof(xrt_plugin_iface)); + // Multi-screen M1 appended get_display_info_for_monitor right after the + // camera block; it now ends the struct. + CHECK(offsetof(xrt_plugin_iface, get_display_info_for_monitor) == + offsetof(xrt_plugin_iface, stereo_camera_close) + sizeof(void *)); + CHECK(offsetof(xrt_plugin_iface, get_display_info_for_monitor) + sizeof(void *) == sizeof(xrt_plugin_iface)); xrt_plugin_iface iface{}; iface.struct_size = (uint32_t)offsetof(xrt_plugin_iface, stereo_camera_enumerate); From 6ed5580d6f2c4386bb40e8a479569abc89d1d129 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:50:41 -0700 Subject: [PATCH 07/10] fix(target_instance): publish the per-monitor registry under the display-info mutex t_instance_enumerate_displays read the registry under g_display_info_mutex, but refresh_display_processors_cb rebuilt it (build_dp_registry) and the desktop rect (fill_display_desktop_info) after refresh_display_info_from_plugin had released that mutex, and target_plugin_resolve_displays zeroes its output before taking the loader's lock. Under the service, a session created with an XrSessionDisplayBindingDXR while another client connects could see an empty or half-built registry and reject a valid displayId. The registry is now resolved into a local and published in one copy; the callback holds g_display_info_mutex across the display-info refresh, the registry rebuild and the desktop-rect fill; the system is published to the enumerate path only once all three exist. Lock order stays g_display_info_mutex -> g_refresh_mutex. Co-Authored-By: Claude Fable 5.1 --- src/xrt/targets/common/target_instance.c | 64 +++++++++++++++++++----- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/xrt/targets/common/target_instance.c b/src/xrt/targets/common/target_instance.c index fd789854f8..e0fa3d5b39 100644 --- a/src/xrt/targets/common/target_instance.c +++ b/src/xrt/targets/common/target_instance.c @@ -178,7 +178,16 @@ build_dp_registry(struct xrt_system_compositor_info *info) struct xrt_display_descriptor descs[XRT_DP_REGISTRY_MAX_ENTRIES]; uint32_t dn = target_plugin_build_descriptors(&edid, descs, XRT_DP_REGISTRY_MAX_ENTRIES); - target_plugin_resolve_displays(descs, dn, &info->dp_registry); + + // Resolve into a local and publish it in one copy: the resolver zeroes its + // output before it takes the loader's lock, so resolving in place would + // expose an empty / half-built registry to a concurrent reader. Callers + // hold g_display_info_mutex (lock order: g_display_info_mutex, then the + // loader's g_refresh_mutex inside the resolver), which is what + // t_instance_enumerate_displays reads under (multi-screen M1). + struct xrt_dp_factory_registry reg; + target_plugin_resolve_displays(descs, dn, ®); + info->dp_registry = reg; } static void @@ -402,18 +411,19 @@ enum display_info_refresh_result * changed — the plug-in contract is "false fast while the panel is unknown, * true with real numbers once identified" (see docs/reference/xrt_plugin_iface.md). * + * Caller holds @ref g_display_info_mutex. + * * @return @ref DISPLAY_INFO_REFRESH_APPLIED if `info` was re-applied and the * caller should re-resolve the desktop rect; see the enum for the rest. */ static enum display_info_refresh_result refresh_display_info_from_plugin(struct xrt_system_compositor_info *info, const struct xrt_plugin_iface *plugin) { + // Caller holds g_display_info_mutex (refresh_display_processors_cb). if (info == NULL || plugin == NULL || !g_display_info_mutex_initialized) { return g_display_info_applied_valid ? DISPLAY_INFO_REFRESH_UNCHANGED : DISPLAY_INFO_REFRESH_UNKNOWN; } - os_mutex_lock(&g_display_info_mutex); - bool applied = false; struct xrt_device *head = g_display_info_head; struct xrt_plugin_display_info pdi; @@ -453,7 +463,6 @@ refresh_display_info_from_plugin(struct xrt_system_compositor_info *info, const result = DISPLAY_INFO_REFRESH_UNCHANGED; } - os_mutex_unlock(&g_display_info_mutex); return result; } @@ -494,6 +503,17 @@ refresh_display_processors_cb(struct xrt_system_compositor_info *info) before->id ? before->id : "?", plugin->id ? plugin->id : "?"); } fill_dp_factories_from_plugin(info, plugin); + + // One critical section for the display info, the per-monitor registry + // and the desktop rect: t_instance_enumerate_displays (multi-screen M1) + // reads all three under this mutex, and two client connects run this + // callback concurrently. Lock order: g_display_info_mutex, then the + // loader's g_refresh_mutex (taken inside build_dp_registry's resolver and + // target_plugin_note_active_panel) — never the reverse. + const bool locked = g_display_info_mutex_initialized; + if (locked) { + os_mutex_lock(&g_display_info_mutex); + } const enum display_info_refresh_result refreshed = refresh_display_info_from_plugin(info, plugin); // #1721/#1722: while the panel is still unidentified this runs once a // second from the IPC main loop, and a registry rebuild is not free — it @@ -503,6 +523,9 @@ refresh_display_processors_cb(struct xrt_system_compositor_info *info) // against; the registry from instance create still stands. Known geometry // (client connect / compositor create, #342) keeps rebuilding as before. if (!swapped && refreshed == DISPLAY_INFO_REFRESH_UNKNOWN) { + if (locked) { + os_mutex_unlock(&g_display_info_mutex); + } return; } // Rebuild the per-monitor registry too — refresh_active invalidates the @@ -514,6 +537,9 @@ refresh_display_processors_cb(struct xrt_system_compositor_info *info) if (refreshed == DISPLAY_INFO_REFRESH_APPLIED) { fill_display_desktop_info(info); } + if (locked) { + os_mutex_unlock(&g_display_info_mutex); + } } /*! @@ -914,7 +940,6 @@ t_instance_create_system(struct xrt_instance *xinst, } g_display_info_head = head; g_display_info_applied_valid = false; - g_screens_info = &xsysc->info; xsysc->info.refresh_display_processors = refresh_display_processors_cb; // Vendor-neutral display-info population through the plug-in @@ -952,6 +977,16 @@ t_instance_create_system(struct xrt_instance *xinst, // monitor, which is the right answer for both readings. fill_display_desktop_info(&xsysc->info); + // Publish the system to xrt_instance::enumerate_displays only now that + // its display info, registry and desktop rect are all built. + if (g_display_info_mutex_initialized) { + os_mutex_lock(&g_display_info_mutex); + } + g_screens_info = &xsysc->info; + if (g_display_info_mutex_initialized) { + os_mutex_unlock(&g_display_info_mutex); + } + // All display-info + DP factories are sourced from the plug-in // iface above (ADR-019 / #256 / #263). The runtime DLL no longer // link-includes any drv_sim_display symbols. @@ -999,8 +1034,11 @@ t_instance_get_active_plugin(struct xrt_instance *xinst, * xrt_instance::enumerate_displays (multi-screen M1) — the screen list of the * system this instance created, built from its per-monitor registry and the * display info applied to it. The service answers `system_enumerate_displays` - * through this too. Serialised with the refresh callback, which rewrites - * both on a client connect. + * through this too. Reads under g_display_info_mutex, the lock + * refresh_display_processors_cb holds across its whole rewrite (display info, + * then the registry — resolved into a local and published in one copy — then + * the desktop rect), so a reader never sees an empty or half-built registry + * while another client's connect refreshes it. */ static xrt_result_t t_instance_enumerate_displays(struct xrt_instance *xinst, struct xrt_screen_list *out_list) @@ -1010,19 +1048,19 @@ t_instance_enumerate_displays(struct xrt_instance *xinst, struct xrt_screen_list return XRT_ERROR_ALLOCATION; } memset(out_list, 0, sizeof(*out_list)); - if (g_screens_info == NULL) { + if (!g_display_info_mutex_initialized) { return XRT_SUCCESS; // no system (or no compositor) yet: no screens } - if (g_display_info_mutex_initialized) { - os_mutex_lock(&g_display_info_mutex); + os_mutex_lock(&g_display_info_mutex); + if (g_screens_info == NULL) { + os_mutex_unlock(&g_display_info_mutex); + return XRT_SUCCESS; } struct target_screens_system sys; target_screens_system_from_info(g_screens_info, &sys); target_screens_build(&g_screens_info->dp_registry, g_screens_info->active_plugin_id, &sys, out_list); - if (g_display_info_mutex_initialized) { - os_mutex_unlock(&g_display_info_mutex); - } + os_mutex_unlock(&g_display_info_mutex); return XRT_SUCCESS; } From 3e3a286b51ad44377ec6b786a234acbcdb6603e7 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:51:22 -0700 Subject: [PATCH 08/10] fix(target_screens): the default screen is the monitor the panel resolver picked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit target_screens_build took xrt_dp_registry_primary_entry() as the system-default screen — entries[0] of the active plug-in's monitors — and then overwrote its rect, device name and primary flag with the #1301-resolved panel rect, even when the resolver had placed the panel on a different monitor: one monitor's id with another's rect. target_screens_pick_default now picks the entry whose rect contains the resolved origin (preferring one the active plug-in won), falls back to primary_entry only when none does, and the system's placement is copied onto the default screen only in the first case. Co-Authored-By: Claude Fable 5.1 --- src/xrt/targets/common/target_screens.c | 63 ++++++++++++++++++++++--- src/xrt/targets/common/target_screens.h | 19 +++++++- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/xrt/targets/common/target_screens.c b/src/xrt/targets/common/target_screens.c index 9f725b60cb..50ac3fc1c2 100644 --- a/src/xrt/targets/common/target_screens.c +++ b/src/xrt/targets/common/target_screens.c @@ -85,6 +85,7 @@ finish_flags(struct xrt_screen *s, bool is_primary, bool is_default) static void build_from_entry(const struct xrt_dp_registry_entry *e, bool is_default, + bool use_sys_placement, const struct target_screens_system *sys, const struct os_display_desktop_info *desk, uint32_t desk_count, @@ -133,9 +134,10 @@ build_from_entry(const struct xrt_dp_registry_entry *e, u_screen_info_resolve(&in, &s->info); // The system-default screen's placement is the one the runtime resolved - // for the panel (#1301) — what XrDisplayDesktopInfoDXR reports — when - // that resolution succeeded. - if (is_default && sys != NULL && sys->desktop_width > 0 && sys->desktop_height > 0) { + // for the panel (#1301) — what XrDisplayDesktopInfoDXR reports — but only + // when that resolution landed on THIS monitor (use_sys_placement), so a + // screen never carries one monitor's id and another monitor's rect. + if (is_default && use_sys_placement) { s->desktop_left = sys->desktop_left; s->desktop_top = sys->desktop_top; s->desktop_width = sys->desktop_width; @@ -156,6 +158,54 @@ build_from_entry(const struct xrt_dp_registry_entry *e, finish_flags(s, is_primary, is_default); } +const struct xrt_dp_registry_entry * +target_screens_pick_default(const struct xrt_dp_factory_registry *reg, + const char *active_plugin_id, + const struct target_screens_system *sys, + bool *out_matches_sys) +{ + if (out_matches_sys != NULL) { + *out_matches_sys = false; + } + if (reg == NULL || reg->entry_count == 0) { + return NULL; + } + + // The monitor the #1301 resolver placed the system panel on: the entry + // whose desktop rect contains the resolved origin. Prefer one the active + // plug-in won (two entries never overlap on a sane desktop, but a + // DRM-only record sits at (0,0) next to a real one). + if (sys != NULL && sys->desktop_width > 0 && sys->desktop_height > 0) { + const struct xrt_dp_registry_entry *hit = NULL; + for (uint32_t i = 0; i < reg->entry_count; i++) { + const struct xrt_dp_registry_entry *e = ®->entries[i]; + const int64_t l = e->screen_left; + const int64_t t = e->screen_top; + if (sys->desktop_left >= l && sys->desktop_left < l + (int64_t)e->pixel_width && + sys->desktop_top >= t && sys->desktop_top < t + (int64_t)e->pixel_height) { + const bool active = active_plugin_id != NULL && active_plugin_id[0] != '\0' && + strcmp(e->plugin_id, active_plugin_id) == 0; + if (hit == NULL || active) { + hit = e; + } + if (active) { + break; + } + } + } + if (hit != NULL) { + if (out_matches_sys != NULL) { + *out_matches_sys = true; + } + return hit; + } + } + + // Nothing contains it (or nothing was resolved): the registry's own + // "don't care which monitor" pick, without the system's placement. + return xrt_dp_registry_primary_entry(reg, active_plugin_id); +} + void target_screens_build(const struct xrt_dp_factory_registry *reg, const char *active_plugin_id, @@ -193,18 +243,19 @@ target_screens_build(const struct xrt_dp_factory_registry *reg, memset(desk, 0, sizeof(desk)); const uint32_t desk_count = os_display_desktop_enumerate(desk, OS_DISPLAY_DESKTOP_MAX_MONITORS); - const struct xrt_dp_registry_entry *def = xrt_dp_registry_primary_entry(reg, active_plugin_id); + bool def_matches_sys = false; + const struct xrt_dp_registry_entry *def = target_screens_pick_default(reg, active_plugin_id, sys, &def_matches_sys); uint32_t n = 0; if (def != NULL) { - build_from_entry(def, true, sys, desk, desk_count, &out->screens[n++]); + build_from_entry(def, true, def_matches_sys, sys, desk, desk_count, &out->screens[n++]); } for (uint32_t i = 0; i < reg->entry_count && n < XRT_SCREEN_LIST_MAX; i++) { const struct xrt_dp_registry_entry *e = ®->entries[i]; if (e == def) { continue; } - build_from_entry(e, false, sys, desk, desk_count, &out->screens[n++]); + build_from_entry(e, false, false, sys, desk, desk_count, &out->screens[n++]); } out->count = n; } diff --git a/src/xrt/targets/common/target_screens.h b/src/xrt/targets/common/target_screens.h index 310c36d20a..9d18dc9e21 100644 --- a/src/xrt/targets/common/target_screens.h +++ b/src/xrt/targets/common/target_screens.h @@ -51,10 +51,25 @@ struct target_screens_system void target_screens_system_from_info(const struct xrt_system_compositor_info *info, struct target_screens_system *out); +/*! + * Which registry entry is the system-default screen: the one whose desktop + * rect contains the origin the runtime resolved for the system panel (#1301, + * `sys->desktop_left/top`), preferring an entry the active plug-in won; when + * none contains it (or no rect was resolved), `xrt_dp_registry_primary_entry`. + * @p out_matches_sys (may be NULL) is true only in the first case — the only + * case in which the system's resolved rect / device name / primary flag may + * be copied onto that entry. NULL on an empty registry. + */ +const struct xrt_dp_registry_entry * +target_screens_pick_default(const struct xrt_dp_factory_registry *reg, + const char *active_plugin_id, + const struct target_screens_system *sys, + bool *out_matches_sys); + /*! * Build the screen list: one @ref xrt_screen per registry entry, the - * system-default one (`xrt_dp_registry_primary_entry(reg, active_plugin_id)`) - * first and flagged SYSTEM_DEFAULT, each screen's info resolved by + * system-default one (@ref target_screens_pick_default) first and flagged + * SYSTEM_DEFAULT, each screen's info resolved by * `u_screen_info_resolve` (system info / the owning plug-in's * `get_display_info_for_monitor` / EDID-derived). An empty registry yields * one synthesized screen (@ref XRT_SCREEN_ID_SYNTHETIC_DEFAULT) from @p sys, From 36625e0616cea8da044426f956bf56aba99ed383 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:52:19 -0700 Subject: [PATCH 09/10] test(target_screens): cover target_screens_build and the default-screen pick tests_target_screens drives the real builder against a hand-made registry, with the loader's monitor side table filled through target_plugin_build_descriptors from a fake EDID list: the active plug-in owning two monitors with the panel resolved onto the second (the id/rect mix-up the previous commit fixes), the primary_entry fallback that must keep its own rect, the active-plug-in preference, slot vs EDID-derived info for non-default screens, and the empty-registry synthesis. Co-Authored-By: Claude Fable 5.1 --- tests/CMakeLists.txt | 6 + tests/tests_target_screens.cpp | 244 +++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 tests/tests_target_screens.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a888781f95..1db7356e37 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -88,6 +88,8 @@ endif() # exists; everything it drives is the exact function pointer providers get. if(TARGET target_lists) list(APPEND tests tests_input_host_geometry) + # Multi-screen M1 -- target_screens_build: registry -> per-screen list. + list(APPEND tests tests_target_screens) endif() # Multi-screen M0 (#69) -- where a plug-in without probe_displays gets its # synthesized display claim (its panel's monitor, not the primary). Pure @@ -327,6 +329,10 @@ target_compile_definitions( tests_comp_color_policy PRIVATE "DXR_COMP_SRC_DIR=\"${PROJECT_SOURCE_DIR}/src/xrt/compositor\"" ) +if(TARGET tests_target_screens) + target_link_libraries(tests_target_screens PRIVATE target_lists xrt-interfaces aux_os) +endif() + if(TARGET tests_input_host_geometry) target_link_libraries(tests_input_host_geometry PRIVATE target_lists xrt-interfaces) endif() diff --git a/tests/tests_target_screens.cpp b/tests/tests_target_screens.cpp new file mode 100644 index 0000000000..325abc5bee --- /dev/null +++ b/tests/tests_target_screens.cpp @@ -0,0 +1,244 @@ +// Copyright 2026, The DisplayXR Project +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Multi-screen M1: target_screens_build — registry -> xrt_screen_list. + * + * Drives the real builder against a hand-made registry. The loader's monitor + * side table is filled through target_plugin_build_descriptors() from a fake + * EDID list, exactly as build_dp_registry does, so the owning plug-in's + * get_display_info_for_monitor slot is reachable. + * + * The case that motivated it: the active plug-in owns TWO monitors, the panel + * resolver (#1301) placed the panel on the second, and the builder used to + * take entries[0] as the system default and paint the second monitor's rect + * onto it — one monitor's id, another's rect. + */ + +#include "catch_amalgamated.hpp" + +#include "xrt/xrt_compositor.h" +#include "xrt/xrt_plugin.h" +#include "xrt/xrt_screen.h" + +#include "os/os_display_edid.h" + +#include "target_plugin_loader.h" +#include "target_screens.h" + +#include +#include + +namespace { + +int g_slot_calls = 0; + +bool +fake_slot(struct xrt_plugin_instance *, + const xrt_display_descriptor *d, + const xrt_display_physical *p, + xrt_plugin_display_info *out) +{ + g_slot_calls++; + out->display_width_m = (float)p->physical_width_mm / 1000.0f; + out->display_height_m = (float)p->physical_height_mm / 1000.0f; + out->nominal_viewer_z_m = 0.7f; + out->display_pixel_width = d->pixel_width; + out->display_pixel_height = d->pixel_height; + return true; +} + +/*! + * Three monitors, left to right: A (laptop, primary), B and C (two identical + * panels the "vendor" plug-in owns). + */ +struct fixture +{ + xrt_plugin_iface vendor{}; + xrt_plugin_iface fallback{}; + xrt_display_descriptor descs[3]{}; + xrt_dp_factory_registry reg{}; + target_screens_system sys{}; + + fixture() + { + g_slot_calls = 0; + vendor.struct_size = (uint32_t)sizeof(vendor); + vendor.id = "vendor"; + fallback.struct_size = (uint32_t)sizeof(fallback); + fallback.id = "fallback"; + fallback.get_display_info_for_monitor = fake_slot; + + static os_display_edid_list edid; + std::memset(&edid, 0, sizeof(edid)); + edid.count = 3; + const struct + { + uint16_t mfr, product; + int32_t left; + uint32_t w, h, mm_w, mm_h; + bool primary; + const char *name; + } mons[3] = { + {0x4C83, 0x423F, 0, 2880, 1800, 300, 190, true, "eDP-1"}, + {0x0472, 0x0001, 2880, 3840, 2160, 344, 193, false, "HDMI-1"}, + {0x0472, 0x0001, 6720, 3840, 2160, 344, 193, false, "DP-1"}, + }; + for (int i = 0; i < 3; i++) { + os_display_edid_monitor &m = edid.monitors[i]; + m.manufacturer_id = mons[i].mfr; + m.product_id = mons[i].product; + m.screen_left = mons[i].left; + m.pixel_width = mons[i].w; + m.pixel_height = mons[i].h; + m.refresh_hz = 60; + m.is_primary = mons[i].primary; + m.physical_width_mm = mons[i].mm_w; + m.physical_height_mm = mons[i].mm_h; + m.native_width = mons[i].w; + m.native_height = mons[i].h; + std::snprintf(m.output_name, sizeof(m.output_name), "%s", mons[i].name); + std::snprintf(m.connector, sizeof(m.connector), "%s", mons[i].name); + } + REQUIRE(target_plugin_build_descriptors(&edid, descs, 3) == 3); + + reg.entry_count = 3; + for (int i = 0; i < 3; i++) { + xrt_dp_registry_entry &e = reg.entries[i]; + e.monitor_id = descs[i].monitor_id; + e.screen_left = descs[i].screen_left; + e.screen_top = descs[i].screen_top; + e.pixel_width = descs[i].pixel_width; + e.pixel_height = descs[i].pixel_height; + const xrt_plugin_iface *owner = i == 0 ? &fallback : &vendor; + std::snprintf(e.plugin_id, sizeof(e.plugin_id), "%s", owner->id); + e.owning_iface = owner; + e.confidence = i == 0 ? 10 : 50; + } + + // The system panel: the vendor's, resolved onto the THIRD monitor. + sys.info_valid = true; + sys.info.width_m = 0.344f; + sys.info.height_m = 0.193f; + sys.info.nominal_viewer_z_m = 0.6f; + sys.info.pixel_width = 3840; + sys.info.pixel_height = 2160; + sys.info.supported_eye_tracking_modes = 1u; + sys.desktop_left = 6720; + sys.desktop_top = 0; + sys.desktop_width = 3840; + sys.desktop_height = 2160; + std::snprintf(sys.device_name, sizeof(sys.device_name), "DP-1"); + } +}; + +} // namespace + +TEST_CASE("system default = the monitor the panel resolver picked, not entries[0] of the active plug-in") +{ + fixture f; + xrt_screen_list list{}; + target_screens_build(&f.reg, "vendor", &f.sys, &list); + + REQUIRE(list.count == 3); + const xrt_screen &def = list.screens[0]; + CHECK((def.flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0); + CHECK(def.id == f.descs[2].monitor_id); // DP-1, where the panel is + CHECK(def.desktop_left == 6720); // its own rect, consistent with its id + CHECK(std::strcmp(def.device_name, "DP-1") == 0); + CHECK(def.info.source == XRT_SCREEN_INFO_SOURCE_SYSTEM); + CHECK((def.flags & XRT_SCREEN_FLAG_TRACKED) != 0); + + // Exactly one default; every other screen keeps its own registry rect. + int defaults = 0; + for (uint32_t i = 0; i < list.count; i++) { + defaults += (list.screens[i].flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0; + if (i > 0) { + const xrt_screen &s = list.screens[i]; + const xrt_dp_registry_entry *e = nullptr; + for (uint32_t k = 0; k < f.reg.entry_count; k++) { + if (f.reg.entries[k].monitor_id == s.id) { + e = &f.reg.entries[k]; + } + } + REQUIRE(e != nullptr); + CHECK(s.desktop_left == e->screen_left); + } + } + CHECK(defaults == 1); +} + +TEST_CASE("resolved origin on no monitor: primary_entry fallback, and its rect is NOT overwritten") +{ + fixture f; + f.sys.desktop_left = -5000; // a rect no entry contains + f.sys.desktop_width = 1234; + xrt_screen_list list{}; + target_screens_build(&f.reg, "vendor", &f.sys, &list); + + REQUIRE(list.count == 3); + const xrt_screen &def = list.screens[0]; + CHECK(def.id == f.descs[1].monitor_id); // primary_entry: first entry the active plug-in won + CHECK(def.desktop_left == 2880); // its own rect + CHECK(def.desktop_width == 3840); + CHECK(std::strcmp(def.device_name, "HDMI-1") == 0); + CHECK(def.info.source == XRT_SCREEN_INFO_SOURCE_SYSTEM); +} + +TEST_CASE("pick_default prefers the active plug-in's entry and reports whether the rect matched") +{ + fixture f; + bool matched = false; + CHECK(target_screens_pick_default(&f.reg, "vendor", &f.sys, &matched) == &f.reg.entries[2]); + CHECK(matched); + + f.sys.desktop_width = 0; // nothing resolved + CHECK(target_screens_pick_default(&f.reg, "vendor", &f.sys, &matched) == &f.reg.entries[1]); + CHECK_FALSE(matched); + + xrt_dp_factory_registry empty{}; + CHECK(target_screens_pick_default(&empty, "vendor", &f.sys, &matched) == nullptr); +} + +TEST_CASE("non-default screens: the owning plug-in's slot, else EDID-derived") +{ + fixture f; + xrt_screen_list list{}; + target_screens_build(&f.reg, "vendor", &f.sys, &list); + + const xrt_screen *laptop = xrt_screen_list_find(&list, f.descs[0].monitor_id); + const xrt_screen *hdmi = xrt_screen_list_find(&list, f.descs[1].monitor_id); + REQUIRE(laptop != nullptr); + REQUIRE(hdmi != nullptr); + + // eDP-1 is owned by the plug-in that implements the slot. + CHECK(g_slot_calls == 1); + CHECK(laptop->info.source == XRT_SCREEN_INFO_SOURCE_PLUGIN_MONITOR); + CHECK(laptop->info.width_m == Catch::Approx(0.300f)); + CHECK(laptop->info.nominal_viewer_z_m == Catch::Approx(0.7f)); + CHECK((laptop->flags & XRT_SCREEN_FLAG_PRIMARY) != 0); + CHECK(laptop->physical_width_mm == 300); + CHECK(std::strcmp(laptop->plugin_id, "fallback") == 0); + + // HDMI-1 is the vendor's but not the system panel, and the vendor has no slot. + CHECK(hdmi->info.source == XRT_SCREEN_INFO_SOURCE_DERIVED); + CHECK(hdmi->info.supported_eye_tracking_modes == 0u); + CHECK((hdmi->flags & XRT_SCREEN_FLAG_TRACKED) == 0); + CHECK(hdmi->info.nominal_viewer_z_m == Catch::Approx(0.6f)); // same height -> same distance +} + +TEST_CASE("empty registry: one synthesized system screen, or nothing when the system knows nothing") +{ + fixture f; + xrt_dp_factory_registry empty{}; + xrt_screen_list list{}; + target_screens_build(&empty, "vendor", &f.sys, &list); + REQUIRE(list.count == 1); + CHECK(list.screens[0].id == XRT_SCREEN_ID_SYNTHETIC_DEFAULT); + CHECK((list.screens[0].flags & XRT_SCREEN_FLAG_SYSTEM_DEFAULT) != 0); + CHECK(list.screens[0].desktop_left == 6720); + + f.sys.info_valid = false; + target_screens_build(&empty, "vendor", &f.sys, &list); + CHECK(list.count == 0); +} From 9b454aac6f410b807af9bb2a37d57764f78df86f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 7 Oct 2026 13:53:37 -0700 Subject: [PATCH 10/10] fix(loader): displayId survives a monitor rearrangement where the connector is known The monitor id (XR_DXR_display_info v22 displayId) hashed the desktop position, and the service rebuilds its registry on every client connect, so an id an app held changed when the user rearranged monitors, while the header and spec called it stable and EDID/connector-derived. Where the platform names the connector (desktop Linux) the id is now EDID manufacturer + product + serial + connector, no position. The connector keeps its DRM card prefix ("card1-HDMI-A-1", as M0 records it), so two GPUs exposing the same connector name do not collide; a kernel card renumbering therefore changes the id, which the docs state. The key is unique per machine: stable across rearrangements and processes, new only when another panel is plugged into the port. Without a connector name (Windows) the old mfr + product + position hash is kept, so Windows ids are unchanged; the header, spec and plugin-discovery doc now say exactly that and tell apps to re-enumerate after a topology change. Pinned in tests_target_screens. Also adds the spec's missing v21 revision row (XrViewActivityStateDXR, ADR-041). Co-Authored-By: Claude Fable 5.1 --- docs/specs/extensions/XR_DXR_display_info.md | 13 ++++- docs/specs/runtime/plugin-discovery.md | 13 ++++- .../openxr/XR_DXR_display_info.h | 17 ++++-- src/xrt/targets/common/target_plugin_loader.c | 55 +++++++++++++------ tests/tests_target_screens.cpp | 48 ++++++++++++++++ 5 files changed, 118 insertions(+), 28 deletions(-) diff --git a/docs/specs/extensions/XR_DXR_display_info.md b/docs/specs/extensions/XR_DXR_display_info.md index 9bfca3a532..34cfdeebfb 100644 --- a/docs/specs/extensions/XR_DXR_display_info.md +++ b/docs/specs/extensions/XR_DXR_display_info.md @@ -1196,9 +1196,15 @@ SYSTEM_DEFAULT display comes first; exactly one entry carries the bit, and its ` and `eyeTracking` are exactly what `XrDisplayInfoDXR` / `XrEyeTrackingModeCapabilitiesDXR` on `XrSystemProperties` report (active-mode view scale included). `XrSystemProperties` keeps that meaning: v22 does not change which display it describes. `displayId` is -derived from the monitor's EDID identity and connector, so it is stable for the -instance and, in practice, across processes on the same boot; an app must still treat -it as opaque. Each display's `info` comes from, in order: the system info (for the +opaque. Where the platform names the connector (desktop Linux) it is derived from the +monitor's EDID identity (manufacturer, product, serial) plus the connector, and not +from the desktop position: it survives the user rearranging monitors, is the same in +every process, and changes only when a different panel is plugged into that port, +the panel moves port, or the kernel renumbers the GPU (the connector key includes its +card, e.g. `card1-HDMI-A-1`). Where it does not (Windows today) the id also covers the +desktop position, so rearranging monitors there yields new ids. An app should +re-enumerate after a topology change and treat an id that is no longer listed as +gone. Each display's `info` comes from, in order: the system info (for the system default); the owning vendor plug-in's per-monitor answer (`get_display_info_for_monitor`, `docs/reference/xrt_plugin_iface.md`); otherwise runtime defaults from EDID — physical size from the EDID millimetres, native pixels @@ -2477,6 +2483,7 @@ the property) silently ignore the call — graceful degradation. | 18 | 2026-09-01 | David Fattal | Added `XrDisplayDesktopInfoDXR` (`1004999211`, new chained struct — additive, no ABI change to existing structs): the panel monitor's **full desktop rect** plus a **stable device name** (`\\.\DISPLAY1`), superseding the origin-only `XrDisplayDesktopPositionDXR`, so a client can place its window on the 3D panel and re-resolve the monitor after a topology change (#1301, unblocking displayxr-unity#266). Adds `isPrimary` and `isPanelConfirmed`. The rect is resolved under a pinned per-monitor-v2 DPI context so it is physical even when the host process is DPI-unaware. | | 19 | 2026-09-17 | David Fattal | **N-view view configuration** (#1486 option B / #80): added `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (`1004999212`) — the extension now extends the core `XrViewConfigurationType` enum. `xrEnumerateViewConfigurations` returns a list: a conformant 2-view `PRIMARY_STEREO` plus, when this extension is enabled, the DXR type reporting the device's max view count across modes. `xrEndFrame` rejects `viewCount > 2` under `PRIMARY_STEREO` and names the DXR type as the opt-in. Additive: no struct, wire or ABI change. Also carries the earlier RAW-mode wording clarification (#1370): eye positions are **relative to the display plane**, not "regardless of the reference space" — `XrViewDisplayRawDXR::displayPlanePose` reports that plane in the locate space, and render-ready views (legacy and rig-chained) honour `XrViewLocateInfo::space` on both legs. | | 20 | 2026-09-18 | David Fattal | **The mode floor** (#1499, follow-up to #1486): added `XR_DISPLAY_MODE_DENIAL_REASON_VIEW_CONFIG_CANNOT_FILL_DXR` (`6`) to `XrDisplayModeDenialReasonDXR`. A session never runs in, or requests, a rendering mode whose `viewCount` exceeds what its primary view configuration reports: `xrBeginSession` moves the display to a fillable mode before the first frame (announced with `XrEventDataRenderingModeChangedDXR`) and `xrRequestDisplayRenderingModeDXR` denies such a request with the new reason, locally, before it reaches the panel-lease holder. A `PRIMARY_MULTIVIEW_DXR` session is never floored or denied; a device that pins its mode and service mode both outrank the floor, and the runtime logs rather than silently clamps. **Enumerant only** — no struct, wire or ABI change. | +| 21 | 2026-09-18 | David Fattal | **Fixed view count, per-frame activity** (ADR-041 Model E, #1528 / #1486): added `XrViewActivityStateDXR` (`1004999213`, chained on `XrViewState` at `xrLocateViews`). The located view count is fixed for the session (2 under `PRIMARY_STEREO`, the device maximum under `PRIMARY_MULTIVIEW_DXR`); `activeViewCount` says how many carry the active mode's views, the inactive tail is located at view 0's pose and ignored. `xrEndFrame`'s `viewCount` must equal the located count — an app rendering only the active views aliases the tail onto view 0's subimage. The pre-v21 1-view 2D submission stays accepted (deprecated, `DXR_UNDER_SUBMIT`). Additive: new chained struct only. | | 22 | 2026-10-07 | David Fattal | **Per-display enumeration** (multi-screen M1, ADR-047): added `XrDisplayDXR` (`1004999214`) + `xrEnumerateDisplaysDXR`, `XrDisplaySpaceCreateInfoDXR` (`1004999215`) + `xrCreateDisplaySpaceDXR`, `XrSessionDisplayBindingDXR` (`1004999216`, chained on `XrSessionCreateInfo`), `XrDisplayFlagsDXR`; `1004999217` reserved for `XrViewDisplayBindingsDXR` (M3). Append-only: `XrSystemProperties` keeps describing the system-default display. v22 limits: every DISPLAY space is the session's single display plane; a binding to a non-default display changes the session's display-scoped Kooima inputs only, not which DP weaves. **Current header version (`XR_DXR_display_info_SPEC_VERSION == 22`).** | > The `XR_DXR_display_info_SPEC_VERSION` define in the header is the authoritative current diff --git a/docs/specs/runtime/plugin-discovery.md b/docs/specs/runtime/plugin-discovery.md index 8155f0df25..9b3300ea43 100644 --- a/docs/specs/runtime/plugin-discovery.md +++ b/docs/specs/runtime/plugin-discovery.md @@ -471,9 +471,16 @@ headless) every connected, enabled connector becomes a DRM-only record flagged 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. +plus a loader side table keyed by `monitor_id`). Where the platform names the +connector, the `monitor_id` hash is EDID manufacturer + product + serial + the +connector name **with its card prefix** (`card1-HDMI-A-1`), with no desktop +position (multi-screen M1): the connector is unique per machine, so the id +survives a monitor rearrangement — it is the `displayId` apps hold via +`xrEnumerateDisplaysDXR` — DRM-only records at (0, 0) stay distinct, and two GPUs +that both expose an `HDMI-A-1` do not collide. The id changes when a different +panel is plugged into the port, the panel moves port, or the kernel renumbers +the card (GPU added/removed, probe order across boots). Windows ids (no +connector name) are unchanged: manufacturer + product + position. **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`, diff --git a/src/external/openxr_includes/openxr/XR_DXR_display_info.h b/src/external/openxr_includes/openxr/XR_DXR_display_info.h index d8231c936e..931702200d 100644 --- a/src/external/openxr_includes/openxr/XR_DXR_display_info.h +++ b/src/external/openxr_includes/openxr/XR_DXR_display_info.h @@ -714,9 +714,16 @@ typedef struct XrDisplayDXR { XrStructureType type; //!< Must be XR_TYPE_DISPLAY_DXR void* XR_MAY_ALIAS next; /*! - * Opaque, non-zero identifier of this display, stable for the life of the - * instance (and, in practice, across processes on the same boot: it is - * derived from the monitor's EDID identity and connector). Pass it to + * Opaque, non-zero identifier of this display. Where the platform names + * the connector (desktop Linux) it is derived from the monitor's EDID + * identity + connector only: it survives the user rearranging monitors + * and is the same in every process, and changes only when a different + * panel is plugged into that port, the panel moves port, or the kernel + * renumbers the GPU (the connector key includes its card, e.g. + * "card1-HDMI-A-1"). Where it does + * not (Windows today) it also covers the desktop position, so a + * rearrangement yields new ids. Either way, re-enumerate after a topology + * change and treat an id that is no longer listed as gone. Pass it to * XrDisplaySpaceCreateInfoDXR and XrSessionDisplayBindingDXR. */ uint64_t displayId; @@ -742,8 +749,8 @@ typedef struct XrDisplayDXR { /*! * @brief Enumerate the system's displays (v22). Standard two-call idiom. * - * The list is snapshotted when the call is made; ids are stable for the - * instance. Order: the SYSTEM_DEFAULT display first, then the rest in the + * The list is snapshotted when the call is made (see XrDisplayDXR::displayId + * for how long an id lasts). Order: the SYSTEM_DEFAULT display first, then the rest in the * runtime's monitor-enumeration order. * * @param instance A valid XrInstance with XR_DXR_display_info enabled. diff --git a/src/xrt/targets/common/target_plugin_loader.c b/src/xrt/targets/common/target_plugin_loader.c index f9b6e85f4d..4c802d7606 100644 --- a/src/xrt/targets/common/target_plugin_loader.c +++ b/src/xrt/targets/common/target_plugin_loader.c @@ -3162,32 +3162,52 @@ target_plugin_refresh_active(void) */ /*! - * Stable-for-this-boot monitor id. FNV-1a-64 over the EDID identity fields - * that survive a mode/refresh change — manufacturer, product, and screen - * position (so two identical-model panels at different positions differ). - * Excludes pixel dims/refresh/HMONITOR (transient). Known limitation: - * repositioning a monitor changes its id within a boot — acceptable for - * Phase 1; EDID device-instance-path keying is the Phase 2/3 hardening. + * Monitor id (also the `displayId` of `XR_DXR_display_info` v22). FNV-1a-64. + * + * Where the platform names the connector (desktop Linux: the DRM connector + * WITH its card prefix, e.g. "card1-HDMI-A-1", else the RandR output name) the + * id hashes the EDID identity + * — manufacturer, product, serial — plus that connector, and NOT the desktop + * position: the connector is unique per machine, so the id survives the user + * rearranging monitors and the service rebuilding its registry on the next + * client connect, and still separates two identical panels (different ports). + * It changes only when a different panel is plugged into that port, the + * panel moves to another port, or the kernel renumbers the card (a GPU added + * or removed, a different driver probe order across boots) (multi-screen M1). + * + * With no connector name (Windows today) the id stays what it has always + * been — manufacturer + product + screen position, position being the only + * thing that separates two identical panels there — so Windows ids are + * unchanged, and a rearrangement there does change them (documented in + * XR_DXR_display_info v22). */ static uint64_t -monitor_id_from_edid(uint16_t mfr, uint16_t product, int32_t left, int32_t top, const char *platform_key) +monitor_id_from_edid( + uint16_t mfr, uint16_t product, uint32_t serial, int32_t left, int32_t top, const char *platform_key) { uint64_t h = 1469598103934665603ULL; /* FNV-1a-64 offset basis */ const uint64_t prime = 1099511628211ULL; + const bool have_key = platform_key != NULL && platform_key[0] != '\0'; uint8_t bytes[12]; + size_t n = 0; memcpy(&bytes[0], &mfr, sizeof(mfr)); memcpy(&bytes[2], &product, sizeof(product)); - memcpy(&bytes[4], &left, sizeof(left)); - memcpy(&bytes[8], &top, sizeof(top)); - for (size_t i = 0; i < sizeof(bytes); i++) { + if (have_key) { + memcpy(&bytes[4], &serial, sizeof(serial)); + n = 8; + } else { + memcpy(&bytes[4], &left, sizeof(left)); + memcpy(&bytes[8], &top, sizeof(top)); + n = 12; + } + for (size_t i = 0; i < n; i++) { h ^= (uint64_t)bytes[i]; h *= prime; } - // The platform's connector key (desktop Linux: "card1-HDMI-A-1"), when there is - // one. Empty on Windows, so ids there are unchanged. It separates two - // identical panels that share a position — every DRM-only record (no X - // server) sits at (0, 0). - for (const char *c = platform_key; c != NULL && *c != '\0'; c++) { + // The connector key exactly as the platform records it — on desktop + // Linux with its DRM card prefix ("card1-HDMI-A-1"), so two GPUs that both + // expose an "HDMI-A-1" still yield different ids. + for (const char *c = platform_key; have_key && *c != '\0'; c++) { h ^= (uint64_t)(uint8_t)*c; h *= prime; } @@ -3301,8 +3321,9 @@ target_plugin_build_descriptors(const struct os_display_edid_list *list, struct xrt_display_descriptor *d = &out[i]; memset(d, 0, sizeof(*d)); d->struct_size = (uint32_t)sizeof(*d); - d->monitor_id = monitor_id_from_edid(m->manufacturer_id, m->product_id, m->screen_left, m->screen_top, - m->connector[0] != '\0' ? m->connector : m->output_name); + d->monitor_id = + monitor_id_from_edid(m->manufacturer_id, m->product_id, m->serial_number, m->screen_left, + m->screen_top, m->connector[0] != '\0' ? m->connector : m->output_name); d->edid_manufacturer = m->manufacturer_id; d->edid_product = m->product_id; d->pixel_width = m->pixel_width; diff --git a/tests/tests_target_screens.cpp b/tests/tests_target_screens.cpp index 325abc5bee..8c61df280c 100644 --- a/tests/tests_target_screens.cpp +++ b/tests/tests_target_screens.cpp @@ -242,3 +242,51 @@ TEST_CASE("empty registry: one synthesized system screen, or nothing when the sy target_screens_build(&empty, "vendor", &f.sys, &list); CHECK(list.count == 0); } + +TEST_CASE("monitor id: survives a rearrangement where the connector is known, separates identical panels") +{ + os_display_edid_list edid{}; + edid.count = 2; + for (int i = 0; i < 2; i++) { + os_display_edid_monitor &m = edid.monitors[i]; + m.manufacturer_id = 0x0472; + m.product_id = 0x0001; + m.serial_number = 0x322EF05E; + m.pixel_width = 3840; + m.pixel_height = 2160; + m.screen_left = i * 3840; + std::snprintf(m.connector, sizeof(m.connector), "%s", i == 0 ? "card1-HDMI-A-1" : "card1-DP-1"); + } + xrt_display_descriptor a[2]{}; + REQUIRE(target_plugin_build_descriptors(&edid, a, 2) == 2); + CHECK(a[0].monitor_id != a[1].monitor_id); // identical panels, different ports + + // The user swaps the two monitors' desktop positions. + edid.monitors[0].screen_left = 3840; + edid.monitors[1].screen_left = 0; + xrt_display_descriptor b[2]{}; + REQUIRE(target_plugin_build_descriptors(&edid, b, 2) == 2); + CHECK(b[0].monitor_id == a[0].monitor_id); + CHECK(b[1].monitor_id == a[1].monitor_id); + + // A different panel on the same port is a different display. + edid.monitors[0].serial_number = 0x1; + xrt_display_descriptor c[2]{}; + REQUIRE(target_plugin_build_descriptors(&edid, c, 2) == 2); + CHECK(c[0].monitor_id != a[0].monitor_id); + + // Same connector name on a second GPU: the card prefix keeps them apart. + edid.monitors[0].serial_number = edid.monitors[1].serial_number; + std::snprintf(edid.monitors[0].connector, sizeof(edid.monitors[0].connector), "%s", "card0-DP-1"); + xrt_display_descriptor e[2]{}; + REQUIRE(target_plugin_build_descriptors(&edid, e, 2) == 2); + CHECK(e[0].monitor_id != e[1].monitor_id); + + // No connector name (Windows): position still separates identical panels. + edid.monitors[0].connector[0] = '\0'; + edid.monitors[1].connector[0] = '\0'; + edid.monitors[0].serial_number = edid.monitors[1].serial_number; + xrt_display_descriptor d[2]{}; + REQUIRE(target_plugin_build_descriptors(&edid, d, 2) == 2); + CHECK(d[0].monitor_id != d[1].monitor_id); +}