From dc7b2adbebea2bd332de52d90dbbe651ee94031c Mon Sep 17 00:00:00 2001 From: Cosmic Linden Date: Wed, 2 Oct 2024 15:25:11 -0700 Subject: secondlife/viewer#2472: Debug tools for labeling textures --- indra/llrender/llgltexture.cpp | 62 +++++++++++++++++++++ indra/llrender/llgltexture.h | 3 + indra/newview/app_settings/settings.xml | 11 ++++ indra/newview/llviewertexturelist.cpp | 98 ++++++++++++++++++++++++++++++++- indra/newview/llviewertexturelist.h | 11 ++++ 5 files changed, 183 insertions(+), 2 deletions(-) diff --git a/indra/llrender/llgltexture.cpp b/indra/llrender/llgltexture.cpp index 4dcca5a726..9a2b2ef524 100644 --- a/indra/llrender/llgltexture.cpp +++ b/indra/llrender/llgltexture.cpp @@ -168,6 +168,68 @@ bool LLGLTexture::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, return ret ; } +void LLGLTexture::getGLObjectLabel(std::string& label, bool& error) const +{ + if (!mGLTexturep) + { + error = true; + label.clear(); + return; + } + LLGLuint texname = mGLTexturep->getTexName(); + if (!texname) + { + error = true; + label.clear(); + return; + } + static GLsizei max_length = 0; + if (max_length == 0) { glGetIntegerv(GL_MAX_LABEL_LENGTH, &max_length); } + static char * clabel = new char[max_length+1]; + GLsizei length; + glGetObjectLabel(GL_TEXTURE, texname, max_length+1, &length, clabel); + error = false; + label.assign(clabel, length); +} + +std::string LLGLTexture::setGLObjectLabel(const std::string& prefix, bool append_texname) const +{ + llassert(mGLTexturep); + if (mGLTexturep) + { + LLGLuint texname = mGLTexturep->getTexName(); + llassert(texname); + if (texname) + { + static GLsizei max_length = 0; + if (max_length == 0) { glGetIntegerv(GL_MAX_LABEL_LENGTH, &max_length); } + + if (append_texname) + { + std::string label_with_texname = prefix + "_" + std::to_string(texname); + label_with_texname.resize(std::min(size_t(max_length), label_with_texname.size())); + glObjectLabel(GL_TEXTURE, texname, (GLsizei)label_with_texname.size(), label_with_texname.c_str()); + return label_with_texname; + } + else + { + if (prefix.size() <= max_length) + { + glObjectLabel(GL_TEXTURE, texname, (GLsizei)prefix.size(), prefix.c_str()); + return prefix; + } + else + { + const std::string label(prefix.c_str(), max_length); + glObjectLabel(GL_TEXTURE, texname, (GLsizei)label.size(), label.c_str()); + return label; + } + } + } + } + return ""; +} + void LLGLTexture::setExplicitFormat(LLGLint internal_format, LLGLenum primary_format, LLGLenum type_format, bool swap_bytes) { llassert(mGLTexturep.notNull()) ; diff --git a/indra/llrender/llgltexture.h b/indra/llrender/llgltexture.h index 122d2a7f9c..22f2ed5131 100644 --- a/indra/llrender/llgltexture.h +++ b/indra/llrender/llgltexture.h @@ -118,6 +118,9 @@ public: LLGLuint getTexName() const ; bool createGLTexture() ; + void getGLObjectLabel(std::string& label, bool& error) const; + std::string setGLObjectLabel(const std::string& prefix, bool append_texname = false) const; + // Create a GL Texture from an image raw // discard_level - mip level, 0 for highest resultion mip // imageraw - the image to copy from diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index ce6d9148c6..5d3f3d58c6 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -7932,6 +7932,17 @@ Value 0 + RenderDebugTextureLabel + + Comment + Enable texture labels via glObjectLabel. Requires restart for some features. + Persist + 1 + Type + Boolean + Value + 0 + RenderDelayCreation Comment diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 7b89ae4e44..99df814f32 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -364,10 +364,31 @@ void LLViewerTextureList::shutdown() mInitialized = false ; //prevent loading textures again. } +namespace +{ + +std::string tex_name_as_string(const LLViewerFetchedTexture* image) +{ + if (!image->getGLTexture()) { return std::string("N/A"); } + return std::to_string(image->getGLTexture()->getTexName()); +} + +const std::string& tex_label_as_string(const LLViewerFetchedTexture* image, std::string& label) +{ + bool error; + image->getGLObjectLabel(label, error); + if (error) { label.assign("N/A"); } + return label; +} + +}; + void LLViewerTextureList::dump() { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - LL_INFOS() << "LLViewerTextureList::dump()" << LL_ENDL; + LL_INFOS() << __FUNCTION__ << "()" << LL_ENDL; + + std::string label; for (image_list_t::iterator it = mImageList.begin(); it != mImageList.end(); ++it) { LLViewerFetchedTexture* image = *it; @@ -378,6 +399,9 @@ void LLViewerTextureList::dump() << " discard " << image->getDiscardLevel() << " desired " << image->getDesiredDiscardLevel() << " http://asset.siva.lindenlab.com/" << image->getID() << ".texture" + << " faces " << image->getTotalNumFaces() + << " texname " << tex_name_as_string(image) + << " label \"" << tex_label_as_string(image, label) << "\"" << LL_ENDL; } } @@ -422,7 +446,13 @@ LLViewerFetchedTexture* LLViewerTextureList::getImageFromFile(const std::string& std::string url = "file://" + full_path; - return getImageFromUrl(url, f_type, usemipmaps, boost_priority, texture_type, internal_format, primary_format, force_id); + LLViewerFetchedTexture* tex = getImageFromUrl(url, f_type, usemipmaps, boost_priority, texture_type, internal_format, primary_format, force_id); + static LLCachedControl debug_texture_label(gSavedSettings, "RenderDebugTextureLabel", false); + if (debug_texture_label()) + { + gTextureList.mNameTextureList.push_back(LLViewerTextureList::NameElement(tex, filename)); + } + return tex; } LLViewerFetchedTexture* LLViewerTextureList::getImageFromUrl(const std::string& url, @@ -844,6 +874,10 @@ void LLViewerTextureList::updateImages(F32 max_time) //handle results from decode threads updateImagesCreateTextures(remaining_time); + // Label all images (if enabled) + updateImagesNameTextures(); + labelAll(); + bool didone = false; for (image_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); ) @@ -1113,6 +1147,62 @@ F32 LLViewerTextureList::updateImagesCreateTextures(F32 max_time) return create_timer.getElapsedTimeF32(); } +void LLViewerTextureList::updateImagesNameTextures() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + if (gGLManager.mIsDisabled) { return; } + static LLCachedControl debug_texture_label(gSavedSettings, "RenderDebugTextureLabel", false); + if (!debug_texture_label()) { return; } + + static GLsizei max_length = 0; + if (max_length == 0) { glGetIntegerv(GL_MAX_LABEL_LENGTH, &max_length); } + + auto it = mNameTextureList.begin(); + while (it != mNameTextureList.end()) // For ALL textures needing names + { + if (it->mTex->hasGLTexture()) + { + if(it->mTex->getTexName()) + { + it->mTex->setGLObjectLabel(it->mPrefix, true); + it = mNameTextureList.erase(it); // Assume no rename needed + } + else + { + ++it; // Not ready + } + } + else + { + ++it; // Not ready + } + } +} + +void LLViewerTextureList::labelAll() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + static LLCachedControl debug_texture_label(gSavedSettings, "RenderDebugTextureLabel", false); + if (!debug_texture_label()) { return; } + + static const std::string local_prefix = "lltexlocal"; + static const std::string other_prefix = "lltexother"; + + std::string label; + bool error; + for (image_list_t::iterator it = mImageList.begin(); it != mImageList.end(); ++it) + { + LLViewerFetchedTexture* image = *it; + image->getGLObjectLabel(label, error); + if (!error && label.empty()) + { + const S32 category = image->getGLTexture()->getCategory(); + const std::string& new_prefix = category == LLGLTexture::LOCAL ? local_prefix : other_prefix; + image->setGLObjectLabel(new_prefix, true); + } + } +} + F32 LLViewerTextureList::updateImagesLoadingFastCache(F32 max_time) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; @@ -1295,6 +1385,10 @@ void LLViewerTextureList::decodeAllImages(F32 max_time) max_time = llmax(max_time, .001f); F32 create_time = updateImagesCreateTextures(max_time); + // Label all images (if enabled) + updateImagesNameTextures(); + labelAll(); + LL_DEBUGS("ViewerImages") << "decodeAllImages() took " << timer.getElapsedTimeF32() << " seconds. " << " fetch_pending " << fetch_pending << " create_time " << create_time diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 7c7112f4cf..6424e5c3f0 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -153,6 +153,9 @@ private: void updateImagesUpdateStats(); F32 updateImagesLoadingFastCache(F32 max_time); + void updateImagesNameTextures(); + void labelAll(); + void addImage(LLViewerFetchedTexture *image, ETexListType tex_type); void deleteImage(LLViewerFetchedTexture *image); @@ -214,6 +217,14 @@ public: // images that have been loaded but are waiting to be uploaded to GL image_queue_t mCreateTextureList; + struct NameElement + { + NameElement(LLViewerFetchedTexture* tex, const std::string& prefix) : mTex(tex), mPrefix(prefix) {} + LLPointer mTex; + std::string mPrefix; + }; + std::vector mNameTextureList; + // images that must be downscaled quickly so we don't run out of memory image_queue_t mDownScaleQueue; -- cgit v1.2.3 From 13221f67c465017f44ca46aeca23b0d820935825 Mon Sep 17 00:00:00 2001 From: Leviathan Linden Date: Tue, 19 Sep 2023 09:40:08 -0700 Subject: add GameControl feature and SDL2 dependency --- autobuild.xml | 68 ++- indra/cmake/CMakeLists.txt | 1 + indra/cmake/Copy3rdPartyLibs.cmake | 4 + indra/cmake/LLWindow.cmake | 14 +- indra/cmake/SDL2.cmake | 22 + indra/llcommon/llkeybind.cpp | 8 +- indra/llcommon/llkeybind.h | 8 +- indra/llmessage/message_prehash.cpp | 2 + indra/llmessage/message_prehash.h | 2 + indra/llui/CMakeLists.txt | 5 +- indra/llwindow/CMakeLists.txt | 3 + indra/llwindow/llgamecontrol.cpp | 605 +++++++++++++++++++++ indra/llwindow/llgamecontrol.h | 86 +++ indra/llwindow/llkeyboard.cpp | 39 ++ indra/llwindow/llkeyboardheadless.h | 19 +- indra/llwindow/llkeyboardmacosx.cpp | 2 +- indra/llwindow/llkeyboardmacosx.h | 14 +- indra/llwindow/llkeyboardwin32.cpp | 4 +- indra/llwindow/llkeyboardwin32.h | 19 +- indra/llwindow/llwindow.cpp | 46 +- indra/llwindow/llwindowsdl.h | 2 + indra/newview/CMakeLists.txt | 5 +- indra/newview/app_settings/settings.xml | 22 + indra/newview/llappviewer.cpp | 65 +++ indra/newview/llfilepicker.cpp | 5 +- indra/newview/llkeyconflict.cpp | 21 +- indra/newview/llkeyconflict.h | 12 +- .../skins/default/xui/en/floater_preferences.xml | 7 + .../xui/en/panel_preferences_game_controls.xml | 32 ++ indra/newview/tests/llgamecontrol_stub.cpp | 76 +++ indra/newview/tests/llversioninfo_test.cpp | 5 +- indra/newview/viewer_manifest.py | 15 +- scripts/messages/message_template.msg | 35 +- 33 files changed, 1159 insertions(+), 114 deletions(-) create mode 100644 indra/cmake/SDL2.cmake create mode 100644 indra/llwindow/llgamecontrol.cpp create mode 100644 indra/llwindow/llgamecontrol.h create mode 100644 indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml create mode 100644 indra/newview/tests/llgamecontrol_stub.cpp diff --git a/autobuild.xml b/autobuild.xml index 4f70212fa8..4ec43be8de 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -9,8 +9,32 @@ SDL2 + copyright + Copyright (C) 1997-2022 Sam Lantinga (slouken@libsdl.org) + description + Simple DirectMedia Layer is a cross-platform multimedia library designed to provide low level access to audio, keyboard, mouse, joystick, 3D hardware via OpenGL, and 2D video framebuffer. + license + lgpl + license_file + LICENSES/SDL2.txt + name + SDL2 platforms + darwin64 + + archive + + hash + fd6368b53609b078b4ed8816bad1d1de2756f4f2 + hash_algorithm + sha1 + url + https://github.com/secondlife/3p-sdl2/releases/download/v2.28.0/SDL2-2.28.0-darwin64-5991c8f.tar.zst + + name + darwin64 + linux64 archive @@ -25,19 +49,21 @@ name linux64 + windows64 + + archive + + hash + 48e8d971dfa8025353293ead7d41a2a77b004faa + hash_algorithm + sha1 + url + https://github.com/secondlife/3p-sdl2/releases/download/v2.28.0/SDL2-2.28.0-windows64-5991c8f.tar.zst + + name + windows64 + - license - lgpl - license_file - LICENSES/SDL2.txt - copyright - Copyright (C) 1997-2022 Sam Lantinga (slouken@libsdl.org) - version - 2.28.0 - name - SDL2 - description - Simple DirectMedia Layer is a cross-platform multimedia library designed to provide low level access to audio, keyboard, mouse, joystick, 3D hardware via OpenGL, and 2D video framebuffer. fltk @@ -1181,6 +1207,18 @@ name darwin64 + linux64 + + archive + + hash + ffbdd109356d66ddfefd8a5d57f63f1f + url + http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/533/1144/libhunspell-1.3.2.500526-linux64-500526.tar.bz2 + + name + linux64 + windows64 archive @@ -1574,7 +1612,7 @@ https://s3-proxy.lindenlab.com/private-builds-secondlife-com/ct2/84731/788139/llphysicsextensions_tpv-1.0.561752-windows64-561752.tar.bz2 name - windows + windows64 license @@ -3254,7 +3292,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors name linux64 - windows + windows64 configurations @@ -3411,7 +3449,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors build_directory build-vc${AUTOBUILD_VSVER|170}-$AUTOBUILD_ADDRSIZE name - windows + windows64 license diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt index 16da388e61..9017fc2fb4 100644 --- a/indra/cmake/CMakeLists.txt +++ b/indra/cmake/CMakeLists.txt @@ -52,6 +52,7 @@ set(cmake_SOURCE_FILES Prebuilt.cmake PulseAudio.cmake Python.cmake + SDL2.cmake TemplateCheck.cmake TinyEXR.cmake TinyGLTF.cmake diff --git a/indra/cmake/Copy3rdPartyLibs.cmake b/indra/cmake/Copy3rdPartyLibs.cmake index 1e6dabd6c0..e98c77497b 100644 --- a/indra/cmake/Copy3rdPartyLibs.cmake +++ b/indra/cmake/Copy3rdPartyLibs.cmake @@ -54,6 +54,7 @@ if(WINDOWS) set(release_src_dir "${ARCH_PREBUILT_DIRS_RELEASE}") set(release_files openjp2.dll + SDL2.dll ) if(LLCOMMON_LINK_SHARED) @@ -169,6 +170,8 @@ elseif(DARWIN) set(release_src_dir "${ARCH_PREBUILT_DIRS_RELEASE}") set(release_files libndofdev.dylib + libSDL2.dylib + libSDL2-2.0.dylib ) if(LLCOMMON_LINK_SHARED) @@ -197,6 +200,7 @@ elseif(LINUX) libortp.so libvivoxoal.so.1 libvivoxsdk.so + libSDL2.so ) set(slvoice_files SLVoice) diff --git a/indra/cmake/LLWindow.cmake b/indra/cmake/LLWindow.cmake index 23f4115aeb..b2c1792df1 100644 --- a/indra/cmake/LLWindow.cmake +++ b/indra/cmake/LLWindow.cmake @@ -3,18 +3,6 @@ include(Variables) include(GLEXT) include(Prebuilt) +include(SDL2) include_guard() -add_library( ll::SDL INTERFACE IMPORTED ) - - -if (LINUX) - #Must come first as use_system_binary can exit this file early - target_compile_definitions( ll::SDL INTERFACE LL_SDL_VERSION=2 LL_SDL) - - #find_package(SDL2 REQUIRED) - #target_link_libraries( ll::SDL INTERFACE SDL2::SDL2 SDL2::SDL2main X11) - - use_prebuilt_binary(SDL2) - target_link_libraries( ll::SDL INTERFACE SDL2 X11) -endif (LINUX) diff --git a/indra/cmake/SDL2.cmake b/indra/cmake/SDL2.cmake new file mode 100644 index 0000000000..87195ed108 --- /dev/null +++ b/indra/cmake/SDL2.cmake @@ -0,0 +1,22 @@ +# -*- cmake -*- +cmake_minimum_required( VERSION 3.13 FATAL_ERROR ) + +include(Linking) +include( Prebuilt ) +include_guard() + +add_library( ll::SDL2 INTERFACE IMPORTED ) + +use_system_binary( SDL2 ) +use_prebuilt_binary( SDL2 ) + +find_library( SDL2_LIBRARY + NAMES SDL2 + PATHS "${LIBS_PREBUILT_DIR}/lib/release") +if ( "${SDL2_LIBRARY}" STREQUAL "SDL2_LIBRARY-NOTFOUND" ) + message( FATAL_ERROR "unable to find SDL2_LIBRARY" ) +endif() + +target_link_libraries( ll::SDL2 INTERFACE "${SDL2_LIBRARY}" ) +target_include_directories( ll::SDL2 SYSTEM INTERFACE "${LIBS_PREBUILT_DIR}/include" ) + diff --git a/indra/llcommon/llkeybind.cpp b/indra/llcommon/llkeybind.cpp index e36c1d0a4c..83c53d220d 100644 --- a/indra/llcommon/llkeybind.cpp +++ b/indra/llcommon/llkeybind.cpp @@ -123,7 +123,7 @@ LLKeyData& LLKeyData::operator=(const LLKeyData& rhs) return *this; } -bool LLKeyData::operator==(const LLKeyData& rhs) +bool LLKeyData::operator==(const LLKeyData& rhs) const { if (mMouse != rhs.mMouse) return false; if (mKey != rhs.mKey) return false; @@ -132,7 +132,7 @@ bool LLKeyData::operator==(const LLKeyData& rhs) return true; } -bool LLKeyData::operator!=(const LLKeyData& rhs) +bool LLKeyData::operator!=(const LLKeyData& rhs) const { if (mMouse != rhs.mMouse) return true; if (mKey != rhs.mKey) return true; @@ -179,7 +179,7 @@ LLKeyBind::LLKeyBind(const LLSD &key_bind) } } -bool LLKeyBind::operator==(const LLKeyBind& rhs) +bool LLKeyBind::operator==(const LLKeyBind& rhs) const { auto size = mData.size(); if (size != rhs.mData.size()) return false; @@ -192,7 +192,7 @@ bool LLKeyBind::operator==(const LLKeyBind& rhs) return true; } -bool LLKeyBind::operator!=(const LLKeyBind& rhs) +bool LLKeyBind::operator!=(const LLKeyBind& rhs) const { auto size = mData.size(); if (size != rhs.mData.size()) return true; diff --git a/indra/llcommon/llkeybind.h b/indra/llcommon/llkeybind.h index 1bbb2fadb5..eb9b68f9d1 100644 --- a/indra/llcommon/llkeybind.h +++ b/indra/llcommon/llkeybind.h @@ -44,8 +44,8 @@ public: bool empty() const { return isEmpty(); }; void reset(); LLKeyData& operator=(const LLKeyData& rhs); - bool operator==(const LLKeyData& rhs); - bool operator!=(const LLKeyData& rhs); + bool operator==(const LLKeyData& rhs) const; + bool operator!=(const LLKeyData& rhs) const; bool canHandle(const LLKeyData& data) const; bool canHandle(EMouseClickType mouse, KEY key, MASK mask) const; @@ -64,8 +64,8 @@ public: LLKeyBind() {} LLKeyBind(const LLSD &key_bind); - bool operator==(const LLKeyBind& rhs); - bool operator!=(const LLKeyBind& rhs); + bool operator==(const LLKeyBind& rhs) const; + bool operator!=(const LLKeyBind& rhs) const; bool isEmpty() const; bool empty() const { return isEmpty(); }; diff --git a/indra/llmessage/message_prehash.cpp b/indra/llmessage/message_prehash.cpp index c264a9f086..d3b80d684f 100644 --- a/indra/llmessage/message_prehash.cpp +++ b/indra/llmessage/message_prehash.cpp @@ -1402,3 +1402,5 @@ char const* const _PREHASH_HoverHeight = LLMessageStringTable::getInstance()->ge char const* const _PREHASH_Experience = LLMessageStringTable::getInstance()->getString("Experience"); char const* const _PREHASH_ExperienceID = LLMessageStringTable::getInstance()->getString("ExperienceID"); char const* const _PREHASH_LargeGenericMessage = LLMessageStringTable::getInstance()->getString("LargeGenericMessage"); +char const* const _PREHASH_GameControlInput = LLMessageStringTable::getInstance()->getString("GameControlInput"); +char const* const _PREHASH_AxisData = LLMessageStringTable::getInstance()->getString("AxisData"); diff --git a/indra/llmessage/message_prehash.h b/indra/llmessage/message_prehash.h index 1d30b69b67..5449eaf2a5 100644 --- a/indra/llmessage/message_prehash.h +++ b/indra/llmessage/message_prehash.h @@ -1403,5 +1403,7 @@ extern char const* const _PREHASH_HoverHeight; extern char const* const _PREHASH_Experience; extern char const* const _PREHASH_ExperienceID; extern char const* const _PREHASH_LargeGenericMessage; +extern char const* const _PREHASH_GameControlInput; +extern char const* const _PREHASH_AxisData; #endif diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index 69e1b57245..13a0250fe5 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -270,12 +270,13 @@ target_link_libraries(llui llmath ll::hunspell llcommon + ll::SDL2 ) # Add tests if(LL_TESTS) include(LLAddBuildTest) - set(test_libs llmessage llcorehttp llxml llrender llcommon ll::hunspell) + set(test_libs llmessage llcorehttp llxml llrender llcommon ll::hunspell ll::SDL2) SET(llui_TEST_SOURCE_FILES llurlmatch.cpp @@ -285,7 +286,7 @@ if(LL_TESTS) # INTEGRATION TESTS if(NOT LINUX) - set(test_libs llui llmessage llcorehttp llxml llrender llcommon ll::hunspell ) + set(test_libs llui llmessage llcorehttp llxml llrender llcommon ll::hunspell ll::SDL2) LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "${test_libs}") endif(NOT LINUX) endif(LL_TESTS) diff --git a/indra/llwindow/CMakeLists.txt b/indra/llwindow/CMakeLists.txt index 075e17235a..e251af3e6c 100644 --- a/indra/llwindow/CMakeLists.txt +++ b/indra/llwindow/CMakeLists.txt @@ -18,9 +18,11 @@ include(LLWindow) include(UI) include(ViewerMiscLibs) include(GLM) +include(SDL2) set(llwindow_SOURCE_FILES llcursortypes.cpp + llgamecontrol.cpp llkeyboard.cpp llkeyboardheadless.cpp llwindowheadless.cpp @@ -32,6 +34,7 @@ set(llwindow_HEADER_FILES CMakeLists.txt llcursortypes.h + llgamecontrol.h llkeyboard.h llkeyboardheadless.h llwindowheadless.h diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp new file mode 100644 index 0000000000..5dc01c5e54 --- /dev/null +++ b/indra/llwindow/llgamecontrol.cpp @@ -0,0 +1,605 @@ +/** + * @file llgamecontrol.h + * @brief GameController detection and management + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llgamecontrol.h" + +#include +#include +#include + +#include "SDL2/SDL.h" +#include "SDL2/SDL_gamecontroller.h" +#include "SDL2/SDL_joystick.h" + +constexpr size_t NUM_AXES = 6; + +// internal class for managing list of controllers and per-controller state +class LLGameControllerManager +{ +public: + void addController(SDL_JoystickID id, SDL_GameController* controller); + void removeController(SDL_JoystickID id); + + void onAxis(SDL_JoystickID id, U8 axis, S16 value); + void onButton(SDL_JoystickID id, U8 button, bool pressed); + + void onKeyButton(U8 button, bool pressed); + void onKeyAxis(U8 axis, U16 value); + + void clearAllInput(); + void clearAllKeys(); + size_t getControllerIndex(SDL_JoystickID id) const; + + void computeFinalState(LLGameControl::State& state); + + void clear(); + +private: + std::vector mControllerIDs; + std::vector mControllers; + std::vector mStates; + LLGameControl::State mKeyboardState; +}; + +// local globals +namespace +{ + LLGameControl* g_gameControl = nullptr; + LLGameControllerManager g_manager; + + // The GameControlInput message is sent via UDP which is lossy. + // Since we send the only the list of pressed buttons the receiving + // side can compute the difference between subsequent states to + // find button-down/button-up events. + // + // To reduce the likelihood of buttons being stuck "pressed" forever + // on the receiving side (for lost final packet) we resend the last + // data state. However, to keep th ambient resend bandwidth low we + // expand the resend period at a geometric rate. + // + constexpr U64 MSEC_PER_NSEC = 1e6; + constexpr U64 FIRST_RESEND_PERIOD = 100 * MSEC_PER_NSEC; + constexpr U64 RESEND_EXPANSION_RATE = 10; + LLGameControl::State g_gameControlState; + U64 g_lastSend = 0; + U64 g_nextResendPeriod = FIRST_RESEND_PERIOD; + + std::map g_keyButtonMap; + std::map g_keyAxisMapPositive; + std::map g_keyAxisMapNegative; + + bool g_includeKeyboardButtons = false; + + constexpr U8 MAX_AXIS = 5; + constexpr U8 MAX_BUTTON = 31; +} + +LLGameControl::~LLGameControl() +{ + terminate(); +} + +LLGameControl::State::State() : mButtons(0) +{ + mAxes.resize(NUM_AXES, 0); + mPrevAxes.resize(NUM_AXES, 0); +} + +bool LLGameControl::State::onButton(U8 button, bool pressed) +{ + U32 old_buttons = mButtons; + if (button <= MAX_BUTTON) + { + if (pressed) + { + mButtons |= (0x01 << button); + } + else + { + mButtons &= ~(0x01 << button); + } + } + bool changed = (old_buttons != mButtons); + return changed; +} + +void LLGameControllerManager::addController(SDL_JoystickID id, SDL_GameController* controller) +{ + if (controller) + { + size_t i = 0; + for (; i < mControllerIDs.size(); ++i) + { + if (id == mControllerIDs[i]) + { + break; + } + } + if (i == mControllerIDs.size()) + { + mControllerIDs.push_back(id); + mControllers.push_back(controller); + mStates.push_back(LLGameControl::State()); + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " controller=" << controller + << LL_ENDL; + } + } +} + +void LLGameControllerManager::removeController(SDL_JoystickID id) +{ + size_t i = 0; + size_t num_controllers = mControllerIDs.size(); + for (; i < num_controllers; ++i) + { + if (id == mControllerIDs[i]) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " controller=" << mControllers[i] + << LL_ENDL; + + mControllerIDs[i] = mControllerIDs[num_controllers - 1]; + mControllers[i] = mControllers[num_controllers - 1]; + mStates[i] = mStates[num_controllers - 1]; + + mControllerIDs.pop_back(); + mControllers.pop_back(); + mStates.pop_back(); + break; + } + } +} + +size_t LLGameControllerManager::getControllerIndex(SDL_JoystickID id) const +{ + constexpr size_t UNREASONABLY_HIGH_INDEX = 1e6; + size_t index = UNREASONABLY_HIGH_INDEX; + for (size_t i = 0; i < mControllers.size(); ++i) + { + if (id == mControllerIDs[i]) + { + index = i; + break; + } + } + return index; +} + +void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) +{ + if (axis > MAX_AXIS) + { + return; + } + size_t index = getControllerIndex(id); + if (index < mControllers.size()) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " axis=" << (S32)(axis) + << " value=" << (S32)(value) << LL_ENDL; + mStates[index].mAxes[axis] = value; + } +} + +void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool pressed) +{ + size_t index = getControllerIndex(id); + if (index < mControllers.size()) + { + if (mStates[index].onButton(button, pressed)) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " button i=" << (S32)(button) + << " pressed=" << pressed << LL_ENDL; + } + } +} + +void LLGameControllerManager::onKeyButton(U8 button, bool pressed) +{ + if (mKeyboardState.onButton(button, pressed)) + { + LL_DEBUGS("SDL2") << " keyboard button i=" << (S32)(button) << " pressed=" << pressed << LL_ENDL; + } +} + +void LLGameControllerManager::onKeyAxis(U8 axis, U16 value) +{ + if (mKeyboardState.mAxes[axis] != value) + { + mKeyboardState.mAxes[axis] = value; + LL_DEBUGS("SDL2") << " keyboard axis i=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; + } +} + +void LLGameControllerManager::clearAllInput() +{ + for (auto& state : mStates) + { + state.mButtons = 0; + std::fill(state.mAxes.begin(), state.mAxes.end(), 0); + } + mKeyboardState.mButtons = 0; + std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); +} + +void LLGameControllerManager::clearAllKeys() +{ + mKeyboardState.mButtons = 0; + std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); +} + +void LLGameControllerManager::computeFinalState(LLGameControl::State& state) +{ + // clear the slate + std::vector axes_accumulator; + axes_accumulator.resize(NUM_AXES, 0); + U32 old_buttons = state.mButtons; + state.mButtons = 0; + + // accumulate the controllers + for (const auto& s : mStates) + { + state.mButtons |= s.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + axes_accumulator[i] += (S32)(s.mAxes[i]); + } + } + + // accumulate the keyboard + state.mButtons |= mKeyboardState.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + axes_accumulator[i] += (S32)(mKeyboardState.mAxes[i]); + } + if (old_buttons != state.mButtons) + { + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + + // clamp the axes + for (size_t i = 0; i < NUM_AXES; ++i) + { + S32 new_axis = (S16)(std::min(std::max(axes_accumulator[i], -32768), 32767)); + // check for change + if (state.mAxes[i] != new_axis) + { + // When axis changes we explicitly update the corresponding prevAxis + // otherwise, we let prevAxis get updated in updateResendPeriod() + // which is explicitly called after a packet is sent. This allows + // unchanged axes to be included in first resend but not later ones. + state.mPrevAxes[i] = state.mAxes[i]; + state.mAxes[i] = new_axis; + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + } +} + +void LLGameControllerManager::clear() +{ + mControllerIDs.clear(); + mControllers.clear(); + mStates.clear(); +} + + +U64 get_now_nsec() +{ + std::chrono::time_point t0; + return (std::chrono::steady_clock::now() - t0).count(); +} + +// util for dumping SDL_GameController info +std::ostream& operator<<(std::ostream& out, SDL_GameController* c) +{ + if (! c) + { + return out << "nullptr"; + } + out << "{"; + out << " name='" << SDL_GameControllerName(c) << "'"; + out << " type='" << SDL_GameControllerGetType(c) << "'"; + out << " vendor='" << SDL_GameControllerGetVendor(c) << "'"; + out << " product='" << SDL_GameControllerGetProduct(c) << "'"; + out << " version='" << SDL_GameControllerGetProductVersion(c) << "'"; + //CRASH! out << " serial='" << SDL_GameControllerGetSerial(c) << "'"; + out << " }"; + return out; +} + +// util for dumping SDL_Joystick info +std::ostream& operator<<(std::ostream& out, SDL_Joystick* j) +{ + if (! j) + { + return out << "nullptr"; + } + out << "{"; + out << " p=0x" << (void*)(j); + out << " name='" << SDL_JoystickName(j) << "'"; + out << " type='" << SDL_JoystickGetType(j) << "'"; + out << " instance='" << SDL_JoystickInstanceID(j) << "'"; + out << " product='" << SDL_JoystickGetProduct(j) << "'"; + out << " version='" << SDL_JoystickGetProductVersion(j) << "'"; + out << " num_axes=" << SDL_JoystickNumAxes(j); + out << " num_balls=" << SDL_JoystickNumBalls(j); + out << " num_hats=" << SDL_JoystickNumHats(j); + out << " num_buttons=" << SDL_JoystickNumHats(j); + out << " }"; + return out; +} + +void onControllerDeviceAdded(const SDL_Event& event) +{ + int device_index = event.cdevice.which; + SDL_JoystickID id = SDL_JoystickGetDeviceInstanceID(device_index); + SDL_GameController* controller = SDL_GameControllerOpen(device_index); + + g_manager.addController(id, controller); +} + +void onControllerDeviceRemoved(const SDL_Event& event) +{ + SDL_JoystickID id = event.cdevice.which; + g_manager.removeController(id); +} + +void onControllerButton(const SDL_Event& event) +{ + g_manager.onButton(event.cbutton.which, event.cbutton.button, event.cbutton.state == SDL_PRESSED); +} + +void onControllerAxis(const SDL_Event& event) +{ + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << event.caxis.which << std::dec + << " axis=" << (S32)(event.caxis.axis) + << " value=" << (S32)(event.caxis.value) << LL_ENDL; + g_manager.onAxis(event.caxis.which, event.caxis.axis, event.caxis.value); +} + +// static +bool LLGameControl::isInitialized() +{ + return g_gameControl != nullptr; +} + +void sdl_logger(void *userdata, int category, SDL_LogPriority priority, const char *message) +{ + LL_DEBUGS("SDL2") << "log='" << message << "'" << LL_ENDL; +} + +// static +void LLGameControl::init() +{ + if (!g_gameControl) + { + g_gameControl = LLGameControl::getInstance(); + SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + SDL_LogSetOutputFunction(&sdl_logger, nullptr); + } +} + +// static +void LLGameControl::terminate() +{ + g_manager.clear(); + SDL_Quit(); +} + +// static +void LLGameControl::addKeyButtonMap(U16 key, U8 button) +{ + g_keyButtonMap[key] = button; +} + +// static +void LLGameControl::removeKeyButtonMap(U16 key) +{ + g_keyButtonMap.erase(key); +} + +// static +void LLGameControl::addKeyAxisMap(U16 key, U8 axis, bool positive) +{ + if (axis > MAX_AXIS) + { + return; + } + if (positive) + { + g_keyAxisMapPositive[key] = axis; + g_keyAxisMapNegative.erase(key); + } + else + { + g_keyAxisMapNegative[key] = axis; + g_keyAxisMapPositive.erase(key); + } +} + +// static +void LLGameControl::removeKeyAxisMap(U16 key) +{ + g_keyAxisMapPositive.erase(key); + g_keyAxisMapNegative.erase(key); +} + +// static +void LLGameControl::onKeyDown(U16 key, U32 mask) +{ + auto itr = g_keyButtonMap.find(key); + if (itr != g_keyButtonMap.end()) + { + g_manager.onKeyButton(itr->second, true); + } + else + { + itr = g_keyAxisMapPositive.find(key); + if (itr != g_keyAxisMapPositive.end()) + { + g_manager.onKeyAxis(itr->second, 32767); + } + else + { + itr = g_keyAxisMapNegative.find(key); + if (itr != g_keyAxisMapNegative.end()) + { + g_manager.onKeyAxis(itr->second, -32768); + } + } + } +} + +// static +void LLGameControl::onKeyUp(U16 key, U32 mask) +{ + auto itr = g_keyButtonMap.find(key); + if (itr != g_keyButtonMap.end()) + { + g_manager.onKeyButton(itr->second, true); + } + else + { + itr = g_keyAxisMapPositive.find(key); + if (itr != g_keyAxisMapPositive.end()) + { + g_manager.onKeyAxis(itr->second, 0); + } + else + { + itr = g_keyAxisMapNegative.find(key); + if (itr != g_keyAxisMapNegative.end()) + { + g_manager.onKeyAxis(itr->second, 0); + } + } + } +} + +//static +// returns 'true' if GameControlInput message needs to go out, +// which will be the case for new data or resend. Call this right +// before deciding to put a GameControlInput packet on the wire +// or not. +bool LLGameControl::computeFinalInputAndCheckForChanges() +{ + g_manager.computeFinalState(g_gameControlState); + return g_lastSend + g_nextResendPeriod < get_now_nsec(); +} + +// static +void LLGameControl::clearAllInput() +{ + g_manager.clearAllInput(); +} + +// static +void LLGameControl::clearAllKeys() +{ + g_manager.clearAllKeys(); +} + +// static +void LLGameControl::processEvents(bool app_has_focus) +{ + SDL_Event event; + if (!app_has_focus) + { + // when SL window lacks focus: pump SDL events but ignore them + while (g_gameControl && SDL_PollEvent(&event)) + { + // do nothing: SDL_PollEvent() is the operator + } + clearAllInput(); + return; + } + + while (g_gameControl && SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_CONTROLLERDEVICEADDED: + onControllerDeviceAdded(event); + break; + case SDL_CONTROLLERDEVICEREMOVED: + onControllerDeviceRemoved(event); + break; + case SDL_CONTROLLERBUTTONDOWN: + /* FALLTHROUGH */ + case SDL_CONTROLLERBUTTONUP: + onControllerButton(event); + break; + case SDL_CONTROLLERAXISMOTION: + onControllerAxis(event); + break; + default: + break; + } + } +} + +// static +const LLGameControl::State& LLGameControl::getState() +{ + return g_gameControlState; +} + +// static +void LLGameControl::setIncludeKeyboardButtons(bool include) +{ + g_includeKeyboardButtons = include; +} + +// static +bool LLGameControl::getIncludeKeyboardButtons() +{ + return g_includeKeyboardButtons; +} + +//static +void LLGameControl::updateResendPeriod() +{ + // we expect this method to be called right after data is sent + g_lastSend = get_now_nsec(); + if (g_nextResendPeriod == 0) + { + g_nextResendPeriod = FIRST_RESEND_PERIOD; + } + else + { + // Reset mPrevAxes only on second resend or higher + // because when the joysticks are being used we expect a steady stream + // of recorrection data rather than sparse changes. + // + // In other words: we want to include changed axes in the first resend + // so we only overrite g_gameControlState.mPrevAxes on higher resends. + g_gameControlState.mPrevAxes = g_gameControlState.mAxes; + g_nextResendPeriod *= RESEND_EXPANSION_RATE; + } +} + diff --git a/indra/llwindow/llgamecontrol.h b/indra/llwindow/llgamecontrol.h new file mode 100644 index 0000000000..fe6d6f0138 --- /dev/null +++ b/indra/llwindow/llgamecontrol.h @@ -0,0 +1,86 @@ +/** + * @file llgamecontrol.h + * @brief GameController detection and management + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include + + +#include "llerror.h" +#include "llsingleton.h" +#include "stdtypes.h" + + +// LLGameControl is a singleton with pure static public interface +class LLGameControl : public LLSingleton +{ + LLSINGLETON_EMPTY_CTOR(LLGameControl); + virtual ~LLGameControl(); + LOG_CLASS(LLGameControl); + +public: + // State is a minimal class for storing axes and buttons values + class State + { + public: + State(); + bool onButton(U8 button, bool pressed); + std::vector mAxes; // [ -32768, 32767 ] + std::vector mPrevAxes; // value in last outgoing packet + U32 mButtons; + }; + + static bool isInitialized(); + static void init(); + static void terminate(); + + static void addKeyButtonMap(U16 key, U8 button); + static void removeKeyButtonMap(U16 key); + static void addKeyAxisMap(U16 key, U8 axis, bool positive); + static void removeKeyAxisMap(U16 key); + + static void onKeyDown(U16 key, U32 mask); + static void onKeyUp(U16 key, U32 mask); + + // returns 'true' if GameControlInput message needs to go out, + // which will be the case for new data or resend. Call this right + // before deciding to put a GameControlInput packet on the wire + // or not. + static bool computeFinalInputAndCheckForChanges(); + + static void clearAllInput(); + static void clearAllKeys(); + + static void processEvents(bool app_has_focus = true); + static const State& getState(); + + static void setIncludeKeyboardButtons(bool include); + static bool getIncludeKeyboardButtons(); + + // call this after putting a GameControlInput packet on the wire + static void updateResendPeriod(); +}; + diff --git a/indra/llwindow/llkeyboard.cpp b/indra/llwindow/llkeyboard.cpp index a16c0a318a..a858d3b16f 100644 --- a/indra/llwindow/llkeyboard.cpp +++ b/indra/llwindow/llkeyboard.cpp @@ -29,6 +29,7 @@ #include "llkeyboard.h" #include "llwindowcallbacks.h" +#include "llgamecontrol.h" // // Globals @@ -161,6 +162,7 @@ void LLKeyboard::resetKeyDownAndHandle() mCallbacks->handleTranslatedKeyUp(i, mask); } } + LLGameControl::clearAllKeys(); } // BUG this has to be called when an OS dialog is shown, otherwise modifier key state @@ -275,6 +277,43 @@ bool LLKeyboard::handleTranslatedKeyUp(KEY translated_key, U32 translated_mask) return handled; } +bool LLKeyboard::handleKeyDown(const U16 key, const U32 mask) +{ + U32 translated_mask = updateModifiers(mask); + + KEY translated_key = 0; + bool handled = false; + if(translateKey(key, &translated_key)) + { + handled = handleTranslatedKeyDown(translated_key, translated_mask); + } + if (!handled) + { + LLGameControl::onKeyDown(translated_key, translated_mask); + } + + return handled; +} + + +bool LLKeyboard::handleKeyUp(const U16 key, const U32 mask) +{ + U32 translated_mask = updateModifiers(mask); + + KEY translated_key = 0; + bool handled = false; + if(translateKey(key, &translated_key)) + { + handled = handleTranslatedKeyUp(translated_key, translated_mask); + } + if (!handled) + { + LLGameControl::onKeyUp(translated_key, translated_mask); + } + + return handled; +} + void LLKeyboard::toggleInsertMode() { diff --git a/indra/llwindow/llkeyboardheadless.h b/indra/llwindow/llkeyboardheadless.h index 439abaf25b..cc31b99d3f 100644 --- a/indra/llwindow/llkeyboardheadless.h +++ b/indra/llwindow/llkeyboardheadless.h @@ -33,20 +33,15 @@ class LLKeyboardHeadless : public LLKeyboard { public: LLKeyboardHeadless(); - /*virtual*/ ~LLKeyboardHeadless() {}; + ~LLKeyboardHeadless() {}; -#ifndef LL_SDL - /*virtual*/ bool handleKeyUp(const U16 key, MASK mask) { return false; } - /*virtual*/ bool handleKeyDown(const U16 key, MASK mask) { return false; } -#else - /*virtual*/ bool handleKeyUp(const U32 key, MASK mask) { return false; } - /*virtual*/ bool handleKeyDown(const U32 key, MASK mask) { return false; } -#endif - /*virtual*/ void resetMaskKeys(); - /*virtual*/ MASK currentMask(bool for_mouse_event); - /*virtual*/ void scanKeyboard(); + bool handleKeyUp(const U16 key, MASK mask) override; + bool handleKeyDown(const U16 key, MASK mask) override; + void resetMaskKeys() override; + MASK currentMask(bool for_mouse_event) override; + void scanKeyboard() override; #ifdef LL_DARWIN - /*virtual*/ void handleModifier(MASK mask); + void handleModifier(MASK mask) override; #endif }; diff --git a/indra/llwindow/llkeyboardmacosx.cpp b/indra/llwindow/llkeyboardmacosx.cpp index 89ff7c6d3f..f590b8db8b 100644 --- a/indra/llwindow/llkeyboardmacosx.cpp +++ b/indra/llwindow/llkeyboardmacosx.cpp @@ -203,7 +203,7 @@ void LLKeyboardMacOSX::handleModifier(MASK mask) updateModifiers(mask); } -MASK LLKeyboardMacOSX::updateModifiers(const U32 mask) +MASK LLKeyboardMacOSX::updateModifiers(U32 mask) { // translate the mask MASK out_mask = 0; diff --git a/indra/llwindow/llkeyboardmacosx.h b/indra/llwindow/llkeyboardmacosx.h index 92ab5c9a85..af8a626db8 100644 --- a/indra/llwindow/llkeyboardmacosx.h +++ b/indra/llwindow/llkeyboardmacosx.h @@ -42,14 +42,14 @@ class LLKeyboardMacOSX : public LLKeyboard { public: LLKeyboardMacOSX(); - /*virtual*/ ~LLKeyboardMacOSX() {}; + ~LLKeyboardMacOSX() {}; - /*virtual*/ bool handleKeyUp(const U16 key, MASK mask); - /*virtual*/ bool handleKeyDown(const U16 key, MASK mask); - /*virtual*/ void resetMaskKeys(); - /*virtual*/ MASK currentMask(bool for_mouse_event); - /*virtual*/ void scanKeyboard(); - /*virtual*/ void handleModifier(MASK mask); + bool handleKeyUp(const U16 key, MASK mask) override; + bool handleKeyDown(const U16 key, MASK mask) override; + void resetMaskKeys() override; + MASK currentMask(bool for_mouse_event) override; + void scanKeyboard() override; + void handleModifier(MASK mask) override; protected: MASK updateModifiers(const U32 mask); diff --git a/indra/llwindow/llkeyboardwin32.cpp b/indra/llwindow/llkeyboardwin32.cpp index 8d6b8d9b93..7ef616a8b7 100644 --- a/indra/llwindow/llkeyboardwin32.cpp +++ b/indra/llwindow/llkeyboardwin32.cpp @@ -182,7 +182,7 @@ void LLKeyboardWin32::resetMaskKeys() //} -MASK LLKeyboardWin32::updateModifiers() +MASK LLKeyboardWin32::updateModifiers(U32 mask) { //RN: this seems redundant, as we should have already received the appropriate // messages for the modifier keys @@ -321,4 +321,4 @@ U16 LLKeyboardWin32::inverseTranslateExtendedKey(const KEY translated_key) return inverseTranslateKey(converted_key); } -#endif +#endif // LL_WINDOWS diff --git a/indra/llwindow/llkeyboardwin32.h b/indra/llwindow/llkeyboardwin32.h index d0dfc5cfdd..d3dc65d9aa 100644 --- a/indra/llwindow/llkeyboardwin32.h +++ b/indra/llwindow/llkeyboardwin32.h @@ -37,15 +37,16 @@ class LLKeyboardWin32 : public LLKeyboard { public: LLKeyboardWin32(); - /*virtual*/ ~LLKeyboardWin32() {}; - - /*virtual*/ bool handleKeyUp(const U16 key, MASK mask); - /*virtual*/ bool handleKeyDown(const U16 key, MASK mask); - /*virtual*/ void resetMaskKeys(); - /*virtual*/ MASK currentMask(bool for_mouse_event); - /*virtual*/ void scanKeyboard(); - bool translateExtendedKey(const U16 os_key, const MASK mask, KEY *translated_key); - U16 inverseTranslateExtendedKey(const KEY translated_key); + ~LLKeyboardWin32() {}; + + bool handleKeyUp(const U16 key, MASK mask) override; + bool handleKeyDown(const U16 key, MASK mask) override; + void resetMaskKeys() override; + MASK currentMask(bool for_mouse_event) override; + void scanKeyboard() override; + + bool translateExtendedKey(const U16 os_key, const MASK mask, KEY *translated_key); + U16 inverseTranslateExtendedKey(const KEY translated_key); protected: MASK updateModifiers(); diff --git a/indra/llwindow/llwindow.cpp b/indra/llwindow/llwindow.cpp index 4016f420e8..a4b7a65cb2 100644 --- a/indra/llwindow/llwindow.cpp +++ b/indra/llwindow/llwindow.cpp @@ -27,14 +27,14 @@ #include "linden_common.h" #include "llwindowheadless.h" -#if LL_MESA_HEADLESS -#include "llwindowmesaheadless.h" -#elif LL_SDL -#include "llwindowsdl.h" -#elif LL_WINDOWS +#if LL_WINDOWS #include "llwindowwin32.h" #elif LL_DARWIN #include "llwindowmacosx.h" +#elif LL_MESA_HEADLESS +#include "llwindowmesaheadless.h" +#elif LL_LINUX +#include "llwindowsdl.h" #endif #include "llerror.h" @@ -72,13 +72,13 @@ S32 OSMessageBox(const std::string& text, const std::string& caption, U32 type) S32 result = 0; LL_WARNS() << "OSMessageBox: " << text << LL_ENDL; -#if LL_MESA_HEADLESS // !!! *FIX: (?) - return OSBTN_OK; -#elif LL_WINDOWS +#if LL_WINDOWS result = OSMessageBoxWin32(text, caption, type); #elif LL_DARWIN result = OSMessageBoxMacOSX(text, caption, type); -#elif LL_SDL +#elif LL_MESA_HEADLESS // !!! *FIX: (?) + return OSBTN_OK; +#elif LL_LINUX result = OSMessageBoxSDL(text, caption, type); #else #error("OSMessageBox not implemented for this platform!") @@ -263,7 +263,7 @@ std::vector LLWindow::getDynamicFallbackFontList() return LLWindowWin32::getDynamicFallbackFontList(); #elif LL_DARWIN return LLWindowMacOSX::getDynamicFallbackFontList(); -#elif LL_SDL +#elif LL_LINUX return LLWindowSDL::getDynamicFallbackFontList(); #else return std::vector(); @@ -342,12 +342,12 @@ bool LLSplashScreen::isVisible() // static LLSplashScreen *LLSplashScreen::create() { -#if LL_MESA_HEADLESS || LL_SDL // !!! *FIX: (?) - return 0; -#elif LL_WINDOWS +#if LL_WINDOWS return new LLSplashScreenWin32; #elif LL_DARWIN return new LLSplashScreenMacOSX; +#elif LL_MESA_HEADLESS || LL_LINUX // !!! *FIX: (?) + return 0; #else #error("LLSplashScreen not implemented on this platform!") #endif @@ -415,22 +415,22 @@ LLWindow* LLWindowManager::createWindow( if (use_gl) { -#if LL_MESA_HEADLESS - new_window = new LLWindowMesaHeadless(callbacks, - title, name, x, y, width, height, flags, - fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth); -#elif LL_SDL - new_window = new LLWindowSDL(callbacks, - title, name, x, y, width, height, flags, - fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth, fsaa_samples); -#elif LL_WINDOWS +#if LL_WINDOWS new_window = new LLWindowWin32(callbacks, title, name, x, y, width, height, flags, - fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth, fsaa_samples, max_cores, max_gl_version); + fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth, fsaa_samples, max_cores, max_vram, max_gl_version); #elif LL_DARWIN new_window = new LLWindowMacOSX(callbacks, title, name, x, y, width, height, flags, fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth, fsaa_samples); +#elif LL_MESA_HEADLESS + new_window = new LLWindowMesaHeadless(callbacks, + title, name, x, y, width, height, flags, + fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth); +#elif LL_LINUX + new_window = new LLWindowSDL(callbacks, + title, name, x, y, width, height, flags, + fullscreen, clearBg, enable_vsync, use_gl, ignore_pixel_depth, fsaa_samples); #endif } else diff --git a/indra/llwindow/llwindowsdl.h b/indra/llwindow/llwindowsdl.h index 7ad30d41ce..609d8a6f49 100644 --- a/indra/llwindow/llwindowsdl.h +++ b/indra/llwindow/llwindowsdl.h @@ -29,6 +29,7 @@ // Simple Directmedia Layer (http://libsdl.org/) implementation of LLWindow class +#if LL_LINUX #include "llwindow.h" #include "lltimer.h" @@ -300,4 +301,5 @@ public: S32 OSMessageBoxSDL(const std::string& text, const std::string& caption, U32 type); +#endif //LL_LINUX #endif //LL_LLWINDOWSDL_H diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 29dbceedac..7fbf214dcf 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -28,6 +28,7 @@ include(LLKDU) include(LLPhysicsExtensions) include(LLPrimitive) include(LLWindow) +include(SDL2) include(NDOF) include(NVAPI) include(OPENAL) @@ -1745,7 +1746,7 @@ if (WINDOWS) # And of course it's straightforward to read a text file in Python. set(COPY_INPUT_DEPENDENCIES - # The following commented dependencies are determined at variably at build time. Can't do this here. + # The following commented dependencies are determined variably at build time. Can't do this here. ${CMAKE_SOURCE_DIR}/../etc/message.xml ${CMAKE_SOURCE_DIR}/../scripts/messages/message_template.msg ${SHARED_LIB_STAGING_DIR}/openjp2.dll @@ -2245,6 +2246,7 @@ if (LL_TESTS) lllogin llplugin llappearance + ll::SDL2 ) set_source_files_properties( @@ -2296,6 +2298,7 @@ if (LL_TESTS) lllogin llprimitive lllogin + ll::SDL2 ) LL_ADD_INTEGRATION_TEST(cppfeatures diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 60d8c6db76..55f8f77383 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2612,6 +2612,28 @@ Value 1 + EnableGameControlInput + + Comment + Transmit game controller input to server + Persist + 1 + Type + Boolean + Value + 0 + + EnableGameControlKeyboardInput + + Comment + Send 'unhandled' keystrokes as GameInput to server + Persist + 1 + Type + Boolean + Value + 0 + EnableGestureSounds Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 6fd58ef1be..ffa742d154 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -64,6 +64,7 @@ #include "llluamanager.h" #include "llurlfloaterdispatchhandler.h" #include "llviewerjoystick.h" +#include "llgamecontrol.h" #include "llcalc.h" #include "llconversationlog.h" #if LL_WINDOWS @@ -1132,6 +1133,7 @@ bool LLAppViewer::init() { LLViewerJoystick::getInstance()->init(false); } + LLGameControl::init(); try { @@ -1416,6 +1418,59 @@ bool LLAppViewer::frame() return ret; } + +// static +bool packGameControlInput(LLMessageSystem* msg) +{ + if (! LLGameControl::computeFinalInputAndCheckForChanges()) + { + return false; + } + if (!gSavedSettings.getBOOL("EnableGameControlInput")) + { + LLGameControl::clearAllInput(); + return false; + } + + msg->newMessageFast(_PREHASH_GameControlInput); + msg->nextBlock("AgentData"); + msg->addUUID("AgentID", gAgentID); + msg->addUUID("SessionID", gAgentSessionID); + + const LLGameControl::State& state = LLGameControl::getState(); + + size_t num_indices = state.mAxes.size(); + for (U8 i = 0; i < num_indices; ++i) + { + if (state.mAxes[i] != state.mPrevAxes[i]) + { + // only pack an axis if it differs from previously packed value + msg->nextBlockFast(_PREHASH_AxisData); + msg->addU8Fast(_PREHASH_Index, i); + msg->addS16Fast(_PREHASH_Value, state.mAxes[i]); + } + } + + U32 button_flags = state.mButtons; + if (button_flags > 0) + { + std::vector buttons; + for (U8 i = 0; i < 32; i++) + { + if (button_flags & (0x1 << i)) + { + buttons.push_back(i); + } + } + msg->nextBlockFast(_PREHASH_ButtonData); + msg->addBinaryDataFast(_PREHASH_Data, (void*)(buttons.data()), (S32)(buttons.size())); + } + + LLGameControl::updateResendPeriod(); + return true; +} + + bool LLAppViewer::doFrame() { LL_RECORD_BLOCK_TIME(FTM_FRAME); @@ -1526,6 +1581,15 @@ bool LLAppViewer::doFrame() joystick->scanJoystick(); gKeyboard->scanKeyboard(); gViewerInput.scanMouse(); + + LLGameControl::setIncludeKeyboardButtons(gSavedSettings.getBOOL("EnableGameControlKeyboardInput")); + LLGameControl::processEvents(gFocusMgr.getAppHasFocus()); + // to help minimize lag we send GameInput packets immediately + // after getting the latest GameController input + if (packGameControlInput(gMessageSystem)) + { + gAgent.sendMessage(); + } } // Update state based on messages, user input, object idle. @@ -1961,6 +2025,7 @@ bool LLAppViewer::cleanup() // Turn off Space Navigator and similar devices LLViewerJoystick::getInstance()->terminate(); } + LLGameControl::terminate(); LL_INFOS() << "Cleaning up Objects" << LL_ENDL; diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index d5e3627d8e..3639064cc4 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -36,11 +36,8 @@ #include "llviewercontrol.h" #include "llwindow.h" // beforeDialog() -#if LL_SDL -#include "llwindowsdl.h" // for some X/GTK utils to help with filepickers -#endif // LL_SDL - #if LL_LINUX +#include "llwindowsdl.h" // for some X/GTK utils to help with filepickers #include "llhttpconstants.h" // file picker uses some of thes constants on Linux #endif diff --git a/indra/newview/llkeyconflict.cpp b/indra/newview/llkeyconflict.cpp index 666ab4f5d0..b9456349cd 100644 --- a/indra/newview/llkeyconflict.cpp +++ b/indra/newview/llkeyconflict.cpp @@ -99,29 +99,34 @@ LLKeyConflictHandler::~LLKeyConflictHandler() // Note: does not reset bindings if temporary file was used } -bool LLKeyConflictHandler::canHandleControl(const std::string &control_name, EMouseClickType mouse_ind, KEY key, MASK mask) +bool LLKeyConflictHandler::canHandleControl(const std::string &control_name, EMouseClickType mouse_ind, KEY key, MASK mask) const { - return mControlsMap[control_name].canHandle(mouse_ind, key, mask); + control_map_t::const_iterator iter = mControlsMap.find(control_name); + if (iter != mControlsMap.end()) + { + return iter->second.canHandle(mouse_ind, key, mask); + } + return false; } -bool LLKeyConflictHandler::canHandleKey(const std::string &control_name, KEY key, MASK mask) +bool LLKeyConflictHandler::canHandleKey(const std::string &control_name, KEY key, MASK mask) const { return canHandleControl(control_name, CLICK_NONE, key, mask); } -bool LLKeyConflictHandler::canHandleMouse(const std::string &control_name, EMouseClickType mouse_ind, MASK mask) +bool LLKeyConflictHandler::canHandleMouse(const std::string &control_name, EMouseClickType mouse_ind, MASK mask) const { return canHandleControl(control_name, mouse_ind, KEY_NONE, mask); } -bool LLKeyConflictHandler::canHandleMouse(const std::string &control_name, S32 mouse_ind, MASK mask) +bool LLKeyConflictHandler::canHandleMouse(const std::string &control_name, S32 mouse_ind, MASK mask) const { return canHandleControl(control_name, (EMouseClickType)mouse_ind, KEY_NONE, mask); } -bool LLKeyConflictHandler::canAssignControl(const std::string &control_name) +bool LLKeyConflictHandler::canAssignControl(const std::string &control_name) const { - control_map_t::iterator iter = mControlsMap.find(control_name); + control_map_t::const_iterator iter = mControlsMap.find(control_name); if (iter != mControlsMap.end()) { return iter->second.mAssignable; @@ -284,7 +289,7 @@ void LLKeyConflictHandler::loadFromSettings(const LLViewerInput::KeyMode& keymod LLKeyboard::keyFromString(it->key, &key); } LLKeyboard::maskFromString(it->mask, &mask); - // Note: it->command is also the name of UI element, howhever xml we are loading from + // Note: it->command is also the name of UI element, however xml we are loading from // might not know all the commands, so UI will have to know what to fill by its own // Assumes U32_MAX conflict mask, and is assignable by default, // but assignability might have been overriden by generatePlaceholders. diff --git a/indra/newview/llkeyconflict.h b/indra/newview/llkeyconflict.h index 6c01ddb7a7..21990688a2 100644 --- a/indra/newview/llkeyconflict.h +++ b/indra/newview/llkeyconflict.h @@ -44,7 +44,7 @@ public: LLKeyData getKeyData(U32 index) { return mKeyBind.getKeyData(index); } void setPrimaryKeyData(const LLKeyData& data) { mKeyBind.replaceKeyData(data, 0); } void setKeyData(const LLKeyData& data, U32 index) { mKeyBind.replaceKeyData(data, index); } - bool canHandle(EMouseClickType mouse, KEY key, MASK mask) { return mKeyBind.canHandle(mouse, key, mask); } + bool canHandle(EMouseClickType mouse, KEY key, MASK mask) const { return mKeyBind.canHandle(mouse, key, mask); } LLKeyBind mKeyBind; bool mAssignable; // whether user can change key or key simply acts as placeholder @@ -76,11 +76,11 @@ public: LLKeyConflictHandler(ESourceMode mode); ~LLKeyConflictHandler(); - bool canHandleControl(const std::string &control_name, EMouseClickType mouse_ind, KEY key, MASK mask); - bool canHandleKey(const std::string &control_name, KEY key, MASK mask); - bool canHandleMouse(const std::string &control_name, EMouseClickType mouse_ind, MASK mask); - bool canHandleMouse(const std::string &control_name, S32 mouse_ind, MASK mask); //Just for convinience - bool canAssignControl(const std::string &control_name); + bool canHandleControl(const std::string &control_name, EMouseClickType mouse_ind, KEY key, MASK mask) const; + bool canHandleKey(const std::string &control_name, KEY key, MASK mask) const; + bool canHandleMouse(const std::string &control_name, EMouseClickType mouse_ind, MASK mask) const; + bool canHandleMouse(const std::string &control_name, S32 mouse_ind, MASK mask) const; //Just for convenience + bool canAssignControl(const std::string &control_name) const; static bool isReservedByMenu(const KEY &key, const MASK &mask); static bool isReservedByMenu(const LLKeyData &data); diff --git a/indra/newview/skins/default/xui/en/floater_preferences.xml b/indra/newview/skins/default/xui/en/floater_preferences.xml index 4b0e0bb221..4614f2f06c 100644 --- a/indra/newview/skins/default/xui/en/floater_preferences.xml +++ b/indra/newview/skins/default/xui/en/floater_preferences.xml @@ -170,6 +170,13 @@ layout="topleft" help_topic="preferences_controls_tab" name="controls" /> + diff --git a/indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml b/indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml new file mode 100644 index 0000000000..4b693e8955 --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/indra/newview/tests/llgamecontrol_stub.cpp b/indra/newview/tests/llgamecontrol_stub.cpp new file mode 100644 index 0000000000..0872f647d7 --- /dev/null +++ b/indra/newview/tests/llgamecontrol_stub.cpp @@ -0,0 +1,76 @@ +/** + * @file llgamecontrol_stub.h + * @brief Stubbery for LLGameControl + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llgamecontrol.h" + +#include "SDL2/SDL_events.h" + + +void LLGameControl::addKeyButtonMap(U16 key, U8 button) +{ +} + +void LLGameControl::removeKeyButtonMap(U16 key) +{ +} + +void LLGameControl::addKeyAxisMap(U16 key, U8 axis, bool positive) +{ +} + +void LLGameControl::removeKeyAxisMap(U16 key) +{ +} + +void LLGameControl::onKeyDown(U16 key, U32 mask) +{ +} + +void LLGameControl::onKeyUp(U16 key, U32 mask) +{ +} + +// static +bool LLGameControl::isInitialized() +{ + return false; +} + +// static +void LLGameControl::init() +{ +} + +// static +void LLGameControl::terminate() +{ +} + +// static +void LLGameControl::processEvents(bool app_has_focus) +{ +} + diff --git a/indra/newview/tests/llversioninfo_test.cpp b/indra/newview/tests/llversioninfo_test.cpp index 8049e67fc5..9eb5146f2b 100644 --- a/indra/newview/tests/llversioninfo_test.cpp +++ b/indra/newview/tests/llversioninfo_test.cpp @@ -29,7 +29,10 @@ #include "../llversioninfo.h" - #include +#include + +#include "llgamecontrol_stub.cpp" + // LL_VIEWER_CHANNEL is a macro defined on the compiler command line. The // macro expands to the string name of the channel, but without quotes. We diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index daa7e58211..16904cc43c 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -584,6 +584,9 @@ class Windows_x86_64_Manifest(ViewerManifest): self.path("vivoxsdk_x64.dll") self.path("ortp_x64.dll") + # SDL2 + self.path("SDL2.dll") + # BugSplat if self.args.get('bugsplat'): self.path("BsSndRpt64.exe") @@ -928,6 +931,7 @@ class Darwin_x86_64_Manifest(ViewerManifest): with self.prefix(src=relpkgdir, dst=""): self.path("libndofdev.dylib") + self.path("libSDL2-*.dylib") with self.prefix(src_dst="cursors_mac"): self.path("*.tif") @@ -1406,7 +1410,7 @@ class Linux_x86_64_Manifest(LinuxManifest): pkgdir = self.args['package_dir'] relpkgdir = os.path.join(pkgdir, "lib", "release") - debpkgdir = os.path.join(pkgdir, "lib", "debug") + #debpkgdir = os.path.join(pkgdir, "lib", "debug") with self.prefix(src=relpkgdir, dst="lib"): self.path("libapr-1.so*") @@ -1415,6 +1419,15 @@ class Linux_x86_64_Manifest(LinuxManifest): self.path_optional("libjemalloc*.so") + self.path("libdb*.so") + self.path("libuuid.so*") + self.path("libdirectfb-1.*.so.*") + self.path("libfusion-1.*.so.*") + self.path("libdirect-1.*.so.*") + self.path("libopenjp2.so*") + self.path("libdirectfb-1.4.so.5") + self.path("libfusion-1.4.so.5") + self.path("libdirect-1.4.so.5*") self.path("libalut.so*") self.path("libopenal.so*") self.path("libopenal.so", "libvivoxoal.so.1") # vivox's sdk expects this soname diff --git a/scripts/messages/message_template.msg b/scripts/messages/message_template.msg index 1450c111c2..4bbcbd369d 100755 --- a/scripts/messages/message_template.msg +++ b/scripts/messages/message_template.msg @@ -6,7 +6,7 @@ version 2.0 // numbers. Each message must be numbered relative to the // other messages of that type. The current highest number // for each type is listed below: -// Low: 430 +// Low: 431 // Medium: 18 // High: 30 // PLEASE UPDATE THIS WHEN YOU ADD A NEW MESSAGE! @@ -9133,3 +9133,36 @@ version 2.0 } } +// viewer->sim +// GameControlInput - input from game controller +// This message is split into two Variable chunks: +// +// AxisData = list of {Index:Value} pairs. Value is an S16 that maps to range [-1, 1]. +// ButtonData = list of indices of pressed buttons +// +// Any Axis ommitted from the message is assumed by the receiving Simulator to be unchanged +// from its last message. +// +// Any Button ommitted from the message is assumed by the receiving Simulator to be unpressed. +// +// Since GameControlInput messages are sent unreliably: whenenver the changes stop the last +// message will be resent a few times in the hopes the server finally receives it. +// +{ + GameControlInput Medium 431 NotTrusted Zerocoded + { + AgentData Single + { AgentID LLUUID } + { SessionID LLUUID } + } + { + AxisData Variable + { Index U8 } + { Value S16 } + } + { + ButtonData Variable + { Data Variable 1 } + } +} + -- cgit v1.2.3 From ed6ecca2a45e52d9be1d91107b9643b5ecdfb8bf Mon Sep 17 00:00:00 2001 From: Leviathan Linden Date: Thu, 16 Nov 2023 13:53:37 -0800 Subject: avatar_motion-->GameControl translation and flycam --- autobuild.xml | 24 +- indra/llcommon/llinitparam.h | 1 + indra/llwindow/CMakeLists.txt | 2 + indra/llwindow/llgamecontrol.cpp | 687 ++++++++++++++++----- indra/llwindow/llgamecontrol.h | 156 ++++- indra/llwindow/llgamecontroltranslator.cpp | 344 +++++++++++ indra/llwindow/llgamecontroltranslator.h | 83 +++ indra/llwindow/llkeyboard.cpp | 3 +- indra/llwindow/llkeyboardheadless.cpp | 1 + indra/llwindow/llkeyboardmacosx.cpp | 1 + indra/llwindow/llkeyboardsdl.cpp | 1 + indra/llwindow/llkeyboardwin32.cpp | 1 + indra/newview/CMakeLists.txt | 2 + indra/newview/app_settings/settings.xml | 19 +- indra/newview/llagent.cpp | 271 +++++++- indra/newview/llagent.h | 32 + indra/newview/llappviewer.cpp | 109 +++- indra/newview/llfloaterjoystick.cpp | 26 +- indra/newview/llfloaterpreference.cpp | 365 ++++++++++- indra/newview/llfloaterpreference.h | 104 +++- indra/newview/llflycam.cpp | 117 ++++ indra/newview/llflycam.h | 55 ++ indra/newview/llviewerinput.cpp | 9 +- indra/newview/llviewerjoystick.cpp | 27 +- indra/newview/llviewerjoystick.h | 2 + .../skins/default/textures/bottomtray/Dpad.png | Bin 0 -> 765 bytes .../skins/default/xui/en/floater_joystick.xml | 36 +- .../skins/default/xui/en/floater_preferences.xml | 10 +- .../xui/en/game_control_table_camera_rows.xml | 106 ++++ .../default/xui/en/game_control_table_columns.xml | 15 + .../default/xui/en/game_control_table_rows.xml | 117 ++++ .../xui/en/panel_preferences_game_control.xml | 244 ++++++++ .../xui/en/panel_preferences_game_controls.xml | 32 - .../default/xui/en/panel_preferences_move.xml | 2 +- indra/newview/tests/llgamecontrol_stub.cpp | 43 +- indra/newview/tests/llversioninfo_test.cpp | 2 +- 36 files changed, 2687 insertions(+), 362 deletions(-) create mode 100644 indra/llwindow/llgamecontroltranslator.cpp create mode 100644 indra/llwindow/llgamecontroltranslator.h create mode 100644 indra/newview/llflycam.cpp create mode 100644 indra/newview/llflycam.h create mode 100644 indra/newview/skins/default/textures/bottomtray/Dpad.png create mode 100644 indra/newview/skins/default/xui/en/game_control_table_camera_rows.xml create mode 100644 indra/newview/skins/default/xui/en/game_control_table_columns.xml create mode 100644 indra/newview/skins/default/xui/en/game_control_table_rows.xml create mode 100644 indra/newview/skins/default/xui/en/panel_preferences_game_control.xml delete mode 100644 indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml diff --git a/autobuild.xml b/autobuild.xml index 4ec43be8de..2d06a159df 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -1,6 +1,6 @@ - + version 1.3 type @@ -1207,18 +1207,6 @@ name darwin64 - linux64 - - archive - - hash - ffbdd109356d66ddfefd8a5d57f63f1f - url - http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/533/1144/libhunspell-1.3.2.500526-linux64-500526.tar.bz2 - - name - linux64 - windows64 archive @@ -1550,7 +1538,7 @@ name linux64 - windows64 + windows archive @@ -1560,7 +1548,7 @@ http://automated-builds-secondlife-com.s3.amazonaws.com/ct2/60043/564063/llphysicsextensions_stub-1.0.542456-windows-542456.tar.bz2 name - windows64 + windows license @@ -1612,7 +1600,7 @@ https://s3-proxy.lindenlab.com/private-builds-secondlife-com/ct2/84731/788139/llphysicsextensions_tpv-1.0.561752-windows64-561752.tar.bz2 name - windows64 + windows license @@ -3292,7 +3280,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors name linux64 - windows64 + windows configurations @@ -3449,7 +3437,7 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors build_directory build-vc${AUTOBUILD_VSVER|170}-$AUTOBUILD_ADDRSIZE name - windows64 + windows license diff --git a/indra/llcommon/llinitparam.h b/indra/llcommon/llinitparam.h index 32d7b17034..c6a8dd737e 100644 --- a/indra/llcommon/llinitparam.h +++ b/indra/llcommon/llinitparam.h @@ -2836,3 +2836,4 @@ namespace LLInitParam #endif // LL_LLPARAM_H + diff --git a/indra/llwindow/CMakeLists.txt b/indra/llwindow/CMakeLists.txt index e251af3e6c..ebac55cb9c 100644 --- a/indra/llwindow/CMakeLists.txt +++ b/indra/llwindow/CMakeLists.txt @@ -23,6 +23,7 @@ include(SDL2) set(llwindow_SOURCE_FILES llcursortypes.cpp llgamecontrol.cpp + llgamecontroltranslator.cpp llkeyboard.cpp llkeyboardheadless.cpp llwindowheadless.cpp @@ -35,6 +36,7 @@ set(llwindow_HEADER_FILES llcursortypes.h llgamecontrol.h + llgamecontroltranslator.h llkeyboard.h llkeyboardheadless.h llwindowheadless.h diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 5dc01c5e54..9853eb763a 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -28,40 +28,198 @@ #include #include -#include +#include #include "SDL2/SDL.h" #include "SDL2/SDL_gamecontroller.h" #include "SDL2/SDL_joystick.h" +#include "indra_constants.h" +#include "llgamecontroltranslator.h" + constexpr size_t NUM_AXES = 6; +std::string LLGameControl::InputChannel::getLocalName() const +{ + // HACK: we hard-code English channel names, but + // they should be loaded from localized XML config files. + std::string name = " "; + if (mType == LLGameControl::InputChannel::TYPE_AXIS) + { + if (mIndex < (U8)(NUM_AXES)) + { + name = "AXIS_"; + name.append(std::to_string((S32)(mIndex))); + if (mSign < 0) + { + name.append("-"); + } + else if (mSign > 0) + { + name.append("+"); + } + } + } + else if (mType == LLGameControl::InputChannel::TYPE_BUTTON) + { + constexpr U8 NUM_BUTTONS = 32; + if (mIndex < NUM_BUTTONS) + { + name = "BUTTON_"; + name.append(std::to_string((S32)(mIndex))); + } + } + return name; +} + +std::string LLGameControl::InputChannel::getRemoteName() const +{ + // HACK: we hard-code English channel names, but + // they should be loaded from localized XML config files. + std::string name = " "; + // GAME_CONTROL_AXIS_LEFTX, GAME_CONTROL_BUTTON_A, etc + if (mType == LLGameControl::InputChannel::TYPE_AXIS) + { + switch(mIndex) + { + case 0: + name = "GAME_CONTROL_AXIS_LEFTX"; + break; + case 1: + name = "GAME_CONTROL_AXIS_LEFTY"; + break; + case 2: + name = "GAME_CONTROL_AXIS_RIGHTX"; + break; + case 3: + name = "GAME_CONTROL_AXIS_RIGHTY"; + break; + case 4: + name = "GAME_CONTROL_AXIS_PADDLELEFT"; + break; + case 5: + name = "GAME_CONTROL_AXIS_PADDLERIGHT"; + break; + default: + break; + } + } + else if (mType == LLGameControl::InputChannel::TYPE_BUTTON) + { + switch(mIndex) + { + case 0: + name = "GAME_CONTROL_BUTTON_A"; + break; + case 1: + name = "GAME_CONTROL_BUTTON_B"; + break; + case 2: + name = "GAME_CONTROL_BUTTON_X"; + break; + case 3: + name = "GAME_CONTROL_BUTTON_Y"; + break; + case 4: + name = "GAME_CONTROL_BUTTON_BACK"; + break; + case 5: + name = "GAME_CONTROL_BUTTON_GUIDE"; + break; + case 6: + name = "GAME_CONTROL_BUTTON_START"; + break; + case 7: + name = "GAME_CONTROL_BUTTON_LEFTSTICK"; + break; + case 8: + name = "GAME_CONTROL_BUTTON_RIGHTSTICK"; + break; + case 9: + name = "GAME_CONTROL_BUTTON_LEFTSHOULDER"; + break; + case 10: + name = "GAME_CONTROL_BUTTON_RIGHTSHOULDER"; + break; + case 11: + name = "GAME_CONTROL_BUTTON_DPAD_UP"; + break; + case 12: + name = "GAME_CONTROL_BUTTON_DPAD_DOWN"; + break; + case 13: + name = "GAME_CONTROL_BUTTON_DPAD_LEFT"; + break; + case 14: + name = "GAME_CONTROL_BUTTON_DPAD_RIGHT"; + break; + case 15: + name = "GAME_CONTROL_BUTTON_MISC1"; + break; + case 16: + name = "GAME_CONTROL_BUTTON_PADDLE1"; + break; + case 17: + name = "GAME_CONTROL_BUTTON_PADDLE2"; + break; + case 18: + name = "GAME_CONTROL_BUTTON_PADDLE3"; + break; + case 19: + name = "GAME_CONTROL_BUTTON_PADDLE4"; + break; + case 20: + name = "GAME_CONTROL_BUTTON_TOUCHPAD"; + break; + default: + break; + } + } + return name; +} + + // internal class for managing list of controllers and per-controller state class LLGameControllerManager { public: + using ActionToChannelMap = std::map< std::string, LLGameControl::InputChannel >; + LLGameControllerManager(); + void addController(SDL_JoystickID id, SDL_GameController* controller); void removeController(SDL_JoystickID id); void onAxis(SDL_JoystickID id, U8 axis, S16 value); void onButton(SDL_JoystickID id, U8 button, bool pressed); - void onKeyButton(U8 button, bool pressed); - void onKeyAxis(U8 axis, U16 value); - - void clearAllInput(); - void clearAllKeys(); + void clearAllState(); size_t getControllerIndex(SDL_JoystickID id) const; + void accumulateInternalState(); void computeFinalState(LLGameControl::State& state); + LLGameControl::InputChannel getChannelByName(const std::string& name) const; + LLGameControl::InputChannel getChannelByActionName(const std::string& action_name) const; + + bool updateActionMap(const std::string& name, LLGameControl::InputChannel channel); + U32 computeInternalActionFlags(); + void getCameraInputs(std::vector& inputs_out); + void setExternalActionFlags(U32 action_flags); + void clear(); private: std::vector mControllerIDs; std::vector mControllers; - std::vector mStates; - LLGameControl::State mKeyboardState; + std::vector mStates; // one state per device + + LLGameControl::State mExternalState; + LLGameControlTranslator mActionTranslator; + ActionToChannelMap mCameraChannelMap; + std::vector mAxesAccumulator; + U32 mButtonAccumulator { 0 }; + U32 mLastActionFlags { 0 }; + U32 mLastCameraActionFlags { 0 }; }; // local globals @@ -83,15 +241,16 @@ namespace constexpr U64 MSEC_PER_NSEC = 1e6; constexpr U64 FIRST_RESEND_PERIOD = 100 * MSEC_PER_NSEC; constexpr U64 RESEND_EXPANSION_RATE = 10; - LLGameControl::State g_gameControlState; + LLGameControl::State g_outerState; // from controller devices + LLGameControl::State g_innerState; // state from gAgent + LLGameControl::State g_finalState; // sum of inner and outer U64 g_lastSend = 0; U64 g_nextResendPeriod = FIRST_RESEND_PERIOD; - std::map g_keyButtonMap; - std::map g_keyAxisMapPositive; - std::map g_keyAxisMapNegative; - - bool g_includeKeyboardButtons = false; + bool g_sendToServer = false; + bool g_controlAgent = false; + bool g_translateAgentActions = false; + LLGameControl::AgentControlMode g_agentControlMode = LLGameControl::CONTROL_MODE_AVATAR; constexpr U8 MAX_AXIS = 5; constexpr U8 MAX_BUTTON = 31; @@ -108,6 +267,16 @@ LLGameControl::State::State() : mButtons(0) mPrevAxes.resize(NUM_AXES, 0); } +void LLGameControl::State::clear() +{ + std::fill(mAxes.begin(), mAxes.end(), 0); + + // DO NOT clear mPrevAxes because those are managed by external logic. + //std::fill(mPrevAxes.begin(), mPrevAxes.end(), 0); + + mButtons = 0; +} + bool LLGameControl::State::onButton(U8 button, bool pressed) { U32 old_buttons = mButtons; @@ -126,6 +295,71 @@ bool LLGameControl::State::onButton(U8 button, bool pressed) return changed; } +LLGameControllerManager::LLGameControllerManager() +{ + mAxesAccumulator.resize(NUM_AXES, 0); + + // Here we build an invarient map between the named agent actions + // and control bit sent to the server. This map will be used, + // in combination with the action->InputChannel map below, + // to maintain an inverse map from control bit masks to GameControl data. + LLGameControlTranslator::ActionToMaskMap actions; + actions["push+"] = AGENT_CONTROL_AT_POS | AGENT_CONTROL_FAST_AT; + actions["push-"] = AGENT_CONTROL_AT_NEG | AGENT_CONTROL_FAST_AT; + actions["slide+"] = AGENT_CONTROL_LEFT_POS | AGENT_CONTROL_FAST_LEFT; + actions["slide-"] = AGENT_CONTROL_LEFT_NEG | AGENT_CONTROL_FAST_LEFT; + actions["jump+"] = AGENT_CONTROL_UP_POS | AGENT_CONTROL_FAST_UP; + actions["jump-"] = AGENT_CONTROL_UP_NEG | AGENT_CONTROL_FAST_UP; + actions["turn+"] = AGENT_CONTROL_YAW_POS; + actions["turn-"] = AGENT_CONTROL_YAW_NEG; + actions["look+"] = AGENT_CONTROL_PITCH_POS; + actions["look-"] = AGENT_CONTROL_PITCH_NEG; + actions["stop"] = AGENT_CONTROL_STOP; + // These are HACKs. We borrow some AGENT_CONTROL bits for "unrelated" features. + // Not a problem because these bits are only used internally. + actions["toggle_run"] = AGENT_CONTROL_NUDGE_AT_POS; // HACK + actions["toggle_fly"] = AGENT_CONTROL_FLY; // HACK + actions["toggle_sit"] = AGENT_CONTROL_SIT_ON_GROUND; // HACK + actions["toggle_flycam"] = AGENT_CONTROL_NUDGE_AT_NEG; // HACK + mActionTranslator.setAvailableActions(actions); + + // Here we build a list of pairs between named agent actions and + // GameControl channels. Note: we only supply the non-signed names + // (e.g. "push" instead of "push+" and "push-") because mActionTranator + // automatially expands action names as necessary. + using type = LLGameControl::InputChannel::Type; + std::vector< std::pair< std::string, LLGameControl::InputChannel> > agent_defaults = + { + { "push", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), 1 } }, + { "slide", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), 1 } }, + { "jump", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERLEFT), 1 } }, + { "turn", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), 1 } }, + { "look", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), 1 } }, + { "toggle_run", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSHOULDER) } }, + { "toggle_fly", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_UP) } }, + { "toggle_sit", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_DOWN) } }, + { "toggle_flycam", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_RIGHTSHOULDER) } }, + { "stop", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSTICK) } } + }; + mActionTranslator.setMappings(agent_defaults); + + // Camera actions don't need bitwise translation, so we maintain the map in + // here directly rather than using an LLGameControlTranslator. + // Note: there must NOT be duplicate names between avatar and camera actions + mCameraChannelMap = + { + { "move", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), -1 } }, + { "pan", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), -1 } }, + { "rise", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT), -1 } }, + { "pitch", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), -1 } }, + { "yaw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), -1 } }, + { "zoom", { type::TYPE_NONE, 0 } }, + // TODO?: allow flycam to roll + //{ "roll_ccw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT) } }, + //{ "roll_cw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERLEFT) } } + }; +} + void LLGameControllerManager::addController(SDL_JoystickID id, SDL_GameController* controller) { if (controller) @@ -198,6 +432,25 @@ void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) size_t index = getControllerIndex(id); if (index < mControllers.size()) { + // Note: the RAW analog joystics provide NEGATIVE X,Y values for LEFT,FORWARD + // whereas those directions are actually POSITIVE in SL's local right-handed + // reference frame. Therefore we implicitly negate those axes here where + // they are extracted from SDL, before being used anywhere. + if (axis < SDL_CONTROLLER_AXIS_TRIGGERLEFT) + { + // Note: S16 value is in range [-32768, 32767] which means + // the negative range has an extra possible value. We need + // to add (or subtract) one during negation. + if (value < 0) + { + value = - (value + 1); + } + else if (value > 0) + { + value = (-value) - 1; + } + } + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec << " axis=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; @@ -219,83 +472,145 @@ void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool presse } } -void LLGameControllerManager::onKeyButton(U8 button, bool pressed) +void LLGameControllerManager::clearAllState() { - if (mKeyboardState.onButton(button, pressed)) + for (auto& state : mStates) { - LL_DEBUGS("SDL2") << " keyboard button i=" << (S32)(button) << " pressed=" << pressed << LL_ENDL; + state.clear(); } + mExternalState.clear(); + mLastActionFlags = 0; + mLastCameraActionFlags = 0; } -void LLGameControllerManager::onKeyAxis(U8 axis, U16 value) +void LLGameControllerManager::accumulateInternalState() { - if (mKeyboardState.mAxes[axis] != value) + // clear the old state + std::fill(mAxesAccumulator.begin(), mAxesAccumulator.end(), 0); + mButtonAccumulator = 0; + + // accumulate the controllers + for (const auto& state : mStates) { - mKeyboardState.mAxes[axis] = value; - LL_DEBUGS("SDL2") << " keyboard axis i=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; + mButtonAccumulator |= state.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + // Note: we don't bother to clamp the axes yet + // because at this stage we haven't yet accumulated the "inner" state. + mAxesAccumulator[i] += (S32)(state.mAxes[i]); + } } } -void LLGameControllerManager::clearAllInput() +void LLGameControllerManager::computeFinalState(LLGameControl::State& final_state) { - for (auto& state : mStates) + // We assume accumulateInternalState() has already been called and we will + // finish by accumulating "external" state (if enabled) + U32 old_buttons = final_state.mButtons; + final_state.mButtons = mButtonAccumulator; + if (g_translateAgentActions) + { + // accumulate from mExternalState + final_state.mButtons |= mExternalState.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + mAxesAccumulator[i] += (S32)(mExternalState.mAxes[i]); + } + } + if (old_buttons != final_state.mButtons) { - state.mButtons = 0; - std::fill(state.mAxes.begin(), state.mAxes.end(), 0); + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + + // clamp the accumulated axes + for (size_t i = 0; i < NUM_AXES; ++i) + { + S32 new_axis = (S16)(std::min(std::max(mAxesAccumulator[i], -32768), 32767)); + // check for change + if (final_state.mAxes[i] != new_axis) + { + // When axis changes we explicitly update the corresponding prevAxis + // prior to storing new_axis. The only other place where prevAxis + // is updated in updateResendPeriod() which is explicitly called after + // a packet is sent. The result is: unchanged axes are included in + // first resend but not later ones. + final_state.mPrevAxes[i] = final_state.mAxes[i]; + final_state.mAxes[i] = new_axis; + g_nextResendPeriod = 0; // packet needs to go out ASAP + } } - mKeyboardState.mButtons = 0; - std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); } -void LLGameControllerManager::clearAllKeys() +LLGameControl::InputChannel LLGameControllerManager::getChannelByActionName(const std::string& name) const { - mKeyboardState.mButtons = 0; - std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); + LLGameControl::InputChannel channel = mActionTranslator.getChannelByAction(name); + if (channel.isNone()) + { + //maybe we're looking for a camera action + ActionToChannelMap::const_iterator itr = mCameraChannelMap.find(name); + if (itr != mCameraChannelMap.end()) + { + channel = itr->second; + } + } + return channel; } -void LLGameControllerManager::computeFinalState(LLGameControl::State& state) +bool LLGameControllerManager::updateActionMap(const std::string& action, LLGameControl::InputChannel channel) { - // clear the slate - std::vector axes_accumulator; - axes_accumulator.resize(NUM_AXES, 0); - U32 old_buttons = state.mButtons; - state.mButtons = 0; - - // accumulate the controllers - for (const auto& s : mStates) + bool success = mActionTranslator.updateMap(action, channel); + if (success) { - state.mButtons |= s.mButtons; - for (size_t i = 0; i < NUM_AXES; ++i) + mLastActionFlags = 0; + } + else + { + // maybe we're looking for a camera action + ActionToChannelMap::iterator itr = mCameraChannelMap.find(action); + if (itr != mCameraChannelMap.end()) { - axes_accumulator[i] += (S32)(s.mAxes[i]); + itr->second = channel; + success = true; } } + if (!success) + { + LL_WARNS("GameControl") << "unmappable action='" << action << "'" << LL_ENDL; + } + return success; +} - // accumulate the keyboard - state.mButtons |= mKeyboardState.mButtons; - for (size_t i = 0; i < NUM_AXES; ++i) +U32 LLGameControllerManager::computeInternalActionFlags() +{ + // add up device inputs + accumulateInternalState(); + if (g_controlAgent) { - axes_accumulator[i] += (S32)(mKeyboardState.mAxes[i]); + return mActionTranslator.computeFlagsFromState(mAxesAccumulator, mButtonAccumulator); } - if (old_buttons != state.mButtons) + return 0; +} + +void LLGameControllerManager::getCameraInputs(std::vector& inputs_out) +{ + // TODO: fill inputs_out with real data + inputs_out.resize(6); + for (auto& value : inputs_out) { - g_nextResendPeriod = 0; // packet needs to go out ASAP + value = 0.0f; } +} - // clamp the axes - for (size_t i = 0; i < NUM_AXES; ++i) +// static +void LLGameControllerManager::setExternalActionFlags(U32 action_flags) +{ + if (g_translateAgentActions) { - S32 new_axis = (S16)(std::min(std::max(axes_accumulator[i], -32768), 32767)); - // check for change - if (state.mAxes[i] != new_axis) + U32 active_flags = action_flags & mActionTranslator.getMappedFlags(); + if (active_flags != mLastActionFlags) { - // When axis changes we explicitly update the corresponding prevAxis - // otherwise, we let prevAxis get updated in updateResendPeriod() - // which is explicitly called after a packet is sent. This allows - // unchanged axes to be included in first resend but not later ones. - state.mPrevAxes[i] = state.mAxes[i]; - state.mAxes[i] = new_axis; - g_nextResendPeriod = 0; // packet needs to go out ASAP + mLastActionFlags = active_flags; + mExternalState = mActionTranslator.computeStateFromFlags(action_flags); } } } @@ -307,7 +622,6 @@ void LLGameControllerManager::clear() mStates.clear(); } - U64 get_now_nsec() { std::chrono::time_point t0; @@ -411,117 +725,27 @@ void LLGameControl::terminate() SDL_Quit(); } -// static -void LLGameControl::addKeyButtonMap(U16 key, U8 button) -{ - g_keyButtonMap[key] = button; -} - -// static -void LLGameControl::removeKeyButtonMap(U16 key) -{ - g_keyButtonMap.erase(key); -} - -// static -void LLGameControl::addKeyAxisMap(U16 key, U8 axis, bool positive) -{ - if (axis > MAX_AXIS) - { - return; - } - if (positive) - { - g_keyAxisMapPositive[key] = axis; - g_keyAxisMapNegative.erase(key); - } - else - { - g_keyAxisMapNegative[key] = axis; - g_keyAxisMapPositive.erase(key); - } -} - -// static -void LLGameControl::removeKeyAxisMap(U16 key) -{ - g_keyAxisMapPositive.erase(key); - g_keyAxisMapNegative.erase(key); -} - -// static -void LLGameControl::onKeyDown(U16 key, U32 mask) -{ - auto itr = g_keyButtonMap.find(key); - if (itr != g_keyButtonMap.end()) - { - g_manager.onKeyButton(itr->second, true); - } - else - { - itr = g_keyAxisMapPositive.find(key); - if (itr != g_keyAxisMapPositive.end()) - { - g_manager.onKeyAxis(itr->second, 32767); - } - else - { - itr = g_keyAxisMapNegative.find(key); - if (itr != g_keyAxisMapNegative.end()) - { - g_manager.onKeyAxis(itr->second, -32768); - } - } - } -} - -// static -void LLGameControl::onKeyUp(U16 key, U32 mask) -{ - auto itr = g_keyButtonMap.find(key); - if (itr != g_keyButtonMap.end()) - { - g_manager.onKeyButton(itr->second, true); - } - else - { - itr = g_keyAxisMapPositive.find(key); - if (itr != g_keyAxisMapPositive.end()) - { - g_manager.onKeyAxis(itr->second, 0); - } - else - { - itr = g_keyAxisMapNegative.find(key); - if (itr != g_keyAxisMapNegative.end()) - { - g_manager.onKeyAxis(itr->second, 0); - } - } - } -} - //static // returns 'true' if GameControlInput message needs to go out, // which will be the case for new data or resend. Call this right // before deciding to put a GameControlInput packet on the wire // or not. -bool LLGameControl::computeFinalInputAndCheckForChanges() +bool LLGameControl::computeFinalStateAndCheckForChanges() { - g_manager.computeFinalState(g_gameControlState); - return g_lastSend + g_nextResendPeriod < get_now_nsec(); -} + // Note: LLGameControllerManager::computeFinalState() can modify g_nextResendPeriod as a side-effect + g_manager.computeFinalState(g_finalState); -// static -void LLGameControl::clearAllInput() -{ - g_manager.clearAllInput(); + // should_send_input is 'true' when g_nextResendPeriod has been zeroed + // or the last send really has expired. + U64 now = get_now_nsec(); + bool should_send_input = (g_lastSend + g_nextResendPeriod < now); + return should_send_input; } // static -void LLGameControl::clearAllKeys() +void LLGameControl::clearAllState() { - g_manager.clearAllKeys(); + g_manager.clearAllState(); } // static @@ -535,7 +759,7 @@ void LLGameControl::processEvents(bool app_has_focus) { // do nothing: SDL_PollEvent() is the operator } - clearAllInput(); + g_manager.clearAllState(); return; } @@ -566,19 +790,141 @@ void LLGameControl::processEvents(bool app_has_focus) // static const LLGameControl::State& LLGameControl::getState() { - return g_gameControlState; + return g_finalState; } // static -void LLGameControl::setIncludeKeyboardButtons(bool include) +void LLGameControl::getCameraInputs(std::vector& inputs_out) { - g_includeKeyboardButtons = include; + return g_manager.getCameraInputs(inputs_out); } // static -bool LLGameControl::getIncludeKeyboardButtons() +void LLGameControl::enableSendToServer(bool enable) { - return g_includeKeyboardButtons; + g_sendToServer = enable; +} + +// static +void LLGameControl::enableControlAgent(bool enable) +{ + g_controlAgent = enable; +} + +// static +void LLGameControl::enableTranslateAgentActions(bool enable) +{ + g_translateAgentActions = enable; +} + +void LLGameControl::setAgentControlMode(LLGameControl::AgentControlMode mode) +{ + g_agentControlMode = mode; +} + +// static +bool LLGameControl::willSendToServer() +{ + return g_sendToServer; +} + +// static +bool LLGameControl::willControlAvatar() +{ + return g_controlAgent && g_agentControlMode == CONTROL_MODE_AVATAR; +} + +// static +bool LLGameControl::willControlFlycam() +{ + return g_controlAgent && g_agentControlMode == CONTROL_MODE_FLYCAM; +} + +// static +bool LLGameControl::willTranslateAgentActions() +{ + return g_translateAgentActions; +} + +/* +// static +LLGameControl::LocalControlMode LLGameControl::getLocalControlMode() +{ + return g_agentControlMode; +} +*/ + +// static +// +// Given a name like "AXIS_1-" or "BUTTON_5" returns the corresponding InputChannel +// If the axis name lacks the +/- postfix it assumes '+' postfix. +LLGameControl::InputChannel LLGameControl::getChannelByName(const std::string& name) +{ + LLGameControl::InputChannel channel; + // 'name' has two acceptable formats: AXIS_[sign] or BUTTON_ + if (name.length() < 6) + { + // name must be at least as long as 'AXIS_n' + return channel; + } + if (name.rfind("AXIS_", 0) == 0) + { + char c = name[5]; + if (c >= '0') + { + channel.mType = LLGameControl::InputChannel::Type::TYPE_AXIS; + channel.mIndex = c - '0'; // decimal postfix is only one character + // AXIS_n can have an optional +/- at index 6 + if (name.length() >= 6) + { + channel.mSign = (name[6] == '-') ? -1 : 1; + } + else + { + // assume positive axis when sign not provided + channel.mSign = 1; + } + } + } + else if (name.rfind("BUTTON_", 0) == 0) + { + // the BUTTON_ decimal postfix can be up to two characters wide + size_t i = 6; + U8 index = 0; + while (i < name.length() && i < 8 && name[i] <= '0') + { + index = index * 10 + name[i] - '0'; + } + channel.mType = LLGameControl::InputChannel::Type::TYPE_BUTTON; + channel.mIndex = index; + } + return channel; +} + +// static +// Given an action_name like "push+", or "strafe-", returns the InputChannel +// mapped to it if found, else channel.isNone() will be true. +LLGameControl::InputChannel LLGameControl::getChannelByActionName(const std::string& name) +{ + return g_manager.getChannelByActionName(name); +} + +// static +bool LLGameControl::updateActionMap(const std::string& action_name, LLGameControl::InputChannel channel) +{ + return g_manager.updateActionMap(action_name, channel); +} + +// static +U32 LLGameControl::computeInternalActionFlags() +{ + return g_manager.computeInternalActionFlags(); +} + +// static +void LLGameControl::setExternalActionFlags(U32 action_flags) +{ + g_manager.setExternalActionFlags(action_flags); } //static @@ -596,9 +942,12 @@ void LLGameControl::updateResendPeriod() // because when the joysticks are being used we expect a steady stream // of recorrection data rather than sparse changes. // + // (The above assumption is not necessarily true for "Actions" input + // (e.g. keyboard events). TODO: figure out what to do about this.) + // // In other words: we want to include changed axes in the first resend - // so we only overrite g_gameControlState.mPrevAxes on higher resends. - g_gameControlState.mPrevAxes = g_gameControlState.mAxes; + // so we only overwrite g_finalState.mPrevAxes on higher resends. + g_finalState.mPrevAxes = g_finalState.mAxes; g_nextResendPeriod *= RESEND_EXPANSION_RATE; } } diff --git a/indra/llwindow/llgamecontrol.h b/indra/llwindow/llgamecontrol.h index fe6d6f0138..50cb78a4ea 100644 --- a/indra/llwindow/llgamecontrol.h +++ b/indra/llwindow/llgamecontrol.h @@ -33,6 +33,36 @@ #include "llsingleton.h" #include "stdtypes.h" +// For reference, here are the RAW indices of the various input channels +// of a standard XBox controller. Button (N) is numbered in parentheses, +// whereas axisN has N+ and N- labels. +// +// leftpaddle rightpaddle +// _______ _______ +// / 4+ '-. .-' 5+ \ +// leftshoulder _(9)_________'-.____ ____.-'_________(10) rightshoulder +// / _________ \_________/ \ +// / / 1- \ (3) \ +// | | | (4) (5) (6) Y | +// | |0- (7) 0+| _________ (2)X B(1) | +// | | | / 3- \ A | +// | | 1+ | | | (0) | +// | \_________/ |2- (8) 2+| | +// | leftstick (11) | | | +// | (13) (14) | 3+ | | +// | (12) \_________/ | +// | d-pad rightstick | +// | ____________________ | +// | / \ | +// | / \ | +// | / \ | +// \__________/ \__________/ +// +// Note: the analog joystics provide NEGATIVE X,Y values for LEFT,FORWARD +// whereas those directions are actually POSITIVE in SL's local right-handed +// reference frame. This is why we implicitly negate those axes the moment +// they are extracted from SDL, before being used anywhere. See the +// implementation in LLGameControllerManager::onAxis(). // LLGameControl is a singleton with pure static public interface class LLGameControl : public LLSingleton @@ -42,11 +72,93 @@ class LLGameControl : public LLSingleton LOG_CLASS(LLGameControl); public: + enum AgentControlMode + { + CONTROL_MODE_AVATAR, + CONTROL_MODE_FLYCAM, + CONTROL_MODE_NONE + }; + + enum KeyboardAxis + { + AXIS_LEFTX = 0, + AXIS_LEFTY, + AXIS_RIGHTX, + AXIS_RIGHTY, + AXIS_TRIGGERLEFT, + AXIS_TRIGGERRIGHT, + AXIS_LAST + }; + + enum Button + { + BUTTON_A = 0, + BUTTON_B, + BUTTON_X, + BUTTON_Y, + BUTTON_BACK, + BUTTON_GUIDE, + BUTTON_START, + BUTTON_LEFTSTICK, + BUTTON_RIGHTSTICK, + BUTTON_LEFTSHOULDER, + BUTTON_RIGHTSHOULDER, // 10 + BUTTON_DPAD_UP, + BUTTON_DPAD_DOWN, + BUTTON_DPAD_LEFT, + BUTTON_DPAD_RIGHT, + BUTTON_MISC1, + BUTTON_PADDLE1, + BUTTON_PADDLE2, + BUTTON_PADDLE3, + BUTTON_PADDLE4, + BUTTON_TOUCHPAD, // 20 + BUTTON_21, + BUTTON_22, + BUTTON_23, + BUTTON_24, + BUTTON_25, + BUTTON_26, + BUTTON_27, + BUTTON_28, + BUTTON_29, + BUTTON_30, + BUTTON_31 + }; + + class InputChannel + { + public: + enum Type + { + TYPE_AXIS, + TYPE_BUTTON, + TYPE_NONE + }; + + InputChannel() {} + InputChannel(Type type, U8 index) : mType(type), mIndex(index) {} + InputChannel(Type type, U8 index, S32 sign) : mType(type), mSign(sign), mIndex(index) {} + + // these methods for readability + bool isAxis() const { return mType == TYPE_AXIS; } + bool isButton() const { return mType == TYPE_BUTTON; } + bool isNone() const { return mType == TYPE_NONE; } + + std::string getLocalName() const; // AXIS_0-, AXIS_0+, BUTTON_0, etc + std::string getRemoteName() const; // GAME_CONTROL_AXIS_LEFTX, GAME_CONTROL_BUTTON_A, etc + + Type mType { TYPE_NONE }; + S32 mSign { 0 }; + U8 mIndex { 255 }; + }; + // State is a minimal class for storing axes and buttons values class State { public: State(); + void clear(); bool onButton(U8 button, bool pressed); std::vector mAxes; // [ -32768, 32767 ] std::vector mPrevAxes; // value in last outgoing packet @@ -57,28 +169,44 @@ public: static void init(); static void terminate(); - static void addKeyButtonMap(U16 key, U8 button); - static void removeKeyButtonMap(U16 key); - static void addKeyAxisMap(U16 key, U8 axis, bool positive); - static void removeKeyAxisMap(U16 key); - - static void onKeyDown(U16 key, U32 mask); - static void onKeyUp(U16 key, U32 mask); - // returns 'true' if GameControlInput message needs to go out, // which will be the case for new data or resend. Call this right // before deciding to put a GameControlInput packet on the wire // or not. - static bool computeFinalInputAndCheckForChanges(); + static bool computeFinalStateAndCheckForChanges(); - static void clearAllInput(); - static void clearAllKeys(); + static void clearAllState(); static void processEvents(bool app_has_focus = true); static const State& getState(); - - static void setIncludeKeyboardButtons(bool include); - static bool getIncludeKeyboardButtons(); + static void getCameraInputs(std::vector& inputs_out); + + // these methods for accepting input from keyboard + static void enableSendToServer(bool enable); + static void enableControlAgent(bool enable); + static void enableTranslateAgentActions(bool enable); + static void setAgentControlMode(AgentControlMode mode); + + static bool willSendToServer(); + static bool willTranslateAgentActions(); + static bool willControlAvatar(); + static bool willControlFlycam(); + //static LocalControlMode getLocalControlMode(); + + // Given a name like "AXIS_1-" or "BUTTON_5" returns the corresponding InputChannel + // If the axis name lacks the +/- postfix it assumes '+' postfix. + static LLGameControl::InputChannel getChannelByName(const std::string& name); + + // action_name = push+, strafe-, etc + static LLGameControl::InputChannel getChannelByActionName(const std::string& name); + + static bool updateActionMap(const std::string& action_name, LLGameControl::InputChannel channel); + + // Keyboard presses produce action_flags which can be translated into State + // and game_control devices produce State which can be translated into action_flags. + // These methods help exchange such translations. + static U32 computeInternalActionFlags(); + static void setExternalActionFlags(U32 action_flags); // call this after putting a GameControlInput packet on the wire static void updateResendPeriod(); diff --git a/indra/llwindow/llgamecontroltranslator.cpp b/indra/llwindow/llgamecontroltranslator.cpp new file mode 100644 index 0000000000..c12d9317a7 --- /dev/null +++ b/indra/llwindow/llgamecontroltranslator.cpp @@ -0,0 +1,344 @@ +/** + * @file llgamecontroltranslator.cpp + * @brief LLGameControlTranslator class implementation + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +/* + * App-wide preferences. Note that these are not per-user, + * because we need to load many preferences before we have + * a login name. + */ + +#include "llgamecontroltranslator.h" +#include "llsd.h" + + +using ActionToMaskMap = LLGameControlTranslator::ActionToMaskMap; + +LLGameControlTranslator::LLGameControlTranslator() +{ +} + +void LLGameControlTranslator::setAvailableActions(ActionToMaskMap& action_to_mask) +{ + mActionToMask = std::move(action_to_mask); +} + +LLGameControl::InputChannel LLGameControlTranslator::getChannelByAction(const std::string& action) const +{ + LLGameControl::InputChannel channel; + ActionToMaskMap::const_iterator mask_itr = mActionToMask.find(action); + if (mask_itr != mActionToMask.end()) + { + U32 mask = mask_itr->second; + LLGameControlTranslator::MaskToChannelMap::const_iterator channel_itr = mMaskToChannel.find(mask); + if (channel_itr != mMaskToChannel.end()) + { + channel = channel_itr->second; + } + } + else + { + // It is expected that sometimes 'action' lacks the postfix '+' or '-'. + // When it is missing we append '+' and try again. + std::string action_plus = action; + action_plus.append("+"); + mask_itr = mActionToMask.find(action_plus); + if (mask_itr != mActionToMask.end()) + { + U32 mask = mask_itr->second; + LLGameControlTranslator::MaskToChannelMap::const_iterator channel_itr = mMaskToChannel.find(mask); + if (channel_itr != mMaskToChannel.end()) + { + channel = channel_itr->second; + } + } + } + return channel; +} + +void LLGameControlTranslator::setMappings(LLGameControlTranslator::NamedChannels& list) +{ + mMaskToChannel.clear(); + mMappedFlags = 0; + mPrevActiveFlags = 0; + + for (auto& name_channel : list) + { + updateMap(name_channel.first, name_channel.second); + } +} + +bool LLGameControlTranslator::updateMap(const std::string& name, const LLGameControl::InputChannel& channel) +{ + bool map_changed = false; + size_t name_length = name.length(); + if (name_length > 1) + { + if (channel.isButton()) + { + map_changed = updateMapInternal(name, channel); + } + else if (channel.isAxis()) + { + U8 last_char = name.at(name_length - 1); + if (last_char == '+' || last_char == '=') + { + map_changed = updateMapInternal(name, channel); + } + else + { + // try to map both "name+" and "name-" + std::string new_name = name; + new_name.append("+"); + bool success = updateMapInternal(new_name, channel); + if (success) + { + //new_name.append("-"); + new_name.data()[name_length] = '-'; + LLGameControl::InputChannel other_channel(channel.mType, channel.mIndex, -channel.mSign); + // HACK: this works for XBox and similar controllers, + // and those are pretty much the only supported devices right now + // however TODO: figure out how to do this better. + // + // AXIS_TRIGGERLEFT and AXIS_TRIGGERRIGHT are separate axes and most devices + // only allow them to read positive, not negative. When used for motion control + // they are typically paired together. We assume as much here when computing + // the other_channel. + if (channel.mIndex == LLGameControl::AXIS_TRIGGERLEFT) + { + other_channel.mIndex = LLGameControl::AXIS_TRIGGERRIGHT; + other_channel.mSign = 1; + } + else if (channel.mIndex == LLGameControl::AXIS_TRIGGERRIGHT) + { + other_channel.mIndex = LLGameControl::AXIS_TRIGGERLEFT; + other_channel.mSign = 1; + } + updateMapInternal(new_name, other_channel); + map_changed = true; + } + } + } + else + { + // channel type is NONE, which means the action needs to be removed from the map + // but we don't know if it mapped to button or axis which is important because + // it if it axis then we need to also remove the other entry. + // So we try to look it up + ActionToMaskMap::iterator mask_itr = mActionToMask.find(name); + if (mask_itr != mActionToMask.end()) + { + // we found the action --> was it mapped to an axis? + bool is_axis = false; + U32 mask = mask_itr->second; + LLGameControlTranslator::MaskToChannelMap::iterator channel_itr = mMaskToChannel.find(mask); + if (channel_itr != mMaskToChannel.end()) + { + if (channel_itr->second.isAxis()) + { + // yes, it is an axis + is_axis = true; + } + } + // remove from map, whether button or axis + updateMapInternal(name, channel); + + if (is_axis) + { + // also need to remove the other entry + std::string other_name = name; + if (other_name.data()[name.length() - 1] == '-') + { + other_name.data()[name.length() - 1] = '+'; + } + else + { + other_name.data()[name.length() - 1] = '-'; + } + // remove from map + updateMapInternal(other_name, channel); + } + } + else if (name.data()[name.length() - 1] == '+' + || name.data()[name.length() - 1] == '-') + { + // action was not found but name doesn't end with +/- + // maybe it is an axis-name sans the +/- on the end + // postfix with '+' and try again + std::string other_name = name; + other_name.append("+"); + map_changed = updateMapInternal(other_name, channel); + if (map_changed) + { + // that worked! now do the other one + other_name.data()[name.length()] = '-'; + updateMapInternal(other_name, channel); + } + } + } + } + + if (map_changed) + { + // recompute mMappedFlags + mMappedFlags = 0; + for (auto& pair : mMaskToChannel) + { + mMappedFlags |= pair.first; + } + mPrevActiveFlags = 0; + } + return map_changed; +} + +// Given external action_flags (i.e. raw avatar input) +// compute the corresponding LLGameControl::State that would have produced those flags. +// Note: "action flags" are similar to, but not quite the same as, "control flags". +// "Action flags" are the raw input of avatar movement intent, whereas "control flags" +// are the consequential set of instructions that are sent to the server for moving +// the avatar character. +const LLGameControl::State& LLGameControlTranslator::computeStateFromFlags(U32 action_flags) +{ + static U32 last_action_flags = 0; + if (last_action_flags != action_flags) + { + last_action_flags = action_flags; + } + // translate action_flag bits to equivalent game controller state + // according to data in mMaskToChannel + + // only bother to update mCachedState if active_flags have changed + U32 active_flags = action_flags & mMappedFlags; + //if (active_flags != mPrevActiveFlags) + { + mCachedState.clear(); + for (const auto& pair : mMaskToChannel) + { + U32 mask = pair.first; + if (mask == (mask & action_flags)) + { + LLGameControl::InputChannel channel = pair.second; + if (channel.isAxis()) + { + if (channel.mSign < 0) + { + mCachedState.mAxes[channel.mIndex] = std::numeric_limits::min(); + } + else + { + mCachedState.mAxes[channel.mIndex] = std::numeric_limits::max(); + } + } + else if (channel.isButton()) + { + mCachedState.mButtons |= (0x01U << channel.mIndex); + } + } + } + mPrevActiveFlags = active_flags; + } + return mCachedState; +} + +// Given LLGameControl::State (i.e. from a real controller) +// compute corresponding action flags (e.g. for moving the avatar around) +U32 LLGameControlTranslator::computeFlagsFromState(const std::vector& axes, U32 buttons) +{ + // HACK: supply hard-coded threshold for ON/OFF zones + constexpr S32 AXIS_THRESHOLD = 32768 / 8; + U32 action_flags = 0; + for (const auto& pair : mMaskToChannel) + { + // pair = { mask, channel } + const LLGameControl::InputChannel& channel = pair.second; + if (channel.isAxis()) + { + if (channel.mSign < 0) + { + if (axes[channel.mIndex] < -AXIS_THRESHOLD) + { + action_flags |= pair.first; + } + } + else if (axes[channel.mIndex] > AXIS_THRESHOLD) + { + action_flags |= pair.first; + } + } + else if (channel.isButton()) + { + U32 bit_set = buttons & (0x01U << channel.mIndex); + if (bit_set) + { + action_flags |= pair.first; + } + } + } + return action_flags; +} + +bool LLGameControlTranslator::updateMapInternal(const std::string& name, const LLGameControl::InputChannel& channel) +{ + bool something_changed = false; + ActionToMaskMap::iterator mask_itr = mActionToMask.find(name); + if (mask_itr != mActionToMask.end()) + { + U32 mask = mask_itr->second; + something_changed = addOrRemoveMaskMapping(mask, channel); + } + return something_changed; +} + +bool LLGameControlTranslator::addOrRemoveMaskMapping(U32 mask, const LLGameControl::InputChannel& channel) +{ + bool success = false; + LLGameControlTranslator::MaskToChannelMap::iterator channel_itr = mMaskToChannel.find(mask); + if (channel_itr != mMaskToChannel.end()) + { + LLGameControl::InputChannel old_channel = channel_itr->second; + if (old_channel.mType != channel.mType || old_channel.mIndex != channel.mIndex || old_channel.mSign != channel.mSign) + { + if (channel.isNone()) + { + // remove old mapping + mMaskToChannel.erase(channel_itr); + } + else + { + // update old mapping + channel_itr->second = channel; + } + success = true; + } + } + else if (! channel.isNone()) + { + // create new mapping + mMaskToChannel[mask] = channel; + success = true; + } + return success; +} + diff --git a/indra/llwindow/llgamecontroltranslator.h b/indra/llwindow/llgamecontroltranslator.h new file mode 100644 index 0000000000..13cbf29db2 --- /dev/null +++ b/indra/llwindow/llgamecontroltranslator.h @@ -0,0 +1,83 @@ +/** + * @file llgamecontroltranslator.h + * @brief LLGameControlTranslator class definition + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include + +#include "stdtypes.h" +#include "llgamecontrol.h" + + +class LLGameControlTranslator +{ +public: + + using ActionToMaskMap = std::map< std::string, U32 >; // < action : mask > + using MaskToChannelMap = std::map< U32, LLGameControl::InputChannel >; // < mask : channel > + using NamedChannel = std::pair < std::string , LLGameControl::InputChannel >; + using NamedChannels = std::vector< NamedChannel >; + + + LLGameControlTranslator(); + void setAvailableActions(ActionToMaskMap& action_to_mask); + LLGameControl::InputChannel getChannelByAction(const std::string& action) const; + void setMappings(NamedChannels& list); + bool updateMap(const std::string& name, const LLGameControl::InputChannel& channel); + // Note: to remove a mapping you can call updateMap() with a TYPE_NONE channel + + // Given external action_flags (i.e. raw avatar input) + // compute the corresponding LLGameControl::State that would have produced those flags. + // Note: "action flags" are similar to, but not quite the same as, "control flags". + const LLGameControl::State& computeStateFromFlags(U32 action_flags); + + // Given LLGameControl::State (i.e. from a real controller) + // compute corresponding action flags (e.g. for moving the avatar around) + U32 computeFlagsFromState(const std::vector& axes, U32 buttons); + + U32 getMappedFlags() const { return mMappedFlags; } + +private: + bool updateMapInternal(const std::string& name, const LLGameControl::InputChannel& channel); + bool addOrRemoveMaskMapping(U32 mask, const LLGameControl::InputChannel& channel); + +private: + // mActionToMask is an invarient map between the possible actions + // and the action bit masks. Only actions therein can have their + // bit masks mapped to channels. + ActionToMaskMap mActionToMask; // invariant map after init + + // mMaskToChannel is a dynamic map between action bit masks + // and GameControl channels. + MaskToChannelMap mMaskToChannel; // dynamic map, per preference changes + + // mCachedState is an optimization: + // it is only recomputed when external action_flags change + LLGameControl::State mCachedState; + + U32 mMappedFlags { 0 }; + U32 mPrevActiveFlags { 0 }; +}; diff --git a/indra/llwindow/llkeyboard.cpp b/indra/llwindow/llkeyboard.cpp index a858d3b16f..7784e6c32a 100644 --- a/indra/llwindow/llkeyboard.cpp +++ b/indra/llwindow/llkeyboard.cpp @@ -29,7 +29,6 @@ #include "llkeyboard.h" #include "llwindowcallbacks.h" -#include "llgamecontrol.h" // // Globals @@ -162,7 +161,7 @@ void LLKeyboard::resetKeyDownAndHandle() mCallbacks->handleTranslatedKeyUp(i, mask); } } - LLGameControl::clearAllKeys(); + mCurTranslatedKey = KEY_NONE; } // BUG this has to be called when an OS dialog is shown, otherwise modifier key state diff --git a/indra/llwindow/llkeyboardheadless.cpp b/indra/llwindow/llkeyboardheadless.cpp index ad8e42a412..a827424141 100644 --- a/indra/llwindow/llkeyboardheadless.cpp +++ b/indra/llwindow/llkeyboardheadless.cpp @@ -57,6 +57,7 @@ void LLKeyboardHeadless::scanKeyboard() mCallbacks->handleScanKey(key, mKeyDown[key], mKeyUp[key], mKeyLevel[key]); } } + mCurScanKey = KEY_NONE; // Reset edges for next frame for (S32 key = 0; key < KEY_COUNT; key++) diff --git a/indra/llwindow/llkeyboardmacosx.cpp b/indra/llwindow/llkeyboardmacosx.cpp index f590b8db8b..4ce98ee32b 100644 --- a/indra/llwindow/llkeyboardmacosx.cpp +++ b/indra/llwindow/llkeyboardmacosx.cpp @@ -291,6 +291,7 @@ void LLKeyboardMacOSX::scanKeyboard() mCallbacks->handleScanKey(key, mKeyDown[key], mKeyUp[key], mKeyLevel[key]); } } + mCurScanKey = KEY_NONE; // Reset edges for next frame for (key = 0; key < KEY_COUNT; key++) diff --git a/indra/llwindow/llkeyboardsdl.cpp b/indra/llwindow/llkeyboardsdl.cpp index 543882fc8f..636eaa5491 100644 --- a/indra/llwindow/llkeyboardsdl.cpp +++ b/indra/llwindow/llkeyboardsdl.cpp @@ -300,6 +300,7 @@ void LLKeyboardSDL::scanKeyboard() mCallbacks->handleScanKey(key, mKeyDown[key], mKeyUp[key], mKeyLevel[key]); } } + mCurScanKey = KEY_NONE; // Reset edges for next frame for (S32 key = 0; key < KEY_COUNT; key++) diff --git a/indra/llwindow/llkeyboardwin32.cpp b/indra/llwindow/llkeyboardwin32.cpp index 7ef616a8b7..756caf6fc3 100644 --- a/indra/llwindow/llkeyboardwin32.cpp +++ b/indra/llwindow/llkeyboardwin32.cpp @@ -259,6 +259,7 @@ void LLKeyboardWin32::scanKeyboard() mCallbacks->handleScanKey(key, mKeyDown[key], mKeyUp[key], mKeyLevel[key]); } } + mCurScanKey = KEY_NONE; // Reset edges for next frame for (key = 0; key < KEY_COUNT; key++) diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 7fbf214dcf..627f1b4326 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -317,6 +317,7 @@ set(viewer_SOURCE_FILES llfollowcam.cpp llfriendcard.cpp llflyoutcombobtn.cpp + llflycam.cpp llgesturelistener.cpp llgesturemgr.cpp llgiveinventory.cpp @@ -991,6 +992,7 @@ set(viewer_HEADER_FILES llfollowcam.h llfriendcard.h llflyoutcombobtn.h + llflycam.h llgesturelistener.h llgesturemgr.h llgiveinventory.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 55f8f77383..ebe027689a 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2612,10 +2612,10 @@ Value 1 - EnableGameControlInput + GameControlToServer Comment - Transmit game controller input to server + Transmit game controller data to server Persist 1 Type @@ -2623,10 +2623,21 @@ Value 0 - EnableGameControlKeyboardInput + GameControlToAgent Comment - Send 'unhandled' keystrokes as GameInput to server + GameControl data moves avatar/flycam + Persist + 1 + Type + Boolean + Value + 0 + + AgentToGameControl + + Comment + Avatar/flycam movement produces GameControl data Persist 1 Type diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index c8b0adbaf8..406734ab61 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -1463,7 +1463,7 @@ LLVector3 LLAgent::getReferenceUpVector() } -// Radians, positive is forward into ground +// Radians, positive is downward toward ground //----------------------------------------------------------------------------- // pitch() //----------------------------------------------------------------------------- @@ -1477,27 +1477,23 @@ void LLAgent::pitch(F32 angle) LLVector3 skyward = getReferenceUpVector(); + // SL-19286 Avatar is upside down when viewed from below + // after left-clicking the mouse on the avatar and dragging down + // + // The issue is observed on angle below 10 degrees + const F32 look_down_limit = 179.f * DEG_TO_RAD; + const F32 look_up_limit = 10.f * DEG_TO_RAD; + + F32 angle_from_skyward = acos(mFrameAgent.getAtAxis() * skyward); + // clamp pitch to limits - if (angle >= 0.f) + if ((angle >= 0.f) && (angle_from_skyward + angle > look_down_limit)) { - const F32 look_down_limit = 179.f * DEG_TO_RAD; - F32 angle_from_skyward = acos(mFrameAgent.getAtAxis() * skyward); - if (angle_from_skyward + angle > look_down_limit) - { - angle = look_down_limit - angle_from_skyward; - } + angle = look_down_limit - angle_from_skyward; } - else if (angle < 0.f) + else if ((angle < 0.f) && (angle_from_skyward + angle < look_up_limit)) { - const F32 look_up_limit = 5.f * DEG_TO_RAD; - const LLVector3& viewer_camera_pos = LLViewerCamera::getInstance()->getOrigin(); - LLVector3 agent_focus_pos = getPosAgentFromGlobal(gAgentCamera.calcFocusPositionTargetGlobal()); - LLVector3 look_dir = agent_focus_pos - viewer_camera_pos; - F32 angle_from_skyward = angle_between(look_dir, skyward); - if (angle_from_skyward + angle < look_up_limit) - { - angle = look_up_limit - angle_from_skyward; - } + angle = look_up_limit - angle_from_skyward; } if (fabs(angle) > 1e-4) @@ -4986,6 +4982,245 @@ void LLAgent::renderAutoPilotTarget() } } +void LLAgent::setExternalActionFlags(U32 outer_flags) +{ + if (LLGameControl::willControlAvatar()) + { + // save these flags for later, for when we're ready + // to actually send an AgentUpdate packet + mExternalActionFlags = outer_flags; + mbFlagsDirty = TRUE; + } +} + +static U64 g_lastUpdateTime { 0 }; +static F32 g_deltaTime { 0.0f }; +static S32 g_lastUpdateFrame { 0 }; +static S32 g_deltaFrame { 0 }; + +void LLAgent::applyExternalActionFlags() +{ + if (! LLGameControl::willControlAvatar()) + { + return; + } + + // HACK: AGENT_CONTROL_NUDGE_AT_NEG is used to toggle Flycam + if ((mExternalActionFlags & AGENT_CONTROL_NUDGE_AT_NEG) > 0) + { + if (mToggleFlycam) + { + mUsingFlycam = !mUsingFlycam; + if (mUsingFlycam) + { + // copy main camera transform to flycam + LLViewerCamera* camera = LLViewerCamera::getInstance(); + mFlycam.setTransform(camera->getOrigin(), camera->getQuaternion()); + mLastFlycamUpdate = LLFrameTimer::getTotalTime(); + } + /* + else + { + // do we need to reset main camera? + } + */ + } + mToggleFlycam = false; + } + else + { + mToggleFlycam = true; + } + + // measure delta time and frame + // Note: it is possible for the deltas to be very large + // and it is the duty of the code that uses them to clamp as necessary + U64 now = LLFrameTimer::getTotalTime(); + g_deltaTime = F32(now - g_lastUpdateTime) / (F32)(USEC_PER_SEC); + g_lastUpdateTime = now; + + S32 frame_count = LLFrameTimer::getFrameCount(); + g_deltaFrame = frame_count - g_lastUpdateFrame; + g_lastUpdateFrame = frame_count; + + if (mUsingFlycam) + { + updateFlycam(); + return; + } + + S32 direction = (S32)(mExternalActionFlags & AGENT_CONTROL_AT_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_AT_NEG) >> 1); + if (direction != 0) + { + moveAt(direction); + } + + static U32 last_non_fly_frame = 0; + static U64 last_non_fly_time = 0; + direction = (S32)(mExternalActionFlags & AGENT_CONTROL_UP_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_UP_NEG) >> 1); + if (direction != 0) + { + // HACK: this auto-fly logic based on original code still extant in llviewerinput.cpp::agent_jump() + // but has been cleaned up. + // TODO?: DRY this logic + if (direction > 0) + { + if (!getFlying() + && !upGrabbed() + && gSavedSettings.getBOOL("AutomaticFly")) + { + constexpr F32 FLY_TIME = 0.5f; + constexpr U32 FLY_FRAMES = 4; + F32 delta_time = (F32)(now - last_non_fly_time) / (F32)(USEC_PER_SEC); + U32 delta_frames = frame_count - last_non_fly_frame; + if( delta_time > FLY_TIME + && delta_frames > FLY_FRAMES) + { + setFlying(TRUE); + } + } + } + else + { + last_non_fly_frame = frame_count; + last_non_fly_time = now; + } + + moveUp(direction); + } + else if (!getFlying()) + { + last_non_fly_frame = frame_count; + last_non_fly_time = now; + } + + direction = (S32)(mExternalActionFlags & AGENT_CONTROL_LEFT_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_LEFT_NEG) >> 1); + if (direction != 0) + { + moveLeft(direction); + } + + direction = (S32)(mExternalActionFlags & AGENT_CONTROL_YAW_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_YAW_NEG) >> 1); + if (direction != 0) + { + F32 sign = (direction < 0 ? -1.0f : 1.0f); + // HACK: hard-code 3.0 seconds for YawRate measure. It is simpler, + // and the missing variable yaw rate is unnoticeable. + moveYaw(sign * LLFloaterMove::getYawRate(3.0f)); + } + + { + F32 pitch = ((mExternalActionFlags & AGENT_CONTROL_PITCH_POS) > 0 ? 1.0f : 0.0f) + - ((mExternalActionFlags & AGENT_CONTROL_PITCH_NEG) > 0 ? 1.0f : 0.0f); + movePitch(pitch); + } + + if ((mExternalActionFlags & AGENT_CONTROL_FLY) > 0) + { + if (mToggleFly) + { + setFlying(!getFlying()); + } + mToggleFly = false; + } + else + { + mToggleFly = true; + } + + if (mExternalActionFlags & AGENT_CONTROL_STOP) + { + setControlFlags(AGENT_CONTROL_STOP); + } + + if ((mExternalActionFlags & AGENT_CONTROL_SIT_ON_GROUND) > 0) + { + if (mToggleSit) + { + if (isSitting()) + { + standUp(); + } + else + { + sitDown(); + } + } + mToggleSit = false; + } + else + { + mToggleSit = true; + } + + // HACK: AGENT_CONTROL_NUDGE_AT_POS is used to toggle running + if ((mExternalActionFlags & AGENT_CONTROL_NUDGE_AT_POS) > 0) + { + if (mToggleRun) + { + if (getRunning()) + { + clearRunning(); + sendWalkRun(false); + } + else + { + setRunning(); + sendWalkRun(true); + } + } + mToggleRun = false; + } + else + { + mToggleRun = true; + } +} + +void LLAgent::updateFlycam() +{ + // Note: no matter how camera_inputs are mapped to the controller + // they arrive in the following order: + enum FLYCAM_AXIS { + FLYCAM_FORWARD = 0, + FLYCAM_LEFT, + FLYCAM_UP, + FLYCAM_PITCH, + FLYCAM_YAW, + FLYCAM_ZOOM + }; + std::vector camera_inputs; + LLGameControl::getCameraInputs(camera_inputs); + + LLVector3 linear_velocity( + camera_inputs[FLYCAM_FORWARD], + camera_inputs[FLYCAM_LEFT], + camera_inputs[FLYCAM_UP]); + constexpr F32 MAX_FLYCAM_SPEED = 10.0f; + mFlycam.setLinearVelocity(MAX_FLYCAM_SPEED * linear_velocity); + + mFlycam.setPitchRate(camera_inputs[FLYCAM_PITCH]); + mFlycam.setYawRate(camera_inputs[FLYCAM_PITCH]); + + mFlycam.integrate(g_deltaTime); + + LLVector3 pos; + LLQuaternion rot; + mFlycam.getTransform(pos, rot); + + // copy flycam transform to main camera + LLMatrix3 mat(rot); + //LLViewerCamera::getInstance()->setView(sFlycamZoom); + LLViewerCamera::getInstance()->setOrigin(pos); + LLViewerCamera::getInstance()->mXAxis = LLVector3(mat.mMatrix[0]); + LLViewerCamera::getInstance()->mYAxis = LLVector3(mat.mMatrix[1]); + LLViewerCamera::getInstance()->mZAxis = LLVector3(mat.mMatrix[2]); +} + /********************************************************************************/ //----------------------------------------------------------------------------- diff --git a/indra/newview/llagent.h b/indra/newview/llagent.h index afc34f747f..0394f9c76c 100644 --- a/indra/newview/llagent.h +++ b/indra/newview/llagent.h @@ -33,6 +33,8 @@ #include "llcharacter.h" #include "llcoordframe.h" // for mFrameAgent #include "llavatarappearancedefines.h" +#include "llflycam.h" +#include "llkeyboard.h" #include "llpermissionsflags.h" #include "llevents.h" #include "v3dmath.h" @@ -485,6 +487,7 @@ public: void resetControlFlags(); bool anyControlGrabbed() const; // True iff a script has taken over a control bool isControlGrabbed(S32 control_index) const; + bool isUsingFlycam() const { return mUsingFlycam; } // Send message to simulator to force grabbed controls to be // released, in case of a poorly written script. void forceReleaseControls(); @@ -492,8 +495,37 @@ public: private: S32 mControlsTakenCount[TOTAL_CONTROLS]; S32 mControlsTakenPassedOnCount[TOTAL_CONTROLS]; + // mControlFlags is a bitmask of behavior instructions for compact + // transmission to the server. It does NOT represent "input", rather + // the consequences of it, which will sometimes depend on "state". U32 mControlFlags; // Replacement for the mFooKey's + //-------------------------------------------------------------------- + // GameControls + //-------------------------------------------------------------------- +public: + // ActionFlags are similar to, but not the same as, ControlFlags! + // An 'ActionFlags' bitmask stores 'simplified input' from key/button + // presses that are mapped to avatar/camera movement actions + // whereas 'mControlFlags' are a more complicated set of behavior bits + // computed as a function of input and state. + // + void setExternalActionFlags(U32 flags); + void applyExternalActionFlags(); + void applyExternalActionFlagsForFlycam(); + +private: + void updateFlycam(); + + U64 mLastFlycamUpdate { 0 }; + U32 mExternalActionFlags { 0 }; + LLFlycam mFlycam; + bool mToggleFly { true }; + bool mToggleSit { true }; + bool mToggleRun { true }; + bool mToggleFlycam { true }; + bool mUsingFlycam { false }; + //-------------------------------------------------------------------- // Animations //-------------------------------------------------------------------- diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index ffa742d154..ed64dce109 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -215,6 +215,7 @@ #include "llvosurfacepatch.h" #include "llviewerfloaterreg.h" #include "llcommandlineparser.h" +#include "llfloaterpreference.h" #include "llfloatermemleak.h" #include "llfloaterreg.h" #include "llfloatersimplesnapshot.h" @@ -1418,27 +1419,76 @@ bool LLAppViewer::frame() return ret; } +// util for detecting most active input channel +LLGameControl::InputChannel get_active_input_channel(const LLGameControl::State& state) +{ + LLGameControl::InputChannel input; + if (state.mButtons > 0) + { + // check buttons + input.mType = LLGameControl::InputChannel::TYPE_BUTTON; + for (U8 i = 0; i < 32; ++i) + { + if ((0x1 << i) & state.mButtons) + { + input.mIndex = i; + break; + } + } + } + else + { + // scan axes + S16 threshold = std::numeric_limits::max() / 2; + for (U8 i = 0; i < 6; ++i) + { + if (abs(state.mAxes[i]) > threshold) + { + input.mType = LLGameControl::InputChannel::TYPE_AXIS; + // input.mIndex ultimately translates to a LLGameControl::KeyboardAxis + // which distinguishes between negative and positive directions + // so we must translate to axis index "i" according to the sign + // of the axis value. + input.mIndex = i; + input.mSign = state.mAxes[i] > 0 ? 1 : -1; + break; + } + } + } + return input; +} // static bool packGameControlInput(LLMessageSystem* msg) { - if (! LLGameControl::computeFinalInputAndCheckForChanges()) + if (! LLGameControl::computeFinalStateAndCheckForChanges()) { + // Note: LLGameControl manages some re-send logic + // so if we get here: nothing changed AND there is no need for a re-send return false; } - if (!gSavedSettings.getBOOL("EnableGameControlInput")) + if (!gSavedSettings.getBOOL("GameControlToServer")) { - LLGameControl::clearAllInput(); + LLGameControl::clearAllState(); return false; } + const LLGameControl::State& state = LLGameControl::getState(); + + if (LLPanelPreferenceGameControl::isWaitingForInputChannel()) + { + LLGameControl::InputChannel channel = get_active_input_channel(state); + if (channel.mType != LLGameControl::InputChannel::TYPE_NONE) + { + LLPanelPreferenceGameControl::applyGameControlInput(channel); + } + } + msg->newMessageFast(_PREHASH_GameControlInput); msg->nextBlock("AgentData"); msg->addUUID("AgentID", gAgentID); msg->addUUID("SessionID", gAgentSessionID); - const LLGameControl::State& state = LLGameControl::getState(); - size_t num_indices = state.mAxes.size(); for (U8 i = 0; i < num_indices; ++i) { @@ -1581,15 +1631,6 @@ bool LLAppViewer::doFrame() joystick->scanJoystick(); gKeyboard->scanKeyboard(); gViewerInput.scanMouse(); - - LLGameControl::setIncludeKeyboardButtons(gSavedSettings.getBOOL("EnableGameControlKeyboardInput")); - LLGameControl::processEvents(gFocusMgr.getAppHasFocus()); - // to help minimize lag we send GameInput packets immediately - // after getting the latest GameController input - if (packGameControlInput(gMessageSystem)) - { - gAgent.sendMessage(); - } } // Update state based on messages, user input, object idle. @@ -4823,11 +4864,37 @@ void LLAppViewer::idle() send_agent_update(false); - // After calling send_agent_update() in the mainloop we always clear - // the agent's ephemeral ControlFlags (whether an AgentUpdate was - // actually sent or not) because these will be recomputed based on - // real-time key/controller input and resubmitted next frame. - gAgent.resetControlFlags(); + // Note: we process game_control before sending AgentUpdate + // because it may translate to control flags that control avatar motion. + LLGameControl::processEvents(gFocusMgr.getAppHasFocus()); + + // trade flags between gAgent and LLGameControl + U32 control_flags = gAgent.getControlFlags(); + U32 action_flags = LLGameControl::computeInternalActionFlags(); + LLGameControl::setExternalActionFlags(control_flags); + if (packGameControlInput(gMessageSystem)) + { + // to help minimize lag we send GameInput packets ASAP + gAgent.sendMessage(); + } + gAgent.setExternalActionFlags(action_flags); + + // When appropriate, update agent location to the simulator. + F32 agent_update_time = agent_update_timer.getElapsedTimeF32(); + F32 agent_force_update_time = mLastAgentForceUpdate + agent_update_time; + bool force_update = gAgent.controlFlagsDirty() + || (mLastAgentControlFlags != gAgent.getControlFlags()) + || (agent_force_update_time > (1.0f / (F32) AGENT_FORCE_UPDATES_PER_SECOND)); + if (force_update || (agent_update_time > (1.0f / (F32) AGENT_UPDATES_PER_SECOND))) + { + gAgent.applyExternalActionFlags(); + LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; + // Send avatar and camera info + mLastAgentControlFlags = gAgent.getControlFlags(); + mLastAgentForceUpdate = force_update ? 0 : agent_force_update_time; + send_agent_update(force_update); + agent_update_timer.reset(); + } } ////////////////////////////////////// @@ -5078,6 +5145,10 @@ void LLAppViewer::idle() { LLViewerJoystick::getInstance()->moveFlycam(); } + else if (gAgent.isUsingFlycam()) + { + // TODO: implement this + } else { if (LLToolMgr::getInstance()->inBuildMode()) diff --git a/indra/newview/llfloaterjoystick.cpp b/indra/newview/llfloaterjoystick.cpp index 68b11ec92b..1f7f97aad5 100644 --- a/indra/newview/llfloaterjoystick.cpp +++ b/indra/newview/llfloaterjoystick.cpp @@ -79,14 +79,17 @@ BOOL CALLBACK di8_list_devices_callback(LPCDIDEVICEINSTANCE device_instance_ptr, if (device_instance_ptr && pvRef) { std::string product_name = utf16str_to_utf8str(llutf16string(device_instance_ptr->tszProductName)); - S32 size = sizeof(GUID); - LLSD::Binary data; //just an std::vector - data.resize(size); - memcpy(&data[0], &device_instance_ptr->guidInstance /*POD _GUID*/, size); - - LLFloaterJoystick * floater = (LLFloaterJoystick*)pvRef; - LLSD value = data; - floater->addDevice(product_name, value); + if (LLViewerJoystick::is3DConnexionDevice(product_name)) + { + S32 size = sizeof(GUID); + LLSD::Binary data; //just an std::vector + data.resize(size); + memcpy(&data[0], &device_instance_ptr->guidInstance /*POD _GUID*/, size); + + LLFloaterJoystick * floater = (LLFloaterJoystick*)pvRef; + LLSD value = data; + floater->addDevice(product_name, value); + } } return DIENUM_CONTINUE; } @@ -303,8 +306,11 @@ void LLFloaterJoystick::refreshListOfDevices() #if LL_WINDOWS && !LL_MESA_HEADLESS LL_WARNS() << "NDOF connected to device without using SL provided handle" << LL_ENDL; #endif - std::string desc = joystick->getDescription(); - if (!desc.empty()) + // This feature used to support various gamepad devices however + // going forward we will restrict it to 3DConnexion devices (SpaceMouse, etc) + // and will handle gamepads with the GameControl feature. + std::string desc = LLViewerJoystick::getInstance()->getDescription(); + if (LLViewerJoystick::is3DConnexionDevice(desc)) { LLSD value = LLSD::Integer(1); // value for selection addDevice(desc, value); diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index d60d41ae3c..17779a24f2 100644 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -54,6 +54,7 @@ #include "llfloaterperformance.h" #include "llfloatersidepanelcontainer.h" #include "llfloaterimsession.h" +#include "llgamecontrol.h" #include "llkeyboard.h" #include "llmodaldialog.h" #include "llnavigationbar.h" @@ -2320,7 +2321,7 @@ public: mAccountIndependentSettings.push_back("AutoDisengageMic"); } - /*virtual*/ void saveSettings() + void saveSettings() override { LLPanelPreference::saveSettings(); @@ -3127,6 +3128,368 @@ void LLPanelPreferenceControls::onCancelKeyBind() pControlsTable->deselectAllItems(); } +//------------------------LLPanelPreferenceGameControl-------------------------------- + +// LLPanelPreferenceGameControl is effectively a singleton, so we track its instance +static LLPanelPreferenceGameControl* gGameControlPanel; + +LLPanelPreferenceGameControl::LLPanelPreferenceGameControl() +{ + gGameControlPanel = this; +} + +LLPanelPreferenceGameControl::~LLPanelPreferenceGameControl() +{ + gGameControlPanel = nullptr; +} + +static LLPanelInjector t_pref_game_control("panel_preference_game_control"); + +void LLPanelPreferenceGameControl::apply() +{ +} + +void LLPanelPreferenceGameControl::loadDefaults() +{ + // TODO: implement this +} + +void LLPanelPreferenceGameControl::loadSettings() +{ + // TODO: implement this +} + +void LLPanelPreferenceGameControl::saveSettings() +{ + // TODO: implement this +} + +void LLPanelPreferenceGameControl::updateEnabledState() +{ + // TODO?: implement this +} + +static LLScrollListItem* gSelectedItem { nullptr }; +static LLScrollListCell* gSelectedCell { nullptr }; + +void LLPanelPreferenceGameControl::onClickGameControlToServer(LLUICtrl* ctrl) +{ + BOOL checked = mCheckGameControlToServer->get(); + gSavedSettings.setBOOL( "GameControlToServer", checked ); + LLGameControl::enableSendToServer(checked); +} + +void LLPanelPreferenceGameControl::onClickGameControlToAgent(LLUICtrl* ctrl) +{ + BOOL checked = mCheckGameControlToAgent->get(); + gSavedSettings.setBOOL( "GameControlToAgent", checked ); + LLGameControl::enableControlAgent(checked); + + mActionTable->deselectAllItems(); + bool table_enabled = checked || mCheckAgentToGameControl->get(); + mActionTable->setEnabled(table_enabled); + mChannelSelector->setEnabled(table_enabled); + LLGameControl::enableTranslateAgentActions(checked); +} + +void LLPanelPreferenceGameControl::onClickAgentToGameControl(LLUICtrl* ctrl) +{ + BOOL checked = mCheckAgentToGameControl->get(); + gSavedSettings.setBOOL( "AgentToGameControl", checked ); + + mActionTable->deselectAllItems(); + bool table_enabled = checked || mCheckGameControlToAgent->get(); + mActionTable->setEnabled(table_enabled); + mChannelSelector->setEnabled(table_enabled); + LLGameControl::enableTranslateAgentActions(checked); + +} + +void LLPanelPreferenceGameControl::onActionSelect() +{ + clearSelectionState(); + + LLScrollListItem* item = mActionTable->getFirstSelected(); + if (item == NULL) + { + return; + } + + std::string action = item->getValue(); + + if (action.empty()) + { + mActionTable->deselectAllItems(); + return; + } + + S32 cell_index = item->getSelectedCell(); + if (cell_index != 1) + { + mActionTable->deselectAllItems(); + return; + } + + LLScrollListText* cell = dynamic_cast(item->getColumn(cell_index)); + if (cell) + { + gSelectedItem = item; + gSelectedCell = cell; + + // compute new rect for mChannelSelector + S32 row = mActionTable->getFirstSelectedIndex(); + S32 column = item->getSelectedCell(); + LLRect cell_rect = mActionTable->getCellRect(row, column); + + LLRect combo_rect = mChannelSelector->getRect(); + S32 width = combo_rect.getWidth(); + S32 height = combo_rect.getHeight(); + S32 left = cell_rect.mLeft + cell->getTextWidth(); + combo_rect.set(left, cell_rect.mTop, left + width, cell_rect.mTop - height); + mChannelSelector->setRect(combo_rect); + + std::string value = gSelectedCell->getValue(); + if (value == " ") + { + value = "NONE"; + } + mChannelSelector->setValue(value); + mChannelSelector->setVisible(TRUE); + mChannelSelector->showList(); + } + else + { + mActionTable->deselectAllItems(); + } +} + +void LLPanelPreferenceGameControl::onCommitInputChannel() +{ + if (gSelectedCell) + { + std::string channel_name = mChannelSelector->getSelectedItemLabel(); + LLGameControl::InputChannel channel = LLGameControl::getChannelByName(channel_name); + + std::string action_name = gSelectedItem->getValue(); + bool success = LLGameControl::updateActionMap(action_name, channel); + if (success) + { + if (channel_name == "NONE") + { + gSelectedCell->setValue(" "); + // TODO?: also clear cell to the right with script-relevant name + } + else + { + gSelectedCell->setValue(channel_name); + // TODO?: also update the cell to the right with script-relevant name + } + } + gGameControlPanel->updateTable(); + clearSelectionState(); + } +} + +bool LLPanelPreferenceGameControl::isWaitingForInputChannel() +{ + return gSelectedItem != nullptr; +} + +// static +void LLPanelPreferenceGameControl::applyGameControlInput(const LLGameControl::InputChannel& channel) +{ + if (gSelectedItem && channel.mType != (U8)(LLPanelPreferenceGameControl::TYPE_NONE)) + { + S32 cell_index = gSelectedItem->getSelectedCell(); + if (cell_index > 0) + { + LLScrollListCell* cell = gSelectedItem->getColumn(cell_index); + if (cell) + { + bool success = LLGameControl::updateActionMap(gSelectedItem->getValue(), channel); + if (success) + { + cell->setValue(channel.getLocalName()); + // TODO?: also update the cell to the right with script-relevant name + gGameControlPanel->updateTable(); + } + + } + } + gGameControlPanel->clearSelectionState(); + } +} + +BOOL LLPanelPreferenceGameControl::postBuild() +{ + mCheckGameControlToServer = getChild("game_control_to_server"); + mCheckGameControlToServer->setCommitCallback(boost::bind(&LLPanelPreferenceGameControl::onClickGameControlToServer, this, _1)); + //mCheckGameControlToServer->setEnabled(gSavedSettings.getBOOL( "GameControlToServer")); + + mCheckGameControlToAgent = getChild("game_control_to_agent"); + mCheckGameControlToAgent->setCommitCallback(boost::bind(&LLPanelPreferenceGameControl::onClickGameControlToAgent, this, _1)); + //mCheckGameControlToAgent->setEnabled(gSavedSettings.getBOOL( "GameControlToAgent")); + + mCheckAgentToGameControl= getChild("agent_to_game_control"); + mCheckAgentToGameControl->setCommitCallback(boost::bind(&LLPanelPreferenceGameControl::onClickAgentToGameControl, this, _1)); + //mCheckAgentToGameControl->setEnabled(gSavedSettings.getBOOL( "AgentToGameControl")); + + mActionTable = getChild("action_table"); + mActionTable->setCommitCallback(boost::bind(&LLPanelPreferenceGameControl::onActionSelect, this)); + + populateActionTable(); + + // enable the table if at least one of the GameControl<-->Avatar options is enabled + mActionTable->setEnabled(mCheckGameControlToAgent->get() || mCheckAgentToGameControl->get()); + + mChannelSelector = getChild("input_channel_combo"); + mChannelSelector->setVisible(FALSE); + mChannelSelector->setCommitCallback(boost::bind(&LLPanelPreferenceGameControl::onCommitInputChannel, this)); + + return TRUE; +} + +void LLPanelPreferenceGameControl::populateActionTable() +{ + loadSettings(); + populateColumns(); + populateRows("game_control_table_rows.xml"); + addTableSeparator(); + populateRows("game_control_table_camera_rows.xml"); +} + +void LLPanelPreferenceGameControl::populateColumns() +{ + // populate columns + std::string filename = "game_control_table_columns.xml"; + LLXMLNodePtr xmlNode; + LLScrollListCtrl::Contents contents; + if (!LLUICtrlFactory::getLayeredXMLNode(filename, xmlNode)) + { + LL_WARNS("Preferences") << "Failed to populate columns from '" << filename << "'" << LL_ENDL; + return; + } + LLXUIParser parser; + parser.readXUI(xmlNode, contents, filename); + if (!contents.validateBlock()) + { + LL_WARNS("Preferences") << "Failed to parse columns from '" << filename << "'" << LL_ENDL; + return; + } + for (LLInitParam::ParamIterator::const_iterator col_it = contents.columns.begin(); + col_it != contents.columns.end(); + ++col_it) + { + mActionTable->addColumn(*col_it); + } +} + +void LLPanelPreferenceGameControl::populateRows(const std::string& filename) +{ + LLXMLNodePtr xmlNode; + if (!LLUICtrlFactory::getLayeredXMLNode(filename, xmlNode)) + { + LL_WARNS("Preferences") << "Failed to populate rows from '" << filename << "'" << LL_ENDL; + return; + } + LLScrollListCtrl::Contents contents; + LLXUIParser parser; + parser.readXUI(xmlNode, contents, filename); + if (!contents.validateBlock()) + { + LL_WARNS("Preferences") << "Failed to parse rows from '" << filename << "'" << LL_ENDL; + return; + } + + // init basic cell params + LLScrollListCell::Params cell_params; + cell_params.font = LLFontGL::getFontSansSerif(); + cell_params.font_halign = LLFontGL::LEFT; + cell_params.column = ""; + cell_params.value = ""; + + // we expect the mActionTable to have at least three columns + if (mActionTable->getNumColumns() < 3) + { + LL_WARNS("Preferences") << "expected at least three columns in '" << filename << "'" << LL_ENDL; + return; + } + LLScrollListColumn* local_channel_column = mActionTable->getColumn(1); + + for (LLInitParam::ParamIterator::const_iterator row_it = contents.rows.begin(); + row_it != contents.rows.end(); + ++row_it) + { + std::string name = row_it->value.getValue().asString(); + if (!name.empty() && name != "menu_separator") + { + LLScrollListItem::Params item_params(*row_it); + item_params.enabled.setValue(true); + size_t num_columns = item_params.columns.size(); + // item_params should already have one column that was defined + // in XUI config file, and now we want to add two more + if (num_columns > 0) + { + LLGameControl::InputChannel channel = LLGameControl::getChannelByActionName(name); + + cell_params.column = local_channel_column->mName; + cell_params.value = channel.getLocalName(); + item_params.columns.add(cell_params); + + // TODO?: add a column with more human readable name + //cell_params.column = remote_channel_column->mName; + //cell_params.value = channel.getRemoteName(); + //item_params.columns.add(cell_params); + } + mActionTable->addRow(item_params, EAddPosition::ADD_BOTTOM); + } + else + { + // Separator example: + // + // + // + mActionTable->addRow(*row_it, EAddPosition::ADD_BOTTOM); + } + } +} + +void LLPanelPreferenceGameControl::clearSelectionState() +{ + if (gSelectedCell) + { + mChannelSelector->setVisible(FALSE); + gSelectedCell = nullptr; + } + gSelectedItem = nullptr; +} + +void LLPanelPreferenceGameControl::addTableSeparator() +{ + LLScrollListItem::Params separator_params; + separator_params.enabled(false); + LLScrollListCell::Params column_params; + column_params.type = "icon"; + column_params.value = "menu_separator"; + column_params.column = "action"; + column_params.color = LLColor4(0.f, 0.f, 0.f, 0.7f); + column_params.font_halign = LLFontGL::HCENTER; + separator_params.columns.add(column_params); + mActionTable->addRow(separator_params, EAddPosition::ADD_BOTTOM); +} + +void LLPanelPreferenceGameControl::updateTable() +{ + mActionTable->deselectAllItems(); +} + + LLFloaterPreferenceProxy::LLFloaterPreferenceProxy(const LLSD& key) : LLFloater(key), mSocksSettingsDirty(false) diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h index 40806c22fc..22421f296c 100644 --- a/indra/newview/llfloaterpreference.h +++ b/indra/newview/llfloaterpreference.h @@ -36,6 +36,7 @@ #include "llfloater.h" #include "llavatarpropertiesprocessor.h" #include "llconversationlog.h" +#include "llgamecontroltranslator.h" #include "llsearcheditor.h" #include "llsetkeybinddialog.h" #include "llkeyconflict.h" @@ -51,6 +52,7 @@ class LLScrollListCell; class LLSliderCtrl; class LLSD; class LLTextBox; +class LLPanelPreferenceGameControl; namespace ll { @@ -80,12 +82,12 @@ public: void apply(); void cancel(const std::vector settings_to_skip = {}); - /*virtual*/ void draw(); - /*virtual*/ bool postBuild(); - /*virtual*/ void onOpen(const LLSD& key); - /*virtual*/ void onClose(bool app_quitting); - /*virtual*/ void changed(); - /*virtual*/ void changed(const LLUUID& session_id, U32 mask) {}; + virtual void draw() override; + virtual bool postBuild() override; + virtual void onOpen(const LLSD& key) override; + virtual void onClose(bool app_quitting) override; + virtual void changed() override; + virtual void changed(const LLUUID& session_id, U32 mask) override {}; // static data update, called from message handler static void updateUserInfo(const std::string& visibility); @@ -99,8 +101,7 @@ public: // update Show Favorites checkbox static void updateShowFavoritesCheckbox(bool val); - void processProperties( void* pData, EAvatarProcessorType type ); - void saveAvatarProperties( void ); + void processProperties( void* pData, EAvatarProcessorType type ) override; static void saveAvatarPropertiesCoro(const std::string url, bool allow_publish); void selectPrivacyPanel(); void selectChatPanel(); @@ -251,7 +252,7 @@ class LLPanelPreference : public LLPanel { public: LLPanelPreference(); - /*virtual*/ bool postBuild(); + virtual bool postBuild() override; virtual ~LLPanelPreference(); @@ -297,14 +298,16 @@ private: class LLPanelPreferenceGraphics : public LLPanelPreference { public: - bool postBuild(); - void draw(); - void cancel(const std::vector settings_to_skip = {}); - void saveSettings(); + bool postBuild() override; + void draw() override; + void cancel(const std::vector settings_to_skip = {}) override; + void saveSettings() override; void resetDirtyChilds(); - void setHardwareDefaults(); + void setHardwareDefaults() override; void setPresetText(); + static const std::string getPresetsPath(); + protected: bool hasDirtyChilds(); @@ -320,11 +323,11 @@ public: LLPanelPreferenceControls(); virtual ~LLPanelPreferenceControls(); - bool postBuild(); + bool postBuild() override; - void apply(); - void cancel(const std::vector settings_to_skip = {}); - void saveSettings(); + void apply() override; + void cancel(const std::vector settings_to_skip = {}) override; + void saveSettings() override; void resetDirtyChilds(); void onListCommit(); @@ -340,9 +343,9 @@ public: void updateAndApply(); // from interface - /*virtual*/ bool onSetKeyBind(EMouseClickType click, KEY key, MASK mask, bool all_modes); - /*virtual*/ void onDefaultKeyBind(bool all_modes); - /*virtual*/ void onCancelKeyBind(); + bool onSetKeyBind(EMouseClickType click, KEY key, MASK mask, bool all_modes) override; + void onDefaultKeyBind(bool all_modes) override; + void onCancelKeyBind() override; private: // reloads settings, discards current changes, updates table @@ -367,6 +370,57 @@ private: S32 mEditingMode; }; +class LLPanelPreferenceGameControl : public LLPanelPreference +{ +public: + + enum InputType + { + TYPE_AXIS, + TYPE_BUTTON, + TYPE_NONE + }; + + LLPanelPreferenceGameControl(); + ~LLPanelPreferenceGameControl(); + + void apply() override; + void loadDefaults(); + void loadSettings(); + void saveSettings() override; + void updateEnabledState(); + + void onClickGameControlToServer(LLUICtrl* ctrl); + // "Agent" in this context means either Avatar or Flycam + void onClickGameControlToAgent(LLUICtrl* ctrl); + void onClickAgentToGameControl(LLUICtrl* ctrl); + void onActionSelect(); + void onCommitInputChannel(); + + static bool isWaitingForInputChannel(); + static void applyGameControlInput(const LLGameControl::InputChannel& channel); +protected: + bool postBuild() override; + + void populateActionTable(); + void populateColumns(); + void populateRows(const std::string& filename); + +private: + void clearSelectionState(); + void addTableSeparator(); + void updateTable(); + LOG_CLASS(LLPanelPreferenceGameControl); + + LLCheckBoxCtrl *mCheckGameControlToServer; // send game_control data to server + LLCheckBoxCtrl *mCheckGameControlToAgent; // use game_control data to move avatar + LLCheckBoxCtrl *mCheckAgentToGameControl; // translate external avatar actions to game_control data + + LLScrollListCtrl* mActionTable; + LLComboBox* mChannelSelector; + LLGameControlTranslator mActionTranslator; +}; + class LLAvatarComplexityControls { public: @@ -391,13 +445,13 @@ public: void cancel(); protected: - bool postBuild(); - void onOpen(const LLSD& key); - void onClose(bool app_quitting); + bool postBuild() override; + void onOpen(const LLSD& key) override; + void onClose(bool app_quitting) override; void saveSettings(); void onBtnOk(); void onBtnCancel(); - void onClickCloseBtn(bool app_quitting = false); + void onClickCloseBtn(bool app_quitting = false) override; void onChangeSocksSettings(); diff --git a/indra/newview/llflycam.cpp b/indra/newview/llflycam.cpp new file mode 100644 index 0000000000..53b8388f98 --- /dev/null +++ b/indra/newview/llflycam.cpp @@ -0,0 +1,117 @@ +/** + * @file llflycam.cpp + * @brief LLFlycam class implementation + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llflycam.h" + +LLFlycam::LLFlycam() +{ +} + + +void LLFlycam::setTransform(const LLVector3& position, const LLQuaternion& rotation) +{ + mPosition = position; + mRotation = rotation; + mRotation.normalize(); +} + +void LLFlycam::getTransform(LLVector3& position_out, LLQuaternion& rotation_out) +{ + position_out = mPosition; + rotation_out = mRotation; +} + + +void LLFlycam::setLinearVelocity(const LLVector3& velocity) +{ + // TODO: cap input + mLinearVelocity = velocity; +} + + +void LLFlycam::setPitchRate(F32 pitch_rate) +{ + // TODO: cap input + mPitchRate = pitch_rate; +} + + +void LLFlycam::setYawRate(F32 yaw_rate) +{ + // TODO: cap input + mYawRate = yaw_rate; +} + + +void LLFlycam::integrate(F32 delta_time) +{ + // cap delta_time to slow camera motion when framerates are low + constexpr F32 MAX_DELTA_TIME = 0.2f; + if (delta_time > MAX_DELTA_TIME) + { + delta_time = MAX_DELTA_TIME; + } + + if (mLinearVelocity.lengthSquared() > 0.0f) + { + mPosition += delta_time * mLinearVelocity; + } + + F32 angle = mPitchRate * delta_time; + bool needs_renormalization = false; + if (fabsf(angle) > 0.0f) + { + LLQuaternion dQ; + dQ.setAngleAxis(angle, 0.0f, 1.0f, 0.0f); + mRotation = dQ * mRotation; + needs_renormalization = true; + } + + angle = mYawRate * delta_time; + if (fabsf(angle) > 0.0f) + { + LLQuaternion dQ; + dQ.setAngleAxis(angle, 0.0f, 0.0f, 1.0f); + mRotation = dQ * mRotation; + needs_renormalization = true; + } + + if (needs_renormalization) + { + mRotation.normalize(); + } + + + /* + // from llviewerjoystick.cpp + LLMatrix3 mat(sFlycamRotation); + LLViewerCamera::getInstance()->setView(sFlycamZoom); + LLViewerCamera::getInstance()->setOrigin(sFlycamPosition); + LLViewerCamera::getInstance()->mXAxis = LLVector3(mat.mMatrix[0]); + LLViewerCamera::getInstance()->mYAxis = LLVector3(mat.mMatrix[1]); + LLViewerCamera::getInstance()->mZAxis = LLVector3(mat.mMatrix[2]); + */ +} diff --git a/indra/newview/llflycam.h b/indra/newview/llflycam.h new file mode 100644 index 0000000000..1125767a19 --- /dev/null +++ b/indra/newview/llflycam.h @@ -0,0 +1,55 @@ +/** + * @file llflycam.h + * @brief LLFlycam class header file + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llcoordframe.h" +#include "v3math.h" +#include "llquaternion.h" + +class LLFlycam +{ +public: + + LLFlycam(); + + void setTransform(const LLVector3& position, const LLQuaternion& rotation); + void getTransform(LLVector3& position_out, LLQuaternion& rotation_out); + + void setLinearVelocity(const LLVector3& velocity); + void setPitchRate(F32 pitch_rate); + void setYawRate(F32 yaw_rate); + + void integrate(F32 delta_time); + + +protected: + LLVector3 mPosition; + LLVector3 mLinearVelocity; + LLQuaternion mRotation; + F32 mPitchRate { 0.0f }; + F32 mYawRate { 0.0f }; +}; diff --git a/indra/newview/llviewerinput.cpp b/indra/newview/llviewerinput.cpp index ea3088613f..4968df943f 100644 --- a/indra/newview/llviewerinput.cpp +++ b/indra/newview/llviewerinput.cpp @@ -35,6 +35,7 @@ #include "llagentcamera.h" #include "llfloaterimnearbychat.h" #include "llfocusmgr.h" +#include "llgamecontrol.h" #include "llkeybind.h" // LLKeyData #include "llmorphview.h" #include "llmoveview.h" @@ -157,9 +158,6 @@ static void agent_handle_doubletap_run(EKeystate s, LLAgent::EDoubleTapRunMode m static void agent_push_forwardbackward( EKeystate s, S32 direction, LLAgent::EDoubleTapRunMode mode ) { - agent_handle_doubletap_run(s, mode); - if (KEYSTATE_UP == s) return; - F32 time = gKeyboard->getCurKeyElapsedTime(); S32 frame_count = ll_round(gKeyboard->getCurKeyElapsedFrameCount()); @@ -668,6 +666,7 @@ bool start_gesture( EKeystate s ) bool run_forward(EKeystate s) { + // HACK: we use AGENT_CONTROL_NUDGE_AT_POS to signify "run forward" if (KEYSTATE_UP != s) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_FORWARD) @@ -693,6 +692,7 @@ bool run_forward(EKeystate s) bool run_backward(EKeystate s) { + // HACK: we use AGENT_CONTROL_NUDGE_AT_NEG to signify "run backward" if (KEYSTATE_UP != s) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_BACKWARD) @@ -718,6 +718,7 @@ bool run_backward(EKeystate s) bool run_left(EKeystate s) { + // HACK: we use AGENT_CONTROL_NUDGE_LEFT_POS to signify "run left" if (KEYSTATE_UP != s) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDELEFT) @@ -768,6 +769,7 @@ bool run_right(EKeystate s) bool toggle_run(EKeystate s) { + // HACK: we use AGENT_CONTROL_FAST_AT to signify "run button" if (KEYSTATE_DOWN != s) return true; bool run = gAgent.getAlwaysRun(); if (run) @@ -786,6 +788,7 @@ bool toggle_run(EKeystate s) bool toggle_sit(EKeystate s) { + // HACK: we use AGENT_CONTROL_SIT_ON_GROUND to signify "sit button" if (KEYSTATE_DOWN != s) return true; if (gAgent.isSitting()) { diff --git a/indra/newview/llviewerjoystick.cpp b/indra/newview/llviewerjoystick.cpp index 7543fb3743..42238da418 100644 --- a/indra/newview/llviewerjoystick.cpp +++ b/indra/newview/llviewerjoystick.cpp @@ -148,18 +148,12 @@ BOOL CALLBACK di8_devices_callback(LPCDIDEVICEINSTANCE device_instance_ptr, LPVO LLSD guid = LLViewerJoystick::getInstance()->getDeviceUUID(); - bool init_device = false; - if (guid.isBinary()) + bool init_device = LLViewerJoystick::is3DConnexionDevice(product_name); + if (init_device && guid.isBinary()) { std::vector bin_bucket = guid.asBinary(); init_device = memcmp(&bin_bucket[0], &device_instance_ptr->guidInstance, sizeof(GUID)) == 0; } - else - { - // It might be better to init space navigator here, but if system doesn't has one, - // ndof will pick a random device, it is simpler to pick first device now to have an id - init_device = true; - } if (init_device) { @@ -1513,14 +1507,23 @@ std::string LLViewerJoystick::getDescription() return res; } +// static +bool LLViewerJoystick::is3DConnexionDevice(const std::string& device_name) +{ + bool answer = device_name.find("Space") == 0 + && ( (device_name.find("SpaceNavigator") == 0) + || (device_name.find("SpaceExplorer") == 0) + || (device_name.find("SpaceTraveler") == 0) + || (device_name.find("SpacePilot") == 0) + || (device_name.find("SpaceMouse") == 0)); + return answer; +} + bool LLViewerJoystick::isLikeSpaceNavigator() const { #if LIB_NDOF return (isJoystickInitialized() - && (strncmp(mNdofDev->product, "SpaceNavigator", 14) == 0 - || strncmp(mNdofDev->product, "SpaceExplorer", 13) == 0 - || strncmp(mNdofDev->product, "SpaceTraveler", 13) == 0 - || strncmp(mNdofDev->product, "SpacePilot", 10) == 0)); + && is3DConnexionDevice(mNdofDev->product)); #else return false; #endif diff --git a/indra/newview/llviewerjoystick.h b/indra/newview/llviewerjoystick.h index c989615653..b4fe3877f4 100644 --- a/indra/newview/llviewerjoystick.h +++ b/indra/newview/llviewerjoystick.h @@ -81,6 +81,8 @@ public: std::string getDescription(); void saveDeviceIdToSettings(); + static bool is3DConnexionDevice(const std::string& device_name); + protected: void updateEnabled(bool autoenable); void handleRun(F32 inc); diff --git a/indra/newview/skins/default/textures/bottomtray/Dpad.png b/indra/newview/skins/default/textures/bottomtray/Dpad.png new file mode 100644 index 0000000000..00fcb4beea Binary files /dev/null and b/indra/newview/skins/default/textures/bottomtray/Dpad.png differ diff --git a/indra/newview/skins/default/xui/en/floater_joystick.xml b/indra/newview/skins/default/xui/en/floater_joystick.xml index e6f0420698..597744238c 100644 --- a/indra/newview/skins/default/xui/en/floater_joystick.xml +++ b/indra/newview/skins/default/xui/en/floater_joystick.xml @@ -3,9 +3,9 @@ legacy_header_height="18" height="500" layout="topleft" - name="Joystick" + name="3Dconnexion Device" help_topic="Viewerhelp:Joystick_Configuration" - title="JOYSTICK CONFIGURATION" + title="3DCONNEXION DEVICE CONFIGURATION" width="569"> @@ -22,7 +22,7 @@ width="50" mouse_opaque="false" name="joystick_lbl"> - Joystick: + Device: + help_topic="preferences_game_control_tab" + name="game_control" /> diff --git a/indra/newview/skins/default/xui/en/game_control_table_camera_rows.xml b/indra/newview/skins/default/xui/en/game_control_table_camera_rows.xml new file mode 100644 index 0000000000..b2381c8493 --- /dev/null +++ b/indra/newview/skins/default/xui/en/game_control_table_camera_rows.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/game_control_table_columns.xml b/indra/newview/skins/default/xui/en/game_control_table_columns.xml new file mode 100644 index 0000000000..f88fc8305c --- /dev/null +++ b/indra/newview/skins/default/xui/en/game_control_table_columns.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/indra/newview/skins/default/xui/en/game_control_table_rows.xml b/indra/newview/skins/default/xui/en/game_control_table_rows.xml new file mode 100644 index 0000000000..90e6990842 --- /dev/null +++ b/indra/newview/skins/default/xui/en/game_control_table_rows.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml b/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml new file mode 100644 index 0000000000..d48c0fe0d2 --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml b/indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml deleted file mode 100644 index 4b693e8955..0000000000 --- a/indra/newview/skins/default/xui/en/panel_preferences_game_controls.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/indra/newview/skins/default/xui/en/panel_preferences_move.xml b/indra/newview/skins/default/xui/en/panel_preferences_move.xml index 0412466b4f..8a372256dc 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_move.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_move.xml @@ -270,7 +270,7 @@ width="200" />