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
10 changed files with 245 additions and 0 deletions

View File

@@ -3591,6 +3591,9 @@
If [code]true[/code], support for the spatial entity extension is requested. If supported, you will be able to access spatial information about the real environment around you. What information is available is dependent on additional extensions.
[b]Note:[/b] This requires that the OpenXR spatial entities extension is supported by the XR runtime. If not supported this setting will be ignored.
</member>
<member name="xr/openxr/extensions/user_presence" type="bool" setter="" getter="" default="false">
If [code]true[/code], the user presence extension is enabled if available.
</member>
<member name="xr/openxr/form_factor" type="int" setter="" getter="" default="&quot;0&quot;">
Specify whether OpenXR should be configured for an HMD or a hand held device.
</member>

View File

@@ -2827,6 +2827,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/extensions/spatial_entity/april_tag_dict", PROPERTY_HINT_ENUM, "4x4H5,5x5H9,6x6H10,6x6H11"), "3");
GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/eye_gaze_interaction", false);
GLOBAL_DEF_BASIC("xr/openxr/extensions/render_model", false);
GLOBAL_DEF_BASIC("xr/openxr/extensions/user_presence", false);
// OpenXR Binding modifier settings
GLOBAL_DEF_BASIC("xr/openxr/binding_modifiers/analog_threshold", false);

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));