CEF In-World Browsing (Linux-First): Implementation Notes
Summary
This document records the current CEF in-world browsing implementation for space, focused on Linux first, with a path to later Windows/macOS support.
The implementation now supports:
- Downloaded/pinned CEF integration in CMake with no extra user flags for the default flow.
- Production-style browser surfaces that can be mapped to arbitrary scene geometry (including cube faces).
- Input routing from world-space ray hits to CEF mouse events.
- Runtime diagnostics for surface health (paint/upload counters, texture info).
- Deterministic visual smoke validation via snapshots/scripts.
Current build workflow remains exactly:
make cmake
make buildGoals and Constraints
Goals
- Linux-first CEF integration that is stable enough for production hardening.
- In-world browsing that is versatile: any mesh/face can host a web surface.
- Keep build/run simple for developers: no extra CMake flags required in standard flow.
- Structure platform code so Windows/macOS can be added without redesigning the Linux path.
Explicit Non-Goals (for this phase)
- Full multi-platform CEF support (Windows/macOS still TODO).
- E2E snapshot test integrated in
assets/lua/tests/e2e.fnl(not yet added). - Final UX/browser app shell features (tabs/history/devtools UI, etc.).
High-Level Architecture
Build/Packaging Layer
Primary files:
cmake/cef-defaults.cmakecmake/cef.cmakeCMakeLists.txtapps/space/cef_subprocess_main.cpp
Key design points:
- CEF is configured via pinned
SPACE_CEF_VERSION,SPACE_CEF_URL, andSPACE_CEF_SHA256defaults so standardmake cmake/make buildworks without manual values. - Download/extract is handled by CMake with retries and SHA256 verification.
- Platform-dispatch function (
space_setup_cef_for_target) currently routes Linux throughspace_setup_cef_for_target_linux, with explicit TODO stubs for Windows/macOS. - Runtime assets (
libcef.so, pak files, locales, etc.) are copied post-build. - A helper subprocess binary (
space_cef_helper) exists for platform compatibility and future expansion.
Runtime CEF Layer
Primary files:
src/cef_runtime.hsrc/cef_runtime.cppapps/space/main.cpp
Key design points:
cef_runtime::maybe_execute_subprocess(argc, argv)is called very early in process startup to correctly handle CEF subprocess roles.cef_runtime::initialize_browser_process(...)initializes the browser process once.- Linux runtime sets CEF resource/locales/cache paths and Chromium switches for this environment:
no-zygote,no-sandbox,disable-gpu,disable-gpu-compositing,enable-begin-frame-scheduling,disable-gpu-vsync.
- Engine loop calls
cef_runtime::do_message_loop_work()every frame. SPACE_SKIP_CEF=1bypasses CEF startup for utility modules (notably remote-control client processes).
Browser Surface Layer
Primary files:
src/browser_system.hsrc/browser_system.cppsrc/engine.cpp(app.engine.browserbinding)
Key design points:
BrowserSystemmanages named windowless CEF surfaces.- Each surface creates a CEF browser + OpenGL texture (
Texture2D) and updates viaOnPaint→ frame queue →tickupload. - Surface API exposed to Fennel through
app.engine.browser:create-surface,destroy-surface,set-url,set-visible,set-focussend-mouse-move,send-mouse-click,send-mouse-wheeltexture-name,texture-info,surface-stats,list-surfaces
- Diagnostics added for production debugging:
paint-count,upload-count,last-upload-frame,texture-allocated, dimensions, visibility.
In-Scene Integration Layer
Primary files:
assets/lua/browser-cube-surface.fnlassets/lua/mesh-renderer.fnlassets/shaders/mesh.fragassets/lua/main.fnlassets/lua/app-projection.fnl
Key design points:
BrowserCubeSurfacecreates six independent browser surfaces and maps them to cube faces.- Hit testing uses scene ray casting (
scene:screen-pos-ray) and per-face intersection to convert mouse input to browser pixel coordinates. - Mesh batches for browser faces are unlit and force opaque compositing to avoid alpha artifacts (
forceOpaque). - Default browser demo URL is now deterministic local smoke content (
data:URL showingCEF OK) so validation does not depend on external network. - Projection was fixed to use viewport aspect (
app-projection) rather than hardcoded invalid values.
Capture/Verification Layer
Primary files:
scripts/capture_cef_surface.shscripts/capture_cef_visible.shassets/lua/render-capture.fnlsrc/lua_opengl.cpp
Key design points:
capture_cef_surface.shvalidates raw browser texture content viaglGetTexImage.capture_cef_visible.shvalidates in-scene composition and now captures atapp.viewportdimensions.- GL bindings were extended for diagnostics:
GL_FRONT,GL_BACK,glReadBuffer,glGetTexImage. - Render capture path explicitly binds/readbacks from default framebuffer back buffer.
Detailed Flow (End-to-End)
- Build system downloads/extracts pinned CEF archive and links engine/helper targets.
- App starts, early-calls
maybe_execute_subprocess; subprocess roles exit immediately. - Browser process initializes CEF once.
- Fennel creates browser surfaces with IDs, URLs, sizes, and max FPS.
- CEF
OnPaintemits BGRA-like buffer frames into a pending frame queue. - Engine
BrowserSystem::tick(frame_id)uploads pending frames into GL textures. - Scene mesh renderer samples these textures on arbitrary geometry.
- Input router computes ray→face hit and forwards mouse move/click/wheel to the corresponding CEF surface.
Major Challenges Encountered and Resolutions
1) Black captures / apparent no-render
Observed:
- Scene snapshots initially black.
- Direct texture dump showed valid browser content.
Resolution:
- Added diagnostics (
surface-stats,texture-info) to prove paint/upload activity. - Fixed startup viewport initialization path in
main.fnlso projection/render state can initialize correctly in this environment.
2) Distorted/garbled in-scene browser output
Observed:
- Severe stretching/visual corruption in cube output despite valid texture data.
Root cause:
- Default projection matrix used hardcoded invalid values (
fov/aspectnot viewport-derived).
Resolution:
assets/lua/app-projection.fnlnow computes perspective with viewport-derived aspect and sane near/FOV values.
3) Alpha/compositing artifacts for browser faces
Observed:
- Browser face readability degraded with transparency-like blending behavior.
Resolution:
- Added
forceOpaquepath in mesh fragment shader. - Enabled it for browser cube batches.
4) Network-dependent verification instability
Observed:
https://example.comcommonly returned Chromium network error page in this environment.
Resolution:
- Default cube URL switched to deterministic local
data:page (CEF OK). - Capture script supports optional URL override for real-world checks.
5) Sandboxed execution limitations for CEF/Xvfb
Observed:
- AF_UNIX/socket/sandbox operations failed in restricted mode.
Resolution:
- Run visual CEF/Xvfb capture flows outside sandbox when required.
Assumptions
- Linux runtime remains first-class target for CEF in current phase.
- CEF binaries come from pinned URL + SHA256.
- Remote-control endpoint is available when using capture scripts.
- Snapshot/capture verification in CI or constrained dev environments may require Xvfb + unrestricted socket access.
- Default smoke verification should not depend on external internet.
Usage
Build
make cmake
make buildRun browser cube demo
SPACE_DISABLE_AUDIO=1 SPACE_ASSETS_PATH=$(pwd)/assets SPACE_BROWSER_CUBE_DEMO=1 ./build/space -m mainCapture visible in-scene browser output (deterministic)
scripts/capture_cef_visible.shOptional args:
- Output path:
scripts/capture_cef_visible.sh /tmp/my-visible.png - URL override:
scripts/capture_cef_visible.sh /tmp/my-visible.png "https://example.org"
Capture raw browser texture upload
scripts/capture_cef_surface.shOperational Debugging Playbook
When browser output looks wrong:
- Check surface activity:
- Use
app.engine.browser.surface-stats(id)and verify non-zeropaint-countandupload-count.
- Use
- Check texture allocation:
- Use
app.engine.browser.texture-info(id)and verify expected dimensions/channels.
- Use
- Isolate composition vs upload:
- If
capture_cef_surface.shimage is correct but visible scene is wrong, issue is projection/compositing/scene-side.
- If
- Validate viewport/projection:
- Verify
app.viewportdimensions and projection math match runtime window size.
- Verify
- Use deterministic URL first:
- Test with local
data:smoke page before testing external sites.
- Test with local
Known Gaps / Limitations
- No integrated e2e snapshot test module yet for CEF browser cube in
assets/lua/tests/e2e.fnl. - Windows/macOS CEF integration handlers are placeholders.
- URL/network policy for production content loading (proxy/certs/security hardening) is not finalized in this note.
- Browser input coverage is currently mouse-focused; keyboard/text IME details need explicit validation.
Next Steps (Recommended)
- Add e2e snapshot test:
- New module:
assets/lua/tests/e2e/test-browser-cube-cef.fnl. - Register in
assets/lua/tests/e2e.fnl. - Use deterministic
data:URL and assert snapshot stability.
- New module:
- Expand production hardening:
- Explicit lifecycle/teardown tests for many create/destroy cycles.
- Memory/VRAM observation under multiple high-resolution surfaces.
- Input routing stress tests under rapid pointer motion/wheel bursts.
- Platform extension prep:
- Implement
space_setup_cef_for_target_windowsand..._macoswithout changing Lua/browser APIs. - Keep
BrowserSystem/Fennel API unchanged so platform bring-up stays in CMake/runtime glue.
- Implement
- Add docs cross-links:
- Link this note from the CEF build section of Building space and test docs once e2e coverage lands.
File Inventory (Implementation Scope)
Primary touched/added files in this implementation:
cmake/cef-defaults.cmakecmake/cef.cmakeapps/space/main.cppapps/space/cef_subprocess_main.cppsrc/cef_runtime.hsrc/cef_runtime.cppsrc/browser_system.hsrc/browser_system.cppsrc/engine.cppassets/lua/browser-cube-surface.fnlassets/lua/main.fnlassets/lua/app-projection.fnlassets/lua/mesh-renderer.fnlassets/shaders/mesh.fragassets/lua/render-capture.fnlsrc/lua_opengl.cppscripts/capture_cef_surface.shscripts/capture_cef_visible.sh
Current Status
Linux-first CEF in-world browsing is implemented and functionally validated with deterministic smoke capture and texture-level diagnostics. The next high-value milestone is adding a formal e2e snapshot test module so this path is continuously regression-tested.
