diff --git a/indra/dullahan b/indra/dullahan index 32f74a4664..95e427b8ea 160000 --- a/indra/dullahan +++ b/indra/dullahan @@ -1 +1 @@ -Subproject commit 32f74a4664699c405bce97dacecc2f4b9fdba4f8 +Subproject commit 95e427b8eab9eb283a6bbd185d6535dc44ee28a1 diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 9403f24469..88ad3e2fae 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -33,6 +33,12 @@ #include "llpluginmessageclasses.h" #include "llcontrol.h" +#if LL_WINDOWS +#include // _getpid (host pid for accelerated-paint handle dup) +#else +#include // getpid +#endif + extern LLControlGroup gSavedSettings; #if LL_DARWIN || LL_LINUX extern bool gHiDPISupport; @@ -56,6 +62,11 @@ LLPluginClassMedia::LLPluginClassMedia(LLPluginClassMediaOwner *owner) mOwner = owner; reset(); + // Unique per-media id for the macOS accelerated-paint mach-port demux. Media + // sources are created on the main thread, so a plain counter is fine. + static int sNextAccelId = 1; + mAccelId = sNextAccelId++; + //debug use mDeleteOK = true ; } @@ -75,11 +86,25 @@ bool LLPluginClassMedia::init(const std::string &launcher_filename, const std::s mPlugin = LLPluginProcessParent::create(this); mPlugin->setSleepTime(mSleepTime); + mPlugin->setUseDaemon(mUseDaemon, mDaemonRendezvous); // Queue up the media init message -- it will be sent after all the currently queued messages. LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "init"); message.setValue("target", mTarget); message.setValueReal("factor", mZoomFactor); + // Zero-copy paint: ask for GPU shared-texture handles, and hand the plugin + // this (viewer) process id so it can DuplicateHandle the shared texture into + // us across the process boundary. + message.setValueBoolean("accelerated_paint", mUseAcceleratedPaint); + // macOS shares the accelerated-paint IOSurface over a mach channel; the plugin + // rendezvous via the bootstrap name derived from host_pid, and tags each frame + // with accel_id so the viewer demuxes it back to this media. + message.setValueS32("accel_id", mAccelId); +#if LL_WINDOWS + message.setValueS32("host_pid", (S32)_getpid()); +#else + message.setValueS32("host_pid", (S32)getpid()); +#endif sendMessage(message); mPlugin->init(launcher_filename, plugin_dir, plugin_filename, debug); @@ -1049,6 +1074,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 d6f07c1632..1793b7e038 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -172,6 +172,42 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner bool getDisableTimeout() { return mPlugin?mPlugin->getDisableTimeout():false; }; void setDisableTimeout(bool disable) { if(mPlugin) mPlugin->setDisableTimeout(disable); }; + // Route this media instance through the shared CEF daemon host, using a + // user-writable rendezvous path (see LLPluginProcessParent::setUseDaemon). + // Set before init(). + void setUseDaemon(bool use_daemon, const std::string& rendezvous_path = std::string()) + { mUseDaemon = use_daemon; mDaemonRendezvous = rendezvous_path; if(mPlugin) mPlugin->setUseDaemon(use_daemon, rendezvous_path); }; + bool getUseDaemon() const { return mUseDaemon; }; + + // Zero-copy paint: ask the plugin to deliver a GPU shared-texture handle + // (duplicated into this process) instead of CPU pixels. Set before init(). + void setUseAcceleratedPaint(bool b) { mUseAcceleratedPaint = b; }; + bool getUseAcceleratedPaint() const { return mUseAcceleratedPaint; }; + + // Per-media id sent to the plugin in init() and tagged onto each macOS + // IOSurface mach-port message, so the process-global viewer-side receiver can + // demux surfaces from many tabs/plugin processes back to the right media. + int getAccelId() const { return mAccelId; } + + // The plugin's stable shared texture: a native handle already duplicated into + // THIS process (Windows: a D3D11 shared-texture HANDLE), plus its + // cef_color_type format and coded size. The handle is PERSISTENT - it is only + // (re)sent when the plugin recreates the texture (per size), so it is kept, + // not consumed: the consumer compares it against what it last bound and only + // re-opens when it changes. mAcceleratedPaintDirty marks a fresh frame. + bool getAcceleratedPaintDirty() const { return mAcceleratedPaintDirty; }; + int getAcceleratedPaintFormat() const { return mAcceleratedPaintFormat; }; + int getAcceleratedPaintWidth() const { return mAcceleratedPaintWidth; }; + int getAcceleratedPaintHeight() const { return mAcceleratedPaintHeight; }; + unsigned long long getAcceleratedPaintHandle() const { return mAcceleratedPaintHandle; }; + void clearAcceleratedPaintDirty() { mAcceleratedPaintDirty = false; }; + // 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(); @@ -430,6 +466,23 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner LLPluginProcessParent::ptr_t mPlugin; + bool mUseDaemon = false; + std::string mDaemonRendezvous; + + // accelerated (zero-copy) paint - see setUseAcceleratedPaint / the getters above + bool mUseAcceleratedPaint = false; + // Unique per-media id (macOS mach-port demux); assigned at construction. + int mAccelId = 0; + unsigned long long mAcceleratedPaintHandle = 0; // native handle, dup'd into this process + int mAcceleratedPaintFormat = 0; + int mAcceleratedPaintWidth = 0; + int mAcceleratedPaintHeight = 0; + bool mAcceleratedPaintDirty = false; + // Linux dma-buf only: + int mAcceleratedPaintStride = 0; + unsigned long long mAcceleratedPaintOffset = 0; + unsigned long long mAcceleratedPaintModifier = 0; + int mAcceleratedPaintSrcPid = 0; LLRect mDirtyRect; diff --git a/indra/llplugin/llplugininstance.cpp b/indra/llplugin/llplugininstance.cpp index 99036d6dc2..5fc255a489 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 5f7af30130..10703cae9d 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/llpluginprocesschild.cpp b/indra/llplugin/llpluginprocesschild.cpp index 7c6dad05ab..390e7cbf42 100644 --- a/indra/llplugin/llpluginprocesschild.cpp +++ b/indra/llplugin/llpluginprocesschild.cpp @@ -55,13 +55,28 @@ LLPluginProcessChild::~LLPluginProcessChild() { sendMessageToPlugin(LLPluginMessage("base", "cleanup")); - // IMPORTANT: under some (unknown) circumstances the library unload triggered when mInstance is deleted - // appears to fail and lock up which means that a given instance of the slplugin process never exits. - // This is bad, especially when users try to update their version of SL - it fails because the slplugin - // process as well as a bunch of plugin specific files are locked and cannot be overwritten. - exit(0); - //delete mInstance; - //mInstance = NULL; + if (mDaemonMode) + { + // Daemon tab: this process hosts many tabs, so exit() here would kill + // every other tab (the "close one media, all of them die" crash). The + // library-unload lockup the exit(0) below guards against cannot happen + // in the daemon either - daemon plugins are statically linked, so + // there is no DSO to unload and ~LLPluginInstance is a no-op. Delete + // the instance directly. (Normally the graceful unload path has + // already nulled mInstance and we never get here.) + delete mInstance; + mInstance = NULL; + } + else + { + // IMPORTANT: under some (unknown) circumstances the library unload triggered when mInstance is deleted + // appears to fail and lock up which means that a given instance of the slplugin process never exits. + // This is bad, especially when users try to update their version of SL - it fails because the slplugin + // process as well as a bunch of plugin specific files are locked and cannot be overwritten. + exit(0); + //delete mInstance; + //mInstance = NULL; + } } } @@ -86,23 +101,34 @@ void LLPluginProcessChild::idle(void) { // Once we have hit the shutdown request state checking for errors might put us in a spurious // error state... don't do that. + // A lost parent socket means this tab is going away. In the daemon a + // hard STATE_ERROR would reap the tab with its browser still live in + // the shared CEF runtime (and, pre-unload, leave mInstance set), which + // crashes every tab. Instead run the same graceful unload as a normal + // shutdown so the browser closes cleanly first, then the daemon reaps + // us. The single-process host keeps the old hard-error behaviour. + const EState socket_dead_state = + (mDaemonMode && mInstance != NULL) ? STATE_SHUTDOWNREQ : STATE_ERROR; + if (APR_STATUS_IS_EOF(mSocketError)) { // Plugin socket was closed. This covers both normal plugin termination and host crashes. - setState(STATE_ERROR); + setState(socket_dead_state); } else if (mSocketError != APR_SUCCESS) { - LL_INFOS("Plugin") << "message pipe is in error state (" << mSocketError << "), moving to STATE_ERROR" << LL_ENDL; - setState(STATE_ERROR); + LL_INFOS("Plugin") << "message pipe is in error state (" << mSocketError << "), moving to " + << (socket_dead_state == STATE_SHUTDOWNREQ ? "STATE_SHUTDOWNREQ" : "STATE_ERROR") << LL_ENDL; + setState(socket_dead_state); } - if ((mState > STATE_INITIALIZED) && (mMessagePipe == NULL)) + if ((mState > STATE_INITIALIZED) && (mState < STATE_SHUTDOWNREQ) && (mMessagePipe == NULL)) { // The pipe has been closed -- we're done. // TODO: This could be slightly more subtle, but I'm not sure it needs to be. - LL_INFOS("Plugin") << "message pipe went away, moving to STATE_ERROR" << LL_ENDL; - setState(STATE_ERROR); + LL_INFOS("Plugin") << "message pipe went away, moving to " + << (socket_dead_state == STATE_SHUTDOWNREQ ? "STATE_SHUTDOWNREQ" : "STATE_ERROR") << LL_ENDL; + setState(socket_dead_state); } } diff --git a/indra/llplugin/llpluginprocesschild.h b/indra/llplugin/llpluginprocesschild.h index 5de0f90209..cefdf37c8a 100644 --- a/indra/llplugin/llpluginprocesschild.h +++ b/indra/llplugin/llpluginprocesschild.h @@ -50,6 +50,13 @@ class LLPluginProcessChild: public LLPluginMessagePipeOwner, public LLPluginInst void sleep(F64 seconds); void pump(); + // Mark this child as a tab inside the shared daemon host (one process serving + // many tabs). In daemon mode the child must never exit() the process on its + // own teardown (that would kill every other tab) and a lost parent socket is + // handled with a graceful plugin unload rather than a hard error. Set before + // the first idle(). + void setDaemonMode(bool daemon) { mDaemonMode = daemon; } + // returns true if the plugin is in the steady state (processing messages) bool isRunning(void); @@ -104,6 +111,7 @@ class LLPluginProcessChild: public LLPluginMessagePipeOwner, public LLPluginInst LLTimer mHeartbeat; F64 mSleepTime; F64 mCPUElapsed; + bool mDaemonMode = false; bool mBlockingRequest; bool mBlockingResponseReceived; std::queue mMessageQueue; diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index 5856f9ddd6..0fab498a80 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -38,6 +38,12 @@ #include "workqueue.h" #include "llapr.h" +#include "llfile.h" + +#include "apr_network_io.h" + +#include +#include //virtual LLPluginProcessParentOwner::~LLPluginProcessParentOwner() @@ -494,8 +500,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) @@ -659,7 +717,26 @@ void LLPluginProcessParent::idle(void) break; case STATE_EXITING: - if (! LLProcess::isRunning(mProcess)) + if (mUseDaemon && !mProcess) + { + // Daemon mode: we do not own the host process, so there is no + // LLProcess to wait on. Instead wait for the shared daemon's + // tab to finish its graceful teardown and drop its end of the + // socket (EOF / socket error), or for the lockup timeout. + // Tearing our socket down before the tab has unloaded its + // browser would abort the shared-runtime teardown and crash + // every other tab in the daemon. + if (mSocketError != APR_SUCCESS || !mMessagePipe) + { + setState(STATE_CLEANUP); + } + else if (pluginLockedUp()) + { + LL_WARNS("Plugin") << "timeout waiting for daemon tab to exit, cleaning up" << LL_ENDL; + setState(STATE_CLEANUP); + } + } + else if (! LLProcess::isRunning(mProcess)) { setState(STATE_CLEANUP); } @@ -1263,7 +1340,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 +1371,111 @@ bool LLPluginProcessParent::pluginLockedUp() return (mHeartbeat.getStarted() && mHeartbeat.hasExpired()); } +std::string LLPluginProcessParent::daemonRendezvousPath() const +{ + // Caller-supplied, user-writable path (see setUseDaemon). All CEF tabs in a + // viewer are given the same path so they agree on one daemon; it must not be + // the install/plugin dir, which may be read-only. + return mDaemonRendezvous; +} + +// static +U32 LLPluginProcessParent::readDaemonControlPort(const std::string& path) +{ + llifstream f(path); + if (!f.is_open()) + { + return 0; + } + U32 port = 0; + 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) +{ + // A stale lock left by an aborted launch is stolen once it is older than this. + static const time_t LOCK_STALE_SECONDS = 15; + + // Atomic create-exclusive: LLFile::noreplace maps to O_EXCL / CREATE_NEW, so + // exactly one parent wins the create even if several race here. The LLFile is + // closed when it leaves scope; the lock file itself persists until the daemon + // publishes the rendezvous (slplugin_daemon_run removes it) or it is stolen + // as stale below. + std::error_code ec; + { + LLFile lock(lock_path, LLFile::out | LLFile::noreplace, ec); + if (lock) + { + return true; + } + } + + if (ec == std::errc::file_exists) + { + llstat st; + if (LLFile::stat(lock_path, &st) == 0 && + (time(nullptr) - st.st_mtime) > LOCK_STALE_SECONDS) + { + LLFile::remove(lock_path); + LLFile lock(lock_path, LLFile::out | LLFile::noreplace, ec); + if (lock) + { + return true; + } + } + } + return false; +} + +LLPluginProcessParent::EDaemonDisp LLPluginProcessParent::daemonDiscoverOrRegister() +{ + const std::string rv = daemonRendezvousPath(); + + // 1. A daemon already running? Register our tab with it (it connects back). + U32 control_port = readDaemonControlPort(rv); + if (control_port && registerWithDaemon(control_port)) + { + LL_INFOS("Plugin") << "registered CEF tab with daemon (control port " << control_port << ")" << LL_ENDL; + return DAEMON_CONNECTED; + } + + // 2. No reachable daemon (missing or stale rendezvous). Serialize launching. + if (acquireSpawnLock(rv + ".lock")) + { + return DAEMON_NEED_SPAWN; + } + + // 3. Another parent is launching the daemon; wait and retry next idle. + return DAEMON_WAIT; +} + diff --git a/indra/llplugin/llpluginprocessparent.h b/indra/llplugin/llpluginprocessparent.h index ea604ca8d7..d0a75f2a68 100644 --- a/indra/llplugin/llpluginprocessparent.h +++ b/indra/llplugin/llpluginprocessparent.h @@ -119,6 +119,18 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner bool getDisableTimeout() { return mDisableTimeout; }; void setDisableTimeout(bool disable) { mDisableTimeout = disable; }; + // In daemon mode this parent connects to a shared host process it does not + // own (mProcess stays null), so liveness is tracked via the socket/heartbeat + // rather than LLProcess::isRunning. Has no effect unless the daemon launch + // path is taken. Set before init(). + bool getUseDaemon() const { return mUseDaemon; }; + // rendezvous_path must be a user-writable absolute path supplied by the + // caller (NOT the install/plugin dir, which may be read-only): the daemon + // writes its control port there and later tabs read it to find the running + // daemon. Daemon mode is skipped if the path is empty. + void setUseDaemon(bool use_daemon, const std::string& rendezvous_path = std::string()) + { mUseDaemon = use_daemon; mDaemonRendezvous = rendezvous_path; }; + void setLaunchTimeout(F32 timeout) { mPluginLaunchTimeout = timeout; }; void setLockupTimeout(F32 timeout) { mPluginLockupTimeout = timeout; }; @@ -160,6 +172,24 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner bool pluginLockedUp(); bool pluginLockedUpOrQuit(); + // --- shared CEF daemon (mUseDaemon) discover-or-spawn --- + enum EDaemonDisp + { + DAEMON_CONNECTED, // registered with a running daemon; it connects back + DAEMON_NEED_SPAWN, // no daemon; this parent should launch one (holds the lock) + DAEMON_WAIT // another parent is launching the daemon; retry next idle + }; + // path of the file the daemon writes its control port to (per plugin dir) + std::string daemonRendezvousPath() const; + // try to register a tab with a running daemon, else decide to spawn/wait + EDaemonDisp daemonDiscoverOrRegister(); + // read the daemon control port from the rendezvous file (0 if none/invalid) + static U32 readDaemonControlPort(const std::string& path); + // connect to the daemon control port and send our listen port (mBoundPort) + bool registerWithDaemon(U32 control_port); + // atomically take the spawn lock (stealing a stale one); true if we got it + static bool acquireSpawnLock(const std::string& lock_path); + bool accept(); LLSocket::ptr_t mListenSocket; @@ -169,6 +199,11 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner LLProcess::Params mProcessParams; LLProcessPtr mProcess; bool mProcessCreationRequested = false; + // true when this parent talks to a shared daemon host instead of its own + // launched process (see setUseDaemon) + bool mUseDaemon = false; + // user-writable path the daemon publishes its control port to (caller-supplied) + std::string mDaemonRendezvous; std::string mPluginFile; std::string mPluginDir; diff --git a/indra/llplugin/slplugin/CMakeLists.txt b/indra/llplugin/slplugin/CMakeLists.txt index 11a165cd14..56c9ae12e8 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 ) @@ -55,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 81a27cf2e5..d372f65cc9 100644 --- a/indra/llplugin/slplugin/slplugin.cpp +++ b/indra/llplugin/slplugin/slplugin.cpp @@ -33,6 +33,7 @@ #include "llpluginprocesschild.h" #include "llpluginmessage.h" +#include "llplugininstance.h" #include "llerrorcontrol.h" #include "llapr.h" #include "llstring.h" @@ -41,6 +42,29 @@ #include using namespace std; +// Returns the statically-linked plugin init entry point for this host, 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(); + +// 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); + +// 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" @@ -147,6 +171,11 @@ int main(int argc, char **argv) // LLError::logToFile("slplugin.log"); } + // Non-empty only for a CEF daemon launch (parsed from argv below); empty for + // the generic host and on Windows (where the bootstrap entry handles daemon + // mode instead). + std::string daemon_rendezvous; + #if LL_WINDOWS if( strlen( lpCmdLine ) == 0 ) { @@ -175,6 +204,18 @@ int main(int argc, char **argv) LL_ERRS("slplugin") << "port number must be numeric" << LL_ENDL; } + // Optional "--daemon " tells a CEF host to run as the shared + // multi-tab daemon (on Windows this is handled by the bootstrap entry). The + // rendezvous path is taken as a single argument. + for (int i = 2; i + 1 < argc; ++i) + { + if (std::string(argv[i]) == "--daemon") + { + daemon_rendezvous = argv[i + 1]; + break; + } + } + // Catch signals that most kinds of crashes will generate, and exit cleanly so the system crash dialog isn't shown. signal(SIGILL, &crash_handler); // illegal instruction signal(SIGFPE, &crash_handler); // floating-point exception @@ -184,12 +225,30 @@ int main(int argc, char **argv) #endif # if LL_DARWIN signal(SIGEMT, &crash_handler); // emulate instruction executed +#endif //LL_DARWIN + // Hand off to the per-host entry (generic: single connection; CEF: persistent + // runtime, and the shared daemon when a rendezvous path was supplied). + int rc = ll_run_slplugin_host(port, daemon_rendezvous); + + ll_cleanup_apr(); + + return rc; +} + +int slplugin_run(U32 port) +{ +#if LL_DARWIN LLCocoaPlugin cocoa_interface; cocoa_interface.setupCocoa(); cocoa_interface.createAutoReleasePool(); #endif //LL_DARWIN + // If this is a dedicated single-plugin host (e.g. SLPluginCEF), register the + // statically-linked plugin entry point so LLPluginInstance::load() calls it + // directly instead of dlopen()ing a plugin library. + LLPluginInstance::setStaticInitFunction(ll_get_static_plugin_init()); + LLPluginProcessChild *plugin = new LLPluginProcessChild(); plugin->init(port); @@ -261,8 +320,5 @@ int main(int argc, char **argv) } delete plugin; - ll_cleanup_apr(); - - return 0; } diff --git a/indra/llplugin/slplugin/slplugin_daemon.cpp b/indra/llplugin/slplugin/slplugin_daemon.cpp new file mode 100644 index 0000000000..2d691ca6fb --- /dev/null +++ b/indra/llplugin/slplugin/slplugin_daemon.cpp @@ -0,0 +1,276 @@ +/** + * @file slplugin_daemon.cpp + * @brief Multi-connection host driver: one process serving N plugin tabs. + * + * The shared tab-manager daemon. Where slplugin_run() serves a single parent + * connection, this serves many: each parent that wants a tab connects to the + * daemon's control port and sends its own listen port as decimal text + '\n'; + * the daemon spawns an LLPluginProcessChild that connects back to that port (one + * tab). Because every tab lives in this one process, they transparently share a + * single CEF runtime (dullahan_runtime - see Phase 1), which is the whole point. + * + * The first tab's parent port is passed at launch so it is served without the + * control channel. The control port is written to the rendezvous file so later + * parents can discover a running daemon (the viewer-side discover-or-spawn that + * drives this is built separately). + * + * This driver is plugin-agnostic; the CEF host (SLPluginCEF) selects it via its + * --daemon argument. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llpluginprocesschild.h" +#include "llplugininstance.h" + +#include "llapr.h" +#include "lltimer.h" +#include "llstring.h" + +#include "apr_network_io.h" + +#if LL_DARWIN +#include "slplugin-objc.h" // LLCocoaPlugin: pump NSApp so macOS sees us alive +#endif + +#include +#include +#include + +// Idle this many seconds with zero live tabs before the daemon exits, so a brief +// gap between the last tab closing and the next opening keeps the warm runtime. +static const F64 DAEMON_IDLE_TIMEOUT = 10.0; + +// Hard ceiling on concurrent tabs in one daemon. The viewer is the primary cap +// (PluginInstancesTotal -> mMaxIntances keeps excess media PRIORITY_UNLOADED, so +// no media source -> no tab); this is only a self-protection backstop so a buggy +// or hostile parent cannot make one process spawn unbounded browsers. Set well +// above any realistic viewer cap. +static const size_t DAEMON_MAX_TABS = 64; + +namespace +{ + // Create a non-blocking TCP listen socket on the loopback for parents to + // register tabs on. Returns the socket (and its bound port) or nullptr. + apr_socket_t* createControlListener(U32& out_port) + { + out_port = 0; + apr_socket_t* sock = nullptr; + if (apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP, gAPRPoolp) != APR_SUCCESS) + { + return nullptr; + } + + apr_socket_opt_set(sock, APR_SO_REUSEADDR, 1); + + apr_sockaddr_t* addr = nullptr; + if (apr_sockaddr_info_get(&addr, "127.0.0.1", APR_INET, 0, 0, gAPRPoolp) != APR_SUCCESS || + apr_socket_bind(sock, addr) != APR_SUCCESS || + apr_socket_listen(sock, SOMAXCONN) != APR_SUCCESS) + { + apr_socket_close(sock); + return nullptr; + } + + apr_sockaddr_t* bound = nullptr; + if (apr_socket_addr_get(&bound, APR_LOCAL, sock) != APR_SUCCESS) + { + apr_socket_close(sock); + return nullptr; + } + out_port = bound->port; + + // Non-blocking so the accept poll below never stalls the tab loop. + apr_socket_opt_set(sock, APR_SO_NONBLOCK, 1); + apr_socket_timeout_set(sock, 0); + return sock; + } + + // Read the decimal listen-port a registering parent sent, spawn a tab for it. + void acceptRegistration(apr_socket_t* listener, std::vector& tabs) + { + apr_socket_t* incoming = nullptr; + if (apr_socket_accept(&incoming, listener, gAPRPoolp) != APR_SUCCESS || !incoming) + { + return; // APR_EAGAIN etc. - no pending registration + } + + // Self-protection backstop (see DAEMON_MAX_TABS): refuse to grow past the + // ceiling. The parent's connect-back never gets a tab, so it heartbeat- + // times-out and reports the media failed - which the viewer should have + // prevented via its own instance cap. Just drop the registration. + if (tabs.size() >= DAEMON_MAX_TABS) + { + LL_WARNS("slplugin") << "daemon: tab ceiling (" << DAEMON_MAX_TABS + << ") reached; refusing registration" << LL_ENDL; + apr_socket_close(incoming); + return; + } + + // Blocking read of the short port line (the registration is tiny). + apr_socket_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->setDaemonMode(true); + tab->init(parent_port); + tabs.push_back(tab); + LL_INFOS("slplugin") << "daemon: new tab connecting to parent port " << parent_port + << " (" << tabs.size() << " live)" << LL_ENDL; + } + } + apr_socket_close(incoming); + } +} + +// Defined in slplugin.cpp. +int slplugin_run(U32 port); + +// Defined per host (slplugin_cef.cpp for SLPluginCEF): the statically-linked +// plugin entry point. Each daemon tab must use it instead of dlopen()ing the +// plugin DLL, otherwise the tab loads a SECOND copy of the plugin (and its +// statically-linked dullahan) - a different dullahan_runtime than the host set +// the sandbox info on, so the tab runs unsandboxed via dullahan_host. +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init(); + +// Run the daemon: serve the first tab (first_port) plus any later tabs that +// register on the control port (written to rendezvous_path). Returns when no +// tab has been live for DAEMON_IDLE_TIMEOUT. +int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path_str) +{ + // Register the statically-linked plugin so every tab's LLPluginInstance::load + // calls it directly instead of dlopen()ing the plugin DLL. Without this the + // daemon tabs load a separate plugin+dullahan copy whose runtime never got + // the host's sandbox info (the cause of the daemon running unsandboxed). + // slplugin_run() does the same for the single-tab host. + LLPluginInstance::setStaticInitFunction(ll_get_static_plugin_init()); + + U32 control_port = 0; + apr_socket_t* control = createControlListener(control_port); + if (!control) + { + // No control channel: fall back to single-tab behaviour so the first + // parent still gets served rather than failing outright. + LL_WARNS("slplugin") << "daemon: control listener failed; serving a single tab" << LL_ENDL; + return slplugin_run(first_port); + } + + std::filesystem::path rendezvous_path = fsyspath(rendezvous_path_str); + std::filesystem::path rendezvous_lock_path = fsyspath(rendezvous_path_str + ".lock"); + + // Publish the control port for discover-or-spawn. Write then flush+close so a + // racing parent reads a complete value. + { + 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. + LLFile::remove(rendezvous_lock_path); + LL_INFOS("slplugin") << "daemon: control port " << control_port + << " -> " << rendezvous_path << LL_ENDL; + +#if LL_DARWIN + // Without a serviced Cocoa run loop the WindowServer flags this process as + // "Not Responding" (even while it works). Pump NSApp each iteration, like the + // single-tab slplugin_run() loop does. + LLCocoaPlugin cocoa_interface; + cocoa_interface.setupCocoa(); +#endif + + std::vector tabs; + { + LLPluginProcessChild* first = new LLPluginProcessChild(); + first->setDaemonMode(true); + first->init(first_port); + tabs.push_back(first); + } + + LLTimer idle_timer; + bool idle_running = false; + + while (true) + { +#if LL_DARWIN + cocoa_interface.createAutoReleasePool(); +#endif + acceptRegistration(control, tabs); + +#if LL_DARWIN + cocoa_interface.processEvents(); +#endif + + // Service every tab, reaping finished ones. + for (std::vector::iterator it = tabs.begin(); it != tabs.end();) + { + LLPluginProcessChild* tab = *it; + tab->idle(); + tab->pump(); + if (tab->isDone()) + { + delete tab; + it = tabs.erase(it); + LL_INFOS("slplugin") << "daemon: tab closed (" << tabs.size() << " live)" << LL_ENDL; + } + else + { + ++it; + } + } + + if (tabs.empty()) + { + if (!idle_running) + { + idle_timer.reset(); + idle_running = true; + } + else if (idle_timer.getElapsedTimeF64() > DAEMON_IDLE_TIMEOUT) + { + break; + } + } + else + { + idle_running = false; + } + +#if LL_DARWIN + cocoa_interface.deleteAutoReleasePool(); +#endif + + ms_sleep(10); + } + + apr_socket_close(control); + LLFile::remove(rendezvous_path); + LL_INFOS("slplugin") << "daemon: idle, exiting" << LL_ENDL; + return 0; +} diff --git a/indra/llplugin/slplugin/slplugin_generic.cpp b/indra/llplugin/slplugin/slplugin_generic.cpp new file mode 100644 index 0000000000..18cabee3eb --- /dev/null +++ b/indra/llplugin/slplugin/slplugin_generic.cpp @@ -0,0 +1,50 @@ +/** + * @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" + +#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 c4597380e0..bf9cd54b4b 100644 --- a/indra/llplugin/slplugin/slplugin_info.plist +++ b/indra/llplugin/slplugin/slplugin_info.plist @@ -1,12 +1,28 @@ - + CFBundleDevelopmentRegion English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CSResourcesFileMapped + + LSFileQuarantineEnabled + LSUIElement - 1 + diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index b06bf7ccf2..08d1a9dd6d 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 0b0c473631..7f1e25f507 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 baf8d19592..3e0e3acaff 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 5d7a1955a3..e052396647 100644 --- a/indra/llwindow/lldxhardware.h +++ b/indra/llwindow/lldxhardware.h @@ -57,6 +57,24 @@ class LLDXHardware // matters mainly for Intel iGPUs. Must run with GL initialized. Shared // by LLWindowWin32's window thread (checkDXMem) and the SDL backend. static void updateVRAMBudgetFromDXGI(); + + // --- Shared D3D11 <-> OpenGL interop (WGL_NV_DX_interop2) --- + // One D3D11 device + one wglDXOpenDeviceNV interop device for the whole + // process, brought up once after the GL context and WGL extensions exist. + // The zero-copy CEF media surfaces share these instead of each creating their + // own. Must be called with the GL context current. Returns true if available. + // No-op / false on platforms without WGL_NV_DX_interop2. + bool initGLDXInterop(); + void cleanupGLDXInterop(); + bool hasGLDXInterop() const { return mGLDXInteropDevice != nullptr; } + void* getD3DDevice() const { return mD3DDevice; } // ID3D11Device1* + void* getD3DContext() const { return mD3DContext; } // ID3D11DeviceContext* + void* getGLDXInteropDevice() const { return mGLDXInteropDevice; } // wglDXOpenDeviceNV handle + +private: + void* mD3DDevice = nullptr; + void* mD3DContext = nullptr; + void* mGLDXInteropDevice = nullptr; }; extern LLDXHardware gDXHardware; diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 4abff070dd..1512d831ba 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -1025,6 +1025,9 @@ void LLWindowWin32::close() gGLManager.shutdownGL(); } + // Tear down the shared D3D11<->GL interop device while its context is current. + gDXHardware.cleanupGLDXInterop(); + LL_DEBUGS("Window") << "Releasing Context" << LL_ENDL; if (mhRC) { @@ -1210,6 +1213,9 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo mRefreshRate = current_refresh; gGLManager.shutdownGL(); + // Tear down the shared D3D11<->GL interop device (recreated below for the new + // context) while the old context is still current. + gDXHardware.cleanupGLDXInterop(); //destroy gl context if (mhRC) { @@ -1719,6 +1725,13 @@ const S32 max_format = (S32)num_formats - 1; return false; } + // Bring up the process-shared D3D11 <-> GL interop device once, here on the + // main thread with the render context current and the WGL extensions loaded. + // Zero-copy CEF media surfaces share it instead of each creating a private + // D3D device. Best-effort - failure just means accelerated paint falls back + // to the CPU path. + gDXHardware.initGLDXInterop(); + // Setup Tracy gpu context { LL_PROFILER_GPU_CONTEXT; diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt index 5033c1609c..7cbd4b2a58 100644 --- a/indra/media_plugins/cef/CMakeLists.txt +++ b/indra/media_plugins/cef/CMakeLists.txt @@ -49,6 +49,19 @@ 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} @@ -61,8 +74,66 @@ 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 +### 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 + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_daemon.cpp + slplugin_cef.cpp + slplugin_cef_bootstrap.cpp + $ + ) +else () + add_executable(SLPluginCEF + MACOSX_BUNDLE + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin.cpp + ${CMAKE_SOURCE_DIR}/llplugin/slplugin/slplugin_daemon.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) + + 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 () + +target_link_libraries(SLPluginCEF + media_plugin_base + dullahan + ll::cef + ll::glib_headers + llplugin + llmessage + llcommon + ll::pluginlibraries +) + +if (WINDOWS) + # zero-copy paint producer: D3D11 keyed-mutex shared texture + target_link_libraries(SLPluginCEF d3d11 dxgi) +endif () + +add_dependencies(SLPluginCEF media_plugin_cef) + if (WINDOWS) set_target_properties(media_plugin_cef PROPERTIES @@ -71,6 +142,24 @@ if (WINDOWS) ) target_link_options(media_plugin_cef PRIVATE /MANIFEST:NO /IGNORE:4099) + # 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_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( @@ -105,14 +194,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/SLPluginCEF-Info.plist.in b/indra/media_plugins/cef/SLPluginCEF-Info.plist.in new file mode 100644 index 0000000000..c86bdf00a9 --- /dev/null +++ b/indra/media_plugins/cef/SLPluginCEF-Info.plist.in @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CSResourcesFileMapped + + LSFileQuarantineEnabled + + LSUIElement + + + diff --git a/indra/media_plugins/cef/media_plugin_cef.cpp b/indra/media_plugins/cef/media_plugin_cef.cpp index 49c9569d53..e711097c7d 100644 --- a/indra/media_plugins/cef/media_plugin_cef.cpp +++ b/indra/media_plugins/cef/media_plugin_cef.cpp @@ -45,8 +45,171 @@ #include #endif +#if LL_DARWIN +#include // accelerated paint hands the viewer an IOSurface +#include // ...shared cross-process via a mach send right +#include // ...rendezvous'd through the bootstrap server +#endif + +#if LL_LINUX +#include +#include +#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 +226,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 +288,23 @@ class MediaPluginCEF : VolumeCatcher mVolumeCatcher; F32 mCurVolume; dullahan* mCEFLib; + + // accelerated (zero-copy) paint: deliver GPU shared-texture handles to the + // viewer instead of CPU pixels. mHostPid is the viewer process, mViewerProcess + // its opened handle (with PROCESS_DUP_HANDLE) used to DuplicateHandle the + // shared texture across the boundary. + bool mUseAcceleratedPaint; + int mHostPid; + int mAccelId; // per-media id echoed in each macOS surface mach message + void* mViewerProcess; +#if LL_WINDOWS + CefAccelProducer* mAccelProducer; +#endif +#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 +325,13 @@ MediaPluginBase(host_send_func, host_user_data) mProxyHost = ""; mProxyPort = 0; mDisableGPU = false; + mUseAcceleratedPaint = false; + mHostPid = 0; + mAccelId = 0; + mViewerProcess = nullptr; +#if LL_WINDOWS + mAccelProducer = nullptr; +#endif mUseMockKeyChain = true; mDisableWebSecurity = false; mFileAccessFromFileUrls = false; @@ -169,6 +361,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 +417,229 @@ 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) +{ +#if LL_WINDOWS + if (!native_handle || !mHostPid) + { + return; + } + + // Open the viewer process once (cached) so we can duplicate handles into it. + if (!mViewerProcess) + { + mViewerProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, (DWORD)mHostPid); + if (!mViewerProcess) + { + LL_WARNS("media") << "accelerated paint: OpenProcess(viewer pid " << mHostPid + << ") failed: " << GetLastError() << LL_ENDL; + return; + } + } + + // Bring up the D3D producer once. CEF hands a different pooled texture each + // frame; the producer copies it into ONE persistent keyed-mutex shared + // texture so we only duplicate a handle to the viewer when that texture is + // (re)created, not every frame. + if (!mAccelProducer) + { + mAccelProducer = new CefAccelProducer(); + if (!mAccelProducer->init()) + { + LL_WARNS("media") << "accelerated paint: D3D producer init failed" << LL_ENDL; + delete mAccelProducer; + mAccelProducer = nullptr; + return; + } + } + + HANDLE stable_handle = nullptr; + bool recreated = false; + int w = 0, h = 0, fmt = 0; + if (!mAccelProducer->produce(native_handle, mViewerProcess, stable_handle, recreated, w, h, fmt)) + { + return; + } + + // Tell the viewer a new frame is ready. The handle field is the stable + // texture's viewer-side handle ONLY when it was just (re)created (sent once + // per size); otherwise "0" means "same texture, new frame". Carried as a + // decimal string so the full 64-bit value survives the LLSD message. + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", recreated ? std::to_string((unsigned long long)(uintptr_t)stable_handle) + : std::string("0")); + message.setValueS32("format", fmt); + message.setValueS32("width", w); + message.setValueS32("height", h); + sendMessage(message); +#elif LL_DARWIN + // macOS: the handle is an IOSurfaceRef shared (by CEF) via mach ports, so a + // global-id lookup in the viewer cannot work. Hand the surface over as a mach + // send right on our side channel, then post the usual message as the viewer's + // per-frame "dirty" trigger (the surface itself arrives out-of-band). + if (!native_handle) + { + return; + } + CefMacSurfaceSender& sender = macSurfaceSender(); + if (!sender.connect(mHostPid)) + { + return; // viewer's receive port not registered yet; retry next frame + } + if (!sender.send(mAccelId, (IOSurfaceRef)native_handle, width, height, format)) + { + return; + } + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "accelerated_paint"); + message.setValue("handle", "0"); // unused on macOS (surface came via mach) + message.setValueS32("format", format); + message.setValueS32("width", width); + message.setValueS32("height", height); + sendMessage(message); +#else + // Linux uses the dma-buf callback path instead (see onAcceleratedPaintDmabufCallback). + (void)native_handle; (void)format; (void)width; (void)height; +#endif +} + +#if LL_LINUX +//////////////////////////////////////////////////////////////////////////////// +// Linux zero-copy paint: CEF hands us a dma-buf (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 +1065,25 @@ void MediaPluginCEF::receiveMessage(const char* message_string) { if (message_name == "init") { + // Zero-copy paint: the viewer asks for GPU shared-texture handles + // and tells us its process id so we can DuplicateHandle into it. + mUseAcceleratedPaint = message_in.getValueBoolean("accelerated_paint"); + mHostPid = message_in.getValueS32("host_pid"); + mAccelId = message_in.getValueS32("accel_id"); + // 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 +1119,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 +1195,10 @@ void MediaPluginCEF::receiveMessage(const char* message_string) message.setValueS32("default_width", 1024); message.setValueS32("default_height", 1024); message.setValueS32("depth", mDepth); - message.setValueU32("internalformat", GL_RGB); + // Accelerated paint copies the BGRA shared texture into the media + // texture with glCopyImageSubData, which needs matching 32-bit + // (RGBA8) storage; the CPU path uploads BGRA into RGB as before. + message.setValueU32("internalformat", mUseAcceleratedPaint ? GL_RGBA8 : GL_RGB); message.setValueU32("format", GL_BGRA); message.setValueU32("type", GL_UNSIGNED_BYTE); message.setValueBoolean("coords_opengl", true); diff --git a/indra/media_plugins/cef/slplugin_cef.cpp b/indra/media_plugins/cef/slplugin_cef.cpp new file mode 100644 index 0000000000..e7c82dfaf2 --- /dev/null +++ b/indra/media_plugins/cef/slplugin_cef.cpp @@ -0,0 +1,79 @@ +/** + * @file slplugin_cef.cpp + * @brief Static-plugin hook for the dedicated SLPluginCEF host. + * + * SLPluginCEF statically links the CEF media plugin (and CEF itself) instead of + * dlopen()ing media_plugin_cef at runtime. media_plugin_base provides the + * exported LLPluginInitEntryPoint; hand its address to slplugin so + * LLPluginInstance::load() calls it directly. This avoids dlopen of libcef + * (which exhausts the static TLS block on Linux) and gives the Windows sandbox + * the single-image host it requires. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llplugininstance.h" + +#include "dullahan.h" + +#include + +// Exported by media_plugin_base, which is statically linked into this host. +extern "C" int LLPluginInitEntryPoint(LLPluginInstance::sendMessageFunction host_send_func, + void *host_user_data, + LLPluginInstance::sendMessageFunction *plugin_send_func, + void **plugin_user_data); + +LLPluginInstance::pluginInitFunction ll_get_static_plugin_init() +{ + return &LLPluginInitEntryPoint; +} + +// Defined in slplugin.cpp / slplugin_daemon.cpp. +int slplugin_run(U32 port); +int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path); + +// CEF host entry handed control by the platform main() (slplugin.cpp). This is +// the non-Windows analog of slplugin_cef_bootstrap.cpp's run_cef_host (on Windows +// the bootstrap entry is used instead, and CEF sub-processes are dispatched there +// by CefExecuteProcess; on macOS/Linux the DullahanHelper bundles / dullahan_host +// dispatch them, so there is nothing to do here for sub-processes). +int ll_run_slplugin_host(U32 port, const std::string& daemon_rendezvous) +{ + // Keep one process-global CEF runtime up for the whole life of this host (a + // dedicated single tab, or the shared daemon serving many) and shut it down + // exactly once below. Without this a zero-browser gap - e.g. the login web + // surface closing just before the next opens - would CefShutdown and the next + // CefInitialize would crash (CEF init is once-per-process). The macOS sandbox + // toggle is applied separately when the plugin builds its settings + // (media_plugin_cef.cpp). + dullahan::setPersistentRuntime(true); + + const int rc = daemon_rendezvous.empty() + ? slplugin_run(port) + : slplugin_daemon_run(port, daemon_rendezvous); + + // Persistent host: release() left CEF running, so tear it down once now, + // before the process exits, for a clean teardown. + dullahan::shutdownRuntime(); + return rc; +} diff --git a/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp new file mode 100644 index 0000000000..3ce1711205 --- /dev/null +++ b/indra/media_plugins/cef/slplugin_cef_bootstrap.cpp @@ -0,0 +1,155 @@ +/** + * @file slplugin_cef_bootstrap.cpp + * @brief Windows CEF-sandbox bootstrap entry point for SLPluginCEF. + * + * On Windows the dedicated CEF host is built as SLPluginCEF.dll and loaded by + * CEF's bootstrap executable (shipped renamed to SLPluginCEF.exe). The bootstrap + * creates the Windows sandbox and calls our exported RunWinMain, handing us the + * sandbox_info. We: + * 1. run CEF's sub-process dispatch (CefExecuteProcess) so the SAME executable + * image services the renderer/GPU/utility sub-processes - which the + * Chromium sandbox requires (broker and targets must be one image); + * 2. for the browser process, hand the sandbox_info to the dullahan runtime + * (so its CefInitialize runs sandboxed) and run the normal slplugin host + * message loop. + * + * The client DLL does NOT link cef_sandbox.lib (M138+ ships that only inside the + * bootstrap executables), so there is no static-CRT requirement here. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Alchemy Viewer Project. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "cef_app.h" +#include "cef_sandbox_win.h" // RunWinMain prototype + CEF_BOOTSTRAP_EXPORT +#include "cef_version_info.h" + +#include "dullahan.h" +#include "dullahan_runtime.h" + +#include "llapr.h" +#include "llerrorcontrol.h" +#include "llstring.h" + +// Defined in slplugin.cpp - the shared plugin<->parent host message loop. +int slplugin_run(U32 port); +// Defined in slplugin_daemon.cpp - the multi-tab daemon host loop. +int slplugin_daemon_run(U32 first_port, const std::string& rendezvous_path); + +namespace +{ + int run_cef_host(HINSTANCE hInstance, LPTSTR lpCmdLine, void* sandbox_info) + { + // Developer escape: SLPLUGIN_CEF_NO_SANDBOX disables the sandbox (so the + // child processes are attachable/debuggable) without rebuilding. Passing + // a null sandbox_info makes dullahan_runtime fall back to no_sandbox plus + // the dullahan_host helper. + if (getenv("SLPLUGIN_CEF_NO_SANDBOX")) + { + sandbox_info = nullptr; + } + + // CEF sub-process dispatch. For a renderer/GPU/utility sub-process this + // blocks until it exits and returns its exit code; for the browser + // process it returns -1 and we continue. The dullahan runtime is the + // process CefApp. + CefMainArgs main_args(hInstance); + CefRefPtr app = &dullahan_runtime::instance(); + int exit_code = CefExecuteProcess(main_args, app, sandbox_info); + if (exit_code >= 0) + { + return exit_code; + } + + // Browser process: make the sandbox info available to the runtime's + // CefInitialize, and tell dullahan that this host dispatches CEF + // sub-processes itself (CEF re-launches this image -> RunWinMain -> + // CefExecuteProcess), so it never uses the dullahan_host helper - + // independent of whether the sandbox is active. Then run the host loop. + dullahan::setSandboxInfo(sandbox_info); + dullahan::setHostHandlesSubprocesses(true); + // This host keeps one CEF runtime for its whole life (a dedicated single + // tab, or the shared daemon serving many) and shuts it down once below + // (shutdownRuntime). Without this the browser refcount would CefShutdown + // on a zero-browser gap - e.g. the login web surface closing just before + // the next opens - and the next CefInitialize would crash (CEF init is + // once-per-process). + dullahan::setPersistentRuntime(true); + + ll_init_apr(); + { + LLError::initForApplication(".", "."); + LLError::setDefaultLevel(LLError::LEVEL_INFO); + } + + // RunWinMain's lpCmdLine is LPTSTR (wide under UNICODE); 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); + + const std::string first = cmd.substr(0, cmd.find(' ')); + U32 port = 0; + if (first.empty() || !LLStringUtil::convertToU32(first, port) || !port) + { + LL_WARNS("slplugin") << "SLPluginCEF: missing/invalid launcher port" << LL_ENDL; + ll_cleanup_apr(); + return 1; + } + + std::string rendezvous; + const size_t dpos = cmd.find("--daemon"); + if (dpos != std::string::npos) + { + rendezvous = cmd.substr(dpos + 8); // strlen("--daemon") + LLStringUtil::trim(rendezvous); + } + + const int rc = rendezvous.empty() ? slplugin_run(port) + : slplugin_daemon_run(port, rendezvous); + + // The host loop has returned (all tabs gone / daemon idle-exit). Because + // this is a persistent host, release() left CEF running - shut it down + // exactly once now, before the process exits, for a clean teardown. + dullahan::shutdownRuntime(); + + ll_cleanup_apr(); + return rc; + } +} + +// Exported entry point the CEF bootstrap executable calls. version_info is +// provided by the bootstrap and not needed here. +extern "C" CEF_BOOTSTRAP_EXPORT int RunWinMain(HINSTANCE hInstance, + LPTSTR lpCmdLine, + int /*nCmdShow*/, + void* sandbox_info, + cef_version_info_t* /*version_info*/) +{ + return run_cef_host(hInstance, lpCmdLine, sandbox_info); +} diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 662af9a61b..de117d9079 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -253,6 +253,8 @@ set(viewer_SOURCE_FILES llbrowsernotification.cpp llbuycurrencyhtml.cpp llcallingcard.cpp + llcefaccelinterop.cpp + llcefsurfacereceiver.cpp llchannelmanager.cpp #llchatbar.cpp llchathistory.cpp @@ -1021,6 +1023,8 @@ set(viewer_HEADER_FILES llbuycurrencyhtml.h llcallingcard.h llcapabilityprovider.h + llcefaccelinterop.h + llcefsurfacereceiver.h llchannelmanager.h #llchatbar.h llchathistory.h @@ -2002,13 +2006,24 @@ if (NOT DISABLE_WEBRTC) endif() endif() +if (DARWIN) + find_library(IOSURFACE_FRAMEWORK IOSurface) + find_library(COREGRAPHICS_FRAMEWORK CoreGraphics) + target_link_libraries(${VIEWER_BINARY_NAME} + ${IOSURFACE_FRAMEWORK} + ${COREGRAPHICS_FRAMEWORK} + ) +endif() + # These are the generated targets that are copied for packaging that we do not directly link to # We special case for windows due to targets incompatible with Debug builds if (WINDOWS) + target_link_libraries(${VIEWER_BINARY_NAME} d3d11 dxgi ) add_custom_target(copy_input_dependencies DEPENDS stage_third_party_libs SLPlugin + $<$:$> $<$:$> $ $<$:$> @@ -2019,6 +2034,7 @@ else() DEPENDS stage_third_party_libs SLPlugin + $ $ $ $ diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 417199ad9b..cf03db9679 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 + 1 + + ALCefDaemonEnabled + + Comment + Host all browser (CEF) media instances in a single shared tab-manager daemon process instead of one process per instance. Requires a viewer restart. (CEF daemon rework, off by default.) + Persist + 1 + Type + Boolean + Value + 1 + + ALCefDedicatedHost + + Comment + Launch browser (CEF) media via the dedicated SLPluginCEF host, which statically links the CEF plugin instead of dlopen()ing it (avoids the Linux static-TLS crash; prerequisite for the sandbox). Requires a viewer restart. (CEF daemon rework, off by default.) + Persist + 1 + Type + Boolean + Value + 1 + + ALCefSandbox + + Comment + Enable the Chromium sandbox for CEF browser sub-processes (renderer/GPU/utility). Requires a viewer restart. (CEF daemon rework, off by default.) + Persist + 1 + Type + Boolean + Value + 1 + ALSceneExplorerActivateAction Comment diff --git a/indra/newview/llcefaccelinterop.cpp b/indra/newview/llcefaccelinterop.cpp new file mode 100644 index 0000000000..f1507d1667 --- /dev/null +++ b/indra/newview/llcefaccelinterop.cpp @@ -0,0 +1,563 @@ +/** + * @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 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; + } + 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 0000000000..f6f627ecf6 --- /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/llcefsurfacereceiver.cpp b/indra/newview/llcefsurfacereceiver.cpp new file mode 100644 index 0000000000..0c48b8055f --- /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 0000000000..3a6e64fedb --- /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/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index d7bed460c2..2836a6fa09 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -366,7 +366,7 @@ bool LLSyntaxDefCache::writeCacheFile(const std::string &fileSpec, const LLSD& c bool binary(content_ref.isBinary()); std::ios_base::openmode mode(binary ? (std::ios_base::out | std::ios_base::binary) : std::ios_base::out); - std::ofstream file(fileSpec.c_str(), mode); + llofstream file(fileSpec, mode); if (!file.is_open()) { diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 381b1fa498..73498dc0a9 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -65,6 +65,10 @@ #include "llviewertexture.h" #include "llviewertexturelist.h" #include "llviewerwindow.h" +#include "llcefaccelinterop.h" +#include "llcefsurfacereceiver.h" +#include "llrender.h" +#include "llgl.h" #include "llvoavatar.h" #include "llvoavatarself.h" #include "llvovolume.h" @@ -168,6 +172,12 @@ static LLViewerMedia::impl_list sViewerMediaImplList; static LLViewerMedia::impl_id_map sViewerMediaTextureIDMap; static LLTimer sMediaCreateTimer; static const F32 LLVIEWERMEDIA_CREATE_DELAY = 1.0f; +// Shared-CEF-daemon crash recovery: cap on consecutive re-init attempts before a +// daemon-mode media gives up, and the backoff schedule between them (so a daemon +// that crashes on every launch can't spin the viewer respawning it forever). +static const S32 DAEMON_RECOVERY_MAX_ATTEMPTS = 5; +static const F32 DAEMON_RECOVERY_BASE_DELAY = 2.0f; // seconds, multiplied by attempt # +static const F32 DAEMON_RECOVERY_MAX_DELAY = 30.0f; // seconds, backoff ceiling static F32 sGlobalVolume = 1.0f; static bool sForceUpdate = false; static LLUUID sOnlyAudibleTextureID = LLUUID::null; @@ -213,6 +223,19 @@ static bool sViewerMediaMuteListObserverInitialized = false; /*static*/ const char* LLViewerMedia::SHOW_MEDIA_WITHIN_PARCEL_SETTING = "MediaShowWithinParcel"; /*static*/ const char* LLViewerMedia::SHOW_MEDIA_OUTSIDE_PARCEL_SETTING = "MediaShowOutsideParcel"; +namespace +{ + // Per-viewer-instance CEF daemon rendezvous file: user-writable logs dir + // (never the read-only install dir) plus this viewer's PID so separate + // viewer instances don't share a daemon. newSourceFromMediaType() hands this + // exact path to setUseDaemon(); the cleanup in ~LLViewerMedia() recomputes + // it to remove the file the (force-killed) daemon cannot remove itself. + std::string cefDaemonRendezvousPath() + { + return gDirUtilp->getExpandedFilename(LL_PATH_LOGS, llformat("SLPluginCEF_%d.daemon", LLApp::getPid())); + } +} + LLViewerMedia::LLViewerMedia(): mAnyMediaShowing(false), mAnyMediaPlaying(false), @@ -231,6 +254,17 @@ LLViewerMedia::~LLViewerMedia() delete mSpareBrowserMediaSource; mSpareBrowserMediaSource = NULL; } + + // The shared CEF daemon lives in this viewer's job object, so it is + // force-killed on exit and never reaches its own rendezvous cleanup. Remove + // the rendezvous (and any stale spawn lock) here. No-op if daemon mode was + // never used or the files are already gone. + if (gDirUtilp) + { + const std::string rv = cefDaemonRendezvousPath(); + LLFile::remove(rv); + LLFile::remove(rv + ".lock"); + } } // static @@ -1677,6 +1711,13 @@ LLViewerMediaImpl::~LLViewerMediaImpl() { destroyMediaSource(); + if (mAccelInterop) + { + mAccelInterop->shutdown(); + delete mAccelInterop; + mAccelInterop = nullptr; + } + LLViewerMediaTexture::removeMediaImplFromTexture(mTextureId) ; setTextureID(); @@ -1747,6 +1788,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 ); @@ -1818,6 +1862,38 @@ 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. 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("ALCefDaemonEnabled"))) + { +#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"; + std::string cef_host_name = gDirUtilp->getLLPluginDir() + gDirUtilp->getDirDelimiter() + cef_host_exe; +#endif + 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(); @@ -1834,6 +1910,19 @@ 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()); + + // 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()); @@ -2915,6 +3004,18 @@ bool LLViewerMediaImpl::canNavigateBack() void LLViewerMediaImpl::update() { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA; //LL_RECORD_BLOCK_TIME(FTM_MEDIA_DO_UPDATE); + + // Shared CEF daemon crash recovery: once the backoff has elapsed, clear the + // failure latch so the normal load path below recreates the source (which + // respawns or reconnects the daemon). isForcedUnloaded() pins mPriority to + // PRIORITY_UNLOADED while mMediaSourceFailed is set, so this is what lets the + // tab come back after a daemon crash. + if (mDaemonRecoveryPending && mDaemonRecoveryTimer.hasExpired()) + { + mDaemonRecoveryPending = false; + mMediaSourceFailed = false; + } + if(mMediaSource == NULL) { if(mPriority == LLPluginClassMedia::PRIORITY_UNLOADED) @@ -2980,6 +3081,25 @@ void LLViewerMediaImpl::update() return; } + // Zero-copy paint: the plugin delivers a GPU shared texture instead of CPU + // pixels, so this media never uses the shm/setSubImage upload path below. + // Pull the latest frame straight into the media GL texture on this (main) + // thread and we're done. + if (mMediaSource->getUseAcceleratedPaint()) + { +#if LL_DARWIN + // 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(); + } + return; + } + if(!mMediaSource->textureValid()) { return; @@ -3100,13 +3220,19 @@ void LLViewerMediaImpl::doMediaTexUpdate(LLViewerMediaTexture* media_tex, U8* da // -Cosmic,2023-04-04 // Allocate GL texture based on LLImageRaw but do NOT copy to GL LLGLuint tex_name = 0; - if (!media_tex->createGLTexture(0, raw, 0, true, LLGLTexture::OTHER, true, &tex_name)) { - LL_WARNS("Media") << "Failed to create media texture" << LL_ENDL; - } + // Phase 0 baseline: this GL allocate + CPU->GPU upload is exactly the + // per-surface cost the accelerated-paint (shared-texture) path removes, + // so isolate it under its own zone for before/after comparison. + LL_PROFILE_ZONE_NAMED_CATEGORY_MEDIA("media texUpload"); + if (!media_tex->createGLTexture(0, raw, 0, true, LLGLTexture::OTHER, true, &tex_name)) + { + LL_WARNS("Media") << "Failed to create media texture" << LL_ENDL; + } - // copy just the subimage covered by the image raw to GL - media_tex->setSubImage(data, data_width, data_height, x_pos, y_pos, width, height, tex_name); + // copy just the subimage covered by the image raw to GL + media_tex->setSubImage(data, data_width, data_height, x_pos, y_pos, width, height, tex_name); + } if (sync) { @@ -3127,6 +3253,86 @@ void LLViewerMediaImpl::updateImagesMediaStreams() { } +////////////////////////////////////////////////////////////////////////////////////////// +// Zero-copy paint consumer. Bring the plugin's GPU shared texture straight into +// the media GL texture with no CPU round-trip. Main thread (has the GL context). +bool LLViewerMediaImpl::updateAcceleratedTexture() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_MEDIA; + + // Bring up the interop first so a failure here doesn't consume the frame; the + // plugin's handle is persistent and we retry next frame. + if (!mAccelInterop) + { + mAccelInterop = new LLCEFAccelInterop(); + if (!mAccelInterop->init()) + { + delete mAccelInterop; + mAccelInterop = nullptr; + return false; + } + } + + mMediaSource->clearAcceleratedPaintDirty(); +#if LL_DARWIN + // macOS: CEF's IOSurface is shared via a mach port (no cross-process global + // id), so the surface arrives out-of-band through the mach receiver, demuxed + // by this media's accel id. Drain the newest and (re)bind it; the interop + // takes ownership of the +1-retained IOSurfaceRef (and releases it on a + // failed bind). If no new surface this frame, keep the current binding. + void* surf = LLCEFSurfaceReceiver::instance().takeLatest(mMediaSource->getAccelId()); + if (surf) + { + 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. + 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; + } + } +#endif + + 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() { @@ -3410,6 +3616,24 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla // Reset the last known state of the media to defaults. resetPreviousMediaState(); + // Shared CEF daemon: one daemon crash drops every tab at once. Rather + // than leaving all of them permanently failed, schedule a bounded, + // backed-off re-init for daemon-mode media. The first impl to recreate + // its source wins the spawn lock and respawns the daemon + // (discover-or-spawn); the rest reconnect to it as new tabs. The retry + // cap + backoff keep a daemon that crashes on launch from spinning. + if (plugin && plugin->getUseDaemon() && mDaemonRecoveryAttempts < DAEMON_RECOVERY_MAX_ATTEMPTS) + { + mDaemonRecoveryAttempts++; + F32 delay = llmin(DAEMON_RECOVERY_BASE_DELAY * (F32)mDaemonRecoveryAttempts, DAEMON_RECOVERY_MAX_DELAY); + mDaemonRecoveryPending = true; + mDaemonRecoveryTimer.reset(); + mDaemonRecoveryTimer.setTimerExpirySec(delay); + LL_WARNS("Media") << "CEF daemon tab failed; scheduling recovery attempt " + << mDaemonRecoveryAttempts << "/" << DAEMON_RECOVERY_MAX_ATTEMPTS + << " in " << delay << "s" << LL_ENDL; + } + LLSD args; args["PLUGIN"] = LLMIMETypes::implType(mCurrentMimeType); // SJB: This is getting called every frame if the plugin fails to load, continuously respawining the alert! @@ -3452,6 +3676,10 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla { LL_DEBUGS("Media") << "MEDIA_EVENT_NAVIGATE_COMPLETE, uri is: " << plugin->getNavigateURI() << LL_ENDL; + // A page finished loading: the (possibly just-respawned) daemon tab is + // healthy again, so clear the crash-recovery backoff counter. + mDaemonRecoveryAttempts = 0; + std::string url = plugin->getNavigateURI(); if(getNavState() == MEDIANAVSTATE_BEGUN) { diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h index a3cb9ec93e..d8176f1165 100644 --- a/indra/newview/llviewermedia.h +++ b/indra/newview/llviewermedia.h @@ -50,6 +50,7 @@ class LLViewerMediaTexture; class LLMediaEntry; class LLVOVolume; class LLMimeDiscoveryResponder; +class LLCEFAccelInterop; typedef LLPointer viewer_media_t; /////////////////////////////////////////////////////////////////////////////// @@ -208,6 +209,10 @@ class LLViewerMediaImpl void createMediaSource(); void destroyMediaSource(); + // Zero-copy paint: pull the plugin's GPU shared texture into the media + // texture (no CPU upload). Main thread only. No-op / returns false if the + // platform interop isn't available. + bool updateAcceleratedTexture(); void setMediaType(const std::string& media_type); bool initializeMedia(const std::string& mime_type); bool initializePlugin(const std::string& media_type); @@ -480,6 +485,19 @@ class LLViewerMediaImpl bool mNavigateRediscoverType; bool mNavigateServerRequest; bool mMediaSourceFailed; + // Shared-CEF-daemon crash recovery: a daemon crash drops every tab at once, + // so instead of leaving each media permanently failed we re-init on a backoff + // (which respawns the daemon and reconnects the tab). Bounded by a retry cap + // that resets on a clean load. Only used for daemon-mode CEF media. + S32 mDaemonRecoveryAttempts = 0; + bool mDaemonRecoveryPending = false; + LLTimer mDaemonRecoveryTimer; + // Zero-copy paint consumer (created lazily on the first accelerated frame); + // owns the D3D/GL interop that aliases the plugin's shared texture. + LLCEFAccelInterop* mAccelInterop = nullptr; + // The stable-texture handle currently bound into the interop; re-bind only + // when the plugin's persistent handle differs from this. + unsigned long long mAccelBoundHandle = 0; F32 mRequestedVolume; F32 mPreviousVolume; bool mIsMuted; diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 014765f5a3..12cc830a22 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") @@ -903,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']), @@ -911,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")): diff --git a/indra/vcpkg.json b/indra/vcpkg.json index 200d9f825f..4940b4ca6b 100644 --- a/indra/vcpkg.json +++ b/indra/vcpkg.json @@ -175,7 +175,7 @@ "name": "glad", "platform": "windows | linux", "features": [ - "gl-api-21", + "gl-api-41", "loader" ] },