diff --git a/indra/cmake/CEFPlugin.cmake b/indra/cmake/CEFPlugin.cmake index bd12d6edfc..07712aa70e 100644 --- a/indra/cmake/CEFPlugin.cmake +++ b/indra/cmake/CEFPlugin.cmake @@ -5,15 +5,24 @@ add_library(ll::cef INTERFACE IMPORTED) if(DARWIN) find_library(APPKIT_LIBRARY AppKit REQUIRED) find_library(LIBCEF_DLL_WRAPPER_LIBRARY_RELEASE NAMES cef_dll_wrapper REQUIRED) + find_library(LIBCEF_DLL_WRAPPER_LIBRARY_DEBUG NAMES cef_dll_wrapper PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib" NO_DEFAULT_PATH) target_link_libraries(ll::cef INTERFACE - ${LIBCEF_DLL_WRAPPER_LIBRARY_RELEASE} + optimized ${LIBCEF_DLL_WRAPPER_LIBRARY_RELEASE} + debug ${LIBCEF_DLL_WRAPPER_LIBRARY_DEBUG} ${APPKIT_LIBRARY} ) set(CEF_FRAMEWORK_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/Chromium Embedded Framework.framework") else() find_library(LIBCEF_LIBRARY_RELEASE NAMES cef libcef PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib" REQUIRED NO_DEFAULT_PATH) find_library(LIBCEF_DLL_WRAPPER_LIBRARY_RELEASE NAMES cef_dll_wrapper PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib" REQUIRED NO_DEFAULT_PATH) - target_link_libraries(ll::cef INTERFACE ${LIBCEF_DLL_WRAPPER_LIBRARY_RELEASE} ${LIBCEF_LIBRARY_RELEASE}) + find_library(LIBCEF_LIBRARY_DEBUG NAMES cef libcef PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib" NO_DEFAULT_PATH) + find_library(LIBCEF_DLL_WRAPPER_LIBRARY_DEBUG NAMES cef_dll_wrapper PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib" NO_DEFAULT_PATH) + target_link_libraries(ll::cef INTERFACE + optimized ${LIBCEF_DLL_WRAPPER_LIBRARY_RELEASE} + debug ${LIBCEF_DLL_WRAPPER_LIBRARY_DEBUG} + optimized ${LIBCEF_LIBRARY_RELEASE} + debug ${LIBCEF_LIBRARY_DEBUG} + ) endif() set(CEF_INCLUDE_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include/cef/include") diff --git a/indra/dullahan b/indra/dullahan index 32f74a4664..9dc955ae26 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit 32f74a4664699c405bce97dacecc2f4b9fdba4f8 +Subproject commit 9dc955ae26de57536212fa8a557ead95407c0f04 diff --git a/indra/llfilesystem/lldir.h b/indra/llfilesystem/lldir.h index adc9234951..18ab82f135 100644 --- a/indra/llfilesystem/lldir.h +++ b/indra/llfilesystem/lldir.h @@ -86,8 +86,10 @@ class LLDir const std::string findFile(const std::string& filename, const std::vector filenames) const; const std::string findFile(const std::string& filename, const std::string& searchPath1 = "", const std::string& searchPath2 = "", const std::string& searchPath3 = "") const; - virtual std::string getLLPluginLauncher() = 0; // full path and name for the plugin shell - virtual std::string getLLPluginFilename(std::string base_name) = 0; // full path and name to the plugin DSO for this base_name (i.e. 'FOO' -> '/bar/baz/libFOO.so') + // full path and name of the plugin's host executable for this base_name + // (i.e. 'media_plugin_cef' -> '/bar/baz/llplugin/media_plugin_cef'); each + // plugin is its own executable now, launched directly (no SLPlugin shell). + virtual std::string getLLPluginFilename(std::string base_name) = 0; const std::string &getExecutablePathAndName() const; // Full pathname of the executable const std::string &getAppName() const; // install directory under progams/ ie "AlchemyViewer" diff --git a/indra/llfilesystem/lldir_linux.cpp b/indra/llfilesystem/lldir_linux.cpp index 0ebaab1114..5f3515d0fa 100644 --- a/indra/llfilesystem/lldir_linux.cpp +++ b/indra/llfilesystem/lldir_linux.cpp @@ -238,14 +238,10 @@ std::string LLDir_Linux::getCurPath() return tmp_str; } -/*virtual*/ std::string LLDir_Linux::getLLPluginLauncher() -{ - return gDirUtilp->getExecutableDir() + gDirUtilp->getDirDelimiter() + - "SLPlugin"; -} - /*virtual*/ std::string LLDir_Linux::getLLPluginFilename(std::string base_name) { - return gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + - "lib" + base_name + ".so"; + // Each plugin is now its own host executable named exactly for the plugin + // (e.g. media_plugin_cef), launched directly - there is no separate SLPlugin + // launcher or dlopen'd .so any more. + return gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + base_name; } diff --git a/indra/llfilesystem/lldir_linux.h b/indra/llfilesystem/lldir_linux.h index ad9cd76554..083575f718 100644 --- a/indra/llfilesystem/lldir_linux.h +++ b/indra/llfilesystem/lldir_linux.h @@ -48,7 +48,6 @@ class LLDir_Linux : public LLDir virtual std::string getCurPath(); virtual U32 countFilesInDir(const std::string &dirname, const std::string &mask); - /*virtual*/ std::string getLLPluginLauncher(); /*virtual*/ std::string getLLPluginFilename(std::string base_name); private: diff --git a/indra/llfilesystem/lldir_mac.h b/indra/llfilesystem/lldir_mac.h index 54dc2eb237..d402cb1612 100644 --- a/indra/llfilesystem/lldir_mac.h +++ b/indra/llfilesystem/lldir_mac.h @@ -46,7 +46,6 @@ class LLDir_Mac : public LLDir virtual std::string getCurPath(); - /*virtual*/ std::string getLLPluginLauncher(); /*virtual*/ std::string getLLPluginFilename(std::string base_name); }; diff --git a/indra/llfilesystem/lldir_mac.mm b/indra/llfilesystem/lldir_mac.mm index b6da2feae6..3eb1996b85 100644 --- a/indra/llfilesystem/lldir_mac.mm +++ b/indra/llfilesystem/lldir_mac.mm @@ -208,16 +208,13 @@ static bool CreateDirectory(const std::string &parent, return std::filesystem::path( std::filesystem::current_path() ).string(); } -/*virtual*/ std::string LLDir_Mac::getLLPluginLauncher() -{ - return gDirUtilp->getAppRODataDir() + gDirUtilp->getDirDelimiter() + - "SLPlugin.app/Contents/MacOS/SLPlugin"; -} - /*virtual*/ std::string LLDir_Mac::getLLPluginFilename(std::string base_name) { - return gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + - base_name + ".dylib"; + // Each plugin is now its own host .app bundle named for the plugin + // (e.g. media_plugin_cef.app), launched directly - there is no separate + // SLPlugin launcher or dlopen'd .dylib any more. + return gDirUtilp->getAppRODataDir() + gDirUtilp->getDirDelimiter() + + base_name + ".app/Contents/MacOS/" + base_name; } diff --git a/indra/llfilesystem/lldir_win32.cpp b/indra/llfilesystem/lldir_win32.cpp index 912a2963c6..699a15c0c8 100644 --- a/indra/llfilesystem/lldir_win32.cpp +++ b/indra/llfilesystem/lldir_win32.cpp @@ -373,16 +373,13 @@ std::string LLDir_Win32::getCurPath() return ll_convert(std::wstring(w_str)); } -/*virtual*/ std::string LLDir_Win32::getLLPluginLauncher() -{ - return gDirUtilp->getExecutableDir() + gDirUtilp->getDirDelimiter() + - "SLPlugin.exe"; -} - /*virtual*/ std::string LLDir_Win32::getLLPluginFilename(std::string base_name) { + // Each plugin is now its own host executable named for the plugin + // (e.g. media_plugin_cef.exe), launched directly - there is no separate + // SLPlugin launcher or dlopen'd .dll any more. return gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + - base_name + ".dll"; + base_name + ".exe"; } diff --git a/indra/llfilesystem/lldir_win32.h b/indra/llfilesystem/lldir_win32.h index c3a41d3e33..4b52286065 100644 --- a/indra/llfilesystem/lldir_win32.h +++ b/indra/llfilesystem/lldir_win32.h @@ -45,7 +45,6 @@ class LLDir_Win32 : public LLDir /*virtual*/ std::string getCurPath(); /*virtual*/ U32 countFilesInDir(const std::string &dirname, const std::string &mask); - /*virtual*/ std::string getLLPluginLauncher(); /*virtual*/ std::string getLLPluginFilename(std::string base_name); private: diff --git a/indra/llfilesystem/tests/lldir_test.cpp b/indra/llfilesystem/tests/lldir_test.cpp index 412feb147b..2dbf80d6bb 100644 --- a/indra/llfilesystem/tests/lldir_test.cpp +++ b/indra/llfilesystem/tests/lldir_test.cpp @@ -133,11 +133,6 @@ struct LLDir_Dummy: public LLDir return (mFilesystem.find(pathname) != mFilesystem.end()); } - virtual std::string getLLPluginLauncher() - { - // Implement this when we write a test that needs it - return ""; - } virtual std::string getLLPluginFilename(std::string base_name) { diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 9403f24469..7d4de31d3a 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -33,6 +33,12 @@ #include "llpluginmessageclasses.h" #include "llcontrol.h" +#if LL_WINDOWS +#include // _getpid (host pid for accelerated-paint handle dup) +#else +#include // getpid +#endif + extern LLControlGroup gSavedSettings; #if LL_DARWIN || LL_LINUX extern bool gHiDPISupport; @@ -56,6 +62,11 @@ LLPluginClassMedia::LLPluginClassMedia(LLPluginClassMediaOwner *owner) mOwner = owner; reset(); + // Unique per-media id for the macOS accelerated-paint mach-port demux. Media + // sources are created on the main thread, so a plain counter is fine. + static int sNextAccelId = 1; + mAccelId = sNextAccelId++; + //debug use mDeleteOK = true ; } @@ -75,11 +86,28 @@ bool LLPluginClassMedia::init(const std::string &launcher_filename, const std::s mPlugin = LLPluginProcessParent::create(this); mPlugin->setSleepTime(mSleepTime); + mPlugin->setUseDaemon(mUseDaemon, mDaemonRendezvous); // Queue up the media init message -- it will be sent after all the currently queued messages. LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "init"); message.setValue("target", mTarget); message.setValueReal("factor", mZoomFactor); + // Zero-copy paint: ask for GPU shared-texture handles, and hand the plugin + // this (viewer) process id so it can DuplicateHandle the shared texture into + // us across the process boundary. + message.setValueBoolean("accelerated_paint", mUseAcceleratedPaint); + // macOS shares the accelerated-paint IOSurface over a mach channel; the plugin + // rendezvous via the bootstrap name derived from host_pid, and tags each frame + // with accel_id so the viewer demuxes it back to this media. + message.setValueS32("accel_id", mAccelId); +#if LL_WINDOWS + message.setValueS32("host_pid", (S32)_getpid()); +#else + message.setValueS32("host_pid", (S32)getpid()); +#endif + // Linux: which windowing backend the viewer chose, so the CEF plugin can pin + // its Ozone platform (X11 vs Wayland) to match. Empty on other platforms. + message.setValue("display_server", mDisplayServer); sendMessage(message); mPlugin->init(launcher_filename, plugin_dir, plugin_filename, debug); @@ -1049,6 +1077,32 @@ void LLPluginClassMedia::receivePluginMessage(const LLPluginMessage &message) mTextureParamsReceived = true; } + else if(message_name == "accelerated_paint") + { + // Zero-copy frame ready. The plugin holds one persistent keyed-mutex + // shared texture and sends its viewer-side handle ONLY when that + // texture is (re)created (handle != 0, once per size); a "0" handle + // means "same texture, new frame". Keep the last real handle so a + // per-frame ping doesn't clear it before the consumer takes it. The + // value is a decimal string so a 64-bit handle survives intact. + // On Windows the handle is the persistent shared-texture handle, sent + // only when it is (re)created (handle != 0, once per size); "0" means + // "same texture, new frame", so keep the last real handle. On macOS and + // Linux the frame travels out-of-band (IOSurface mach port / dma-buf + // SCM_RIGHTS), so handle is always "0" and this message is just the + // per-frame dirty trigger. Value is a decimal string so a 64-bit handle + // survives intact. + unsigned long long h = strtoull(message.getValue("handle").c_str(), nullptr, 10); + if (h != 0) + { + mAcceleratedPaintHandle = h; + } + mAcceleratedPaintFormat = message.getValueS32("format"); + mAcceleratedPaintWidth = message.getValueS32("width"); + mAcceleratedPaintHeight = message.getValueS32("height"); + mAcceleratedPaintDirty = true; + mediaEvent(LLPluginClassMediaOwner::MEDIA_EVENT_CONTENT_UPDATED); + } else if(message_name == "updated") { if(message.hasValue("left")) diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h index d6f07c1632..c2df9c9883 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -172,6 +172,44 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner bool getDisableTimeout() { return mPlugin?mPlugin->getDisableTimeout():false; }; void setDisableTimeout(bool disable) { if(mPlugin) mPlugin->setDisableTimeout(disable); }; + // Route this media instance through the shared CEF daemon host, using a + // user-writable rendezvous path (see LLPluginProcessParent::setUseDaemon). + // Set before init(). + void setUseDaemon(bool use_daemon, const std::string& rendezvous_path = std::string()) + { mUseDaemon = use_daemon; mDaemonRendezvous = rendezvous_path; if(mPlugin) mPlugin->setUseDaemon(use_daemon, rendezvous_path); }; + bool getUseDaemon() const { return mUseDaemon; }; + + // Zero-copy paint: ask the plugin to deliver a GPU shared-texture handle + // (duplicated into this process) instead of CPU pixels. Set before init(). + void setUseAcceleratedPaint(bool b) { mUseAcceleratedPaint = b; }; + bool getUseAcceleratedPaint() const { return mUseAcceleratedPaint; }; + + // Linux: the viewer's windowing backend ("wayland"/"x11"), forwarded to the + // CEF plugin in init() so its Ozone platform matches ours. Empty lets the + // plugin auto-detect. Set before init(). + void setDisplayServer(const std::string& display_server) { mDisplayServer = display_server; }; + + // Per-media id sent to the plugin in init() and tagged onto each macOS + // IOSurface mach-port message, so the process-global viewer-side receiver can + // demux surfaces from many tabs/plugin processes back to the right media. + int getAccelId() const { return mAccelId; } + + // The plugin's stable shared texture: a native handle already duplicated into + // THIS process (Windows: a D3D11 shared-texture HANDLE), plus its + // cef_color_type format and coded size. The handle is PERSISTENT - it is only + // (re)sent when the plugin recreates the texture (per size), so it is kept, + // not consumed: the consumer compares it against what it last bound and only + // re-opens when it changes. mAcceleratedPaintDirty marks a fresh frame. + bool getAcceleratedPaintDirty() const { return mAcceleratedPaintDirty; }; + int getAcceleratedPaintFormat() const { return mAcceleratedPaintFormat; }; + int getAcceleratedPaintWidth() const { return mAcceleratedPaintWidth; }; + int getAcceleratedPaintHeight() const { return mAcceleratedPaintHeight; }; + unsigned long long getAcceleratedPaintHandle() const { return mAcceleratedPaintHandle; }; + void clearAcceleratedPaintDirty() { mAcceleratedPaintDirty = false; }; + // The accelerated frame's pixel layout/handle travels out-of-band on macOS + // (IOSurface mach port) and Linux (dma-buf fds via SCM_RIGHTS, demuxed by accel + // id - see LLCEFSurfaceReceiver), so no dma-buf plumbing is carried here. + // Inherited from LLPluginProcessParentOwner /* virtual */ void receivePluginMessage(const LLPluginMessage &message); /* virtual */ void pluginLaunchFailed(); @@ -430,6 +468,20 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner LLPluginProcessParent::ptr_t mPlugin; + bool mUseDaemon = false; + std::string mDaemonRendezvous; + + // accelerated (zero-copy) paint - see setUseAcceleratedPaint / the getters above + bool mUseAcceleratedPaint = false; + // Linux windowing backend name forwarded to the CEF plugin - see setDisplayServer. + std::string mDisplayServer; + // Unique per-media id (macOS mach-port demux); assigned at construction. + int mAccelId = 0; + unsigned long long mAcceleratedPaintHandle = 0; // native handle, dup'd into this process + int mAcceleratedPaintFormat = 0; + int mAcceleratedPaintWidth = 0; + int mAcceleratedPaintHeight = 0; + bool mAcceleratedPaintDirty = false; LLRect mDirtyRect; diff --git a/indra/llplugin/llplugininstance.cpp b/indra/llplugin/llplugininstance.cpp index 99036d6dc2..20000f2ece 100644 --- a/indra/llplugin/llplugininstance.cpp +++ b/indra/llplugin/llplugininstance.cpp @@ -30,22 +30,18 @@ #include "llplugininstance.h" -#include -#include - -#if LL_WINDOWS -#include "direct.h" // needed for _chdir() -#endif - /** Virtual destructor. */ LLPluginInstanceMessageListener::~LLPluginInstanceMessageListener() { } -/** - * TODO:DOC describe how it's used - */ -const char *LLPluginInstance::PLUGIN_INIT_FUNCTION_NAME = "LLPluginInitEntryPoint"; +LLPluginInstance::pluginInitFunction LLPluginInstance::sStaticInitFunction = NULL; + +// static +void LLPluginInstance::setStaticInitFunction(pluginInitFunction func) +{ + sStaticInitFunction = func; +} /** * Constructor. @@ -64,70 +60,33 @@ LLPluginInstance::LLPluginInstance(LLPluginInstanceMessageListener *owner) : */ LLPluginInstance::~LLPluginInstance() { - // mDSOHandle's destructor unloads the shared library if loaded. } /** - * Dynamically loads the plugin and runs the plugin's init function. + * Runs the statically-linked plugin's init function. + * + * Each plugin now lives in its own host executable that statically links exactly + * one plugin and registers its entry point via setStaticInitFunction(), so there + * is no longer a dlopen path. plugin_dir/plugin_file are vestigial - they remain + * in the load_plugin message contract but are ignored here. * - * @param[in] plugin_file Name of plugin dll/dylib/so. TODO:DOC is this correct? see .h - * @return 0 if successful, APR error code or error code from the plugin's init function on failure. + * @return 0 if successful, or the error code returned from the plugin's init function. */ int LLPluginInstance::load(const std::string& plugin_dir, std::string &plugin_file) { - pluginInitFunction init_function = NULL; + (void)plugin_dir; + (void)plugin_file; - if ( plugin_dir.length() ) + if (!sStaticInitFunction) { -#if LL_WINDOWS - // VWR-21275: - // *SOME* Windows systems fail to load the Qt plugins if the current working - // directory is not the same as the directory with the Qt DLLs in. - // This should not cause any run time issues since we are changing the cwd for the - // plugin shell process and not the viewer. - // Changing back to the previous directory is not necessary since the plugin shell - // quits once the plugin exits. - _chdir( plugin_dir.c_str() ); -#endif - }; - - int result = 0; - - try - { - mDSOHandle.load(boost::dll::fs::path(plugin_file), - boost::dll::load_mode::rtld_now); - } - catch (const std::exception& e) - { - LL_WARNS("Plugin") << "boost::dll load of " << plugin_file - << " failed: " << e.what() << LL_ENDL; - result = -1; + LL_WARNS("Plugin") << "no statically-linked plugin init function registered" << LL_ENDL; + return -1; } - if(result == 0) + int result = sStaticInitFunction(staticReceiveMessage, (void*)this, &mPluginSendMessageFunction, &mPluginUserData); + if (result != 0) { - try - { - init_function = &mDSOHandle.get>( - PLUGIN_INIT_FUNCTION_NAME); - } - catch (const std::exception& e) - { - LL_WARNS("Plugin") << "symbol lookup for " << PLUGIN_INIT_FUNCTION_NAME - << " failed: " << e.what() << LL_ENDL; - result = -1; - } - } - - if(result == 0) - { - result = init_function(staticReceiveMessage, (void*)this, &mPluginSendMessageFunction, &mPluginUserData); - - if(result != 0) - { - LL_WARNS("Plugin") << "call to init function failed with error " << result << LL_ENDL; - } + LL_WARNS("Plugin") << "call to init function failed with error " << result << LL_ENDL; } return result; diff --git a/indra/llplugin/llplugininstance.h b/indra/llplugin/llplugininstance.h index 5f7af30130..efca93c107 100644 --- a/indra/llplugin/llplugininstance.h +++ b/indra/llplugin/llplugininstance.h @@ -30,8 +30,6 @@ #include "llstring.h" -#include - /** * @brief LLPluginInstanceMessageListener receives messages sent from the plugin loader shell to the plugin. */ @@ -53,8 +51,9 @@ class LLPluginInstance LLPluginInstance(LLPluginInstanceMessageListener *owner); virtual ~LLPluginInstance(); - // Load a plugin dll/dylib/so - // Returns 0 if successful, APR error code or error code returned from the plugin's init function on failure. + // Run the statically-linked plugin's init function (registered via + // setStaticInitFunction). plugin_dir/plugin_file are ignored - kept only for + // the load_plugin message contract. Returns 0 on success. int load(const std::string& plugin_dir, std::string &plugin_file); // Sends a message to the plugin. @@ -80,19 +79,24 @@ class LLPluginInstance */ typedef int (*pluginInitFunction) (sendMessageFunction host_send_func, void *host_user_data, sendMessageFunction *plugin_send_func, void **plugin_user_data); - /** Name of plugin init function */ - static const char *PLUGIN_INIT_FUNCTION_NAME; + // Register the statically-linked plugin init entry point. Each plugin now + // lives in its own host executable that links exactly one plugin, so load() + // always calls this directly - there is no dlopen path. (This also satisfies + // the Windows sandbox requirement that a host and its sub-processes be one + // image, and avoids dlopen of large TLS-using libraries like CEF on Linux.) + static void setStaticInitFunction(pluginInitFunction func); private: static void staticReceiveMessage(const char *message_string, void **user_data); void receiveMessage(const char *message_string); - boost::dll::shared_library mDSOHandle; - void *mPluginUserData; sendMessageFunction mPluginSendMessageFunction; LLPluginInstanceMessageListener *mOwner; + + // non-null in dedicated single-plugin host executables; see setStaticInitFunction + static pluginInitFunction sStaticInitFunction; }; #endif // LL_LLPLUGININSTANCE_H diff --git a/indra/llplugin/llpluginprocesschild.cpp b/indra/llplugin/llpluginprocesschild.cpp index 7c6dad05ab..390e7cbf42 100644 --- a/indra/llplugin/llpluginprocesschild.cpp +++ b/indra/llplugin/llpluginprocesschild.cpp @@ -55,13 +55,28 @@ LLPluginProcessChild::~LLPluginProcessChild() { sendMessageToPlugin(LLPluginMessage("base", "cleanup")); - // IMPORTANT: under some (unknown) circumstances the library unload triggered when mInstance is deleted - // appears to fail and lock up which means that a given instance of the slplugin process never exits. - // This is bad, especially when users try to update their version of SL - it fails because the slplugin - // process as well as a bunch of plugin specific files are locked and cannot be overwritten. - exit(0); - //delete mInstance; - //mInstance = NULL; + if (mDaemonMode) + { + // Daemon tab: this process hosts many tabs, so exit() here would kill + // every other tab (the "close one media, all of them die" crash). The + // library-unload lockup the exit(0) below guards against cannot happen + // in the daemon either - daemon plugins are statically linked, so + // there is no DSO to unload and ~LLPluginInstance is a no-op. Delete + // the instance directly. (Normally the graceful unload path has + // already nulled mInstance and we never get here.) + delete mInstance; + mInstance = NULL; + } + else + { + // IMPORTANT: under some (unknown) circumstances the library unload triggered when mInstance is deleted + // appears to fail and lock up which means that a given instance of the slplugin process never exits. + // This is bad, especially when users try to update their version of SL - it fails because the slplugin + // process as well as a bunch of plugin specific files are locked and cannot be overwritten. + exit(0); + //delete mInstance; + //mInstance = NULL; + } } } @@ -86,23 +101,34 @@ void LLPluginProcessChild::idle(void) { // Once we have hit the shutdown request state checking for errors might put us in a spurious // error state... don't do that. + // A lost parent socket means this tab is going away. In the daemon a + // hard STATE_ERROR would reap the tab with its browser still live in + // the shared CEF runtime (and, pre-unload, leave mInstance set), which + // crashes every tab. Instead run the same graceful unload as a normal + // shutdown so the browser closes cleanly first, then the daemon reaps + // us. The single-process host keeps the old hard-error behaviour. + const EState socket_dead_state = + (mDaemonMode && mInstance != NULL) ? STATE_SHUTDOWNREQ : STATE_ERROR; + if (APR_STATUS_IS_EOF(mSocketError)) { // Plugin socket was closed. This covers both normal plugin termination and host crashes. - setState(STATE_ERROR); + setState(socket_dead_state); } else if (mSocketError != APR_SUCCESS) { - LL_INFOS("Plugin") << "message pipe is in error state (" << mSocketError << "), moving to STATE_ERROR" << LL_ENDL; - setState(STATE_ERROR); + LL_INFOS("Plugin") << "message pipe is in error state (" << mSocketError << "), moving to " + << (socket_dead_state == STATE_SHUTDOWNREQ ? "STATE_SHUTDOWNREQ" : "STATE_ERROR") << LL_ENDL; + setState(socket_dead_state); } - if ((mState > STATE_INITIALIZED) && (mMessagePipe == NULL)) + if ((mState > STATE_INITIALIZED) && (mState < STATE_SHUTDOWNREQ) && (mMessagePipe == NULL)) { // The pipe has been closed -- we're done. // TODO: This could be slightly more subtle, but I'm not sure it needs to be. - LL_INFOS("Plugin") << "message pipe went away, moving to STATE_ERROR" << LL_ENDL; - setState(STATE_ERROR); + LL_INFOS("Plugin") << "message pipe went away, moving to " + << (socket_dead_state == STATE_SHUTDOWNREQ ? "STATE_SHUTDOWNREQ" : "STATE_ERROR") << LL_ENDL; + setState(socket_dead_state); } } diff --git a/indra/llplugin/llpluginprocesschild.h b/indra/llplugin/llpluginprocesschild.h index 5de0f90209..cefdf37c8a 100644 --- a/indra/llplugin/llpluginprocesschild.h +++ b/indra/llplugin/llpluginprocesschild.h @@ -50,6 +50,13 @@ class LLPluginProcessChild: public LLPluginMessagePipeOwner, public LLPluginInst void sleep(F64 seconds); void pump(); + // Mark this child as a tab inside the shared daemon host (one process serving + // many tabs). In daemon mode the child must never exit() the process on its + // own teardown (that would kill every other tab) and a lost parent socket is + // handled with a graceful plugin unload rather than a hard error. Set before + // the first idle(). + void setDaemonMode(bool daemon) { mDaemonMode = daemon; } + // returns true if the plugin is in the steady state (processing messages) bool isRunning(void); @@ -104,6 +111,7 @@ class LLPluginProcessChild: public LLPluginMessagePipeOwner, public LLPluginInst LLTimer mHeartbeat; F64 mSleepTime; F64 mCPUElapsed; + bool mDaemonMode = false; bool mBlockingRequest; bool mBlockingResponseReceived; std::queue mMessageQueue; diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index 5856f9ddd6..c5d7ba311e 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -38,6 +38,12 @@ #include "workqueue.h" #include "llapr.h" +#include "llfile.h" + +#include "apr_network_io.h" + +#include +#include //virtual LLPluginProcessParentOwner::~LLPluginProcessParentOwner() @@ -494,8 +500,72 @@ void LLPluginProcessParent::idle(void) case STATE_LISTENING: { + // Shared CEF daemon: register a tab with (or decide to spawn) + // the daemon instead of launching a private process. Runs + // once, gated by the launch state below. + if (mUseDaemon && !mDaemonRendezvous.empty() && !mProcess && !mProcessCreationRequested) + { + EDaemonDisp disp = daemonDiscoverOrRegister(); + if (disp == DAEMON_CONNECTED) + { + // Registered: the daemon connects back to our listen + // port. We own no process (mProcess stays null); + // liveness comes from the heartbeat. + mProcessCreationRequested = true; + mHeartbeat.start(); + mHeartbeat.setTimerExpirySec(mPluginLaunchTimeout); + setState(STATE_LAUNCHED); + break; + } + if (disp == DAEMON_WAIT) + { + // Another parent is launching the daemon; retry next idle. + break; + } + + // DAEMON_NEED_SPAWN: launch the daemon so it outlives this + // parent and is shared by later tabs - no parent owns it + // (otherwise closing this tab's media would kill the whole + // daemon). It serves us as its first tab via our port, then + // connects back like any registration. + // + // Keep autokill at its default (true) so the daemon joins + // the viewer's job object: it dies with the viewer, and the + // CEF sandbox broker requires it - the job object is the + // only launch difference from the (working) per-process + // host, and launching the daemon outside it breaks the + // sandbox. attached=false so discarding this handle below + // does NOT kill the daemon (it must outlive this tab). + LLProcess::Params params = mProcessParams; + params.args.add(stringize(mBoundPort)); + params.args.add("--daemon"); + params.args.add(daemonRendezvousPath()); + params.attached = false; + // fire and forget; keep no mProcess (attached=false means + // dropping this handle does not terminate the daemon). + LLProcessPtr daemon_process = LLProcess::create(params); + if (!daemon_process) + { + // Launch failed - release the spawn lock so another + // parent can retry instead of waiting for a daemon + // callback that will never arrive. + LL_WARNS("Plugin") << "failed to launch CEF daemon" << LL_ENDL; + LLFile::remove(daemonRendezvousPath() + ".lock"); + errorState(); + break; + } + mProcessCreationRequested = true; + mHeartbeat.start(); + mHeartbeat.setTimerExpirySec(mPluginLaunchTimeout); + setState(STATE_LAUNCHED); + break; + } + // Only argument to the launcher is the port number we're listening on - mProcessParams.args.add(stringize(mBoundPort)); + if (!mProcess && !mProcessCreationRequested) + { + mProcessParams.args.add(stringize(mBoundPort)); + } // Launch the plugin process. if (mDebug && !mProcess) @@ -659,7 +729,26 @@ void LLPluginProcessParent::idle(void) break; case STATE_EXITING: - if (! LLProcess::isRunning(mProcess)) + if (mUseDaemon && !mProcess) + { + // Daemon mode: we do not own the host process, so there is no + // LLProcess to wait on. Instead wait for the shared daemon's + // tab to finish its graceful teardown and drop its end of the + // socket (EOF / socket error), or for the lockup timeout. + // Tearing our socket down before the tab has unloaded its + // browser would abort the shared-runtime teardown and crash + // every other tab in the daemon. + if (mSocketError != APR_SUCCESS || !mMessagePipe) + { + setState(STATE_CLEANUP); + } + else if (pluginLockedUp()) + { + LL_WARNS("Plugin") << "timeout waiting for daemon tab to exit, cleaning up" << LL_ENDL; + setState(STATE_CLEANUP); + } + } + else if (! LLProcess::isRunning(mProcess)) { setState(STATE_CLEANUP); } @@ -1263,7 +1352,12 @@ bool LLPluginProcessParent::pluginLockedUpOrQuit() { bool result = false; - if (! LLProcess::isRunning(mProcess)) + // In daemon mode this parent does not own the host process (mProcess is + // null - the shared daemon owns it), so a missing process is NOT death; + // liveness comes from the socket/heartbeat (pluginLockedUp) instead. When a + // process is owned (the normal path, and daemon launch before connect) the + // isRunning check applies as usual. + if (!(mUseDaemon && !mProcess) && ! LLProcess::isRunning(mProcess)) { LL_WARNS("Plugin") << "child exited" << LL_ENDL; result = true; @@ -1289,3 +1383,116 @@ bool LLPluginProcessParent::pluginLockedUp() return (mHeartbeat.getStarted() && mHeartbeat.hasExpired()); } +std::string LLPluginProcessParent::daemonRendezvousPath() const +{ + // Caller-supplied, user-writable path (see setUseDaemon). All CEF tabs in a + // viewer are given the same path so they agree on one daemon; it must not be + // the install/plugin dir, which may be read-only. + return mDaemonRendezvous; +} + +// static +U32 LLPluginProcessParent::readDaemonControlPort(const std::string& path) +{ + llifstream f(path); + if (!f.is_open()) + { + return 0; + } + U32 port = 0; + // A corrupt/stale rendezvous file can hold a value above 65535; casting that to + // apr_port_t would wrap and register with the wrong localhost port. Reject it. + if (!(f >> port) || port > 65535) + { + return 0; + } + return port; +} + +bool LLPluginProcessParent::registerWithDaemon(U32 control_port) +{ + if (!control_port) + { + return false; + } + + apr_socket_t* sock = nullptr; + if (apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP, gAPRPoolp) != APR_SUCCESS) + { + return false; + } + apr_socket_timeout_set(sock, 2 * APR_USEC_PER_SEC); + + bool ok = false; + apr_sockaddr_t* addr = nullptr; + if (apr_sockaddr_info_get(&addr, "127.0.0.1", APR_INET, (apr_port_t)control_port, 0, gAPRPoolp) == APR_SUCCESS && + apr_socket_connect(sock, addr) == APR_SUCCESS) + { + // Tell the daemon which port to connect back to for this tab. + std::string msg = stringize(mBoundPort) + "\n"; + apr_size_t len = msg.size(); + ok = (apr_socket_send(sock, msg.data(), &len) == APR_SUCCESS) && (len == msg.size()); + } + apr_socket_close(sock); + return ok; +} + +// static +bool LLPluginProcessParent::acquireSpawnLock(const std::string& lock_path) +{ + // A stale lock left by an aborted launch is stolen once it is older than this. + static const time_t LOCK_STALE_SECONDS = 15; + + // Atomic create-exclusive: LLFile::noreplace maps to O_EXCL / CREATE_NEW, so + // exactly one parent wins the create even if several race here. The LLFile is + // closed when it leaves scope; the lock file itself persists until the daemon + // publishes the rendezvous (slplugin_daemon_run removes it) or it is stolen + // as stale below. + std::error_code ec; + { + LLFile lock(lock_path, LLFile::out | LLFile::noreplace, ec); + if (lock) + { + return true; + } + } + + if (ec == std::errc::file_exists) + { + llstat st; + if (LLFile::stat(lock_path, &st) == 0 && + (time(nullptr) - st.st_mtime) > LOCK_STALE_SECONDS) + { + LLFile::remove(lock_path); + LLFile lock(lock_path, LLFile::out | LLFile::noreplace, ec); + if (lock) + { + return true; + } + } + } + return false; +} + +LLPluginProcessParent::EDaemonDisp LLPluginProcessParent::daemonDiscoverOrRegister() +{ + const std::string rv = daemonRendezvousPath(); + + // 1. A daemon already running? Register our tab with it (it connects back). + U32 control_port = readDaemonControlPort(rv); + if (control_port && registerWithDaemon(control_port)) + { + LL_INFOS("Plugin") << "registered CEF tab with daemon (control port " << control_port << ")" << LL_ENDL; + return DAEMON_CONNECTED; + } + + // 2. No reachable daemon (missing or stale rendezvous). Serialize launching. + if (acquireSpawnLock(rv + ".lock")) + { + return DAEMON_NEED_SPAWN; + } + + // 3. Another parent is launching the daemon; wait and retry next idle. + return DAEMON_WAIT; +} + diff --git a/indra/llplugin/llpluginprocessparent.h b/indra/llplugin/llpluginprocessparent.h index ea604ca8d7..d0a75f2a68 100644 --- a/indra/llplugin/llpluginprocessparent.h +++ b/indra/llplugin/llpluginprocessparent.h @@ -119,6 +119,18 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner bool getDisableTimeout() { return mDisableTimeout; }; void setDisableTimeout(bool disable) { mDisableTimeout = disable; }; + // In daemon mode this parent connects to a shared host process it does not + // own (mProcess stays null), so liveness is tracked via the socket/heartbeat + // rather than LLProcess::isRunning. Has no effect unless the daemon launch + // path is taken. Set before init(). + bool getUseDaemon() const { return mUseDaemon; }; + // rendezvous_path must be a user-writable absolute path supplied by the + // caller (NOT the install/plugin dir, which may be read-only): the daemon + // writes its control port there and later tabs read it to find the running + // daemon. Daemon mode is skipped if the path is empty. + void setUseDaemon(bool use_daemon, const std::string& rendezvous_path = std::string()) + { mUseDaemon = use_daemon; mDaemonRendezvous = rendezvous_path; }; + void setLaunchTimeout(F32 timeout) { mPluginLaunchTimeout = timeout; }; void setLockupTimeout(F32 timeout) { mPluginLockupTimeout = timeout; }; @@ -160,6 +172,24 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner bool pluginLockedUp(); bool pluginLockedUpOrQuit(); + // --- shared CEF daemon (mUseDaemon) discover-or-spawn --- + enum EDaemonDisp + { + DAEMON_CONNECTED, // registered with a running daemon; it connects back + DAEMON_NEED_SPAWN, // no daemon; this parent should launch one (holds the lock) + DAEMON_WAIT // another parent is launching the daemon; retry next idle + }; + // path of the file the daemon writes its control port to (per plugin dir) + std::string daemonRendezvousPath() const; + // try to register a tab with a running daemon, else decide to spawn/wait + EDaemonDisp daemonDiscoverOrRegister(); + // read the daemon control port from the rendezvous file (0 if none/invalid) + static U32 readDaemonControlPort(const std::string& path); + // connect to the daemon control port and send our listen port (mBoundPort) + bool registerWithDaemon(U32 control_port); + // atomically take the spawn lock (stealing a stale one); true if we got it + static bool acquireSpawnLock(const std::string& lock_path); + bool accept(); LLSocket::ptr_t mListenSocket; @@ -169,6 +199,11 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner LLProcess::Params mProcessParams; LLProcessPtr mProcess; bool mProcessCreationRequested = false; + // true when this parent talks to a shared daemon host instead of its own + // launched process (see setUseDaemon) + bool mUseDaemon = false; + // user-writable path the daemon publishes its control port to (caller-supplied) + std::string mDaemonRendezvous; std::string mPluginFile; std::string mPluginDir; diff --git a/indra/llplugin/slplugin/CMakeLists.txt b/indra/llplugin/slplugin/CMakeLists.txt index 11a165cd14..83b9b98b35 100644 --- a/indra/llplugin/slplugin/CMakeLists.txt +++ b/indra/llplugin/slplugin/CMakeLists.txt @@ -1,60 +1,8 @@ - -include(PluginAPI) - -### SLPlugin - -add_executable(SLPlugin - WIN32 - MACOSX_BUNDLE - ) - -target_sources(SLPlugin - PRIVATE - slplugin.cpp - ) - - -if (WINDOWS) - target_sources(SLPlugin - PRIVATE - slplugin.manifest - ) -elseif (DARWIN) - target_sources(SLPlugin - PRIVATE - slplugin-objc.mm - - PUBLIC - FILE_SET HEADERS - FILES - slplugin-objc.h - ) - - set_source_files_properties( - slplugin-objc.mm - PROPERTIES - SKIP_PRECOMPILE_HEADERS TRUE - ) -endif () - -target_link_libraries(SLPlugin - llplugin - llmessage - llcommon - ll::pluginlibraries - ) - -if(WINDOWS) - set_target_properties(SLPlugin - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${VIEWER_STAGING_DIR} - ) -elseif (DARWIN) - set_target_properties(SLPlugin - PROPERTIES - BUILD_WITH_INSTALL_RPATH 1 - INSTALL_RPATH "@executable_path/../../../../Frameworks;@executable_path/../Frameworks;@executable_path/../Frameworks/plugins" - MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/slplugin_info.plist - XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym" - ) -endif () +# -*- cmake -*- + +# The generic SLPlugin launcher and its dlopen-based plugin loading have been +# removed: each media plugin is now its own host executable that statically links +# the plugin (see media_plugins/*/CMakeLists.txt). This directory no longer builds +# a target of its own - it just provides the shared host driver sources +# (slplugin.cpp, slplugin_static.cpp, slplugin_daemon.cpp, slplugin-objc.mm) that +# those per-plugin executables compile in by path. diff --git a/indra/llplugin/slplugin/slplugin.cpp b/indra/llplugin/slplugin/slplugin.cpp index 81a27cf2e5..3553d53686 100644 --- a/indra/llplugin/slplugin/slplugin.cpp +++ b/indra/llplugin/slplugin/slplugin.cpp @@ -33,6 +33,7 @@ #include "llpluginprocesschild.h" #include "llpluginmessage.h" +#include "llplugininstance.h" #include "llerrorcontrol.h" #include "llapr.h" #include "llstring.h" @@ -41,6 +42,29 @@ #include using namespace std; +// Returns the statically-linked plugin init entry point for this host. Each +// per-plugin host executable links exactly one definition: slplugin_static.cpp +// for a plain plugin, or slplugin_cef.cpp for the CEF host - both return +// &LLPluginInitEntryPoint (there is no dlopen path any more). +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init(); + +// The host driver: register the statically-linked plugin, then pump the +// plugin<->parent message loop until the plugin is done. Factored out of the +// platform entry points so the CEF bootstrap host (slplugin_cef_bootstrap.cpp) +// can reuse it after setting up the sandbox. Defined below. +int slplugin_run(U32 port); + +// Host-provided entry the platform main() hands control to. Serves connection +// `port`; if `daemon_rendezvous` is non-empty the host runs as the shared +// multi-tab CEF daemon (publishing its control port to that path), otherwise it +// serves the single connection. The plain hosts' definition (slplugin_static.cpp) +// just calls slplugin_run(); the CEF host's (slplugin_cef.cpp) adds the +// persistent-runtime / daemon behaviour, keeping dullahan out of the other +// hosts. The Windows CEF DLL uses its own bootstrap entry +// (slplugin_cef_bootstrap.cpp) instead of this. Mirrors the per-host +// ll_get_static_plugin_init() hook above. +int ll_run_slplugin_host(U32 port, const std::string& daemon_rendezvous); + #if LL_DARWIN #include "slplugin-objc.h" @@ -147,6 +171,11 @@ int main(int argc, char **argv) // LLError::logToFile("slplugin.log"); } + // Non-empty only for a CEF daemon launch (parsed from argv below); empty for + // the generic host and on Windows (where the bootstrap entry handles daemon + // mode instead). + std::string daemon_rendezvous; + #if LL_WINDOWS if( strlen( lpCmdLine ) == 0 ) { @@ -175,6 +204,18 @@ int main(int argc, char **argv) LL_ERRS("slplugin") << "port number must be numeric" << LL_ENDL; } + // Optional "--daemon " tells a CEF host to run as the shared + // multi-tab daemon (on Windows this is handled by the bootstrap entry). The + // rendezvous path is taken as a single argument. + for (int i = 2; i + 1 < argc; ++i) + { + if (std::string(argv[i]) == "--daemon") + { + daemon_rendezvous = argv[i + 1]; + break; + } + } + // Catch signals that most kinds of crashes will generate, and exit cleanly so the system crash dialog isn't shown. signal(SIGILL, &crash_handler); // illegal instruction signal(SIGFPE, &crash_handler); // floating-point exception @@ -184,12 +225,30 @@ int main(int argc, char **argv) #endif # if LL_DARWIN signal(SIGEMT, &crash_handler); // emulate instruction executed +#endif //LL_DARWIN + // Hand off to the per-host entry (generic: single connection; CEF: persistent + // runtime, and the shared daemon when a rendezvous path was supplied). + int rc = ll_run_slplugin_host(port, daemon_rendezvous); + + ll_cleanup_apr(); + + return rc; +} + +int slplugin_run(U32 port) +{ +#if LL_DARWIN LLCocoaPlugin cocoa_interface; cocoa_interface.setupCocoa(); cocoa_interface.createAutoReleasePool(); #endif //LL_DARWIN + // If this is a dedicated single-plugin host (e.g. SLPluginCEF), register the + // statically-linked plugin entry point so LLPluginInstance::load() calls it + // directly instead of dlopen()ing a plugin library. + LLPluginInstance::setStaticInitFunction(ll_get_static_plugin_init()); + LLPluginProcessChild *plugin = new LLPluginProcessChild(); plugin->init(port); @@ -261,8 +320,5 @@ int main(int argc, char **argv) } delete plugin; - ll_cleanup_apr(); - - return 0; } diff --git a/indra/llplugin/slplugin/slplugin_daemon.cpp b/indra/llplugin/slplugin/slplugin_daemon.cpp new file mode 100644 index 0000000000..e8011a5e8b --- /dev/null +++ b/indra/llplugin/slplugin/slplugin_daemon.cpp @@ -0,0 +1,303 @@ +/** + * @file slplugin_daemon.cpp + * @brief Multi-connection host driver: one process serving N plugin tabs. + * + * The shared tab-manager daemon. Where slplugin_run() serves a single parent + * connection, this serves many: each parent that wants a tab connects to the + * daemon's control port and sends its own listen port as decimal text + '\n'; + * the daemon spawns an LLPluginProcessChild that connects back to that port (one + * tab). Because every tab lives in this one process, they transparently share a + * single CEF runtime (dullahan_runtime - see Phase 1), which is the whole point. + * + * The first tab's parent port is passed at launch so it is served without the + * control channel. The control port is written to the rendezvous file so later + * parents can discover a running daemon (the viewer-side discover-or-spawn that + * drives this is built separately). + * + * This driver is plugin-agnostic; the CEF host (SLPluginCEF) selects it via its + * --daemon argument. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llpluginprocesschild.h" +#include "llplugininstance.h" + +#include "llapr.h" +#include "lltimer.h" +#include "llstring.h" + +#include "apr_network_io.h" + +#if LL_DARWIN +#include "slplugin-objc.h" // LLCocoaPlugin: pump NSApp so macOS sees us alive +#endif + +#include +#include +#include + +// Idle this many seconds with zero live tabs before the daemon exits, so a brief +// gap between the last tab closing and the next opening keeps the warm runtime. +static const F64 DAEMON_IDLE_TIMEOUT = 10.0; + +// Hard ceiling on concurrent tabs in one daemon. The viewer is the primary cap +// (PluginInstancesTotal -> mMaxIntances keeps excess media PRIORITY_UNLOADED, so +// no media source -> no tab); this is only a self-protection backstop so a buggy +// or hostile parent cannot make one process spawn unbounded browsers. Set well +// above any realistic viewer cap. +static const size_t DAEMON_MAX_TABS = 64; + +namespace +{ + // Create a non-blocking TCP listen socket on the loopback for parents to + // register tabs on. Returns the socket (and its bound port) or nullptr. + apr_socket_t* createControlListener(U32& out_port) + { + out_port = 0; + apr_socket_t* sock = nullptr; + if (apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP, gAPRPoolp) != APR_SUCCESS) + { + return nullptr; + } + + apr_socket_opt_set(sock, APR_SO_REUSEADDR, 1); + + apr_sockaddr_t* addr = nullptr; + if (apr_sockaddr_info_get(&addr, "127.0.0.1", APR_INET, 0, 0, gAPRPoolp) != APR_SUCCESS || + apr_socket_bind(sock, addr) != APR_SUCCESS || + apr_socket_listen(sock, SOMAXCONN) != APR_SUCCESS) + { + apr_socket_close(sock); + return nullptr; + } + + apr_sockaddr_t* bound = nullptr; + if (apr_socket_addr_get(&bound, APR_LOCAL, sock) != APR_SUCCESS) + { + apr_socket_close(sock); + return nullptr; + } + out_port = bound->port; + + // Non-blocking so the accept poll below never stalls the tab loop. + apr_socket_opt_set(sock, APR_SO_NONBLOCK, 1); + apr_socket_timeout_set(sock, 0); + return sock; + } + + // Read the decimal listen-port a registering parent sent, spawn a tab for it. + void acceptRegistration(apr_socket_t* listener, std::vector& tabs) + { + apr_socket_t* incoming = nullptr; + if (apr_socket_accept(&incoming, listener, gAPRPoolp) != APR_SUCCESS || !incoming) + { + return; // APR_EAGAIN etc. - no pending registration + } + + // Self-protection backstop (see DAEMON_MAX_TABS): refuse to grow past the + // ceiling. The parent's connect-back never gets a tab, so it heartbeat- + // times-out and reports the media failed - which the viewer should have + // prevented via its own instance cap. Just drop the registration. + if (tabs.size() >= DAEMON_MAX_TABS) + { + LL_WARNS("slplugin") << "daemon: tab ceiling (" << DAEMON_MAX_TABS + << ") reached; refusing registration" << LL_ENDL; + apr_socket_close(incoming); + return; + } + + // Blocking read of the short port line (the registration is tiny). + // apr_socket_recv can return a partial TCP segment, so accumulate until the + // newline terminator (or the buffer fills / times out) before parsing - + // a trimmed partial port would connect the tab to the wrong local port. + apr_socket_timeout_set(incoming, 1000000); // 1s + std::string s; + char buf[32]; + while (s.find('\n') == std::string::npos && s.size() < sizeof(buf)) + { + apr_size_t len = sizeof(buf); + if (apr_socket_recv(incoming, buf, &len) != APR_SUCCESS || len == 0) + { + break; + } + s.append(buf, len); + } + if (s.find('\n') != std::string::npos) + { + LLStringUtil::trim(s); + U32 parent_port = 0; + if (!s.empty() && LLStringUtil::convertToU32(s, parent_port) && parent_port) + { + LLPluginProcessChild* tab = new LLPluginProcessChild(); + tab->setDaemonMode(true); + tab->init(parent_port); + tabs.push_back(tab); + LL_INFOS("slplugin") << "daemon: new tab connecting to parent port " << parent_port + << " (" << tabs.size() << " live)" << LL_ENDL; + } + } + apr_socket_close(incoming); + } +} + +// Defined in slplugin.cpp. +int slplugin_run(U32 port); + +// Defined per host (slplugin_cef.cpp for SLPluginCEF): the statically-linked +// plugin entry point. Each daemon tab must use it instead of dlopen()ing the +// plugin DLL, otherwise the tab loads a SECOND copy of the plugin (and its +// statically-linked dullahan) - a different dullahan_runtime than the host set +// the sandbox info on, so the tab runs unsandboxed via dullahan_host. +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init(); + +// Run the daemon: serve the first tab (first_port) plus any later tabs that +// register on the control port (written to rendezvous_path). Returns when no +// tab has been live for DAEMON_IDLE_TIMEOUT. +int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path_str) +{ + // Register the statically-linked plugin so every tab's LLPluginInstance::load + // calls it directly instead of dlopen()ing the plugin DLL. Without this the + // daemon tabs load a separate plugin+dullahan copy whose runtime never got + // the host's sandbox info (the cause of the daemon running unsandboxed). + // slplugin_run() does the same for the single-tab host. + LLPluginInstance::setStaticInitFunction(ll_get_static_plugin_init()); + + U32 control_port = 0; + apr_socket_t* control = createControlListener(control_port); + if (!control) + { + // No control channel: fall back to single-tab behaviour so the first + // parent still gets served rather than failing outright. + LL_WARNS("slplugin") << "daemon: control listener failed; serving a single tab" << LL_ENDL; + return slplugin_run(first_port); + } + + std::filesystem::path rendezvous_path = fsyspath(rendezvous_path_str); + std::filesystem::path rendezvous_lock_path = fsyspath(rendezvous_path_str + ".lock"); + + // Publish the control port for discover-or-spawn. Write then flush+close so a + // racing parent reads a complete value. + bool rendezvous_published = false; + { + llofstream rv(rendezvous_path, std::ios::trunc); + if (rv) + { + rv << control_port << std::endl; + rv.close(); + rendezvous_published = rv.good(); + } + } + if (!rendezvous_published) + { + // If the rendezvous never published, releasing the lock would let later + // parents spawn additional daemons instead of finding this one. Drop the + // lock and fall back to serving only the first tab. + LL_WARNS("slplugin") << "daemon: failed to publish rendezvous at " + << rendezvous_path << "; serving a single tab" << LL_ENDL; + LLFile::remove(rendezvous_lock_path); + apr_socket_close(control); + return slplugin_run(first_port); + } + // The rendezvous is now published, so release the spawn lock the launching + // parent took - other parents can stop waiting and register. + LLFile::remove(rendezvous_lock_path); + LL_INFOS("slplugin") << "daemon: control port " << control_port + << " -> " << rendezvous_path << LL_ENDL; + +#if LL_DARWIN + // Without a serviced Cocoa run loop the WindowServer flags this process as + // "Not Responding" (even while it works). Pump NSApp each iteration, like the + // single-tab slplugin_run() loop does. + LLCocoaPlugin cocoa_interface; + cocoa_interface.setupCocoa(); +#endif + + std::vector tabs; + { + LLPluginProcessChild* first = new LLPluginProcessChild(); + first->setDaemonMode(true); + first->init(first_port); + tabs.push_back(first); + } + + LLTimer idle_timer; + bool idle_running = false; + + while (true) + { +#if LL_DARWIN + cocoa_interface.createAutoReleasePool(); +#endif + acceptRegistration(control, tabs); + +#if LL_DARWIN + cocoa_interface.processEvents(); +#endif + + // Service every tab, reaping finished ones. + for (std::vector::iterator it = tabs.begin(); it != tabs.end();) + { + LLPluginProcessChild* tab = *it; + tab->idle(); + tab->pump(); + if (tab->isDone()) + { + delete tab; + it = tabs.erase(it); + LL_INFOS("slplugin") << "daemon: tab closed (" << tabs.size() << " live)" << LL_ENDL; + } + else + { + ++it; + } + } + + if (tabs.empty()) + { + if (!idle_running) + { + idle_timer.reset(); + idle_running = true; + } + else if (idle_timer.getElapsedTimeF64() > DAEMON_IDLE_TIMEOUT) + { + break; + } + } + else + { + idle_running = false; + } + +#if LL_DARWIN + cocoa_interface.deleteAutoReleasePool(); +#endif + + ms_sleep(10); + } + + apr_socket_close(control); + LLFile::remove(rendezvous_path); + LL_INFOS("slplugin") << "daemon: idle, exiting" << LL_ENDL; + return 0; +} diff --git a/indra/llplugin/slplugin/slplugin_info.plist b/indra/llplugin/slplugin/slplugin_info.plist index c4597380e0..bf9cd54b4b 100644 --- a/indra/llplugin/slplugin/slplugin_info.plist +++ b/indra/llplugin/slplugin/slplugin_info.plist @@ -1,12 +1,28 @@ - + CFBundleDevelopmentRegion English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CSResourcesFileMapped + + LSFileQuarantineEnabled + LSUIElement - 1 + diff --git a/indra/llplugin/slplugin/slplugin_static.cpp b/indra/llplugin/slplugin/slplugin_static.cpp new file mode 100644 index 0000000000..aeae3df25a --- /dev/null +++ b/indra/llplugin/slplugin/slplugin_static.cpp @@ -0,0 +1,59 @@ +/** + * @file slplugin_static.cpp + * @brief Static-plugin hook for a dedicated single-plugin host executable. + * + * Each media plugin is now its own executable (e.g. media_plugin_libvlc) that + * statically links exactly one plugin's object code. This file registers that + * plugin's entry point with LLPluginInstance so load() calls it directly - there + * is no dlopen path any more. The CEF host links its own variant + * (slplugin_cef.cpp) instead, which adds the shared-daemon mode and the CEF + * runtime lifecycle. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llplugininstance.h" + +#include + +// Exported by media_plugin_base, which is statically linked into this host along +// with exactly one media plugin's object code. +extern "C" int LLPluginInitEntryPoint(LLPluginInstance::sendMessageFunction host_send_func, + void *host_user_data, + LLPluginInstance::sendMessageFunction *plugin_send_func, + void **plugin_user_data); + +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init() +{ + return &LLPluginInitEntryPoint; +} + +// Defined in slplugin.cpp. +int slplugin_run(U32 port); + +// A plain single-plugin host has no daemon mode: ignore any rendezvous path and +// serve the single connection. (Only the CEF host overrides this; see +// slplugin_cef.cpp.) +int ll_run_slplugin_host(U32 port, const std::string& /*daemon_rendezvous*/) +{ + return slplugin_run(port); +} diff --git a/indra/llprecompiled/CMakeLists.txt b/indra/llprecompiled/CMakeLists.txt index 813b1afa6d..15164d572e 100644 --- a/indra/llprecompiled/CMakeLists.txt +++ b/indra/llprecompiled/CMakeLists.txt @@ -15,6 +15,7 @@ target_link_libraries(llprecompiled ll::oslibraries ll::tracy ll::sse2neon + Threads::Threads ) target_precompile_headers(llprecompiled PRIVATE @@ -45,6 +46,7 @@ target_link_libraries(llprecompiled_exe ll::oslibraries ll::tracy ll::sse2neon + Threads::Threads ) target_precompile_headers(llprecompiled_exe PRIVATE diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index b06bf7ccf2..ce4b5cf15b 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -245,6 +245,14 @@ PFNWGLBLITCONTEXTFRAMEBUFFERAMDPROC wglBlitContextFramebufferAMD = n // WGL_EXT_swap_control PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = nullptr; PFNWGLGETSWAPINTERVALEXTPROC wglGetSwapIntervalEXT = nullptr; + +// WGL_NV_DX_interop / interop2 +PFNWGLDXOPENDEVICENVPROC wglDXOpenDeviceNV = nullptr; +PFNWGLDXCLOSEDEVICENVPROC wglDXCloseDeviceNV = nullptr; +PFNWGLDXREGISTEROBJECTNVPROC wglDXRegisterObjectNV = nullptr; +PFNWGLDXUNREGISTEROBJECTNVPROC wglDXUnregisterObjectNV = nullptr; +PFNWGLDXLOCKOBJECTSNVPROC wglDXLockObjectsNV = nullptr; +PFNWGLDXUNLOCKOBJECTSNVPROC wglDXUnlockObjectsNV = nullptr; #endif #if LL_LINUX && LL_X11 && !LL_MESA_HEADLESS @@ -258,6 +266,13 @@ PFNGLXQUERYRENDERERSTRINGMESAPROC glXQueryRendererStringMESA = nullptr; #if LL_LINUX && LL_WAYLAND &&!LL_MESA_HEADLESS // EGL_VERSION_1_0 PFNEGLQUERYSTRINGPROC eglQueryString = nullptr; + +// EGL_KHR_image +PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = nullptr; +PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = nullptr; + +// GL_OES_EGL_image +PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = nullptr; #endif // GL_VERSION_1_0 @@ -1037,6 +1052,17 @@ void LLGLManager::initWGL() wglGetSwapIntervalEXT = (PFNWGLGETSWAPINTERVALEXTPROC)LL_GET_PROC_ADDRESS("wglGetSwapIntervalEXT"); } + // WGL_NV_DX_interop2 (D3D<->GL sharing for zero-copy CEF media textures) + if (mGLExtensions.contains("WGL_NV_DX_interop2") || mGLExtensions.contains("WGL_NV_DX_interop")) + { + wglDXOpenDeviceNV = (PFNWGLDXOPENDEVICENVPROC)LL_GET_PROC_ADDRESS("wglDXOpenDeviceNV"); + wglDXCloseDeviceNV = (PFNWGLDXCLOSEDEVICENVPROC)LL_GET_PROC_ADDRESS("wglDXCloseDeviceNV"); + wglDXRegisterObjectNV = (PFNWGLDXREGISTEROBJECTNVPROC)LL_GET_PROC_ADDRESS("wglDXRegisterObjectNV"); + wglDXUnregisterObjectNV = (PFNWGLDXUNREGISTEROBJECTNVPROC)LL_GET_PROC_ADDRESS("wglDXUnregisterObjectNV"); + wglDXLockObjectsNV = (PFNWGLDXLOCKOBJECTSNVPROC)LL_GET_PROC_ADDRESS("wglDXLockObjectsNV"); + wglDXUnlockObjectsNV = (PFNWGLDXUNLOCKOBJECTSNVPROC)LL_GET_PROC_ADDRESS("wglDXUnlockObjectsNV"); + } + if(!mGLExtensions.contains("WGL_ARB_pbuffer")) { LL_WARNS("RenderInit") << "No ARB WGL PBuffer extensions" << LL_ENDL; @@ -1077,10 +1103,26 @@ void LLGLManager::initEGL() reloadExtensionsString(); // EGL_VERSION_1_0 - eglQueryString = (PFNEGLQUERYSTRINGPROC)LL_GET_PROC_ADDRESS("eglQueryString"); + eglQueryString = (PFNEGLQUERYSTRINGPROC)SDL_EGL_GetProcAddress("eglQueryString"); + + // EGL_KHR_image + eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)SDL_EGL_GetProcAddress("eglCreateImageKHR"); + eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)SDL_EGL_GetProcAddress("eglDestroyImageKHR"); - LL_INFOS("RenderInit") << "EGL_VENDOR " << ll_safe_string((const char *)eglQueryString(SDL_EGL_GetCurrentDisplay(), EGL_VENDOR)) << LL_ENDL; - LL_INFOS("RenderInit") << "EGL_VERSION " << ll_safe_string((const char *)eglQueryString(SDL_EGL_GetCurrentDisplay(), EGL_VERSION)) << LL_ENDL; + // GL_OES_EGL_image + glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)SDL_EGL_GetProcAddress("glEGLImageTargetTexture2DOES"); + + // SDL_EGL_GetProcAddress can return null for core EGL symbols (driver/version + // dependent), so guard before calling through the pointer. + if (eglQueryString) + { + LL_INFOS("RenderInit") << "EGL_VENDOR " << ll_safe_string((const char *)eglQueryString(SDL_EGL_GetCurrentDisplay(), EGL_VENDOR)) << LL_ENDL; + LL_INFOS("RenderInit") << "EGL_VERSION " << ll_safe_string((const char *)eglQueryString(SDL_EGL_GetCurrentDisplay(), EGL_VERSION)) << LL_ENDL; + } + else + { + LL_WARNS("RenderInit") << "eglQueryString unavailable; skipping EGL vendor/version log" << LL_ENDL; + } #endif } @@ -1554,7 +1596,7 @@ void LLGLManager::reloadExtensionsString() SDL_EGLDisplay egl_display = SDL_EGL_GetCurrentDisplay(); if (egl_display) { - PFNEGLQUERYSTRINGPROC lleglQueryString = (PFNEGLQUERYSTRINGPROC)LL_GET_PROC_ADDRESS("eglQueryString"); + PFNEGLQUERYSTRINGPROC lleglQueryString = (PFNEGLQUERYSTRINGPROC)SDL_EGL_GetProcAddress("eglQueryString"); if (lleglQueryString) { std::string egl_exts = ll_safe_string((const char*)lleglQueryString((EGLDisplay)egl_display, EGL_EXTENSIONS)); diff --git a/indra/llrender/llglheaders.h b/indra/llrender/llglheaders.h index 0b0c473631..a16cc43c8e 100644 --- a/indra/llrender/llglheaders.h +++ b/indra/llrender/llglheaders.h @@ -59,6 +59,9 @@ #if LL_LINUX && LL_WAYLAND && !LL_MESA_HEADLESS #define EGL_EGL_PROTOTYPES 0 #include +#include +#include +#include // glEGLImageTargetTexture2DOES #endif // GL_NVX_gpu_memory_info constants @@ -152,6 +155,14 @@ extern PFNWGLGETPIXELFORMATATTRIBIVARBPROC wglGetPixelFormatAttribivARB; extern PFNWGLGETPIXELFORMATATTRIBFVARBPROC wglGetPixelFormatAttribfvARB; extern PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB; +// WGL_NV_DX_interop / interop2 (D3D<->GL sharing; used for zero-copy CEF media) +extern PFNWGLDXOPENDEVICENVPROC wglDXOpenDeviceNV; +extern PFNWGLDXCLOSEDEVICENVPROC wglDXCloseDeviceNV; +extern PFNWGLDXREGISTEROBJECTNVPROC wglDXRegisterObjectNV; +extern PFNWGLDXUNREGISTEROBJECTNVPROC wglDXUnregisterObjectNV; +extern PFNWGLDXLOCKOBJECTSNVPROC wglDXLockObjectsNV; +extern PFNWGLDXUNLOCKOBJECTSNVPROC wglDXUnlockObjectsNV; + #endif // LL_WINDOWS #if LL_LINUX && LL_X11 && !LL_MESA_HEADLESS @@ -163,7 +174,15 @@ extern PFNGLXQUERYRENDERERSTRINGMESAPROC glXQueryRendererStringMESA; #endif #if LL_LINUX && LL_WAYLAND &&!LL_MESA_HEADLESS +// EGL_VERSION_1_0 extern PFNEGLQUERYSTRINGPROC eglQueryString; + +// EGL_KHR_image +extern PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR; +extern PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR; + +// GL_OES_EGL_image +extern PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES; #endif // We get all functions via getProcAddress when using SDL diff --git a/indra/llwindow/lldxhardware.cpp b/indra/llwindow/lldxhardware.cpp index baf8d19592..4373e97129 100644 --- a/indra/llwindow/lldxhardware.cpp +++ b/indra/llwindow/lldxhardware.cpp @@ -37,6 +37,8 @@ #include #include #include +#include // shared D3D11 <-> GL interop device +#pragma comment(lib, "d3d11") #include @@ -596,4 +598,84 @@ void LLDXHardware::updateVRAMBudgetFromDXGI() } } +// One D3D11 device + one GL interop device for the whole process. CEF zero-copy +// media surfaces register their textures against these instead of each spinning +// up a private D3D device. Requires the GL context current and the WGL +// extensions loaded (LLGLManager::initWGL). +bool LLDXHardware::initGLDXInterop() +{ + if (mGLDXInteropDevice) + { + return true; // already up + } + // Require every WGL DX interop entry point the accelerated-paint path uses, not + // just wglDXOpenDeviceNV. Otherwise hasGLDXInterop() could report success while + // (e.g.) wglDXUnlockObjectsNV is missing, and the consumer would fail mid-blit + // instead of cleanly disabling the path up front. + if (!wglDXOpenDeviceNV || !wglDXCloseDeviceNV || !wglDXRegisterObjectNV || + !wglDXUnregisterObjectNV || !wglDXLockObjectsNV || !wglDXUnlockObjectsNV) + { + LL_INFOS("RenderInit") << "WGL_NV_DX_interop2 not fully available; no D3D/GL interop" << LL_ENDL; + return false; + } + + ID3D11Device* device = nullptr; + ID3D11DeviceContext* context = nullptr; + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, + nullptr, 0, D3D11_SDK_VERSION, &device, nullptr, &context); + if (FAILED(hr) || !device) + { + LL_WARNS("RenderInit") << "D3D11CreateDevice (GL interop) failed: 0x" << std::hex << hr << std::dec << LL_ENDL; + if (context) context->Release(); + if (device) device->Release(); + return false; + } + + // ID3D11Device1 is needed for OpenSharedResource1 (NT-handle shared textures). + ID3D11Device1* device1 = nullptr; + device->QueryInterface(__uuidof(ID3D11Device1), (void**)&device1); + device->Release(); + if (!device1) + { + LL_WARNS("RenderInit") << "ID3D11Device1 unavailable; no D3D/GL interop" << LL_ENDL; + if (context) context->Release(); + return false; + } + + HANDLE gl_dx = wglDXOpenDeviceNV(device1); + if (!gl_dx) + { + LL_WARNS("RenderInit") << "wglDXOpenDeviceNV failed; no D3D/GL interop" << LL_ENDL; + device1->Release(); + if (context) context->Release(); + return false; + } + + mD3DDevice = device1; + mD3DContext = context; + mGLDXInteropDevice = gl_dx; + LL_INFOS("RenderInit") << "D3D11 <-> GL interop device ready" << LL_ENDL; + return true; +} + +void LLDXHardware::cleanupGLDXInterop() +{ + if (mGLDXInteropDevice && wglDXCloseDeviceNV) + { + wglDXCloseDeviceNV((HANDLE)mGLDXInteropDevice); + } + mGLDXInteropDevice = nullptr; + if (mD3DContext) + { + ((ID3D11DeviceContext*)mD3DContext)->Release(); + mD3DContext = nullptr; + } + if (mD3DDevice) + { + ((ID3D11Device1*)mD3DDevice)->Release(); + mD3DDevice = nullptr; + } +} + #endif diff --git a/indra/llwindow/lldxhardware.h b/indra/llwindow/lldxhardware.h index 5d7a1955a3..e052396647 100644 --- a/indra/llwindow/lldxhardware.h +++ b/indra/llwindow/lldxhardware.h @@ -57,6 +57,24 @@ class LLDXHardware // matters mainly for Intel iGPUs. Must run with GL initialized. Shared // by LLWindowWin32's window thread (checkDXMem) and the SDL backend. static void updateVRAMBudgetFromDXGI(); + + // --- Shared D3D11 <-> OpenGL interop (WGL_NV_DX_interop2) --- + // One D3D11 device + one wglDXOpenDeviceNV interop device for the whole + // process, brought up once after the GL context and WGL extensions exist. + // The zero-copy CEF media surfaces share these instead of each creating their + // own. Must be called with the GL context current. Returns true if available. + // No-op / false on platforms without WGL_NV_DX_interop2. + bool initGLDXInterop(); + void cleanupGLDXInterop(); + bool hasGLDXInterop() const { return mGLDXInteropDevice != nullptr; } + void* getD3DDevice() const { return mD3DDevice; } // ID3D11Device1* + void* getD3DContext() const { return mD3DContext; } // ID3D11DeviceContext* + void* getGLDXInteropDevice() const { return mGLDXInteropDevice; } // wglDXOpenDeviceNV handle + +private: + void* mD3DDevice = nullptr; + void* mD3DContext = nullptr; + void* mGLDXInteropDevice = nullptr; }; extern LLDXHardware gDXHardware; diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h index e4cad9370e..d512c130f4 100644 --- a/indra/llwindow/llwindow.h +++ b/indra/llwindow/llwindow.h @@ -207,6 +207,12 @@ class LLWindow // return a platform-specific window reference (HWND on Windows, WindowRef on the Mac, Gtk window on Linux) virtual void *getPlatformWindow() = 0; + // Lowercase name of the display server / windowing backend in use + // ("wayland" or "x11" on Linux/SDL). Empty when not applicable or unknown. + // Lets out-of-process children (e.g. the CEF media plugin) pin their backend + // to the viewer's instead of guessing from their own environment. + virtual std::string getDisplayServer() const { return {}; } + // control platform's Language Text Input mechanisms. virtual void allowLanguageTextInput(LLPreeditor *preeditor, bool b) {} virtual void setLanguageTextInput( const LLCoordGL & pos ) {}; diff --git a/indra/llwindow/llwindowsdl.cpp b/indra/llwindow/llwindowsdl.cpp index bc43cd8bc0..d904af8bc8 100644 --- a/indra/llwindow/llwindowsdl.cpp +++ b/indra/llwindow/llwindowsdl.cpp @@ -499,11 +499,11 @@ bool LLWindowSDL::createContext(int x, int y, int width, int height, int bits, b // NOTE: the Wayland init path used to unsetenv("DISPLAY") here to // coax dullahan/CEF onto the native Wayland path. That global env // mutation affected every other subprocess the viewer spawns, so - // we dropped it. Routing CEF to Wayland needs a per-spawn fix on - // the dullahan side (e.g. scrubbing DISPLAY only when launching - // dullahan_host, or passing OZONE_PLATFORM=wayland via the launch - // env). Until that lands, CEF will continue to use XWayland when - // DISPLAY is set, which is the SDL2-era behaviour. + // we dropped it. Instead, the viewer now reports its backend to the + // CEF media plugin via getDisplayServer() -> + // LLPluginClassMedia::setDisplayServer(), which forwards it to + // dullahan as the forced Ozone platform - routing CEF onto native + // Wayland without mutating any shared environment. } #endif @@ -3936,6 +3936,17 @@ void* LLWindowSDL::getPlatformWindow() return ret; } +std::string LLWindowSDL::getDisplayServer() const +{ + // mServerProtocol is latched in createContext() from SDL_GetCurrentVideoDriver(). + switch (mServerProtocol) + { + case Wayland: return "wayland"; + case X11: return "x11"; + default: return {}; + } +} + #if LL_WINDOWS void LLWindowSDL::installWin32Subclass() { diff --git a/indra/llwindow/llwindowsdl.h b/indra/llwindow/llwindowsdl.h index 43a0daf4b5..0de02c54c5 100644 --- a/indra/llwindow/llwindowsdl.h +++ b/indra/llwindow/llwindowsdl.h @@ -188,6 +188,9 @@ class LLWindowSDL final : public LLWindow void *getPlatformWindow() override; + // "wayland" / "x11" / "" - reports the SDL video driver this window is on. + std::string getDisplayServer() const override; + void bringToFront() override; void setLanguageTextInput(const LLCoordGL& pos) override; diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 4abff070dd..1512d831ba 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -1025,6 +1025,9 @@ void LLWindowWin32::close() gGLManager.shutdownGL(); } + // Tear down the shared D3D11<->GL interop device while its context is current. + gDXHardware.cleanupGLDXInterop(); + LL_DEBUGS("Window") << "Releasing Context" << LL_ENDL; if (mhRC) { @@ -1210,6 +1213,9 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo mRefreshRate = current_refresh; gGLManager.shutdownGL(); + // Tear down the shared D3D11<->GL interop device (recreated below for the new + // context) while the old context is still current. + gDXHardware.cleanupGLDXInterop(); //destroy gl context if (mhRC) { @@ -1719,6 +1725,13 @@ const S32 max_format = (S32)num_formats - 1; return false; } + // Bring up the process-shared D3D11 <-> GL interop device once, here on the + // main thread with the render context current and the WGL extensions loaded. + // Zero-copy CEF media surfaces share it instead of each creating a private + // D3D device. Best-effort - failure just means accelerated paint falls back + // to the CPU path. + gDXHardware.initGLDXInterop(); + // Setup Tracy gpu context { LL_PROFILER_GPU_CONTEXT; diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index 5033c1609c..8864795625 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -49,33 +49,109 @@ endif () list(APPEND media_plugin_cef_SOURCE_FILES ${media_plugin_cef_HEADER_FILES}) -add_library(media_plugin_cef - SHARED - ${media_plugin_cef_SOURCE_FILES} - ) +# Compile the plugin sources once into an object library, statically linked into +# the media_plugin_cef host executable below. Static linking avoids dlopen of +# libcef (which exhausts the static TLS block on Linux) and is the basis for the +# Windows-sandbox single-image host. +add_library(media_plugin_cef_objs OBJECT ${media_plugin_cef_SOURCE_FILES}) +target_link_libraries(media_plugin_cef_objs PUBLIC + media_plugin_base + dullahan + ll::cef + ll::glib_headers +) + +### media_plugin_cef - the CEF media plugin's dedicated host executable. It +### statically links the plugin object code plus the slplugin host driver and +### slplugin_cef.cpp (which registers the linked entry point and adds the shared +### daemon mode + CEF runtime lifecycle). There is no dlopen'd plugin library any +### more. On Windows it is a DLL exporting RunWinMain, loaded by CEF's bootstrap +### executable (shipped renamed to media_plugin_cef.exe) which sets up the +### sandbox; elsewhere it is a plain executable. +if (WINDOWS) + add_library(media_plugin_cef MODULE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_daemon.cpp + slplugin_cef.cpp + slplugin_cef_bootstrap.cpp + $ + ) +else () + add_executable(media_plugin_cef + MACOSX_BUNDLE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_daemon.cpp + slplugin_cef.cpp + $ + ) + if (DARWIN) + target_sources(media_plugin_cef PRIVATE ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin-objc.mm) + set_source_files_properties(${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin-objc.mm + PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE) + + set_target_properties(media_plugin_cef PROPERTIES + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/SLPluginCEF-Info.plist.in + MACOSX_BUNDLE_GUI_IDENTIFIER "org.alchemyviewer.viewer.mediaplugincef" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "org.alchemyviewer.viewer.mediaplugincef" + ) + endif () +endif () target_link_libraries(media_plugin_cef media_plugin_base dullahan ll::cef ll::glib_headers + llplugin + llmessage + llcommon + ll::pluginlibraries ) +if (WINDOWS) + # zero-copy paint producer: D3D11 keyed-mutex shared texture + target_link_libraries(media_plugin_cef d3d11 dxgi) +endif () + add_dependencies(media_plugin_cef dullahan_host) if (WINDOWS) + # media_plugin_cef.dll lives next to libcef.dll and the CEF bootstrap (deployed + # below) so the CEF runtime resolves from its own dir. set_target_properties(media_plugin_cef PROPERTIES - EXCLUDE_FROM_DEFAULT_BUILD_DEBUG ON RUNTIME_OUTPUT_DIRECTORY "${VIEWER_STAGING_DIR}/llplugin" + LIBRARY_OUTPUT_DIRECTORY "${VIEWER_STAGING_DIR}/llplugin" ) - target_link_options(media_plugin_cef PRIVATE /MANIFEST:NO /IGNORE:4099) + target_link_options(media_plugin_cef PRIVATE /IGNORE:4099) - # Copy plugin files to packaging directory set(CEF_BIN_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/cef-bin") + set(CEF_BIN_BINARY_DIR "$,${CEF_BIN_DIR}/Debug,${CEF_BIN_DIR}/Release>") + + # Ship CEF's bootstrap.exe renamed to media_plugin_cef.exe; matching base name + # makes it load media_plugin_cef.dll and call our exported RunWinMain with the + # sandbox info. The bootstrap.exe is part of the CEF Release payload copied below. + add_custom_command(TARGET media_plugin_cef POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CEF_BIN_BINARY_DIR}/bootstrap.exe" "${VIEWER_STAGING_DIR}/llplugin/media_plugin_cef.exe" + ) + + # Copy the CEF runtime + resources to the packaging directory. + set(CEF_BIN_FILES + "${CEF_BIN_BINARY_DIR}/chrome_elf.dll" + "${CEF_BIN_BINARY_DIR}/d3dcompiler_47.dll" + "${CEF_BIN_BINARY_DIR}/dxcompiler.dll" + "${CEF_BIN_BINARY_DIR}/dxil.dll" + "${CEF_BIN_BINARY_DIR}/libEGL.dll" + "${CEF_BIN_BINARY_DIR}/libGLESv2.dll" + "${CEF_BIN_BINARY_DIR}/v8_context_snapshot.bin" + "${CEF_BIN_BINARY_DIR}/vk_swiftshader.dll" + "${CEF_BIN_BINARY_DIR}/vk_swiftshader_icd.json" + "${CEF_BIN_BINARY_DIR}/vulkan-1.dll" + ) add_custom_command( TARGET media_plugin_cef POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different "${CEF_BIN_DIR}/Release" "${VIEWER_STAGING_DIR}/llplugin" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CEF_BIN_FILES} "${VIEWER_STAGING_DIR}/llplugin" + COMMAND_EXPAND_LISTS ) set(CEF_RESOURCE_FILES @@ -105,23 +181,18 @@ elseif (DARWIN) # macOS has extra helper bundles add_dependencies(media_plugin_cef dullahan_host_alerts dullahan_host_gpu dullahan_host_plugin dullahan_host_renderer) + find_library(IOSURFACE_FRAMEWORK IOSurface) + find_library(COREGRAPHICS_FRAMEWORK CoreGraphics) find_library(CORESERVICES_LIBRARY CoreServices) find_library(AUDIOUNIT_LIBRARY AudioUnit) target_link_libraries(media_plugin_cef + ${IOSURFACE_FRAMEWORK} + ${COREGRAPHICS_FRAMEWORK} ${CORESERVICES_LIBRARY} # for Component Manager calls ${AUDIOUNIT_LIBRARY} # for AudioUnit calls ) - # Don't prepend 'lib' to the executable name, and don't embed a full path in the library's install name - set_target_properties( - media_plugin_cef - PROPERTIES - PREFIX "" - BUILD_WITH_INSTALL_RPATH 1 - INSTALL_RPATH "@executable_path/../Frameworks" - ) - elseif (LINUX) target_link_options(media_plugin_cef PRIVATE "LINKER:--build-id" "LINKER:-rpath,'$ORIGIN:$ORIGIN/../../lib'") endif () diff --git a/indra/media_plugins/cef/SLPluginCEF-Info.plist.in b/indra/media_plugins/cef/SLPluginCEF-Info.plist.in new file mode 100644 index 0000000000..c86bdf00a9 --- /dev/null +++ b/indra/media_plugins/cef/SLPluginCEF-Info.plist.in @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CSResourcesFileMapped + + LSFileQuarantineEnabled + + LSUIElement + + + diff --git a/indra/media_plugins/cef/media_plugin_cef.cpp b/indra/media_plugins/cef/media_plugin_cef.cpp index 49c9569d53..604d09f7f7 100644 --- a/indra/media_plugins/cef/media_plugin_cef.cpp +++ b/indra/media_plugins/cef/media_plugin_cef.cpp @@ -45,8 +45,185 @@ #include #endif +#if LL_DARWIN +#include // accelerated paint hands the viewer an IOSurface +#include // ...shared cross-process via a mach send right +#include // ...rendezvous'd through the bootstrap server +#endif + +#if LL_LINUX +#include +#include +#include +#include // accelerated paint hands the viewer the dma-buf fds... +#include // ...over an AF_UNIX datagram, as SCM_RIGHTS ancillary data +#endif + #include "dullahan.h" +#if LL_WINDOWS +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// Producer half of zero-copy paint. Owns a D3D11 device and ONE persistent +// keyed-mutex shared texture. Each accelerated-paint frame it copies CEF's +// pooled (NT-handle) shared texture into the stable one under the mutex. The +// stable texture's NT handle is duplicated to the viewer only when it is +// (re)created (first frame / size change) - no per-frame DuplicateHandle. The +// viewer opens it once and samples under the same single-key (mutual-exclusion) +// mutex, which also provides the cross-process/cross-device GPU sync. +class CefAccelProducer +{ +public: + CefAccelProducer() = default; + ~CefAccelProducer() { shutdown(); } + + bool init() + { + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, + nullptr, 0, D3D11_SDK_VERSION, &mDevice, nullptr, &mContext); + if (FAILED(hr) || !mDevice) + { + return false; + } + mDevice->QueryInterface(__uuidof(ID3D11Device1), (void**)&mDevice1); + return mDevice1 != nullptr; + } + + void shutdown() + { + releaseStable(); + if (mDevice1) { mDevice1->Release(); mDevice1 = nullptr; } + if (mContext) { mContext->Release(); mContext = nullptr; } + if (mDevice) { mDevice->Release(); mDevice = nullptr; } + } + + // Copy this frame's CEF shared texture (cef_handle) into the stable texture. + // If the stable texture was (re)created, out_handle receives a fresh NT handle + // duplicated into viewer_process (to send once) and out_recreated is true. + bool produce(void* cef_handle, void* viewer_process, + HANDLE& out_handle, bool& out_recreated, int& out_w, int& out_h, int& out_fmt) + { + out_recreated = false; + out_handle = nullptr; + if (!mDevice1 || !cef_handle) + { + return false; + } + + ID3D11Texture2D* cef = nullptr; + if (FAILED(mDevice1->OpenSharedResource1((HANDLE)cef_handle, __uuidof(ID3D11Texture2D), (void**)&cef)) || !cef) + { + return false; + } + + D3D11_TEXTURE2D_DESC cd = {}; + cef->GetDesc(&cd); + out_w = (int)cd.Width; + out_h = (int)cd.Height; + out_fmt = (int)cd.Format; + + if (!mStable || mW != cd.Width || mH != cd.Height || mFmt != cd.Format) + { + if (!createStable(cd, viewer_process, out_handle)) + { + cef->Release(); + return false; + } + out_recreated = true; + } + + // Only report success when the frame actually lands in the stable texture. + // Returning true on a missed copy (no mutex / acquire timeout) would emit + // an accelerated_paint message for a texture the consumer never received - + // worst case a just-(re)created texture that was never populated. + if (!mMutex) + { + cef->Release(); + return false; + } + if (FAILED(mMutex->AcquireSync(0, 1000))) + { + cef->Release(); + return false; + } + mContext->CopyResource(mStable, cef); + mContext->Flush(); + mMutex->ReleaseSync(0); + cef->Release(); + return true; + } + +private: + bool createStable(const D3D11_TEXTURE2D_DESC& cd, void* viewer_process, HANDLE& out_handle) + { + releaseStable(); + + D3D11_TEXTURE2D_DESC d = {}; + d.Width = cd.Width; + d.Height = cd.Height; + d.MipLevels = 1; + d.ArraySize = 1; + d.Format = cd.Format; + d.SampleDesc.Count = 1; + d.Usage = D3D11_USAGE_DEFAULT; + d.BindFlags = D3D11_BIND_SHADER_RESOURCE; + d.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; + if (FAILED(mDevice->CreateTexture2D(&d, nullptr, &mStable)) || !mStable) + { + return false; + } + + mStable->QueryInterface(__uuidof(IDXGIKeyedMutex), (void**)&mMutex); + + IDXGIResource1* res = nullptr; + if (FAILED(mStable->QueryInterface(__uuidof(IDXGIResource1), (void**)&res)) || !res) + { + return false; + } + HANDLE local = nullptr; + HRESULT hr = res->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, nullptr, &local); + res->Release(); + if (FAILED(hr) || !local) + { + return false; + } + + // Share the stable texture to the viewer; we keep the texture alive so the + // local share handle can be closed once duplicated. + BOOL ok = DuplicateHandle(GetCurrentProcess(), local, (HANDLE)viewer_process, + &out_handle, 0, FALSE, DUPLICATE_SAME_ACCESS); + CloseHandle(local); + if (!ok || !out_handle) + { + return false; + } + + mW = cd.Width; + mH = cd.Height; + mFmt = cd.Format; + return true; + } + + void releaseStable() + { + if (mMutex) { mMutex->Release(); mMutex = nullptr; } + if (mStable) { mStable->Release(); mStable = nullptr; } + mW = 0; mH = 0; mFmt = DXGI_FORMAT_UNKNOWN; + } + + ID3D11Device* mDevice = nullptr; + ID3D11Device1* mDevice1 = nullptr; + ID3D11DeviceContext* mContext = nullptr; + ID3D11Texture2D* mStable = nullptr; + IDXGIKeyedMutex* mMutex = nullptr; + UINT mW = 0, mH = 0; + DXGI_FORMAT mFmt = DXGI_FORMAT_UNKNOWN; +}; +#endif // LL_WINDOWS + //////////////////////////////////////////////////////////////////////////////// // class MediaPluginCEF : @@ -63,6 +240,11 @@ class MediaPluginCEF : bool init(); void onPageChangedCallback(const unsigned char* pixels, int x, int y, const int width, const int height); + void onAcceleratedPaintCallback(void* native_handle, int format, int width, int height); +#if LL_LINUX + void onAcceleratedPaintDmabufCallback(const dullahan::dmabuf_plane* planes, int plane_count, + int format, int width, int height, unsigned long long modifier); +#endif void onCustomSchemeURLCallback(std::string url, bool user_gesture, bool is_redirect); void onConsoleMessageCallback(std::string message, std::string source, int line); void onStatusMessageCallback(std::string value); @@ -103,6 +285,7 @@ class MediaPluginCEF : bool mDisableWebSecurity; bool mFileAccessFromFileUrls; std::string mUserAgentSubtring; + std::string mDisplayServer; // viewer's windowing backend ("wayland"/"x11"); Linux Ozone platform std::string mAuthUsername; std::string mAuthPassword; bool mAuthOK; @@ -120,6 +303,18 @@ class MediaPluginCEF : VolumeCatcher mVolumeCatcher; F32 mCurVolume; dullahan* mCEFLib; + + // accelerated (zero-copy) paint: deliver GPU shared-texture handles to the + // viewer instead of CPU pixels. mHostPid is the viewer process, mViewerProcess + // its opened handle (with PROCESS_DUP_HANDLE) used to DuplicateHandle the + // shared texture across the boundary. + bool mUseAcceleratedPaint; + int mHostPid; + int mAccelId; // per-media id echoed in each macOS surface mach message + void* mViewerProcess; +#if LL_WINDOWS + CefAccelProducer* mAccelProducer; +#endif }; //////////////////////////////////////////////////////////////////////////////// @@ -140,6 +335,13 @@ MediaPluginBase(host_send_func, host_user_data) mProxyHost = ""; mProxyPort = 0; mDisableGPU = false; + mUseAcceleratedPaint = false; + mHostPid = 0; + mAccelId = 0; + mViewerProcess = nullptr; +#if LL_WINDOWS + mAccelProducer = nullptr; +#endif mUseMockKeyChain = true; mDisableWebSecurity = false; mFileAccessFromFileUrls = false; @@ -169,6 +371,19 @@ MediaPluginBase(host_send_func, host_user_data) MediaPluginCEF::~MediaPluginCEF() { mCEFLib->shutdown(); +#if LL_WINDOWS + if (mAccelProducer) + { + mAccelProducer->shutdown(); + delete mAccelProducer; + mAccelProducer = nullptr; + } + if (mViewerProcess) + { + CloseHandle((HANDLE)mViewerProcess); + mViewerProcess = nullptr; + } +#endif } //////////////////////////////////////////////////////////////////////////////// @@ -205,6 +420,322 @@ void MediaPluginCEF::onPageChangedCallback(const unsigned char* pixels, int x, i } } +//////////////////////////////////////////////////////////////////////////////// +// Zero-copy paint: CEF handed us a GPU shared-texture handle (valid in THIS +// process). Duplicate it into the viewer process and send the viewer the +#if LL_DARWIN +namespace +{ + // Sends a CEF accelerated-paint IOSurface to the viewer over a mach channel. + // CEF's IOSurface is shared via mach ports (not a global id), so the only way + // to hand it across the process boundary is a mach send right - which can't go + // through the socket/LLSD channel. We rendezvous with the viewer's receive + // port through the bootstrap server using a name derived from its pid, then + // mach_msg one IOSurfaceCreateMachPort() right per frame, tagged with accel_id. + // + // Wire format MUST match LLCEFSurfaceReceiver (newview/llcefsurfacereceiver.cpp). + typedef struct + { + mach_msg_header_t header; + mach_msg_body_t body; + mach_msg_port_descriptor_t surface; + int32_t accel_id; + int32_t width; + int32_t height; + int32_t format; + } CefSurfaceSendMsg; + + class CefMacSurfaceSender + { + public: + // Look up the viewer's receive port once; retried each frame until the + // viewer has registered it (no ordering guarantee between the processes). + bool connect(int host_pid) + { + if (mViewerPort != MACH_PORT_NULL) return true; + if (host_pid <= 0) return false; + + char name[128]; + snprintf(name, sizeof(name), "org.alchemyviewer.cefsurface.%d", host_pid); + kern_return_t kr = bootstrap_look_up(bootstrap_port, name, &mViewerPort); + if (kr != KERN_SUCCESS) + { + mViewerPort = MACH_PORT_NULL; // not up yet; try again next frame + return false; + } + return true; + } + + bool send(int accel_id, IOSurfaceRef surface, int width, int height, int format) + { + if (mViewerPort == MACH_PORT_NULL || !surface) return false; + + // A fresh send right to this frame's surface; the message copies it to + // the viewer and we drop our reference afterwards. + mach_port_t surf_port = IOSurfaceCreateMachPort(surface); + if (surf_port == MACH_PORT_NULL) return false; + + CefSurfaceSendMsg msg = {}; + msg.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX; + msg.header.msgh_size = sizeof(msg); + msg.header.msgh_remote_port = mViewerPort; + msg.header.msgh_local_port = MACH_PORT_NULL; + msg.body.msgh_descriptor_count = 1; + msg.surface.name = surf_port; + msg.surface.disposition = MACH_MSG_TYPE_COPY_SEND; + msg.surface.type = MACH_MSG_PORT_DESCRIPTOR; + msg.accel_id = accel_id; + msg.width = width; + msg.height = height; + msg.format = format; + + kern_return_t kr = mach_msg(&msg.header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, + sizeof(msg), 0, MACH_PORT_NULL, 100 /*ms*/, MACH_PORT_NULL); + mach_port_deallocate(mach_task_self(), surf_port); + + if (kr != KERN_SUCCESS) + { + // The viewer likely went away / its port died; drop it and re-look-up. + if (kr == MACH_SEND_INVALID_DEST) + { + mach_port_deallocate(mach_task_self(), mViewerPort); + mViewerPort = MACH_PORT_NULL; + } + return false; + } + return true; + } + + private: + mach_port_t mViewerPort = MACH_PORT_NULL; + }; + + CefMacSurfaceSender& macSurfaceSender() + { + static CefMacSurfaceSender sSender; + return sSender; + } +} +#endif // LL_DARWIN + +#if LL_LINUX +namespace +{ + // Wire header shared with the viewer (newview/llcefsurfacereceiver.cpp). + // MUST stay in sync. The plane fds ride alongside as SCM_RIGHTS ancillary data. + struct CefDmabufMsg + { + int32_t accel_id; + int32_t plane_count; + int32_t width; + int32_t height; + int32_t format; + uint32_t stride[4]; + uint64_t offset[4]; + uint64_t modifier; + }; + + // Hands CEF's accelerated-paint dma-buf to the viewer over an AF_UNIX datagram + // socket. CEF's dma-buf fd cannot cross to the viewer through the LLSD/TCP + // channel (text fd numbers re-opened via /proc fail with ENXIO for a dma-buf), + // so we pass the fds themselves as SCM_RIGHTS ancillary data. We send to the + // viewer's abstract socket named from its pid (learned via the init message's + // host_pid), tagging each frame with accel_id so the viewer demuxes it back. + class CefLinuxSurfaceSender + { + public: + // CEF's fds are valid only during this callback, but sendmsg() captures a + // kernel reference to each underlying dma-buf, so they survive until the + // viewer recvmsg()s them - we do not dup or keep them alive ourselves. + bool send(int host_pid, int accel_id, const dullahan::dmabuf_plane* planes, int plane_count, + int format, int width, int height, unsigned long long modifier) + { + if (host_pid <= 0 || !planes || plane_count <= 0) return false; + if (plane_count > 4) plane_count = 4; + + if (mSock < 0) + { + mSock = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (mSock < 0) return false; + } + + CefDmabufMsg hdr = {}; + hdr.accel_id = accel_id; + hdr.plane_count = plane_count; + hdr.width = width; + hdr.height = height; + hdr.format = format; + hdr.modifier = modifier; + int fds[4]; + for (int i = 0; i < plane_count; ++i) + { + fds[i] = planes[i].fd; + hdr.stride[i] = planes[i].stride; + hdr.offset[i] = planes[i].offset; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + int n = snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, + "org.alchemyviewer.cefsurface.%d", host_pid); // [0] NUL = abstract + socklen_t addrlen = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + n); + + struct iovec iov = { &hdr, sizeof(hdr) }; + char cbuf[CMSG_SPACE(sizeof(int) * 4)]; + memset(cbuf, 0, sizeof(cbuf)); + + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &addr; + msg.msg_namelen = addrlen; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cbuf; + msg.msg_controllen = CMSG_SPACE(sizeof(int) * plane_count); + + struct cmsghdr* c = CMSG_FIRSTHDR(&msg); + c->cmsg_level = SOL_SOCKET; + c->cmsg_type = SCM_RIGHTS; + c->cmsg_len = CMSG_LEN(sizeof(int) * plane_count); + memcpy(CMSG_DATA(c), fds, sizeof(int) * plane_count); + + // Non-blocking + connectionless: a not-yet-listening viewer (ENOENT/ + // ECONNREFUSED) or a full queue (EAGAIN) just drops this frame; the + // viewer keeps the previous frame and we send a fresh one next paint. + ssize_t s = sendmsg(mSock, &msg, MSG_DONTWAIT | MSG_NOSIGNAL); + return s == (ssize_t)sizeof(hdr); + } + + private: + int mSock = -1; + }; + + CefLinuxSurfaceSender& linuxSurfaceSender() + { + static CefLinuxSurfaceSender sSender; + return sSender; + } +} +#endif // LL_LINUX + +// duplicated handle, so it can open the texture and bind it with no CPU copy. +// The handle is only valid for the duration of this callback, so duplicate now. +void MediaPluginCEF::onAcceleratedPaintCallback(void* native_handle, int format, int width, int height) +{ +#if LL_WINDOWS + if (!native_handle || !mHostPid) + { + return; + } + + // Open the viewer process once (cached) so we can duplicate handles into it. + if (!mViewerProcess) + { + mViewerProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, (DWORD)mHostPid); + if (!mViewerProcess) + { + LL_WARNS("media") << "accelerated paint: OpenProcess(viewer pid " << mHostPid + << ") failed: " << GetLastError() << LL_ENDL; + return; + } + } + + // Bring up the D3D producer once. CEF hands a different pooled texture each + // frame; the producer copies it into ONE persistent keyed-mutex shared + // texture so we only duplicate a handle to the viewer when that texture is + // (re)created, not every frame. + if (!mAccelProducer) + { + mAccelProducer = new CefAccelProducer(); + if (!mAccelProducer->init()) + { + LL_WARNS("media") << "accelerated paint: D3D producer init failed" << LL_ENDL; + delete mAccelProducer; + mAccelProducer = nullptr; + return; + } + } + + HANDLE stable_handle = nullptr; + bool recreated = false; + int w = 0, h = 0, fmt = 0; + if (!mAccelProducer->produce(native_handle, mViewerProcess, stable_handle, recreated, w, h, fmt)) + { + return; + } + + // Tell the viewer a new frame is ready. The handle field is the stable + // texture's viewer-side handle ONLY when it was just (re)created (sent once + // per size); otherwise "0" means "same texture, new frame". Carried as a + // decimal string so the full 64-bit value survives the LLSD message. + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", recreated ? std::to_string((unsigned long long)(uintptr_t)stable_handle) + : std::string("0")); + message.setValueS32("format", fmt); + message.setValueS32("width", w); + message.setValueS32("height", h); + sendMessage(message); +#elif LL_DARWIN + // macOS: the handle is an IOSurfaceRef shared (by CEF) via mach ports, so a + // global-id lookup in the viewer cannot work. Hand the surface over as a mach + // send right on our side channel, then post the usual message as the viewer's + // per-frame "dirty" trigger (the surface itself arrives out-of-band). + if (!native_handle) + { + return; + } + CefMacSurfaceSender& sender = macSurfaceSender(); + if (!sender.connect(mHostPid)) + { + return; // viewer's receive port not registered yet; retry next frame + } + if (!sender.send(mAccelId, (IOSurfaceRef)native_handle, width, height, format)) + { + return; + } + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", "0"); // unused on macOS (surface came via mach) + message.setValueS32("format", format); + message.setValueS32("width", width); + message.setValueS32("height", height); + sendMessage(message); +#else + // Linux uses the dma-buf callback path instead (see onAcceleratedPaintDmabufCallback). + (void)native_handle; (void)format; (void)width; (void)height; +#endif +} + +#if LL_LINUX +//////////////////////////////////////////////////////////////////////////////// +// Linux zero-copy paint: CEF hands us a dma-buf (one or more planes + a DRM +// modifier) valid only for this callback. Pass the plane fds to the viewer over +// the SCM_RIGHTS side channel - a dma-buf fd cannot be reopened across the +// process boundary via /proc (open() returns ENXIO), so the fds themselves must +// be handed over as ancillary data. Then post the usual LLSD "dirty" trigger; the +// fds arrived out-of-band, demuxed by accel_id, mirroring the macOS IOSurface path. +void MediaPluginCEF::onAcceleratedPaintDmabufCallback(const dullahan::dmabuf_plane* planes, int plane_count, + int format, int width, int height, unsigned long long modifier) +{ + if (!planes || plane_count <= 0) + { + return; + } + + if (!linuxSurfaceSender().send(mHostPid, mAccelId, planes, plane_count, format, width, height, modifier)) + { + return; // viewer not listening yet / queue full - retried next paint + } + + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", "0"); // fds came via the side channel, not here + message.setValueS32("format", format); + message.setValueS32("width", width); + message.setValueS32("height", height); + sendMessage(message); +} +#endif + //////////////////////////////////////////////////////////////////////////////// // void MediaPluginCEF::onConsoleMessageCallback(std::string message, std::string source, int line) @@ -630,8 +1161,28 @@ void MediaPluginCEF::receiveMessage(const char* message_string) { if (message_name == "init") { + // Zero-copy paint: the viewer asks for GPU shared-texture handles + // and tells us its process id so we can DuplicateHandle into it. + mUseAcceleratedPaint = message_in.getValueBoolean("accelerated_paint"); + mHostPid = message_in.getValueS32("host_pid"); + mAccelId = message_in.getValueS32("accel_id"); + // Linux: the viewer's windowing backend, used below to pin CEF's + // Ozone platform (X11 vs Wayland) instead of guessing from env. + mDisplayServer = message_in.getValue("display_server"); + // event callbacks from Dullahan mCEFLib->setOnPageChangedCallback(std::bind(&MediaPluginCEF::onPageChangedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + if (mUseAcceleratedPaint) + { +#if LL_LINUX + // Linux frames are dma-bufs (planes + modifier), not a single handle. + mCEFLib->setOnAcceleratedPaintDmabufCallback(std::bind(&MediaPluginCEF::onAcceleratedPaintDmabufCallback, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); +#else + mCEFLib->setOnAcceleratedPaintCallback(std::bind(&MediaPluginCEF::onAcceleratedPaintCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); +#endif + } mCEFLib->setOnCustomSchemeURLCallback(std::bind(&MediaPluginCEF::onCustomSchemeURLCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); mCEFLib->setOnConsoleMessageCallback(std::bind(&MediaPluginCEF::onConsoleMessageCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); mCEFLib->setOnStatusMessageCallback(std::bind(&MediaPluginCEF::onStatusMessageCallback, this, std::placeholders::_1)); @@ -667,6 +1218,7 @@ void MediaPluginCEF::receiveMessage(const char* message_string) settings.host_process_path = ll_convert_wide_to_string(&buffer[0]); #endif settings.accept_language_list = mHostLanguage; + settings.accelerated_paint = mUseAcceleratedPaint; // SL-15560: Product team overruled my change to set the default // embedded background color to match the floater background @@ -676,6 +1228,10 @@ void MediaPluginCEF::receiveMessage(const char* message_string) settings.root_cache_path = mRootCachePath; settings.cookies_enabled = mCookiesEnabled; + // Linux only: pin CEF's Ozone backend to the viewer's windowing + // backend (empty = let dullahan auto-detect). No-op elsewhere. + settings.ozone_platform = mDisplayServer; + // configure proxy argument if enabled and valid if (mProxyEnabled && mProxyHost.length()) { @@ -742,7 +1298,10 @@ void MediaPluginCEF::receiveMessage(const char* message_string) message.setValueS32("default_width", 1024); message.setValueS32("default_height", 1024); message.setValueS32("depth", mDepth); - message.setValueU32("internalformat", GL_RGB); + // Accelerated paint copies the BGRA shared texture into the media + // texture with glCopyImageSubData, which needs matching 32-bit + // (RGBA8) storage; the CPU path uploads BGRA into RGB as before. + message.setValueU32("internalformat", mUseAcceleratedPaint ? GL_RGBA8 : GL_RGB); message.setValueU32("format", GL_BGRA); message.setValueU32("type", GL_UNSIGNED_BYTE); message.setValueBoolean("coords_opengl", true); diff --git a/indra/media_plugins/cef/slplugin_cef.cpp b/indra/media_plugins/cef/slplugin_cef.cpp new file mode 100644 index 0000000000..e7c82dfaf2 --- /dev/null +++ b/indra/media_plugins/cef/slplugin_cef.cpp @@ -0,0 +1,79 @@ +/** + * @file slplugin_cef.cpp + * @brief Static-plugin hook for the dedicated SLPluginCEF host. + * + * SLPluginCEF statically links the CEF media plugin (and CEF itself) instead of + * dlopen()ing media_plugin_cef at runtime. media_plugin_base provides the + * exported LLPluginInitEntryPoint; hand its address to slplugin so + * LLPluginInstance::load() calls it directly. This avoids dlopen of libcef + * (which exhausts the static TLS block on Linux) and gives the Windows sandbox + * the single-image host it requires. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llplugininstance.h" + +#include "dullahan.h" + +#include + +// Exported by media_plugin_base, which is statically linked into this host. +extern "C" int LLPluginInitEntryPoint(LLPluginInstance::sendMessageFunction host_send_func, + void *host_user_data, + LLPluginInstance::sendMessageFunction *plugin_send_func, + void **plugin_user_data); + +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init() +{ + return &LLPluginInitEntryPoint; +} + +// Defined in slplugin.cpp / slplugin_daemon.cpp. +int slplugin_run(U32 port); +int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path); + +// CEF host entry handed control by the platform main() (slplugin.cpp). This is +// the non-Windows analog of slplugin_cef_bootstrap.cpp's run_cef_host (on Windows +// the bootstrap entry is used instead, and CEF sub-processes are dispatched there +// by CefExecuteProcess; on macOS/Linux the DullahanHelper bundles / dullahan_host +// dispatch them, so there is nothing to do here for sub-processes). +int ll_run_slplugin_host(U32 port, const std::string& daemon_rendezvous) +{ + // Keep one process-global CEF runtime up for the whole life of this host (a + // dedicated single tab, or the shared daemon serving many) and shut it down + // exactly once below. Without this a zero-browser gap - e.g. the login web + // surface closing just before the next opens - would CefShutdown and the next + // CefInitialize would crash (CEF init is once-per-process). The macOS sandbox + // toggle is applied separately when the plugin builds its settings + // (media_plugin_cef.cpp). + dullahan::setPersistentRuntime(true); + + const int rc = daemon_rendezvous.empty() + ? slplugin_run(port) + : slplugin_daemon_run(port, daemon_rendezvous); + + // Persistent host: release() left CEF running, so tear it down once now, + // before the process exits, for a clean teardown. + dullahan::shutdownRuntime(); + return rc; +} diff --git a/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp new file mode 100644 index 0000000000..1977927130 --- /dev/null +++ b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp @@ -0,0 +1,160 @@ +/** + * @file slplugin_cef_bootstrap.cpp + * @brief Windows CEF-sandbox bootstrap entry point for SLPluginCEF. + * + * On Windows the dedicated CEF host is built as SLPluginCEF.dll and loaded by + * CEF's bootstrap executable (shipped renamed to SLPluginCEF.exe). The bootstrap + * creates the Windows sandbox and calls our exported RunWinMain, handing us the + * sandbox_info. We: + * 1. run CEF's sub-process dispatch (CefExecuteProcess) so the SAME executable + * image services the renderer/GPU/utility sub-processes - which the + * Chromium sandbox requires (broker and targets must be one image); + * 2. for the browser process, hand the sandbox_info to the dullahan runtime + * (so its CefInitialize runs sandboxed) and run the normal slplugin host + * message loop. + * + * The client DLL does NOT link cef_sandbox.lib (M138+ ships that only inside the + * bootstrap executables), so there is no static-CRT requirement here. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "cef_app.h" +#include "cef_sandbox_win.h" // RunWinMain prototype + CEF_BOOTSTRAP_EXPORT +#include "cef_version_info.h" + +#include "dullahan.h" +#include "dullahan_runtime.h" + +#include "llapr.h" +#include "llerrorcontrol.h" +#include "llstring.h" + +// Defined in slplugin.cpp - the shared plugin<->parent host message loop. +int slplugin_run(U32 port); +// Defined in slplugin_daemon.cpp - the multi-tab daemon host loop. +int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path); + +namespace +{ + int run_cef_host(HINSTANCE hInstance, LPTSTR lpCmdLine, void* sandbox_info) + { + // Developer escape: SLPLUGIN_CEF_NO_SANDBOX disables the sandbox (so the + // child processes are attachable/debuggable) without rebuilding. Passing + // a null sandbox_info makes dullahan_runtime fall back to no_sandbox plus + // the dullahan_host helper. + if (getenv("SLPLUGIN_CEF_NO_SANDBOX")) + { + sandbox_info = nullptr; + } + + // CEF sub-process dispatch. For a renderer/GPU/utility sub-process this + // blocks until it exits and returns its exit code; for the browser + // process it returns -1 and we continue. The dullahan runtime is the + // process CefApp. + CefMainArgs main_args(hInstance); + CefRefPtr app = &dullahan_runtime::instance(); + int exit_code = CefExecuteProcess(main_args, app, sandbox_info); + if (exit_code >= 0) + { + return exit_code; + } + + // Browser process: make the sandbox info available to the runtime's + // CefInitialize, and tell dullahan that this host dispatches CEF + // sub-processes itself (CEF re-launches this image -> RunWinMain -> + // CefExecuteProcess), so it never uses the dullahan_host helper - + // independent of whether the sandbox is active. Then run the host loop. + dullahan::setSandboxInfo(sandbox_info); + dullahan::setHostHandlesSubprocesses(true); + // This host keeps one CEF runtime for its whole life (a dedicated single + // tab, or the shared daemon serving many) and shuts it down once below + // (shutdownRuntime). Without this the browser refcount would CefShutdown + // on a zero-browser gap - e.g. the login web surface closing just before + // the next opens - and the next CefInitialize would crash (CEF init is + // once-per-process). + dullahan::setPersistentRuntime(true); + + ll_init_apr(); + { + LLError::initForApplication(".", "."); + LLError::setDefaultLevel(LLError::LEVEL_INFO); + } + + // RunWinMain's lpCmdLine is LPTSTR (wide under UNICODE). Convert it to UTF-8 + // rather than narrowing each wchar, so a localized rendezvous path with + // non-ASCII characters survives. The command line is "" for a single + // tab, or " --daemon " to run as the shared multi-tab + // daemon (the rendezvous path may contain spaces, so it is taken as the + // whole remainder after --daemon). + std::string cmd; + if (lpCmdLine && *lpCmdLine) + { + int needed = WideCharToMultiByte(CP_UTF8, 0, lpCmdLine, -1, nullptr, 0, nullptr, nullptr); + if (needed > 1) + { + cmd.resize(static_cast(needed) - 1); // drop the NUL terminator + WideCharToMultiByte(CP_UTF8, 0, lpCmdLine, -1, cmd.data(), needed, nullptr, nullptr); + } + } + LLStringUtil::trim(cmd); + + const std::string first = cmd.substr(0, cmd.find(' ')); + U32 port = 0; + if (first.empty() || !LLStringUtil::convertToU32(first, port) || !port) + { + LL_WARNS("slplugin") << "SLPluginCEF: missing/invalid launcher port" << LL_ENDL; + ll_cleanup_apr(); + return 1; + } + + std::string rendezvous; + const size_t dpos = cmd.find("--daemon"); + if (dpos != std::string::npos) + { + rendezvous = cmd.substr(dpos + 8); // strlen("--daemon") + LLStringUtil::trim(rendezvous); + } + + const int rc = rendezvous.empty() ? slplugin_run(port) + : slplugin_daemon_run(port, rendezvous); + + // The host loop has returned (all tabs gone / daemon idle-exit). Because + // this is a persistent host, release() left CEF running - shut it down + // exactly once now, before the process exits, for a clean teardown. + dullahan::shutdownRuntime(); + + ll_cleanup_apr(); + return rc; + } +} + +// Exported entry point the CEF bootstrap executable calls. version_info is +// provided by the bootstrap and not needed here. +extern "C" CEF_BOOTSTRAP_EXPORT int RunWinMain(HINSTANCE hInstance, + LPTSTR lpCmdLine, + int /*nCmdShow*/, + void* sandbox_info, + cef_version_info_t* /*version_info*/) +{ + return run_cef_host(hInstance, lpCmdLine, sandbox_info); +} diff --git a/indra/media_plugins/example/CMakeLists.txt b/indra/media_plugins/example/CMakeLists.txt index a9dcc0c1eb..dfd3af703d 100644 --- a/indra/media_plugins/example/CMakeLists.txt +++ b/indra/media_plugins/example/CMakeLists.txt @@ -2,18 +2,34 @@ include(PluginAPI) -### media_plugin_example +### media_plugin_example - dedicated host executable that statically links the +### example media plugin together with the slplugin host driver (no dlopen). set(media_plugin_example_SOURCE_FILES media_plugin_example.cpp ) -add_library(media_plugin_example - SHARED +add_executable(media_plugin_example + WIN32 + MACOSX_BUNDLE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_static.cpp ${media_plugin_example_SOURCE_FILES} ) -target_link_libraries(media_plugin_example media_plugin_base ) +if (DARWIN) + target_sources(media_plugin_example PRIVATE ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin-objc.mm) + set_source_files_properties(${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin-objc.mm + PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE) +endif () + +target_link_libraries(media_plugin_example + media_plugin_base + llplugin + llmessage + llcommon + ll::pluginlibraries +) if (WINDOWS) set_target_properties(media_plugin_example @@ -22,13 +38,9 @@ if (WINDOWS) ) target_link_options(media_plugin_example PRIVATE /MANIFEST:NO) elseif (DARWIN) - # Don't prepend 'lib' to the executable name, and don't embed a full path in the library's install name - set_target_properties( - media_plugin_example + set_target_properties(media_plugin_example PROPERTIES - PREFIX "" BUILD_WITH_INSTALL_RPATH 1 - INSTALL_RPATH "@executable_path/../Frameworks" + INSTALL_RPATH "@executable_path/../../../../Frameworks;@executable_path/../Frameworks;@executable_path/../Frameworks/plugins" ) - endif () diff --git a/indra/media_plugins/gstreamer10/CMakeLists.txt b/indra/media_plugins/gstreamer10/CMakeLists.txt index 2dc8eaf8c2..d9896b0b43 100644 --- a/indra/media_plugins/gstreamer10/CMakeLists.txt +++ b/indra/media_plugins/gstreamer10/CMakeLists.txt @@ -5,7 +5,9 @@ include(GLIB) include(GStreamer10Plugin) -### media_plugin_gstreamer10 +### media_plugin_gstreamer10 - dedicated host executable that statically links the +### GStreamer media plugin together with the slplugin host driver (no dlopen). This +### plugin is only built on UNIX-but-not-Apple (see BUILD_GSTREAMER_PLUGIN). set(media_plugin_gstreamer10_SOURCE_FILES media_plugin_gstreamer10.cpp @@ -15,17 +17,17 @@ set(media_plugin_gstreamer10_HEADER_FILES llmediaimplgstreamer_syms.h ) -add_library(media_plugin_gstreamer10 - SHARED +add_executable(media_plugin_gstreamer10 + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_static.cpp ${media_plugin_gstreamer10_SOURCE_FILES} ) -target_link_libraries(media_plugin_gstreamer10 media_plugin_base ll::gstreamer10 ) - -if (WINDOWS) - set_target_properties( - media_plugin_gstreamer10 - PROPERTIES - LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /NODEFAULTLIB:LIBCMT" - ) -endif (WINDOWS) +target_link_libraries(media_plugin_gstreamer10 + media_plugin_base + ll::gstreamer10 + llplugin + llmessage + llcommon + ll::pluginlibraries +) diff --git a/indra/media_plugins/libvlc/CMakeLists.txt b/indra/media_plugins/libvlc/CMakeLists.txt index a7d7d6eb9c..3db44094cc 100644 --- a/indra/media_plugins/libvlc/CMakeLists.txt +++ b/indra/media_plugins/libvlc/CMakeLists.txt @@ -3,21 +3,35 @@ include(PluginAPI) include(LibVLCPlugin) -### media_plugin_libvlc - +### media_plugin_libvlc - dedicated host executable that statically links the +### LibVLC media plugin together with the slplugin host driver (there is no +### dlopen'd plugin library any more). set(media_plugin_libvlc_SOURCE_FILES media_plugin_libvlc.cpp ) -add_library(media_plugin_libvlc - SHARED +add_executable(media_plugin_libvlc + WIN32 + MACOSX_BUNDLE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_static.cpp ${media_plugin_libvlc_SOURCE_FILES} ) +if (DARWIN) + target_sources(media_plugin_libvlc PRIVATE ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin-objc.mm) + set_source_files_properties(${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin-objc.mm + PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE) +endif () + target_link_libraries(media_plugin_libvlc media_plugin_base ll::libvlc + llplugin + llmessage + llcommon + ll::pluginlibraries ) if (WINDOWS) @@ -28,19 +42,15 @@ if (WINDOWS) ) target_link_options(media_plugin_libvlc PRIVATE /MANIFEST:NO) - # Copy plugin dlls to packaging directory + # Copy VLC runtime plugins to the packaging directory add_custom_command( TARGET media_plugin_libvlc POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/plugins/vlc-bin" "${VIEWER_STAGING_DIR}/llplugin/plugins" ) elseif (DARWIN) - # Don't prepend 'lib' to the executable name, and don't embed a full path in the library's install name - set_target_properties( - media_plugin_libvlc + set_target_properties(media_plugin_libvlc PROPERTIES - PREFIX "" BUILD_WITH_INSTALL_RPATH 1 - INSTALL_RPATH "@executable_path/../Frameworks" + INSTALL_RPATH "@executable_path/../../../../Frameworks;@executable_path/../Frameworks;@executable_path/../Frameworks/plugins" ) - endif () diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 662af9a61b..42c23a6fc6 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -253,6 +253,8 @@ set(viewer_SOURCE_FILES llbrowsernotification.cpp llbuycurrencyhtml.cpp llcallingcard.cpp + llcefaccelinterop.cpp + llcefsurfacereceiver.cpp llchannelmanager.cpp #llchatbar.cpp llchathistory.cpp @@ -1021,6 +1023,8 @@ set(viewer_HEADER_FILES llbuycurrencyhtml.h llcallingcard.h llcapabilityprovider.h + llcefaccelinterop.h + llcefsurfacereceiver.h llchannelmanager.h #llchatbar.h llchathistory.h @@ -2002,14 +2006,23 @@ if (NOT DISABLE_WEBRTC) endif() endif() +if (DARWIN) + find_library(IOSURFACE_FRAMEWORK IOSurface) + find_library(COREGRAPHICS_FRAMEWORK CoreGraphics) + target_link_libraries(${VIEWER_BINARY_NAME} + ${IOSURFACE_FRAMEWORK} + ${COREGRAPHICS_FRAMEWORK} + ) +endif() + # These are the generated targets that are copied for packaging that we do not directly link to # We special case for windows due to targets incompatible with Debug builds if (WINDOWS) + target_link_libraries(${VIEWER_BINARY_NAME} d3d11 dxgi ) add_custom_target(copy_input_dependencies DEPENDS stage_third_party_libs - SLPlugin - $<$:$> + $ $ $<$:$> $ @@ -2018,7 +2031,6 @@ else() add_custom_target(copy_input_dependencies DEPENDS stage_third_party_libs - SLPlugin $ $ $ diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 417199ad9b..7ed918cd34 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2,6 +2,50 @@ + ALCefAcceleratedPaint + + Comment + Use CEF GPU shared-texture (accelerated paint) zero-copy path for browser media instead of CPU pixel readback/upload. Requires a viewer restart. (CEF daemon rework, on by default; falls back to CPU paint where the GPU interop is unavailable.) + Persist + 1 + Type + Boolean + Value + 1 + + ALCefDaemonEnabled + + Comment + Host all browser (CEF) media instances in a single shared tab-manager daemon process instead of one process per instance. Requires a viewer restart. (CEF daemon rework, on by default.) + Persist + 1 + Type + Boolean + Value + 1 + + ALCefDedicatedHost + + Comment + Launch browser (CEF) media via the dedicated SLPluginCEF host, which statically links the CEF plugin instead of dlopen()ing it (avoids the Linux static-TLS crash; prerequisite for the sandbox). Requires a viewer restart. (CEF daemon rework, on by default.) + Persist + 1 + Type + Boolean + Value + 1 + + ALCefSandbox + + Comment + Enable the Chromium sandbox for CEF browser sub-processes (renderer/GPU/utility). Requires a viewer restart. (CEF daemon rework, on by default.) + Persist + 1 + Type + Boolean + Value + 1 + ALSceneExplorerActivateAction Comment diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp new file mode 100644 index 0000000000..25a674ec0c --- /dev/null +++ b/indra/newview/llcefaccelinterop.cpp @@ -0,0 +1,670 @@ +/** + * @file llcefaccelinterop.cpp + * @brief Viewer-side consumer of the CEF accelerated-paint shared texture. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llcefaccelinterop.h" + +#if LL_WINDOWS + +#include "llgl.h" // viewer GL entry points + the global wglDX* pointers +#include "llrender.h" +#include "lldxhardware.h" // shared D3D11 device + GL interop device (gDXHardware) + +#include +#include +#include + +namespace +{ + // The D3D11 device + the WGL interop device are process-shared (created once + // in LLDXHardware), and the wglDX* entry points are loaded by the viewer's + // WGL loader (LLGLManager::initWGL). Convenience accessors: + inline ID3D11Device1* sharedDevice() { return (ID3D11Device1*)gDXHardware.getD3DDevice(); } + inline ID3D11DeviceContext* sharedContext() { return (ID3D11DeviceContext*)gDXHardware.getD3DContext(); } + inline HANDLE interopDevice() { return (HANDLE)gDXHardware.getGLDXInteropDevice(); } + + struct WinAccel + { + // The plugin's shared texture opened in the shared device (cross-device, + // NT handle - so it can't be GL-registered directly), plus its keyed mutex. + ID3D11Texture2D* stable = nullptr; + IDXGIKeyedMutex* mutex = nullptr; + + // An own-(shared-)device intermediate, which CAN be GL-registered; we copy + // the stable texture into it under the mutex each frame. + ID3D11Texture2D* local = nullptr; + GLuint local_gl = 0; + HANDLE local_obj = nullptr; + int width = 0; + int height = 0; + bool logged_register = false; + + // FBOs for the flip+convert blit (created once, reused). + GLuint read_fbo = 0; + GLuint draw_fbo = 0; + + void releaseStable() + { + if (local_obj) { wglDXUnregisterObjectNV(interopDevice(), local_obj); local_obj = nullptr; } + if (local_gl) { glDeleteTextures(1, &local_gl); local_gl = 0; } + if (local) { local->Release(); local = nullptr; } + if (mutex) { mutex->Release(); mutex = nullptr; } + if (stable) { stable->Release(); stable = nullptr; } + width = height = 0; + } + }; +} + +LLCEFAccelInterop::~LLCEFAccelInterop() +{ + shutdown(); +} + +bool LLCEFAccelInterop::init() +{ + // The shared interop device is brought up once at window init; just verify it + // (and the WGL entry points) are available. + if (!gDXHardware.hasGLDXInterop() || !wglDXRegisterObjectNV || !wglDXLockObjectsNV) + { + return false; + } + mImpl = new WinAccel(); + mValid = true; + return true; +} + +void LLCEFAccelInterop::shutdown() +{ + if (!mImpl) + { + return; + } + WinAccel* w = (WinAccel*)mImpl; + w->releaseStable(); + if (w->read_fbo) { glDeleteFramebuffers(1, &w->read_fbo); w->read_fbo = 0; } + if (w->draw_fbo) { glDeleteFramebuffers(1, &w->draw_fbo); w->draw_fbo = 0; } + delete w; + mImpl = nullptr; + mValid = false; +} + +bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, int height, + int format, unsigned int stride, + unsigned long long offset, unsigned long long modifier, int src_pid, + int plane_count, const unsigned long long* plane_fds, + const unsigned int* plane_strides, const unsigned long long* plane_offsets) +{ + (void)format; (void)stride; (void)offset; (void)modifier; (void)src_pid; + (void)plane_count; (void)plane_fds; (void)plane_strides; (void)plane_offsets; + if (!mValid || !handle) + { + return false; + } + WinAccel* w = (WinAccel*)mImpl; + w->releaseStable(); + + // Open the plugin's keyed-mutex shared texture in the shared device. + if (FAILED(sharedDevice()->OpenSharedResource1((HANDLE)(uintptr_t)handle, __uuidof(ID3D11Texture2D), (void**)&w->stable)) || !w->stable) + { + LL_WARNS("Media") << "accelerated paint: OpenSharedResource1 failed" << LL_ENDL; + return false; + } + // The producer/consumer contract relies on keyed-mutex synchronization; an + // unsynchronized CopyResource could read while the plugin is mid-write. Fail + // the bind (rather than silently fall back) if the mutex is missing. + if (FAILED(w->stable->QueryInterface(__uuidof(IDXGIKeyedMutex), (void**)&w->mutex)) || !w->mutex) + { + LL_WARNS("Media") << "accelerated paint: shared texture is missing IDXGIKeyedMutex" << LL_ENDL; + w->releaseStable(); + return false; + } + + D3D11_TEXTURE2D_DESC sd = {}; + w->stable->GetDesc(&sd); + + // An own-device intermediate of the same format; this one can be GL-registered + // (the opened shared texture can't - NT-handle cross-device, same as CEF's). + D3D11_TEXTURE2D_DESC d = {}; + d.Width = sd.Width; d.Height = sd.Height; d.MipLevels = 1; d.ArraySize = 1; + d.Format = sd.Format; d.SampleDesc.Count = 1; d.Usage = D3D11_USAGE_DEFAULT; + d.BindFlags = D3D11_BIND_SHADER_RESOURCE; + if (FAILED(sharedDevice()->CreateTexture2D(&d, nullptr, &w->local)) || !w->local) + { + LL_WARNS("Media") << "accelerated paint: CreateTexture2D(local) failed" << LL_ENDL; + w->releaseStable(); + return false; + } + + glGenTextures(1, &w->local_gl); + w->local_obj = wglDXRegisterObjectNV(interopDevice(), w->local, w->local_gl, GL_TEXTURE_2D, WGL_ACCESS_READ_ONLY_NV); + if (!w->local_obj) + { + LL_WARNS("Media") << "accelerated paint: wglDXRegisterObjectNV failed (" << GetLastError() << ")" << LL_ENDL; + w->releaseStable(); + return false; + } + + w->width = (int)sd.Width; + w->height = (int)sd.Height; + if (!w->logged_register) + { + LL_INFOS("Media") << "accelerated paint: stable texture bound " << sd.Width << "x" << sd.Height + << " dxfmt=" << sd.Format << LL_ENDL; + w->logged_register = true; + } + return true; +} + +bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) +{ + if (!mValid || !dst_tex) + { + return false; + } + WinAccel* w = (WinAccel*)mImpl; + if (!w->stable || !w->local || !w->local_obj) + { + return false; + } + + int cw = llmin(width, w->width); + int ch = llmin(height, w->height); + if (cw <= 0 || ch <= 0) + { + return false; + } + + ID3D11DeviceContext* ctx = sharedContext(); + + // Copy the plugin's latest frame into our intermediate under the keyed mutex + // (single key 0 = mutual exclusion with the producer + cross-process sync). + // Acquire with a zero timeout so a busy producer never stalls the viewer GL + // thread; just skip this frame and retry next tick. + const HRESULT acq_hr = w->mutex->AcquireSync(0, 0); + if (acq_hr == (HRESULT)WAIT_TIMEOUT || acq_hr == DXGI_ERROR_WAIT_TIMEOUT) + { + return false; // producer busy this frame; try again next + } + if (FAILED(acq_hr)) + { + LL_WARNS("Media") << "accelerated paint: AcquireSync failed hr=0x" + << std::hex << acq_hr << std::dec << LL_ENDL; + return false; + } + ctx->CopyResource(w->local, w->stable); + ctx->Flush(); + w->mutex->ReleaseSync(0); + + // Lock the GL view of the intermediate and blit it into the media texture + // via framebuffers. Reading the interop texture through a framebuffer samples + // it in the correct channel order (same as a normal texture fetch - so no + // BGRA swizzle is needed), and the inverted destination rectangle flips Y + // (CEF's accelerated texture is top-down; the viewer expects bottom-up). + HANDLE gl_dx = interopDevice(); + if (!wglDXLockObjectsNV(gl_dx, 1, &w->local_obj)) + { + return false; + } + + if (!w->read_fbo) glGenFramebuffers(1, &w->read_fbo); + if (!w->draw_fbo) glGenFramebuffers(1, &w->draw_fbo); + + GLint prev_read = 0, prev_draw = 0; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_draw); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, w->read_fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, w->local_gl, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, w->draw_fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, (GLuint)dst_tex, 0); + + glBlitFramebuffer(0, 0, cw, ch, // src + 0, ch, cw, 0, // dst, Y-flipped + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prev_draw); + + wglDXUnlockObjectsNV(gl_dx, 1, &w->local_obj); + return true; +} + +#elif LL_DARWIN // macOS: bind the shared IOSurface to a GL texture, blit to media + +#include "llgl.h" +#include +#include +#include + +#ifndef GL_TEXTURE_RECTANGLE_ARB +#define GL_TEXTURE_RECTANGLE_ARB 0x84F5 +#endif + +namespace +{ + struct MacAccel + { + GLuint tex = 0; // GL_TEXTURE_RECTANGLE bound to the IOSurface + IOSurfaceRef surface = nullptr; + int width = 0; + int height = 0; + GLuint read_fbo = 0; + GLuint draw_fbo = 0; + }; +} + +LLCEFAccelInterop::~LLCEFAccelInterop() +{ + shutdown(); +} + +bool LLCEFAccelInterop::init() +{ + if (!CGLGetCurrentContext()) + { + return false; + } + MacAccel* m = new MacAccel(); + glGenTextures(1, &m->tex); + mImpl = m; + mValid = true; + return true; +} + +void LLCEFAccelInterop::shutdown() +{ + if (!mImpl) + { + return; + } + MacAccel* m = (MacAccel*)mImpl; + if (m->tex) { glDeleteTextures(1, &m->tex); } + if (m->read_fbo) { glDeleteFramebuffers(1, &m->read_fbo); } + if (m->draw_fbo) { glDeleteFramebuffers(1, &m->draw_fbo); } + if (m->surface) { CFRelease(m->surface); } + delete m; + mImpl = nullptr; + mValid = false; +} + +bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, int height, + int format, unsigned int stride, + unsigned long long offset, unsigned long long modifier, int src_pid, + int plane_count, const unsigned long long* plane_fds, + const unsigned int* plane_strides, const unsigned long long* plane_offsets) +{ + (void)format; (void)stride; (void)offset; (void)modifier; (void)src_pid; + (void)plane_count; (void)plane_fds; (void)plane_strides; (void)plane_offsets; + if (!mValid || !handle) + { + return false; + } + MacAccel* m = (MacAccel*)mImpl; + + // The "handle" is an already-resolved IOSurfaceRef (+1 retained) handed over + // by the mach receiver (LLCEFSurfaceReceiver) - CEF shares its IOSurface via a + // mach port, which can't be resolved from a cross-process global id, so the + // surface arrives out-of-band rather than as an IOSurfaceID. We take ownership. + IOSurfaceRef surf = (IOSurfaceRef)(uintptr_t)handle; + if (!surf) + { + return false; + } + // Bind into the GL texture first; only swap in the new surface once the bind + // succeeds, so a failed import leaves the previous (good) binding intact for + // blitTo() rather than dropping to a stale/invalid surface. + const int new_width = (int)IOSurfaceGetWidth(surf); + const int new_height = (int)IOSurfaceGetHeight(surf); + + CGLContextObj cgl = CGLGetCurrentContext(); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, m->tex); + CGLError err = CGLTexImageIOSurface2D(cgl, GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, + new_width, new_height, GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, surf, 0); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + if (err != kCGLNoError) + { + CFRelease(surf); + return false; + } + + if (m->surface) { CFRelease(m->surface); } + m->surface = surf; + m->width = new_width; + m->height = new_height; + return true; +} + +bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) +{ + if (!mValid || !dst_tex) + { + return false; + } + MacAccel* m = (MacAccel*)mImpl; + if (!m->surface || !m->tex) + { + return false; + } + int cw = llmin(width, m->width); + int ch = llmin(height, m->height); + if (cw <= 0 || ch <= 0) + { + return false; + } + + if (!m->read_fbo) glGenFramebuffers(1, &m->read_fbo); + if (!m->draw_fbo) glGenFramebuffers(1, &m->draw_fbo); + + GLint prev_read = 0, prev_draw = 0; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_draw); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, m->read_fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, m->tex, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->draw_fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, (GLuint)dst_tex, 0); + + glBlitFramebuffer(0, 0, cw, ch, // src + 0, ch, cw, 0, // dst, Y-flipped + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, 0, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prev_draw); + return true; +} + +#elif LL_LINUX // import the plugin's dma-buf via EGL, blit to the media texture + +#include "llgl.h" +#include +#include +#include +#include +#include + +namespace +{ + // fourcc codes (avoid a hard dependency on drm_fourcc.h) + inline unsigned int dh_fourcc(char a, char b, char c, char d) + { + return (unsigned)a | ((unsigned)b << 8) | ((unsigned)c << 16) | ((unsigned)d << 24); + } + + // DRM "no explicit modifier" sentinel (from drm_fourcc.h; declared here to + // avoid a hard dependency on it). Must NOT be passed to eglCreateImageKHR as + // an explicit modifier - doing so fails the import. + static const unsigned long long DH_DRM_FORMAT_MOD_INVALID = 0x00ffffffffffffffULL; + + struct LinuxAccel + { + EGLDisplay display = EGL_NO_DISPLAY; + bool has_import_modifiers = false; // EGL_EXT_image_dma_buf_import_modifiers + + GLuint tex = 0; // GL texture the EGLImage is bound to + EGLImageKHR image = EGL_NO_IMAGE_KHR; + int width = 0; + int height = 0; + GLuint read_fbo = 0; + GLuint draw_fbo = 0; + + void releaseImage() + { + if (image != EGL_NO_IMAGE_KHR && eglDestroyImageKHR) + { + eglDestroyImageKHR(display, image); + image = EGL_NO_IMAGE_KHR; + } + } + }; +} + +LLCEFAccelInterop::~LLCEFAccelInterop() +{ + shutdown(); +} + +bool LLCEFAccelInterop::init() +{ + SDL_EGLDisplay egl_display = SDL_EGL_GetCurrentDisplay(); + if(!egl_display) + { + LL_WARNS("Media") << "accelerated paint: EGL display not found (no EGL context?); CPU paint" << LL_ENDL; + return false; + } + + LinuxAccel* l = new LinuxAccel(); + l->display = egl_display; // requires the viewer to use an EGL context + + if (l->display == EGL_NO_DISPLAY || !eglCreateImageKHR || !glEGLImageTargetTexture2DOES) + { + LL_WARNS("Media") << "accelerated paint: EGL dma-buf import unavailable (no EGL context?); CPU paint" << LL_ENDL; + delete l; + return false; + } + + // DRM format modifiers (tiling/compression) can only be passed to the import + // when the driver advertises this extension; otherwise we must let it assume + // the buffer's default layout. (Mesa supports it; this is a safety net.) + const char* exts = eglQueryString ? eglQueryString(l->display, EGL_EXTENSIONS) : nullptr; + l->has_import_modifiers = exts && strstr(exts, "EGL_EXT_image_dma_buf_import_modifiers") != nullptr; + + glGenTextures(1, &l->tex); + mImpl = l; + mValid = true; + return true; +} + +void LLCEFAccelInterop::shutdown() +{ + if (!mImpl) + { + return; + } + LinuxAccel* l = (LinuxAccel*)mImpl; + l->releaseImage(); + if (l->tex) { glDeleteTextures(1, &l->tex); } + if (l->read_fbo) { glDeleteFramebuffers(1, &l->read_fbo); } + if (l->draw_fbo) { glDeleteFramebuffers(1, &l->draw_fbo); } + delete l; + mImpl = nullptr; + mValid = false; +} + +bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, int height, + int format, unsigned int stride, + unsigned long long offset, unsigned long long modifier, int src_pid, + int plane_count, const unsigned long long* plane_fds, + const unsigned int* plane_strides, const unsigned long long* plane_offsets) +{ + if (!mValid) + { + return false; + } + (void)src_pid; // unused on Linux: the fds arrive already open via SCM_RIGHTS + LinuxAccel* l = (LinuxAccel*)mImpl; + + // Build a local plane table. The plane fds are already open in THIS (viewer) + // process - handed over by the plugin as SCM_RIGHTS ancillary data - so we + // import them directly (a dma-buf fd cannot be reopened via /proc anyway). We + // do NOT own them: the caller closes them after we return, and EGL keeps its + // own reference once the image is created. Fall back to the scalar + // handle/stride/offset for plane 0 if no array was supplied. + int n = plane_count; + if (n <= 0) n = 1; + if (n > 4) n = 4; + int fds[4]; + unsigned int strides[4]; + unsigned long long offsets[4]; + for (int i = 0; i < n; ++i) + { + fds[i] = (int)(plane_fds ? plane_fds[i] : (i == 0 ? handle : 0)); + strides[i] = plane_strides ? plane_strides[i] : (i == 0 ? stride : 0); + offsets[i] = plane_offsets ? plane_offsets[i] : (i == 0 ? offset : 0); + } + + // CEF formats: 0 = RGBA_8888, 1 = BGRA_8888 (cef_color_type_t). Map to fourcc. + unsigned int fourcc = (format == 0) ? dh_fourcc('A','B','2','4') // DRM_FORMAT_ABGR8888 (RGBA) + : dh_fourcc('A','R','2','4'); // DRM_FORMAT_ARGB8888 (BGRA) + + // Pass the DRM modifier per plane ONLY when it's a real, known value and the + // driver supports import-with-modifiers. Passing DRM_FORMAT_MOD_INVALID (or + // any modifier without the extension) makes eglCreateImageKHR fail - the + // classic "accelerated web page renders grey". + const bool use_modifier = l->has_import_modifiers && modifier != DH_DRM_FORMAT_MOD_INVALID; + + static const EGLint FD_ATTR[4] = { EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE3_FD_EXT }; + static const EGLint OFF_ATTR[4] = { EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT }; + static const EGLint PIT_ATTR[4] = { EGL_DMA_BUF_PLANE0_PITCH_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT, EGL_DMA_BUF_PLANE2_PITCH_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT }; + static const EGLint MLO_ATTR[4] = { EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT }; + static const EGLint MHI_ATTR[4] = { EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT }; + + EGLint attrs[64]; + int a = 0; + attrs[a++] = EGL_WIDTH; attrs[a++] = width; + attrs[a++] = EGL_HEIGHT; attrs[a++] = height; + attrs[a++] = EGL_LINUX_DRM_FOURCC_EXT; attrs[a++] = (EGLint)fourcc; + for (int i = 0; i < n; ++i) + { + attrs[a++] = FD_ATTR[i]; attrs[a++] = fds[i]; + attrs[a++] = OFF_ATTR[i]; attrs[a++] = (EGLint)offsets[i]; + attrs[a++] = PIT_ATTR[i]; attrs[a++] = (EGLint)strides[i]; + if (use_modifier) + { + attrs[a++] = MLO_ATTR[i]; attrs[a++] = (EGLint)(modifier & 0xFFFFFFFFu); + attrs[a++] = MHI_ATTR[i]; attrs[a++] = (EGLint)(modifier >> 32); + } + } + attrs[a++] = EGL_NONE; + + // Import into a temporary handle first; a bad frame must not destroy the + // current binding before we know the replacement is usable. + EGLImageKHR new_image = eglCreateImageKHR(l->display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)0, attrs); + // The fds belong to the caller (received via SCM_RIGHTS); it closes them once + // we return. EGL has taken its own reference to the buffer by now. + + static bool logged = false; + if (!logged) + { + logged = true; + LL_INFOS("Media") << "accelerated paint dma-buf import: " << (new_image != EGL_NO_IMAGE_KHR ? "ok" : "FAILED") + << " planes=" << n << " fourcc=0x" << std::hex << fourcc + << " modifier=0x" << modifier << std::dec + << " mod_ext=" << (l->has_import_modifiers ? 1 : 0) + << " used_mod=" << (use_modifier ? 1 : 0) + << " " << width << "x" << height << LL_ENDL; + } + + if (new_image == EGL_NO_IMAGE_KHR) + { + LL_WARNS("Media") << "accelerated paint: eglCreateImageKHR(dma_buf) failed (planes=" << n + << " modifier=0x" << std::hex << modifier << std::dec << ")" << LL_ENDL; + return false; + } + + l->releaseImage(); + l->image = new_image; + l->width = width; + l->height = height; + glBindTexture(GL_TEXTURE_2D, l->tex); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, l->image); + glBindTexture(GL_TEXTURE_2D, 0); + return true; +} + +bool LLCEFAccelInterop::blitTo(unsigned int dst_tex, int width, int height) +{ + if (!mValid || !dst_tex) + { + return false; + } + LinuxAccel* l = (LinuxAccel*)mImpl; + if (l->image == EGL_NO_IMAGE_KHR || !l->tex) + { + return false; + } + int cw = llmin(width, l->width); + int ch = llmin(height, l->height); + if (cw <= 0 || ch <= 0) + { + return false; + } + + if (!l->read_fbo) glGenFramebuffers(1, &l->read_fbo); + if (!l->draw_fbo) glGenFramebuffers(1, &l->draw_fbo); + + GLint prev_read = 0, prev_draw = 0; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_draw); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, l->read_fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, l->tex, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, l->draw_fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, (GLuint)dst_tex, 0); + + glBlitFramebuffer(0, 0, cw, ch, // src + 0, ch, cw, 0, // dst, Y-flipped + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prev_draw); + return true; +} + +#else // other platforms - stub + +LLCEFAccelInterop::~LLCEFAccelInterop() { shutdown(); } +bool LLCEFAccelInterop::init() { return false; } +void LLCEFAccelInterop::shutdown() {} +bool LLCEFAccelInterop::setStableTexture(unsigned long long, int, int, int, unsigned int, unsigned long long, unsigned long long, int, + int, const unsigned long long*, const unsigned int*, const unsigned long long*) { return false; } +bool LLCEFAccelInterop::blitTo(unsigned int, int, int) { return false; } + +#endif + +// static - platform-agnostic capability preflight. Probes the interop once (it +// needs a current GL/EGL context, which exists on the main thread when media +// sources are created) so callers only request accelerated paint from the plugin +// when the consumer can actually bind it - otherwise the plugin would emit GPU +// frames the viewer drops, leaving the media blank with no CPU fallback. +bool LLCEFAccelInterop::isSupported() +{ + static bool s_probed = false; + static bool s_supported = false; + if (!s_probed) + { + LLCEFAccelInterop probe; + s_supported = probe.init(); + probe.shutdown(); + s_probed = true; + } + return s_supported; +} diff --git a/indra/newview/llcefaccelinterop.h b/indra/newview/llcefaccelinterop.h new file mode 100644 index 0000000000..8e9cbc0986 --- /dev/null +++ b/indra/newview/llcefaccelinterop.h @@ -0,0 +1,80 @@ +/** + * @file llcefaccelinterop.h + * @brief Viewer-side consumer of the CEF accelerated-paint shared texture. + * + * Opens the plugin's keyed-mutex shared texture (delivered once per size as a + * duplicated NT handle), copies each frame into a viewer-owned texture under the + * mutex, and blits that into a media GL texture - no CPU round-trip. Windows + * only (D3D11 + WGL_NV_DX_interop2); a stub elsewhere. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#ifndef LL_LLCEFACCELINTEROP_H +#define LL_LLCEFACCELINTEROP_H + +// Bridges the plugin's shared GPU texture to a viewer media GL texture with no +// CPU copy. All calls must be made on a thread holding the viewer's GL context. +class LLCEFAccelInterop +{ +public: + LLCEFAccelInterop() = default; + ~LLCEFAccelInterop(); + + // Create the D3D11 device + WGL interop device. False if unsupported (caller + // should fall back to the CPU paint path). + bool init(); + void shutdown(); + bool valid() const { return mValid; } + + // True if this platform can actually consume the plugin's GPU shared texture. + // Probed once (requires a current GL context). Callers should only request + // accelerated paint from the plugin when this is true; otherwise the plugin + // would produce GPU frames the viewer cannot bind, leaving the media blank. + static bool isSupported(); + + // (Re)bind the plugin's shared frame. `handle` is the platform shared-texture + // handle on Windows (NT handle) / macOS (IOSurfaceID). On Linux the frame is a + // dma-buf: `handle` is plane 0's fd number in process `src_pid` (opened via + // /proc//fd/) and the layout is given by format / modifier plus + // the per-plane table. A tiled/compressed (CCS) buffer has more than one plane; + // pass them all via plane_fds / plane_strides / plane_offsets (each of length + // plane_count) or the import fails and the surface renders grey. When the plane + // arrays are null, the single `stride`/`offset` describe plane 0. Everything + // past `format` is ignored on Windows/macOS. + bool setStableTexture(unsigned long long handle, int width, int height, + int format = 0, unsigned int stride = 0, + unsigned long long offset = 0, unsigned long long modifier = 0, + int src_pid = 0, int plane_count = 1, + const unsigned long long* plane_fds = nullptr, + const unsigned int* plane_strides = nullptr, + const unsigned long long* plane_offsets = nullptr); + + // Copy the latest plugin frame into dst_tex (a GL_RGBA8 texture). The data is + // BGRA-ordered, so the caller should sample dst_tex with an R<->B swizzle. + // Returns true if dst_tex was updated. + bool blitTo(unsigned int dst_tex, int width, int height); + +private: + bool mValid = false; + void* mImpl = nullptr; // platform state (Windows only) +}; + +#endif // LL_LLCEFACCELINTEROP_H diff --git a/indra/newview/llcefsurfacereceiver.cpp b/indra/newview/llcefsurfacereceiver.cpp new file mode 100644 index 0000000000..990e3afc11 --- /dev/null +++ b/indra/newview/llcefsurfacereceiver.cpp @@ -0,0 +1,358 @@ +/** + * @file llcefsurfacereceiver.cpp + * @brief Viewer-side mach-port receiver for CEF accelerated-paint IOSurfaces. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llcefsurfacereceiver.h" + +// static +LLCEFSurfaceReceiver& LLCEFSurfaceReceiver::instance() +{ + static LLCEFSurfaceReceiver sInstance; + return sInstance; +} + +#if LL_DARWIN + +#include +#include +#include +#include + +#include +#include + +// Shared wire format with the producer (media_plugin_cef.cpp). MUST stay in sync. +// A complex message carrying one IOSurface mach port plus inline frame metadata. +typedef struct +{ + mach_msg_header_t header; + mach_msg_body_t body; // descriptor count = 1 + mach_msg_port_descriptor_t surface; // the IOSurfaceCreateMachPort() right + int32_t accel_id; + int32_t width; + int32_t height; + int32_t format; +} CefSurfaceSendMsg; + +// Receive needs room for the kernel-appended trailer. +typedef struct +{ + CefSurfaceSendMsg msg; + mach_msg_trailer_t trailer; +} CefSurfaceRecvMsg; + +namespace +{ + struct Receiver + { + mach_port_t port = MACH_PORT_NULL; // our receive right (also the service) + bool started = false; + bool failed = false; // bootstrap registration refused; give up + // Newest pending IOSurface mach port per accel id (a send right we own + // until it is looked up or superseded). + std::map latest; + + // Allocate the receive right and register it with the bootstrap server + // under the per-viewer name the plugin derives from host_pid. + bool ensureStarted() + { + if (started) return true; + if (failed) return false; + + kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port); + if (kr != KERN_SUCCESS) + { + LL_WARNS("Media") << "accel surface receiver: mach_port_allocate failed (" << kr << ")" << LL_ENDL; + failed = true; + return false; + } + // A send right (same name) for bootstrap to hand to look_up callers. + mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND); + + char name[128]; + snprintf(name, sizeof(name), "org.alchemyviewer.cefsurface.%d", (int)getpid()); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + kr = bootstrap_register(bootstrap_port, name, port); +#pragma clang diagnostic pop + if (kr != KERN_SUCCESS) + { + LL_WARNS("Media") << "accel surface receiver: bootstrap_register(" << name + << ") failed (" << kr << "); accelerated paint cannot share the surface" << LL_ENDL; + mach_port_mod_refs(mach_task_self(), port, MACH_PORT_RIGHT_RECEIVE, -1); + port = MACH_PORT_NULL; + failed = true; + return false; + } + + LL_INFOS("Media") << "accel surface receiver: registered " << name << LL_ENDL; + started = true; + return true; + } + + // Non-blocking drain: pull every queued message, keeping only the newest + // surface port per accel id (deallocating superseded ones). + void drain() + { + while (true) + { + CefSurfaceRecvMsg rcv; + kern_return_t kr = mach_msg(&rcv.msg.header, MACH_RCV_MSG | MACH_RCV_TIMEOUT, + 0, sizeof(rcv), port, 0, MACH_PORT_NULL); + if (kr != KERN_SUCCESS) + { + break; // MACH_RCV_TIMED_OUT (empty) or error - stop + } + if (!(rcv.msg.header.msgh_bits & MACH_MSGH_BITS_COMPLEX) || + rcv.msg.body.msgh_descriptor_count < 1) + { + continue; // malformed; nothing to release + } + + mach_port_t surf_port = rcv.msg.surface.name; + int id = rcv.msg.accel_id; + + auto it = latest.find(id); + if (it != latest.end() && it->second != MACH_PORT_NULL) + { + mach_port_deallocate(mach_task_self(), it->second); // drop superseded + } + latest[id] = surf_port; + } + } + }; + + Receiver& rcv() + { + static Receiver r; + return r; + } +} + +void LLCEFSurfaceReceiver::ensureStarted() +{ + rcv().ensureStarted(); +} + +void* LLCEFSurfaceReceiver::takeLatest(int accel_id) +{ + Receiver& r = rcv(); + if (!r.ensureStarted()) + { + return nullptr; + } + r.drain(); + + auto it = r.latest.find(accel_id); + if (it == r.latest.end() || it->second == MACH_PORT_NULL) + { + return nullptr; // no new frame for this media + } + + mach_port_t surf_port = it->second; + r.latest.erase(it); + + IOSurfaceRef surf = IOSurfaceLookupFromMachPort(surf_port); + mach_port_deallocate(mach_task_self(), surf_port); // release our send right + return surf; // +1 retained (or null); ownership transferred to caller +} + +// macOS shares the frame as an IOSurface (above), not as dma-buf fds. +void LLCEFSurfaceReceiver::DmabufFrame::closeFds() {} +bool LLCEFSurfaceReceiver::takeLatestDmabuf(int, DmabufFrame&) { return false; } + +#elif LL_LINUX // dma-buf fds handed over via an AF_UNIX datagram + SCM_RIGHTS + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + // Wire header shared with the producer (media_plugin_cef.cpp). MUST stay in + // sync. The dma-buf plane fds ride alongside as SCM_RIGHTS ancillary data. + struct CefDmabufMsg + { + int32_t accel_id; + int32_t plane_count; + int32_t width; + int32_t height; + int32_t format; + uint32_t stride[4]; + uint64_t offset[4]; + uint64_t modifier; + }; + + // Abstract-namespace socket address from the viewer pid (the Linux analog of + // the macOS bootstrap name org.alchemyviewer.cefsurface.). Abstract + // sockets need no filesystem entry and vanish when the socket closes. + socklen_t makeAddr(struct sockaddr_un& addr, int pid) + { + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + int n = snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, + "org.alchemyviewer.cefsurface.%d", pid); // [0] stays NUL = abstract + return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + n); + } + + struct Receiver + { + int sock = -1; + bool started = false; + bool failed = false; + // Newest pending frame per accel id (its fds are open + owned by us until + // taken or superseded). + std::map latest; + + bool ensureStarted() + { + if (started) return true; + if (failed) return false; + + sock = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sock < 0) { failed = true; return false; } + + struct sockaddr_un addr; + socklen_t len = makeAddr(addr, (int)getpid()); + if (bind(sock, (struct sockaddr*)&addr, len) != 0) + { + LL_WARNS("Media") << "accel surface receiver: bind failed errno=" << errno + << "; accelerated paint cannot share the frame" << LL_ENDL; + ::close(sock); sock = -1; failed = true; return false; + } + int rcvbuf = 4 * 1024 * 1024; // hold a few queued frames across a slow tick + setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + + LL_INFOS("Media") << "accel surface receiver: listening (abstract org.alchemyviewer.cefsurface." + << (int)getpid() << ")" << LL_ENDL; + started = true; + return true; + } + + // Non-blocking drain: pull every queued datagram, keeping only the newest + // frame per accel id (closing the fds of superseded / malformed ones). + void drain() + { + if (sock < 0) return; + while (true) + { + CefDmabufMsg hdr; + struct iovec iov = { &hdr, sizeof(hdr) }; + char cbuf[CMSG_SPACE(sizeof(int) * 4)]; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cbuf; + msg.msg_controllen = sizeof(cbuf); + + ssize_t r = recvmsg(sock, &msg, MSG_DONTWAIT | MSG_CMSG_CLOEXEC); + if (r < 0) break; // EAGAIN (empty) or error - stop + + // Collect any fds first so a malformed frame never leaks them. + int fds[4]; int nfds = 0; + for (struct cmsghdr* c = CMSG_FIRSTHDR(&msg); c; c = CMSG_NXTHDR(&msg, c)) + { + if (c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) + { + int cnt = (int)((c->cmsg_len - CMSG_LEN(0)) / sizeof(int)); + int* p = (int*)CMSG_DATA(c); + for (int i = 0; i < cnt; ++i) + (nfds < 4) ? (void)(fds[nfds++] = p[i]) : (void)::close(p[i]); + } + } + + bool bad = (r != (ssize_t)sizeof(hdr)) || + (msg.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) || + nfds <= 0 || nfds != hdr.plane_count; + if (bad) + { + for (int i = 0; i < nfds; ++i) ::close(fds[i]); + continue; + } + + LLCEFSurfaceReceiver::DmabufFrame f; + f.plane_count = nfds; + f.width = hdr.width; f.height = hdr.height; f.format = hdr.format; + f.modifier = hdr.modifier; + for (int i = 0; i < nfds; ++i) + { + f.fd[i] = fds[i]; + f.stride[i] = hdr.stride[i]; + f.offset[i] = hdr.offset[i]; + } + auto it = latest.find(hdr.accel_id); + if (it != latest.end()) it->second.closeFds(); // drop superseded + latest[hdr.accel_id] = f; + } + } + }; + + Receiver& rcv() { static Receiver r; return r; } +} + +void LLCEFSurfaceReceiver::DmabufFrame::closeFds() +{ + for (int i = 0; i < plane_count; ++i) + if (fd[i] >= 0) { ::close(fd[i]); fd[i] = -1; } +} + +void LLCEFSurfaceReceiver::ensureStarted() { rcv().ensureStarted(); } + +void* LLCEFSurfaceReceiver::takeLatest(int) { return nullptr; } // macOS-only path + +bool LLCEFSurfaceReceiver::takeLatestDmabuf(int accel_id, DmabufFrame& out) +{ + Receiver& r = rcv(); + if (!r.ensureStarted()) + { + return false; + } + r.drain(); + + auto it = r.latest.find(accel_id); + if (it == r.latest.end() || it->second.plane_count <= 0) + { + return false; // no new frame for this media + } + out = it->second; // transfer fd ownership to the caller + r.latest.erase(it); + return true; +} + +#else // other platforms: no side-channel handoff (Windows shares via NT handle) + +void LLCEFSurfaceReceiver::ensureStarted() {} +void* LLCEFSurfaceReceiver::takeLatest(int) { return nullptr; } +void LLCEFSurfaceReceiver::DmabufFrame::closeFds() {} +bool LLCEFSurfaceReceiver::takeLatestDmabuf(int, DmabufFrame&) { return false; } + +#endif diff --git a/indra/newview/llcefsurfacereceiver.h b/indra/newview/llcefsurfacereceiver.h new file mode 100644 index 0000000000..84a24215a9 --- /dev/null +++ b/indra/newview/llcefsurfacereceiver.h @@ -0,0 +1,89 @@ +/** + * @file llcefsurfacereceiver.h + * @brief Viewer-side mach-port receiver for CEF accelerated-paint IOSurfaces (macOS). + * + * CEF's accelerated-paint IOSurface is shared between its GPU and browser + * processes via mach ports, NOT a global IOSurfaceID, so the browser-side plugin + * cannot hand it to the viewer through the socket/LLSD channel (which carries no + * mach rights) and a cross-process IOSurfaceLookup(id) fails. Instead the plugin + * sends an IOSurfaceCreateMachPort() right over a mach channel rendezvous'd + * through the bootstrap server under a per-viewer name; this singleton owns the + * receive end and demuxes incoming surfaces by the per-media accel id. + * + * Non-macOS builds get a stub (those platforms share via NT handle / dma-buf). + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * 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 + * $/LicenseInfo$ + */ + +#ifndef LL_LLCEFSURFACERECEIVER_H +#define LL_LLCEFSURFACERECEIVER_H + +// Process-global receiver for accelerated-paint frames handed over by the CEF +// media plugin(s) over a side channel separate from the (TCP) LLSD message pipe. +// The rendezvous name is derived from this (viewer) process id, which the plugin +// already learns from the "init" message's host_pid, so no extra handshake field +// is needed. macOS uses a mach port carrying an IOSurface; Linux uses an AF_UNIX +// datagram socket carrying the dma-buf fds as SCM_RIGHTS ancillary data (a TCP +// socket cannot carry fds, and a dma-buf fd cannot be reopened via /proc). +class LLCEFSurfaceReceiver +{ +public: + static LLCEFSurfaceReceiver& instance(); + + // Register the receive endpoint if not already (idempotent). MUST be called + // independently of frame delivery: the plugin only starts producing once the + // endpoint exists, so waiting for the first frame to register would deadlock + // (no endpoint -> no frames -> never registered). No-op on non-mac/non-Linux. + void ensureStarted(); + + // macOS: drain all pending surface messages (non-blocking), keep only the + // newest surface per accel id, then hand off the newest for `accel_id`. + // Returns a +1-retained IOSurfaceRef (opaque pointer; caller takes ownership + // and must CFRelease / hand to code that does) or nullptr if no new frame. + void* takeLatest(int accel_id); + + // Linux: a dma-buf frame received via SCM_RIGHTS. The fds are open in THIS + // (viewer) process and owned by whoever takes the frame, which must close them + // (closeFds()) once imported. Pure data so it can cross to the interop without + // pulling unistd.h into this header. + struct DmabufFrame + { + int plane_count = 0; + int fd[4] = { -1, -1, -1, -1 }; + unsigned int stride[4] = { 0, 0, 0, 0 }; + unsigned long long offset[4] = { 0, 0, 0, 0 }; + unsigned long long modifier = 0; + int width = 0; + int height = 0; + int format = 0; + void closeFds(); // defined in the .cpp (Linux closes; elsewhere a no-op) + }; + + // Linux: drain pending dma-buf frames (non-blocking), keep only the newest per + // accel id, then hand off the newest for `accel_id`. Returns true and fills + // `out` (caller owns out.fd[] and must closeFds()), or false if no new frame. + // Always false on non-Linux. + bool takeLatestDmabuf(int accel_id, DmabufFrame& out); + +private: + LLCEFSurfaceReceiver() = default; +}; + +#endif // LL_LLCEFSURFACERECEIVER_H diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index d7bed460c2..2836a6fa09 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -366,7 +366,7 @@ bool LLSyntaxDefCache::writeCacheFile(const std::string &fileSpec, const LLSD& c bool binary(content_ref.isBinary()); std::ios_base::openmode mode(binary ? (std::ios_base::out | std::ios_base::binary) : std::ios_base::out); - std::ofstream file(fileSpec.c_str(), mode); + llofstream file(fileSpec, mode); if (!file.is_open()) { diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 381b1fa498..d70973df1f 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -65,6 +65,10 @@ #include "llviewertexture.h" #include "llviewertexturelist.h" #include "llviewerwindow.h" +#include "llcefaccelinterop.h" +#include "llcefsurfacereceiver.h" +#include "llrender.h" +#include "llgl.h" #include "llvoavatar.h" #include "llvoavatarself.h" #include "llvovolume.h" @@ -168,6 +172,12 @@ static LLViewerMedia::impl_list sViewerMediaImplList; static LLViewerMedia::impl_id_map sViewerMediaTextureIDMap; static LLTimer sMediaCreateTimer; static const F32 LLVIEWERMEDIA_CREATE_DELAY = 1.0f; +// Shared-CEF-daemon crash recovery: cap on consecutive re-init attempts before a +// daemon-mode media gives up, and the backoff schedule between them (so a daemon +// that crashes on every launch can't spin the viewer respawning it forever). +static const S32 DAEMON_RECOVERY_MAX_ATTEMPTS = 5; +static const F32 DAEMON_RECOVERY_BASE_DELAY = 2.0f; // seconds, multiplied by attempt # +static const F32 DAEMON_RECOVERY_MAX_DELAY = 30.0f; // seconds, backoff ceiling static F32 sGlobalVolume = 1.0f; static bool sForceUpdate = false; static LLUUID sOnlyAudibleTextureID = LLUUID::null; @@ -213,6 +223,19 @@ static bool sViewerMediaMuteListObserverInitialized = false; /*static*/ const char* LLViewerMedia::SHOW_MEDIA_WITHIN_PARCEL_SETTING = "MediaShowWithinParcel"; /*static*/ const char* LLViewerMedia::SHOW_MEDIA_OUTSIDE_PARCEL_SETTING = "MediaShowOutsideParcel"; +namespace +{ + // Per-viewer-instance CEF daemon rendezvous file: user-writable logs dir + // (never the read-only install dir) plus this viewer's PID so separate + // viewer instances don't share a daemon. newSourceFromMediaType() hands this + // exact path to setUseDaemon(); the cleanup in ~LLViewerMedia() recomputes + // it to remove the file the (force-killed) daemon cannot remove itself. + std::string cefDaemonRendezvousPath() + { + return gDirUtilp->getExpandedFilename(LL_PATH_LOGS, llformat("SLPluginCEF_%d.daemon", LLApp::getPid())); + } +} + LLViewerMedia::LLViewerMedia(): mAnyMediaShowing(false), mAnyMediaPlaying(false), @@ -231,6 +254,17 @@ LLViewerMedia::~LLViewerMedia() delete mSpareBrowserMediaSource; mSpareBrowserMediaSource = NULL; } + + // The shared CEF daemon lives in this viewer's job object, so it is + // force-killed on exit and never reaches its own rendezvous cleanup. Remove + // the rendezvous (and any stale spawn lock) here. No-op if daemon mode was + // never used or the files are already gone. + if (gDirUtilp) + { + const std::string rv = cefDaemonRendezvousPath(); + LLFile::remove(rv); + LLFile::remove(rv + ".lock"); + } } // static @@ -1677,6 +1711,13 @@ LLViewerMediaImpl::~LLViewerMediaImpl() { destroyMediaSource(); + if (mAccelInterop) + { + mAccelInterop->shutdown(); + delete mAccelInterop; + mAccelInterop = nullptr; + } + LLViewerMediaTexture::removeMediaImplFromTexture(mTextureId) ; setTextureID(); @@ -1747,6 +1788,17 @@ void LLViewerMediaImpl::destroyMediaSource() { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA; mNeedsNewTexture = true; + // The plugin's shared texture is going away; tear the interop down fully and + // force a fresh bind when the next media source delivers a handle (even if the + // value is reused). Keeping the old interop alive would let a stray accelerated + // dirty event blit stale/invalid content from the previous source. + mAccelBoundHandle = 0; + if (mAccelInterop) + { + mAccelInterop->shutdown(); + delete mAccelInterop; + mAccelInterop = nullptr; + } // Tell the viewer media texture it's no longer active LLViewerMediaTexture* oldImage = LLViewerTextureManager::findMediaTexture( mTextureId ); @@ -1815,25 +1867,42 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ } #endif - std::string launcher_name = gDirUtilp->getLLPluginLauncher(); - std::string plugin_name = gDirUtilp->getLLPluginFilename(plugin_basename); + // Each plugin is its own host executable now, named exactly for the + // plugin (e.g. media_plugin_cef) and launched directly - there is no + // generic SLPlugin shell and no dlopen'd plugin library. The CEF host's + // daemon/sandbox behaviour is selected at runtime (see setUseDaemon and + // the ALCef* settings), not by choosing a different executable. + std::string launcher_name = gDirUtilp->getLLPluginFilename(plugin_basename); std::string user_data_path_cache = gDirUtilp->getCacheDir(false); user_data_path_cache += gDirUtilp->getDirDelimiter(); - // See if the plugin executable exists + // See if the plugin host executable exists if (!LLFile::isfile(launcher_name)) { - LL_WARNS_ONCE("Media") << "Couldn't find launcher at " << launcher_name << LL_ENDL; - } - else if (!LLFile::isfile(plugin_name)) - { - LL_WARNS_ONCE("Media") << "Couldn't find plugin at " << plugin_name << LL_ENDL; + LL_WARNS_ONCE("Media") << "Couldn't find plugin host at " << launcher_name << LL_ENDL; } else { media_source = new LLPluginClassMedia(owner); media_source->setSize(default_width, default_height); + // Route CEF media through the shared daemon host when enabled. The + // rendezvous file lives in a user-writable runtime dir (the logs + // dir) - never the install/plugin dir, which may be read-only - and + // carries this viewer's PID so separate viewer instances do not + // share a daemon. Computed once and used by every CEF tab here. + const bool use_daemon = (plugin_basename == "media_plugin_cef" && + gSavedSettings.getBOOL("ALCefDaemonEnabled")); + media_source->setUseDaemon(use_daemon, use_daemon ? cefDaemonRendezvousPath() : std::string()); + + // Zero-copy GPU paint: deliver shared-texture handles instead of CPU + // pixels (the plugin duplicates them into this process). Only request + // it when the consumer-side interop is actually available on this + // platform - otherwise the plugin would emit GPU frames the viewer + // cannot bind, leaving the media blank with no CPU fallback. + media_source->setUseAcceleratedPaint(plugin_basename == "media_plugin_cef" && + gSavedSettings.getBOOL("ALCefAcceleratedPaint") && + LLCEFAccelInterop::isSupported()); std::string user_data_path_cef_log = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "cef.log"); media_source->setUserDataPath(user_data_path_cache, gDirUtilp->getUserName(), user_data_path_cef_log); media_source->setLanguageCode(LLUI::getLanguage()); @@ -1874,8 +1943,21 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ media_source->setTarget(target); +#if LL_LINUX + // Tell the CEF plugin which windowing backend the viewer is on so + // its Ozone platform (X11 vs Wayland) matches ours, rather than the + // plugin guessing from its own subprocess environment. + if (gViewerWindow && gViewerWindow->getWindow()) + { + media_source->setDisplayServer(gViewerWindow->getWindow()->getDisplayServer()); + } +#endif + const std::string plugin_dir = gDirUtilp->getLLPluginDir(); - if (media_source->init(launcher_name, plugin_dir, plugin_name, gSavedSettings.getBOOL("PluginAttachDebuggerToPlugins"))) + // plugin_dir/plugin_basename are vestigial now (the host statically + // links its plugin); they ride along in the load_plugin message only + // to satisfy its non-empty contract. + if (media_source->init(launcher_name, plugin_dir, plugin_basename, gSavedSettings.getBOOL("PluginAttachDebuggerToPlugins"))) { return media_source; } @@ -2915,6 +2997,18 @@ bool LLViewerMediaImpl::canNavigateBack() void LLViewerMediaImpl::update() { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA; //LL_RECORD_BLOCK_TIME(FTM_MEDIA_DO_UPDATE); + + // Shared CEF daemon crash recovery: once the backoff has elapsed, clear the + // failure latch so the normal load path below recreates the source (which + // respawns or reconnects the daemon). isForcedUnloaded() pins mPriority to + // PRIORITY_UNLOADED while mMediaSourceFailed is set, so this is what lets the + // tab come back after a daemon crash. + if (mDaemonRecoveryPending && mDaemonRecoveryTimer.hasExpired()) + { + mDaemonRecoveryPending = false; + mMediaSourceFailed = false; + } + if(mMediaSource == NULL) { if(mPriority == LLPluginClassMedia::PRIORITY_UNLOADED) @@ -2980,6 +3074,26 @@ void LLViewerMediaImpl::update() return; } + // Zero-copy paint: the plugin delivers a GPU shared texture instead of CPU + // pixels, so this media never uses the shm/setSubImage upload path below. + // Pull the latest frame straight into the media GL texture on this (main) + // thread and we're done. + if (mMediaSource->getUseAcceleratedPaint()) + { +#if LL_DARWIN || LL_LINUX + // Bring up the surface side channel up front (not gated on a frame): the + // plugin only starts producing once our receive endpoint exists (mach port + // on macOS, AF_UNIX socket on Linux), so waiting for the first frame to + // register would deadlock. + LLCEFSurfaceReceiver::instance().ensureStarted(); +#endif + if (!mSuspendUpdates && mVisible && mMediaSource->getAcceleratedPaintDirty()) + { + updateAcceleratedTexture(); + } + return; + } + if(!mMediaSource->textureValid()) { return; @@ -3100,13 +3214,19 @@ void LLViewerMediaImpl::doMediaTexUpdate(LLViewerMediaTexture* media_tex, U8* da // -Cosmic,2023-04-04 // Allocate GL texture based on LLImageRaw but do NOT copy to GL LLGLuint tex_name = 0; - if (!media_tex->createGLTexture(0, raw, 0, true, LLGLTexture::OTHER, true, &tex_name)) { - LL_WARNS("Media") << "Failed to create media texture" << LL_ENDL; - } + // Phase 0 baseline: this GL allocate + CPU->GPU upload is exactly the + // per-surface cost the accelerated-paint (shared-texture) path removes, + // so isolate it under its own zone for before/after comparison. + LL_PROFILE_ZONE_NAMED_CATEGORY_MEDIA("media texUpload"); + if (!media_tex->createGLTexture(0, raw, 0, true, LLGLTexture::OTHER, true, &tex_name)) + { + LL_WARNS("Media") << "Failed to create media texture" << LL_ENDL; + } - // copy just the subimage covered by the image raw to GL - media_tex->setSubImage(data, data_width, data_height, x_pos, y_pos, width, height, tex_name); + // copy just the subimage covered by the image raw to GL + media_tex->setSubImage(data, data_width, data_height, x_pos, y_pos, width, height, tex_name); + } if (sync) { @@ -3127,6 +3247,124 @@ void LLViewerMediaImpl::updateImagesMediaStreams() { } +////////////////////////////////////////////////////////////////////////////////////////// +// Zero-copy paint consumer. Bring the plugin's GPU shared texture straight into +// the media GL texture with no CPU round-trip. Main thread (has the GL context). +bool LLViewerMediaImpl::updateAcceleratedTexture() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA; + + // Bring up the interop first so a failure here doesn't consume the frame; the + // plugin's handle is persistent and we retry next frame. + if (!mAccelInterop) + { + mAccelInterop = new LLCEFAccelInterop(); + if (!mAccelInterop->init()) + { + delete mAccelInterop; + mAccelInterop = nullptr; + return false; + } + } + + mMediaSource->clearAcceleratedPaintDirty(); +#if LL_DARWIN + // macOS: CEF's IOSurface is shared via a mach port (no cross-process global + // id), so the surface arrives out-of-band through the mach receiver, demuxed + // by this media's accel id. Drain the newest and (re)bind it; the interop + // takes ownership of the +1-retained IOSurfaceRef (and releases it on a + // failed bind). If no new surface this frame, keep the current binding. + void* surf = LLCEFSurfaceReceiver::instance().takeLatest(mMediaSource->getAccelId()); + if (surf) + { + if (!mAccelInterop->setStableTexture((unsigned long long)(uintptr_t)surf, + mMediaSource->getAcceleratedPaintWidth(), + mMediaSource->getAcceleratedPaintHeight(), + mMediaSource->getAcceleratedPaintFormat(), + 0, 0, 0, 0)) + { + return false; // import failed; don't blit a stale/invalid binding + } + } +#elif LL_WINDOWS + // The handle is persistent (re)sent only on (re)create. (Re)bind the interop + // when it differs from what we have bound; only advance mAccelBoundHandle on a + // successful bind so a transient failure is retried with the same handle. + unsigned long long handle = mMediaSource->getAcceleratedPaintHandle(); + if (handle != 0 && handle != mAccelBoundHandle) + { + if (mAccelInterop->setStableTexture(handle, + mMediaSource->getAcceleratedPaintWidth(), + mMediaSource->getAcceleratedPaintHeight(), + mMediaSource->getAcceleratedPaintFormat())) + { + mAccelBoundHandle = handle; + } + else + { + return false; + } + } +#else // LL_LINUX + // CEF's dma-buf fds arrive out-of-band via the SCM_RIGHTS side channel (a + // dma-buf fd cannot cross the process boundary through the LLSD/TCP channel, + // nor be reopened via /proc), demuxed by this media's accel id. Take the + // newest frame, import its planes directly (the fds are open in this process), + // then close them. Keep the current binding if no new frame arrived this tick. + LLCEFSurfaceReceiver::DmabufFrame frame; + if (LLCEFSurfaceReceiver::instance().takeLatestDmabuf(mMediaSource->getAccelId(), frame)) + { + unsigned long long plane_fds[4] = {}; + unsigned int plane_strides[4] = {}; + unsigned long long plane_offsets[4] = {}; + for (int i = 0; i < frame.plane_count && i < 4; ++i) + { + plane_fds[i] = (unsigned long long)frame.fd[i]; + plane_strides[i] = frame.stride[i]; + plane_offsets[i] = frame.offset[i]; + } + // The interop imports the fds directly (no /proc); we own them and close + // them right after - EGL keeps its own reference once the image is made. + bool imported = mAccelInterop->setStableTexture(plane_fds[0], + frame.width, frame.height, frame.format, + plane_strides[0], plane_offsets[0], frame.modifier, + 0, frame.plane_count, plane_fds, plane_strides, plane_offsets); + frame.closeFds(); + if (!imported) + { + return false; // import failed; don't blit a stale/invalid binding + } + } +#endif + + LLViewerMediaTexture* media_tex = updateMediaImage(); + if (!media_tex || !media_tex->getGLTexture()) + { + return false; + } + + U32 tex_name = media_tex->getGLTexture()->getTexName(); + if (!tex_name) + { + return false; + } + + // The interop blit reads the shared texture through a framebuffer, which + // both samples it in the correct channel order and flips it to bottom-up, so + // no swizzle / coord fix-up is needed here. + if (!mAccelInterop->blitTo(tex_name, media_tex->getWidth(), media_tex->getHeight())) + { + return false; + } + + // Only switch the prim face onto the media texture once it actually holds a + // blitted frame. The CPU path waits on textureValid() for the same reason; + // the accelerated path skips that check, so presenting before the first blit + // would flash the empty placeholder texture (the grey-before-media flicker). + media_tex->setPlaying(true); + return true; +} + ////////////////////////////////////////////////////////////////////////////////////////// LLViewerMediaTexture* LLViewerMediaImpl::updateMediaImage() { @@ -3410,6 +3648,24 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla // Reset the last known state of the media to defaults. resetPreviousMediaState(); + // Shared CEF daemon: one daemon crash drops every tab at once. Rather + // than leaving all of them permanently failed, schedule a bounded, + // backed-off re-init for daemon-mode media. The first impl to recreate + // its source wins the spawn lock and respawns the daemon + // (discover-or-spawn); the rest reconnect to it as new tabs. The retry + // cap + backoff keep a daemon that crashes on launch from spinning. + if (plugin && plugin->getUseDaemon() && mDaemonRecoveryAttempts < DAEMON_RECOVERY_MAX_ATTEMPTS) + { + mDaemonRecoveryAttempts++; + F32 delay = llmin(DAEMON_RECOVERY_BASE_DELAY * (F32)mDaemonRecoveryAttempts, DAEMON_RECOVERY_MAX_DELAY); + mDaemonRecoveryPending = true; + mDaemonRecoveryTimer.reset(); + mDaemonRecoveryTimer.setTimerExpirySec(delay); + LL_WARNS("Media") << "CEF daemon tab failed; scheduling recovery attempt " + << mDaemonRecoveryAttempts << "/" << DAEMON_RECOVERY_MAX_ATTEMPTS + << " in " << delay << "s" << LL_ENDL; + } + LLSD args; args["PLUGIN"] = LLMIMETypes::implType(mCurrentMimeType); // SJB: This is getting called every frame if the plugin fails to load, continuously respawining the alert! @@ -3452,6 +3708,10 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla { LL_DEBUGS("Media") << "MEDIA_EVENT_NAVIGATE_COMPLETE, uri is: " << plugin->getNavigateURI() << LL_ENDL; + // A page finished loading: the (possibly just-respawned) daemon tab is + // healthy again, so clear the crash-recovery backoff counter. + mDaemonRecoveryAttempts = 0; + std::string url = plugin->getNavigateURI(); if(getNavState() == MEDIANAVSTATE_BEGUN) { diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h index a3cb9ec93e..d8176f1165 100644 --- a/indra/newview/llviewermedia.h +++ b/indra/newview/llviewermedia.h @@ -50,6 +50,7 @@ class LLViewerMediaTexture; class LLMediaEntry; class LLVOVolume; class LLMimeDiscoveryResponder; +class LLCEFAccelInterop; typedef LLPointer viewer_media_t; /////////////////////////////////////////////////////////////////////////////// @@ -208,6 +209,10 @@ class LLViewerMediaImpl void createMediaSource(); void destroyMediaSource(); + // Zero-copy paint: pull the plugin's GPU shared texture into the media + // texture (no CPU upload). Main thread only. No-op / returns false if the + // platform interop isn't available. + bool updateAcceleratedTexture(); void setMediaType(const std::string& media_type); bool initializeMedia(const std::string& mime_type); bool initializePlugin(const std::string& media_type); @@ -480,6 +485,19 @@ class LLViewerMediaImpl bool mNavigateRediscoverType; bool mNavigateServerRequest; bool mMediaSourceFailed; + // Shared-CEF-daemon crash recovery: a daemon crash drops every tab at once, + // so instead of leaving each media permanently failed we re-init on a backoff + // (which respawns the daemon and reconnects the tab). Bounded by a retry cap + // that resets on a clean load. Only used for daemon-mode CEF media. + S32 mDaemonRecoveryAttempts = 0; + bool mDaemonRecoveryPending = false; + LLTimer mDaemonRecoveryTimer; + // Zero-copy paint consumer (created lazily on the first accelerated frame); + // owns the D3D/GL interop that aliases the plugin's shared texture. + LLCEFAccelInterop* mAccelInterop = nullptr; + // The stable-texture handle currently bound into the interop; re-bind only + // when the plugin's persistent handle differs from this. + unsigned long long mAccelBoundHandle = 0; F32 mRequestedVolume; F32 mPreviousVolume; bool mIsMuted; diff --git a/indra/newview/tests/lldir_stub.cpp b/indra/newview/tests/lldir_stub.cpp index ff4a4daf6d..96ec1d33e3 100644 --- a/indra/newview/tests/lldir_stub.cpp +++ b/indra/newview/tests/lldir_stub.cpp @@ -51,7 +51,6 @@ class LLDir_stub : public LLDir std::string getCurPath() override { return "CUR_PATH_FROM_LLDIR"; } bool fileExists(const std::string &filename) const override { return false; } - std::string getLLPluginLauncher() override { return ""; } std::string getLLPluginFilename(std::string base_name) override { return ""; } }; diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 014765f5a3..c9a5599d2e 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -513,22 +513,22 @@ def construct(self): with self.prefix(src_dst=self.get_dst_prefix()): self.path(self.final_exe()) self.path("*.dll") - self.path("SLPlugin.exe") - - # Plugins are only built in non-debug builds on windows - if self.args['buildtype'].lower() != 'debug': - with self.prefix(src_dst=os.path.join(self.get_dst_prefix(), 'llplugin')): - # Plugin and dependency DLL files - self.path("*.dll") - # CEF files - self.path("*.exe") - self.path("*.pak") - self.path("*.bin") - self.path("*.json") - # VLC files - self.path("*.dat") - self.path("locales") - self.path("plugins") + + with self.prefix(src_dst=os.path.join(self.get_dst_prefix(), 'llplugin')): + # Per-plugin host executables (media_plugin_*.exe, including the + # renamed CEF bootstrap media_plugin_cef.exe) and their dependency + # DLLs (media_plugin_cef.dll, libcef.dll, ...). The *.exe / *.dll + # globs below pick them all up. + self.path("*.dll") + # CEF files + self.path("*.exe") + self.path("*.pak") + self.path("*.bin") + self.path("*.json") + # VLC files + self.path("*.dat") + self.path("locales") + self.path("plugins") self.path(src="licenses-win32.txt", dst="licenses.txt") self.path("featuretable.txt") @@ -901,9 +901,11 @@ def path_optional(src, dst): libfile_parent = self.get_dst_prefix() dylibs=[] - # our apps + # our apps - each media plugin is now its own host .app bundle + # (no SLPlugin.app launcher and no dlopen'd plugin dylib). executable_path = {} - embedded_apps = [ (os.path.join("llplugin", "slplugin"), "SLPlugin.app") ] + embedded_apps = [ (os.path.join("media_plugins", "cef"), "media_plugin_cef.app"), + (os.path.join("media_plugins", "libvlc"), "media_plugin_libvlc.app") ] for app_bld_dir, app in embedded_apps: self.path2basename(os.path.join(os.pardir, app_bld_dir, self.args['configuration']), @@ -911,14 +913,11 @@ def path_optional(src, dst): executable_path[app] = \ self.dst_path_of(os.path.join(app, "Contents", "MacOS")) - # Dullahan helper apps go inside SLPlugin.app + # The CEF framework + dullahan helper apps live in the CEF host + # bundle (media_plugin_cef.app), the only host that runs CEF. with self.prefix(dst=os.path.join( - "SLPlugin.app", "Contents", "Frameworks")): - # copy CEF plugin - self.path2basename("../media_plugins/cef/" + self.args['configuration'], - "media_plugin_cef.dylib") + "media_plugin_cef.app", "Contents", "Frameworks")): - # CEF framework and vlc libraries goes inside Contents/Frameworks. with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'lib')): self.path("Chromium Embedded Framework.framework") @@ -929,15 +928,12 @@ def path_optional(src, dst): self.path("DullahanHelper (Renderer).app") self.path("DullahanHelper (Plugin).app") - # copy LibVLC plugin - self.path2basename("../media_plugins/libvlc/" + self.args['configuration'], - "media_plugin_libvlc.dylib") - - # Copy libvlc + # libvlc + its runtime plugins live in the LibVLC host bundle. + with self.prefix(dst=os.path.join( + "media_plugin_libvlc.app", "Contents", "Frameworks")): with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'lib')): self.path( "libvlc*.dylib*" ) - # copy LibVLC plugins folder with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'plugins', 'vlc-bin'), dst="plugins"): self.path( "*.dylib" ) self.path( "plugins.dat" ) @@ -1122,10 +1118,6 @@ def construct(self): self.path("refresh_desktop_app_entry.sh") self.path("install.sh") - with self.prefix(dst="bin"): - with self.prefix(src=os.path.join(self.args['build'], os.pardir, 'llplugin', 'slplugin', self.args['configuration'])): - self.path("SLPlugin") - # recurses, packaged again self.path("res-sdl") @@ -1145,21 +1137,34 @@ def construct(self): # plugins with self.prefix(dst="bin/llplugin"): with self.prefix(src=os.path.join(self.args['build'], os.pardir, 'media_plugins')): + # Each media plugin is its own host executable now - there is no + # SLPlugin launcher and no dlopen'd plugin .so. with self.prefix(src=os.path.join('cef', self.args['configuration'])): - self.path("libmedia_plugin_cef.so") + self.path("media_plugin_cef") # Media plugins - LibVLC with self.prefix(src=os.path.join('libvlc', self.args['configuration'])): - self.path("libmedia_plugin_libvlc.so") + self.path("media_plugin_libvlc") # GStreamer 1.0 Media Plugin with self.prefix(src=os.path.join('gstreamer10', self.args['configuration'])): - self.path("libmedia_plugin_gstreamer10.so") + self.path("media_plugin_gstreamer10") # Media plugins - Example (useful for debugging - not shipped with release viewer) if self.channel_type() != 'release': with self.prefix(src=os.path.join('example', self.args['configuration'])): - self.path("libmedia_plugin_example.so") + self.path("media_plugin_example") + + with self.prefix(src=os.path.join(self.args['build'], os.pardir, 'dullahan', self.args['configuration'])): + self.path( "dullahan_host" ) + + with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'share', 'cef-bin', 'Release')): + self.path( "chrome-sandbox" ) + self.path( "v8_context_snapshot.bin" ) + self.path( "vk_swiftshader_icd.json") + + with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'share', 'cef-bin', 'Resources', 'locales'), dst="locales"): + self.path("*.pak") with self.prefix(src=os.path.join(self.args['build'], os.pardir, 'dullahan', self.args['configuration']), dst="bin"): self.path( "dullahan_host" ) @@ -1175,11 +1180,6 @@ def construct(self): self.path( "v8_context_snapshot.bin" ) self.path( "vk_swiftshader_icd.json") - with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'share', 'cef-bin', 'Release'), dst="bin"): - self.path( "chrome-sandbox" ) - self.path( "v8_context_snapshot.bin" ) - self.path( "vk_swiftshader_icd.json") - with self.prefix(src=os.path.join(self.args['vcpkg_dir'], 'share', 'cef-bin', 'Resources'), dst="lib"): self.path( "chrome_100_percent.pak" ) self.path( "chrome_200_percent.pak" ) diff --git a/indra/vcpkg.json b/indra/vcpkg.json index 200d9f825f..4940b4ca6b 100644 --- a/indra/vcpkg.json +++ b/indra/vcpkg.json @@ -175,7 +175,7 @@ "name": "glad", "platform": "windows | linux", "features": [ - "gl-api-21", + "gl-api-41", "loader" ] }, diff --git a/indra/vcpkg/ports/cef-bin/portfile.cmake b/indra/vcpkg/ports/cef-bin/portfile.cmake index 4b2a4a2070..40684abaee 100644 --- a/indra/vcpkg/ports/cef-bin/portfile.cmake +++ b/indra/vcpkg/ports/cef-bin/portfile.cmake @@ -1,38 +1,35 @@ -set(VCPKG_POLICY_ALLOW_EMPTY_FOLDERS enabled) set(VCPKG_POLICY_DLLS_IN_STATIC_LIBRARY enabled) -set(VCPKG_POLICY_MISMATCHED_NUMBER_OF_BINARIES enabled) set(VCPKG_FIXUP_MACHO_RPATH OFF) set(VCPKG_FIXUP_ELF_RPATH OFF) -set(VCPKG_BUILD_TYPE release) set(VCPKG_LIBRARY_LINKAGE static) if(VCPKG_TARGET_IS_WINDOWS) vcpkg_download_distfile(ARCHIVE - URLS "https://cef-builds.spotifycdn.com/cef_binary_148.0.9%2Bg0d9d52a%2Bchromium-148.0.7778.180_windows64_minimal.tar.bz2" + URLS "https://cef-builds.spotifycdn.com/cef_binary_149.0.6%2Bg0d0eeb6%2Bchromium-149.0.7827.201_windows64.tar.bz2" FILENAME "cef.${VERSION}.windows64.tar.bz2" - SHA512 f38218298e44e7fedfa55438101a0d6ff11f03a95c13664fb5747089cdf25ff08be9648d03ae3b899c7e8bc2e8db17035cfbaf762213c47e63ce6fcc59349d20 + SHA512 66200e0050721e2d5df68fcd737daa48080ae616cc063d0cc452d0059ae9e355b5c50182fe532120ba19d92a91559db1a8b68f7ae51d5e281f189474bdf8da52 ) elseif(VCPKG_TARGET_IS_OSX) if(VCPKG_OSX_ARCHITECTURES MATCHES "arm64") set(MACOS_ARCH_FLAG "-DPROJECT_ARCH=arm64") vcpkg_download_distfile(ARCHIVE - URLS "https://cef-builds.spotifycdn.com/cef_binary_148.0.9%2Bg0d9d52a%2Bchromium-148.0.7778.180_macosarm64_minimal.tar.bz2" + URLS "https://cef-builds.spotifycdn.com/cef_binary_149.0.6%2Bg0d0eeb6%2Bchromium-149.0.7827.201_macosarm64.tar.bz2" FILENAME "cef.${VERSION}.macosarm64.tar.bz2" - SHA512 3bb9f7cb5ee62bde7ac68e377341b7de3df64f32d8de1c2dbafd92da33146f8a5845ebbbe803e7660d921206e987ab651482e716357806cb30c9061a4f8684b5 + SHA512 93a547aa37226c4a7715a0b142249f1e654d97f21c2e6b63bf78e2d3c6ec6f7f3ea35bc34e319125972ae48670c78dfc636b996881a11e1dc8b34110bf9b29b0 ) else() set(MACOS_ARCH_FLAG "-DPROJECT_ARCH=x86_64") vcpkg_download_distfile(ARCHIVE - URLS "https://cef-builds.spotifycdn.com/cef_binary_148.0.9%2Bg0d9d52a%2Bchromium-148.0.7778.180_macosx64_minimal.tar.bz2" + URLS "https://cef-builds.spotifycdn.com/cef_binary_149.0.6%2Bg0d0eeb6%2Bchromium-149.0.7827.201_macosx64.tar.bz2" FILENAME "cef.${VERSION}.macosx64.tar.bz2" - SHA512 4280392cdddc7524fcf38ddabef81386083a73c624d412deb88f6888534690327a0713b30abc65dab2379f5e5a53f06c445f665405716762bc34df867bc54001 + SHA512 dcd06bdf58c19731f2eeedabc92ce8476510aea3074418d57f1b1c7a8e5753b848ca4bf5744906cca9ee3685af4f8c6b081d0c8350bb2a87b1366cf80b46f4a1 ) endif() elseif(VCPKG_TARGET_IS_LINUX) vcpkg_download_distfile(ARCHIVE - URLS "https://cef-builds.spotifycdn.com/cef_binary_148.0.9%2Bg0d9d52a%2Bchromium-148.0.7778.180_linux64_minimal.tar.bz2" + URLS "https://cef-builds.spotifycdn.com/cef_binary_149.0.6%2Bg0d0eeb6%2Bchromium-149.0.7827.201_linux64.tar.bz2" FILENAME "cef.${VERSION}.linux64.tar.bz2" - SHA512 197a1598f56b636db558d1f6dddfa3885f529571d354c0ff98dd27dbea56e73790f65ddece89e130bdc76611e374de8da5282e517980ffac98ef59037422e390 + SHA512 dab6871d532675cf63bf6935ba95fc9f8364eecbf3627ea28acf4a40b09cce466e40dd43095f4be89b7094a9c75311cac65bcfc56d30bde381308783644d6d5c ) endif() @@ -74,8 +71,14 @@ file(INSTALL "${CEF_SOURCE_PATH}/include/" DESTINATION "${CURRENT_PACKAGES_DIR}/ if(VCPKG_TARGET_IS_WINDOWS) file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/libcef_dll_wrapper/libcef_dll_wrapper.lib" DESTINATION "${CURRENT_PACKAGES_DIR}/lib") + if(NOT VCPKG_BUILD_TYPE) + file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/libcef_dll_wrapper/libcef_dll_wrapper.lib" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib") + endif() else() file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/libcef_dll_wrapper/libcef_dll_wrapper.a" DESTINATION "${CURRENT_PACKAGES_DIR}/lib") + if(NOT VCPKG_BUILD_TYPE) + file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/libcef_dll_wrapper/libcef_dll_wrapper.a" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib") + endif() endif() if(VCPKG_TARGET_IS_WINDOWS) @@ -89,10 +92,27 @@ if(VCPKG_TARGET_IS_WINDOWS) PATTERN "libcef.dll" EXCLUDE PATTERN "libcef.lib" EXCLUDE ) + if(NOT VCPKG_BUILD_TYPE) + file(INSTALL "${CEF_SOURCE_PATH}/Debug/libcef.dll" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/bin") + file(INSTALL "${CEF_SOURCE_PATH}/Debug/libcef.lib" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib") + file(INSTALL + DIRECTORY "${CEF_SOURCE_PATH}/Debug/" + DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}/Debug" + FILES_MATCHING + PATTERN "*.*" + PATTERN "libcef.dll" EXCLUDE + PATTERN "libcef.lib" EXCLUDE + ) + endif() + file(INSTALL "${CEF_SOURCE_PATH}/Resources" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") elseif(VCPKG_TARGET_IS_OSX) set(CEF_RELEASE_FRAMEWORK_DIR "${CURRENT_PACKAGES_DIR}/lib/Chromium Embedded Framework.framework") file(RENAME "${CEF_SOURCE_PATH}/Release/Chromium Embedded Framework.framework" "${CEF_RELEASE_FRAMEWORK_DIR}") + if(NOT VCPKG_BUILD_TYPE) + set(CEF_RELEASE_FRAMEWORK_DIR "${CURRENT_PACKAGES_DIR}/debug/lib/Chromium Embedded Framework.framework") + file(RENAME "${CEF_SOURCE_PATH}/Debug/Chromium Embedded Framework.framework" "${CEF_RELEASE_FRAMEWORK_DIR}") + endif() elseif(VCPKG_TARGET_IS_LINUX) file(INSTALL "${CEF_SOURCE_PATH}/Release/libcef.so" DESTINATION "${CURRENT_PACKAGES_DIR}/lib") file(INSTALL @@ -103,6 +123,18 @@ elseif(VCPKG_TARGET_IS_LINUX) PATTERN "chrome-sandbox" PATTERN "libcef.so" EXCLUDE ) + if(NOT VCPKG_BUILD_TYPE) + file(INSTALL "${CEF_SOURCE_PATH}/Debug/libcef.so" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib") + file(INSTALL + DIRECTORY "${CEF_SOURCE_PATH}/Debug/" + DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}/Debug" + FILES_MATCHING + PATTERN "*.*" + PATTERN "chrome-sandbox" + PATTERN "libcef.so" EXCLUDE + ) + endif() + file(INSTALL "${CEF_SOURCE_PATH}/Resources" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") endif() diff --git a/indra/vcpkg/ports/cef-bin/vcpkg.json b/indra/vcpkg/ports/cef-bin/vcpkg.json index 26cee1ace6..a960bf7cec 100644 --- a/indra/vcpkg/ports/cef-bin/vcpkg.json +++ b/indra/vcpkg/ports/cef-bin/vcpkg.json @@ -1,7 +1,6 @@ { "name": "cef-bin", - "version": "148.0.7778.180", - "port-version": 1, + "version": "149.0.7827.201", "dependencies": [ { "name": "vcpkg-cmake",