OpenXR startup: failures and session events
A complete C# controller, focus, event cleanup and independent pause policy.
This controller handles startup failure and OpenXR events while leaving game policy to your code. It does not change physics frequency, select display refresh rate, enable VRS or clear a global pause when focus returns.
First complete XR setup and prepare an action map. Save the code as XrStartup.cs and attach it to a separate XRSession Entity without a gameplay subtree. Use one controller per Viewport.
Complete script
using Reckfell;
public partial class XrStartup : Entity
{
[Signal] public delegate void FocusChangedEventHandler(bool focused);
[Signal] public delegate void SessionStoppedEventHandler();
[Signal] public delegate void PoseRecenteredEventHandler();
public string FailureReason { get; private set; } = "";
public bool HasFocus { get; private set; }
private OpenXRInterface? xr;
private Viewport? target;
private bool previousUseXr;
public override void _Ready()
{
ProcessMode = ProcessModeEnum.Always;
if (!ProjectSettings.GetSetting("xr/openxr/enabled").AsBool())
{
Fail("openxr-disabled"); return;
}
if (!ProjectSettings.GetSetting("xr/shaders/enabled").AsBool())
{
Fail("xr-shaders-disabled"); return;
}
xr = XRServer.FindInterface("OpenXR") as OpenXRInterface;
if (xr == null) { Fail("openxr-interface-unavailable"); return; }
xr.SessionVisible += OnNotFocused;
xr.SessionSynchronized += OnNotFocused;
xr.SessionFocussed += OnFocused;
xr.SessionStopping += OnStopped;
xr.SessionLossPending += OnStopped;
xr.InstanceExiting += OnStopped;
xr.PoseRecentered += OnRecentered;
if (!xr.IsInitialized() && !xr.Initialize())
{
Detach(); Fail("openxr-initialization-failed"); return;
}
target = GetViewport();
previousUseXr = target.UseXR;
target.UseXR = true;
SetFocus(xr.GetSessionState() == OpenXRInterface.SessionState.Focused);
}
private void Fail(string reason)
{
FailureReason = reason;
ReckfellApi.Print("XR startup: " + reason);
}
private void SetFocus(bool focused)
{
if (HasFocus == focused) return;
HasFocus = focused;
EmitSignal(SignalName.FocusChanged, focused);
}
private void OnFocused() => SetFocus(true);
private void OnNotFocused() => SetFocus(false);
private void OnStopped()
{
SetFocus(false);
EmitSignal(SignalName.SessionStopped);
}
private void OnRecentered() => EmitSignal(SignalName.PoseRecentered);
private void Detach()
{
if (xr == null) return;
xr.SessionVisible -= OnNotFocused;
xr.SessionSynchronized -= OnNotFocused;
xr.SessionFocussed -= OnFocused;
xr.SessionStopping -= OnStopped;
xr.SessionLossPending -= OnStopped;
xr.InstanceExiting -= OnStopped;
xr.PoseRecentered -= OnRecentered;
xr = null; // Borrowed from XRServer: do not Dispose/Free/Uninitialize it here.
}
public override void _ExitTree()
{
Detach();
if (target != null && ReckfellObject.IsInstanceValid(target))
target.UseXR = previousUseXr;
target = null; // Borrowed Viewport.
}
}Handle the result
Read FailureReason after _Ready(). An empty string means this controller recorded no configuration failure; determine input readiness separately through HasFocus.
| FailureReason | Action |
|---|---|
openxr-disabled | Enable xr/openxr/enabled, save and restart |
xr-shaders-disabled | Enable xr/shaders/enabled before startup |
openxr-interface-unavailable | Check that the build includes OpenXR and that the application starts correctly |
openxr-initialization-failed | Check active runtime, device connection and native initialization logs; check for --xr-mode off |
After failure, the script returns and leaves the Viewport in its previous mode. For a normal non-XR mode, show a useful message or load the corresponding game interface. Do not run XR locomotion from nonexistent poses. The controller does not automatically reconnect a lost session.
Focus and pause are different states
FocusChanged reports XR-focus changes only. Visible and Synchronized clear HasFocus; SessionFocussed sets it. Focussed matches the current C# signal spelling, while the enum value is SessionState.Focused. OpenXR state meanings.
Use HasFocus to gate game input. Focus loss can add a separate pause reason, but focus gain should remove only that reason. If a game menu already paused play, do not unconditionally set GetTree().Paused=false.
The controller uses ProcessMode.Always so it remains available during pause. Children using Inherit may inherit this behavior: do not put Gameplay under the controller. The setup hierarchy keeps Gameplay in a separate Pausable branch while the XR rig and necessary interface continue working. In Visible, do not stop XR output together with game interaction.
Stopping and recentering
SessionStopped forwards SessionStopping, SessionLossPending and InstanceExiting. The game handler must tolerate repeats: several events may request the same XR shutdown. Choose a policy: exit, ordinary mode or a restart prompt. This example neither calls Quit automatically nor manages native-session destruction.
PoseRecentered forwards a notification. It does not call CenterOnHmd automatically: additional recentering depends on origin, locomotion and selected space. Do not add head offsets again to an already tracked camera.
On leaving the tree, the controller unsubscribes and restores the previous UseXR. It assumes exclusive ownership of that flag on the selected Viewport. XRServer owns the interface: the script does not Dispose, Free or Uninitialize it. Changes to shared physics frequency, refresh rate, VRS or quality require separate measurement and are not mandatory startup steps.
Verification scope
With --xr-mode off, the installed engine compiled the complete file and executed disabled-setting and initialization-failure paths. Local focus/stop/recenter handlers were invoked synthetically: signals, deduplicated FocusChanged and preserved independent pause were checked. Physics frequency was unchanged.
This is not a check of real headset events. Successful initialization, subscriptions with a running runtime, UseXR restoration after successful startup, stereo output and tracking were not tested. On hardware, separately check startup, focus loss/regain, stopping, disconnection and removing the controller from the tree.