Skip to content

Project/embedded browser - #6195

Open
callumlinden wants to merge 78 commits into
secondlife:developfrom
callumlinden:project/embedded-browser
Open

Project/embedded browser#6195
callumlinden wants to merge 78 commits into
secondlife:developfrom
callumlinden:project/embedded-browser

Conversation

@callumlinden

Copy link
Copy Markdown
Contributor

Description

Indirect way to push the Embedded Viewer repo to a Viewer branch: Go via my own Linden repository and avoid the pre-commit branch protection rules.

callumlinden and others added 30 commits July 13, 2026 15:13
…lled llembedded browser. Creates a floater than consumes it. Currently does next to nothing - next up is to push the llembeddedbrowser::update() calls into their own thread and then flesh out the interface so we can create, destroy, update etc. different 'browser tabs'
…this appears to work fine (might need to consider double buffering the output to avoid flicker) - then the next step is to fill in more of the browser manager and browser tab stuff
…re swatch by creating a next-POT texture and drawing the output ourselves with some UV scaling
…dBrowser

debug setting, defaulted on for this experimental viewer.

Makes llembeddedbrowser multi-instance (one tab/thread/buffer per media
source instead of a single global one), wires LLViewerMediaImpl's create/
destroy/update/resize/navigate lifecycle to it alongside the existing CEF
path, and adds backend-agnostic width/height/texture-size accessors so
LLMediaCtrl's draw/layout code doesn't need to reach into LLPluginClassMedia
directly. Also: fast row-based checkerboard fill and size-scaled update
rate to bound CPU/lock cost on large (up to 4096x4096, EmbeddedBrowserMax
Width/Height) buffers, and thread-safety fixes (shared_ptr-held tabs, an
atomic pixel+size snapshot handed to the async GL upload, per-tab RNG)
found in review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r, proving out real CEF frames over shared memory in place of the checkerboard placeholder.

LLEmbeddedBrowserTab now claims a view from an external cefshm_producer
over the control channel (see the sibling llcefshm-example/llshmframe/
llCefBrowser repos) and pumps read_latest() frames into its pixel buffer
each tick instead of generating a checkerboard; navigate()/resize() send
kSetUrl/kResize over the same channel. cefshm_protocol.h is a deliberate
byte-compatible copy of that repo's own protocol header.

Fixes found getting this working end-to-end:
- Source pixel format is BGRA (matching CEF's native OnPaint, same as the
  legacy plugin), not RGBA -- llviewermedia.cpp's embedded-browser texture
  setup had it hardcoded wrong.
- Prim-face rendering has no orientation compensation anywhere (unlike
  LLMediaCtrl's floater quad, which picks its UV winding based on the
  media source's self-reported coordinate convention) -- it just trusts
  the raw buffer's row order, which the legacy plugin has always supplied
  bottom-up. The shm pipeline hands back top-down rows, so
  LLEmbeddedBrowserTab::update() now flips on copy, and
  getMediaTextureCoordsOpenGL() now reports true for the embedded-browser
  backend to keep LLMediaCtrl's own UV winding in sync with that.
- LLViewerMediaImpl::navigateTo() could lose a race against the
  priority-driven idle pass: a UI-driven LLMediaCtrl calling navigateTo()
  before createMediaSource() ever ran for that impl would fall through to
  the legacy plugin path and permanently lock UseEmbeddedBrowser out via
  createMediaSource()'s own idempotency guard. navigateTo() now resolves
  the backend up front when neither has been decided yet.
- data: URIs need LLURI::escapePathAndData()'s payload re-escaping to
  parse correctly, same as the legacy loadURI() path already applies --
  the embedded-browser entry points were passing the raw string through
  unescaped.

Also repoints llfloaterembeddedbrowsertest.cpp's three test buttons (and
its initial create() call) at real URLs instead of the "red"/"green"/
"blue" tokens the checkerboard generator used to special-case, since those
have no meaning to a real CEF page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…browser.

destinations/avatar_welcome_pack/search/marketplace used to be
force-instantiated on login specifically to warm up their CEF instances
ahead of time -- worth revisiting whether that's still needed now that
those floaters can run on the embedded-browser backend instead of the
memory-heavier plugin.
…kend is wired into real LLMediaCtrl/prim media paths.

llfloaterembeddedbrowsertest predates LLViewerMediaImpl routing through
UseEmbeddedBrowser and was the only way to exercise create/destroy/
navigate/getPixels directly; that's now covered more realistically by the
Media Settings preview, the login screen, and real prim media, and its
own hand-rolled swatch-drawing code (unlike LLMediaCtrl) has no UV
compensation for the row-flip added in the previous commit, so it was
displaying upside-down. Removes the source files, XUI floater definition,
its two "Embedded Browser Test" menu entries, and the CMake/registration
references.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e entries, and menu items.

The previous commit deleted the floater's source/XUI files but missed
staging its LLFloaterReg::add() call, CMakeLists.txt entries, and the two
"Embedded Browser Test" menu items (menu_viewer.xml, menu_login.xml) --
completing that removal here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vents to the embedded browser.

Extends cefshm_protocol.h (this repo's copy and llcefshm-example's
canonical one) with kScrollWheel/kKeyEvent (consumer->producer) and
kEventLoadStart/LoadEnd/TitleChanged/AddressChanged/CursorChanged
(producer->consumer). Keyboard rides LLWindowWin32::getNativeKeyData()'s
raw msg/wParam/lParam straight into llCefBrowserManager::SendKeyEvent(),
a clean fit since that's exactly the shape it already expects.

LLEmbeddedBrowserTab gains mouseMove/mouseButton/scrollWheel/keyEvent
senders and a popEvent() FIFO; LLViewerMediaImpl wires those into every
mouse/keyboard entry point and drains events each update() via
emitEvent(nullptr, ...) -- nullptr standing in for "no real
LLPluginClassMedia behind this," since a genuine plugin callback never
passes one.

That nullptr choice forced a real audit: grepped every
LLViewerMediaObserver::handleMediaEvent override for the 5 events now
emitted, and found genuine unconditional (non-debug-log) self-></
dereferences in llfloaterhelpbrowser.cpp, llfloaterwebcontent.cpp (x3),
llpaneldirweb.cpp, and llfloater360capture.cpp -- real crash risks once
embedded-browser events start flowing, not hypothetical. Patched with
fallbacks to cached state (LLMediaCtrl::getCurrentNavUrl(), and a new
backend-agnostic LLViewerMediaImpl::getMediaName()/LLMediaCtrl::
getMediaName() pair for title, mirroring the width/height/textureCoords
accessors already added). Also defensively guarded the debug-log-only
cases in llmediactrl.cpp and llviewerparcelmedia.cpp.

Two more bugs surfaced through actual interactive testing, both in
pre-existing code this session never touched before today:

- LLMediaCtrl::convertInputCoords() computed its OpenGL-coords flag via
  mMediaSource->getMediaPlugin()->getTextureCoordsOpenGL(), bypassing the
  backend-agnostic getMediaTextureCoordsOpenGL() accessor added earlier
  this session -- getMediaPlugin() is null for embedded-browser by
  construction, so this always read false, inverting mouse Y for every
  embedded-browser click/move/scroll regardless of window size.
- Once that was fixed, Y was still wrong whenever the widget was larger
  than its aspect-corrected content (mStretchToFill/mMaintainAspectRatio
  default true for every LLMediaCtrl): the flip used getRect().getHeight()
  (the full widget) instead of the locally computed, aspect-corrected
  height from calcOffsetsAndSize(), throwing Y off by exactly the
  centering/letterbox offset. Likely a longstanding latent bug for the
  legacy plugin path too whenever centering was actually active; just
  never reported.

Also raised LLEmbeddedBrowserUpdateThread::run()'s size-scaled fps floor
(10->30) and widened its full-60fps zone (512x512->1280x720): those
numbers were tuned for bounding the CPU cost of painting the old
checkerboard placeholder, not for real interactive latency, and made
real mouse-move feedback feel sluggish on anything larger than a small
thumbnail.

Scroll wheel needed one more fix on top: LLMediaCtrl::handleScrollWheel/
handleScrollHWheel gate on hasMedia(), which is hardcoded to "is there a
real plugin" (mMediaSource != NULL) and always false for embedded-browser,
silently dropping every scroll event before it could reach any of the
above. Added a narrow LLViewerMediaImpl::isUsingEmbeddedBrowser()
accessor rather than widening hasMedia() itself, which has ~60 call sites
and at least one (LLMediaCtrl::handleToolTip) that immediately follows it
with an unchecked getMediaPlugin()->getHoverText() -- widening hasMedia()
would have traded a silent scroll-wheel bug for a tooltip crash.

Known gaps, out of scope for this pass: prim-face media has no scroll
wheel support at all today, for either backend (not a regression); mouse
double-click is sent as a plain click, relying on CEF's own
consecutive-click timing rather than an explicit wire signal; horizontal
scroll is dropped (SendMouseWheelEvent has no deltaX equivalent); and
input-to-display latency still feels sluggish to the user even after the
fps-floor fix, not yet root-caused further.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverses 1537870 -- keeping destinations/avatar_welcome_pack/search/
marketplace preloaded on login after all, a deliberate call independent
of the embedded-browser work.
…lifeviewer, not secondlife-bin.

secondlife-bin is this experimental build's own binary name; the actual
release viewer (used as the legacy comparison baseline) runs as
secondlifeviewer. Also trims the example DurationMinutes from 10 to 5.
…ugin behavior.

CEF's SendMouseWheelEvent expects wheel-delta units (~120/notch), not SL's raw
per-notch click value, and its sign convention for scroll direction is the
opposite of Dullahan's for the legacy plugin path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Forwards MEDIA_EVENT_CLICK_LINK_HREF/CLICK_LINK_NOFOLLOW so links that want
a new window/tab or navigate to a custom URL scheme (e.g. secondlife://) get
handled the same way as the legacy CEF plugin, and forwards focus/blur to the
embedded browser so CEF drives caret blink and focus/blur page JS on click,
matching the plugin path's existing behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Forwards MEDIA_EVENT_PICK_FILE_REQUEST/FILE_DOWNLOAD via a new
LLEmbeddedMediaFilePicker (mirrors LLMediaFilePicker but keyed to
LLViewerMediaImpl instead of LLPluginClassMedia, since embedded browser has
no plugin pointer) so native OS open/save dialogs work the same way as the
legacy CEF plugin -- this is what llfloater360capture.cpp's image-save flow
needs once it moves to the embedded backend. Also forwards
MEDIA_EVENT_STATUS_TEXT_CHANGED (e.g. showing a hovered link's URL), guarding
every LLViewerMediaObserver::handleMediaEvent override across the viewer that
previously dereferenced self unconditionally for that event.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LLViewerMediaImpl::executeJavaScript() had no embedded-browser branch, and
LLMediaCtrl::executeJavaScript()'s hasMedia() gate is plugin-only-false, so
neither backend-agnostic path actually worked for embedded browser -- this
is what silently broke both the WebGL preview (init(...) never ran) and the
Save button (saveAsEqrImage(...) never ran) in llfloater360capture.cpp, which
bypassed both and called getMediaPlugin()->executeJavaScript() directly.
Wires the new kExecuteJavaScript command through LLEmbeddedBrowser/Tab and
the producer, widens LLMediaCtrl's gate, and simplifies llfloater360capture.cpp
to the backend-agnostic LLMediaCtrl::executeJavaScript() call it should have
used all along.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Forwards console.log/warn/error calls via a new kEventConsoleMessage command,
composed into the same "Console message: <msg> in file(<source>) at line
<n>" text MediaPluginCEF::onConsoleMessageCallback produces, so
LLMediaCtrlListener::getMediaText()'s PAGE_TEXT_EXTRACT_MARKER search keeps
working unmodified. Also stops getMediaText() hard-failing when there's no
LLPluginClassMedia, since executeJavaScript() already gates correctly for
either backend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LLEmbeddedBrowserTab gains a mHadDisconnected flag making disconnect/
reconnect edge-triggered (fires once per outage, not once per retry tick):
update()'s existing LLReadResult::Disconnected handling pushes one
ProducerDisconnected event, connectToProducer() pushes one
ProducerReconnected event the next time it succeeds. LLViewerMediaImpl
treats ProducerDisconnected like its own MEDIA_EVENT_PLUGIN_FAILED handling
(mMediaSourceFailed, resetPreviousMediaState) but with the notification left
enabled -- unlike that case, whose notification is disabled after a past
"fires every frame" spam incident -- since edge-triggering guarantees this
one fires at most once. The popup names "Embedded Browser Provider" rather
than the legacy plugin's mime-type-derived name, since no such plugin is
involved in this failure path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the untracked local-path hack with proper use_prebuilt_binary()
installables (llshmframe, llcefbrowser), modeled on the existing
dullahan/CEFPlugin.cmake pattern. EmbeddedBrowser.cmake now defines real
ll::shmframe and ll::cefbrowser interface targets, each with an optional
LLSHMFRAME_LOCAL_BUILD_DIR/LLCEFBROWSER_LOCAL_BUILD_DIR override that
skips the autobuild fetch and links straight against a local `stage`
build, to keep fast local iteration on those repos.

llembeddedbrowser now links ll::shmframe explicitly instead of relying
on whatever was previously injecting the raw include path.

Note: the two installables currently point at local file:// archives for
testing, not a real published package -- publishing is a separate,
explicit follow-up step.
Real autobuild publishing hit a wall: both repos are private, so their
GitHub Release assets 404 for a plain unauthenticated autobuild install,
unlike cef-bin's public S3-hosted package. Until that's resolved
(pending confirmation on making the repos public), stay on local
file:// packages for both -- refreshed to match current source (the
llcefbrowser cygpath fix, llshmframe's canonical_repo correction).

canonical_repo for llshmframe now points at its real current location
(secondlife/llshmframe, moved from a personal account) regardless of
where its package is actually hosted.
Adds kEventVersionInfo to the cefshm wire protocol: cefshm_producer now
sends its CEF/Chromium version string once per slot right after
allocation, since the Viewer doesn't link CEF/llcefbrowser directly for
this path and has no other way to know which build is actually in play.
LLEmbeddedBrowser stores the most recently reported version and exposes
it alongside llshmframe's own (build-time) version.

Both now appear in the About box's Libraries section as "Embedded
Browser llShmFrame Version" / "Embedded Browser CEF Version", falling
back to "Not connected" if no producer has completed the handshake yet.
The old legacy-plugin CEF/Chromium version block (Dullahan-based) is no
longer relevant now that the embedded-browser path exists, so drop it
entirely (including the now-unused dullahan_version.h include) and
show llCefBrowser's/CEF's/Chromium's versions from the wire message in
its place instead.

Also: getShmFrameVersion() drops its redundant "llshmframe " prefix
(the About-box label already says that), and the CEF version block now
includes llCefBrowser's own version, formatted like the block it
replaces.

Note: only the English strings.xml was updated -- other locales still
reference [LIBCEF_VERSION], which will now render as a literal
unsubstituted placeholder. Pre-existing localization drift in this
fork, not something this change fixes.
Adds indra/llcefproducer (SLCefProducer.exe), a direct port of
llcefshm-example's cefshm_producer.cpp: windowless by default via a
WinMain wrapper (forwarding the MSVC CRT's __argc/__argv, since CEF's
subprocess re-exec model needs real argc/argv and WinMain's own
lpCmdLine can't provide them), with an opt-in --console debug console
toggleable via the new CefProducerShowConsole setting.

LLEmbeddedBrowser::init()/reset() (previously vestigial -- nothing
called them) now launch/kill SLCefProducer via LLProcess, gated on
UseEmbeddedBrowser; called from LLAppViewer::init()/cleanup(). The
launch is eager, not lazy, since the login screen itself renders
through this same path and needs a live producer immediately.
connectToProducer()'s existing "no producer reachable" branch now
triggers a debounced, capped auto-relaunch, reusing the disconnect
detection already built and tested on llshmframe's own heartbeat
protocol rather than adding a second liveness mechanism.

SLCefProducer.exe is packaged into llplugin/ alongside libcef.dll and
its other CEF runtime files (new LLDir::getSLCefProducerLauncher(),
mirroring getLLPluginLauncher()) rather than next to secondlife-bin.exe
-- unlike a DLL loaded from there, a standalone .exe only checks its
own directory in the default DLL search order, so it needs to actually
live where those files are.

Also refreshes the local llshmframe package registration in
autobuild.xml: a clean rebuild had fallen back to a stale local
package predating yesterday's versioning work, since the
LLSHMFRAME_LOCAL_BUILD_DIR cache override doesn't survive a clean
build-dir wipe.
LLMediaCtrl::calcOffsetsAndSize() letterboxed/pillarboxed whenever
getMediaWidth()/getMediaHeight() didn't match the widget's own aspect
ratio -- correct for a video plugin's fixed intrinsic resolution, but
for the embedded-browser (CEF) backend that value is just "the size of
the last frame that happened to arrive," not a property of the
content: a CEF browser always renders at exactly the size it was last
told to.

This showed up as the login screen's HTML widget intermittently not
filling its panel: getMediaWidth()/getMediaHeight() only updates once
a frame at a newly-requested size round-trips through the producer,
and now that the producer starts asynchronously at Viewer launch
instead of being pre-launched by hand, that round trip can take
several real seconds -- long enough for a post-construction reshape
(e.g. the window settling to its restored size) to be visibly
letterboxed against the stale reported size until the next frame
lands. Skip the aspect-ratio branch entirely for this backend and
always fill the rect; the in-flight resize converges the actual
browser size to match regardless.
Replaces duckduckgo.com with an S3-hosted page listing bookmarks useful
for testing the embedded-browser work, in both the login-screen and
main-viewer copies of this debug menu item.
New log_info()/log_connect()/log_disconnect() helpers (yellow/green/red,
via ANSI escape codes -- explicitly enabling ENABLE_VIRTUAL_TERMINAL_
PROCESSING, since a freshly allocated Windows console doesn't interpret
them by default). Deliberately ad hoc rather than a logging framework:
enough structure that adding another call site later is a one-line
thing.

Logs: slot connect (green), URL navigation (green), disconnect with a
reason -- "crashed consumer" or "idle timeout" (red), plus the existing
startup/shutdown banners now go through log_info() for consistency.

Also adds an edge-triggered "viewer detached" log using the had_subscriber
field (previously written but never read) at the moment a subscriber's
has_subscriber() flips false, separate from the slot's actual teardown
below it -- which deliberately still waits out the idle grace period in
case the same consumer reconnects shortly.

CefProducerShowConsole now defaults to true so this is visible during
active development; flip back to false once this settles down.
callumlinden and others added 21 commits August 19, 2026 11:43
The embedded-browser build's producer moved in-tree as SLCefProducer
a few days ago; this script's own examples still named the old
standalone cefshm_producer.exe, which would silently sample zero
processes for anyone following them literally.
mStatus was a plain EThreadStatus, written by the worker thread as its
last act before returning and polled by shutdown() on the destroying
thread, with the code's own comment already flagging the gap ("we
really need memory barriers here"). LLThread::start() detaches its
underlying std::thread immediately, so isStopped() becoming true is
the *only* signal the destroying thread ever gets that it's safe to
free anything -- there is no real join() anywhere in this lifecycle.
A plain, non-atomic flag gives no real cross-thread visibility
guarantee for that decision. Costs nothing (a plain enum load/store is
already lock-free) and closes a real, if narrow, race that mostly
would only show up under heavy thread create/destroy churn.
Root cause of a run of hard-to-diagnose Viewer crashes reported from a
region with many concurrent media sources. LLEmbeddedBrowser::destroy()
(and reset()) extracted a tab's shared_ptr under mTabsMutex, erased the
map entry, and let the tab's destructor run after releasing the lock --
deliberately, to avoid an already-known deadlock hazard (see that
function's own existing comment). But that destructor call could land
on *any* thread holding the last reference, including the tab's own
LLEmbeddedBrowserUpdateThread: a concurrent findTab() (called from that
same thread's own LLEmbeddedBrowser::update(id)) can race the erase()
and end up holding the final shared_ptr. When that thread's own copy
went out of scope, ~LLEmbeddedBrowserTab() ran *on that update thread*,
which then called shutdown() and blocked waiting for itself to stop --
a self-referential deadlock that either hangs or eventually hits
LLThread::shutdown()'s own 60s force-kill fallback, which calls
TerminateThread() on the currently-executing thread. Killing a thread
mid-heap-operation like that leaves the process's C-runtime heap
locked/corrupted, which is why every crash this session showed
different, seemingly-unrelated garbage in memory that should have been
untouched -- it wasn't one consistent overwrite, it was heap-wide
damage from the aftermath of a violent self-kill.

Confirmed via two independent diagnostics before fixing:
- A canary pair on the LLEmbeddedBrowser singleton itself, to rule out
  (and ultimately rule in favor of a different mechanism than) the
  singleton's own memory being corrupted or freed early.
- isInsideRunLoop(), an atomic flag proving a tab's update thread was
  still genuinely inside run()'s loop body at the exact moment its own
  shutdown() call believed it was safe to proceed -- confirmed firing
  in practice, pinpointing this exact mechanism.

Fix: LLEmbeddedBrowserTab::stopUpdateThread() is now called explicitly,
by destroy()/reset(), on the thread that's actually dropping the map's
own reference, before any shared_ptr copy (anyone's) can go out of
scope. This guarantees the update thread has already, genuinely exited
run() by the time any destructor can possibly run, regardless of which
thread ends up holding the last reference -- so the destructor's own
shutdown() call becomes a guaranteed no-op no matter what.

Both diagnostics are left in deliberately for now (not yet fully
soak-tested) rather than removed immediately -- cheap safety nets that
turn any recurrence into a clear signal instead of another blind
investigation.
allocate_slot()'s own per-index retry (10x/10ms) paid that full wait
cost for every stuck index encountered along the way, even when some
other index was actually free right now -- with more than one index
stuck at once, that cost compounds and can itself exceed a caller's
own patience (e.g. the Viewer's disconnect-alert grace period).
Restructured so the outer loop does one fast pass over every free
index per attempt (a stuck one costs nothing beyond moving to the
next), only retrying the whole pass, with a brief wait, if every free
index failed on it.
…aling outages

ProducerDisconnected fired the "Embedded Media has failed" alert
immediately, every time -- but a producer briefly (and wrongly)
concluding a tab's own thread had crashed under heavy system load,
then reconnecting a moment later via the existing ProducerReconnected
path, would still interrupt the user with a failure dialog for
something that was already resolved by the time they saw it. Now waits
a 2s grace period before actually raising the alert; a
ProducerReconnected arriving within that window cancels it entirely,
matching what actually happened from the user's point of view. A
genuine, lasting outage still alerts exactly as before, just slightly
later.
…efault it off

Matches this project's other EmbeddedBrowser* debug settings naming.
Defaulting to off keeps production quiet; still a persisted Debug
Setting, so it can be flipped back on for debugging with no rebuild.
Every connect/disconnect/navigate line now appends "(N/32 active)", so
watching the console doesn't require manually counting colored lines
to see how close the producer is to its concurrent-instance ceiling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nce cap staying exhausted after a priority-based unload

Embedded-browser media never received any volume/mute signal at all:
LLViewerMediaImpl::updateVolume() (distance rolloff, the global media-
volume slider, "mute all but this one") was entirely gated behind
mMediaSource, which is always null for this backend. That's why parcel/
prim media kept playing indefinitely after leaving its region or parcel
-- legacy plugin media went silent instantly (its own updateVolume()
call reacts to the same enormous post-teleport distance), embedded
browser had no equivalent call at all.

CEF only exposes a binary SetAudioMuted()/IsAudioMuted() pair, not a
continuous per-browser volume level, so updateVolume() now collapses
its existing distance/global-volume computation to a mute/unmute
decision for this backend, sent through a new kSetMuted opcode:
LLViewerMediaImpl -> LLEmbeddedBrowser -> SLCefProducer -> llCefBrowserManager
-> CefBrowserHost::SetAudioMuted (llcefbrowser repo, committed separately).

Investigating why media still didn't reliably reappear after a
teleport turned up a second, unrelated bug in tonight's earlier
SLIDESHOW/HIDDEN auto-unload fix: LLViewerMedia::updateMedia()'s
instance-cap accounting (PluginInstancesTotal) counts impls by their
*priority state*, not by whether they still hold a live resource. That
unload fix tore down the CEF tab but left the impl's priority at
SLIDESHOW/HIDDEN rather than UNLOADED, so it kept counting against the
cap forever even with no resource behind it -- once every slot had
ever been touched by "closest N" churn, the cap could never free up
again, blocking new-region media from loading at all. Shared
wouldUnloadEmbeddedBrowserMedia() between setPriority() (tears the
resource down) and updateMedia()'s own counting step fixes this.

Also added an immediate fast-path in removeObject(): once a media
impl's last owning object is gone, don't wait for the generic
priority loop's texture-interest stat (which decays over many seconds,
not instantly) to notice -- destroy the embedded-browser tab right
away.

Finally, fixed a spurious SLCefProducer relaunch found while testing:
LLEmbeddedBrowser::reset() kills the producer before stopping every
tab's update thread, and a thread still alive in that window could
notice the producer is gone and relaunch a brand new one right as the
Viewer exits. Gated maybeRelaunchProducer() on LLApp::isExiting().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e the concurrent-instance headroom

Diagnosed why a lighter region's own media could take 60-90s (or fail
outright) to appear after leaving a media-heavy one, even with last
night's cap-accounting fix in place:

- setPriority()'s SLIDESHOW/HIDDEN auto-unload debounce (added this
  morning) only guarded that one branch. The cap-accounting fix
  excludes an already-pending impl from impl_count_total, which lets
  other candidates fill the cap and can then push that same impl into
  literal PRIORITY_UNLOADED on a later frame -- a separate, unconditional
  branch with no debounce at all, silently bypassing the protection.
  Unified both routes (literal UNLOADED and SLIDESHOW/HIDDEN-active)
  into the same debounced path.

- Even once individual impls stopped being destroyed prematurely, the
  "closest N" sort itself had no memory: two same-interest candidates
  (the common case right after a region change, when a crowd of stale
  not-yet-culled impls from the old region and freshly-noticed impls
  from the new one are mostly tied at zero interest) were treated as
  fully interchangeable, tie-broken only by distance. That let the
  rotation keep reshuffling which ~N candidates counted as loadable
  every time distances shifted slightly, bumping an already-loading
  tab out before its own page ever finished rendering -- a different
  impl winning the slot, not the same one being torn down repeatedly.
  Added stickiness to that tie-break in priorityComparitor(): prefer
  whichever side already holds a live embedded-browser resource.

- Raised PluginInstancesTotal's default from 8 to 12. The test region
  used for this investigation turned out to be a genuine outlier --
  even at a raised cap, 24+ distinct media-bearing candidates were
  still found competing for slots simultaneously, roughly double any
  reasonable cap size. The fixes above make the rotation itself well-
  behaved; this just gives it more headroom on top, comfortably within
  SLCefProducer's own 32-slot ceiling.

Net result, confirmed by hand across several region-transition runs:
media that previously took 60-90+ seconds (or never appeared) now
loads within a few seconds at worst, immediately in the common case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tely on teardown

LLViewerMediaFocus::setFocusFace() only skipped its re-navigate when
hasMedia() was true -- but hasMedia() is deliberately mMediaSource !=
NULL only (see its own comment), which is always false for embedded-
browser media. So clicking off an already-loaded embedded-browser
face and back onto it (the only path that re-enters setFocusFace() at
all -- repeat clicks on an already-focused face are a no-op higher up)
always re-navigated to the same URL from scratch, restarting it
(losing scroll position, replaying audio/video, etc.) where the
legacy plugin backend just resumed. Fixed by using the already-
documented `hasMedia() || isUsingEmbeddedBrowser()` pattern.

Also deduplicated: last night's cap-accounting/sort-stickiness work
added a second, differently-named getter (getUseEmbeddedBrowser()) for
the same mUseEmbeddedBrowser flag isUsingEmbeddedBrowser() already
exposes. Switched those call sites onto the existing getter and
removed the redundant one.

Separately: destroyMediaSource() now mutes the tab immediately, before
asking LLEmbeddedBrowser to actually tear it down. Closing the media
debug floater (or anything else that unloads embedded-browser media)
left audio audibly playing for a few seconds while the real teardown
(closing the CEF browser, then the producer's own slot-release
handling) ran in the background. Muting is one cheap opcode that
takes effect immediately, independent of how long that actual
teardown takes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ture

Covers why the new CEF approach was needed, the four-piece producer/
consumer design, repo locations, the codec-enabled CEF build, cookie
isolation, page preloading, debugging, and remote DevTools.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Calls out the all-or-nothing volume control, the open LibVLC-vs-CEF
codec coverage question, the current Windows-only scope, and that the
legacy media plugin stays in place until this system is closer to
feature-complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds kGoBack/kGoForward/kStopLoad commands and a kEventNavStateChanged
event to the shm protocol, dispatches them through llcefbrowser's
already-existing GoBack/GoForward/StopLoad, and threads canGoBack/
canGoForward state back through llembeddedbrowser into
LLViewerMediaImpl::navigateBack/Forward/Stop and canNavigateBack/Forward.

Also fixes LLMediaCtrl::navigateBack/Forward/Stop, which were bypassing
LLViewerMediaImpl and calling into the legacy plugin pointer directly
(always null for embedded-browser media, so those buttons silently did
nothing). Fixes the same dead buttons on in-world prim media controls
as a side effect, since those already called LLViewerMediaImpl directly.

Confirmed working live: floater back/forward/stop and button enable/
disable state, plus prim media controls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a kSetRenderRate wire command that caps how often SLCefProducer
calls SendExternalBeginFrame() for a given tab, and threads the same
priority value LLViewerMedia::updateMedia() already computes through
LLViewerMediaImpl::setPriority() into a target fps for non-UI, non-
parcel prim media (NORMAL/HIGH unthrottled, LOW 15fps, SLIDESHOW 2fps,
HIDDEN 1fps). This is the embedded-browser equivalent of the legacy
plugin's own setPriority()/setLowPrioritySizeLimit() throttle, which
embedded-browser media never had until now.

UI and parcel media always send a target rate of 0 regardless of what
priority they're assigned, deliberately not keyed off the raw priority
value alone -- the shared priority computation can still push UI media
down to HIDDEN/LOW when the Viewer window is minimized or loses focus,
and priority mis-assignment was suspected as a possible cause of the
legacy plugin's own reputation for feeling slow and clumsy.

Also documents this in doc/Embedded_Browser.md.

Confirmed building clean end-to-end (SLCefProducer, llembeddedbrowser,
secondlife-bin).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds URL/slot/priority to kSetRenderRate's payload so both the Viewer
log (tag PluginPriority) and SLCefProducer's own console (new purple
log_priority(), gated on EmbeddedBrowserProducerConsole like every
other console line, always written to slcefproducer_log.txt
regardless) report every render-rate tier change for QA.

Also fixes a real UX issue found during testing: LLViewerMediaFocus's
auto-zoom moves the camera in on media focus and back out on defocus,
so clicking off a media face demoted it from PRIORITY_HIGH straight to
PRIORITY_LOW purely from the camera position change, not because the
resident actually moved or stopped watching. Demotions (not promotions)
now debounce for EMBEDDED_BROWSER_RENDER_RATE_DEMOTION_GRACE_PERIOD (2s)
before applying, and EMBEDDED_BROWSER_FPS_LOW is raised 15->30 so that
specific, very common transition is far less visible. Both are starting
points for the product team to tune further.

Documents all of this in doc/Embedded_Browser.md.

Confirmed working live: purple producer-console lines and Viewer log
lines appear on tier changes, and defocusing a media face no longer
visibly drops its frame rate unless the demotion actually holds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kRequestSlot now carries the consumer's own current max-dimension
ceiling alongside the existing UI/prim flag. The producer clamps it to
its own absolute maximum (1920x1080, unchanged) and floor (960x540,
its own default browser creation size) and sizes that slot's shared-
memory segment to the result, instead of always reserving the full
1920x1080x3-buffers regardless of what the Viewer will ever request.

Lowering EmbeddedBrowserMaxWidth/Height now genuinely reduces memory
per tab, not just the resolution it's allowed to request -- previously
it only did the latter. Each slot remembers its own negotiated
ceiling so the kResize handler can clamp against it instead of the old
global constant, which matters for correctness: a resize bigger than
what a slot actually reserved would otherwise write past its end.

Documents this, including the 960x540 floor's effect on very low
settings, in doc/Embedded_Browser.md.

Confirmed working live, including a floater resized past a lowered
EmbeddedBrowserMaxWidth/Height stretching the smaller rendered texture
to fill the larger rect, as expected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both repos are public now and their release CI (secondlife/action-
autobuild-release) has produced real, downloadable packages: llcefbrowser
v1.31.0 and llshmframe v1.19.0. Replaces the two file:/// local-override
URLs with the real release URLs and their SHA1 hashes.

Verified end-to-end: autobuild install fetches and unpacks both real
packages cleanly, and SLCefProducer/llembeddedbrowser rebuild against
them with no changes needed elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ENABLE_MEDIA_PLUGINS now defaults off (it already gated the whole
media_plugins subdirectory, just needed flipping). SLPlugin.exe's own
add_subdirectory() is now gated by the same flag: the only caller of
LLPluginProcessParent::create() in the whole codebase is
LLPluginClassMedia::init(), so with no media plugins to host it has
nothing left to do. Source for both is untouched -- this is build/
deploy configuration only, easily reverted by flipping the flag back.

Also stops copying, into the deployed package: media_plugin_cef/
libvlc/example (path_optional(), a no-op once they're not built),
dullahan_host.exe (SLCefProducer.exe re-execs itself instead via
ExecuteSubProcess(), so it never needed this) and the LibVLC runtime
(libvlc.dll/libvlccore.dll/plugins/, 367 files). The latter two ship
inside the CEF/VLC autobuild packages themselves regardless of
ENABLE_MEDIA_PLUGINS, so path_optional() alone doesn't skip them --
had to comment out the copy calls outright, confirmed by testing.

SLCefProducer.exe (and the CEF runtime files it needs, now the sole
consumer of them) moves from the now sometimes-empty llplugin\ into
its own SLCefProducer\ directory, matching the executable's own name.

Confirmed via live build: media_plugin_cef/libvlc/example and
SLPlugin.exe no longer build; llplugin\ no longer exists in the
deployed package when media plugins are off; SLCefProducer\ contains
only SLCefProducer.exe and its actual CEF runtime dependencies, no
VLC or dullahan_host.exe. Deployed package footprint for this one
directory alone: 526MB -> 390MB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…libvlc header

llappviewer.cpp unconditionally includes vlc/libvlc_version.h (guarded
only by !LL_LINUX) to report the LibVLC version in the About-box info
dump -- unrelated to whether media_plugin_libvlc itself gets built.
That header's include path previously only existed as a side effect of
media_plugin_libvlc's own CMakeLists.txt running (include(LibVLCPlugin)),
which the last commit's ENABLE_MEDIA_PLUGINS=OFF default now skips,
breaking the build with a missing-include error on a from-scratch
configure.

Fixed by including LibVLCPlugin and linking ll::libvlc directly for
the Viewer binary target itself, independent of ENABLE_MEDIA_PLUGINS.
Confirmed via a real rebuild from a fresh CMake configure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r\ move

The media-plugin pruning work moved SLCefProducer.exe's deployed
location from llplugin\ to its own SLCefProducer\ directory in
viewer_manifest.py, but missed two runtime call sites that
independently built the old path: LLDir_Win32::getSLCefProducerLauncher()
still pointed at getLLPluginDir(), and LLEmbeddedBrowser::launchProducer()
separately re-derived the same (now wrong) working directory instead of
using the launcher path it already had. Result: SLCefProducer.exe failed
to launch at all, so no media (web or otherwise) rendered.

getSLCefProducerLauncher() now builds its own path directly. The launch
working directory is now derived from that same path (getDirName()),
not a second independent lookup -- so this can't drift out of sync
again the way it just did.

Confirmed fixed by the user on a real clean-build Viewer: embedded
browser media loads normally again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
callumlinden and others added 3 commits August 25, 2026 12:01
Resolved conflicts:
- indra/llmediactrl.cpp: kept our null-safety fix for handleMediaEvent's
  self pointer (always null for embedded-browser media).
- indra/llplugin/CMakeLists.txt: kept both additions (develop's
  precompiled-headers support, our ENABLE_MEDIA_PLUGINS gate around
  add_subdirectory(slplugin)).
- indra/newview/CMakeLists.txt: kept both additions (our libvlc header
  fix, develop's DISABLE_WEBRTC conditional).
- indra/newview/llappviewer.cpp: kept both watchdog-shutdown additions;
  for the About-box info dump, report both the embedded-browser
  (SHMFRAME_VERSION/EMBEDDED_LLCEFBROWSER_VERSION) and legacy Dullahan
  (LIBCEF_VERSION) version blocks, since dullahan_version.h's macros
  are available either way and this stays meaningful if
  ENABLE_MEDIA_PLUGINS is ever turned back on.
- autobuild.xml (21 hunks): mechanical package-list staleness resolved
  by taking develop's side; kept our side on the dullahan/CEF version
  specifically (ours is CEF 150, develop's is still CEF 139 -- taking
  theirs would reintroduce the version mismatch this project already
  solved between the legacy plugin and llcefbrowser/SLCefProducer);
  the 6 self-build command hunks (devenv/MSBuild-style flags ->
  develop's modernized plain `cmake --build`) were resolved by
  replacing our entire windows/linux64 self-build sections with
  develop's verified real content rather than hand-patching, after
  finding stale unconflicted content the raw diff hunks alone would
  have missed. Verified: well-formed XML, and `autobuild install
  --list` parses it cleanly with llcefbrowser/llshmframe/dullahan all
  present.

This is a local sync merge (develop into our branch), not a request to
land embedded-browser in develop -- see PR secondlife#6195's own description for
the actual intent.
- llmediactrl.cpp: mHidingInitialLoad no longer exists -- develop
  removed it (commit bd3a7c9, "Fix uninitialized member variables
  reported by ubsan": it was never initialized in the constructor on
  either side, and used nowhere else in the codebase). My earlier
  conflict resolution kept a reference to it without noticing the
  header declaration was gone. Removed the dead block, matching
  develop's own conclusion that this mechanism should go away.

- llappviewer.cpp: mismatched #if/#endif. develop actually removed
  the !LL_LINUX guard entirely around both dullahan_version.h and
  vlc/libvlc_version.h (both now unconditional there), not just
  moved it -- my earlier resolution only captured part of that change
  and left a dangling #if with no matching #endif. Now matches
  develop's real structure exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…acOS/Linux

A macOS or Linux failure was cancelling the Windows job via GitHub's
default fail-fast matrix behavior (seen firsthand: a macOS Tests
failure on PR secondlife#6195 cancelled the Windows Viewer/Tests jobs before
they ever ran). This project is Windows-only for now -- macOS/Linux
results aren't a current priority, but they shouldn't be able to hide
a real Windows result either way.

Deliberate divergence from develop's own workflow file.

Co-Authored-By: Claude Sonnet 5 <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.

1 participant