XR session setup: StartXR, pause on focus loss, XR shaders #15

Open
hiccup444 wants to merge 2 commits from hiccup444/da-game-hiccup:xr-session-setup into main
First-time contributor

Summary

Game._init_xr() only switched the viewport to XR. It did none of the session setup that Godot's XR docs treat as required:

  • Physics ran at the default 60 Hz while the headset renders at 72 to 120 Hz. XR Tools moves the player by moving the XR origin in _physics_process, so at 90 Hz only two of every three rendered frames see the origin move. Smooth locomotion and smooth turning judder.
  • V-Sync stayed on, so the swap interval of the desktop mirror window took part in frame pacing.
  • Nothing reacted when the runtime took focus away (dashboard open, headset off). The world kept running while controller input was gone.
  • xr/shaders/enabled was off, though Godot's XR setup guide lists it as a required step.

The vendored XR Tools already ships the fix, XRToolsStartXR, and this PR uses it.

Changes

  • scenes/game.tscn: res://addons/godot-xr-tools/xr/start_xr.tscn is added as the first child of Game, with default settings.
    • Children are ready before their parent, so StartXR's _ready runs before Game._ready (checked on 4.6.3).
    • In its _ready, once the OpenXR interface is up, StartXR turns V-Sync off.
    • On session_begun it sets Engine.physics_ticks_per_second from the runtime's refresh rate.
  • src/core/game.gd: adds an @onready reference to StartXR. A new _on_xr_focus_lost() handles its xr_ended signal by calling open_pause().
    • open_pause() already ignores MENU and an open pause. So focus loss pauses GAME and SANDBOX, and it never toggles the panel shut the way the pause button does.
    • There is deliberately no resume on xr_started. Controllers and hand tracking come back all at once when focus returns, so the player resumes from the panel instead of landing mid-action.
    • StartXR only emits xr_ended after it has seen the focused state. So the visible state the session passes through at startup does not pause anything.
    • The pause section's header comment says the input system decides how pause is triggered. It now also notes that Game itself opens the pause menu on focus loss.
    • _init_xr() stays as the switch between XR and flat mode.
  • project.godot: sets xr/shaders/enabled=true and removes the two driver/driver_name.*="vulkan" lines under [rendering].
    • 14dc157 added that pin to keep Forward+ off D3D12, where OpenXR could not create a session.
    • The two lines did nothing. The renderer has been gl_compatibility since f4ab9fb, and Godot 4.6.3 does not register a rendering/driver/driver_name setting (checked in a clean project, where no such key exists).
    • If the project ever moves to Mobile or Forward+, the 4.x key for that pin is rendering/rendering_device/driver.windows. Its engine default on 4.6.3 is vulkan.
  • No docs change: nothing in docs/ or README.md describes XR init, the physics rate, V-Sync or the Vulkan pin.

Left out on purpose, as a design question for you: should quitting from the runtime save the run the way Return to Menu does? That would mean OpenXR session_stopping, plus NOTIFICATION_WM_CLOSE_REQUEST on desktop. The answer depends on the save design:

  • what a quit means for a run;
  • whether it should respect save_blocked in the traversal region;
  • whether a quit from the dashboard counts as a deliberate exit.

Tests

New checks:

  • smoke_menu:
    • "XR shaders enabled": xr/shaders/enabled is true.
    • "game scene has StartXR": Game has a StartXR child that is an XRToolsStartXR.
  • pause_check, using a guard that StartXR is in the game scene:
    • Emitting StartXR's xr_ended in GAME opens the pause menu with the panel visible.
    • A second xr_ended leaves it paused.
    • Emitting xr_started after that does not resume.
    • In MENU, xr_ended does nothing.
    • In SANDBOX, xr_ended opens the pause menu with the panel visible.

Each new check fails on main or under a targeted break:

  • On main (no StartXR, setting off): "XR shaders enabled", "game scene has StartXR" and the StartXR guard fail. The focus checks sit behind the guard.
  • With the xr_ended connection removed: the GAME, still paused, focus return and SANDBOX checks fail.
  • With a handler that toggles: the still paused and focus return checks fail.
  • With a resume on xr_started: "focus return does not resume" fails.
  • With the MENU guard removed from open_pause(): "focus loss in MENU does nothing" fails.
  • With the pause panel shown only in GAME: the SANDBOX check fails.

