Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/reference/view-configuration-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 43 additions & 1 deletion docs/reference/xrt_plugin_iface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
119 changes: 111 additions & 8 deletions docs/specs/extensions/XR_DXR_display_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading