Skip to content

Rework of CEF integration to support broker-mode and accelerated copy#334

Open
RyeMutt wants to merge 14 commits into
developfrom
rye/cef-daemon
Open

Rework of CEF integration to support broker-mode and accelerated copy#334
RyeMutt wants to merge 14 commits into
developfrom
rye/cef-daemon

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Jun 29, 2026

Copy link
Copy Markdown
Member

Description

Related Issues

  • Please link to a relevant GitHub issue for additional context.
    • Bug Fix: Link to an issue that includes reproduction steps and testing guidance.
    • Feature/Enhancement: Link to an issue with a write-up, rationale, and requirements.

Issue Link:


Checklist

Please ensure the following before requesting review:

  • I have provided a clear title and detailed description for this pull request.
  • If useful, I have included media such as screenshots and video to show off my changes.
  • I have tested the changes locally and verified they work as intended.
  • All new and existing tests pass.
  • Code follows the project's style guidelines.
  • Documentation has been updated if needed.
  • Any dependent changes have been merged and published in downstream modules
  • I have reviewed the contributing guidelines.

Additional Notes

RyeMutt and others added 10 commits June 29, 2026 09:21
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>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 368e2734-35aa-4a2c-8eb7-5a1861e3f892

📥 Commits

Reviewing files that changed from the base of the PR and between 9f830d9 and f53fc71.

📒 Files selected for processing (11)
  • indra/llplugin/llpluginprocessparent.cpp
  • indra/llplugin/slplugin/slplugin_daemon.cpp
  • indra/llrender/llgl.cpp
  • indra/llwindow/lldxhardware.cpp
  • indra/media_plugins/cef/CMakeLists.txt
  • indra/media_plugins/cef/media_plugin_cef.cpp
  • indra/media_plugins/cef/slplugin_cef_bootstrap.cpp
  • indra/newview/app_settings/settings_alchemy.xml
  • indra/newview/llcefaccelinterop.cpp
  • indra/newview/llcefaccelinterop.h
  • indra/newview/llviewermedia.cpp
🚧 Files skipped from review as they are similar to previous changes (11)
  • indra/newview/app_settings/settings_alchemy.xml
  • indra/newview/llcefaccelinterop.h
  • indra/media_plugins/cef/slplugin_cef_bootstrap.cpp
  • indra/llrender/llgl.cpp
  • indra/newview/llcefaccelinterop.cpp
  • indra/llplugin/llpluginprocessparent.cpp
  • indra/llwindow/lldxhardware.cpp
  • indra/llplugin/slplugin/slplugin_daemon.cpp
  • indra/media_plugins/cef/CMakeLists.txt
  • indra/newview/llviewermedia.cpp
  • indra/media_plugins/cef/media_plugin_cef.cpp

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added faster CEF media rendering with accelerated, zero-copy texture sharing on Windows, macOS, and Linux.
    • Introduced a shared host mode for media plugins to improve startup and reuse across tabs.
    • Updated platform packaging to ship dedicated media plugin host apps/executables.
  • Bug Fixes

    • Improved plugin launch and cleanup behavior across platforms.
    • Added better recovery handling for media plugin failures and restarts.
    • Updated display backend detection for Linux and Wayland.

Walkthrough

Replaces the old plugin launcher model with statically linked media-plugin host executables, adds shared CEF daemon mode with recovery logic, and introduces accelerated paint transport plus viewer-side GPU interop for Windows, macOS, and Linux.

Changes

Static plugin hosts and packaging

Layer / File(s) Summary
Plugin path API and host names
indra/llfilesystem/lldir.h, indra/llfilesystem/lldir_linux.cpp, indra/llfilesystem/lldir_linux.h, indra/llfilesystem/lldir_mac.h, indra/llfilesystem/lldir_mac.mm, indra/llfilesystem/lldir_win32.cpp, indra/llfilesystem/lldir_win32.h, indra/llfilesystem/tests/lldir_test.cpp, indra/newview/tests/lldir_stub.cpp
getLLPluginLauncher() is removed and getLLPluginFilename() now resolves to direct per-plugin host executable paths on Linux, macOS, and Windows.
Static plugin init registration
indra/llplugin/llplugininstance.h, indra/llplugin/llplugininstance.cpp
LLPluginInstance removes runtime shared-library loading and stores a registered static init function that load() calls directly.
Host entry wiring
indra/llplugin/slplugin/slplugin.cpp, indra/llplugin/slplugin/slplugin_static.cpp, indra/llplugin/slplugin/CMakeLists.txt, indra/llplugin/slplugin/slplugin_info.plist
slplugin.cpp parses --daemon and forwards the rendezvous path, slplugin_static.cpp exposes the static init hook and host entry, and the SLPlugin packaging plist is expanded for the new host bundle layout.
Media plugin host builds and packaging
indra/media_plugins/cef/CMakeLists.txt, indra/media_plugins/example/CMakeLists.txt, indra/media_plugins/gstreamer10/CMakeLists.txt, indra/media_plugins/libvlc/CMakeLists.txt, indra/newview/CMakeLists.txt, indra/newview/viewer_manifest.py, indra/dullahan, indra/vcpkg.json, indra/media_plugins/cef/SLPluginCEF-Info.plist.in
CEF, example, GStreamer, and LibVLC targets switch to statically linked host executables; viewer build and manifest rules package per-plugin hosts; CEF plist and vcpkg inputs are updated for the host layout.
Minor viewer file update
indra/newview/llsyntaxid.cpp
The syntax cache writer switches to llofstream for the cache file stream.

Shared CEF daemon mode

Layer / File(s) Summary
Child daemon teardown
indra/llplugin/llpluginprocesschild.h, indra/llplugin/llpluginprocesschild.cpp
LLPluginProcessChild gains daemon mode, changing teardown and socket-loss handling so daemon tabs can shut down without exiting the whole process.
Parent daemon discovery
indra/llplugin/llpluginprocessparent.h, indra/llplugin/llpluginprocessparent.cpp
LLPluginProcessParent adds daemon configuration, rendezvous handling, lockfile coordination, and daemon-aware launch, exit, and lockup transitions.
Daemon host loop
indra/llplugin/slplugin/slplugin_daemon.cpp
slplugin_daemon_run() creates the control listener, publishes the rendezvous file, launches the initial tab, accepts registrations, tracks live tabs, and exits after the idle timeout.
Viewer daemon routing and recovery
indra/llplugin/llpluginclassmedia.h, indra/llplugin/llpluginclassmedia.cpp, indra/newview/llviewermedia.h, indra/newview/llviewermedia.cpp, indra/newview/app_settings/settings_alchemy.xml
LLPluginClassMedia adds daemon and display-server fields, and LLViewerMediaImpl launches daemon-backed CEF tabs, clears stale rendezvous files, and schedules bounded recovery retries after plugin failure.

Accelerated paint and texture interop

Layer / File(s) Summary
GL, EGL, and D3D11 interop plumbing
indra/llrender/llglheaders.h, indra/llrender/llgl.cpp, indra/llwindow/lldxhardware.h, indra/llwindow/lldxhardware.cpp, indra/llwindow/llwindowwin32.cpp
WGL and EGL interop entry points are declared and loaded, LLDXHardware gains D3D11↔OpenGL interop setup/teardown, and Win32 window lifecycle code initializes and tears down the shared interop state.
Display server reporting
indra/llwindow/llwindow.h, indra/llwindow/llwindowsdl.h, indra/llwindow/llwindowsdl.cpp
LLWindow adds getDisplayServer(), and LLWindowSDL implements it to report the active SDL backend for Linux CEF routing.
Plugin accelerated paint signaling
indra/llplugin/llpluginclassmedia.h, indra/llplugin/llpluginclassmedia.cpp, indra/media_plugins/cef/media_plugin_cef.cpp
LLPluginClassMedia adds accelerated paint metadata, and media_plugin_cef adds platform-specific accelerated paint producers and init-time wiring for accelerated_paint, host_pid, accel_id, and display_server.
Viewer surface receiver
indra/newview/llcefsurfacereceiver.h, indra/newview/llcefsurfacereceiver.cpp
LLCEFSurfaceReceiver is added as a singleton that drains macOS Mach messages into IOSurfaceRef values and Linux datagrams into dma-buf frames, with no-op stubs on other platforms.
Viewer texture interop
indra/newview/llcefaccelinterop.h, indra/newview/llcefaccelinterop.cpp
LLCEFAccelInterop is added to import shared handles into GL textures and blit them on Windows, macOS, and Linux, with stubbed behavior on other platforms.
Viewer accelerated paint consumer
indra/newview/llviewermedia.h, indra/newview/llviewermedia.cpp
LLViewerMediaImpl tears down interop state, clears bound-handle state when sources are destroyed, adds an accelerated paint update path, and imports the newest platform surface into the viewer texture through LLCEFAccelInterop.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hop-hop, the launcher’s gone,
Per-plugin hosts now roll along.
One daemon hums, one texture gleams,
No-copy paint for bunny dreams.
Wayland, macOS, and Win32—
The rabbit dances GPU-free.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is still the default template and contains no actual summary, related issue, checklist details, or notes. Replace the template text with a real PR summary, link a related issue, and note testing, documentation, and any reviewer context.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main CEF daemon and accelerated-copy rework.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread indra/llplugin/llpluginprocessparent.cpp Dismissed
RyeMutt and others added 2 commits June 30, 2026 01:54
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>
@RyeMutt RyeMutt marked this pull request as ready for review June 30, 2026 07:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Tear down the accelerated binding when the media source is destroyed.

Resetting only mAccelBoundHandle leaves mAccelInterop holding 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 win

Move 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 plus static_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 win

Update the public contract comments to match the implemented platforms.

The implementation expects a +1 IOSurfaceRef on 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 win

Check framebuffer completeness before reporting a successful blit.

All three platform paths return true after glBlitFramebuffer without verifying the read/draw FBOs are complete. Add glCheckFramebufferStatus checks 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

📥 Commits

Reviewing files that changed from the base of the PR and between cee877a and 9f830d9.

📒 Files selected for processing (50)
  • indra/dullahan
  • indra/llfilesystem/lldir.h
  • indra/llfilesystem/lldir_linux.cpp
  • indra/llfilesystem/lldir_linux.h
  • indra/llfilesystem/lldir_mac.h
  • indra/llfilesystem/lldir_mac.mm
  • indra/llfilesystem/lldir_win32.cpp
  • indra/llfilesystem/lldir_win32.h
  • indra/llfilesystem/tests/lldir_test.cpp
  • indra/llplugin/llpluginclassmedia.cpp
  • indra/llplugin/llpluginclassmedia.h
  • indra/llplugin/llplugininstance.cpp
  • indra/llplugin/llplugininstance.h
  • indra/llplugin/llpluginprocesschild.cpp
  • indra/llplugin/llpluginprocesschild.h
  • indra/llplugin/llpluginprocessparent.cpp
  • indra/llplugin/llpluginprocessparent.h
  • indra/llplugin/slplugin/CMakeLists.txt
  • indra/llplugin/slplugin/slplugin.cpp
  • indra/llplugin/slplugin/slplugin_daemon.cpp
  • indra/llplugin/slplugin/slplugin_info.plist
  • indra/llplugin/slplugin/slplugin_static.cpp
  • indra/llrender/llgl.cpp
  • indra/llrender/llglheaders.h
  • indra/llwindow/lldxhardware.cpp
  • indra/llwindow/lldxhardware.h
  • indra/llwindow/llwindow.h
  • indra/llwindow/llwindowsdl.cpp
  • indra/llwindow/llwindowsdl.h
  • indra/llwindow/llwindowwin32.cpp
  • indra/media_plugins/cef/CMakeLists.txt
  • indra/media_plugins/cef/SLPluginCEF-Info.plist.in
  • indra/media_plugins/cef/media_plugin_cef.cpp
  • indra/media_plugins/cef/slplugin_cef.cpp
  • indra/media_plugins/cef/slplugin_cef_bootstrap.cpp
  • indra/media_plugins/example/CMakeLists.txt
  • indra/media_plugins/gstreamer10/CMakeLists.txt
  • indra/media_plugins/libvlc/CMakeLists.txt
  • indra/newview/CMakeLists.txt
  • indra/newview/app_settings/settings_alchemy.xml
  • indra/newview/llcefaccelinterop.cpp
  • indra/newview/llcefaccelinterop.h
  • indra/newview/llcefsurfacereceiver.cpp
  • indra/newview/llcefsurfacereceiver.h
  • indra/newview/llsyntaxid.cpp
  • indra/newview/llviewermedia.cpp
  • indra/newview/llviewermedia.h
  • indra/newview/tests/lldir_stub.cpp
  • indra/newview/viewer_manifest.py
  • indra/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

Comment thread indra/llplugin/llpluginprocessparent.cpp
Comment thread indra/llplugin/llpluginprocessparent.cpp
Comment thread indra/llplugin/slplugin/slplugin_daemon.cpp
Comment thread indra/llplugin/slplugin/slplugin_daemon.cpp
Comment thread indra/llrender/llgl.cpp Outdated
Comment thread indra/newview/llcefaccelinterop.cpp Outdated
Comment thread indra/newview/llcefaccelinterop.cpp Outdated
Comment thread indra/newview/llcefaccelinterop.cpp Outdated
Comment thread indra/newview/llviewermedia.cpp
Comment thread indra/newview/llviewermedia.cpp
Resolve all 16 actionable CodeRabbit findings:

Daemon / IPC robustness:
- llpluginprocessparent: handle LLProcess::create daemon-launch failure
  (release spawn lock + errorState); reject control ports > 65535.
- slplugin_daemon: accumulate the registration read until newline
  (partial-segment safe); verify the rendezvous published before
  releasing the spawn lock, else fall back to single-tab.
- slplugin_cef_bootstrap: convert lpCmdLine via WideCharToMultiByte so
  non-ASCII rendezvous paths survive.

GPU interop correctness:
- media_plugin_cef: produce() reports a dropped frame (not success) when
  the keyed-mutex copy does not complete.
- llcefaccelinterop: fail the Windows bind without IDXGIKeyedMutex; use a
  non-blocking AcquireSync (no 1s GL-thread stall); on macOS/Linux import
  into a temp then swap only on success (no stale binding on a bad frame).
- lldxhardware: require all wglDX* entry points before reporting interop.
- llgl: null-check eglQueryString before use.

Viewer media path:
- llviewermedia: capability preflight (LLCEFAccelInterop::isSupported) so
  accelerated paint is only requested when the consumer can bind it;
  honor macOS/Linux setStableTexture failures; fully tear down the interop
  in destroyMediaSource.

Misc:
- settings_alchemy: comments now match the intentional on-by-default values.
- CMake: fix achemyviewer -> alchemyviewer macOS bundle id typo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants