Merge pull request #115190 from ashwinvbs/xr_ext_user_presence

Implement support for `XR_EXT_user_presence` extension
This commit is contained in:
Thaddeus Crews 2026-01-28 17:58:26 -06:00
commit 5ad8b27d8d
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
10 changed files with 245 additions and 0 deletions

View file

@ -75,6 +75,7 @@ def get_doc_classes():
"OpenXRSpatialPlaneTrackingCapability",
"OpenXRSpatialMarkerTrackingCapability",
"OpenXRAndroidThreadSettingsExtension",
"OpenXRUserPresenceExtension",
]

View file

@ -126,6 +126,19 @@
[b]Note:[/b] This only returns a valid value after OpenXR has been initialized.
</description>
</method>
<method name="is_user_presence_supported" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if OpenXR's user presence extension is supported and enabled.
[b]Note:[/b] This only returns a valid value after OpenXR has been initialized.
</description>
</method>
<method name="is_user_present" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if system has detected the presence of a user in the XR experience.
</description>
</method>
<method name="set_action_set_active">
<return type="void" />
<param index="0" name="name" type="String" />
@ -245,6 +258,13 @@
Informs our OpenXR session is now visible, for example output is sent to the HMD but we don't receive XR input.
</description>
</signal>
<signal name="user_presence_changed">
<param index="0" name="is_user_present" type="bool" />
<description>
Signal emitted when the user presence value changes.
[b]Note:[/b] This signal will not be emitted during application startup and application shutdown. Developers should assume user presence is gained on startup and lost on shutdown.
</description>
</signal>
</signals>
<constants>
<constant name="SESSION_STATE_UNKNOWN" value="0" enum="SessionState">

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="OpenXRUserPresenceExtension" inherits="OpenXRExtensionWrapper" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
This class implements the OpenXR User Presence Extension.
</brief_description>
<description>
This class implements the OpenXR User Presence Extension.
</description>
<tutorials>
</tutorials>
</class>

View file

