hotswap-extended-state.md 6.3 KB

Task: extend hotswap to cover layers, volume, and other live settings

Context

--list-objects / --disable-object / --enable-object were recently added (see ApplicationContext.cpp), and the we_manager Electron app now has a UI for toggling layers and adjusting per-wallpaper volume live from a sidebar.

Because those settings are only read from argv at process startup (ApplicationContext::loadSettingsFromArgv), the app currently applies any change by killing the running process and spawning a brand new one with the updated flags. That works, but it's the wrong tool for the job:

  • Full CEF/GL/DBus teardown and reinit on every toggle, visible as a flash.
  • A real bug we hit and fixed on the app side: stopLwe() sends SIGTERM and moves on without waiting for the old process to actually exit. If it's still shutting down when the new one starts, both processes are briefly alive at once (we saw DBus service org.linuxwallpaperengine.WaylandDetector is already owned by another process in the logs from exactly this). The app now guards against the worst symptom (a corrupted activeProcess reference that made the new launch hang forever - see we_manager/src/main/services/lwe.service.ts, the cleanup closure in launchLweAsync), but the underlying process-overlap window is still there and still wasteful.
  • Every toggle costs ~2-3 seconds end to end for something that should be near-instant.

The hotswap mechanism (SIGUSR1 + control file) already exists for switching which background plays, without a process restart. This task is to extend it to carry more than just a path, so layers/volume/etc. can be updated the same lightweight way.

Current hotswap mechanism

  • WallpaperApplication::checkHotswapRequest() in src/WallpaperEngine/Application/WallpaperApplication.cpp (~line 506).
  • Reads a single line (the new background path) from $XDG_RUNTIME_DIR/lwe-control (or /tmp/lwe-control), triggered by SIGUSR1 (m_hotswapRequested flag, set in WallpaperApplication::signal).
  • Fully reloads the project (loadBackground), re-runs setupPropertiesForProject, and reconstructs a CWallpaper per screen via CWallpaper::fromWallpaper(...).
  • On the app side, this is driven by hotReloadLwe() in we_manager/src/main/services/lwe.service.ts, which just writes the path and sends the signal.

Why this should be less work than it sounds

Both target settings already re-evaluate fresh on every reload, they're just not reachable from the control-file path yet:

  • Layers: ApplicationContext::resolveObjectVisibility(id, name) (ApplicationContext.cpp:261) reads straight from settings.general.disabledObjects / enabledObjects, and is called during object construction (CImage.cpp:865,929, CText.cpp:592, CParticle.cpp:178, CScene.cpp:366) - which already runs again on every hotswap reload. If the control-file handler updates those two vectors on m_context.settings.general before calling loadBackground, visibility should just work without touching the object-construction code at all.

  • Volume: GLPlayer::setVolume(double) (VideoPlayback/MPV/GLPlayer.cpp:102) already does a live mpv_set_property(..., "volume", ...) on the existing mpv handle - no reload needed at all for video wallpapers, it just needs a path from the control file to this call. Scene/particle audio (Render/Objects/CSound.h) wasn't checked in detail; confirm whether it has an equivalent live setter or needs the same "re-apply on reload" treatment as layers.

Proposed design

  1. Replace the single-line control file with a small line-based or JSON format so it can carry multiple fields without breaking existing readers, e.g.:

    path=/path/to/wallpaper
    disable-object=13
    disable-object=110
    enable-object=7
    volume=42
    

    or a JSON object with the same fields. Either is fine; pick whichever is less code given checkHotswapRequest is currently a simple std::getline.

  2. In checkHotswapRequest, before calling loadBackground:

    • If object fields are present, replace m_context.settings.general.disabledObjects / enabledObjects with the new lists.
    • If a volume field is present and the current wallpaper is a video, route it to GLPlayer::setVolume directly (no reload needed - this can probably be handled as its own signal path, separate from a full background reload, since it doesn't need loadBackground at all).
  3. Path stays optional in the payload: a volume-only or layers-only update shouldn't require re-sending the current path and doing a full reload when nothing about the project changed. Worth splitting into two distinct requests internally: "reload background" (path/layers, since layers currently only take effect through object construction) vs. "live property push" (volume, and anything else with a direct setter).

Acceptance criteria

  • Toggling a layer on a running wallpaper no longer restarts the process (no new PID, no DBus/CEF re-init log lines).
  • Changing volume on a running video wallpaper is audible within one mpv property update, no visual interruption.
  • Existing plain path-only hotswap (playlist advance, "Play wallpaper" on a different item) keeps working unchanged.
  • Rapid repeated toggles (stress case: 10 layer toggles in under 2 seconds) don't crash, hang, or leak processes.

Follow-up work in we_manager (once this lands)

  • src/main/services/lwe.service.ts: extend getControlFilePath/ hotReloadLwe to write the richer payload, and add a variant that doesn't require isLweRunning() to be false (currently only used as a fallback inside launchLweAsync).
  • src/renderer/src/components/common/DetailSidebar.tsx: toggleObject and commitVolume currently call lwe.stop() + wallpaper.apply() when isActive is true - swap that for a new lighter IPC call (e.g. lwe.hotswapSettings(...)) once the above exists, dropping the ~2-3s relaunch wait and the "live" restart flash entirely.

Non-goals

  • fps: changing FPS live is a bigger change (render loop timing), not covered here - keep it on the restart path unless someone wants to take that on separately.
  • Web wallpapers: CEF browser reload semantics are different enough from scene/video that they're out of scope for this task.