Skip to content

feat(test_apps): macOS cube apps ease 2D/3D through dxr::ModeSwitch::step(); linter INV-2.9 - #1904

Merged
dfattal merged 1 commit into
mainfrom
feat/macos-mode-switch-ease
Oct 12, 2026
Merged

dfattal merged 1 commit into
mainfrom
feat/macos-mode-switch-ease

Conversation

@dfattal

@dfattal dfattal commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Ask

David: "ease 2D<->3D transitions (mode switching) the same way we do on Windows; that's valid for all apps and should be clear in displayxr-common."

What Windows actually does (researched)

The easing is entirely app-side:

  • Runtime: switches a rendering mode in one frame (atlas recipe, DP weave-vs-flat, lens; ADR-028). It has no ramp, fade or lens-settle hold. Its only aid is the one-frame XR_EXT_display_info: rendering-mode switch doesn't smoothly honor per-frame view-rig IPD (output snaps) + no completion event #615 eye-set coherence guard in oxr_session.c, which is platform-neutral. The runtime-owned V key (qwerty, used by hosted/legacy apps) toggles instantly on every OS.
  • Leia plug-in: no fade on any arm. request_display_mode flips the lens on the frame the mode lands (Windows SwitchableLensHint, macOS/Linux srLensEnable/Disable).
  • App: the Windows cube_handle_* apps call displayxr-common's XrSessionUpdateModeSwitch(), which drives dxr::ModeSwitch: ramp ipdFactor to 0, then request 2D; request 3D, then ramp up over 0.18 s.

The macOS gap

The macOS cube_handle_{metal,vk,gl} apps (and the modelviewer demo) already eased, through hand-copied 40-line versions of that glue. XrSessionUpdateModeSwitch takes the Windows-only XrSessionManager, so they could not call it. I measured the copies on sim-display and they work. The real gaps:

  1. The glue was Windows-only, and nothing in displayxr-common explained the contract.
  2. The four macOS zones apps never wired it and snapped. So do the six Windows zones apps and cube_handle_vk_linux (not touched here, now flagged by the linter).

