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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to the DisplayXR Unity plugin will be documented in this fil
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **The projection layer's view count now comes from the ACTIVE rendering mode at submit, not from the swapchain-create latch.** `displayxr-runtime#1486` tightened `xrEndFrame`: under `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO` — which is what this provider begins with, and stays on, because Unity's stereo topology is fixed at 2 — a projection layer must carry exactly 2 views, or 1 **only** while the active rendering mode is itself 1-view (and the instance enabled `XR_DXR_display_info`, which the provider always does). A 1-view submission in a 2-view mode is now `XR_ERROR_VALIDATION_FAILURE` where the runtime used to silently flat-blit it. The provider derived that count from `s_ps.sc_view_count`, a shadow of the mode latched when the swapchain was created and refreshed only by `ps_reconcile_primary()` — a refresh that is skippable and can lag: it early-returns on a zero-size target (minimised window, unresolved zone rect) *before* it ever compares the view count, `dxr_prov_reconcile_size()` returns early while a frame is begun, a failed swapchain recreate leaves the stale value in place, and on macOS the mode event is polled from the main thread (`DisplayXRProviderDriver.LateUpdate`) while submit runs on the render thread. So a 2D→3D flip could leave a 1-view submission outliving its 1-view mode. `dxr_prov_submit_frame` now reads the active mode directly and clamps to the slices actually allocated (`arraySize`, always 2), so **2 — always legal — is the failure direction and a 1 is emitted only while the mode says 1-view**. Raising the count is safe content-wise: `GfxPopulateNextFrameDesc` fills both SPI slices / both MultiPass passes every frame whatever the mode, so slice 1 is never stale. A count/latch disagreement logs once (capped at 8 lines, never per frame). No behaviour change in the steady state of either mode.
- **`native~/displayxr_extensions.h`: `XR_DXR_DISPLAY_INFO_SPEC_VERSION` 12 → 19**, and the hand-written mirror now carries `XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR` (`1004999212`) with a note that the provider deliberately does **not** use it — Unity's topology is fixed at 2 views, so `PRIMARY_STEREO` is the right type and now reports exactly that. Documentation value only; nothing compiles against the constant today, and a future N-view/quilt path (ADR-007) is the only consumer it anticipates. The mirror is otherwise complete for everything the provider references — audited by diffing every `XR_*_DXR` identifier used in `native~/` against what the header defines.

## [2.19.3] - 2026-09-15

### Fixed
Expand Down
Binary file not shown.
28 changes: 27 additions & 1 deletion native~/displayxr_extensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ extern "C" {
#endif

// --- XR_DXR_display_info ---
// This header is a HAND-WRITTEN mirror of the runtime's
// src/external/openxr_includes/openxr/XR_DXR_display_info.h — it carries only the
// constants and structs the provider actually uses, so the version below tracks the
// runtime header it was last reconciled against, not everything that header defines.
// The runtime spells the macro XR_DXR_display_info_SPEC_VERSION; the mirror's
// all-caps spelling is kept for source compatibility with the provider TUs.
#define XR_DXR_DISPLAY_INFO_EXTENSION_NAME "XR_DXR_display_info"
#define XR_DXR_DISPLAY_INFO_SPEC_VERSION 12
#define XR_DXR_DISPLAY_INFO_SPEC_VERSION 19

#define XR_TYPE_DISPLAY_INFO_DXR ((XrStructureType)1004999003)

Expand All @@ -29,6 +35,26 @@ typedef struct XrDisplayInfoDXR {
uint32_t displayPixelHeight;
} XrDisplayInfoDXR;

// --- N-view view configuration (XR_DXR_display_info SPEC_VERSION 19) ---
// DisplayXR/displayxr-runtime#1486: PRIMARY_STEREO now reports EXACTLY 2 views and
// xrEndFrame under it accepts exactly 2 — or 1, but only while the ACTIVE rendering
// mode is itself 1-view AND the instance enabled XR_DXR_display_info. An app that
// wants the device's N-view (quad / light-field) modes enables XR_DXR_display_info,
// finds this type in xrEnumerateViewConfigurations, and begins its session with it.
//
// THIS PROVIDER DOES NOT USE IT, deliberately. Unity's stereo topology is fixed at 2
// (DXR_PROV_MAX_VIEWS, one arraySize=2 swapchain, two render passes/params), so the
// provider begins PRIMARY_STEREO (dxr_prov_poll_events, SESSION_STATE_READY) and gets
// the 2 views it can fill. The define is mirrored for documentation value — so the
// next reader does not have to go to the runtime to learn why PRIMARY_STEREO is a
// choice here rather than the only option — and so a future N-view/quilt render path
// (ADR-007) has the constant already in hand. It is a cast #define, not an
// enumerator, so it is invisible to -Wswitch: any switch over XrViewConfigurationType
// that must handle it needs an explicit case.
//
// Runtime reference: docs/reference/view-configuration-model.md.
#define XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MULTIVIEW_DXR ((XrViewConfigurationType)1004999212)

// --- Desktop position of the 3D panel (XR_DXR_display_info SPEC_VERSION 16) ---
// Chained onto XrSystemProperties (alongside XrDisplayInfoDXR) so a client can find
// out WHERE on the Windows virtual desktop the panel is, which XrDisplayInfoDXR does
Expand Down
48 changes: 45 additions & 3 deletions native~/displayxr_xrprovider/displayxr_provider_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6430,7 +6430,49 @@ int dxr_prov_submit_frame(uint32_t image_index)
}
s_ps.frame_begun = 0;