Full suites: smoke_menu, arms_check, addon_check, save_check, rig_motion_check, pause_check and input_check all PASS. That run was local, on Windows, with --headless --xr-mode off. CI covers Linux, where OpenXR is enabled but has no runtime.

New in the logs:

  • Every suite that boots game.tscn now prints one ERROR: OpenXR: Failed to initialize from StartXR, with its GDScript backtrace. Any desktop run without a headset prints it too. It is expected (see the corrections below). It has no uppercase FAIL in it, so the CI grep is unaffected.
  • pause_check prints five more of the known harness lines "Invalid assignment of property or key 'enabled' ... Nil", one for each extra set_anchored call in the new steps.

Corrections to the review

  • What StartXR does without a runtime. The review said StartXR "only logs that no XR interface was found". In fact the OpenXR interface is registered even when OpenXR did not start; it is just uninitialized. StartXR then:

    • prints OpenXR: Configuring interface and OpenXR: Initializing interface;
    • calls initialize(), which returns false without an engine message;
    • logs ERROR: OpenXR: Failed to initialize and emits xr_failed_to_initialize.

    On that path V-Sync and the physics rate are left alone, and the game falls back to flat mode as before. This was observed on 4.6.3 with --xr-mode off. Judging from the engine source, CI (Linux, xr/openxr/enabled=true, no runtime) takes the same path:

    • The OpenXR module's startup init fails. The module warns as it does today and drops the API.
    • The interface is still added to the XRServer at the scene init level.
    • OpenXRInterface::initialize() returns false when there is no API.
  • Who initializes the interface. The review said _init_xr() can stay because StartXR "has initialized the interface by the time it runs". In practice the engine already initializes the interface at startup whenever OpenXR started. StartXR's own initialize() only runs when OpenXR is off or failed, and then it fails. What StartXR adds is the session setup (V-Sync, physics rate) and the focus signals, which is what this PR uses it for. _init_xr() stays as the flat-mode switch, as proposed.