@ -0,0 +1,100 @@
/**************************************************************************/
/* openxr_user_presence_extension.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "openxr_user_presence_extension.h"
#include "../openxr_interface.h"
#include "core/config/project_settings.h"
OpenXRUserPresenceExtension *OpenXRUserPresenceExtension::singleton = nullptr;
OpenXRUserPresenceExtension *OpenXRUserPresenceExtension::get_singleton() {
return singleton;
}
OpenXRUserPresenceExtension::OpenXRUserPresenceExtension() {
singleton = this;
}
OpenXRUserPresenceExtension::~OpenXRUserPresenceExtension() {
singleton = nullptr;
}
HashMap<String, bool *> OpenXRUserPresenceExtension::get_requested_extensions(XrVersion p_version) {
HashMap<String, bool *> request_extensions;
if (GLOBAL_GET("xr/openxr/extensions/user_presence")) {
request_extensions[XR_EXT_USER_PRESENCE_EXTENSION_NAME] = &available;
}
return request_extensions;
}
void *OpenXRUserPresenceExtension::set_system_properties_and_get_next_pointer(void *p_next_pointer) {
if (!available) {
return p_next_pointer;
}
properties.type = XR_TYPE_SYSTEM_USER_PRESENCE_PROPERTIES_EXT;
properties.next = p_next_pointer;
properties.supportsUserPresence = false;
return &properties;
}
bool OpenXRUserPresenceExtension::is_active() const {
return available && properties.supportsUserPresence;
}
void OpenXRUserPresenceExtension::on_state_ready() {
user_present = true;
}
void OpenXRUserPresenceExtension::on_state_stopping() {
user_present = false;
}
bool OpenXRUserPresenceExtension::on_event_polled(const XrEventDataBuffer &event) {
if (!is_active() || event.type != XR_TYPE_EVENT_DATA_USER_PRESENCE_CHANGED_EXT) {
return false;
}
const XrEventDataUserPresenceChangedEXT *user_presence_changed_event = (XrEventDataUserPresenceChangedEXT *)&event;
if (user_present != (bool)user_presence_changed_event->isUserPresent) {
user_present = user_presence_changed_event->isUserPresent;
OpenXRInterface *xr_interface = OpenXRAPI::get_singleton()->get_xr_interface();
if (xr_interface) {
xr_interface->emit_signal(SNAME("user_presence_changed"), user_present);
}
}
return true;
}
bool OpenXRUserPresenceExtension::is_user_present() const {
return user_present;
}

View file

@ -0,0 +1,71 @@
/**************************************************************************/
/* openxr_user_presence_extension.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "openxr_extension_wrapper.h"
// When supported, the user presence extension allows an application to detect and respond to change
// of user presence, such as when the user has taken off or put on an XR headset.
//
// See: https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#XR_EXT_user_presence
// for more information.
class OpenXRUserPresenceExtension : public OpenXRExtensionWrapper {
GDCLASS(OpenXRUserPresenceExtension, OpenXRExtensionWrapper);
protected:
static void _bind_methods() {}
public:
static OpenXRUserPresenceExtension *get_singleton();
OpenXRUserPresenceExtension();
virtual ~OpenXRUserPresenceExtension() override;
virtual HashMap<String, bool *> get_requested_extensions(XrVersion p_version) override;
virtual void *set_system_properties_and_get_next_pointer(void *p_next_pointer) override;
virtual void on_state_ready() override;
virtual void on_state_stopping() override;
virtual bool on_event_polled(const XrEventDataBuffer &event) override;
bool is_user_present() const;
bool is_active() const;
private:
static OpenXRUserPresenceExtension *singleton;
bool available = false;
XrSystemUserPresencePropertiesEXT properties;
bool user_present = true;
};

View file

@ -36,6 +36,7 @@
#include "extensions/openxr_eye_gaze_interaction.h"
#include "extensions/openxr_hand_interaction_extension.h"
#include "extensions/openxr_performance_settings_extension.h"
#include "extensions/openxr_user_presence_extension.h"
#include "servers/rendering/renderer_compositor.h"
#include <openxr/openxr.h>
@ -58,6 +59,12 @@ void OpenXRInterface::_bind_methods() {
// State
ClassDB::bind_method(D_METHOD("get_session_state"), &OpenXRInterface::get_session_state);
// User presence
ADD_SIGNAL(MethodInfo("user_presence_changed", PropertyInfo(Variant::BOOL, "is_user_present")));
ClassDB::bind_method(D_METHOD("is_user_presence_supported"), &OpenXRInterface::is_user_presence_supported);
ClassDB::bind_method(D_METHOD("is_user_present"), &OpenXRInterface::is_user_present);
// Display refresh rate
ClassDB::bind_method(D_METHOD("get_display_refresh_rate"), &OpenXRInterface::get_display_refresh_rate);
ClassDB::bind_method(D_METHOD("set_display_refresh_rate", "refresh_rate"), &OpenXRInterface::set_display_refresh_rate);
@ -1448,6 +1455,26 @@ OpenXRInterface::SessionState OpenXRInterface::get_session_state() {
return SESSION_STATE_UNKNOWN;
}
/** User Presence. */
bool OpenXRInterface::is_user_presence_supported() const {
if (!openxr_api || !openxr_api->is_initialized()) {
return false;
} else {
OpenXRUserPresenceExtension *user_presence_ext = OpenXRUserPresenceExtension::get_singleton();
return user_presence_ext && user_presence_ext->is_active();
}
}
bool OpenXRInterface::is_user_present() const {
// If extension is unavailable or unsupported, we default to user is present.
if (!is_user_presence_supported()) {
return true;
} else {
OpenXRUserPresenceExtension *user_presence_ext = OpenXRUserPresenceExtension::get_singleton();
return user_presence_ext->is_user_present();
}
}
/** Hand tracking. */
void OpenXRInterface::set_motion_range(const Hand p_hand, const HandMotionRange p_motion_range) {
ERR_FAIL_INDEX(p_hand, HAND_MAX);

View file

@ -232,6 +232,10 @@ public:
SessionState get_session_state();
/** User presence. */
bool is_user_presence_supported() const;
bool is_user_present() const;
/** Hand tracking. */
enum Hand {
HAND_LEFT,

View file

@ -80,6 +80,7 @@
#ifdef MODULE_GLTF_ENABLED
#include "extensions/openxr_render_model_extension.h"
#endif
#include "extensions/openxr_user_presence_extension.h"
#include "extensions/openxr_valve_analog_threshold_extension.h"
#include "extensions/openxr_valve_controller_extension.h"
#include "extensions/openxr_visibility_mask_extension.h"
@ -142,6 +143,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(OpenXRRenderModelExtension);
#endif
GDREGISTER_CLASS(OpenXRAndroidThreadSettingsExtension);
GDREGISTER_CLASS(OpenXRUserPresenceExtension);
// Note, we're not registering all wrapper classes here, there is no point in exposing them
// if there isn't specific logic to expose.
@ -214,6 +216,11 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
OpenXRAPI::register_extension_wrapper(android_thread_settings);
Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRAndroidThreadSettingsExtension", android_thread_settings));
// Register user presence extension as a singleton
OpenXRUserPresenceExtension *user_presence_extension = memnew(OpenXRUserPresenceExtension);
OpenXRAPI::register_extension_wrapper(user_presence_extension);
Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRUserPresenceExtension", user_presence_extension));
// register gated extensions
if (int(GLOBAL_GET("xr/openxr/extensions/debug_utils")) > 0) {
OpenXRAPI::register_extension_wrapper(memnew(OpenXRDebugUtilsExtension));