Skip to content

docs: design & work plan for native virtual display on Linux - #3

Merged
unjordi merged 1 commit into
developfrom
docs/vdisplay-linux-plan
Jun 28, 2026
Merged

unjordi merged 1 commit into
developfrom
docs/vdisplay-linux-plan

Conversation

@unjordi

@unjordi unjordi commented Jun 28, 2026

Copy link
Copy Markdown
Owner

The detailed engineering plan for Helios' flagship feature: a native virtual display on Linux/Wayland that matches the client's resolution/refresh/HDR and tears down on disconnect — parity with the Windows SudoVDA path.

Grounded in research, not hand-waving:

  • Approach = adopt & finish Apollo PR #1477: EDID-override via debugfs on a disconnected DRM connector, captured by the existing kmsgrab (no kernel module, no evdi). Plus a pluggable pre_probe_cmd fallback (Sunshine #4762) for headless / no-free-connector cases.
  • Architecture map with exact integration points: misc.cpp:955 capture selector, the process.cpp:236 #ifdef _WIN32 bifurcation, the nvhttp.cpp capability flag (why Selene says "not supported"), libdisplaydevice returning nullptr on Linux.
  • Phased plan: Phase 0 reproduce Add experimental virtual display support for Linux ClassicOldSong/Apollo#1477 on our rig + settle the wlr-vs-KMS capture question → Phase 1 client capability parity → Phase 2 lifecycle + HDR → Phase 3 pluggable fallback → Phase 4 monitor orchestration.
  • Risk register (NVIDIA, X11, OpenGL OpenGL games cannot launch when only virtual display is connected ClassicOldSong/Apollo#1414, no-free-connector, 240Hz+HDR flicker), testing matrix, and open questions.

Doc: docs/design/virtual-display-linux.md.

🤖 Generated with Claude Code

The flagship feature of this fork. Grounds the plan in (a) the actual
approach of Apollo PR ClassicOldSong#1477 (EDID-override via debugfs on a disconnected
DRM connector, captured by kmsgrab — no kernel module, no evdi) and (b) a
map of Apollo's Linux capture/display code and the exact integration
points (misc.cpp display() selector, the process.cpp #ifdef _WIN32
bifurcation, the nvhttp.cpp capability flag, libdisplaydevice nullptr on
Linux).

Defines the chosen approach (adopt/finish ClassicOldSong#1477 + a pluggable
pre_probe_cmd fallback per Sunshine #4762 for headless/no-free-connector
cases), a phased work plan (Phase 0 reproduce & settle the wlr-vs-KMS
capture question → client capability parity → lifecycle+HDR hardening →
pluggable fallback → monitor orchestration), a risk register, a testing
matrix, and open questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unjordi
unjordi merged commit a420b12 into develop Jun 28, 2026
@unjordi
unjordi deleted the docs/vdisplay-linux-plan branch June 28, 2026 21:00
unjordi added a commit that referenced this pull request Jul 28, 2026
`proc_t::terminate()` es alcanzable desde al menos CUATRO hilos sin ninguna
sincronizacion, y recorre `_app_prep_it`, que es estado MIEMBRO:

  - hilo del system tray          (system_tray.cpp:98,137,144,456)
  - hilo HTTPS de la config web   (confighttp.cpp:894,1184,1235)
  - hilo HTTPS de nvhttp, via el endpoint /cancel -> rtsp_stream::terminate_sessions()
  - la ruta de teardown de sesion (process.cpp)

Escenario que se dispara solo: al cerrar el stream, el cliente manda /cancel por
HTTP mientras la ruta de desconexion ya esta terminando la app. Los dos
`terminate()` recorren el mismo cursor -> los undo prep-cmds se ejecutan DOS
VECES y el iterador se pasa de `_app_prep_begin` -> SIGSEGV.

No es una carrera estrecha: el `child.wait()` del bucle es bloqueante, asi que la
ventana dura segundos.

Capturado en vivo (2026-07-28), hilo `nvhttp::47984`:

    #0  proc::proc_t::terminate(bool, bool)        <-- SIGSEGV (SEGV_MAPERR)
    #1  stream::session::join(session_t&)
    #2  rtsp_stream::terminate_sessions()
    #3  nvhttp::cancel(Response, Request)
    ...
    #13 std::thread ... nvhttp::start()

con el sintoma delator en el log: "Executing Undo Cmd" DOS veces, 0.8 s aparte.
Tres crashes en dos dias en el mismo host (2 x SIGSEGV + 1 x SIGTRAP).

Consecuencia grave: cuando el daemon muere a media terminacion, los undo que
faltaban NO corren. En un host con un prep-cmd que bloquea la sesion al
desconectar, eso deja la MAQUINA DESBLOQUEADA.

Cambios:

1. `std::recursive_mutex` compartido, tomado en execute(), pause(), terminate(),
   launch_input_only() y refresh(). Recursivo porque pause() reentra por
   terminate() cuando terminate_on_pause esta activo.
   Es `static inline` a proposito: `proc_t` se move-asigna al recargar apps.json
   (`proc = std::move(*proc_opt)`), y un mutex miembro eliminaria el move
   assignment defaulted. Ademas asi el lock sobrevive esa recarga, que es
   justamente la otra mitad de la carrera.

2. Guarda de idempotencia `_terminating`: una segunda llamada concurrente sale
   sin repetir los undo, en vez de duplicarlos.

3. El bucle de undo recorre un cursor LOCAL y retira el compartido de entrada, de
   modo que ninguna otra llamada pueda observar un iterador a medio consumir.

4. Timeout por comando (10 s) en el bucle de undo. Antes, un undo colgado impedia
   que corrieran los siguientes y disparaba el watchdog de sesion ("Hang
   detected!"), que mata el daemon — el SIGTRAP de la lista de arriba. Se usa un
   bucle de sondeo porque `child::wait_for()` esta roto/deprecado en
   Boost.Process v1 (ya documentado en `terminate_process_group`).

Los puntos 1-3 atacan el SIGSEGV; el 4 ataca el SIGTRAP. Son el mismo bucle.

Build verde con gcc-14, Release, cero warnings.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant