Rework of CEF integration to support broker-mode and accelerated copy#334
Rework of CEF integration to support broker-mode and accelerated copy#334RyeMutt wants to merge 13 commits into
Conversation
Add ALCefDaemonEnabled / ALCefSandbox / ALCefAcceleratedPaint settings
(all off by default, persisted) to gate the CEF media rework, and wrap
the per-surface GL allocate + CPU->GPU upload in doMediaTexUpdate() in a
named "media texUpload" Tracy zone. That upload is exactly the cost the
accelerated-paint shared-texture path removes, so isolating it gives a
before/after baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 1: split dullahan into shared runtime + per-browser impl
Update dullahan submodule to 961ed95: hoist the process-global CEF
runtime (CefApp, CefInitialize/Shutdown, command-line flags, message
pump) into a reference-counted dullahan_runtime shared by every browser
in the process, leaving dullahan_impl as one offscreen browser. This
lets multiple browsers share a single CEF runtime - the foundation for
the shared tab-manager daemon. Public dullahan API unchanged; dullahan +
media_plugin_cef build and link on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 1: opengl-example two-browser proof harness
Update dullahan submodule to 004132a: the opengl-example now opens two
browser tabs that share a single CEF runtime (switchable via Tabs menu /
Ctrl+1/2), exercising the dullahan_runtime split at runtime - the
standalone proof that multiple CEF browsers coexist in one process.
Builds and links on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 2a: static plugin entry-point path in LLPluginInstance
Add the mechanism a dedicated single-plugin host needs to call its
statically-linked plugin directly instead of dlopen()ing a plugin
library. LLPluginInstance::setStaticInitFunction() registers a
LLPluginInitEntryPoint; when set, load() calls it and skips the
boost::dll dlopen/dlsym (and the Windows cwd hack) - ignoring the
plugin dir/file. This avoids dlopen of large TLS-using libraries (CEF on
Linux, where dlopen exhausts the static TLS block) and is a prerequisite
for the Windows sandbox, which requires the host and its sub-processes
to be a single executable image.
slplugin's main now registers ll_get_static_plugin_init(); the generic
SLPlugin links slplugin_generic.cpp which returns NULL, so its behaviour
is unchanged (still dlopen the plugin named in load_plugin). A dedicated
host (SLPluginCEF, next) will link a definition returning its plugin's
&LLPluginInitEntryPoint.
No behaviour change for the existing SLPlugin. Builds and links on
Windows (llplugin + SLPlugin).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 2b: SLPluginCEF dedicated static-link host
Build a dedicated host executable that statically links the CEF media
plugin (and CEF itself) instead of dlopen()ing media_plugin_cef:
- compile the plugin sources once into an object library
(media_plugin_cef_objs) consumed by both the existing loadable
media_plugin_cef module (legacy dlopen path, unchanged) and the new
host
- SLPluginCEF = the generic slplugin host driver + slplugin_cef.cpp
(returns &LLPluginInitEntryPoint) + the plugin objects, linking
media_plugin_base/dullahan/ll::cef/llplugin/llmessage/llcommon. Via
the Phase 2a static path, LLPluginInstance::load() calls the linked
entry point directly - no dlopen of libcef.
On Windows the host is staged into newview/.../llplugin next to
libcef.dll so the statically-referenced CEF runtime resolves from its
own directory. This removes the Linux static-TLS-block crash (libcef is
now link-time, not dlopen'd) and gives the Windows sandbox the
single-image host it needs.
Still one process per media instance; only the host shape changes. The
viewer does not launch SLPluginCEF yet (launcher selection is the next
step). Builds and links on Windows (media_plugin_cef DLL + SLPluginCEF).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 2b: launch SLPluginCEF for CEF media when enabled
Add ALCefDedicatedHost (Boolean, off) and wire LLViewerMediaImpl::
newSourceFromMediaType to launch the SLPluginCEF host instead of the
generic SLPlugin for media_plugin_cef when it is set. The dedicated host
statically links the CEF plugin, so it avoids dlopen of libcef (the
Linux static-TLS crash) and is the single-image host the Windows sandbox
needs. plugin_name is still passed and validated; the static host
ignores it. Falls back to the generic launcher (with a warning) if
SLPluginCEF is not present, so enabling the flag without the host built
degrades gracefully rather than breaking media.
Still one process per media instance. Opt-in and restart-gated so the
existing dlopen path stays the default until verified. llviewermedia.cpp
compiles (single-file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the Windows SLPluginCEF host a CEF bootstrap client so the Chromium
sandbox can be enabled for browser sub-processes.
- slplugin.cpp: factor the plugin<->parent host message loop out of the
platform entry points into slplugin_run(port) so the bootstrap entry
can reuse it.
- media_plugins/cef/slplugin_cef_bootstrap.cpp: RunWinMain (exported
via CEF_BOOTSTRAP_EXPORT) - CefExecuteProcess for sub-processes (same
image, as the sandbox requires), then dullahan::setSandboxInfo() +
slplugin_run() for the browser process. SLPLUGIN_CEF_NO_SANDBOX env
var disables the sandbox for debugging.
- CMake: on Windows SLPluginCEF is now a DLL (RunWinMain); ship CEF's
bootstrap.exe renamed to SLPluginCEF.exe (matching base name loads
SLPluginCEF.dll). Stays an executable on Linux/macOS.
- llviewermedia: ALCefSandbox also routes CEF media to SLPluginCEF (the
sandbox requires the dedicated host).
- Bumps dullahan submodule to 5bfd90b (sandbox_info plumbing + the
example bootstrap client).
cef_sandbox.lib is not linked (M138+ ships it only inside the bootstrap
executables), so no static-CRT requirement. Builds on Windows:
SLPluginCEF.dll (RunWinMain exported) + SLPluginCEF.exe, media_plugin_cef
DLL, llviewermedia. Sandbox actually engaging needs runtime verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the shared tab-manager daemon's host side: one process serving N
plugin tabs. slplugin_daemon_run() listens on a control port (written to
a rendezvous file for discover-or-spawn), serves the first tab from the
launch-arg port, and spawns an LLPluginProcessChild for each later parent
that registers its listen port on the control channel. Every tab lives
in this one process, so they transparently share a single CEF runtime
(dullahan_runtime, Phase 1) - the point of the daemon. Exits after
DAEMON_IDLE_TIMEOUT with no live tabs (keeps the runtime warm across
brief gaps).
SLPluginCEF's bootstrap entry now parses "<port> --daemon <rendezvous>"
and routes to slplugin_daemon_run vs the single-tab slplugin_run. The
daemon driver is plugin-agnostic (lives in llplugin/slplugin) and is
linked only into SLPluginCEF for now.
Builds and links on Windows. This is the daemon SIDE only; the
viewer-side discover-or-spawn that drives it (LLPluginProcessParent
connect-or-launch + liveness/crash recovery) is the next step and needs
runtime iteration - see the commit body of the follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 4b step 1: mUseDaemon plumb + daemon-aware liveness
Lay the inert groundwork for the viewer-side discover-or-spawn without
touching the launch path yet.
- LLPluginProcessParent: setUseDaemon()/getUseDaemon() + mUseDaemon
flag. pluginLockedUpOrQuit() no longer treats a null mProcess as
"child exited" when mUseDaemon is set - in daemon mode this parent
talks to a shared host it does not own, so liveness comes from the
socket/heartbeat (pluginLockedUp) instead. (STATE_EXITING reaching
CLEANUP on a null process is already correct - nothing to wait on.)
- LLPluginClassMedia: setUseDaemon() forwarding to the parent at
create time.
- llviewermedia: set it for CEF media when ALCefDaemonEnabled.
This is deliberately inert: the launch path still creates a process, so
mProcess stays non-null and the new branch never fires. With the flag
off it is a no-op. It only becomes live once step 2 (discover-or-spawn)
can leave mProcess null. Builds: llplugin + llviewermedia compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon Phase 4b step 2: viewer-side discover-or-spawn
Wire LLPluginProcessParent to the shared daemon. At STATE_LISTENING,
when mUseDaemon is set, instead of launching a private process it:
- reads the daemon control port from the per-plugin-dir rendezvous
file and, if a daemon is reachable, registers this tab by connecting
to the control port and sending its listen port (the daemon connects
back -> the existing accept() path, mProcess stays null);
- else takes an atomic spawn lock (stealing a stale one) and launches
the daemon DETACHED (autokill=false, attached=false) with
"<port> --daemon <rendezvous>", so it outlives this parent and is
shared by later tabs - no parent owns it (otherwise closing one
tab's media would kill the whole daemon). It serves the spawner as
its first tab, then connects back like any registration;
- else (another parent is launching) waits and retries next idle.
The daemon removes the spawn lock once it has published the rendezvous.
llviewermedia routes CEF media to SLPluginCEF when ALCefDaemonEnabled
too (the daemon requires the dedicated host; the generic SLPlugin can't
parse --daemon).
All gated by mUseDaemon/ALCefDaemonEnabled - the default path is
untouched. Builds: llplugin + SLPluginCEF + llviewermedia compile/link.
The concurrent lifecycle (spawn race, register-vs-spawn timing, daemon
liveness) needs runtime verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon: rendezvous path must be caller-supplied + user-writable
daemonRendezvousPath() derived the rendezvous/lock path from mPluginDir
(the install/plugin dir), which is read-only on a packaged build - the
daemon could not publish its control port and no spawn lock could be
taken. The low-level llplugin layer also has no business inventing user
paths.
Make it an explicit, caller-supplied path: setUseDaemon() now takes a
rendezvous_path; LLPluginProcessParent stores it and the daemon branch
is skipped if it is empty. The viewer supplies a user-writable path in
the logs dir (LL_PATH_LOGS) carrying the viewer PID, so it is writable on
an installed build and separate viewer instances do not share a daemon.
Plumbed viewer -> LLPluginClassMedia -> LLPluginProcessParent.
Builds: llplugin + llviewermedia compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon: dedicated host never falls back to dullahan_host
Fix daemon (and single-tab) SLPluginCEF launching dullahan_host.exe for
CEF sub-processes when the sandbox is not active. The dedicated host
dispatches its own sub-processes (CEF re-launches SLPluginCEF.exe ->
RunWinMain -> CefExecuteProcess), so dullahan_host is never needed - but
dullahan only skipped it when sandboxed.
The bootstrap entry now calls dullahan::setHostHandlesSubprocesses(true)
alongside setSandboxInfo, so CEF re-launches the SLPluginCEF image for
sub-processes whether or not the sandbox engaged. Bumps dullahan
submodule to e9bc2a8 (the decoupling + example). Builds on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon: launch daemon in the viewer job object (fixes sandbox)
The Windows CEF sandbox worked for the per-process SLPluginCEF host but
failed for the daemon. The only launch difference was the job object:
the daemon was spawned with autokill=false, putting it OUTSIDE the
viewer's job object that the working per-process host (autokill=true)
runs in - and the sandbox broker requires it.
autokill (the APR job-object association) and attached (kill the child
when this LLProcess handle is destroyed) had been conflated. Keep
autokill at its default (true) so the daemon joins the job and dies with
the viewer, and set only attached=false so discarding the fire-and-forget
handle does not kill the daemon (it must outlive the spawning tab).
Builds: llplugin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix cef daemon build-link to alchemy-bin and packaging
CEF daemon: register static plugin in daemon host (fixes sandbox)
The daemon ran unsandboxed (and spawned dullahan_host) because its tabs
dlopen()ed media_plugin_cef.dll instead of using the statically-linked
plugin. dullahan is statically linked, so the dlopen'd DLL carries a
SECOND dullahan_runtime - a different instance than the one the bootstrap
host set the sandbox info / host-handles-subprocesses flags on. That tab
runtime saw mSandboxInfo=NULL, so it disabled the sandbox and used the
dullahan_host helper.
slplugin_run() (the single-tab host) calls
LLPluginInstance::setStaticInitFunction() so load() uses the linked
plugin entry directly; slplugin_daemon_run() did not. Add the same call
so daemon tabs run in the host's own dullahan_runtime - the one with the
sandbox info.
Builds: SLPluginCEF.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CEF daemon: clean up the rendezvous file on viewer shutdown
The daemon runs in the viewer's job object, so on viewer exit it is
force-killed and never reaches the std::remove(rendezvous) at the end of
slplugin_daemon_run (that only runs on the idle-timeout path). Because
the rendezvous filename carries the viewer PID, each run left a distinct
orphan in the logs dir.
Delete it (and any stale spawn lock) from ~LLViewerMedia(), which runs at
shutdown while gDirUtilp is still valid - before the daemon is killed.
Factor the path into a shared helper so the create site
(newSourceFromMediaType) and the cleanup agree. No-op when daemon mode
was unused.
Builds: llviewermedia compiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… daemon In daemon mode the parent owns no host process (mProcess is null), so STATE_EXITING fell straight through to STATE_CLEANUP and slammed the socket shut while the tab's browser was still live in the shared CEF runtime. The tab then lost its parent socket before processing shutdown_plugin, dropped into STATE_ERROR (which never nulls mInstance), and was reaped by the daemon - whose ~LLPluginProcessChild calls exit(0) when mInstance is set, taking down every other tab. Net effect: open two web instances, close one, the whole SLPluginCEF.exe disappears. Fix the teardown end to end: - Parent: in daemon mode STATE_EXITING now waits for the tab to finish its graceful unload and drop its end of the socket (EOF/error) or for the lockup timeout, instead of tearing the socket down immediately. - Child: a lost parent socket in daemon mode routes to the normal graceful unload (STATE_SHUTDOWNREQ) so the browser closes cleanly in the shared runtime before the tab is reaped. - Child: ~LLPluginProcessChild no longer exit(0)s in daemon mode - daemon plugins are statically linked (no DSO unload to lock up), so it deletes the instance directly and lets the other tabs live. - Daemon marks every tab with setDaemonMode(true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: recover media after a daemon crash instead of failing permanently A shared-daemon crash drops every tab's socket at once, so each media's LLPluginProcessParent reports the plugin dead (MEDIA_EVENT_PLUGIN_FAILED) and the impl latched mMediaSourceFailed = true -> isForcedUnloaded() pins it to PRIORITY_UNLOADED forever. Net effect of one daemon crash: all CEF media go permanently dark, a regression from per-process hosting where a crash only loses one media. For daemon-mode CEF media, schedule a bounded, backed-off re-init on failure (DAEMON_RECOVERY_MAX_ATTEMPTS, base*attempt up to a ceiling) rather than failing permanently. When the backoff elapses, update() clears the failure latch and the normal load path recreates the source - the first impl to do so wins the spawn lock and respawns the daemon (discover-or-spawn), the rest reconnect as fresh tabs. A completed navigate resets the attempt counter, so the cap only trips on a daemon that keeps crashing on launch (no respawn storm). Non-daemon plugins are untouched (gated on LLPluginClassMedia::getUseDaemon()). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: hard tab ceiling as a runaway backstop PluginInstancesTotal -> mMaxIntances is the primary tab cap (excess media stay PRIORITY_UNLOADED, so they never create a source or a tab). Add a generous daemon-side ceiling (DAEMON_MAX_TABS) so a buggy or hostile parent can't drive one process to spawn unbounded browsers regardless of the viewer-side accounting; over the ceiling, registrations are dropped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: cut idle CPU by driving the external pump from CEF's schedule Bump dullahan submodule: dullahan_runtime now implements OnScheduleMessagePumpWork so CefDoMessageLoopWork() runs only when CEF asks for it, instead of on every host idle tick. Removes the constant idle CPU draw of the fixed-cadence pump and collapses N daemon tabs' per-frame pumps into at most one shared pump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: fix resize blanking via external-pump watchdog Bump dullahan submodule: the OnScheduleMessagePumpWork-gated pump could let a needed repaint go unscheduled after a resize (CEF coalesces its external-pump notifications), leaving the media surface blank with no event to recover it. dullahan_runtime now watchdogs the pump so it can never stay idle longer than 100ms, bounding recovery while keeping the idle-CPU win. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: stop per-tab 50ms stall from the browser-init settle hack Bump dullahan submodule: the post-CefInitialize "settle" pump (a 50ms blocking sleep that let the shared global request context finish initializing) ran in every browser's init(). In the daemon that froze the host thread - and so every other tab - for 50ms on each new tab. It now runs once per process in dullahan_runtime::acquire(); later daemon tabs skip it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: fix login-time re-CefInitialize crash (persistent runtime) When the login-screen web surface transitioned away, the daemon's CEF browser count hit zero, release() called CefShutdown(), and the next web surface crashed in CefInitialize() (CEF cannot be re-initialized in a process). Bump dullahan submodule for the persistent-runtime mode, and have the SLPluginCEF bootstrap opt in (setPersistentRuntime(true)) and shut CEF down once (shutdownRuntime()) after the host loop returns. CEF now stays up across zero-browser gaps and is torn down exactly once at process exit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump dullahan submodule: adds the accelerated_paint setting + the OnAcceleratedPaint -> onAcceleratedPaint callback path (GPU shared-texture handle, format, coded size), shared_texture_enabled on the window, and GPU-compositing-on in that mode. Default off; no behavior change yet. Foundation for zero-copy media textures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5b - opengl-example zero-copy paint proof (Win) Bump dullahan submodule: the opengl-example now drives OnAcceleratedPaint through a D3D11 + WGL_NV_DX_interop2 helper to alias CEF's GPU shared texture into the GL quad with no CPU copy, with a CPU-path fallback (DULLAHAN_FORCE_CPU_PAINT) for A/B. Standalone proof before wiring the cross-process transport + viewer media-texture import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5b - diagnostics for white-screen accelerated paint Bump dullahan submodule: debugger-visible logging in the opengl-example interop to locate why the accelerated path renders white (callback firing? shared-resource open? register? lock?). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5b - fix accelerated paint NT-handle register failure Bump dullahan submodule: WGL_NV_DX_interop2 can't register Chromium's NT-handle shared texture directly (wglDXRegisterObjectNV ERROR_OPEN_FAILED -> white quad). Open it, GPU-copy (CopyResource) into an own-device intermediate texture that registers cleanly, and sample that. Still no CPU readback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5b - fix accelerated-paint page-change flicker Bump dullahan submodule: the OnAcceleratedPaint shared-texture pool (no keyed mutex) was being read stale - cached opens could point at a remapped resource and the GPU copy wasn't waited on before CEF recycled the source. Open fresh each frame + wait on an event query after CopyResource. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5c - carry the GPU shared-texture handle to the viewer The media IPC only transported CPU pixels via shared memory. Add a parallel path that hands the viewer a GPU shared-texture handle for zero-copy paint. - LLPluginClassMedia: setUseAcceleratedPaint() + the viewer's pid go in the media "init" message; a new "accelerated_paint" message delivers the duplicated handle (decimal string so the full 64-bit value survives) plus cef_color_type format and coded size, exposed via getAcceleratedPaint*/ takeAcceleratedPaintHandle(). - media_plugin_cef: when accelerated_paint is requested, register setOnAcceleratedPaintCallback; on each frame OpenProcess(viewer pid) once and DuplicateHandle the CEF shared texture into the viewer (the handle is only valid during the callback), then send accelerated_paint. The browser host is unsandboxed (broker), so the cross-process dup is allowed. - llviewermedia: enable it from ALCefAcceleratedPaint for CEF media; update() logs the first arriving handle (transport checkpoint) and closes each so the per-frame duplicates don't leak. Transport only - the viewer does not yet import/bind the texture (5d). Builds on Windows; runtime checkpoint is the "accelerated paint frame received" log. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5d (producer) - keyed-mutex stable texture, drop per-frame dup 5c duplicated a CEF shared-texture handle into the viewer EVERY frame (plus a per-frame OpenSharedResource on the viewer). Replace that with a producer-side stable texture so the handle crosses the boundary only once per size. media_plugin_cef gains CefAccelProducer (Windows): a D3D11 device + one persistent shared texture created with NT-handle + keyed mutex. Each accelerated-paint frame it opens CEF's pooled texture and CopyResources it into the stable texture under the mutex (single key 0 = mutual exclusion, which also gives the cross-process/cross-device GPU sync). The stable texture's NT handle is DuplicateHandle'd into the viewer only when it is (re)created; per-frame messages carry handle "0" = "same texture, new frame". Single-key (not 0/1 ping-pong) so the producer never deadlocks before the consumer exists. LLPluginClassMedia keeps the last real handle (a "0" ping no longer clears it) and always marks the frame dirty. CMake links d3d11/dxgi into media_plugin_cef + SLPluginCEF. Producer only; the viewer still just logs+closes the handle (5c checkpoint) - it opens/binds the stable texture next. Builds on Windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5d (consumer) - zero-copy CEF media in-world The viewer now imports the plugin's shared GPU texture and blits it into the media texture with no CPU upload (replacing the setSubImage path for accelerated CEF media). - new LLCEFAccelInterop (newview, Windows): D3D11 device + WGL_NV_DX_interop2. Opens the plugin's keyed-mutex stable texture (delivered once per size), copies it into an own-device intermediate under the mutex (single key 0 = mutual exclusion + cross-process sync), GL-registers that intermediate (the opened cross-device NT-handle texture can't be registered directly), and blits it into the media GL texture. - the blit uses glBlitFramebuffer with an inverted destination rectangle: a framebuffer read samples the interop texture in the correct channel order (no BGRA swizzle needed) and the flip turns CEF's top-down texture bottom-up for the viewer. - LLViewerMediaImpl::update() runs the accelerated path on the main thread and skips the shm/upload path; the interop is created lazily and torn down with the impl. - media_plugin_cef requests an RGBA8 media texture in accelerated mode. Builds on Windows (new files + d3d11/dxgi wired into newview). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5d - don't lose the stable handle if the consumer isn't ready The plugin sends its stable shared-texture handle only once per size, but the consumer consumed (zeroed) it before confirming the interop was ready - so if interop init failed on that frame the handle was lost and the media stuck until a resize. Make the handle persistent on the viewer side (getAcceleratedPaintHandle / clearAcceleratedPaintDirty replace takeAcceleratedPaintHandle). The consumer brings up the interop first, then compares the persistent handle against the one it last bound and only advances its bound-handle on a SUCCESSFUL setStableTexture - so a transient failure retries with the same handle next frame. Reset the bound handle on destroyMediaSource so a recreated source rebinds fresh even if the handle value is reused. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5e - dullahan macOS/Linux accelerated-paint plumbing Bump dullahan submodule: OnAcceleratedPaint now yields the macOS IOSurfaceRef (via the existing void* callback) and the Linux dma-buf (new fd-based callback), alongside the Windows handle. Foundation for the macOS/Linux zero-copy paths; written blind from the CEF API (no Linux/mac CEF headers or build here), unverified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5e - macOS zero-copy paint (IOSurface), untested macOS path end-to-end. The accelerated frame is an IOSurface, which is shareable by its global IOSurfaceID (an integer) - no handle duplication or producer texture needed: - media_plugin_cef sends IOSurfaceGetID over the existing accelerated_paint message each frame (the surface is pooled, so the id can change). - LLCEFAccelInterop gains a macOS impl: IOSurfaceLookup -> bind to a GL_TEXTURE_RECTANGLE via CGLTexImageIOSurface2D, then the same flipped glBlitFramebuffer into the media texture as the Windows path. Windows still builds (the macOS branch is #elif-guarded). The macOS code is written blind (no macOS build/test here) and unverified. Linux dma-buf transport + import still to come. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF daemon: Phase 5e - Linux zero-copy paint (dma-buf + EGL), untested Linux path end-to-end. CEF delivers the accelerated frame as a dma-buf, so: - media_plugin_cef registers the dma-buf callback on Linux; per frame it dup()s the fd (CEF's is valid only during the callback), keeps a small ring alive, and sends the fd number + its pid + plane layout (stride/offset/DRM modifier/format) over the accelerated_paint message. The viewer re-opens the fd via /proc/<pid>/fd - no SCM_RIGHTS side channel needed. - LLPluginClassMedia carries the dma-buf layout fields (0 on Windows/macOS); LLCEFAccelInterop::setStableTexture gained them as trailing args. - LLCEFAccelInterop Linux impl: open /proc/<src_pid>/fd/<fd>, import it with eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) -> glEGLImageTargetTexture2DOES, then the same flipped glBlitFramebuffer into the media texture. Windows still builds (the Linux branch is #elif-guarded). Written blind (no Linux build/test here) and unverified - notably it assumes the viewer's GL context is EGL (not GLX) for eglGetCurrentDisplay to work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> CEF media: share one D3D11<->GL interop device instead of one per media impl Each LLCEFAccelInterop created its own D3D11 device + wglDXOpenDeviceNV and ad-hoc wglGetProcAddress'd the WGL_NV_DX_interop2 entry points, so N web surfaces meant N D3D devices and N interop devices. Centralize it: - the wglDX* entry points are now loaded in LLGLManager::initWGL alongside the other WGL extensions (declared in llglheaders.h, defined in llgl.cpp, gated on WGL_NV_DX_interop2). - LLDXHardware owns one D3D11 device + one wglDXOpenDeviceNV interop device for the whole process (initGLDXInterop / cleanupGLDXInterop). It is brought up once in LLWindowWin32::switchContext - main thread, render context current, WGL loaded - and torn down with the context. - LLCEFAccelInterop drops its private device/loader and uses the shared device, context and interop handle plus the global wglDX* pointers; per surface it still owns only its opened texture, intermediate, registration and FBOs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build slplugin_daemon.cpp into the non-Windows SLPluginCEF and route the platform main() through a per-host ll_run_slplugin_host() hook: the generic SLPlugin serves a single connection, while the CEF host marks the runtime persistent and dispatches slplugin_daemon_run() when launched with --daemon (mirroring the Windows bootstrap). Make SLPlugin and SLPluginCEF proper macOS app bundles with their own Info.plist and bundle identifiers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump the example's glad feature from gl-api-21 to gl-api-41 for the 4.1 Core context, and advance the dullahan submodule to the 4.1 Core / macOS IOSurface zero-copy example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Update dullahan: drop default framerate to 30 to reduce render load Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The viewer rendered media grey under accelerated paint on macOS: the plugin sent only the global IOSurfaceID and the viewer did IOSurfaceLookup, but CEF shares its accelerated-paint IOSurfaces between its GPU and browser processes via mach ports, not global ids, so a cross-process id lookup returns NULL and nothing is ever bound. Hand the surface over a mach channel instead: the viewer registers a bootstrap receive port named from its pid (LLCEFSurfaceReceiver); the plugin looks it up (host_pid is already in the init handshake) and mach_msg's an IOSurfaceCreateMachPort() right per frame, tagged with a new per-media accel_id so one receiver can demux many tabs. The viewer resolves it with IOSurfaceLookupFromMachPort and binds it as before. The receiver registers up front (not gated on a frame) since the plugin only produces once the port exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughReplaces the single ChangesStatic plugin loading and per-plugin host executables
CEF shared daemon mode
CEF accelerated zero-copy paint
Sequence DiagramsequenceDiagram
rect rgba(100, 149, 237, 0.5)
Note over Viewer,Daemon: Daemon Discovery/Spawn
end
participant Viewer as LLViewerMediaImpl
participant Parent as LLPluginProcessParent
participant Daemon as media_plugin_cef (daemon)
participant Child as LLPluginProcessChild
Viewer->>Parent: setUseDaemon(true, rendezvous_path)
Parent->>Parent: daemonDiscoverOrRegister()
alt daemon not running
Parent->>Daemon: spawn --daemon rendezvous_path port
Daemon->>Daemon: write port to rendezvous file
else daemon running
Parent->>Daemon: TCP register(parent_port)
end
Daemon->>Child: spawn LLPluginProcessChild(daemon_mode)
rect rgba(144, 238, 144, 0.5)
Note over Viewer,Child: Accelerated Paint Frame
end
Child->>Child: CEF onPaint → CefAccelProducer/Sender
Child->>Viewer: side channel (Mach port / NT handle / SCM_RIGHTS)
Child->>Parent: accelerated_paint message (accel_id, handle, dims)
Parent->>Viewer: MEDIA_EVENT_CONTENT_UPDATED
Viewer->>Viewer: updateAcceleratedTexture()
Viewer->>Viewer: LLCEFAccelInterop::blitTo(dst_tex)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| if (LLFile::stat(lock_path, &st) == 0 && | ||
| (time(nullptr) - st.st_mtime) > LOCK_STALE_SECONDS) | ||
| { | ||
| LLFile::remove(lock_path); |
CEF's accelerated-paint dma-buf fd cannot cross the plugin->viewer boundary through the TCP LLSD channel, and a dma-buf fd cannot be reopened via /proc (open() returns ENXIO), so the page rendered grey. Pass the plane fds as SCM_RIGHTS ancillary data over a dedicated AF_UNIX datagram side channel (LLCEFSurfaceReceiver), keyed by the viewer pid via an abstract socket name - the Linux analog of the macOS IOSurface mach channel - and import the received fds directly as an EGLImage. Also: - Expose the windowing backend from LLWindow::getDisplayServer() and forward it to the CEF plugin so its Ozone platform matches the viewer (native Wayland instead of XWayland). - Load the EGL image-import entry points (eglCreateImageKHR etc.). - Package SLPluginCEF + the CEF runtime in the Linux viewer manifest. Bumps the dullahan submodule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each media plugin is now its own host executable named for the plugin (media_plugin_cef / media_plugin_libvlc / media_plugin_gstreamer10 / media_plugin_example) that statically links the plugin's object code plus the slplugin host driver - generalizing what SLPluginCEF already did. The generic SLPlugin launcher, the dlopen'd plugin shared libraries, and the loader machinery are removed. - llplugin: new slplugin_static.cpp registers the statically-linked &LLPluginInitEntryPoint (replacing slplugin_generic.cpp's nullptr/dlopen hook). LLPluginInstance is now static-only - boost::dll, mDSOHandle and PLUGIN_INIT_FUNCTION_NAME are gone. - CMake: remove the SLPlugin target; media_plugins/cef drops the .so and renames its host SLPluginCEF -> media_plugin_cef; libvlc/gstreamer10/ example become executables; newview's dependency list is updated. - lldir: getLLPluginFilename() now returns the plugin's host-executable path; getLLPluginLauncher() is removed (incl. test stubs). - viewer: launch each plugin's own executable directly; the CEF launcher-override and the dual .so validation are gone (daemon/sandbox is still selected at runtime via setUseDaemon / the ALCef* settings). - viewer_manifest: ship the per-plugin executables (Linux), the *.exe/*.dll hosts incl. the bootstrap renamed to media_plugin_cef.exe (Windows), and per-plugin .app bundles with CEF/helpers inside media_plugin_cef.app (macOS). Verified on Linux: all four plugin hosts build and link, the viewer and the static-only loader compile. Windows/macOS mirror the existing SLPluginCEF pattern and still need building on those platforms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
indra/newview/llviewermedia.cpp (1)
1787-1793: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTear down the accelerated binding when the media source is destroyed.
Resetting only
mAccelBoundHandleleavesmAccelInteropholding the previous source’s platform texture; the next accelerated dirty event can blit stale/invalid content before a fresh bind arrives.Proposed fix
// 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; + if (mAccelInterop) + { + mAccelInterop->shutdown(); + delete mAccelInterop; + mAccelInterop = nullptr; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@indra/newview/llviewermedia.cpp` around lines 1787 - 1793, The destroyMediaSource teardown in LLViewerMediaImpl::destroyMediaSource currently clears mAccelBoundHandle but leaves mAccelInterop pointing at the old plugin texture, which can allow stale accelerated content to be used. Update the destroyMediaSource cleanup to fully reset the accelerated binding state by clearing mAccelInterop as well as the existing handle reset, so the next dirty event forces a clean rebind to the new media source.
🧹 Nitpick comments (3)
indra/media_plugins/cef/media_plugin_cef.cpp (1)
425-435: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the side-channel frame ABI into one shared definition.
The Mach and dma-buf payload structs are duplicated here and again in
indra/newview/llcefsurfacereceiver.cpp. Because this is a cross-process ABI, a future field-order or packing drift will fail only at runtime. A shared header plusstatic_assert(sizeof(...))would make this contract much safer.Also applies to: 515-525
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@indra/media_plugins/cef/media_plugin_cef.cpp` around lines 425 - 435, Move the side-channel frame ABI into a single shared definition so the sender and receiver stay in lockstep. The duplicated Mach/dma-buf payload structs in media_plugin_cef.cpp and LLCEFSurfaceReceiver should be replaced by a common header or shared type used by both CefSurfaceSendMsg and the receiver-side equivalents, and add size/packing checks with static_assert to guard the cross-process contract. Keep the existing wire-format field order and reference the shared ABI from both send/receive paths rather than maintaining two copies.indra/newview/llcefaccelinterop.h (1)
5-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate the public contract comments to match the implemented platforms.
The implementation expects a +1
IOSurfaceRefon macOS and already-open SCM_RIGHTS dma-buf fds on Linux; the header still documents IOSurfaceID,/proc/<pid>/fd, Windows-only support, and caller-side swizzling.Proposed documentation fix
- * 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. + * Opens the plugin's platform shared surface, copies/imports it into a + * viewer-owned GL texture, and blits that into a media GL texture - no CPU + * round-trip. Implemented on Windows (D3D11 + WGL_NV_DX_interop2), macOS + * (IOSurface), and Linux (dma-buf/EGL); stubbed elsewhere. @@ - // handle on Windows (NT handle) / macOS (IOSurfaceID). On Linux the frame is a - // dma-buf: `handle` is plane 0's fd number in process `src_pid` (opened via - // /proc/<src_pid>/fd/<fd>) and the layout is given by format / modifier plus + // handle on Windows (NT handle). On macOS, `handle` is a +1-retained + // IOSurfaceRef delivered out-of-band by LLCEFSurfaceReceiver. On Linux the + // frame is a dma-buf whose plane fds are already open in the viewer process + // via SCM_RIGHTS; `src_pid` is ignored and the layout is given by format / modifier plus @@ - // 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. + // Copy/import the latest plugin frame into dst_tex (a GL_RGBA8 texture). + // The implementation handles channel order and Y-flip during the blit.Also applies to: 47-65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@indra/newview/llcefaccelinterop.h` around lines 5 - 8, The public contract comment in llcefaccelinterop.h is stale and should be updated to match the actual supported platforms and ownership rules. Revise the documentation around the relevant interface symbols to describe the implemented macOS path as taking a +1 IOSurfaceRef, the Linux path as consuming already-open SCM_RIGHTS dma-buf file descriptors, and remove references to IOSurfaceID, /proc/<pid>/fd, Windows-only support, and caller-side swizzling. Keep the comment aligned with the current behavior of the related interop entry points so the header accurately describes what callers must provide.indra/newview/llcefaccelinterop.cpp (1)
229-244: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCheck framebuffer completeness before reporting a successful blit.
All three platform paths return
trueafterglBlitFramebufferwithout verifying the read/draw FBOs are complete. AddglCheckFramebufferStatuschecks so accelerated paint can fall back instead of silently presenting stale/blank media.Also applies to: 367-380, 603-616
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@indra/newview/llcefaccelinterop.cpp` around lines 229 - 244, The accelerated blit paths in llcefaccelinterop.cpp return success from the framebuffer copy without verifying the FBOs are complete. In the code around the glBlitFramebuffer usage (including the related platform-specific paths mentioned in the comment), add glCheckFramebufferStatus checks for the read and draw framebuffer setup before treating the blit as successful, and make the function fall back instead of returning true when completeness fails. Use the existing blit helpers and platform-path logic in this file to keep the check consistent across all three paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@indra/llplugin/llpluginprocessparent.cpp`:
- Around line 1390-1417: The daemon registration path in
LLPluginProcessParent::registerWithDaemon accepts a U32 control_port but casts
it directly to apr_port_t, which can wrap if the rendezvous file contains an
out-of-range value. Add a bounds check before apr_sockaddr_info_get() so only
valid TCP ports (1-65535) are accepted, and return false for invalid values.
Keep the validation in registerWithDaemon so the bad port is rejected before any
socket connection attempt.
- Around line 539-548: The daemon launch path in LLPluginProcessParent::setState
handling ignores the return from LLProcess::create(params), so failures leave
the parent waiting indefinitely and holding the spawn lock. Update this launch
block to check the create result, and on failure immediately reset the launch
state/flags and perform the same cleanup/error handling used by the other
process creation paths in LLPluginProcessParent.
In `@indra/llplugin/slplugin/slplugin_daemon.cpp`:
- Around line 129-140: The registration read in slplugin_daemon.cpp is parsing
whatever bytes arrive from apr_socket_recv() once, which can be only a partial
TCP segment; update the socket-read logic in the daemon’s registration handling
to keep reading into an accumulator until a full newline-terminated line is
received or the timeout expires, then trim and pass that complete string to
LLStringUtil::convertToU32(). Keep the fix localized around the incoming socket
receive block so the port is only accepted from a complete registration line.
- Around line 190-197: The rendezvous publication in slplugin_daemon.cpp should
be verified before the spawn lock is removed. In the code that writes the
control port via llofstream rv and then calls
LLFile::remove(rendezvous_lock_path), add a success check for opening/writing
the rendezvous file and only release the lock after confirming the publish
succeeded; if it fails, log an error and keep the lock behavior consistent so
other parents do not spawn a duplicate daemon. Use the existing rendezvous_path,
rendezvous_lock_path, and control_port flow to locate the fix.
In `@indra/llrender/llgl.cpp`:
- Around line 1106-1116: Guard the eglQueryString function pointer before using
it in the EGL init path: the lookup via SDL_EGL_GetProcAddress("eglQueryString")
can fail, and the first RenderInit log calls use it immediately. Update the
llgl.cpp initialization around eglQueryString so it checks for a valid pointer
before calling it, and fall back to the same optional/skip behavior used by
reloadExtensionsString() when the symbol is unavailable.
In `@indra/llwindow/lldxhardware.cpp`:
- Around line 611-615: The interop capability check is too permissive and can
report success even when the accelerated-paint path’s required callback is
missing. Update hasGLDXInterop() to gate on the same WGL DX interop callbacks
that LLCEFAccelInterop::blitTo() actually uses, not just wglDXOpenDeviceNV from
initGLDXInterop(). Make sure the check fails early when wglDXUnlockObjectsNV
(and any other required callbacks) is unavailable so the path is disabled up
front instead of failing later.
In `@indra/media_plugins/cef/CMakeLists.txt`:
- Around line 94-95: Fix the macOS bundle identifier typo in the CEF plugin
CMake configuration: both MACOSX_BUNDLE_GUI_IDENTIFIER and
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER currently use org.achemyviewer...,
which should be corrected to the proper alchemyviewer spelling so the generated
Info.plist and bundle identity stay consistent. Update the identifiers in the
CMakeLists entry for the CEF bundle settings.
In `@indra/media_plugins/cef/media_plugin_cef.cpp`:
- Around line 138-145: Treat a failed keyed-mutex copy as a dropped frame in
onAcceleratedPaintCallback(): currently the code returns true even when mMutex
is null or AcquireSync() fails, which can emit accelerated_paint for an
uninitialized recreated texture. Update the copy path so success is returned
only when mContext->CopyResource and the mutex handoff both complete; otherwise
release cef and return false, and apply the same logic to the duplicated block
mentioned in the review.
In `@indra/media_plugins/cef/slplugin_cef_bootstrap.cpp`:
- Around line 103-114: Preserve Unicode in the lpCmdLine handling inside
slplugin_cef_bootstrap.cpp: the current loop in the RunWinMain command-line
parsing path truncates non-ASCII characters when building cmd, which breaks
localized rendezvous paths. Replace the manual wide-to-char cast with a proper
UTF-16 conversion using WideCharToMultiByte, or keep the command line wide
through the parsing logic, and make sure the resulting value is still trimmed
and passed through the existing command-line handling flow unchanged otherwise.
In `@indra/newview/app_settings/settings_alchemy.xml`:
- Around line 8-14: The CEF feature settings are inconsistent because the
comments say “off by default” while the Boolean defaults in the
ALCefAcceleratedPaint, ALCefDaemonEnabled, ALCefDedicatedHost, and ALCefSandbox
entries are enabled. Update the settings in settings_alchemy.xml so the default
Value matches the comments by setting these feature defaults to off, or revise
the comments if the intended rollout is enabled by default.
In `@indra/newview/llcefaccelinterop.cpp`:
- Around line 546-568: The accelerated paint import path in
llcefaccelinterop::releaseImage/eglCreateImageKHR currently destroys the
existing Linux EGLImage before confirming the new dma-buf import succeeds, so a
failed frame can leave blitTo() with nothing usable. Change the import flow to
create the new EGLImage in a temporary handle first, and only call
releaseImage() or replace l->image after eglCreateImageKHR succeeds; if it
fails, keep the previous image intact and return false while preserving the last
good binding.
- Around line 195-199: The AcquireSync call in llcefaccelinterop.cpp currently
waits up to 1000 ms inside the viewer GL update path, which can stall rendering
when the producer is busy. Update the logic in the relevant sync/acquire section
around w->mutex->AcquireSync so it uses a non-blocking or very short timeout and
immediately returns false to retry on the next frame, preserving the existing
“try again next” behavior without blocking the viewer thread.
- Around line 134-135: The Windows shared-texture binding path in
llcefaccelinterop should fail fast when the texture does not expose
IDXGIKeyedMutex instead of silently continuing. Update the binding logic around
QueryInterface on the stable texture (and any caller that proceeds with
CopyResource) so it checks the keyed-mutex result, returns an error/false, and
aborts the bind when w->mutex is unavailable; keep the normal keyed-mutex path
unchanged.
- Around line 328-339: The macOS IOSurface binding in blitTo() is replacing
m->surface before confirming CGLTexImageIOSurface2D succeeded, which can leave a
stale surface/texture state on failure. Update llcefaccelinterop.cpp’s surface
update path so the new surf is only assigned to m->surface after the bind call
returns kCGLNoError, and keep the previous surface/metadata intact if binding
fails. Make sure any cleanup or release of the old surface happens only after a
successful bind, using the existing m->surface, m->width, and m->height members
as the state to preserve.
In `@indra/newview/llviewermedia.cpp`:
- Around line 3069-3082: The accelerated paint path in llviewermedia.cpp can
leave media blank when LLCEFAccelInterop::init() fails because update() returns
early after updateAcceleratedTexture() without falling back to CPU upload.
Update the logic around mMediaSource->getUseAcceleratedPaint(),
updateAcceleratedTexture(), and the later CPU paint path so that failed interop
either disables/recreates accelerated mode and allows CPU painting, or only
enables accelerated paint after a successful capability preflight; apply the
same fix in both affected update branches.
- Around line 3265-3317: The macOS/Linux paths in llviewermedia.cpp are ignoring
setStableTexture() import failures and still proceeding to blitTo(), which can
leave stale content visible. Update the platform-specific binding logic in the
media update path around LLCEFSurfaceReceiver::instance().takeLatest() /
takeLatestDmabuf() so that a failed mAccelInterop->setStableTexture() causes an
immediate return false, matching the Windows behavior and preventing fallback to
the previous texture binding.
---
Outside diff comments:
In `@indra/newview/llviewermedia.cpp`:
- Around line 1787-1793: The destroyMediaSource teardown in
LLViewerMediaImpl::destroyMediaSource currently clears mAccelBoundHandle but
leaves mAccelInterop pointing at the old plugin texture, which can allow stale
accelerated content to be used. Update the destroyMediaSource cleanup to fully
reset the accelerated binding state by clearing mAccelInterop as well as the
existing handle reset, so the next dirty event forces a clean rebind to the new
media source.
---
Nitpick comments:
In `@indra/media_plugins/cef/media_plugin_cef.cpp`:
- Around line 425-435: Move the side-channel frame ABI into a single shared
definition so the sender and receiver stay in lockstep. The duplicated
Mach/dma-buf payload structs in media_plugin_cef.cpp and LLCEFSurfaceReceiver
should be replaced by a common header or shared type used by both
CefSurfaceSendMsg and the receiver-side equivalents, and add size/packing checks
with static_assert to guard the cross-process contract. Keep the existing
wire-format field order and reference the shared ABI from both send/receive
paths rather than maintaining two copies.
In `@indra/newview/llcefaccelinterop.cpp`:
- Around line 229-244: The accelerated blit paths in llcefaccelinterop.cpp
return success from the framebuffer copy without verifying the FBOs are
complete. In the code around the glBlitFramebuffer usage (including the related
platform-specific paths mentioned in the comment), add glCheckFramebufferStatus
checks for the read and draw framebuffer setup before treating the blit as
successful, and make the function fall back instead of returning true when
completeness fails. Use the existing blit helpers and platform-path logic in
this file to keep the check consistent across all three paths.
In `@indra/newview/llcefaccelinterop.h`:
- Around line 5-8: The public contract comment in llcefaccelinterop.h is stale
and should be updated to match the actual supported platforms and ownership
rules. Revise the documentation around the relevant interface symbols to
describe the implemented macOS path as taking a +1 IOSurfaceRef, the Linux path
as consuming already-open SCM_RIGHTS dma-buf file descriptors, and remove
references to IOSurfaceID, /proc/<pid>/fd, Windows-only support, and caller-side
swizzling. Keep the comment aligned with the current behavior of the related
interop entry points so the header accurately describes what callers must
provide.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 47362874-2ea6-4681-aa58-ad9ab399d3d6
📒 Files selected for processing (50)
indra/dullahanindra/llfilesystem/lldir.hindra/llfilesystem/lldir_linux.cppindra/llfilesystem/lldir_linux.hindra/llfilesystem/lldir_mac.hindra/llfilesystem/lldir_mac.mmindra/llfilesystem/lldir_win32.cppindra/llfilesystem/lldir_win32.hindra/llfilesystem/tests/lldir_test.cppindra/llplugin/llpluginclassmedia.cppindra/llplugin/llpluginclassmedia.hindra/llplugin/llplugininstance.cppindra/llplugin/llplugininstance.hindra/llplugin/llpluginprocesschild.cppindra/llplugin/llpluginprocesschild.hindra/llplugin/llpluginprocessparent.cppindra/llplugin/llpluginprocessparent.hindra/llplugin/slplugin/CMakeLists.txtindra/llplugin/slplugin/slplugin.cppindra/llplugin/slplugin/slplugin_daemon.cppindra/llplugin/slplugin/slplugin_info.plistindra/llplugin/slplugin/slplugin_static.cppindra/llrender/llgl.cppindra/llrender/llglheaders.hindra/llwindow/lldxhardware.cppindra/llwindow/lldxhardware.hindra/llwindow/llwindow.hindra/llwindow/llwindowsdl.cppindra/llwindow/llwindowsdl.hindra/llwindow/llwindowwin32.cppindra/media_plugins/cef/CMakeLists.txtindra/media_plugins/cef/SLPluginCEF-Info.plist.inindra/media_plugins/cef/media_plugin_cef.cppindra/media_plugins/cef/slplugin_cef.cppindra/media_plugins/cef/slplugin_cef_bootstrap.cppindra/media_plugins/example/CMakeLists.txtindra/media_plugins/gstreamer10/CMakeLists.txtindra/media_plugins/libvlc/CMakeLists.txtindra/newview/CMakeLists.txtindra/newview/app_settings/settings_alchemy.xmlindra/newview/llcefaccelinterop.cppindra/newview/llcefaccelinterop.hindra/newview/llcefsurfacereceiver.cppindra/newview/llcefsurfacereceiver.hindra/newview/llsyntaxid.cppindra/newview/llviewermedia.cppindra/newview/llviewermedia.hindra/newview/tests/lldir_stub.cppindra/newview/viewer_manifest.pyindra/vcpkg.json
💤 Files with no reviewable changes (5)
- indra/llfilesystem/lldir_mac.h
- indra/llfilesystem/lldir_linux.h
- indra/llfilesystem/lldir_win32.h
- indra/newview/tests/lldir_stub.cpp
- indra/llfilesystem/tests/lldir_test.cpp
| 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); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Handle daemon launch failures immediately.
LLProcess::create(params) can fail, as the nearby launch paths already handle. Ignoring the return leaves this parent waiting for a daemon callback that will never arrive and keeps the spawn lock until it becomes stale.
Proposed fix
- LLProcess::create(params); // fire and forget; keep no mProcess
+ LLProcessPtr daemon_process = LLProcess::create(params);
+ if (!daemon_process)
+ {
+ LL_WARNS("Plugin") << "failed to launch CEF daemon" << LL_ENDL;
+ LLFile::remove(daemonRendezvousPath() + ".lock");
+ mProcessCreationRequested = false;
+ errorState();
+ break;
+ }
+ // fire and forget; params.attached=false means dropping
+ // this handle does not terminate the daemon.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| LLProcess::Params params = mProcessParams; | |
| params.args.add(stringize(mBoundPort)); | |
| params.args.add("--daemon"); | |
| params.args.add(daemonRendezvousPath()); | |
| params.attached = false; | |
| LLProcessPtr daemon_process = LLProcess::create(params); | |
| if (!daemon_process) | |
| { | |
| LL_WARNS("Plugin") << "failed to launch CEF daemon" << LL_ENDL; | |
| LLFile::remove(daemonRendezvousPath() + ".lock"); | |
| mProcessCreationRequested = false; | |
| errorState(); | |
| break; | |
| } | |
| // fire and forget; params.attached=false means dropping | |
| // this handle does not terminate the daemon. | |
| mProcessCreationRequested = true; | |
| mHeartbeat.start(); | |
| mHeartbeat.setTimerExpirySec(mPluginLaunchTimeout); | |
| setState(STATE_LAUNCHED); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/llplugin/llpluginprocessparent.cpp` around lines 539 - 548, The daemon
launch path in LLPluginProcessParent::setState handling ignores the return from
LLProcess::create(params), so failures leave the parent waiting indefinitely and
holding the spawn lock. Update this launch block to check the create result, and
on failure immediately reset the launch state/flags and perform the same
cleanup/error handling used by the other process creation paths in
LLPluginProcessParent.
| 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()); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject invalid daemon control ports before casting.
A corrupt/stale rendezvous file can contain a value above 65535; casting that U32 to apr_port_t can wrap and register with the wrong localhost port.
Proposed fix
U32 port = 0;
- f >> port;
+ if (!(f >> port) || port > 65535)
+ {
+ return 0;
+ }
return port;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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()); | |
| U32 port = 0; | |
| if (!(f >> port) || port > 65535) | |
| { | |
| return 0; | |
| } | |
| return port; | |
| } | |
| bool LLPluginProcessParent::registerWithDaemon(U32 control_port) | |
| { | |
| if (!control_port) | |
| { | |
| return false; | |
| } | |
| apr_socket_t* sock = nullptr; | |
| if (apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP, gAPRPoolp) != APR_SUCCESS) | |
| { | |
| return false; | |
| } | |
| apr_socket_timeout_set(sock, 2 * APR_USEC_PER_SEC); | |
| bool ok = false; | |
| apr_sockaddr_t* addr = nullptr; | |
| if (apr_sockaddr_info_get(&addr, "127.0.0.1", APR_INET, (apr_port_t)control_port, 0, gAPRPoolp) == APR_SUCCESS && | |
| apr_socket_connect(sock, addr) == APR_SUCCESS) | |
| { | |
| // Tell the daemon which port to connect back to for this tab. | |
| std::string msg = stringize(mBoundPort) + "\n"; | |
| apr_size_t len = msg.size(); | |
| ok = (apr_socket_send(sock, msg.data(), &len) == APR_SUCCESS) && (len == msg.size()); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/llplugin/llpluginprocessparent.cpp` around lines 1390 - 1417, The
daemon registration path in LLPluginProcessParent::registerWithDaemon accepts a
U32 control_port but casts it directly to apr_port_t, which can wrap if the
rendezvous file contains an out-of-range value. Add a bounds check before
apr_sockaddr_info_get() so only valid TCP ports (1-65535) are accepted, and
return false for invalid values. Keep the validation in registerWithDaemon so
the bad port is rejected before any socket connection attempt.
| // 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) | ||
| { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Read the full newline-terminated registration before parsing.
apr_socket_recv() can return a partial TCP segment; accepting a trimmed partial port can connect the tab to the wrong local port or fail registration. Accumulate until \n or timeout before convertToU32().
Proposed fix
- char buf[32] = {0};
- apr_size_t len = sizeof(buf) - 1;
- if (apr_socket_recv(incoming, buf, &len) == APR_SUCCESS && len > 0)
+ std::string s;
+ char buf[32] = {0};
+ while (s.find('\n') == std::string::npos && s.size() < sizeof(buf) - 1)
{
- buf[len] = '\0';
- std::string s(buf);
+ apr_size_t len = sizeof(buf) - 1 - s.size();
+ if (apr_socket_recv(incoming, buf, &len) != APR_SUCCESS || len == 0)
+ {
+ break;
+ }
+ s.append(buf, len);
+ }
+ if (s.find('\n') != std::string::npos)
+ {
LLStringUtil::trim(s);
U32 parent_port = 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 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) | |
| { | |
| // Blocking read of the short port line (the registration is tiny). | |
| apr_socket_timeout_set(incoming, 1000000); // 1s | |
| std::string s; | |
| char buf[32] = {0}; | |
| while (s.find('\n') == std::string::npos && s.size() < sizeof(buf) - 1) | |
| { | |
| apr_size_t len = sizeof(buf) - 1 - s.size(); | |
| if (apr_socket_recv(incoming, buf, &len) != APR_SUCCESS || len == 0) | |
| { | |
| break; | |
| } | |
| s.append(buf, len); | |
| } | |
| if (s.find('\n') != std::string::npos) | |
| { | |
| LLStringUtil::trim(s); | |
| U32 parent_port = 0; | |
| if (!s.empty() && LLStringUtil::convertToU32(s, parent_port) && parent_port) | |
| { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/llplugin/slplugin/slplugin_daemon.cpp` around lines 129 - 140, The
registration read in slplugin_daemon.cpp is parsing whatever bytes arrive from
apr_socket_recv() once, which can be only a partial TCP segment; update the
socket-read logic in the daemon’s registration handling to keep reading into an
accumulator until a full newline-terminated line is received or the timeout
expires, then trim and pass that complete string to
LLStringUtil::convertToU32(). Keep the fix localized around the incoming socket
receive block so the port is only accepted from a complete registration line.
| { | ||
| 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 |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Verify rendezvous publication before releasing the spawn lock.
If the file open/write fails, the daemon runs undiscoverable after Line 196 removes the lock; later parents can spawn additional daemons instead of registering with this one.
Proposed fix
- {
- llofstream rv(rendezvous_path, std::ios::trunc);
- rv << control_port << std::endl;
- }
+ bool rendezvous_published = false;
+ {
+ llofstream rv(rendezvous_path, std::ios::trunc);
+ if (rv)
+ {
+ rv << control_port << std::endl;
+ rv.close();
+ rendezvous_published = rv.good();
+ }
+ }
+ if (!rendezvous_published)
+ {
+ LL_WARNS("slplugin") << "daemon: failed to publish rendezvous at "
+ << rendezvous_path << "; serving a single tab" << LL_ENDL;
+ LLFile::remove(rendezvous_lock_path);
+ apr_socket_close(control);
+ return slplugin_run(first_port);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| 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 | |
| bool rendezvous_published = false; | |
| { | |
| llofstream rv(rendezvous_path, std::ios::trunc); | |
| if (rv) | |
| { | |
| rv << control_port << std::endl; | |
| rv.close(); | |
| rendezvous_published = rv.good(); | |
| } | |
| } | |
| if (!rendezvous_published) | |
| { | |
| LL_WARNS("slplugin") << "daemon: failed to publish rendezvous at " | |
| << rendezvous_path << "; serving a single tab" << LL_ENDL; | |
| LLFile::remove(rendezvous_lock_path); | |
| apr_socket_close(control); | |
| return slplugin_run(first_port); | |
| } | |
| // The rendezvous is now published, so release the spawn lock the launching | |
| // parent took - other parents can stop waiting and register. | |
| LLFile::remove(rendezvous_lock_path); | |
| LL_INFOS("slplugin") << "daemon: control port " << control_port |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/llplugin/slplugin/slplugin_daemon.cpp` around lines 190 - 197, The
rendezvous publication in slplugin_daemon.cpp should be verified before the
spawn lock is removed. In the code that writes the control port via llofstream
rv and then calls LLFile::remove(rendezvous_lock_path), add a success check for
opening/writing the rendezvous file and only release the lock after confirming
the publish succeeded; if it fails, log an error and keep the lock behavior
consistent so other parents do not spawn a duplicate daemon. Use the existing
rendezvous_path, rendezvous_lock_path, and control_port flow to locate the fix.
| eglQueryString = (PFNEGLQUERYSTRINGPROC)SDL_EGL_GetProcAddress("eglQueryString"); | ||
|
|
||
| // EGL_KHR_image | ||
| eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)SDL_EGL_GetProcAddress("eglCreateImageKHR"); | ||
| eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)SDL_EGL_GetProcAddress("eglDestroyImageKHR"); | ||
|
|
||
| // GL_OES_EGL_image | ||
| glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)SDL_EGL_GetProcAddress("glEGLImageTargetTexture2DOES"); | ||
|
|
||
| LL_INFOS("RenderInit") << "EGL_VENDOR " << ll_safe_string((const char *)eglQueryString(SDL_EGL_GetCurrentDisplay(), EGL_VENDOR)) << LL_ENDL; | ||
| LL_INFOS("RenderInit") << "EGL_VERSION " << ll_safe_string((const char *)eglQueryString(SDL_EGL_GetCurrentDisplay(), EGL_VERSION)) << LL_ENDL; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For SDL3 on Wayland/EGL, does SDL_EGL_GetProcAddress("eglQueryString") always return a valid pointer for core EGL symbols, or should callers null-check before invoking it?
💡 Result:
You should always null-check the return value of SDL_EGL_GetProcAddress. While SDL_EGL_GetProcAddress is designed to provide access to EGL library functions, it does not guarantee that the returned pointer will be valid in all circumstances [1][2]. Specifically: 1. Dynamic Loading and Backend Support: SDL_EGL_GetProcAddress relies on the underlying EGL implementation and whether an EGL library has been successfully loaded for the current video backend [3][4]. If the video driver is not initialized or if the requested function is unavailable, the function can return NULL [3][4]. 2. EGL Version Constraints: The underlying mechanism used by SDL—often involving eglGetProcAddress—is subject to EGL version constraints [3][5]. For EGL versions earlier than 1.5, eglGetProcAddress is officially specified to only return addresses for extension functions, not core entry points [5]. Although SDL attempts to mitigate this by falling back to dlsym (or the platform-specific equivalent) via SDL_LoadFunction for core symbols, there is no guarantee that every symbol will be resolvable across all platforms and driver configurations [3]. 3. Runtime Safety: Best practice in C/C++ graphics programming—and as evidenced by existing SDL implementations and user code—is to treat any function pointer retrieval as a potentially failing operation [6]. Failing to check for a NULL pointer before dereferencing it will lead to an application crash if the symbol lookup fails [7]. In summary, you should treat the return value as untrusted and verify it is not NULL before use to ensure the robustness of your application [6].
Citations:
- 1: https://wiki.libsdl.org/SDL3/SDL_EGL_GetProcAddress
- 2: https://manpages.ubuntu.com/manpages/questing/man3/SDL_EGL_GetProcAddress.3.html
- 3: https://github.com/libsdl-org/SDL/blob/41f07949/src/video/SDL_egl.c
- 4: libsdl-org/SDL@0644042
- 5: https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetProcAddress.xhtml
- 6: https://gitlab.unistra.fr/adarwich/pa2022/-/blob/58640090311d0a8447f62392e6c76d084b56eda6/SDL/src/video/offscreen/SDL_offscreenopengl.c
- 7: SDL_GL_LoadLibrary / SDL_GL_GetProcAddress fails with KMSDRM backend. libsdl-org/SDL#5399
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- llgl.cpp around initEGL ---'
sed -n '1088,1135p' indra/llrender/llgl.cpp
echo
echo '--- llgl.cpp around reloadExtensionsString ---'
sed -n '1578,1605p' indra/llrender/llgl.cpp
echo
echo '--- SDL_EGL_GetProcAddress usages in repo ---'
rg -n 'SDL_EGL_GetProcAddress\(' indra | sed -n '1,120p'
echo
echo '--- direct eglQueryString call sites in repo ---'
rg -n '\beglQueryString\b' indra | sed -n '1,160p'Repository: AlchemyViewer/Alchemy
Length of output: 5645
Guard the eglQueryString lookup before the first call. SDL_EGL_GetProcAddress("eglQueryString") is used immediately on lines 1115-1116, so a failed lookup turns Wayland/EGL startup into a null-call. reloadExtensionsString() already treats the same symbol as optional.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/llrender/llgl.cpp` around lines 1106 - 1116, Guard the eglQueryString
function pointer before using it in the EGL init path: the lookup via
SDL_EGL_GetProcAddress("eglQueryString") can fail, and the first RenderInit log
calls use it immediately. Update the llgl.cpp initialization around
eglQueryString so it checks for a valid pointer before calling it, and fall back
to the same optional/skip behavior used by reloadExtensionsString() when the
symbol is unavailable.
| if (w->mutex) | ||
| { | ||
| if (FAILED(w->mutex->AcquireSync(0, 1000))) | ||
| { | ||
| return false; // producer busy this frame; try again next |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Avoid a 1-second stall on the viewer GL thread.
AcquireSync(0, 1000) can block the media update/render path for up to a second when the producer is busy. Use a non-blocking or very short timeout and retry next frame.
Proposed fix
- if (FAILED(w->mutex->AcquireSync(0, 1000)))
+ const HRESULT hr = w->mutex->AcquireSync(0, 0);
+ if (hr == DXGI_ERROR_WAIT_TIMEOUT)
{
return false; // producer busy this frame; try again next
}
+ if (FAILED(hr))
+ {
+ LL_WARNS("Media") << "accelerated paint: AcquireSync failed hr=0x"
+ << std::hex << hr << std::dec << LL_ENDL;
+ return false;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (w->mutex) | |
| { | |
| if (FAILED(w->mutex->AcquireSync(0, 1000))) | |
| { | |
| return false; // producer busy this frame; try again next | |
| if (w->mutex) | |
| { | |
| const HRESULT hr = w->mutex->AcquireSync(0, 0); | |
| if (hr == DXGI_ERROR_WAIT_TIMEOUT) | |
| { | |
| return false; // producer busy this frame; try again next | |
| } | |
| if (FAILED(hr)) | |
| { | |
| LL_WARNS("Media") << "accelerated paint: AcquireSync failed hr=0x" | |
| << std::hex << hr << std::dec << LL_ENDL; | |
| return false; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/newview/llcefaccelinterop.cpp` around lines 195 - 199, The AcquireSync
call in llcefaccelinterop.cpp currently waits up to 1000 ms inside the viewer GL
update path, which can stall rendering when the producer is busy. Update the
logic in the relevant sync/acquire section around w->mutex->AcquireSync so it
uses a non-blocking or very short timeout and immediately returns false to retry
on the next frame, preserving the existing “try again next” behavior without
blocking the viewer thread.
| 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; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep the previous macOS surface until the new IOSurface bind succeeds.
This releases/replaces m->surface before checking CGLTexImageIOSurface2D; on failure blitTo() can still see a non-null surface and continue with an invalid or stale texture binding.
Proposed fix
- if (m->surface) { CFRelease(m->surface); }
- m->surface = surf;
- m->width = (int)IOSurfaceGetWidth(surf);
- m->height = (int)IOSurfaceGetHeight(surf);
+ const int new_width = (int)IOSurfaceGetWidth(surf);
+ const int new_height = (int)IOSurfaceGetHeight(surf);
CGLContextObj cgl = CGLGetCurrentContext();
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, m->tex);
CGLError err = CGLTexImageIOSurface2D(cgl, GL_TEXTURE_RECTANGLE_ARB, GL_RGBA,
- m->width, m->height, GL_BGRA,
+ new_width, new_height, GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV, surf, 0);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
- return err == kCGLNoError;
+ if (err != kCGLNoError)
+ {
+ CFRelease(surf);
+ return false;
+ }
+
+ if (m->surface) { CFRelease(m->surface); }
+ m->surface = surf;
+ m->width = new_width;
+ m->height = new_height;
+ return true;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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; | |
| const int new_width = (int)IOSurfaceGetWidth(surf); | |
| const int new_height = (int)IOSurfaceGetHeight(surf); | |
| CGLContextObj cgl = CGLGetCurrentContext(); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_ARB, m->tex); | |
| CGLError err = CGLTexImageIOSurface2D(cgl, GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, | |
| new_width, new_height, GL_BGRA, | |
| GL_UNSIGNED_INT_8_8_8_8_REV, surf, 0); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); | |
| if (err != kCGLNoError) | |
| { | |
| CFRelease(surf); | |
| return false; | |
| } | |
| if (m->surface) { CFRelease(m->surface); } | |
| m->surface = surf; | |
| m->width = new_width; | |
| m->height = new_height; | |
| return true; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/newview/llcefaccelinterop.cpp` around lines 328 - 339, The macOS
IOSurface binding in blitTo() is replacing m->surface before confirming
CGLTexImageIOSurface2D succeeded, which can leave a stale surface/texture state
on failure. Update llcefaccelinterop.cpp’s surface update path so the new surf
is only assigned to m->surface after the bind call returns kCGLNoError, and keep
the previous surface/metadata intact if binding fails. Make sure any cleanup or
release of the old surface happens only after a successful bind, using the
existing m->surface, m->width, and m->height members as the state to preserve.
| l->releaseImage(); | ||
| l->image = eglCreateImageKHR(l->display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)0, attrs); | ||
| // The fds belong to the caller (received via SCM_RIGHTS); it closes them once | ||
| // we return. EGL has taken its own reference to the buffer by now. | ||
|
|
||
| static bool logged = false; | ||
| if (!logged) | ||
| { | ||
| logged = true; | ||
| LL_INFOS("Media") << "accelerated paint dma-buf import: " << (l->image != EGL_NO_IMAGE_KHR ? "ok" : "FAILED") | ||
| << " planes=" << n << " fourcc=0x" << std::hex << fourcc | ||
| << " modifier=0x" << modifier << std::dec | ||
| << " mod_ext=" << (l->has_import_modifiers ? 1 : 0) | ||
| << " used_mod=" << (use_modifier ? 1 : 0) | ||
| << " " << width << "x" << height << LL_ENDL; | ||
| } | ||
|
|
||
| if (l->image == EGL_NO_IMAGE_KHR) | ||
| { | ||
| LL_WARNS("Media") << "accelerated paint: eglCreateImageKHR(dma_buf) failed (planes=" << n | ||
| << " modifier=0x" << std::hex << modifier << std::dec << ")" << LL_ENDL; | ||
| return false; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not discard the last good Linux EGLImage before the replacement imports.
releaseImage() runs before eglCreateImageKHR; one bad dma-buf frame destroys the current binding and leaves blitTo() with no image. Create into a temporary image, then swap only on success.
Proposed fix
- l->releaseImage();
- l->image = eglCreateImageKHR(l->display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)0, attrs);
+ EGLImageKHR new_image = eglCreateImageKHR(l->display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)0, attrs);
@@
- LL_INFOS("Media") << "accelerated paint dma-buf import: " << (l->image != EGL_NO_IMAGE_KHR ? "ok" : "FAILED")
+ LL_INFOS("Media") << "accelerated paint dma-buf import: " << (new_image != EGL_NO_IMAGE_KHR ? "ok" : "FAILED")
@@
- if (l->image == EGL_NO_IMAGE_KHR)
+ if (new_image == EGL_NO_IMAGE_KHR)
{
LL_WARNS("Media") << "accelerated paint: eglCreateImageKHR(dma_buf) failed (planes=" << n
<< " modifier=0x" << std::hex << modifier << std::dec << ")" << LL_ENDL;
return false;
}
+ l->releaseImage();
+ l->image = new_image;
l->width = width;Also applies to: 570-574
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/newview/llcefaccelinterop.cpp` around lines 546 - 568, The accelerated
paint import path in llcefaccelinterop::releaseImage/eglCreateImageKHR currently
destroys the existing Linux EGLImage before confirming the new dma-buf import
succeeds, so a failed frame can leave blitTo() with nothing usable. Change the
import flow to create the new EGLImage in a temporary handle first, and only
call releaseImage() or replace l->image after eglCreateImageKHR succeeds; if it
fails, keep the previous image intact and return false while preserving the last
good binding.
| if (mMediaSource->getUseAcceleratedPaint()) | ||
| { | ||
| #if LL_DARWIN || LL_LINUX | ||
| // Bring up the surface side channel up front (not gated on a frame): the | ||
| // plugin only starts producing once our receive endpoint exists (mach port | ||
| // on macOS, AF_UNIX socket on Linux), so waiting for the first frame to | ||
| // register would deadlock. | ||
| LLCEFSurfaceReceiver::instance().ensureStarted(); | ||
| #endif | ||
| if (!mSuspendUpdates && mVisible && mMediaSource->getAcceleratedPaintDirty()) | ||
| { | ||
| updateAcceleratedTexture(); | ||
| } | ||
| return; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Add a CPU fallback when accelerated interop is unavailable.
If LLCEFAccelInterop::init() returns false, updateAcceleratedTexture() returns false but update() still skips the CPU upload path, leaving media blank and retrying interop initialization on later dirty frames. Disable/recreate with CPU paint, or only request accelerated paint after a successful capability preflight.
Also applies to: 3247-3255
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/newview/llviewermedia.cpp` around lines 3069 - 3082, The accelerated
paint path in llviewermedia.cpp can leave media blank when
LLCEFAccelInterop::init() fails because update() returns early after
updateAcceleratedTexture() without falling back to CPU upload. Update the logic
around mMediaSource->getUseAcceleratedPaint(), updateAcceleratedTexture(), and
the later CPU paint path so that failed interop either disables/recreates
accelerated mode and allows CPU painting, or only enables accelerated paint
after a successful capability preflight; apply the same fix in both affected
update branches.
| 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); | ||
| } | ||
| #elif LL_WINDOWS | ||
| // The handle is persistent (re)sent only on (re)create. (Re)bind the interop | ||
| // when it differs from what we have bound; only advance mAccelBoundHandle on a | ||
| // successful bind so a transient failure is retried with the same handle. | ||
| unsigned long long handle = mMediaSource->getAcceleratedPaintHandle(); | ||
| if (handle != 0 && handle != mAccelBoundHandle) | ||
| { | ||
| if (mAccelInterop->setStableTexture(handle, | ||
| mMediaSource->getAcceleratedPaintWidth(), | ||
| mMediaSource->getAcceleratedPaintHeight(), | ||
| mMediaSource->getAcceleratedPaintFormat())) | ||
| { | ||
| mAccelBoundHandle = handle; | ||
| } | ||
| else | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
| #else // LL_LINUX | ||
| // CEF's dma-buf fds arrive out-of-band via the SCM_RIGHTS side channel (a | ||
| // dma-buf fd cannot cross the process boundary through the LLSD/TCP channel, | ||
| // nor be reopened via /proc), demuxed by this media's accel id. Take the | ||
| // newest frame, import its planes directly (the fds are open in this process), | ||
| // then close them. Keep the current binding if no new frame arrived this tick. | ||
| LLCEFSurfaceReceiver::DmabufFrame frame; | ||
| if (LLCEFSurfaceReceiver::instance().takeLatestDmabuf(mMediaSource->getAccelId(), frame)) | ||
| { | ||
| unsigned long long plane_fds[4] = {}; | ||
| unsigned int plane_strides[4] = {}; | ||
| unsigned long long plane_offsets[4] = {}; | ||
| for (int i = 0; i < frame.plane_count && i < 4; ++i) | ||
| { | ||
| plane_fds[i] = (unsigned long long)frame.fd[i]; | ||
| plane_strides[i] = frame.stride[i]; | ||
| plane_offsets[i] = frame.offset[i]; | ||
| } | ||
| // The interop imports the fds directly (no /proc); we own them and close | ||
| // them right after - EGL keeps its own reference once the image is made. | ||
| mAccelInterop->setStableTexture(plane_fds[0], | ||
| frame.width, frame.height, frame.format, | ||
| plane_strides[0], plane_offsets[0], frame.modifier, | ||
| 0, frame.plane_count, plane_fds, plane_strides, plane_offsets); | ||
| frame.closeFds(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Honor setStableTexture() failures on macOS and Linux.
The Windows branch returns on bind failure, but macOS/Linux continue to blitTo() after a failed import, which can display stale content from the previous binding.
Proposed fix
- mAccelInterop->setStableTexture((unsigned long long)(uintptr_t)surf,
- mMediaSource->getAcceleratedPaintWidth(),
- mMediaSource->getAcceleratedPaintHeight(),
- mMediaSource->getAcceleratedPaintFormat(),
- 0, 0, 0, 0);
+ if (!mAccelInterop->setStableTexture((unsigned long long)(uintptr_t)surf,
+ mMediaSource->getAcceleratedPaintWidth(),
+ mMediaSource->getAcceleratedPaintHeight(),
+ mMediaSource->getAcceleratedPaintFormat(),
+ 0, 0, 0, 0))
+ {
+ return false;
+ }
@@
- mAccelInterop->setStableTexture(plane_fds[0],
- frame.width, frame.height, frame.format,
- plane_strides[0], plane_offsets[0], frame.modifier,
- 0, frame.plane_count, plane_fds, plane_strides, plane_offsets);
+ bool imported = mAccelInterop->setStableTexture(plane_fds[0],
+ frame.width, frame.height, frame.format,
+ plane_strides[0], plane_offsets[0], frame.modifier,
+ 0, frame.plane_count, plane_fds, plane_strides, plane_offsets);
frame.closeFds();
+ if (!imported)
+ {
+ return false;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| } | |
| #elif LL_WINDOWS | |
| // The handle is persistent (re)sent only on (re)create. (Re)bind the interop | |
| // when it differs from what we have bound; only advance mAccelBoundHandle on a | |
| // successful bind so a transient failure is retried with the same handle. | |
| unsigned long long handle = mMediaSource->getAcceleratedPaintHandle(); | |
| if (handle != 0 && handle != mAccelBoundHandle) | |
| { | |
| if (mAccelInterop->setStableTexture(handle, | |
| mMediaSource->getAcceleratedPaintWidth(), | |
| mMediaSource->getAcceleratedPaintHeight(), | |
| mMediaSource->getAcceleratedPaintFormat())) | |
| { | |
| mAccelBoundHandle = handle; | |
| } | |
| else | |
| { | |
| return false; | |
| } | |
| } | |
| #else // LL_LINUX | |
| // CEF's dma-buf fds arrive out-of-band via the SCM_RIGHTS side channel (a | |
| // dma-buf fd cannot cross the process boundary through the LLSD/TCP channel, | |
| // nor be reopened via /proc), demuxed by this media's accel id. Take the | |
| // newest frame, import its planes directly (the fds are open in this process), | |
| // then close them. Keep the current binding if no new frame arrived this tick. | |
| LLCEFSurfaceReceiver::DmabufFrame frame; | |
| if (LLCEFSurfaceReceiver::instance().takeLatestDmabuf(mMediaSource->getAccelId(), frame)) | |
| { | |
| unsigned long long plane_fds[4] = {}; | |
| unsigned int plane_strides[4] = {}; | |
| unsigned long long plane_offsets[4] = {}; | |
| for (int i = 0; i < frame.plane_count && i < 4; ++i) | |
| { | |
| plane_fds[i] = (unsigned long long)frame.fd[i]; | |
| plane_strides[i] = frame.stride[i]; | |
| plane_offsets[i] = frame.offset[i]; | |
| } | |
| // The interop imports the fds directly (no /proc); we own them and close | |
| // them right after - EGL keeps its own reference once the image is made. | |
| mAccelInterop->setStableTexture(plane_fds[0], | |
| frame.width, frame.height, frame.format, | |
| plane_strides[0], plane_offsets[0], frame.modifier, | |
| 0, frame.plane_count, plane_fds, plane_strides, plane_offsets); | |
| frame.closeFds(); | |
| void* surf = LLCEFSurfaceReceiver::instance().takeLatest(mMediaSource->getAccelId()); | |
| if (surf) | |
| { | |
| if (!mAccelInterop->setStableTexture((unsigned long long)(uintptr_t)surf, | |
| mMediaSource->getAcceleratedPaintWidth(), | |
| mMediaSource->getAcceleratedPaintHeight(), | |
| mMediaSource->getAcceleratedPaintFormat(), | |
| 0, 0, 0, 0)) | |
| { | |
| return false; | |
| } | |
| } | |
| `#elif` LL_WINDOWS | |
| // The handle is persistent (re)sent only on (re)create. (Re)bind the interop | |
| // when it differs from what we have bound; only advance mAccelBoundHandle on a | |
| // successful bind so a transient failure is retried with the same handle. | |
| unsigned long long handle = mMediaSource->getAcceleratedPaintHandle(); | |
| if (handle != 0 && handle != mAccelBoundHandle) | |
| { | |
| if (mAccelInterop->setStableTexture(handle, | |
| mMediaSource->getAcceleratedPaintWidth(), | |
| mMediaSource->getAcceleratedPaintHeight(), | |
| mMediaSource->getAcceleratedPaintFormat())) | |
| { | |
| mAccelBoundHandle = handle; | |
| } | |
| else | |
| { | |
| return false; | |
| } | |
| } | |
| `#else` // LL_LINUX | |
| // CEF's dma-buf fds arrive out-of-band via the SCM_RIGHTS side channel (a | |
| // dma-buf fd cannot cross the process boundary through the LLSD/TCP channel, | |
| // nor be reopened via /proc), demuxed by this media's accel id. Take the | |
| // newest frame, import its planes directly (the fds are open in this process), | |
| // then close them. Keep the current binding if no new frame arrived this tick. | |
| LLCEFSurfaceReceiver::DmabufFrame frame; | |
| if (LLCEFSurfaceReceiver::instance().takeLatestDmabuf(mMediaSource->getAccelId(), frame)) | |
| { | |
| unsigned long long plane_fds[4] = {}; | |
| unsigned int plane_strides[4] = {}; | |
| unsigned long long plane_offsets[4] = {}; | |
| for (int i = 0; i < frame.plane_count && i < 4; ++i) | |
| { | |
| plane_fds[i] = (unsigned long long)frame.fd[i]; | |
| plane_strides[i] = frame.stride[i]; | |
| plane_offsets[i] = frame.offset[i]; | |
| } | |
| // The interop imports the fds directly (no /proc); we own them and close | |
| // them right after - EGL keeps its own reference once the image is made. | |
| bool imported = mAccelInterop->setStableTexture(plane_fds[0], | |
| frame.width, frame.height, frame.format, | |
| plane_strides[0], plane_offsets[0], frame.modifier, | |
| 0, frame.plane_count, plane_fds, plane_strides, plane_offsets); | |
| frame.closeFds(); | |
| if (!imported) | |
| { | |
| return false; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indra/newview/llviewermedia.cpp` around lines 3265 - 3317, The macOS/Linux
paths in llviewermedia.cpp are ignoring setStableTexture() import failures and
still proceeding to blitTo(), which can leave stale content visible. Update the
platform-specific binding logic in the media update path around
LLCEFSurfaceReceiver::instance().takeLatest() / takeLatestDmabuf() so that a
failed mAccelInterop->setStableTexture() causes an immediate return false,
matching the Windows behavior and preventing fallback to the previous texture
binding.
Description
Related Issues
Issue Link:
Checklist
Please ensure the following before requesting review:
Additional Notes