Skip to content

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

Draft
RyeMutt wants to merge 10 commits into
developfrom
rye/cef-daemon
Draft

Rework of CEF integration to support broker-mode and accelerated copy#334
RyeMutt wants to merge 10 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

Warning

Review limit reached

@RyeMutt, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8ee83cd3-311c-4aba-abb5-2dbc83b75ab3

📥 Commits

Reviewing files that changed from the base of the PR and between cee877a and 936c0b1.

📒 Files selected for processing (35)
  • indra/dullahan
  • 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_generic.cpp
  • indra/llplugin/slplugin/slplugin_info.plist
  • indra/llrender/llgl.cpp
  • indra/llrender/llglheaders.h
  • indra/llwindow/lldxhardware.cpp
  • indra/llwindow/lldxhardware.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/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/viewer_manifest.py
  • indra/vcpkg.json

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.

if (LLFile::stat(lock_path, &st) == 0 &&
(time(nullptr) - st.st_mtime) > LOCK_STALE_SECONDS)
{
LLFile::remove(lock_path);
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