uint32_t submit_n = s_ps.sc_view_count >= 2 ? 2 : 1;
// How many views this frame's projection layer carries. Derived from the ACTIVE
// rendering mode HERE, at submit, NOT inherited from s_ps.sc_view_count.
//
// Why (runtime #1486): under XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO — which is
// what this provider begins with (dxr_prov_poll_events, SESSION_STATE_READY) —
// xrEndFrame now accepts exactly 2 views, or 1 ONLY while the active rendering mode
// is itself 1-view and the instance enabled XR_DXR_display_info (it always does).
// A 1-view submission in a 2-view mode is XR_ERROR_VALIDATION_FAILURE where it used
// to be silently flat-blitted. So 2 is always legal and 1 never is unless the mode
// says so — which makes the mode, not a latch, the only safe source for the count.
//
// s_ps.sc_view_count is a SHADOW of the mode latched at swapchain create
// (ps_create_swapchain) and refreshed only by ps_reconcile_primary(). That refresh
// is skippable and can lag by frames:
// - ps_reconcile_primary() early-returns on a 0-size target (minimised window,
// unresolved zone rect) BEFORE it ever compares the view count;
// - dxr_prov_reconcile_size() returns early while a frame is begun;
// - a failed ps_recreate_primary_swapchain() leaves the old value in place;
// - on macOS the mode event is polled from the MAIN thread
// (DisplayXRProviderDriver.LateUpdate) while this runs on the render thread.
// The mode read below is still the provider's cached active_mode_index, so the
// residual window is the event-delivery latency itself — but it is the freshest
// value we hold, and every path above that used to widen it is closed.
//
// Clamped to the slices we actually allocated (arraySize, always 2), so we can
// never submit more views than Unity rendered. Raising the count to 2 is always
// safe content-wise: the render topology is fixed at 2 (GfxPopulateNextFrameDesc
// always fills both SPI slices / both MultiPass passes) whatever the mode.
uint32_t submit_n = ps_active_view_count();
if (s_ps.sc_array > 0 && submit_n > s_ps.sc_array) submit_n = s_ps.sc_array;
if (submit_n < 1) submit_n = 1;
if (submit_n != s_ps.sc_view_count) {
// Transient by construction (the next reconcile reallocs the tile to match),
// but it is exactly the window that used to produce a rejected xrEndFrame —
// so record it, hard-capped so it can never become a per-frame log.
static unsigned s_vc_skew_logged = 0;
if (s_vc_skew_logged < 8) {
s_vc_skew_logged++;
ps_log("[DisplayXR-PROV] submit views %u (active mode) != swapchain latch %u "
"— submitting the mode's count (#1486)\n",
submit_n, s_ps.sc_view_count);
}
}
// D3D12: Unity rendered both eyes into the shared BRIDGE on its device (topology is
// fixed at 2 views). Copy the SUBMITTED slices into the acquired runtime swapchain
// image on our own device, fence-synced against Unity's render (shared fence below).
Expand Down Expand Up @@ -6614,8 +6656,8 @@ int dxr_prov_submit_frame(uint32_t image_index)
}
if (d11_diag) ps_log("[DisplayXR-PROV] D3D11 submit[%u]: released, building layers\n", d11_frames);

// Build the projection layer. Submit sc_view_count views (#172 P4): 2 in stereo 3D
// (array layers 0/1), 1 in hardware 2D (layer 0 only = a single full-res tile). The
// Build the projection layer. Submit the ACTIVE mode's view count (#172 P4, #1486):
// 2 in stereo 3D (array layers 0/1), 1 in hardware 2D (layer 0 only = a single tile). The
// runtime composites only the submitted views, so 2D shows one clean view (no weave
// ghost) even though Unity rendered both eyes into the 2-slice bridge.
uint32_t n = submit_n;
Expand Down
Loading