From a182e182115f5dc3b4d37dcdc0b76a1a4b88b116 Mon Sep 17 00:00:00 2001 From: Ashwin Balasubramaniyan Date: Tue, 6 Jan 2026 15:57:21 +0000 Subject: [PATCH] Implement support for XR_EXT_user_presence extension --- doc/classes/ProjectSettings.xml | 3 + main/main.cpp | 1 + modules/openxr/config.py | 1 + .../openxr/doc_classes/OpenXRInterface.xml | 20 ++++ .../OpenXRUserPresenceExtension.xml | 11 ++ .../openxr_user_presence_extension.cpp | 100 ++++++++++++++++++ .../openxr_user_presence_extension.h | 71 +++++++++++++ modules/openxr/openxr_interface.cpp | 27 +++++ modules/openxr/openxr_interface.h | 4 + modules/openxr/register_types.cpp | 7 ++ 10 files changed, 245 insertions(+) create mode 100644 modules/openxr/doc_classes/OpenXRUserPresenceExtension.xml create mode 100644 modules/openxr/extensions/openxr_user_presence_extension.cpp create mode 100644 modules/openxr/extensions/openxr_user_presence_extension.h diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 2bd6df9015d..7d146df6e97 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -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. + + If [code]true[/code], the user presence extension is enabled if available. + Specify whether OpenXR should be configured for an HMD or a hand held device. diff --git a/main/main.cpp b/main/main.cpp index 064c20dab44..6e5b3bd3486 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -2819,6 +2819,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); diff --git a/modules/openxr/config.py b/modules/openxr/config.py index 98b5d7bf428..eac756df36b 100644 --- a/modules/openxr/config.py +++ b/modules/openxr/config.py @@ -75,6 +75,7 @@ def get_doc_classes(): "OpenXRSpatialPlaneTrackingCapability", "OpenXRSpatialMarkerTrackingCapability", "OpenXRAndroidThreadSettingsExtension", + "OpenXRUserPresenceExtension", ] diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index 9c506de94d6..29c53d5070e 100644 --- a/modules/openxr/doc_classes/OpenXRInterface.xml +++ b/modules/openxr/doc_classes/OpenXRInterface.xml @@ -126,6 +126,19 @@ [b]Note:[/b] This only returns a valid value after OpenXR has been initialized. + + + + 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. + + + + + + Returns [code]true[/code] if system has detected the presence of a user in the XR experience. + + @@ -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. + + + + 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. + + diff --git a/modules/openxr/doc_classes/OpenXRUserPresenceExtension.xml b/modules/openxr/doc_classes/OpenXRUserPresenceExtension.xml new file mode 100644 index 00000000000..6e6be6b626e --- /dev/null +++ b/modules/openxr/doc_classes/OpenXRUserPresenceExtension.xml @@ -0,0 +1,11 @@ + + + + This class implements the OpenXR User Presence Extension. + + + This class implements the OpenXR User Presence Extension. + + + + diff --git a/modules/openxr/extensions/openxr_user_presence_extension.cpp b/modules/openxr/extensions/openxr_user_presence_extension.cpp new file mode 100644 index 00000000000..6b7097e6171 --- /dev/null +++ b/modules/openxr/extensions/openxr_user_presence_extension.cpp @@ -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 OpenXRUserPresenceExtension::get_requested_extensions(XrVersion p_version) { + HashMap 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; +} diff --git a/modules/openxr/extensions/openxr_user_presence_extension.h b/modules/openxr/extensions/openxr_user_presence_extension.h new file mode 100644 index 00000000000..bf7c408bb69 --- /dev/null +++ b/modules/openxr/extensions/openxr_user_presence_extension.h @@ -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 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; +}; diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index 26949e78e03..d8b90cca874 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -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 @@ -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); diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index 425ea46e2c9..1bcf01b1b2a 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -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, diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index 0b73fcd6049..1262e7ed0fe 100644 --- a/modules/openxr/register_types.cpp +++ b/modules/openxr/register_types.cpp @@ -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));