From ae2804361239dbdac26ee75753d0350df29086f3 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 27 Jun 2026 07:22:45 -0400 Subject: [PATCH 01/10] CEF daemon Phase 0: feature flags + media upload baseline zone Add ALCefDaemonEnabled / ALCefSandbox / ALCefAcceleratedPaint settings (all off by default, persisted) to gate the CEF media rework, and wrap the per-surface GL allocate + CPU->GPU upload in doMediaTexUpdate() in a named "media texUpload" Tracy zone. That upload is exactly the cost the accelerated-paint shared-texture path removes, so isolating it gives a before/after baseline. Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 1: split dullahan into shared runtime + per-browser impl Update dullahan submodule to 961ed95: hoist the process-global CEF runtime (CefApp, CefInitialize/Shutdown, command-line flags, message pump) into a reference-counted dullahan_runtime shared by every browser in the process, leaving dullahan_impl as one offscreen browser. This lets multiple browsers share a single CEF runtime - the foundation for the shared tab-manager daemon. Public dullahan API unchanged; dullahan + media_plugin_cef build and link on Windows. Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 1: opengl-example two-browser proof harness Update dullahan submodule to 004132a: the opengl-example now opens two browser tabs that share a single CEF runtime (switchable via Tabs menu / Ctrl+1/2), exercising the dullahan_runtime split at runtime - the standalone proof that multiple CEF browsers coexist in one process. Builds and links on Windows. Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 2a: static plugin entry-point path in LLPluginInstance Add the mechanism a dedicated single-plugin host needs to call its statically-linked plugin directly instead of dlopen()ing a plugin library. LLPluginInstance::setStaticInitFunction() registers a LLPluginInitEntryPoint; when set, load() calls it and skips the boost::dll dlopen/dlsym (and the Windows cwd hack) - ignoring the plugin dir/file. This avoids dlopen of large TLS-using libraries (CEF on Linux, where dlopen exhausts the static TLS block) and is a prerequisite for the Windows sandbox, which requires the host and its sub-processes to be a single executable image. slplugin's main now registers ll_get_static_plugin_init(); the generic SLPlugin links slplugin_generic.cpp which returns NULL, so its behaviour is unchanged (still dlopen the plugin named in load_plugin). A dedicated host (SLPluginCEF, next) will link a definition returning its plugin's &LLPluginInitEntryPoint. No behaviour change for the existing SLPlugin. Builds and links on Windows (llplugin + SLPlugin). Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 2b: SLPluginCEF dedicated static-link host Build a dedicated host executable that statically links the CEF media plugin (and CEF itself) instead of dlopen()ing media_plugin_cef: - compile the plugin sources once into an object library (media_plugin_cef_objs) consumed by both the existing loadable media_plugin_cef module (legacy dlopen path, unchanged) and the new host - SLPluginCEF = the generic slplugin host driver + slplugin_cef.cpp (returns &LLPluginInitEntryPoint) + the plugin objects, linking media_plugin_base/dullahan/ll::cef/llplugin/llmessage/llcommon. Via the Phase 2a static path, LLPluginInstance::load() calls the linked entry point directly - no dlopen of libcef. On Windows the host is staged into newview/.../llplugin next to libcef.dll so the statically-referenced CEF runtime resolves from its own directory. This removes the Linux static-TLS-block crash (libcef is now link-time, not dlopen'd) and gives the Windows sandbox the single-image host it needs. Still one process per media instance; only the host shape changes. The viewer does not launch SLPluginCEF yet (launcher selection is the next step). Builds and links on Windows (media_plugin_cef DLL + SLPluginCEF). Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 2b: launch SLPluginCEF for CEF media when enabled Add ALCefDedicatedHost (Boolean, off) and wire LLViewerMediaImpl:: newSourceFromMediaType to launch the SLPluginCEF host instead of the generic SLPlugin for media_plugin_cef when it is set. The dedicated host statically links the CEF plugin, so it avoids dlopen of libcef (the Linux static-TLS crash) and is the single-image host the Windows sandbox needs. plugin_name is still passed and validated; the static host ignores it. Falls back to the generic launcher (with a warning) if SLPluginCEF is not present, so enabling the flag without the host built degrades gracefully rather than breaking media. Still one process per media instance. Opt-in and restart-gated so the existing dlopen path stays the default until verified. llviewermedia.cpp compiles (single-file). Co-Authored-By: Claude Opus 4.8 --- indra/dullahan | 2 +- indra/llplugin/llplugininstance.cpp | 73 ++++++++++++------- indra/llplugin/llplugininstance.h | 11 +++ indra/llplugin/slplugin/CMakeLists.txt | 1 + indra/llplugin/slplugin/slplugin.cpp | 12 +++ indra/llplugin/slplugin/slplugin_generic.cpp | 38 ++++++++++ indra/media_plugins/cef/CMakeLists.txt | 55 +++++++++++++- indra/media_plugins/cef/slplugin_cef.cpp | 45 ++++++++++++ .../newview/app_settings/settings_alchemy.xml | 44 +++++++++++ indra/newview/llviewermedia.cpp | 40 ++++++++-- 10 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 indra/llplugin/slplugin/slplugin_generic.cpp create mode 100644 indra/media_plugins/cef/slplugin_cef.cpp diff --git a/indra/dullahan b/indra/dullahan index 32f74a46646..004132a0aa0 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit 32f74a4664699c405bce97dacecc2f4b9fdba4f8 +Subproject commit 004132a0aa0984cfb64acee269fe047a0f59ee8c diff --git a/indra/llplugin/llplugininstance.cpp b/indra/llplugin/llplugininstance.cpp index 99036d6dc2f..5fc255a4895 100644 --- a/indra/llplugin/llplugininstance.cpp +++ b/indra/llplugin/llplugininstance.cpp @@ -47,6 +47,14 @@ LLPluginInstanceMessageListener::~LLPluginInstanceMessageListener() */ const char *LLPluginInstance::PLUGIN_INIT_FUNCTION_NAME = "LLPluginInitEntryPoint"; +LLPluginInstance::pluginInitFunction LLPluginInstance::sStaticInitFunction = NULL; + +// static +void LLPluginInstance::setStaticInitFunction(pluginInitFunction func) +{ + sStaticInitFunction = func; +} + /** * Constructor. * @@ -76,48 +84,57 @@ LLPluginInstance::~LLPluginInstance() int LLPluginInstance::load(const std::string& plugin_dir, std::string &plugin_file) { pluginInitFunction init_function = NULL; - - if ( plugin_dir.length() ) - { -#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 + if (sStaticInitFunction) { - mDSOHandle.load(boost::dll::fs::path(plugin_file), - boost::dll::load_mode::rtld_now); + // Dedicated single-plugin host: the plugin is statically linked into + // this executable, so call its entry point directly and skip dlopen + // (and the cwd hack below) entirely. plugin_dir/plugin_file are ignored. + init_function = sStaticInitFunction; } - catch (const std::exception& e) + else { - LL_WARNS("Plugin") << "boost::dll load of " << plugin_file - << " failed: " << e.what() << LL_ENDL; - result = -1; - } + if ( plugin_dir.length() ) + { +#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 + }; - if(result == 0) - { try { - init_function = &mDSOHandle.get>( - PLUGIN_INIT_FUNCTION_NAME); + mDSOHandle.load(boost::dll::fs::path(plugin_file), + boost::dll::load_mode::rtld_now); } catch (const std::exception& e) { - LL_WARNS("Plugin") << "symbol lookup for " << PLUGIN_INIT_FUNCTION_NAME + LL_WARNS("Plugin") << "boost::dll load of " << plugin_file << " failed: " << e.what() << LL_ENDL; result = -1; } + + 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) diff --git a/indra/llplugin/llplugininstance.h b/indra/llplugin/llplugininstance.h index 5f7af301308..10703cae9d9 100644 --- a/indra/llplugin/llplugininstance.h +++ b/indra/llplugin/llplugininstance.h @@ -83,6 +83,14 @@ class LLPluginInstance /** Name of plugin init function */ static const char *PLUGIN_INIT_FUNCTION_NAME; + // Register a statically-linked plugin init entry point. When set, load() + // calls it directly instead of dlopen()ing a plugin library. Used by + // dedicated single-plugin host executables (see slplugin) to avoid dlopen + // of large TLS-using libraries (CEF on Linux) and to satisfy the Windows + // sandbox requirement that the host and its sub-processes be one image. + // Process-global: a host links exactly one plugin. + static void setStaticInitFunction(pluginInitFunction func); + private: static void staticReceiveMessage(const char *message_string, void **user_data); void receiveMessage(const char *message_string); @@ -93,6 +101,9 @@ class LLPluginInstance 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/slplugin/CMakeLists.txt b/indra/llplugin/slplugin/CMakeLists.txt index 11a165cd145..9e88abacdf7 100644 --- a/indra/llplugin/slplugin/CMakeLists.txt +++ b/indra/llplugin/slplugin/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(SLPlugin target_sources(SLPlugin PRIVATE slplugin.cpp + slplugin_generic.cpp ) diff --git a/indra/llplugin/slplugin/slplugin.cpp b/indra/llplugin/slplugin/slplugin.cpp index 81a27cf2e55..423eeb7a8aa 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,12 @@ #include using namespace std; +// Returns the statically-linked plugin init entry point for this host, or NULL +// for the generic SLPlugin (which dlopen()s the plugin named in load_plugin). +// Each SLPlugin variant links exactly one definition: slplugin_generic.cpp +// (NULL) or a per-plugin file such as slplugin_cef.cpp (&LLPluginInitEntryPoint). +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init(); + #if LL_DARWIN #include "slplugin-objc.h" @@ -190,6 +197,11 @@ int main(int argc, char **argv) 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); diff --git a/indra/llplugin/slplugin/slplugin_generic.cpp b/indra/llplugin/slplugin/slplugin_generic.cpp new file mode 100644 index 00000000000..26934373e5d --- /dev/null +++ b/indra/llplugin/slplugin/slplugin_generic.cpp @@ -0,0 +1,38 @@ +/** + * @file slplugin_generic.cpp + * @brief Static-plugin hook for the generic SLPlugin host. + * + * The generic SLPlugin dlopen()s whatever plugin the viewer names in the + * load_plugin message, so it has no statically-linked plugin: return NULL and + * LLPluginInstance::load() takes the normal dlopen path. Dedicated single-plugin + * hosts (e.g. SLPluginCEF) link a different definition that returns their + * plugin's &LLPluginInitEntryPoint instead. + * + * $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" + +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init() +{ + return nullptr; +} diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index 5033c1609c0..a432cb613b7 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -49,9 +49,22 @@ endif () list(APPEND media_plugin_cef_SOURCE_FILES ${media_plugin_cef_HEADER_FILES}) +# Compile the plugin sources once into an object library so they can be both +# built into the loadable media_plugin_cef module (the legacy dlopen path) and +# statically linked into the dedicated SLPluginCEF host (Phase 2). 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 +) + add_library(media_plugin_cef SHARED - ${media_plugin_cef_SOURCE_FILES} + $ ) target_link_libraries(media_plugin_cef @@ -63,6 +76,36 @@ target_link_libraries(media_plugin_cef add_dependencies(media_plugin_cef dullahan_host) +### SLPluginCEF - dedicated host that statically links the CEF plugin instead of +### dlopen()ing it. Reuses the generic slplugin host driver (slplugin.cpp) plus +### slplugin_cef.cpp, which registers the linked LLPluginInitEntryPoint. +add_executable(SLPluginCEF + WIN32 + MACOSX_BUNDLE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + slplugin_cef.cpp + $ + ) + +if (DARWIN) + target_sources(SLPluginCEF 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(SLPluginCEF + media_plugin_base + dullahan + ll::cef + ll::glib_headers + llplugin + llmessage + llcommon + ll::pluginlibraries +) + +add_dependencies(SLPluginCEF media_plugin_cef) + if (WINDOWS) set_target_properties(media_plugin_cef PROPERTIES @@ -71,6 +114,16 @@ if (WINDOWS) ) target_link_options(media_plugin_cef PRIVATE /MANIFEST:NO /IGNORE:4099) + # SLPluginCEF lives next to libcef.dll (deployed below by media_plugin_cef) so + # the statically-referenced CEF runtime resolves from the host's own directory. + set_target_properties(SLPluginCEF + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD_DEBUG ON + RUNTIME_OUTPUT_DIRECTORY "${VIEWER_STAGING_DIR}/llplugin" + ) + target_sources(SLPluginCEF PRIVATE "${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.manifest") + target_link_options(SLPluginCEF PRIVATE /IGNORE:4099) + # Copy plugin files to packaging directory set(CEF_BIN_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/cef-bin") add_custom_command( diff --git a/indra/media_plugins/cef/slplugin_cef.cpp b/indra/media_plugins/cef/slplugin_cef.cpp new file mode 100644 index 00000000000..f8eaddf2c74 --- /dev/null +++ b/indra/media_plugins/cef/slplugin_cef.cpp @@ -0,0 +1,45 @@ +/** + * @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" + +// 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; +} diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 417199ad9b2..aa5c9c476ac 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, off by default.) + Persist + 1 + Type + Boolean + Value + 0 + + 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, off by default.) + Persist + 1 + Type + Boolean + Value + 0 + + 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, off by default.) + Persist + 1 + Type + Boolean + Value + 0 + + ALCefSandbox + + Comment + Enable the Chromium sandbox for CEF browser sub-processes (renderer/GPU/utility). Requires a viewer restart. (CEF daemon rework, off by default.) + Persist + 1 + Type + Boolean + Value + 0 + ALSceneExplorerActivateAction Comment diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 381b1fa498c..66c373f67b1 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -1818,6 +1818,30 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ std::string launcher_name = gDirUtilp->getLLPluginLauncher(); std::string plugin_name = gDirUtilp->getLLPluginFilename(plugin_basename); + // When the dedicated CEF host is enabled, launch SLPluginCEF (which + // statically links the CEF plugin) instead of the generic SLPlugin that + // dlopen()s media_plugin_cef. plugin_name is still passed and validated + // below, but the static host ignores it. Falls back to the generic + // launcher if SLPluginCEF is missing. + if (plugin_basename == "media_plugin_cef" && gSavedSettings.getBOOL("ALCefDedicatedHost")) + { +#if LL_WINDOWS + const std::string cef_host_exe = "SLPluginCEF.exe"; +#else + const std::string cef_host_exe = "SLPluginCEF"; +#endif + std::string cef_host_name = gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + cef_host_exe; + if (LLFile::isfile(cef_host_name)) + { + launcher_name = cef_host_name; + } + else + { + LL_WARNS_ONCE("Media") << "ALCefDedicatedHost set but " << cef_host_name + << " not found; using generic launcher" << LL_ENDL; + } + } + std::string user_data_path_cache = gDirUtilp->getCacheDir(false); user_data_path_cache += gDirUtilp->getDirDelimiter(); @@ -3100,13 +3124,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) { From 35695a1b378af2a0d2d49b144d8ad76f93899b83 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 27 Jun 2026 13:04:51 -0400 Subject: [PATCH 02/10] CEF daemon Phase 3: Windows sandbox via CEF bootstrap host Make the Windows SLPluginCEF host a CEF bootstrap client so the Chromium sandbox can be enabled for browser sub-processes. - slplugin.cpp: factor the plugin<->parent host message loop out of the platform entry points into slplugin_run(port) so the bootstrap entry can reuse it. - media_plugins/cef/slplugin_cef_bootstrap.cpp: RunWinMain (exported via CEF_BOOTSTRAP_EXPORT) - CefExecuteProcess for sub-processes (same image, as the sandbox requires), then dullahan::setSandboxInfo() + slplugin_run() for the browser process. SLPLUGIN_CEF_NO_SANDBOX env var disables the sandbox for debugging. - CMake: on Windows SLPluginCEF is now a DLL (RunWinMain); ship CEF's bootstrap.exe renamed to SLPluginCEF.exe (matching base name loads SLPluginCEF.dll). Stays an executable on Linux/macOS. - llviewermedia: ALCefSandbox also routes CEF media to SLPluginCEF (the sandbox requires the dedicated host). - Bumps dullahan submodule to 5bfd90b (sandbox_info plumbing + the example bootstrap client). cef_sandbox.lib is not linked (M138+ ships it only inside the bootstrap executables), so no static-CRT requirement. Builds on Windows: SLPluginCEF.dll (RunWinMain exported) + SLPluginCEF.exe, media_plugin_cef DLL, llviewermedia. Sandbox actually engaging needs runtime verification. Co-Authored-By: Claude Opus 4.8 --- indra/dullahan | 2 +- indra/llplugin/slplugin/slplugin.cpp | 21 ++- indra/media_plugins/cef/CMakeLists.txt | 52 +++++--- .../cef/slplugin_cef_bootstrap.cpp | 123 ++++++++++++++++++ indra/newview/llviewermedia.cpp | 8 +- 5 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 indra/media_plugins/cef/slplugin_cef_bootstrap.cpp diff --git a/indra/dullahan b/indra/dullahan index 004132a0aa0..5bfd90bbbd1 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit 004132a0aa0984cfb64acee269fe047a0f59ee8c +Subproject commit 5bfd90bbbd1c2c88da7f6185de60a3e1637bf895 diff --git a/indra/llplugin/slplugin/slplugin.cpp b/indra/llplugin/slplugin/slplugin.cpp index 423eeb7a8aa..8ecfebbd38e 100644 --- a/indra/llplugin/slplugin/slplugin.cpp +++ b/indra/llplugin/slplugin/slplugin.cpp @@ -48,6 +48,12 @@ using namespace std; // (NULL) or a per-plugin file such as slplugin_cef.cpp (&LLPluginInitEntryPoint). LLPluginInstance::pluginInitFunction ll_get_static_plugin_init(); +// The host driver: register the (static or dlopen'd) 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); + #if LL_DARWIN #include "slplugin-objc.h" @@ -191,7 +197,19 @@ int main(int argc, char **argv) #endif # if LL_DARWIN signal(SIGEMT, &crash_handler); // emulate instruction executed +#endif //LL_DARWIN + + // Hand off to the shared host driver (the CEF bootstrap host reuses it too). + int rc = slplugin_run(port); + + ll_cleanup_apr(); + + return rc; +} +int slplugin_run(U32 port) +{ +#if LL_DARWIN LLCocoaPlugin cocoa_interface; cocoa_interface.setupCocoa(); cocoa_interface.createAutoReleasePool(); @@ -273,8 +291,5 @@ int main(int argc, char **argv) } delete plugin; - ll_cleanup_apr(); - - return 0; } diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index a432cb613b7..56d4d5fff33 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -77,20 +77,30 @@ target_link_libraries(media_plugin_cef add_dependencies(media_plugin_cef dullahan_host) ### SLPluginCEF - dedicated host that statically links the CEF plugin instead of -### dlopen()ing it. Reuses the generic slplugin host driver (slplugin.cpp) plus -### slplugin_cef.cpp, which registers the linked LLPluginInitEntryPoint. -add_executable(SLPluginCEF - WIN32 - MACOSX_BUNDLE - ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp - slplugin_cef.cpp - $ - ) - -if (DARWIN) - target_sources(SLPluginCEF 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) +### dlopen()ing it. Both forms reuse the generic slplugin host driver +### (slplugin.cpp) plus slplugin_cef.cpp (registers the linked entry point). +### On Windows it is a DLL exporting RunWinMain, loaded by CEF's bootstrap +### executable (shipped renamed to SLPluginCEF.exe) which sets up the sandbox; +### elsewhere it is a plain executable. +if (WINDOWS) + add_library(SLPluginCEF MODULE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + slplugin_cef.cpp + slplugin_cef_bootstrap.cpp + $ + ) +else () + add_executable(SLPluginCEF + MACOSX_BUNDLE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + slplugin_cef.cpp + $ + ) + if (DARWIN) + target_sources(SLPluginCEF 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 () endif () target_link_libraries(SLPluginCEF @@ -114,16 +124,24 @@ if (WINDOWS) ) target_link_options(media_plugin_cef PRIVATE /MANIFEST:NO /IGNORE:4099) - # SLPluginCEF lives next to libcef.dll (deployed below by media_plugin_cef) so - # the statically-referenced CEF runtime resolves from the host's own directory. + # SLPluginCEF.dll lives next to libcef.dll and the CEF bootstrap (deployed + # below by media_plugin_cef) so the CEF runtime resolves from its own dir. set_target_properties(SLPluginCEF PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD_DEBUG ON RUNTIME_OUTPUT_DIRECTORY "${VIEWER_STAGING_DIR}/llplugin" + LIBRARY_OUTPUT_DIRECTORY "${VIEWER_STAGING_DIR}/llplugin" ) - target_sources(SLPluginCEF PRIVATE "${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.manifest") target_link_options(SLPluginCEF PRIVATE /IGNORE:4099) + # Ship CEF's bootstrap.exe renamed to SLPluginCEF.exe; matching base name makes + # it load SLPluginCEF.dll and call our exported RunWinMain with the sandbox + # info. media_plugin_cef (a dependency) has already copied bootstrap.exe into + # the llplugin dir by this point. + add_custom_command(TARGET SLPluginCEF POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VIEWER_STAGING_DIR}/llplugin/bootstrap.exe" "${VIEWER_STAGING_DIR}/llplugin/SLPluginCEF.exe" + ) + # Copy plugin files to packaging directory set(CEF_BIN_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/cef-bin") add_custom_command( 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 00000000000..81edffa1a12 --- /dev/null +++ b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp @@ -0,0 +1,123 @@ +/** + * @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); + +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 (no_sandbox=false, no browser_subprocess_path), then run + // the standard plugin host loop. + dullahan::setSandboxInfo(sandbox_info); + + ll_init_apr(); + { + LLError::initForApplication(".", "."); + LLError::setDefaultLevel(LLError::LEVEL_INFO); + } + + // RunWinMain's lpCmdLine is LPTSTR (wide under UNICODE); the launcher + // passes just the numeric port, so narrow each char explicitly + // (ASCII-safe, and avoids the implicit-narrowing warning-as-error). + U32 port = 0; + std::string cmd; + for (LPCWSTR p = lpCmdLine; p && *p; ++p) + { + cmd.push_back(static_cast(*p)); + } + LLStringUtil::trim(cmd); + if (cmd.empty() || !LLStringUtil::convertToU32(cmd, port)) + { + LL_WARNS("slplugin") << "SLPluginCEF: missing/invalid launcher port" << LL_ENDL; + ll_cleanup_apr(); + return 1; + } + + const int rc = slplugin_run(port); + 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/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 66c373f67b1..a04d11f94f6 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -1821,9 +1821,11 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ // When the dedicated CEF host is enabled, launch SLPluginCEF (which // statically links the CEF plugin) instead of the generic SLPlugin that // dlopen()s media_plugin_cef. plugin_name is still passed and validated - // below, but the static host ignores it. Falls back to the generic - // launcher if SLPluginCEF is missing. - if (plugin_basename == "media_plugin_cef" && gSavedSettings.getBOOL("ALCefDedicatedHost")) + // below, but the static host ignores it. The Windows sandbox also + // requires this host (ALCefSandbox implies it). Falls back to the + // generic launcher if SLPluginCEF is missing. + if (plugin_basename == "media_plugin_cef" && + (gSavedSettings.getBOOL("ALCefDedicatedHost") || gSavedSettings.getBOOL("ALCefSandbox"))) { #if LL_WINDOWS const std::string cef_host_exe = "SLPluginCEF.exe"; From 427fdad85a64577cb17c1d24048b5f0ebb6ca8fc Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 27 Jun 2026 22:14:36 -0400 Subject: [PATCH 03/10] CEF daemon Phase 4a: daemon-side multi-connection host Add the shared tab-manager daemon's host side: one process serving N plugin tabs. slplugin_daemon_run() listens on a control port (written to a rendezvous file for discover-or-spawn), serves the first tab from the launch-arg port, and spawns an LLPluginProcessChild for each later parent that registers its listen port on the control channel. Every tab lives in this one process, so they transparently share a single CEF runtime (dullahan_runtime, Phase 1) - the point of the daemon. Exits after DAEMON_IDLE_TIMEOUT with no live tabs (keeps the runtime warm across brief gaps). SLPluginCEF's bootstrap entry now parses " --daemon " and routes to slplugin_daemon_run vs the single-tab slplugin_run. The daemon driver is plugin-agnostic (lives in llplugin/slplugin) and is linked only into SLPluginCEF for now. Builds and links on Windows. This is the daemon SIDE only; the viewer-side discover-or-spawn that drives it (LLPluginProcessParent connect-or-launch + liveness/crash recovery) is the next step and needs runtime iteration - see the commit body of the follow-up. Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 4b step 1: mUseDaemon plumb + daemon-aware liveness Lay the inert groundwork for the viewer-side discover-or-spawn without touching the launch path yet. - LLPluginProcessParent: setUseDaemon()/getUseDaemon() + mUseDaemon flag. pluginLockedUpOrQuit() no longer treats a null mProcess as "child exited" when mUseDaemon is set - in daemon mode this parent talks to a shared host it does not own, so liveness comes from the socket/heartbeat (pluginLockedUp) instead. (STATE_EXITING reaching CLEANUP on a null process is already correct - nothing to wait on.) - LLPluginClassMedia: setUseDaemon() forwarding to the parent at create time. - llviewermedia: set it for CEF media when ALCefDaemonEnabled. This is deliberately inert: the launch path still creates a process, so mProcess stays non-null and the new branch never fires. With the flag off it is a no-op. It only becomes live once step 2 (discover-or-spawn) can leave mProcess null. Builds: llplugin + llviewermedia compile. Co-Authored-By: Claude Opus 4.8 CEF daemon Phase 4b step 2: viewer-side discover-or-spawn Wire LLPluginProcessParent to the shared daemon. At STATE_LISTENING, when mUseDaemon is set, instead of launching a private process it: - reads the daemon control port from the per-plugin-dir rendezvous file and, if a daemon is reachable, registers this tab by connecting to the control port and sending its listen port (the daemon connects back -> the existing accept() path, mProcess stays null); - else takes an atomic spawn lock (stealing a stale one) and launches the daemon DETACHED (autokill=false, attached=false) with " --daemon ", so it outlives this parent and is shared by later tabs - no parent owns it (otherwise closing one tab's media would kill the whole daemon). It serves the spawner as its first tab, then connects back like any registration; - else (another parent is launching) waits and retries next idle. The daemon removes the spawn lock once it has published the rendezvous. llviewermedia routes CEF media to SLPluginCEF when ALCefDaemonEnabled too (the daemon requires the dedicated host; the generic SLPlugin can't parse --daemon). All gated by mUseDaemon/ALCefDaemonEnabled - the default path is untouched. Builds: llplugin + SLPluginCEF + llviewermedia compile/link. The concurrent lifecycle (spawn race, register-vs-spawn timing, daemon liveness) needs runtime verification. Co-Authored-By: Claude Opus 4.8 CEF daemon: rendezvous path must be caller-supplied + user-writable daemonRendezvousPath() derived the rendezvous/lock path from mPluginDir (the install/plugin dir), which is read-only on a packaged build - the daemon could not publish its control port and no spawn lock could be taken. The low-level llplugin layer also has no business inventing user paths. Make it an explicit, caller-supplied path: setUseDaemon() now takes a rendezvous_path; LLPluginProcessParent stores it and the daemon branch is skipped if it is empty. The viewer supplies a user-writable path in the logs dir (LL_PATH_LOGS) carrying the viewer PID, so it is writable on an installed build and separate viewer instances do not share a daemon. Plumbed viewer -> LLPluginClassMedia -> LLPluginProcessParent. Builds: llplugin + llviewermedia compile. Co-Authored-By: Claude Opus 4.8 CEF daemon: dedicated host never falls back to dullahan_host Fix daemon (and single-tab) SLPluginCEF launching dullahan_host.exe for CEF sub-processes when the sandbox is not active. The dedicated host dispatches its own sub-processes (CEF re-launches SLPluginCEF.exe -> RunWinMain -> CefExecuteProcess), so dullahan_host is never needed - but dullahan only skipped it when sandboxed. The bootstrap entry now calls dullahan::setHostHandlesSubprocesses(true) alongside setSandboxInfo, so CEF re-launches the SLPluginCEF image for sub-processes whether or not the sandbox engaged. Bumps dullahan submodule to e9bc2a8 (the decoupling + example). Builds on Windows. Co-Authored-By: Claude Opus 4.8 CEF daemon: launch daemon in the viewer job object (fixes sandbox) The Windows CEF sandbox worked for the per-process SLPluginCEF host but failed for the daemon. The only launch difference was the job object: the daemon was spawned with autokill=false, putting it OUTSIDE the viewer's job object that the working per-process host (autokill=true) runs in - and the sandbox broker requires it. autokill (the APR job-object association) and attached (kill the child when this LLProcess handle is destroyed) had been conflated. Keep autokill at its default (true) so the daemon joins the job and dies with the viewer, and set only attached=false so discarding the fire-and-forget handle does not kill the daemon (it must outlive the spawning tab). Builds: llplugin. Co-Authored-By: Claude Opus 4.8 Fix cef daemon build-link to alchemy-bin and packaging CEF daemon: register static plugin in daemon host (fixes sandbox) The daemon ran unsandboxed (and spawned dullahan_host) because its tabs dlopen()ed media_plugin_cef.dll instead of using the statically-linked plugin. dullahan is statically linked, so the dlopen'd DLL carries a SECOND dullahan_runtime - a different instance than the one the bootstrap host set the sandbox info / host-handles-subprocesses flags on. That tab runtime saw mSandboxInfo=NULL, so it disabled the sandbox and used the dullahan_host helper. slplugin_run() (the single-tab host) calls LLPluginInstance::setStaticInitFunction() so load() uses the linked plugin entry directly; slplugin_daemon_run() did not. Add the same call so daemon tabs run in the host's own dullahan_runtime - the one with the sandbox info. Builds: SLPluginCEF. Co-Authored-By: Claude Opus 4.8 CEF daemon: clean up the rendezvous file on viewer shutdown The daemon runs in the viewer's job object, so on viewer exit it is force-killed and never reaches the std::remove(rendezvous) at the end of slplugin_daemon_run (that only runs on the idle-timeout path). Because the rendezvous filename carries the viewer PID, each run left a distinct orphan in the logs dir. Delete it (and any stale spawn lock) from ~LLViewerMedia(), which runs at shutdown while gDirUtilp is still valid - before the daemon is killed. Factor the path into a shared helper so the create site (newSourceFromMediaType) and the cleanup agree. No-op when daemon mode was unused. Builds: llviewermedia compiles. Co-Authored-By: Claude Opus 4.8 --- indra/dullahan | 2 +- indra/llplugin/llpluginclassmedia.cpp | 1 + indra/llplugin/llpluginclassmedia.h | 8 + indra/llplugin/llpluginprocessparent.cpp | 167 ++++++++++++- indra/llplugin/llpluginprocessparent.h | 35 +++ indra/llplugin/slplugin/slplugin_daemon.cpp | 229 ++++++++++++++++++ indra/media_plugins/cef/CMakeLists.txt | 1 + .../cef/slplugin_cef_bootstrap.cpp | 35 ++- indra/newview/CMakeLists.txt | 2 + indra/newview/llviewermedia.cpp | 42 +++- indra/newview/viewer_manifest.py | 1 + 11 files changed, 508 insertions(+), 15 deletions(-) create mode 100644 indra/llplugin/slplugin/slplugin_daemon.cpp diff --git a/indra/dullahan b/indra/dullahan index 5bfd90bbbd1..e9bc2a84422 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit 5bfd90bbbd1c2c88da7f6185de60a3e1637bf895 +Subproject commit e9bc2a84422c505fab7ca4972e11c97ab0aaabfc diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 9403f244690..8dc34fbb398 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -75,6 +75,7 @@ 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"); diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h index d6f07c16321..101c82cd750 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -172,6 +172,12 @@ 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); }; + // Inherited from LLPluginProcessParentOwner /* virtual */ void receivePluginMessage(const LLPluginMessage &message); /* virtual */ void pluginLaunchFailed(); @@ -430,6 +436,8 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner LLPluginProcessParent::ptr_t mPlugin; + bool mUseDaemon = false; + std::string mDaemonRendezvous; LLRect mDirtyRect; diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index 5856f9ddd64..d880a222226 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -39,6 +39,11 @@ #include "llapr.h" +#include "apr_network_io.h" +#include "apr_file_io.h" + +#include + //virtual LLPluginProcessParentOwner::~LLPluginProcessParentOwner() { @@ -494,8 +499,60 @@ 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; + LLProcess::create(params); // fire and forget; keep no mProcess + 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) @@ -1263,7 +1320,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 +1351,104 @@ 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) +{ + std::ifstream f(path.c_str()); + if (!f.is_open()) + { + return 0; + } + U32 port = 0; + f >> port; + 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) +{ + apr_file_t* f = nullptr; + apr_status_t s = apr_file_open(&f, lock_path.c_str(), + APR_FOPEN_CREATE | APR_FOPEN_EXCL | APR_FOPEN_WRITE, APR_FPROT_OS_DEFAULT, gAPRPoolp); + if (s == APR_SUCCESS) + { + apr_file_close(f); + return true; + } + if (APR_STATUS_IS_EEXIST(s)) + { + // Steal a stale lock left by an aborted launch (older than the launch timeout). + apr_finfo_t finfo; + if (apr_stat(&finfo, lock_path.c_str(), APR_FINFO_MTIME, gAPRPoolp) == APR_SUCCESS && + (apr_time_now() - finfo.mtime) > 15 * APR_USEC_PER_SEC) + { + apr_file_remove(lock_path.c_str(), gAPRPoolp); + if (apr_file_open(&f, lock_path.c_str(), + APR_FOPEN_CREATE | APR_FOPEN_EXCL | APR_FOPEN_WRITE, APR_FPROT_OS_DEFAULT, gAPRPoolp) == APR_SUCCESS) + { + apr_file_close(f); + 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 ea604ca8d7d..d0a75f2a682 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/slplugin_daemon.cpp b/indra/llplugin/slplugin/slplugin_daemon.cpp new file mode 100644 index 00000000000..9c0d11ffd5c --- /dev/null +++ b/indra/llplugin/slplugin/slplugin_daemon.cpp @@ -0,0 +1,229 @@ +/** + * @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" + +#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; + +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 + } + + // Blocking read of the short port line (the registration is tiny). + apr_socket_timeout_set(incoming, 1000000); // 1s + char buf[32] = {0}; + apr_size_t len = sizeof(buf) - 1; + if (apr_socket_recv(incoming, buf, &len) == APR_SUCCESS && len > 0) + { + buf[len] = '\0'; + std::string s(buf); + LLStringUtil::trim(s); + U32 parent_port = 0; + if (!s.empty() && LLStringUtil::convertToU32(s, parent_port) && parent_port) + { + LLPluginProcessChild* tab = new LLPluginProcessChild(); + 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) +{ + // 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); + } + + // Publish the control port for discover-or-spawn. Write then flush+close so a + // racing parent reads a complete value. + { + std::ofstream rv(rendezvous_path.c_str(), std::ios::trunc); + rv << control_port << std::endl; + } + // The rendezvous is now published, so release the spawn lock the launching + // parent took - other parents can stop waiting and register. + std::remove((rendezvous_path + ".lock").c_str()); + LL_INFOS("slplugin") << "daemon: control port " << control_port + << " -> " << rendezvous_path << LL_ENDL; + + std::vector tabs; + { + LLPluginProcessChild* first = new LLPluginProcessChild(); + first->init(first_port); + tabs.push_back(first); + } + + LLTimer idle_timer; + bool idle_running = false; + + while (true) + { + acceptRegistration(control, tabs); + + // 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; + } + + ms_sleep(10); + } + + apr_socket_close(control); + std::remove(rendezvous_path.c_str()); + LL_INFOS("slplugin") << "daemon: idle, exiting" << LL_ENDL; + return 0; +} diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index 56d4d5fff33..c79bb43eee4 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -85,6 +85,7 @@ add_dependencies(media_plugin_cef dullahan_host) if (WINDOWS) add_library(SLPluginCEF MODULE ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_daemon.cpp slplugin_cef.cpp slplugin_cef_bootstrap.cpp $ diff --git a/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp index 81edffa1a12..c5c2b0dee55 100644 --- a/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp +++ b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp @@ -51,6 +51,8 @@ // 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 { @@ -78,9 +80,12 @@ namespace } // Browser process: make the sandbox info available to the runtime's - // CefInitialize (no_sandbox=false, no browser_subprocess_path), then run - // the standard plugin host loop. + // 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); ll_init_apr(); { @@ -88,24 +93,38 @@ namespace LLError::setDefaultLevel(LLError::LEVEL_INFO); } - // RunWinMain's lpCmdLine is LPTSTR (wide under UNICODE); the launcher - // passes just the numeric port, so narrow each char explicitly - // (ASCII-safe, and avoids the implicit-narrowing warning-as-error). - U32 port = 0; + // RunWinMain's lpCmdLine is LPTSTR (wide under UNICODE); narrow each char + // explicitly (ASCII-safe, avoids the implicit-narrowing warning-as-error). + // 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; for (LPCWSTR p = lpCmdLine; p && *p; ++p) { cmd.push_back(static_cast(*p)); } LLStringUtil::trim(cmd); - if (cmd.empty() || !LLStringUtil::convertToU32(cmd, port)) + + 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; } - const int rc = slplugin_run(port); + 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); ll_cleanup_apr(); return rc; } diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 662af9a61b9..390b19347d7 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -2009,6 +2009,7 @@ if (WINDOWS) DEPENDS stage_third_party_libs SLPlugin + $<$:$> $<$:$> $ $<$:$> @@ -2019,6 +2020,7 @@ else() DEPENDS stage_third_party_libs SLPlugin + $ $ $ $ diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index a04d11f94f6..1b7ed629b7b 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -213,6 +213,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 +244,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(); + std::remove(rv.c_str()); + std::remove((rv + ".lock").c_str()); + } } // static @@ -1821,11 +1845,13 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ // When the dedicated CEF host is enabled, launch SLPluginCEF (which // statically links the CEF plugin) instead of the generic SLPlugin that // dlopen()s media_plugin_cef. plugin_name is still passed and validated - // below, but the static host ignores it. The Windows sandbox also - // requires this host (ALCefSandbox implies it). Falls back to the - // generic launcher if SLPluginCEF is missing. + // below, but the static host ignores it. The Windows sandbox and the + // shared daemon both require this host (ALCefSandbox/ALCefDaemonEnabled + // imply it). Falls back to the generic launcher if SLPluginCEF is missing. if (plugin_basename == "media_plugin_cef" && - (gSavedSettings.getBOOL("ALCefDedicatedHost") || gSavedSettings.getBOOL("ALCefSandbox"))) + (gSavedSettings.getBOOL("ALCefDedicatedHost") || + gSavedSettings.getBOOL("ALCefSandbox") || + gSavedSettings.getBOOL("ALCefDaemonEnabled"))) { #if LL_WINDOWS const std::string cef_host_exe = "SLPluginCEF.exe"; @@ -1860,6 +1886,14 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ { 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()); 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()); diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 014765f5a3f..24fb7811dde 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -519,6 +519,7 @@ def construct(self): 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("SLPluginCEF.exe") self.path("*.dll") # CEF files self.path("*.exe") From 669f59dcd18ed38e4294cb5736a313b12b1b7ee3 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 28 Jun 2026 07:05:24 -0400 Subject: [PATCH 04/10] CEF daemon: graceful tab teardown so closing one tab doesn't kill the daemon In daemon mode the parent owns no host process (mProcess is null), so STATE_EXITING fell straight through to STATE_CLEANUP and slammed the socket shut while the tab's browser was still live in the shared CEF runtime. The tab then lost its parent socket before processing shutdown_plugin, dropped into STATE_ERROR (which never nulls mInstance), and was reaped by the daemon - whose ~LLPluginProcessChild calls exit(0) when mInstance is set, taking down every other tab. Net effect: open two web instances, close one, the whole SLPluginCEF.exe disappears. Fix the teardown end to end: - Parent: in daemon mode STATE_EXITING now waits for the tab to finish its graceful unload and drop its end of the socket (EOF/error) or for the lockup timeout, instead of tearing the socket down immediately. - Child: a lost parent socket in daemon mode routes to the normal graceful unload (STATE_SHUTDOWNREQ) so the browser closes cleanly in the shared runtime before the tab is reaped. - Child: ~LLPluginProcessChild no longer exit(0)s in daemon mode - daemon plugins are statically linked (no DSO unload to lock up), so it deletes the instance directly and lets the other tabs live. - Daemon marks every tab with setDaemonMode(true). Co-Authored-By: Claude Opus 4.8 CEF daemon: recover media after a daemon crash instead of failing permanently A shared-daemon crash drops every tab's socket at once, so each media's LLPluginProcessParent reports the plugin dead (MEDIA_EVENT_PLUGIN_FAILED) and the impl latched mMediaSourceFailed = true -> isForcedUnloaded() pins it to PRIORITY_UNLOADED forever. Net effect of one daemon crash: all CEF media go permanently dark, a regression from per-process hosting where a crash only loses one media. For daemon-mode CEF media, schedule a bounded, backed-off re-init on failure (DAEMON_RECOVERY_MAX_ATTEMPTS, base*attempt up to a ceiling) rather than failing permanently. When the backoff elapses, update() clears the failure latch and the normal load path recreates the source - the first impl to do so wins the spawn lock and respawns the daemon (discover-or-spawn), the rest reconnect as fresh tabs. A completed navigate resets the attempt counter, so the cap only trips on a daemon that keeps crashing on launch (no respawn storm). Non-daemon plugins are untouched (gated on LLPluginClassMedia::getUseDaemon()). Co-Authored-By: Claude Opus 4.8 CEF daemon: hard tab ceiling as a runaway backstop PluginInstancesTotal -> mMaxIntances is the primary tab cap (excess media stay PRIORITY_UNLOADED, so they never create a source or a tab). Add a generous daemon-side ceiling (DAEMON_MAX_TABS) so a buggy or hostile parent can't drive one process to spawn unbounded browsers regardless of the viewer-side accounting; over the ceiling, registrations are dropped. Co-Authored-By: Claude Opus 4.8 CEF daemon: cut idle CPU by driving the external pump from CEF's schedule Bump dullahan submodule: dullahan_runtime now implements OnScheduleMessagePumpWork so CefDoMessageLoopWork() runs only when CEF asks for it, instead of on every host idle tick. Removes the constant idle CPU draw of the fixed-cadence pump and collapses N daemon tabs' per-frame pumps into at most one shared pump. Co-Authored-By: Claude Opus 4.8 CEF daemon: fix resize blanking via external-pump watchdog Bump dullahan submodule: the OnScheduleMessagePumpWork-gated pump could let a needed repaint go unscheduled after a resize (CEF coalesces its external-pump notifications), leaving the media surface blank with no event to recover it. dullahan_runtime now watchdogs the pump so it can never stay idle longer than 100ms, bounding recovery while keeping the idle-CPU win. Co-Authored-By: Claude Opus 4.8 CEF daemon: stop per-tab 50ms stall from the browser-init settle hack Bump dullahan submodule: the post-CefInitialize "settle" pump (a 50ms blocking sleep that let the shared global request context finish initializing) ran in every browser's init(). In the daemon that froze the host thread - and so every other tab - for 50ms on each new tab. It now runs once per process in dullahan_runtime::acquire(); later daemon tabs skip it. Co-Authored-By: Claude Opus 4.8 CEF daemon: fix login-time re-CefInitialize crash (persistent runtime) When the login-screen web surface transitioned away, the daemon's CEF browser count hit zero, release() called CefShutdown(), and the next web surface crashed in CefInitialize() (CEF cannot be re-initialized in a process). Bump dullahan submodule for the persistent-runtime mode, and have the SLPluginCEF bootstrap opt in (setPersistentRuntime(true)) and shut CEF down once (shutdownRuntime()) after the host loop returns. CEF now stays up across zero-browser gaps and is torn down exactly once at process exit. Co-Authored-By: Claude Opus 4.8 --- indra/dullahan | 2 +- indra/llplugin/llpluginclassmedia.h | 1 + indra/llplugin/llpluginprocesschild.cpp | 52 ++++++++++++++----- indra/llplugin/llpluginprocesschild.h | 8 +++ indra/llplugin/llpluginprocessparent.cpp | 21 +++++++- indra/llplugin/slplugin/slplugin_daemon.cpp | 21 ++++++++ .../cef/slplugin_cef_bootstrap.cpp | 13 +++++ indra/newview/llviewermedia.cpp | 40 ++++++++++++++ indra/newview/llviewermedia.h | 7 +++ 9 files changed, 150 insertions(+), 15 deletions(-) diff --git a/indra/dullahan b/indra/dullahan index e9bc2a84422..4b4ac183e93 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit e9bc2a84422c505fab7ca4972e11c97ab0aaabfc +Subproject commit 4b4ac183e93e7a883a640e15031246b9067d74cf diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h index 101c82cd750..a835becfd17 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -177,6 +177,7 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner // 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; }; // Inherited from LLPluginProcessParentOwner /* virtual */ void receivePluginMessage(const LLPluginMessage &message); diff --git a/indra/llplugin/llpluginprocesschild.cpp b/indra/llplugin/llpluginprocesschild.cpp index 7c6dad05abd..390e7cbf427 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 5de0f90209d..cefdf37c8ae 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 d880a222226..510f0462f2e 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -716,7 +716,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); } diff --git a/indra/llplugin/slplugin/slplugin_daemon.cpp b/indra/llplugin/slplugin/slplugin_daemon.cpp index 9c0d11ffd5c..9aab1ec74dc 100644 --- a/indra/llplugin/slplugin/slplugin_daemon.cpp +++ b/indra/llplugin/slplugin/slplugin_daemon.cpp @@ -56,6 +56,13 @@ // 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 @@ -103,6 +110,18 @@ namespace 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_timeout_set(incoming, 1000000); // 1s char buf[32] = {0}; @@ -116,6 +135,7 @@ namespace 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 @@ -173,6 +193,7 @@ int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path) std::vector tabs; { LLPluginProcessChild* first = new LLPluginProcessChild(); + first->setDaemonMode(true); first->init(first_port); tabs.push_back(first); } diff --git a/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp index c5c2b0dee55..3ce1711205e 100644 --- a/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp +++ b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp @@ -86,6 +86,13 @@ namespace // 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(); { @@ -125,6 +132,12 @@ namespace 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; } diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 1b7ed629b7b..f70f0038f0d 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -168,6 +168,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; @@ -2975,6 +2981,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) @@ -3476,6 +3494,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! @@ -3518,6 +3554,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 a3cb9ec93e7..68138e40cff 100644 --- a/indra/newview/llviewermedia.h +++ b/indra/newview/llviewermedia.h @@ -480,6 +480,13 @@ 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; F32 mRequestedVolume; F32 mPreviousVolume; bool mIsMuted; From 4e0e23797129ff22e3e87cc51dab1a622293b266 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 28 Jun 2026 19:27:08 -0400 Subject: [PATCH 05/10] CEF daemon: Phase 5a - dullahan OnAcceleratedPaint plumbing Bump dullahan submodule: adds the accelerated_paint setting + the OnAcceleratedPaint -> onAcceleratedPaint callback path (GPU shared-texture handle, format, coded size), shared_texture_enabled on the window, and GPU-compositing-on in that mode. Default off; no behavior change yet. Foundation for zero-copy media textures. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5b - opengl-example zero-copy paint proof (Win) Bump dullahan submodule: the opengl-example now drives OnAcceleratedPaint through a D3D11 + WGL_NV_DX_interop2 helper to alias CEF's GPU shared texture into the GL quad with no CPU copy, with a CPU-path fallback (DULLAHAN_FORCE_CPU_PAINT) for A/B. Standalone proof before wiring the cross-process transport + viewer media-texture import. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5b - diagnostics for white-screen accelerated paint Bump dullahan submodule: debugger-visible logging in the opengl-example interop to locate why the accelerated path renders white (callback firing? shared-resource open? register? lock?). Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5b - fix accelerated paint NT-handle register failure Bump dullahan submodule: WGL_NV_DX_interop2 can't register Chromium's NT-handle shared texture directly (wglDXRegisterObjectNV ERROR_OPEN_FAILED -> white quad). Open it, GPU-copy (CopyResource) into an own-device intermediate texture that registers cleanly, and sample that. Still no CPU readback. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5b - fix accelerated-paint page-change flicker Bump dullahan submodule: the OnAcceleratedPaint shared-texture pool (no keyed mutex) was being read stale - cached opens could point at a remapped resource and the GPU copy wasn't waited on before CEF recycled the source. Open fresh each frame + wait on an event query after CopyResource. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5c - carry the GPU shared-texture handle to the viewer The media IPC only transported CPU pixels via shared memory. Add a parallel path that hands the viewer a GPU shared-texture handle for zero-copy paint. - LLPluginClassMedia: setUseAcceleratedPaint() + the viewer's pid go in the media "init" message; a new "accelerated_paint" message delivers the duplicated handle (decimal string so the full 64-bit value survives) plus cef_color_type format and coded size, exposed via getAcceleratedPaint*/ takeAcceleratedPaintHandle(). - media_plugin_cef: when accelerated_paint is requested, register setOnAcceleratedPaintCallback; on each frame OpenProcess(viewer pid) once and DuplicateHandle the CEF shared texture into the viewer (the handle is only valid during the callback), then send accelerated_paint. The browser host is unsandboxed (broker), so the cross-process dup is allowed. - llviewermedia: enable it from ALCefAcceleratedPaint for CEF media; update() logs the first arriving handle (transport checkpoint) and closes each so the per-frame duplicates don't leak. Transport only - the viewer does not yet import/bind the texture (5d). Builds on Windows; runtime checkpoint is the "accelerated paint frame received" log. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5d (producer) - keyed-mutex stable texture, drop per-frame dup 5c duplicated a CEF shared-texture handle into the viewer EVERY frame (plus a per-frame OpenSharedResource on the viewer). Replace that with a producer-side stable texture so the handle crosses the boundary only once per size. media_plugin_cef gains CefAccelProducer (Windows): a D3D11 device + one persistent shared texture created with NT-handle + keyed mutex. Each accelerated-paint frame it opens CEF's pooled texture and CopyResources it into the stable texture under the mutex (single key 0 = mutual exclusion, which also gives the cross-process/cross-device GPU sync). The stable texture's NT handle is DuplicateHandle'd into the viewer only when it is (re)created; per-frame messages carry handle "0" = "same texture, new frame". Single-key (not 0/1 ping-pong) so the producer never deadlocks before the consumer exists. LLPluginClassMedia keeps the last real handle (a "0" ping no longer clears it) and always marks the frame dirty. CMake links d3d11/dxgi into media_plugin_cef + SLPluginCEF. Producer only; the viewer still just logs+closes the handle (5c checkpoint) - it opens/binds the stable texture next. Builds on Windows. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5d (consumer) - zero-copy CEF media in-world The viewer now imports the plugin's shared GPU texture and blits it into the media texture with no CPU upload (replacing the setSubImage path for accelerated CEF media). - new LLCEFAccelInterop (newview, Windows): D3D11 device + WGL_NV_DX_interop2. Opens the plugin's keyed-mutex stable texture (delivered once per size), copies it into an own-device intermediate under the mutex (single key 0 = mutual exclusion + cross-process sync), GL-registers that intermediate (the opened cross-device NT-handle texture can't be registered directly), and blits it into the media GL texture. - the blit uses glBlitFramebuffer with an inverted destination rectangle: a framebuffer read samples the interop texture in the correct channel order (no BGRA swizzle needed) and the flip turns CEF's top-down texture bottom-up for the viewer. - LLViewerMediaImpl::update() runs the accelerated path on the main thread and skips the shm/upload path; the interop is created lazily and torn down with the impl. - media_plugin_cef requests an RGBA8 media texture in accelerated mode. Builds on Windows (new files + d3d11/dxgi wired into newview). Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5d - don't lose the stable handle if the consumer isn't ready The plugin sends its stable shared-texture handle only once per size, but the consumer consumed (zeroed) it before confirming the interop was ready - so if interop init failed on that frame the handle was lost and the media stuck until a resize. Make the handle persistent on the viewer side (getAcceleratedPaintHandle / clearAcceleratedPaintDirty replace takeAcceleratedPaintHandle). The consumer brings up the interop first, then compares the persistent handle against the one it last bound and only advances its bound-handle on a SUCCESSFUL setStableTexture - so a transient failure retries with the same handle next frame. Reset the bound handle on destroyMediaSource so a recreated source rebinds fresh even if the handle value is reused. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5e - dullahan macOS/Linux accelerated-paint plumbing Bump dullahan submodule: OnAcceleratedPaint now yields the macOS IOSurfaceRef (via the existing void* callback) and the Linux dma-buf (new fd-based callback), alongside the Windows handle. Foundation for the macOS/Linux zero-copy paths; written blind from the CEF API (no Linux/mac CEF headers or build here), unverified. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5e - macOS zero-copy paint (IOSurface), untested macOS path end-to-end. The accelerated frame is an IOSurface, which is shareable by its global IOSurfaceID (an integer) - no handle duplication or producer texture needed: - media_plugin_cef sends IOSurfaceGetID over the existing accelerated_paint message each frame (the surface is pooled, so the id can change). - LLCEFAccelInterop gains a macOS impl: IOSurfaceLookup -> bind to a GL_TEXTURE_RECTANGLE via CGLTexImageIOSurface2D, then the same flipped glBlitFramebuffer into the media texture as the Windows path. Windows still builds (the macOS branch is #elif-guarded). The macOS code is written blind (no macOS build/test here) and unverified. Linux dma-buf transport + import still to come. Co-Authored-By: Claude Opus 4.8 CEF daemon: Phase 5e - Linux zero-copy paint (dma-buf + EGL), untested Linux path end-to-end. CEF delivers the accelerated frame as a dma-buf, so: - media_plugin_cef registers the dma-buf callback on Linux; per frame it dup()s the fd (CEF's is valid only during the callback), keeps a small ring alive, and sends the fd number + its pid + plane layout (stride/offset/DRM modifier/format) over the accelerated_paint message. The viewer re-opens the fd via /proc//fd - no SCM_RIGHTS side channel needed. - LLPluginClassMedia carries the dma-buf layout fields (0 on Windows/macOS); LLCEFAccelInterop::setStableTexture gained them as trailing args. - LLCEFAccelInterop Linux impl: open /proc//fd/, import it with eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) -> glEGLImageTargetTexture2DOES, then the same flipped glBlitFramebuffer into the media texture. Windows still builds (the Linux branch is #elif-guarded). Written blind (no Linux build/test here) and unverified - notably it assumes the viewer's GL context is EGL (not GLX) for eglGetCurrentDisplay to work. Co-Authored-By: Claude Opus 4.8 CEF media: share one D3D11<->GL interop device instead of one per media impl Each LLCEFAccelInterop created its own D3D11 device + wglDXOpenDeviceNV and ad-hoc wglGetProcAddress'd the WGL_NV_DX_interop2 entry points, so N web surfaces meant N D3D devices and N interop devices. Centralize it: - the wglDX* entry points are now loaded in LLGLManager::initWGL alongside the other WGL extensions (declared in llglheaders.h, defined in llgl.cpp, gated on WGL_NV_DX_interop2). - LLDXHardware owns one D3D11 device + one wglDXOpenDeviceNV interop device for the whole process (initGLDXInterop / cleanupGLDXInterop). It is brought up once in LLWindowWin32::switchContext - main thread, render context current, WGL loaded - and torn down with the context. - LLCEFAccelInterop drops its private device/loader and uses the shared device, context and interop handle plus the global wglDX* pointers; per surface it still owns only its opened texture, intermediate, registration and FBOs. Co-Authored-By: Claude Opus 4.8 --- indra/dullahan | 2 +- indra/llplugin/llpluginclassmedia.cpp | 39 ++ indra/llplugin/llpluginclassmedia.h | 37 ++ indra/llplugin/llpluginprocessparent.cpp | 42 +- indra/llplugin/slplugin/slplugin_daemon.cpp | 11 +- indra/llrender/llgl.cpp | 19 + indra/llrender/llglheaders.h | 8 + indra/llwindow/lldxhardware.cpp | 77 +++ indra/llwindow/lldxhardware.h | 18 + indra/llwindow/llwindowwin32.cpp | 13 + indra/media_plugins/cef/CMakeLists.txt | 21 +- indra/media_plugins/cef/media_plugin_cef.cpp | 350 +++++++++++- indra/newview/CMakeLists.txt | 12 + indra/newview/llcefaccelinterop.cpp | 561 +++++++++++++++++++ indra/newview/llcefaccelinterop.h | 67 +++ indra/newview/llsyntaxid.cpp | 2 +- indra/newview/llviewermedia.cpp | 104 +++- indra/newview/llviewermedia.h | 11 + indra/newview/viewer_manifest.py | 17 +- 19 files changed, 1382 insertions(+), 29 deletions(-) create mode 100644 indra/newview/llcefaccelinterop.cpp create mode 100644 indra/newview/llcefaccelinterop.h diff --git a/indra/dullahan b/indra/dullahan index 4b4ac183e93..fb099f6b552 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit 4b4ac183e93e7a883a640e15031246b9067d74cf +Subproject commit fb099f6b552807e0bd6952b7ae3743f8faeec7b2 diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 8dc34fbb398..25c60b8b8d3 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; @@ -81,6 +87,15 @@ bool LLPluginClassMedia::init(const std::string &launcher_filename, const std::s 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); +#if LL_WINDOWS + message.setValueS32("host_pid", (S32)_getpid()); +#else + message.setValueS32("host_pid", (S32)getpid()); +#endif sendMessage(message); mPlugin->init(launcher_filename, plugin_dir, plugin_filename, debug); @@ -1050,6 +1065,30 @@ 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. + 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"); + // Linux dma-buf layout (absent -> 0 on Windows/macOS). + mAcceleratedPaintStride = message.getValueS32("stride"); + mAcceleratedPaintSrcPid = message.getValueS32("src_pid"); + mAcceleratedPaintOffset = strtoull(message.getValue("offset").c_str(), nullptr, 10); + mAcceleratedPaintModifier = strtoull(message.getValue("modifier").c_str(), nullptr, 10); + 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 a835becfd17..384d8021fb0 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -179,6 +179,30 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner { 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; }; + + // 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; }; + // Linux dma-buf layout (0 on Windows/macOS): the handle is an fd in process + // getAcceleratedPaintSrcPid(), with this plane stride/offset and DRM modifier. + int getAcceleratedPaintStride() const { return mAcceleratedPaintStride; }; + unsigned long long getAcceleratedPaintOffset() const { return mAcceleratedPaintOffset; }; + unsigned long long getAcceleratedPaintModifier() const { return mAcceleratedPaintModifier; }; + int getAcceleratedPaintSrcPid() const { return mAcceleratedPaintSrcPid; }; + // Inherited from LLPluginProcessParentOwner /* virtual */ void receivePluginMessage(const LLPluginMessage &message); /* virtual */ void pluginLaunchFailed(); @@ -440,6 +464,19 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner bool mUseDaemon = false; std::string mDaemonRendezvous; + // accelerated (zero-copy) paint - see setUseAcceleratedPaint / the getters above + bool mUseAcceleratedPaint = false; + unsigned long long mAcceleratedPaintHandle = 0; // native handle, dup'd into this process + int mAcceleratedPaintFormat = 0; + int mAcceleratedPaintWidth = 0; + int mAcceleratedPaintHeight = 0; + bool mAcceleratedPaintDirty = false; + // Linux dma-buf only: + int mAcceleratedPaintStride = 0; + unsigned long long mAcceleratedPaintOffset = 0; + unsigned long long mAcceleratedPaintModifier = 0; + int mAcceleratedPaintSrcPid = 0; + LLRect mDirtyRect; std::string translateModifiers(MASK modifiers); diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index 510f0462f2e..0fab498a809 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -38,10 +38,11 @@ #include "workqueue.h" #include "llapr.h" +#include "llfile.h" #include "apr_network_io.h" -#include "apr_file_io.h" +#include #include //virtual @@ -1381,7 +1382,7 @@ std::string LLPluginProcessParent::daemonRendezvousPath() const // static U32 LLPluginProcessParent::readDaemonControlPort(const std::string& path) { - std::ifstream f(path.c_str()); + llifstream f(path); if (!f.is_open()) { return 0; @@ -1422,26 +1423,33 @@ bool LLPluginProcessParent::registerWithDaemon(U32 control_port) // static bool LLPluginProcessParent::acquireSpawnLock(const std::string& lock_path) { - apr_file_t* f = nullptr; - apr_status_t s = apr_file_open(&f, lock_path.c_str(), - APR_FOPEN_CREATE | APR_FOPEN_EXCL | APR_FOPEN_WRITE, APR_FPROT_OS_DEFAULT, gAPRPoolp); - if (s == APR_SUCCESS) + // 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; { - apr_file_close(f); - return true; + LLFile lock(lock_path, LLFile::out | LLFile::noreplace, ec); + if (lock) + { + return true; + } } - if (APR_STATUS_IS_EEXIST(s)) + + if (ec == std::errc::file_exists) { - // Steal a stale lock left by an aborted launch (older than the launch timeout). - apr_finfo_t finfo; - if (apr_stat(&finfo, lock_path.c_str(), APR_FINFO_MTIME, gAPRPoolp) == APR_SUCCESS && - (apr_time_now() - finfo.mtime) > 15 * APR_USEC_PER_SEC) + llstat st; + if (LLFile::stat(lock_path, &st) == 0 && + (time(nullptr) - st.st_mtime) > LOCK_STALE_SECONDS) { - apr_file_remove(lock_path.c_str(), gAPRPoolp); - if (apr_file_open(&f, lock_path.c_str(), - APR_FOPEN_CREATE | APR_FOPEN_EXCL | APR_FOPEN_WRITE, APR_FPROT_OS_DEFAULT, gAPRPoolp) == APR_SUCCESS) + LLFile::remove(lock_path); + LLFile lock(lock_path, LLFile::out | LLFile::noreplace, ec); + if (lock) { - apr_file_close(f); return true; } } diff --git a/indra/llplugin/slplugin/slplugin_daemon.cpp b/indra/llplugin/slplugin/slplugin_daemon.cpp index 9aab1ec74dc..8f0f0170a0f 100644 --- a/indra/llplugin/slplugin/slplugin_daemon.cpp +++ b/indra/llplugin/slplugin/slplugin_daemon.cpp @@ -159,7 +159,7 @@ 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) +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 @@ -178,15 +178,18 @@ int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path) 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. { - std::ofstream rv(rendezvous_path.c_str(), std::ios::trunc); + llofstream rv(rendezvous_path, std::ios::trunc); rv << control_port << std::endl; } // The rendezvous is now published, so release the spawn lock the launching // parent took - other parents can stop waiting and register. - std::remove((rendezvous_path + ".lock").c_str()); + LLFile::remove(rendezvous_lock_path); LL_INFOS("slplugin") << "daemon: control port " << control_port << " -> " << rendezvous_path << LL_ENDL; @@ -244,7 +247,7 @@ int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path) } apr_socket_close(control); - std::remove(rendezvous_path.c_str()); + LLFile::remove(rendezvous_path); LL_INFOS("slplugin") << "daemon: idle, exiting" << LL_ENDL; return 0; } diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index b06bf7ccf21..08d1a9dd6d7 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 @@ -1037,6 +1045,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; diff --git a/indra/llrender/llglheaders.h b/indra/llrender/llglheaders.h index 0b0c4736319..7f1e25f507a 100644 --- a/indra/llrender/llglheaders.h +++ b/indra/llrender/llglheaders.h @@ -152,6 +152,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 diff --git a/indra/llwindow/lldxhardware.cpp b/indra/llwindow/lldxhardware.cpp index baf8d195922..3e0e3acaffe 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,79 @@ 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 + } + if (!wglDXOpenDeviceNV) + { + LL_INFOS("RenderInit") << "WGL_NV_DX_interop2 not 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 5d7a1955a35..e0523966471 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/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 4abff070dd1..1512d831ba2 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 c79bb43eee4..88e5b36fe5e 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -64,7 +64,7 @@ target_link_libraries(media_plugin_cef_objs PUBLIC add_library(media_plugin_cef SHARED - $ + ${media_plugin_cef_SOURCE_FILES} ) target_link_libraries(media_plugin_cef @@ -74,6 +74,11 @@ target_link_libraries(media_plugin_cef ll::glib_headers ) +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) ### SLPluginCEF - dedicated host that statically links the CEF plugin instead of @@ -115,6 +120,11 @@ target_link_libraries(SLPluginCEF ll::pluginlibraries ) +if (WINDOWS) + # zero-copy paint producer: D3D11 keyed-mutex shared texture + target_link_libraries(SLPluginCEF d3d11 dxgi) +endif () + add_dependencies(SLPluginCEF media_plugin_cef) if (WINDOWS) @@ -177,14 +187,23 @@ 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 ) + target_link_libraries(SLPluginCEF + ${IOSURFACE_FRAMEWORK} + ${COREGRAPHICS_FRAMEWORK} + ) + # 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 diff --git a/indra/media_plugins/cef/media_plugin_cef.cpp b/indra/media_plugins/cef/media_plugin_cef.cpp index 49c9569d535..26795779167 100644 --- a/indra/media_plugins/cef/media_plugin_cef.cpp +++ b/indra/media_plugins/cef/media_plugin_cef.cpp @@ -45,8 +45,169 @@ #include #endif +#if LL_DARWIN +#include // accelerated paint shares an IOSurface by id +#endif + +#if LL_LINUX +#include +#include +#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; + } + + if (mMutex && SUCCEEDED(mMutex->AcquireSync(0, 1000))) + { + 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 +224,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(int fd, int format, int width, int height, + unsigned int stride, unsigned long long offset, 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); @@ -120,6 +286,22 @@ 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; + void* mViewerProcess; +#if LL_WINDOWS + CefAccelProducer* mAccelProducer; +#endif +#if LL_LINUX + // dma-buf fds dup'd from CEF, kept alive briefly so the viewer can re-open + // them via /proc//fd before they are closed. + std::deque mDmabufFdRing; +#endif }; //////////////////////////////////////////////////////////////////////////////// @@ -140,6 +322,12 @@ MediaPluginBase(host_send_func, host_user_data) mProxyHost = ""; mProxyPort = 0; mDisableGPU = false; + mUseAcceleratedPaint = false; + mHostPid = 0; + mViewerProcess = nullptr; +#if LL_WINDOWS + mAccelProducer = nullptr; +#endif mUseMockKeyChain = true; mDisableWebSecurity = false; mFileAccessFromFileUrls = false; @@ -169,6 +357,26 @@ 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 +#if LL_LINUX + for (int fd : mDmabufFdRing) + { + ::close(fd); + } + mDmabufFdRing.clear(); +#endif } //////////////////////////////////////////////////////////////////////////////// @@ -205,6 +413,126 @@ 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 +// 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. Share its global IOSurfaceID (an + // integer any process can IOSurfaceLookup), so no per-frame duplication or + // producer texture is needed - send the id every frame (CEF cycles a pool, + // so the id can change). The viewer looks it up and binds it. + if (!native_handle) + { + return; + } + uint32_t iosurface_id = IOSurfaceGetID((IOSurfaceRef)native_handle); + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", std::to_string((unsigned long long)iosurface_id)); + 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 (fd + plane layout) valid only +// for this callback. dup the fd and keep it alive briefly so the viewer can +// re-open it via /proc//fd/ (no SCM_RIGHTS side channel needed), +// then send the fd number + layout. Single plane only. +void MediaPluginCEF::onAcceleratedPaintDmabufCallback(int fd, int format, int width, int height, + unsigned int stride, unsigned long long offset, unsigned long long modifier) +{ + if (fd < 0) + { + return; + } + int dupfd = dup(fd); + if (dupfd < 0) + { + return; + } + mDmabufFdRing.push_back(dupfd); + while (mDmabufFdRing.size() > 4) + { + ::close(mDmabufFdRing.front()); + mDmabufFdRing.pop_front(); + } + + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", std::to_string((unsigned long long)dupfd)); + message.setValueS32("src_pid", (S32)getpid()); + message.setValueS32("format", format); + message.setValueS32("width", width); + message.setValueS32("height", height); + message.setValueS32("stride", (S32)stride); + message.setValue("offset", std::to_string(offset)); + message.setValue("modifier", std::to_string(modifier)); + sendMessage(message); +} +#endif + //////////////////////////////////////////////////////////////////////////////// // void MediaPluginCEF::onConsoleMessageCallback(std::string message, std::string source, int line) @@ -630,8 +958,24 @@ 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"); + // 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 (fd + layout), 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, std::placeholders::_7)); +#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 +1011,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 @@ -742,7 +1087,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/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 390b19347d7..24608989459 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -253,6 +253,7 @@ set(viewer_SOURCE_FILES llbrowsernotification.cpp llbuycurrencyhtml.cpp llcallingcard.cpp + llcefaccelinterop.cpp llchannelmanager.cpp #llchatbar.cpp llchathistory.cpp @@ -1021,6 +1022,7 @@ set(viewer_HEADER_FILES llbuycurrencyhtml.h llcallingcard.h llcapabilityprovider.h + llcefaccelinterop.h llchannelmanager.h #llchatbar.h llchathistory.h @@ -2002,9 +2004,19 @@ 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 diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp new file mode 100644 index 00000000000..4dfe75cad6c --- /dev/null +++ b/indra/newview/llcefaccelinterop.cpp @@ -0,0 +1,561 @@ +/** + * @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) +{ + (void)format; (void)stride; (void)offset; (void)modifier; (void)src_pid; + 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; + } + w->stable->QueryInterface(__uuidof(IDXGIKeyedMutex), (void**)&w->mutex); + + 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). + if (w->mutex) + { + if (FAILED(w->mutex->AcquireSync(0, 1000))) + { + return false; // producer busy this frame; try again next + } + ctx->CopyResource(w->local, w->stable); + ctx->Flush(); + w->mutex->ReleaseSync(0); + } + else + { + ctx->CopyResource(w->local, w->stable); + ctx->Flush(); + } + + // 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) +{ + (void)format; (void)stride; (void)offset; (void)modifier; (void)src_pid; + if (!mValid || !handle) + { + return false; + } + MacAccel* m = (MacAccel*)mImpl; + + // The "handle" is an IOSurfaceID (the surface changes each frame in CEF's + // pool); look it up and (re)bind it to our rectangle texture. + IOSurfaceRef surf = IOSurfaceLookup((IOSurfaceID)handle); + if (!surf) + { + return false; + } + if (m->surface) { CFRelease(m->surface); } + m->surface = surf; + m->width = (int)IOSurfaceGetWidth(surf); + m->height = (int)IOSurfaceGetHeight(surf); + + CGLContextObj cgl = CGLGetCurrentContext(); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, m->tex); + CGLError err = CGLTexImageIOSurface2D(cgl, GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, + m->width, m->height, GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, surf, 0); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + return err == kCGLNoError; +} + +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 // glEGLImageTargetTexture2DOES +#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); + } + + struct LinuxAccel + { + EGLDisplay display = EGL_NO_DISPLAY; + PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = nullptr; + PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = nullptr; + PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = nullptr; + + 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() +{ + LinuxAccel* l = new LinuxAccel(); + l->display = eglGetCurrentDisplay(); // requires the viewer to use an EGL context + l->eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); + l->eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); + l->glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); + + if (l->display == EGL_NO_DISPLAY || !l->eglCreateImageKHR || !l->glEGLImageTargetTexture2DOES) + { + LL_WARNS("Media") << "accelerated paint: EGL dma-buf import unavailable (no EGL context?); CPU paint" << LL_ENDL; + delete l; + return false; + } + 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) +{ + if (!mValid || !src_pid) + { + return false; + } + LinuxAccel* l = (LinuxAccel*)mImpl; + + // `handle` is the dma-buf fd number in the plugin process; re-open it here via + // /proc (the plugin keeps it alive briefly). + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/fd/%llu", src_pid, handle); + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + return false; + } + + // 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) + + EGLint attrs[] = { + EGL_WIDTH, width, + EGL_HEIGHT, height, + EGL_LINUX_DRM_FOURCC_EXT, (EGLint)fourcc, + EGL_DMA_BUF_PLANE0_FD_EXT, fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)offset, + EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)stride, + EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, (EGLint)(modifier & 0xFFFFFFFFu), + EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (EGLint)(modifier >> 32), + EGL_NONE + }; + + l->releaseImage(); + l->image = l->eglCreateImageKHR(l->display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)0, attrs); + close(fd); // EGL keeps its own reference once the image is created + if (l->image == EGL_NO_IMAGE_KHR) + { + LL_WARNS("Media") << "accelerated paint: eglCreateImageKHR(dma_buf) failed" << LL_ENDL; + return false; + } + + l->width = width; + l->height = height; + glBindTexture(GL_TEXTURE_2D, l->tex); + l->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) { return false; } +bool LLCEFAccelInterop::blitTo(unsigned int, int, int) { return false; } + +#endif diff --git a/indra/newview/llcefaccelinterop.h b/indra/newview/llcefaccelinterop.h new file mode 100644 index 00000000000..f6f627ecf6c --- /dev/null +++ b/indra/newview/llcefaccelinterop.h @@ -0,0 +1,67 @@ +/** + * @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; } + + // (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 the fd number in process `src_pid` (opened via + // /proc//fd/) and the dma-buf layout is given by format / + // stride / offset / modifier. The trailing args are 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); + + // 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/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index d7bed460c2b..2836a6fa091 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 f70f0038f0d..9a162e6c115 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -65,6 +65,9 @@ #include "llviewertexture.h" #include "llviewertexturelist.h" #include "llviewerwindow.h" +#include "llcefaccelinterop.h" +#include "llrender.h" +#include "llgl.h" #include "llvoavatar.h" #include "llvoavatarself.h" #include "llvovolume.h" @@ -258,8 +261,8 @@ LLViewerMedia::~LLViewerMedia() if (gDirUtilp) { const std::string rv = cefDaemonRendezvousPath(); - std::remove(rv.c_str()); - std::remove((rv + ".lock").c_str()); + LLFile::remove(rv); + LLFile::remove(rv + ".lock"); } } @@ -1707,6 +1710,13 @@ LLViewerMediaImpl::~LLViewerMediaImpl() { destroyMediaSource(); + if (mAccelInterop) + { + mAccelInterop->shutdown(); + delete mAccelInterop; + mAccelInterop = nullptr; + } + LLViewerMediaTexture::removeMediaImplFromTexture(mTextureId) ; setTextureID(); @@ -1777,6 +1787,9 @@ void LLViewerMediaImpl::destroyMediaSource() { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA; mNeedsNewTexture = true; + // The plugin's shared texture is going away; force a fresh interop bind when + // the next media source delivers a handle (even if the value is reused). + mAccelBoundHandle = 0; // Tell the viewer media texture it's no longer active LLViewerMediaTexture* oldImage = LLViewerTextureManager::findMediaTexture( mTextureId ); @@ -1861,10 +1874,14 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ { #if LL_WINDOWS const std::string cef_host_exe = "SLPluginCEF.exe"; + std::string cef_host_name = gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + cef_host_exe; +#elif LL_DARWIN + const std::string cef_host_exe = "SLPluginCEF.app/Contents/MacOS/SLPluginCEF"; + std::string cef_host_name = gDirUtilp->getAppRODataDir() + gDirUtilp->getDirDelimiter() + cef_host_exe; #else const std::string cef_host_exe = "SLPluginCEF"; -#endif std::string cef_host_name = gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + cef_host_exe; +#endif if (LLFile::isfile(cef_host_name)) { launcher_name = cef_host_name; @@ -1900,6 +1917,11 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ 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). + media_source->setUseAcceleratedPaint(plugin_basename == "media_plugin_cef" && + gSavedSettings.getBOOL("ALCefAcceleratedPaint")); 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()); @@ -3058,6 +3080,19 @@ 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 (!mSuspendUpdates && mVisible && mMediaSource->getAcceleratedPaintDirty()) + { + updateAcceleratedTexture(); + } + return; + } + if(!mMediaSource->textureValid()) { return; @@ -3211,6 +3246,69 @@ 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; + } + } + + // 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. + mMediaSource->clearAcceleratedPaintDirty(); + unsigned long long handle = mMediaSource->getAcceleratedPaintHandle(); + if (handle != 0 && handle != mAccelBoundHandle) + { + if (mAccelInterop->setStableTexture(handle, + mMediaSource->getAcceleratedPaintWidth(), + mMediaSource->getAcceleratedPaintHeight(), + mMediaSource->getAcceleratedPaintFormat(), + (unsigned int)mMediaSource->getAcceleratedPaintStride(), + mMediaSource->getAcceleratedPaintOffset(), + mMediaSource->getAcceleratedPaintModifier(), + mMediaSource->getAcceleratedPaintSrcPid())) + { + mAccelBoundHandle = handle; + } + else + { + return false; + } + } + + LLViewerMediaTexture* media_tex = updateMediaImage(); + if (!media_tex || !media_tex->getGLTexture()) + { + return false; + } + media_tex->setPlaying(true); + + 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. + return mAccelInterop->blitTo(tex_name, media_tex->getWidth(), media_tex->getHeight()); +} + ////////////////////////////////////////////////////////////////////////////////////////// LLViewerMediaTexture* LLViewerMediaImpl::updateMediaImage() { diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h index 68138e40cff..d8176f11657 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); @@ -487,6 +492,12 @@ class LLViewerMediaImpl 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/viewer_manifest.py b/indra/newview/viewer_manifest.py index 24fb7811dde..12cc830a225 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -904,7 +904,7 @@ def path_optional(src, dst): # our apps executable_path = {} - embedded_apps = [ (os.path.join("llplugin", "slplugin"), "SLPlugin.app") ] + embedded_apps = [ (os.path.join("llplugin", "slplugin"), "SLPlugin.app"), (os.path.join("media_plugins", "cef"), "SLPluginCEF.app") ] for app_bld_dir, app in embedded_apps: self.path2basename(os.path.join(os.pardir, app_bld_dir, self.args['configuration']), @@ -912,6 +912,21 @@ 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 + with self.prefix(dst=os.path.join( + "SLPluginCEF.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") + + with self.prefix(src=os.path.join(self.args['build'], os.pardir, 'dullahan', self.args['configuration'])): + self.path("DullahanHelper.app") + self.path("DullahanHelper (Alerts).app") + self.path("DullahanHelper (GPU).app") + self.path("DullahanHelper (Renderer).app") + self.path("DullahanHelper (Plugin).app") + # Dullahan helper apps go inside SLPlugin.app with self.prefix(dst=os.path.join( "SLPlugin.app", "Contents", "Frameworks")): From 1c5c72310ff51d494141d039a20b4dd1aee38c68 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 29 Jun 2026 07:55:18 -0400 Subject: [PATCH 06/10] CEF daemon: run the shared daemon in the macOS/Linux SLPluginCEF host Build slplugin_daemon.cpp into the non-Windows SLPluginCEF and route the platform main() through a per-host ll_run_slplugin_host() hook: the generic SLPlugin serves a single connection, while the CEF host marks the runtime persistent and dispatches slplugin_daemon_run() when launched with --daemon (mirroring the Windows bootstrap). Make SLPlugin and SLPluginCEF proper macOS app bundles with their own Info.plist and bundle identifiers. Co-Authored-By: Claude Opus 4.8 --- indra/llplugin/slplugin/CMakeLists.txt | 2 ++ indra/llplugin/slplugin/slplugin.cpp | 33 ++++++++++++++++-- indra/llplugin/slplugin/slplugin_generic.cpp | 12 +++++++ indra/llplugin/slplugin/slplugin_info.plist | 20 +++++++++-- indra/media_plugins/cef/CMakeLists.txt | 7 ++++ .../cef/SLPluginCEF-Info.plist.in | 26 ++++++++++++++ indra/media_plugins/cef/slplugin_cef.cpp | 34 +++++++++++++++++++ 7 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 indra/media_plugins/cef/SLPluginCEF-Info.plist.in diff --git a/indra/llplugin/slplugin/CMakeLists.txt b/indra/llplugin/slplugin/CMakeLists.txt index 9e88abacdf7..56c9ae12e89 100644 --- a/indra/llplugin/slplugin/CMakeLists.txt +++ b/indra/llplugin/slplugin/CMakeLists.txt @@ -56,6 +56,8 @@ elseif (DARWIN) 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 + MACOSX_BUNDLE_GUI_IDENTIFIER "org.achemyviewer.viewer.slplugin" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "org.achemyviewer.viewer.slplugin" XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym" ) endif () diff --git a/indra/llplugin/slplugin/slplugin.cpp b/indra/llplugin/slplugin/slplugin.cpp index 8ecfebbd38e..d372f65cc9f 100644 --- a/indra/llplugin/slplugin/slplugin.cpp +++ b/indra/llplugin/slplugin/slplugin.cpp @@ -54,6 +54,17 @@ LLPluginInstance::pluginInitFunction ll_get_static_plugin_init(); // 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 generic loader's definition +// (slplugin_generic.cpp) just calls slplugin_run(); the CEF host's +// (slplugin_cef.cpp) adds the persistent-runtime / daemon behaviour, keeping +// dullahan out of the generic host. 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" @@ -160,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 ) { @@ -188,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 @@ -199,8 +227,9 @@ int main(int argc, char **argv) signal(SIGEMT, &crash_handler); // emulate instruction executed #endif //LL_DARWIN - // Hand off to the shared host driver (the CEF bootstrap host reuses it too). - int rc = slplugin_run(port); + // 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(); diff --git a/indra/llplugin/slplugin/slplugin_generic.cpp b/indra/llplugin/slplugin/slplugin_generic.cpp index 26934373e5d..18cabee3eb2 100644 --- a/indra/llplugin/slplugin/slplugin_generic.cpp +++ b/indra/llplugin/slplugin/slplugin_generic.cpp @@ -32,7 +32,19 @@ #include "llplugininstance.h" +#include + LLPluginInstance::pluginInitFunction ll_get_static_plugin_init() { return nullptr; } + +// Defined in slplugin.cpp. +int slplugin_run(U32 port); + +// The generic 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/llplugin/slplugin/slplugin_info.plist b/indra/llplugin/slplugin/slplugin_info.plist index c4597380e0a..bf9cd54b4bf 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/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index 88e5b36fe5e..7cbd4b2a58c 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -99,6 +99,7 @@ else () add_executable(SLPluginCEF MACOSX_BUNDLE ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_daemon.cpp slplugin_cef.cpp $ ) @@ -106,6 +107,12 @@ else () target_sources(SLPluginCEF 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(SLPluginCEF PROPERTIES + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/SLPluginCEF-Info.plist.in + MACOSX_BUNDLE_GUI_IDENTIFIER "org.achemyviewer.viewer.slplugincef" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "org.achemyviewer.viewer.slplugincef" + ) endif () 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 00000000000..c86bdf00a99 --- /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/slplugin_cef.cpp b/indra/media_plugins/cef/slplugin_cef.cpp index f8eaddf2c74..e7c82dfaf2c 100644 --- a/indra/media_plugins/cef/slplugin_cef.cpp +++ b/indra/media_plugins/cef/slplugin_cef.cpp @@ -33,6 +33,10 @@ #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, @@ -43,3 +47,33 @@ 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; +} From 18bbce3202f32e0fcf6b4a634b98ca3586b220cd Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 29 Jun 2026 08:38:45 -0400 Subject: [PATCH 07/10] opengl-example: OpenGL 4.1 Core (glad 4.1 + dullahan bump) Bump the example's glad feature from gl-api-21 to gl-api-41 for the 4.1 Core context, and advance the dullahan submodule to the 4.1 Core / macOS IOSurface zero-copy example. Co-Authored-By: Claude Opus 4.8 Update dullahan: drop default framerate to 30 to reduce render load Co-Authored-By: Claude Opus 4.8 --- indra/dullahan | 2 +- indra/vcpkg.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/dullahan b/indra/dullahan index fb099f6b552..95e427b8eab 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit fb099f6b552807e0bd6952b7ae3743f8faeec7b2 +Subproject commit 95e427b8eab9eb283a6bbd185d6535dc44ee28a1 diff --git a/indra/vcpkg.json b/indra/vcpkg.json index 200d9f825f5..4940b4ca6bf 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" ] }, From 77b6b3504f82ed194b498dfc19ec1b6d961fc30c Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 29 Jun 2026 08:38:58 -0400 Subject: [PATCH 08/10] CEF accelerated paint (macOS): share the IOSurface via a mach port The viewer rendered media grey under accelerated paint on macOS: the plugin sent only the global IOSurfaceID and the viewer did IOSurfaceLookup, but CEF shares its accelerated-paint IOSurfaces between its GPU and browser processes via mach ports, not global ids, so a cross-process id lookup returns NULL and nothing is ever bound. Hand the surface over a mach channel instead: the viewer registers a bootstrap receive port named from its pid (LLCEFSurfaceReceiver); the plugin looks it up (host_pid is already in the init handshake) and mach_msg's an IOSurfaceCreateMachPort() right per frame, tagged with a new per-media accel_id so one receiver can demux many tabs. The viewer resolves it with IOSurfaceLookupFromMachPort and binds it as before. The receiver registers up front (not gated on a frame) since the plugin only produces once the port exists. Co-Authored-By: Claude Opus 4.8 --- indra/llplugin/llpluginclassmedia.cpp | 9 + indra/llplugin/llpluginclassmedia.h | 7 + indra/media_plugins/cef/media_plugin_cef.cpp | 122 +++++++++++- indra/newview/CMakeLists.txt | 2 + indra/newview/llcefaccelinterop.cpp | 8 +- indra/newview/llcefsurfacereceiver.cpp | 188 +++++++++++++++++++ indra/newview/llcefsurfacereceiver.h | 63 +++++++ indra/newview/llviewermedia.cpp | 26 ++- 8 files changed, 414 insertions(+), 11 deletions(-) create mode 100644 indra/newview/llcefsurfacereceiver.cpp create mode 100644 indra/newview/llcefsurfacereceiver.h diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 25c60b8b8d3..88ad3e2fae0 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -62,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 ; } @@ -91,6 +96,10 @@ bool LLPluginClassMedia::init(const std::string &launcher_filename, const std::s // 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 diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h index 384d8021fb0..1793b7e0388 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -184,6 +184,11 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner void setUseAcceleratedPaint(bool b) { mUseAcceleratedPaint = b; }; bool getUseAcceleratedPaint() const { return mUseAcceleratedPaint; }; + // 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 @@ -466,6 +471,8 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner // accelerated (zero-copy) paint - see setUseAcceleratedPaint / the getters above bool mUseAcceleratedPaint = false; + // 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; diff --git a/indra/media_plugins/cef/media_plugin_cef.cpp b/indra/media_plugins/cef/media_plugin_cef.cpp index 26795779167..e711097c7d2 100644 --- a/indra/media_plugins/cef/media_plugin_cef.cpp +++ b/indra/media_plugins/cef/media_plugin_cef.cpp @@ -46,7 +46,9 @@ #endif #if LL_DARWIN -#include // accelerated paint shares an IOSurface by id +#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 @@ -293,6 +295,7 @@ class MediaPluginCEF : // 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; @@ -324,6 +327,7 @@ MediaPluginBase(host_send_func, host_user_data) mDisableGPU = false; mUseAcceleratedPaint = false; mHostPid = 0; + mAccelId = 0; mViewerProcess = nullptr; #if LL_WINDOWS mAccelProducer = nullptr; @@ -416,6 +420,101 @@ 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 + // 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) @@ -474,17 +573,25 @@ void MediaPluginCEF::onAcceleratedPaintCallback(void* native_handle, int format, message.setValueS32("height", h); sendMessage(message); #elif LL_DARWIN - // macOS: the handle is an IOSurfaceRef. Share its global IOSurfaceID (an - // integer any process can IOSurfaceLookup), so no per-frame duplication or - // producer texture is needed - send the id every frame (CEF cycles a pool, - // so the id can change). The viewer looks it up and binds it. + // 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; } - uint32_t iosurface_id = IOSurfaceGetID((IOSurfaceRef)native_handle); + 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", std::to_string((unsigned long long)iosurface_id)); + message.setValue("handle", "0"); // unused on macOS (surface came via mach) message.setValueS32("format", format); message.setValueS32("width", width); message.setValueS32("height", height); @@ -962,6 +1069,7 @@ void MediaPluginCEF::receiveMessage(const char* message_string) // 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"); // 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)); diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 24608989459..de117d90792 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -254,6 +254,7 @@ set(viewer_SOURCE_FILES llbuycurrencyhtml.cpp llcallingcard.cpp llcefaccelinterop.cpp + llcefsurfacereceiver.cpp llchannelmanager.cpp #llchatbar.cpp llchathistory.cpp @@ -1023,6 +1024,7 @@ set(viewer_HEADER_FILES llcallingcard.h llcapabilityprovider.h llcefaccelinterop.h + llcefsurfacereceiver.h llchannelmanager.h #llchatbar.h llchathistory.h diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp index 4dfe75cad6c..f1507d1667c 100644 --- a/indra/newview/llcefaccelinterop.cpp +++ b/indra/newview/llcefaccelinterop.cpp @@ -310,9 +310,11 @@ bool LLCEFAccelInterop::setStableTexture(unsigned long long handle, int width, i } MacAccel* m = (MacAccel*)mImpl; - // The "handle" is an IOSurfaceID (the surface changes each frame in CEF's - // pool); look it up and (re)bind it to our rectangle texture. - IOSurfaceRef surf = IOSurfaceLookup((IOSurfaceID)handle); + // 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; diff --git a/indra/newview/llcefsurfacereceiver.cpp b/indra/newview/llcefsurfacereceiver.cpp new file mode 100644 index 00000000000..0c48b8055f2 --- /dev/null +++ b/indra/newview/llcefsurfacereceiver.cpp @@ -0,0 +1,188 @@ +/** + * @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 +} + +#else // non-macOS: no mach handoff (NT handle / dma-buf paths handle sharing) + +void LLCEFSurfaceReceiver::ensureStarted() {} +void* LLCEFSurfaceReceiver::takeLatest(int) { return nullptr; } + +#endif diff --git a/indra/newview/llcefsurfacereceiver.h b/indra/newview/llcefsurfacereceiver.h new file mode 100644 index 00000000000..3a6e64fedba --- /dev/null +++ b/indra/newview/llcefsurfacereceiver.h @@ -0,0 +1,63 @@ +/** + * @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 IOSurfaces handed over by the +// CEF media plugin(s) via mach ports. The bootstrap service 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 for rendezvous. +class LLCEFSurfaceReceiver +{ +public: + static LLCEFSurfaceReceiver& instance(); + + // Register the bootstrap receive port if not already (idempotent). MUST be + // called independently of frame delivery: the plugin only starts producing + // once this port exists, so waiting for the first frame to register would + // deadlock (no port -> no frames -> never registered). No-op on non-macOS. + void ensureStarted(); + + // Drain all pending surface messages (non-blocking), keeping only the newest + // surface per accel id, then hand off the newest for `accel_id`. Returns a + // +1-retained IOSurfaceRef (as an opaque pointer; the caller takes ownership + // and must CFRelease / hand to code that does) or nullptr if no new frame. + void* takeLatest(int accel_id); + +private: + LLCEFSurfaceReceiver() = default; +}; + +#endif // LL_LLCEFSURFACERECEIVER_H diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 9a162e6c115..73498dc0a93 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -66,6 +66,7 @@ #include "llviewertexturelist.h" #include "llviewerwindow.h" #include "llcefaccelinterop.h" +#include "llcefsurfacereceiver.h" #include "llrender.h" #include "llgl.h" #include "llvoavatar.h" @@ -3086,6 +3087,12 @@ void LLViewerMediaImpl::update() // thread and we're done. if (mMediaSource->getUseAcceleratedPaint()) { +#if LL_DARWIN + // Register the mach receive port up front (not gated on a frame): the + // plugin only starts producing once our bootstrap service exists, so + // waiting for the first frame to register would deadlock. + LLCEFSurfaceReceiver::instance().ensureStarted(); +#endif if (!mSuspendUpdates && mVisible && mMediaSource->getAcceleratedPaintDirty()) { updateAcceleratedTexture(); @@ -3266,10 +3273,26 @@ bool LLViewerMediaImpl::updateAcceleratedTexture() } } + 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) + { + mAccelInterop->setStableTexture((unsigned long long)(uintptr_t)surf, + mMediaSource->getAcceleratedPaintWidth(), + mMediaSource->getAcceleratedPaintHeight(), + mMediaSource->getAcceleratedPaintFormat(), + 0, 0, 0, 0); + } +#else // 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. - mMediaSource->clearAcceleratedPaintDirty(); unsigned long long handle = mMediaSource->getAcceleratedPaintHandle(); if (handle != 0 && handle != mAccelBoundHandle) { @@ -3289,6 +3312,7 @@ bool LLViewerMediaImpl::updateAcceleratedTexture() return false; } } +#endif LLViewerMediaTexture* media_tex = updateMediaImage(); if (!media_tex || !media_tex->getGLTexture()) From c71ef29cdf0f4a8d32ff84c2ad708ef5467d9428 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 29 Jun 2026 08:39:49 -0400 Subject: [PATCH 09/10] Default new CEF daemon mode and sandboxing on --- indra/newview/app_settings/settings_alchemy.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index aa5c9c476ac..cf03db9679c 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -11,7 +11,7 @@ Type Boolean Value - 0 + 1 ALCefDaemonEnabled @@ -22,7 +22,7 @@ Type Boolean Value - 0 + 1 ALCefDedicatedHost @@ -33,7 +33,7 @@ Type Boolean Value - 0 + 1 ALCefSandbox @@ -44,7 +44,7 @@ Type Boolean Value - 0 + 1 ALSceneExplorerActivateAction From 936c0b1c283d7d27a0918e8101f079bb42dec645 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 29 Jun 2026 08:59:34 -0400 Subject: [PATCH 10/10] Pump cocoa event loop in slplugin daemon for NSEvent loop servicing --- indra/llplugin/slplugin/slplugin_daemon.cpp | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/indra/llplugin/slplugin/slplugin_daemon.cpp b/indra/llplugin/slplugin/slplugin_daemon.cpp index 8f0f0170a0f..2d691ca6fba 100644 --- a/indra/llplugin/slplugin/slplugin_daemon.cpp +++ b/indra/llplugin/slplugin/slplugin_daemon.cpp @@ -48,6 +48,10 @@ #include "apr_network_io.h" +#if LL_DARWIN +#include "slplugin-objc.h" // LLCocoaPlugin: pump NSApp so macOS sees us alive +#endif + #include #include #include @@ -193,6 +197,14 @@ int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path_str) 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(); @@ -206,8 +218,15 @@ int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path_str) 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();) { @@ -243,6 +262,10 @@ int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path_str) idle_running = false; } +#if LL_DARWIN + cocoa_interface.deleteAutoReleasePool(); +#endif + ms_sleep(10); }