Left for in-headset checking

  • Smooth-locomotion judder at 90 Hz: walk in a straight line, before and after this change.
  • Open the SteamVR dashboard mid-game. On return the pause panel is still up, and the game stays paused until Resume.
  • Once the session begins, the log shows StartXR: Setting physics rate to N (the headset's rate).
  • With XR shaders on, both eyes still render as before.

Overlap with open PRs

  • #11 also edits src/core/game.gd and scenes/game.tscn. Here StartXR goes in as the first child, and the @onready line sits above _slot. The new sentence in the pause header comment stays clear of #11's _active_world() hunk. So this merges with #11 as it stands, without conflicts.
  • #11's open_pause() supports pausing mid-traversal. Focus loss goes through open_pause(), so it covers the traversal region once both land.

Sibling PRs from the same review: #16, #17, #18 and #19. Their source changes merge cleanly with each other. Several of them add checks at the same spot in tests/pause_check.gd (and two in tests/save_check.gd), so whichever merges second may need a small rebase that keeps both blocks.

## Summary `Game._init_xr()` only switched the viewport to XR. It did none of the session setup that Godot's XR docs treat as required: - Physics ran at the default 60 Hz while the headset renders at 72 to 120 Hz. XR Tools moves the player by moving the XR origin in `_physics_process`, so at 90 Hz only two of every three rendered frames see the origin move. Smooth locomotion and smooth turning judder. - V-Sync stayed on, so the swap interval of the desktop mirror window took part in frame pacing. - Nothing reacted when the runtime took focus away (dashboard open, headset off). The world kept running while controller input was gone. - `xr/shaders/enabled` was off, though Godot's XR setup guide lists it as a required step. The vendored XR Tools already ships the fix, `XRToolsStartXR`, and this PR uses it. ## Changes - `scenes/game.tscn`: `res://addons/godot-xr-tools/xr/start_xr.tscn` is added as the first child of `Game`, with default settings. - Children are ready before their parent, so StartXR's `_ready` runs before `Game._ready` (checked on 4.6.3). - In its `_ready`, once the OpenXR interface is up, StartXR turns V-Sync off. - On `session_begun` it sets `Engine.physics_ticks_per_second` from the runtime's refresh rate. - `src/core/game.gd`: adds an `@onready` reference to StartXR. A new `_on_xr_focus_lost()` handles its `xr_ended` signal by calling `open_pause()`. - `open_pause()` already ignores MENU and an open pause. So focus loss pauses GAME and SANDBOX, and it never toggles the panel shut the way the pause button does. - There is deliberately no resume on `xr_started`. Controllers and hand tracking come back all at once when focus returns, so the player resumes from the panel instead of landing mid-action. - StartXR only emits `xr_ended` after it has seen the focused state. So the visible state the session passes through at startup does not pause anything. - The pause section's header comment says the input system decides how pause is triggered. It now also notes that Game itself opens the pause menu on focus loss. - `_init_xr()` stays as the switch between XR and flat mode. - `project.godot`: sets `xr/shaders/enabled=true` and removes the two `driver/driver_name.*="vulkan"` lines under `[rendering]`. - 14dc157 added that pin to keep Forward+ off D3D12, where OpenXR could not create a session. - The two lines did nothing. The renderer has been `gl_compatibility` since f4ab9fb, and Godot 4.6.3 does not register a `rendering/driver/driver_name` setting (checked in a clean project, where no such key exists). - If the project ever moves to Mobile or Forward+, the 4.x key for that pin is `rendering/rendering_device/driver.windows`. Its engine default on 4.6.3 is `vulkan`. - No docs change: nothing in `docs/` or `README.md` describes XR init, the physics rate, V-Sync or the Vulkan pin. Left out on purpose, as a design question for you: should quitting from the runtime save the run the way Return to Menu does? That would mean OpenXR `session_stopping`, plus `NOTIFICATION_WM_CLOSE_REQUEST` on desktop. The answer depends on the save design: - what a quit means for a run; - whether it should respect `save_blocked` in the traversal region; - whether a quit from the dashboard counts as a deliberate exit. ## Tests New checks: - `smoke_menu`: - "XR shaders enabled": `xr/shaders/enabled` is true. - "game scene has StartXR": `Game` has a `StartXR` child that is an `XRToolsStartXR`. - `pause_check`, using a guard that StartXR is in the game scene: - Emitting StartXR's `xr_ended` in GAME opens the pause menu with the panel visible. - A second `xr_ended` leaves it paused. - Emitting `xr_started` after that does not resume. - In MENU, `xr_ended` does nothing. - In SANDBOX, `xr_ended` opens the pause menu with the panel visible. Each new check fails on `main` or under a targeted break: - On `main` (no StartXR, setting off): "XR shaders enabled", "game scene has StartXR" and the StartXR guard fail. The focus checks sit behind the guard. - With the `xr_ended` connection removed: the GAME, still paused, focus return and SANDBOX checks fail. - With a handler that toggles: the still paused and focus return checks fail. - With a resume on `xr_started`: "focus return does not resume" fails. - With the MENU guard removed from `open_pause()`: "focus loss in MENU does nothing" fails. - With the pause panel shown only in GAME: the SANDBOX check fails. Full suites: smoke_menu, arms_check, addon_check, save_check, rig_motion_check, pause_check and input_check all PASS. That run was local, on Windows, with `--headless --xr-mode off`. CI covers Linux, where OpenXR is enabled but has no runtime. New in the logs: - Every suite that boots `game.tscn` now prints one `ERROR: OpenXR: Failed to initialize` from StartXR, with its GDScript backtrace. Any desktop run without a headset prints it too. It is expected (see the corrections below). It has no uppercase FAIL in it, so the CI grep is unaffected. - pause_check prints five more of the known harness lines "Invalid assignment of property or key 'enabled' ... Nil", one for each extra `set_anchored` call in the new steps. ## Corrections to the review - **What StartXR does without a runtime.** The review said StartXR "only logs that no XR interface was found". In fact the OpenXR interface is registered even when OpenXR did not start; it is just uninitialized. StartXR then: - prints `OpenXR: Configuring interface` and `OpenXR: Initializing interface`; - calls `initialize()`, which returns false without an engine message; - logs `ERROR: OpenXR: Failed to initialize` and emits `xr_failed_to_initialize`. On that path V-Sync and the physics rate are left alone, and the game falls back to flat mode as before. This was observed on 4.6.3 with `--xr-mode off`. Judging from the engine source, CI (Linux, `xr/openxr/enabled=true`, no runtime) takes the same path: - The OpenXR module's startup init fails. The module warns as it does today and drops the API. - The interface is still added to the XRServer at the scene init level. - `OpenXRInterface::initialize()` returns false when there is no API. - **Who initializes the interface.** The review said `_init_xr()` can stay because StartXR "has initialized the interface by the time it runs". In practice the engine already initializes the interface at startup whenever OpenXR started. StartXR's own `initialize()` only runs when OpenXR is off or failed, and then it fails. What StartXR adds is the session setup (V-Sync, physics rate) and the focus signals, which is what this PR uses it for. `_init_xr()` stays as the flat-mode switch, as proposed. ## Left for in-headset checking - Smooth-locomotion judder at 90 Hz: walk in a straight line, before and after this change. - Open the SteamVR dashboard mid-game. On return the pause panel is still up, and the game stays paused until Resume. - Once the session begins, the log shows `StartXR: Setting physics rate to N` (the headset's rate). - With XR shaders on, both eyes still render as before. ## Overlap with open PRs - #11 also edits `src/core/game.gd` and `scenes/game.tscn`. Here StartXR goes in as the first child, and the `@onready` line sits above `_slot`. The new sentence in the pause header comment stays clear of #11's `_active_world()` hunk. So this merges with #11 as it stands, without conflicts. - #11's `open_pause()` supports pausing mid-traversal. Focus loss goes through `open_pause()`, so it covers the traversal region once both land. Sibling PRs from the same review: #16, #17, #18 and #19. Their source changes merge cleanly with each other. Several of them add checks at the same spot in `tests/pause_check.gd` (and two in `tests/save_check.gd`), so whichever merges second may need a small rebase that keeps both blocks.
Game only switched the viewport to XR. Physics stayed at the default
60 Hz while the headset renders at 72 to 120 Hz, so smooth locomotion
and turning judder (XR Tools moves the origin in _physics_process),
and V-Sync stayed on. Nothing reacted to the runtime taking focus
away, so the world kept running behind the dashboard.

The vendored StartXR, now the first child of Game, turns V-Sync off
as soon as the OpenXR interface is up, and sets the physics rate from
the runtime's refresh rate when the session begins. Its xr_ended
signal (dashboard open, headset off) now opens the pause menu. There
is no resume on xr_started: the player resumes from the panel instead
of landing mid-action.

Without a headset StartXR finds the OpenXR interface uninitialized,
its initialize() call fails, and it logs "OpenXR: Failed to
initialize"; _init_xr still puts the game in flat mode as before.
project: enable XR shaders, drop the stale driver keys
Some checks failed
tests / test (pull_request) Failing after 1m9s
d7a8fd8b66
Godot's XR setup guide lists XR > Shaders as a required setting, and it
was off.

driver/driver_name.windows and .linuxbsd under [rendering] were added
to keep Forward+ off D3D12, where OpenXR could not create a session.
rendering/driver/driver_name is not a setting Godot 4.6 registers, and
the renderer is gl_compatibility now, so the two lines did nothing. If
the project moves to Mobile or Forward+, the 4.x key for the pin is
rendering/rendering_device/driver.windows.
Some checks failed
tests / test (pull_request) Failing after 1m9s
This pull request has changes conflicting with the target branch.
  • project.godot
  • scenes/game.tscn
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u xr-session-setup:hiccup444-xr-session-setup
git switch hiccup444-xr-session-setup

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff hiccup444-xr-session-setup
git switch hiccup444-xr-session-setup
git rebase main
git switch main
git merge --ff-only hiccup444-xr-session-setup
git switch hiccup444-xr-session-setup
git rebase main
git switch main
git merge --no-ff hiccup444-xr-session-setup
git switch main
git merge --squash hiccup444-xr-session-setup
git switch main
git merge --ff-only hiccup444-xr-session-setup
git switch main
git merge hiccup444-xr-session-setup
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine!15
No description provided.