diff --git a/Dockerfile.sway-helix b/Dockerfile.sway-helix index 4c5bee5d1..c0fd40df9 100644 --- a/Dockerfile.sway-helix +++ b/Dockerfile.sway-helix @@ -56,7 +56,7 @@ RUN apt-get update \ # Install grim, lsof, git, bash, OnlyOffice, Ghostty, and emoji fonts RUN apt-get update && apt-get install -y \ # Screenshot tool for Wayland and debugging utilities - grim lsof \ + grim lsof seatd \ # Clipboard tools for Wayland clipboard sync wl-clipboard \ # Shell and Git for repository access diff --git a/api/pkg/external-agent/wolf_executor.go b/api/pkg/external-agent/wolf_executor.go index 53644abce..1f555ada4 100644 --- a/api/pkg/external-agent/wolf_executor.go +++ b/api/pkg/external-agent/wolf_executor.go @@ -20,6 +20,33 @@ import ( "github.com/helixml/helix/api/pkg/wolf" ) +// detectNVIDIADRMDevice finds the DRM card device for NVIDIA GPU +// Returns the card path (e.g., "/dev/dri/card1") or empty string if not found +func detectNVIDIADRMDevice() string { + // Check /sys/class/drm/card*/device/vendor for NVIDIA vendor ID (0x10de) + for i := 0; i < 10; i++ { + vendorPath := fmt.Sprintf("/sys/class/drm/card%d/device/vendor", i) + + vendor, err := os.ReadFile(vendorPath) + if err != nil { + continue // Card doesn't exist or no vendor file + } + + // NVIDIA vendor ID is 0x10de + if strings.TrimSpace(string(vendor)) == "0x10de" { + cardPath := fmt.Sprintf("/dev/dri/card%d", i) + log.Info(). + Str("nvidia_card", cardPath). + Int("card_index", i). + Msg("Detected NVIDIA DRM device for desktop streaming") + return cardPath + } + } + + log.Warn().Msg("No NVIDIA DRM device found, wlroots will auto-detect GPU") + return "" // Let wlroots auto-detect +} + // lobbyCacheEntry represents a cached lobby lookup result type lobbyCacheEntry struct { lobbyID string @@ -121,8 +148,19 @@ type SwayWolfAppConfig struct { func (w *WolfExecutor) createSwayWolfApp(config SwayWolfAppConfig) *wolf.App { // Build base environment variables (common to all Sway apps) env := []string{ - "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", + "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia* /dev/dma_heap/system", "RUN_SWAY=1", + "WLR_BACKENDS=drm", // Force wlroots to use DRM backend for headless NVIDIA GPU operation + // Note: We run seatd in the container for GPU device access management + } + + // Auto-detect NVIDIA DRM device and set WLR_DRM_DEVICES if found + // This handles systems with multiple GPUs (e.g., Azure VMs with Hyper-V + NVIDIA) + if nvidiaCard := detectNVIDIADRMDevice(); nvidiaCard != "" { + env = append(env, fmt.Sprintf("WLR_DRM_DEVICES=%s", nvidiaCard)) + } + + env = append(env, fmt.Sprintf("ANTHROPIC_API_KEY=%s", os.Getenv("ANTHROPIC_API_KEY")), "ZED_EXTERNAL_SYNC_ENABLED=true", "ZED_HELIX_URL=api:8080", @@ -135,7 +173,7 @@ func (w *WolfExecutor) createSwayWolfApp(config SwayWolfAppConfig) *wolf.App { "HELIX_API_URL=http://api:8080", fmt.Sprintf("HELIX_API_TOKEN=%s", w.helixAPIToken), "SETTINGS_SYNC_PORT=9877", - } + ) // Startup script is executed directly from cloned internal Git repo // No need to pass as environment variable - start-zed-helix.sh will execute from disk @@ -190,7 +228,7 @@ func (w *WolfExecutor) createSwayWolfApp(config SwayWolfAppConfig) *wolf.App { "Privileged": false, "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"], "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], - "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"], + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw", "c 249:* rwm"], "Ulimits": [ { "Name": "nofile", @@ -634,7 +672,7 @@ func (w *WolfExecutor) StartZedAgent(ctx context.Context, agent *types.ZedAgent) RefreshRate: displayRefreshRate, WaylandRenderNode: "/dev/dri/renderD128", RunnerRenderNode: "/dev/dri/renderD128", - VideoProducerBufferCaps: "video/x-raw(memory:CUDAMemory)", // Match Wolf UI's CUDA memory type + VideoProducerBufferCaps: "video/x-raw", // System memory for headless wlroots (CUDA memory doesn't work with headless backend) }, AudioSettings: &wolf.LobbyAudioSettings{ ChannelCount: 2, diff --git a/api/pkg/external-agent/wolf_executor_apps.go b/api/pkg/external-agent/wolf_executor_apps.go index 7242a4038..0b235ccb4 100644 --- a/api/pkg/external-agent/wolf_executor_apps.go +++ b/api/pkg/external-agent/wolf_executor_apps.go @@ -876,8 +876,19 @@ input * { func createSwayWolfAppForAppsMode(config SwayWolfAppConfig, zedImage, helixAPIToken string) *wolf.App { env := []string{ - "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", + "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia* /dev/dma_heap/system", "RUN_SWAY=1", + "WLR_BACKENDS=drm", // Force wlroots to use DRM backend for headless NVIDIA GPU operation + // Note: We run seatd in the container for GPU device access management + } + + // Auto-detect NVIDIA DRM device and set WLR_DRM_DEVICES if found + // This handles systems with multiple GPUs (e.g., Azure VMs with Hyper-V + NVIDIA) + if nvidiaCard := detectNVIDIADRMDevice(); nvidiaCard != "" { + env = append(env, fmt.Sprintf("WLR_DRM_DEVICES=%s", nvidiaCard)) + } + + env = append(env, fmt.Sprintf("ANTHROPIC_API_KEY=%s", os.Getenv("ANTHROPIC_API_KEY")), "ZED_EXTERNAL_SYNC_ENABLED=true", "ZED_HELIX_URL=api:8080", @@ -888,7 +899,7 @@ func createSwayWolfAppForAppsMode(config SwayWolfAppConfig, zedImage, helixAPITo "HELIX_API_URL=http://api:8080", fmt.Sprintf("HELIX_API_TOKEN=%s", helixAPIToken), "SETTINGS_SYNC_PORT=9877", - } + ) env = append(env, config.ExtraEnv...) mounts := []string{ @@ -928,7 +939,7 @@ func createSwayWolfAppForAppsMode(config SwayWolfAppConfig, zedImage, helixAPITo "Privileged": false, "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"], "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], - "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw", "c 249:* rwm"] } }`, config.ContainerHostname) diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 27a2deb38..3e25679bf 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -299,7 +299,8 @@ services: devices: - /dev/dri - /dev/uinput - - /dev/uhid + # /dev/uhid - Optional: uncomment if uhid kernel module is available + # If missing, run: sudo modprobe uhid || echo "uhid not available" ports: # Moonlight protocol ports - "47984:47984" # HTTPS diff --git a/docker-compose.yaml b/docker-compose.yaml index bb10f58b7..530d09937 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -257,10 +257,12 @@ services: - /var/run/wolf:/var/run/wolf:rw device_cgroup_rules: - 'c 13:* rmw' + - 'c 249:* rwm' # DMA heap device for NVIDIA GBM buffer allocation devices: - /dev/dri - /dev/uinput - /dev/uhid + - /dev/dma_heap ports: # Moonlight protocol ports - "47984:47984" # HTTPS diff --git a/install.sh b/install.sh index 296467853..8be424709 100755 --- a/install.sh +++ b/install.sh @@ -942,6 +942,41 @@ EOF fi } +# Function to enable nvidia-drm modeset for NVIDIA GBM/DMA-BUF support +# Required for desktop streaming (Wolf/Sway) with NVIDIA GPUs +setup_nvidia_drm_modeset() { + # Check if NVIDIA GPU is present + if ! command -v nvidia-smi &> /dev/null; then + return 0 # No NVIDIA GPU, skip + fi + + echo "🔧 Checking NVIDIA DRM modeset configuration for desktop streaming..." + + # Check if already enabled + if [ -f /sys/module/nvidia_drm/parameters/modeset ]; then + MODESET=$(cat /sys/module/nvidia_drm/parameters/modeset 2>/dev/null || echo "N") + if [ "$MODESET" = "Y" ]; then + echo "✅ nvidia-drm modeset already enabled" + return 0 + fi + fi + + # Enable nvidia-drm modeset + echo "📝 Enabling nvidia-drm modeset for GBM/DMA-BUF support..." + echo " This is required for NVIDIA desktop streaming (Wolf/Sway)" + echo "options nvidia-drm modeset=1" | sudo tee /etc/modprobe.d/nvidia-drm.conf > /dev/null + + # Update initramfs if available + if command -v update-initramfs &> /dev/null; then + echo " Updating initramfs..." + sudo update-initramfs -u > /dev/null 2>&1 + fi + + echo "⚠️ IMPORTANT: System reboot required for nvidia-drm modeset to take effect" + echo " After reboot, NVIDIA desktop streaming (Helix Code) will be fully functional" + REBOOT_REQUIRED=true +} + # Function to check if Ollama is running on localhost:11434 or Docker bridge IP check_ollama() { # Check localhost with a short read timeout using curl @@ -1241,6 +1276,7 @@ fi if [ "$CODE" = true ] && [ "$RUNNER" = false ]; then if check_nvidia_runtime_needed; then install_nvidia_docker + setup_nvidia_drm_modeset fi fi @@ -1837,6 +1873,7 @@ fi # Install runner if requested or in AUTO mode with GPU if [ "$RUNNER" = true ]; then install_nvidia_docker + setup_nvidia_drm_modeset # Determine runner tag if [ "$LARGE" = true ]; then @@ -1989,3 +2026,19 @@ if [ -n "$API_HOST" ] && [ "$CONTROLPLANE" = true ]; then fi echo -e "\nInstallation complete." + +if [ "$REBOOT_REQUIRED" = true ]; then + echo + echo "┌───────────────────────────────────────────────────────────────────────────┐" + echo "│ ⚠️ SYSTEM REBOOT REQUIRED │" + echo "├───────────────────────────────────────────────────────────────────────────┤" + echo "│ NVIDIA DRM modeset has been enabled for desktop streaming support. │" + echo "│ A system reboot is required for this change to take effect. │" + echo "│ │" + echo "│ After reboot, Helix Code desktop streaming will be fully functional. │" + echo "│ │" + echo "│ Please reboot your system when convenient: │" + echo "│ sudo reboot │" + echo "└───────────────────────────────────────────────────────────────────────────┘" + echo +fi diff --git a/moonlight-web-config/config.json b/moonlight-web-config/config.json index f68a4594a..81e7f7cdb 100644 --- a/moonlight-web-config/config.json +++ b/moonlight-web-config/config.json @@ -1,6 +1,6 @@ { "bind_address": "0.0.0.0:8080", - "credentials": "helix", + "credentials": "eJxU2s5BGnfjmXku", "webrtc_ice_servers": [ { "urls": [ @@ -14,11 +14,11 @@ }, { "urls": [ - "turn:212.82.90.199:3478?transport=udp", - "turn:212.82.90.199:3478?transport=tcp" + "turn:priyaazurecode.helix.ml:3478?transport=udp", + "turn:priyaazurecode.helix.ml:3478?transport=tcp" ], "username": "helix", - "credential": "helix-turn-secret" + "credential": "0GdURv9Jm52tZ8" } ], "webrtc_port_range": { diff --git a/stack b/stack index df12796b8..d736defc3 100755 --- a/stack +++ b/stack @@ -565,7 +565,35 @@ function up() { # Wolf services are optional via --profile wolf flag # To enable Wolf and Moonlight Web: ./stack up --profile wolf - docker compose -f docker-compose.dev.yaml up -d $@ + # Parse arguments to separate profiles from other args + local PROFILES=() + local ARGS=() + local HAS_WOLF_PROFILE=false + + while [[ $# -gt 0 ]]; do + case $1 in + --profile) + if [[ -n "$2" ]]; then + PROFILES+=(--profile "$2") + if [[ "$2" == "wolf" ]]; then + HAS_WOLF_PROFILE=true + fi + shift 2 + else + echo "❌ Error: --profile requires an argument" + return 1 + fi + ;; + *) + ARGS+=("$1") + shift + ;; + esac + done + + + # Run docker compose with profiles before 'up' + docker compose -f docker-compose.dev.yaml "${PROFILES[@]}" up -d "${ARGS[@]}" } function build-zed-agent() { @@ -625,7 +653,58 @@ function zed-agent-logs() { } function rebuild() { - docker compose -f docker-compose.dev.yaml up -d --build $@ + # Parse arguments to check what to rebuild + local REBUILD_WOLF=false + local REBUILD_MOONLIGHT=false + local DOCKER_COMPOSE_ARGS=() + + # If no arguments, rebuild everything + if [[ $# -eq 0 ]]; then + REBUILD_WOLF=true + REBUILD_MOONLIGHT=true + DOCKER_COMPOSE_ARGS=() + else + # Parse arguments + for arg in "$@"; do + case "$arg" in + wolf) + REBUILD_WOLF=true + ;; + moonlight-web) + REBUILD_MOONLIGHT=true + ;; + *) + DOCKER_COMPOSE_ARGS+=("$arg") + ;; + esac + done + fi + + # Build wolf if requested + if [[ "$REBUILD_WOLF" == "true" ]]; then + echo "🐺 Rebuilding wolf..." + rebuild-wolf || { + echo "❌ Failed to build wolf" + return 1 + } + fi + + # Build moonlight-web if requested + if [[ "$REBUILD_MOONLIGHT" == "true" ]]; then + echo "🌙 Rebuilding moonlight-web..." + build-moonlight-web || { + echo "❌ Failed to build moonlight-web" + return 1 + } + fi + + # Rebuild other services with docker compose if specified + if [[ ${#DOCKER_COMPOSE_ARGS[@]} -gt 0 ]]; then + docker compose -f docker-compose.dev.yaml up -d --build "${DOCKER_COMPOSE_ARGS[@]}" + elif [[ $# -eq 0 ]]; then + # No arguments = rebuild everything + docker compose -f docker-compose.dev.yaml up -d --build + fi } # Helper function to build image tags string (commit hash + git tag if available) @@ -685,7 +764,7 @@ function rebuild-wolf() { echo "" echo "Please run:" echo " cd .." - echo " git clone https://github.com/games-on-whales/wolf.git" + echo " git clone https://github.com/helixml/wolf.git" echo " cd helix" echo " ./stack rebuild-wolf" exit 1 @@ -844,7 +923,23 @@ function build-moonlight-web() { echo "Please run:" echo " cd .." echo " git clone https://github.com/helixml/moonlight-web-stream.git" - echo " cd helix" + echo " cd moonlight-web-stream" + echo " git submodule update --init --recursive" + echo " cd ../helix" + echo " ./stack build-moonlight-web" + exit 1 + fi + + # Check if git submodules are initialized + if [ ! -f "../moonlight-web-stream/moonlight-common-sys/moonlight-common-c/src/Limelight.h" ]; then + echo "❌ ERROR: moonlight-web-stream git submodules not initialized" + echo "" + echo "The moonlight-common-c submodule is required for building." + echo "" + echo "Please run:" + echo " cd ../moonlight-web-stream" + echo " git submodule update --init --recursive" + echo " cd ../helix" echo " ./stack build-moonlight-web" exit 1 fi diff --git a/wolf/config.toml.initial b/wolf/config.toml.initial new file mode 100644 index 000000000..649f7d16e --- /dev/null +++ b/wolf/config.toml.initial @@ -0,0 +1,29 @@ +# Wolf configuration for Helix +# +# IMPORTANT: This file intentionally has NO [[profiles]] section +# to prevent static Wolf apps (especially Wolf UI app) from being loaded. +# +# Helix creates dynamic sessions via Wolf's API instead. +# +# Why this matters: +# - Wolf UI app uses video/x-raw(memory:CUDAMemory) buffer caps +# - CUDA memory doesn't work with headless wlroots backend (uses renderD128) +# - Blank screen results when Wolf UI interferes with Helix sessions +# +# The init script at /etc/cont-init.d/05-init-wolf-config.sh will populate +# this file from config.toml.template on first run if empty. +# This minimal config prevents that by being non-empty. + +# A unique identifier for this host (will be auto-generated on first run) +uuid = "" + +# The name that will be displayed in Moonlight +hostname = "Helix ({{HELIX_HOSTNAME}})" + +# The version of this config file +config_version = 6 + +# A list of paired clients that will be allowed to stream +paired_clients = [] + +# NO [[profiles]] SECTION - Helix uses dynamic API-created sessions instead diff --git a/wolf/config.toml.template b/wolf/config.toml.template index ab7f72666..4b18d0331 100644 --- a/wolf/config.toml.template +++ b/wolf/config.toml.template @@ -35,7 +35,7 @@ video_producer_buffer_caps = 'video/x-raw(memory:CUDAMemory)' "IpcMode": "host", "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], "Privileged": false, - "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + "DeviceCgroupRules": ["c 13:* rmw", "c 195:* rmw", "c 226:* rmw", "c 244:* rmw"] } }''' diff --git a/wolf/sway-config/startup-app.sh b/wolf/sway-config/startup-app.sh index 279813236..a2b1a00ce 100755 --- a/wolf/sway-config/startup-app.sh +++ b/wolf/sway-config/startup-app.sh @@ -207,8 +207,43 @@ EOF # echo -n " && killall sway" >> $HOME/.config/sway/config # fi - # Start sway - dbus-run-session -- sway --unsupported-gpu + # Start sway with DRM backend (required for headless NVIDIA GPU operation) + # WLR_BACKENDS=drm forces wlroots to use direct GPU access instead of nested Wayland + + # In headless containers without TTYs, standard seat management doesn't work + # Configure wlroots to run without session management + echo "🔍 Configuring headless GPU access (no session management)..." + + # Ensure retro user has access to DRI and video devices + echo " Adding retro to video and render groups..." + sudo usermod -aG video retro 2>/dev/null || true + sudo usermod -aG render retro 2>/dev/null || true + + # Make DRI devices accessible + echo " Setting DRI device permissions..." + sudo chmod 666 /dev/dri/* 2>/dev/null || true + sudo chmod 666 /dev/nvidia* 2>/dev/null || true + + echo " Available devices:" + ls -l /dev/dri/ 2>/dev/null || echo " /dev/dri not available" + + echo " Current user groups:" + id + + # Configure wlroots for headless operation with renderD128 + # Use headless backend instead of drm - this uses /dev/dri/renderD128 (unprivileged) + # instead of trying to acquire drm master on /dev/dri/card* (which Wolf already has) + export WLR_BACKENDS=headless + export LIBSEAT_BACKEND=noop + export WLR_RENDER_DRM_DEVICE=/dev/dri/renderD128 + + echo " Starting Sway with headless backend (renderD128)..." + echo " WLR_BACKENDS=$WLR_BACKENDS" + echo " WLR_RENDER_DRM_DEVICE=$WLR_RENDER_DRM_DEVICE" + echo " LIBSEAT_BACKEND=$LIBSEAT_BACKEND" + + # Run Sway with headless backend (doesn't need root or drm master) + sway --unsupported-gpu else echo "[exec] Starting: $@" exec $@