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..34cfdeebfb 100644 --- a/docs/specs/extensions/XR_DXR_display_info.md +++ b/docs/specs/extensions/XR_DXR_display_info.md @@ -1139,6 +1139,102 @@ 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 +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 +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 +1999,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 +2482,9 @@ 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. | +| 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 > revision. Earlier revision numbers in this table reflect the proposal's editing history and do 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/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..931702200d 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,179 @@ 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. 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; + //! 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 (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. + * @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/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_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/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/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/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) { 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/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..e0fa3d5b39 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" @@ -176,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 @@ -204,6 +215,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. @@ -396,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; @@ -447,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; } @@ -488,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 @@ -497,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 @@ -508,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); + } } /*! @@ -945,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. @@ -988,6 +1030,40 @@ 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. 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) +{ + (void)xinst; + if (out_list == NULL) { + return XRT_ERROR_ALLOCATION; + } + memset(out_list, 0, sizeof(*out_list)); + if (!g_display_info_mutex_initialized) { + return XRT_SUCCESS; // no system (or no compositor) yet: no screens + } + + 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); + 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 +1090,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..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; } @@ -3205,6 +3225,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; @@ -3300,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; @@ -3319,6 +3341,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 +3349,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..50ac3fc1c2 --- /dev/null +++ b/src/xrt/targets/common/target_screens.c @@ -0,0 +1,261 @@ +// 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, + bool use_sys_placement, + 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 — 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; + 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); +} + +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, + 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); + + 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, 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, 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..9d18dc9e21 --- /dev/null +++ b/src/xrt/targets/common/target_screens.h @@ -0,0 +1,89 @@ +// 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); + +/*! + * 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 (@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, + * 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 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}); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 834d9cec36..1db7356e37 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 @@ -87,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 @@ -261,6 +264,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. @@ -321,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_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); +} 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); diff --git a/tests/tests_target_screens.cpp b/tests/tests_target_screens.cpp new file mode 100644 index 0000000000..8c61df280c --- /dev/null +++ b/tests/tests_target_screens.cpp @@ -0,0 +1,292 @@ +// 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); +} + +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); +}