Changes

  • cube_handle_{metal,vk,gl}_macos: the inline copies become one call to the new dxr::ModeSwitch::step() (feat(mode_switch): one-call ModeSwitch::step() for every platform; document the 2D/3D transition contract displayxr-common#73).
  • cube_zones_{metal,vk}macos, cube_zones_texture{metal,gl}_macos: adopt it. Zones carry per-zone ipdFactor (1.0 / 0.6), so the sequencer runs as a 0..1 scale (steadyIpd = 1) that multiplies every rig's ipdFactor.
  • test_apps/common/mode_switch_test_knob.h: with DXR_TEST_MODE_TOGGLE_S=<s>, those apps toggle 3D<->2D through the sequencer every <s> seconds and log each ramp frame (ModeSwitch trace:), so the ramp can be checked without synthesizing key presses.
  • Linter INV-2.9 (advisory, per leg): an app that requests rendering modes but never references ModeSwitch is flagged. Hermetic tests are in scripts/tests/test_check_displayxr_app_mode_switch.py, run by lint.yml.
  • Docs: INV-2.9 in displayxr-app-rules.md; a new section in XR_DXR_display_info.md, "Smooth transitions are the application's job".

Merge order (DRAFT)

displayxr-common is pinned to the #73 commit (5d71fdb, GIT_SHALLOW FALSE, because a SHA cannot be fetched shallow) in test_apps/common and the 7 touched apps. Merge and tag common #73 first, then re-pin these 8 GIT_TAGs to that tag (GIT_SHALLOW TRUE), then merge this PR.

Overlap with open drafts:

No src/ changes here, so there is no overlap with oxr_session* or comp_metal.

Evidence (sim-display, anaglyph, built-in display; Apple M1 Pro)

Toggles were scripted with DXR_TEST_MODE_TOGGLE_S against this branch's runtime and apps. cube_handle_metal_macos trace (every 4th frame):

ModeSwitch trace: ipd=0.9964 fire=0 mode=0 current=1   <- 3D->2D requested: ramp down first
ModeSwitch trace: ipd=0.8245 ...
ModeSwitch trace: ipd=0.4074 ...
ModeSwitch trace: ipd=0.0043 ...
OXR EVENT: Rendering mode changed 1 -> 0
ModeSwitch trace: ipd=0.0000 fire=1 mode=0             <- 2D requested only on the flat frame
OXR EVENT: Rendering mode changed 0 -> 1
ModeSwitch trace: ipd=0.0144 fire=1 mode=1 current=0   <- 2D->3D requested FIRST, flat
ModeSwitch trace: ipd=0.2599 / 0.5797 / 0.8133 / 0.9747 <- then ramp up over ~0.18 s
app compositor toggles ramp traces
cube_handle_metal_macos Metal native 2 50
cube_handle_vk_macos vk_native (120 Hz, dt 8.3 ms) 5 88
cube_handle_gl_macos GL 3 63
cube_zones_metal_macos Metal native 1 23
cube_zones_vk_macos vk_native 2 12
cube_zones_texture_metal_macos Metal 4 23
cube_zones_texture_gl_macos GL 4 63

The fresh fetch of the pinned SHA builds (aggregate configure, no local override). Linter: the 7 macOS apps are clean; all 6 tests in the new linter suite pass, and the INV-4.6 suite still passes.

Not run: Windows, Linux, the Leia panel. VK-via-Metal (#1900) is not exercised here: cube_handle_vk_macos ran on vk_native.

Eyeball on the Leia panel (David)

Run this branch's built runtime and app with no XRT_PLUGIN_SEARCH_PATH override, so the installed Leia plug-in is discovered. Then drag the window onto the Odyssey and press V, or let the knob toggle every 3 s:

cd ~/Documents/GitHub/displayxr-runtime/.claude/worktrees/agent-a90d93089fe86c4b0
XR_RUNTIME_JSON=$PWD/build/openxr_displayxr-dev.json DYLD_LIBRARY_PATH=/tmp/openxr-install-1.1.63/lib \
  DXR_TEST_MODE_TOGGLE_S=3 ./test_apps/build/bin/cube_handle_metal_macos

Expected: 3D->2D, the depth flattens over about 0.2 s, then the lens drops. 2D->3D, the lens rises on a flat image, then the depth grows in. For an A/B, cube_hosted_metal_macos uses the runtime-owned V and still switches instantly by design.

🤖 Generated with Claude Code

… dxr::ModeSwitch::step()

The runtime switches a rendering mode in one frame (ADR-028) and never ramps
the stereo disparity, so the smooth Windows V-key transition is app-side:
displayxr-common's dxr::ModeSwitch, driven by the Windows-only
XrSessionUpdateModeSwitch. The macOS cube_handle apps carried hand-copied
40-line versions of that glue, and the four macOS zones apps (and the Windows
zones apps and cube_handle_vk_linux) never wired it, so they snapped.

- cube_handle_{metal,vk,gl}_macos: the inline copies become one call to the
  new platform-neutral ModeSwitch::step() (displayxr-common#73).
- cube_zones_{metal,vk}_macos, cube_zones_texture_{metal,gl}_macos: adopt it.
  Zones carry per-zone ipdFactor (1.0 / 0.6), so the sequencer runs as a 0..1
  scale (steadyIpd = 1) that multiplies every rig's ipdFactor.
- test_apps/common/mode_switch_test_knob.h: DXR_TEST_MODE_TOGGLE_S=<s> makes
  those apps toggle 3D<->2D through the sequencer every <s> seconds and log
  each ramp frame ("ModeSwitch trace:"), so the ramp can be verified without
  synthesizing key presses.
- displayxr-common pinned to the #73 commit (GIT_SHALLOW FALSE: a SHA cannot be
  fetched shallow). DRAFT: re-pin to the release tag that carries #73 before
  merging.
- check_displayxr_app.py INV-2.9 (advisory, per leg): an app that requests
  rendering modes but never references ModeSwitch is flagged. Hermetic unit
  tests in scripts/tests/test_check_displayxr_app_mode_switch.py, run by
  lint.yml. Still flagged after this change: the six Windows zones apps and
  cube_handle_vk_linux (not touched here).
- docs: INV-2.9 in displayxr-app-rules.md; XR_DXR_display_info.md
  "Smooth transitions are the application's job".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal force-pushed the feat/macos-mode-switch-ease branch from 9080639 to c7fb3b1 Compare October 12, 2026 03:29
@dfattal
dfattal marked this pull request as ready for review October 12, 2026 03:52
@dfattal
dfattal requested a review from a team as a code owner October 12, 2026 03:52
@dfattal
dfattal merged commit f17e599 into main Oct 12, 2026
36 checks passed
@dfattal
dfattal deleted the feat/macos-mode-switch-ease branch October 12, 2026 03:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant