Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5db00d1
Fix Sway compositor startup on NVIDIA GPUs by forcing DRM backend
chocobar Nov 19, 2025
b4db0a1
Add DMA heap device support for NVIDIA GBM buffer allocation
chocobar Nov 19, 2025
e5a99ea
Add DMA heap device support to Sway/Zed containers
chocobar Nov 19, 2025
33c925d
Add WLR_BACKENDS=drm to Zed container environment
chocobar Nov 19, 2025
7092245
Fix NVIDIA GPU support in correct function (createSwayWolfApp)
chocobar Nov 19, 2025
f71bdd0
Add nvidia-drm modeset setup for NVIDIA desktop streaming
chocobar Nov 19, 2025
b4a7047
Fix DMA heap device path for Wolf container mounting
chocobar Nov 19, 2025
84269c7
Add LIBSEAT_BACKEND=noop to disable session management in headless co…
chocobar Nov 19, 2025
ec651fa
Add WLR_DRM_DEVICES to force wlroots to use NVIDIA GPU on Azure
chocobar Nov 19, 2025
4fa8758
Replace hardcoded card1 with dynamic NVIDIA GPU detection
chocobar Nov 19, 2025
3992bbb
Changes proposed by Gemini-3
chocobar Nov 20, 2025
bdbcd44
Clone the right repo
chocobar Nov 20, 2025
f8bae2c
Fix stack up --profile argument parsing
chocobar Nov 20, 2025
20f17a7
Fix array expansion in stack up --profile parsing
chocobar Nov 20, 2025
0f251c8
Auto-build moonlight-web when using --profile wolf
chocobar Nov 20, 2025
b07ba87
Add git submodule check to build-moonlight-web
chocobar Nov 20, 2025
ee544f3
Make ./stack rebuild build wolf and moonlight-web
chocobar Nov 20, 2025
992dc11
Simplify ./stack rebuild to always build wolf components
chocobar Nov 20, 2025
8de96d1
Fix ./stack rebuild to support both wolf and specific services
chocobar Nov 20, 2025
f765b98
Make ./stack rebuild work with explicit parameters
chocobar Nov 20, 2025
176715e
Fix moonlight-web submodule path check
chocobar Nov 20, 2025
352b455
Make /dev/uhid optional for wolf container
chocobar Nov 20, 2025
e442ee2
Fix seatd startup in headless mode with LIBSEAT_BACKEND=noop
chocobar Nov 20, 2025
2df8265
WIP: Debug Wolf/Sway GPU access in headless containers
chocobar Nov 20, 2025
50a19ec
WIP: Sway GPU access still failing - extensive debugging
chocobar Nov 20, 2025
313a73a
Fix Wolf Sway compositor GPU access using headless backend
chocobar Nov 20, 2025
be5601e
Fix Wolf video capture for headless Sway backend
chocobar Nov 20, 2025
faad44f
Disable Wolf UI app to prevent CUDA memory interference with headless…
chocobar Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile.sway-helix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 42 additions & 4 deletions api/pkg/external-agent/wolf_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 14 additions & 3 deletions api/pkg/external-agent/wolf_executor_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion docker-compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: /dev/dma_heap missing from here

ports:
# Moonlight protocol ports
- "47984:47984" # HTTPS
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions moonlight-web-config/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"bind_address": "0.0.0.0:8080",
"credentials": "helix",
"credentials": "eJxU2s5BGnfjmXku",
"webrtc_ice_servers": [
{
"urls": [
Expand All @@ -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": {
Expand Down
103 changes: 99 additions & 4 deletions stack
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading