From cedbf23fd19cb8c155b7e18a922a6da3c317ca1a Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 17 Dec 2018 09:58:00 -0500 Subject: SL-10190: Add menu commands to force AV or exception in coroutine. "Bad memory access" and "unhandled exception" are the two categories of error that we expect might be different in a coroutine than in the viewer's main fiber. Without this change, we've had no reliable way to force either of those to occur. This will require translation work for two new menu items. # Conflicts: # indra/newview/skins/default/xui/en/menu_viewer.xml --- indra/newview/llviewermenu.cpp | 55 +++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 17 deletions(-) (limited to 'indra/newview/llviewermenu.cpp') diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 01a4bd587d..92bca90aeb 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -33,10 +33,11 @@ #include "llviewermenu.h" // linden library includes -#include "llavatarnamecache.h" // IDEVO +#include "llavatarnamecache.h" // IDEVO (I Are Not Men!) +#include "llcombobox.h" +#include "llcoros.h" #include "llfloaterreg.h" #include "llfloatersidepanelcontainer.h" -#include "llcombobox.h" #include "llinventorypanel.h" #include "llnotifications.h" #include "llnotificationsutil.h" @@ -2379,6 +2380,7 @@ class LLAdvancedForceErrorLlerror : public view_listener_t return true; } }; + class LLAdvancedForceErrorBadMemoryAccess : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -2388,6 +2390,22 @@ class LLAdvancedForceErrorBadMemoryAccess : public view_listener_t } }; +class LLAdvancedForceErrorBadMemoryAccessCoro : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + LLCoros::instance().launch( + "AdvancedForceErrorBadMemoryAccessCoro", + [](){ + // Wait for one mainloop() iteration, letting the enclosing + // handleEvent() method return. + llcoro::suspend(); + force_error_bad_memory_access(NULL); + }); + return true; + } +}; + class LLAdvancedForceErrorInfiniteLoop : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -2406,6 +2424,22 @@ class LLAdvancedForceErrorSoftwareException : public view_listener_t } }; +class LLAdvancedForceErrorSoftwareExceptionCoro : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + LLCoros::instance().launch( + "AdvancedForceErrorSoftwareExceptionCoro", + [](){ + // Wait for one mainloop() iteration, letting the enclosing + // handleEvent() method return. + llcoro::suspend(); + force_error_software_exception(NULL); + }); + return true; + } +}; + class LLAdvancedForceErrorDriverCrash : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -2415,15 +2449,6 @@ class LLAdvancedForceErrorDriverCrash : public view_listener_t } }; -class LLAdvancedForceErrorCoroutineCrash : public view_listener_t -{ - bool handleEvent(const LLSD& userdata) - { - force_error_coroutine_crash(NULL); - return true; - } -}; - class LLAdvancedForceErrorThreadCrash : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -8150,11 +8175,6 @@ void force_error_driver_crash(void *) LLAppViewer::instance()->forceErrorDriverCrash(); } -void force_error_coroutine_crash(void *) -{ - LLAppViewer::instance()->forceErrorCoroutineCrash(); -} - void force_error_thread_crash(void *) { LLAppViewer::instance()->forceErrorThreadCrash(); @@ -9330,10 +9350,11 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedForceErrorBreakpoint(), "Advanced.ForceErrorBreakpoint"); view_listener_t::addMenu(new LLAdvancedForceErrorLlerror(), "Advanced.ForceErrorLlerror"); view_listener_t::addMenu(new LLAdvancedForceErrorBadMemoryAccess(), "Advanced.ForceErrorBadMemoryAccess"); + view_listener_t::addMenu(new LLAdvancedForceErrorBadMemoryAccessCoro(), "Advanced.ForceErrorBadMemoryAccessCoro"); view_listener_t::addMenu(new LLAdvancedForceErrorInfiniteLoop(), "Advanced.ForceErrorInfiniteLoop"); view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareException(), "Advanced.ForceErrorSoftwareException"); + view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareExceptionCoro(), "Advanced.ForceErrorSoftwareExceptionCoro"); view_listener_t::addMenu(new LLAdvancedForceErrorDriverCrash(), "Advanced.ForceErrorDriverCrash"); - view_listener_t::addMenu(new LLAdvancedForceErrorCoroutineCrash(), "Advanced.ForceErrorCoroutineCrash"); view_listener_t::addMenu(new LLAdvancedForceErrorThreadCrash(), "Advanced.ForceErrorThreadCrash"); view_listener_t::addMenu(new LLAdvancedForceErrorDisconnectViewer(), "Advanced.ForceErrorDisconnectViewer"); -- cgit v1.3 From 3f0a0051ba7cd54ddd83daed392996d9b23b9ee1 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Fri, 25 Mar 2022 18:48:44 +0200 Subject: SL-17036 update menu item similar to NavMesh icon --- indra/newview/lllocationinputctrl.h | 2 ++ indra/newview/llnavigationbar.cpp | 5 +++++ indra/newview/llnavigationbar.h | 2 ++ indra/newview/llviewermenu.cpp | 11 +++++------ 4 files changed, 14 insertions(+), 6 deletions(-) (limited to 'indra/newview/llviewermenu.cpp') diff --git a/indra/newview/lllocationinputctrl.h b/indra/newview/lllocationinputctrl.h index 79dbe17982..af2a9f6afd 100644 --- a/indra/newview/lllocationinputctrl.h +++ b/indra/newview/lllocationinputctrl.h @@ -109,6 +109,8 @@ public: LLLineEditor* getTextEntry() const { return mTextEntry; } void handleLoginComplete(); + bool isNavMeshDirty() { return mIsNavMeshDirty; } + private: enum EParcelIcon diff --git a/indra/newview/llnavigationbar.cpp b/indra/newview/llnavigationbar.cpp index f0ed4108cb..2dd7cfab27 100644 --- a/indra/newview/llnavigationbar.cpp +++ b/indra/newview/llnavigationbar.cpp @@ -733,3 +733,8 @@ int LLNavigationBar::getDefFavBarHeight() { return mDefaultFpRect.getHeight(); } + +bool LLNavigationBar::isRebakeNavMeshAvailable() +{ + return mCmbLocation->isNavMeshDirty(); +} diff --git a/indra/newview/llnavigationbar.h b/indra/newview/llnavigationbar.h index 646911a62c..11c671294a 100755 --- a/indra/newview/llnavigationbar.h +++ b/indra/newview/llnavigationbar.h @@ -102,6 +102,8 @@ public: int getDefNavBarHeight(); int getDefFavBarHeight(); + + bool isRebakeNavMeshAvailable(); private: // the distance between navigation panel and favorites panel in pixels diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index b902d346cb..77f8160aa5 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -94,6 +94,7 @@ #include "llmarketplacefunctions.h" #include "llmenuoptionpathfindingrebakenavmesh.h" #include "llmoveview.h" +#include "llnavigationbar.h" #include "llparcel.h" #include "llrootview.h" #include "llsceneview.h" @@ -5331,12 +5332,10 @@ class LLToolsEnablePathfindingRebakeRegion : public view_listener_t { bool returnValue = false; - if (LLPathfindingManager::getInstance() != NULL) - { - LLMenuOptionPathfindingRebakeNavmesh *rebakeInstance = LLMenuOptionPathfindingRebakeNavmesh::getInstance(); - returnValue = (rebakeInstance->canRebakeRegion() && - (rebakeInstance->getMode() == LLMenuOptionPathfindingRebakeNavmesh::kRebakeNavMesh_Available)); - } + if (LLNavigationBar::instanceExists()) + { + returnValue = LLNavigationBar::getInstance()->isRebakeNavMeshAvailable(); + } return returnValue; } }; -- cgit v1.3 From dd2fe2687ce58c6f6ebf16b2d66ae797022076f6 Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy Date: Tue, 31 May 2022 02:24:14 +0300 Subject: DRTVWR-544 post-merge fix (restored SL-14961) --- indra/newview/llappviewer.cpp | 6 ++++++ indra/newview/llappviewer.h | 20 ++++++++++---------- indra/newview/llviewermenu.cpp | 15 +++++++++++++++ indra/newview/skins/default/xui/en/menu_viewer.xml | 6 +++--- 4 files changed, 34 insertions(+), 13 deletions(-) (limited to 'indra/newview/llviewermenu.cpp') diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 5ec2b141ff..50ae72bbaa 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -5518,6 +5518,12 @@ void LLAppViewer::forceErrorDriverCrash() glDeleteTextures(1, NULL); } +void LLAppViewer::forceErrorCoroutineCrash() +{ + LL_WARNS() << "Forcing a crash in LLCoros" << LL_ENDL; + LLCoros::instance().launch("LLAppViewer::crashyCoro", [] {throw LLException("A deliberate crash from LLCoros"); }); +} + void LLAppViewer::forceErrorThreadCrash() { class LLCrashTestThread : public LLThread diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 1967ea6896..808af67d13 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -153,16 +153,16 @@ public: void removeMarkerFiles(); void removeDumpDir(); - // LLAppViewer testing helpers. - // *NOTE: These will potentially crash the viewer. Only for debugging. - virtual void forceErrorLLError(); - virtual void forceErrorBreakpoint(); - virtual void forceErrorBadMemoryAccess(); - virtual void forceErrorInfiniteLoop(); - virtual void forceErrorSoftwareException(); - virtual void forceErrorDriverCrash(); - virtual void forceErrorCoroutineCrash(); - virtual void forceErrorThreadCrash(); + // LLAppViewer testing helpers. + // *NOTE: These will potentially crash the viewer. Only for debugging. + virtual void forceErrorLLError(); + virtual void forceErrorBreakpoint(); + virtual void forceErrorBadMemoryAccess(); + virtual void forceErrorInfiniteLoop(); + virtual void forceErrorSoftwareException(); + virtual void forceErrorDriverCrash(); + virtual void forceErrorCoroutineCrash(); + virtual void forceErrorThreadCrash(); // The list is found in app_settings/settings_files.xml // but since they are used explicitly in code, diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index d32ad1fc10..e869d2dcc4 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -2466,6 +2466,15 @@ class LLAdvancedForceErrorDriverCrash : public view_listener_t } }; +class LLAdvancedForceErrorCoroutineCrash : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + force_error_coroutine_crash(NULL); + return true; + } +}; + class LLAdvancedForceErrorThreadCrash : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -8216,6 +8225,11 @@ void force_error_driver_crash(void *) LLAppViewer::instance()->forceErrorDriverCrash(); } +void force_error_coroutine_crash(void *) +{ + LLAppViewer::instance()->forceErrorCoroutineCrash(); +} + void force_error_thread_crash(void *) { LLAppViewer::instance()->forceErrorThreadCrash(); @@ -9397,6 +9411,7 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareException(), "Advanced.ForceErrorSoftwareException"); view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareExceptionCoro(), "Advanced.ForceErrorSoftwareExceptionCoro"); view_listener_t::addMenu(new LLAdvancedForceErrorDriverCrash(), "Advanced.ForceErrorDriverCrash"); + view_listener_t::addMenu(new LLAdvancedForceErrorCoroutineCrash(), "Advanced.ForceErrorCoroutineCrash"); view_listener_t::addMenu(new LLAdvancedForceErrorThreadCrash(), "Advanced.ForceErrorThreadCrash"); view_listener_t::addMenu(new LLAdvancedForceErrorDisconnectViewer(), "Advanced.ForceErrorDisconnectViewer"); diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 1801ccd432..8c38691852 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -2673,10 +2673,10 @@ function="World.EnvPreset" function="Advanced.ForceErrorSoftwareException" /> + label="Force a Crash in a Coroutine" + name="Force a Crash in a Coroutine"> + function="Advanced.ForceErrorCoroutineCrash" /> Date: Wed, 15 Jun 2022 18:03:11 +0300 Subject: SL-17584 Frame buffer object is now nessesary to run viewer --- indra/llrender/llgl.cpp | 14 ++++++++------ indra/llrender/llrendertarget.cpp | 2 +- indra/newview/lldynamictexture.cpp | 4 ++-- indra/newview/llfloaterpreference.cpp | 5 +---- indra/newview/llviewermenu.cpp | 17 ++--------------- 5 files changed, 14 insertions(+), 28 deletions(-) (limited to 'indra/newview/llviewermenu.cpp') diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index 18168d1c3f..df7c84e761 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -747,6 +747,14 @@ bool LLGLManager::initGL() LL_WARNS("RenderInit") << "GL Drivers do not support GL_ARB_multitexture" << LL_ENDL; return false; } + + if (!mHasFramebufferObject) + { + mHasRequirements = FALSE; + + LL_WARNS("RenderInit") << "GL Drivers do not support GL_ARB_framebuffer_object" << LL_ENDL; + return false; + } stop_glerror(); @@ -762,12 +770,6 @@ bool LLGLManager::initGL() //HACK always disable texture multisample, use FXAA instead mHasTextureMultisample = FALSE; -#if LL_WINDOWS - if (mIsIntel && mGLVersion <= 3.f) - { //never try to use framebuffer objects on older intel drivers (crashy) - mHasFramebufferObject = FALSE; - } -#endif if (mHasFramebufferObject) { diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index 0408010513..fffc15efc3 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -133,7 +133,7 @@ bool LLRenderTarget::allocate(U32 resx, U32 resy, U32 color_fmt, bool depth, boo mUsage = usage; mUseDepth = depth; - if ((sUseFBO || use_fbo) && gGLManager.mHasFramebufferObject) + if (sUseFBO || use_fbo) { if (depth) { diff --git a/indra/newview/lldynamictexture.cpp b/indra/newview/lldynamictexture.cpp index 63e7887d81..361a7666fa 100644 --- a/indra/newview/lldynamictexture.cpp +++ b/indra/newview/lldynamictexture.cpp @@ -125,7 +125,7 @@ void LLViewerDynamicTexture::preRender(BOOL clear_depth) llassert(mFullHeight <= static_cast(gPipeline.mPhysicsDisplay.getHeight())); } - if (gGLManager.mHasFramebufferObject && gPipeline.mPhysicsDisplay.isComplete() && !gGLManager.mIsAMD) + if (gPipeline.mPhysicsDisplay.isComplete() && !gGLManager.mIsAMD) { //using offscreen render target, just use the bottom left corner mOrigin.set(0, 0); } @@ -212,7 +212,7 @@ BOOL LLViewerDynamicTexture::updateAllInstances() return TRUE; } - bool use_fbo = gGLManager.mHasFramebufferObject && gPipeline.mBake.isComplete() && !gGLManager.mIsAMD; + bool use_fbo = gPipeline.mBake.isComplete() && !gGLManager.mIsAMD; if (use_fbo) { diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index 8d19aa36bb..1a76766322 100644 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -1200,7 +1200,6 @@ void LLFloaterPreference::refreshEnabledState() BOOL enabled = LLFeatureManager::getInstance()->isFeatureAvailable("RenderDeferred") && bumpshiny && shaders && - gGLManager.mHasFramebufferObject && (ctrl_wind_light->get()) ? TRUE : FALSE; ctrl_deferred->setEnabled(enabled); @@ -1251,7 +1250,6 @@ void LLFloaterPreferenceGraphicsAdvanced::refreshEnabledState() BOOL enabled = LLFeatureManager::getInstance()->isFeatureAvailable("RenderDeferred") && ((bumpshiny_ctrl && bumpshiny_ctrl->get()) ? TRUE : FALSE) && - gGLManager.mHasFramebufferObject && (ctrl_wind_light->get()) ? TRUE : FALSE; ctrl_deferred->setEnabled(enabled); @@ -1385,8 +1383,7 @@ void LLFloaterPreferenceGraphicsAdvanced::disableUnavailableSettings() } // disabled deferred - if (!LLFeatureManager::getInstance()->isFeatureAvailable("RenderDeferred") || - !gGLManager.mHasFramebufferObject) + if (!LLFeatureManager::getInstance()->isFeatureAvailable("RenderDeferred")) { ctrl_shadows->setEnabled(FALSE); ctrl_shadows->setValue(0); diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index e3de4267dc..5b300e96c6 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -2311,18 +2311,6 @@ class LLAdvancedEnableObjectObjectOcclusion: public view_listener_t } }; -///////////////////////////////////// -// Enable Framebuffer Objects /// -///////////////////////////////////// -class LLAdvancedEnableRenderFBO: public view_listener_t -{ - bool handleEvent(const LLSD& userdata) - { - bool new_value = gGLManager.mHasFramebufferObject; - return new_value; - } -}; - ///////////////////////////////////// // Enable Deferred Rendering /// ///////////////////////////////////// @@ -2330,7 +2318,7 @@ class LLAdvancedEnableRenderDeferred: public view_listener_t { bool handleEvent(const LLSD& userdata) { - bool new_value = gGLManager.mHasFramebufferObject && LLViewerShaderMgr::instance()->getShaderLevel(LLViewerShaderMgr::SHADER_WINDLIGHT) > 1 && + bool new_value = LLViewerShaderMgr::instance()->getShaderLevel(LLViewerShaderMgr::SHADER_WINDLIGHT) > 1 && LLViewerShaderMgr::instance()->getShaderLevel(LLViewerShaderMgr::SHADER_AVATAR) > 0; return new_value; } @@ -2343,7 +2331,7 @@ class LLAdvancedEnableRenderDeferredOptions: public view_listener_t { bool handleEvent(const LLSD& userdata) { - bool new_value = gGLManager.mHasFramebufferObject && LLViewerShaderMgr::instance()->getShaderLevel(LLViewerShaderMgr::SHADER_WINDLIGHT) > 1 && + bool new_value = LLViewerShaderMgr::instance()->getShaderLevel(LLViewerShaderMgr::SHADER_WINDLIGHT) > 1 && LLViewerShaderMgr::instance()->getShaderLevel(LLViewerShaderMgr::SHADER_AVATAR) > 0 && gSavedSettings.getBOOL("RenderDeferred"); return new_value; } @@ -9264,7 +9252,6 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedCheckWireframe(), "Advanced.CheckWireframe"); // Develop > Render view_listener_t::addMenu(new LLAdvancedEnableObjectObjectOcclusion(), "Advanced.EnableObjectObjectOcclusion"); - view_listener_t::addMenu(new LLAdvancedEnableRenderFBO(), "Advanced.EnableRenderFBO"); view_listener_t::addMenu(new LLAdvancedEnableRenderDeferred(), "Advanced.EnableRenderDeferred"); view_listener_t::addMenu(new LLAdvancedEnableRenderDeferredOptions(), "Advanced.EnableRenderDeferredOptions"); view_listener_t::addMenu(new LLAdvancedToggleRandomizeFramerate(), "Advanced.ToggleRandomizeFramerate"); -- cgit v1.3 From b7ff4d316d646ffbcdfda3c8539d45433c55c5c9 Mon Sep 17 00:00:00 2001 From: Cosmic Linden Date: Mon, 23 May 2022 16:29:39 -0700 Subject: SL-17329: Add advanced developer menu button to purge the disk cache --- indra/newview/llviewermenu.cpp | 30 ++++++++++++++++++++++ indra/newview/skins/default/xui/en/menu_viewer.xml | 12 +++++++++ 2 files changed, 42 insertions(+) (limited to 'indra/newview/llviewermenu.cpp') diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 5b300e96c6..78783de6ac 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -53,6 +53,7 @@ #include "llcompilequeue.h" #include "llconsole.h" #include "lldebugview.h" +#include "lldiskcache.h" #include "llenvironment.h" #include "llfilepicker.h" #include "llfirstuse.h" @@ -2088,6 +2089,32 @@ class LLAdvancedDropPacket : public view_listener_t } }; +////////////////////// +// PURGE DISK CACHE // +////////////////////// + + +class LLAdvancedPurgeDiskCache : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + LL::WorkQueue::ptr_t general_queue = LL::WorkQueue::getInstance("General"); + llassert_always(main_queue); + llassert_always(general_queue); + main_queue->postTo( + general_queue, + []() // Work done on general queue + { + LLDiskCache::getInstance()->purge(); + // Nothing needed to return + }, + [](){}); // Callback to main thread is empty as there is nothing left to do + + return true; + } +}; + //////////////////// // EVENT Recorder // @@ -9350,6 +9377,9 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedDisableMessageLog(), "Advanced.DisableMessageLog"); view_listener_t::addMenu(new LLAdvancedDropPacket(), "Advanced.DropPacket"); + // Advanced > Cache + view_listener_t::addMenu(new LLAdvancedPurgeDiskCache(), "Advanced.PurgeDiskCache"); + // Advanced > Recorder view_listener_t::addMenu(new LLAdvancedAgentPilot(), "Advanced.AgentPilot"); view_listener_t::addMenu(new LLAdvancedToggleAgentPilotLoop(), "Advanced.ToggleAgentPilotLoop"); diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 8d7cfe1116..cb3bbcfba0 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -3326,6 +3326,18 @@ function="World.EnvPreset" function="Advanced.DropPacket" /> + + + + + -- cgit v1.3