Using the ZED SDK with a Virtual Display on NVIDIA® Jetson™

When working with a ZED camera and a ZED SDK-based application running on a remote NVIDIA® Jetson™ device, you may need to access the display output without physically connecting a monitor (headless mode). While a dummy HDMI dongle is a common solution, you can also configure a virtual display entirely through software.

This guide shows how to set up a virtual display on your NVIDIA® Jetson™ device, allowing you to use the ZED camera with remote desktop applications like VNC, NoMachine, or X11 forwarding without requiring a physical dongle.

Prerequisites #

Before starting, ensure you have:

  • Set up your remote NVIDIA® Jetson™ device to run ZED SDK applications.
  • Access to the device physically or remotely via SSH.
  • [Optional] Installed and configured a remote desktop application if you plan to use one.

Virtual Display Setup #

Create the Setup Script #

First, create a script file to configure the virtual display.

You can download a pre-made script from this link or manually create a file named setup-virtual-display.sh with the following content:

Expand to view the content of setup-virtual-display.sh
#!/bin/bash
# =============================================================================
# Jetson Headless Virtual Display Setup (GPU-Accelerated)
# =============================================================================
#
# Creates a virtual 1920x1080@60Hz display on Jetson AGX Orin without a
# physical monitor. EGL, CUDA, and NVIDIA Argus all work. A real monitor
# can still be hot-plugged at any time.
#
# Platform: Jetson AGX Orin
#   - JetPack 5.x (L4T R35.x, Ubuntu 20.04) — Xorg "nvidia" DDX driver
#   - JetPack 6.x (L4T R36.x, Ubuntu 22.04) — Xorg "modesetting" driver
#
# JP6-SPECIFIC ISSUES (solved by this script):
# -----------------------------------------------
# 1. nvidia-drm.modeset=1 is required for EGL-CUDA interop (e.g. ZED SDK's
#    cuGraphicsEGLRegisterImage). But it means the nvidia-drm kernel module
#    OWNS modesetting, so the Xorg "nvidia" DDX driver fails with:
#      (EE) NVIDIA(GPU-0): Failed to acquire modesetting permission.
#    Fix: use the Xorg "modesetting" driver instead of the "nvidia" DDX.
#    Application-level EGL/CUDA/Argus still use the NVIDIA GPU directly —
#    they go through the NVIDIA EGL ICD / libcuda, not through Xorg.
#
# 2. Jetson has two DRM cards: card0 (nvidia-drm, 13800000.display, has DP)
#    and card1 (tegra_drm, host1x, no connectors). Xorg auto-probes both;
#    card1's ScreenInit fails fatally:
#      (EE) AddScreen/ScreenInit failed for gpu driver 0 -1
#    Fix: AutoAddGPU=false in ServerFlags blocks card1 auto-probe.
#
# 3. GDM launches Xorg through systemd-logind, which calls drmSetMaster
#    via TakeDevice. On this nvidia-drm + Tegra combo, logind's
#    drmSetMaster fails: "Device or resource busy". Running X as root
#    directly bypasses this.
#    Fix: a dedicated systemd service (xorg-virtual.service) starts Xorg
#    as root before GDM, then GDM attaches to the existing display.
#
# 4. seatd.service (if installed) competes with systemd-logind for DRM
#    master, causing another "Device or resource busy" failure.
#    Fix: disable seatd.
#
# GPU ACCELERATION:
# -----------------
# The modesetting Xorg driver uses software rendering for X11/GLX (llvmpipe).
# This does NOT affect GPU-accelerated workloads:
#   - EGL:   works via NVIDIA EGL ICD (libEGL_nvidia.so) — verified
#   - CUDA:  works (cuInit returns 0) — verified
#   - Argus: works (GPU initialized by nvidia-drm.modeset=1 at boot)
# X11 desktop compositing will be SW-rendered, which is fine for headless.
#
# REAL MONITOR:
# -------------
# If you plug in a real DP monitor, it appears automatically in xrandr
# and is usable alongside (or instead of) the virtual framebuffer.
#
# USAGE:
#   chmod +x setup-virtual-display-fixed.sh
#   sudo ./setup-virtual-display-fixed.sh
#   sudo reboot
#
# VERIFY:
#   DISPLAY=:0 xrandr
#   DISPLAY=:0 eglinfo | head -5           # Should show NVIDIA
#   python3 -c "import ctypes; cuda = ctypes.CDLL('libcuda.so.1'); \
#     print('cuInit:', cuda.cuInit(0))"     # Should print 0
#
# TO REVERT:
#   sudo systemctl disable --now xorg-virtual.service 2>/dev/null
#   sudo rm -f /etc/systemd/system/xorg-virtual.service
#   sudo rm -f /etc/udev/rules.d/99-jetson-drm-seat.rules
#   sudo cp /etc/X11/xorg.conf.bak.original /etc/X11/xorg.conf  # or rm it
#   sudo rm -f /etc/X11/edid-1080p.bin
#   sudo systemctl enable seatd 2>/dev/null  # if you need seatd back
#   sudo systemctl daemon-reload
#   sudo reboot
#
# =============================================================================

set -euo pipefail

if [[ $EUID -ne 0 ]]; then
    echo "ERROR: Must run as root (sudo)."
    exit 1
fi

# --- Detect JetPack / L4T version ---
detect_jetpack_version() {
    local l4t_major=""
    if [[ -f /etc/nv_tegra_release ]]; then
        l4t_major=$(sed -n 's/^# R\([0-9]*\).*/\1/p' /etc/nv_tegra_release)
    fi
    if [[ -z "$l4t_major" ]] && command -v dpkg-query &>/dev/null; then
        l4t_major=$(dpkg-query -W -f='${Version}' nvidia-l4t-core 2>/dev/null \
                    | sed -n 's/^\([0-9]*\)\..*/\1/p') || true
    fi
    if [[ -n "$l4t_major" && "$l4t_major" -ge 36 ]]; then
        echo 6
    elif [[ -n "$l4t_major" && "$l4t_major" -ge 35 ]]; then
        echo 5
    else
        echo 0
    fi
}

# --- Detect first available DP connector ---
detect_dp_connector_drm() {
    # DRM naming (1-indexed): DP-1, DP-2, etc.
    for card_dp in /sys/class/drm/card*-DP-*; do
        if [[ -d "$card_dp" ]]; then
            basename "$card_dp" | sed 's/card[0-9]*-//'
            return
        fi
    done
    echo "DP-1"
}

detect_dp_connector_xorg() {
    # nvidia DDX uses 0-indexed: DP-0, DP-1, etc.
    local drm_idx
    drm_idx=$(detect_dp_connector_drm | sed 's/.*DP-//')
    echo "DP-$((drm_idx - 1))"
}

JP_VERSION=$(detect_jetpack_version)
DP_DRM=$(detect_dp_connector_drm)
DP_XORG=$(detect_dp_connector_xorg)

echo "=== Jetson Virtual Display Setup ==="

if [[ "$JP_VERSION" -eq 6 ]]; then
    echo "Detected: JetPack 6 (L4T R36.x)"
elif [[ "$JP_VERSION" -eq 5 ]]; then
    echo "Detected: JetPack 5 (L4T R35.x)"
else
    echo "WARNING: Could not detect JetPack version, defaulting to JP5 path"
    JP_VERSION=5
fi

# =========================================================================
# JP5 PATH: nvidia DDX driver with ConnectedMonitor + CustomEDID
# =========================================================================
if [[ "$JP_VERSION" -eq 5 ]]; then

    echo "Using nvidia DDX driver with connector ${DP_XORG}"
    STEP=0; TOTAL=3

    # --- Backup ---
    STEP=$((STEP+1))
    if [[ -f /etc/X11/xorg.conf && ! -f /etc/X11/xorg.conf.bak.original ]]; then
        cp /etc/X11/xorg.conf /etc/X11/xorg.conf.bak.original
        echo "[${STEP}/${TOTAL}] Backed up xorg.conf"
    else
        echo "[${STEP}/${TOTAL}] Backup: skipped (already exists or no file)"
    fi

    # --- EDID binary ---
    STEP=$((STEP+1))
    base64 -d > /etc/X11/edid-1080p.bin << 'EDID_EOF'
AP///////wBZ5QEAAQAAAAEiAQOAAAB4Cu6Ro1RMmSYPUFQhCAABAQEBAQEBAQEBAQEBAQEBAjqA
GHE4LUBYLEUADyghAAAeAAAA/ABWaXJ0dWFsIDEwODBwAAAA/QAySx5REQAKICAgICAgAAAA/wAw
MDAwMDAwMDAwMDAxAJc=
EDID_EOF
    chmod 644 /etc/X11/edid-1080p.bin
    echo "[${STEP}/${TOTAL}] Installed EDID binary"

    # --- xorg.conf ---
    STEP=$((STEP+1))
    cat > /etc/X11/xorg.conf << XORG_EOF
# Jetson JP5 - nvidia DDX driver, virtual display
Section "DRI"
    Mode 0666
EndSection

Section "Module"
    Disable     "dri"
    SubSection  "extmod"
        Option  "omit xfree86-dga"
    EndSubSection
EndSection

Section "Device"
    Identifier  "Tegra0"
    Driver      "nvidia"
    Option      "AllowEmptyInitialConfiguration" "true"
    Option      "ConnectedMonitor"               "${DP_XORG}"
    Option      "CustomEDID"                     "${DP_XORG}:/etc/X11/edid-1080p.bin"
    Option      "HardDPMS"                       "false"
EndSection

Section "Monitor"
    Identifier  "Virtual-1080p"
    HorizSync   30-81
    VertRefresh 50-75
    ModeLine    "1920x1080_60" 148.50 1920 2008 2052 2200 1080 1084 1089 1125 +HSync +VSync
    Option      "PreferredMode" "1920x1080_60"
EndSection

Section "Screen"
    Identifier  "Default Screen"
    Device      "Tegra0"
    Monitor     "Virtual-1080p"
    DefaultDepth 24
    SubSection "Display"
        Depth    24
        Modes    "1920x1080_60"
    EndSubSection
EndSection
XORG_EOF
    echo "[${STEP}/${TOTAL}] Wrote xorg.conf (nvidia DDX)"

# =========================================================================
# JP6 PATH: modesetting driver + root Xorg service
# =========================================================================
else

    echo "Using modesetting driver on card0 with root Xorg service"
    STEP=0; TOTAL=6

    # ------------------------------------------------------------------
    # Step 1: Backup xorg.conf
    # ------------------------------------------------------------------
    STEP=$((STEP+1))
    if [[ -f /etc/X11/xorg.conf && ! -f /etc/X11/xorg.conf.bak.original ]]; then
        cp /etc/X11/xorg.conf /etc/X11/xorg.conf.bak.original
        echo "[${STEP}/${TOTAL}] Backed up xorg.conf"
    else
        echo "[${STEP}/${TOTAL}] Backup: skipped"
    fi

    # ------------------------------------------------------------------
    # Step 2: Write xorg.conf (modesetting driver, card0 only)
    #
    # Key points:
    # • modesetting driver talks to nvidia-drm KMS — no DDX conflict
    # • AutoAddGPU=false blocks card1 (tegra_drm, no connectors)
    # • Virtual 1920 1080 provides framebuffer when headless
    # • Real monitors hot-plug via DP and appear in xrandr
    # ------------------------------------------------------------------
    STEP=$((STEP+1))
    cat > /etc/X11/xorg.conf << 'XORG_EOF'
# Jetson AGX Orin JP6 — modesetting (KMS) driver
#
# nvidia-drm.modeset=1 required for EGL-CUDA interop.
# nvidia DDX cannot coexist → modesetting driver instead.
# AutoAddGPU=false prevents card1 (tegra_drm, no connectors) from
# causing "AddScreen/ScreenInit failed for gpu driver".
# Real monitors hot-plug normally via DP.

Section "ServerFlags"
    Option      "AutoAddGPU"    "false"
EndSection

Section "Device"
    Identifier  "Tegra0"
    Driver      "modesetting"
EndSection

Section "Monitor"
    Identifier  "Virtual-1080p"
    ModeLine    "1920x1080_60" 148.50 1920 2008 2052 2200 1080 1084 1089 1125 +HSync +VSync
    Option      "PreferredMode" "1920x1080_60"
EndSection

Section "Screen"
    Identifier  "Default Screen"
    Device      "Tegra0"
    Monitor     "Virtual-1080p"
    DefaultDepth 24
    SubSection "Display"
        Depth    24
        Virtual  1920 1080
        Modes    "1920x1080_60"
    EndSubSection
EndSection

Section "ServerLayout"
    Identifier  "Layout"
    Screen      "Default Screen"
EndSection
XORG_EOF
    echo "[${STEP}/${TOTAL}] Wrote /etc/X11/xorg.conf"

    # ------------------------------------------------------------------
    # Step 3: Disable seatd (conflicts with logind for DRM master)
    # ------------------------------------------------------------------
    STEP=$((STEP+1))
    if systemctl is-enabled seatd &>/dev/null 2>&1; then
        systemctl stop seatd 2>/dev/null || true
        systemctl disable seatd 2>/dev/null || true
        echo "[${STEP}/${TOTAL}] Disabled seatd (conflicts with logind)"
    else
        echo "[${STEP}/${TOTAL}] seatd not present or already disabled"
    fi

    # ------------------------------------------------------------------
    # Step 4: udev rule — remove card1 (tegra_drm) from seat0
    # ------------------------------------------------------------------
    STEP=$((STEP+1))
    cat > /etc/udev/rules.d/99-jetson-drm-seat.rules << 'UDEV_EOF'
# Remove tegra_drm (card1/host1x) from seat0.
# It has no display connectors and confuses logind DRM master negotiation.
ACTION=="add", SUBSYSTEM=="drm", KERNEL=="card1", TAG-="seat", ENV{ID_SEAT}=""
UDEV_EOF
    udevadm control --reload-rules 2>/dev/null || true
    echo "[${STEP}/${TOTAL}] Installed udev rule to remove card1 from seat0"

    # ------------------------------------------------------------------
    # Step 5: systemd service — start Xorg as root
    #
    # GDM uses logind to open DRM devices, but logind's drmSetMaster
    # fails on JP6's nvidia-drm ("Device or resource busy").
    # Running Xorg as root directly bypasses logind for DRM and works.
    #
    # The service starts before GDM. GDM detects :0 is already running
    # and attaches to it for session management / login.
    # ------------------------------------------------------------------
    STEP=$((STEP+1))
    cat > /etc/systemd/system/xorg-virtual.service << 'SVCEOF'
[Unit]
Description=Xorg Virtual Display (root, modesetting)
After=systemd-logind.service nvidia-persistenced.service
Before=display-manager.service
Wants=systemd-logind.service

[Service]
Type=simple
ExecStartPre=/bin/sleep 2
ExecStart=/usr/lib/xorg/Xorg :0 -config /etc/X11/xorg.conf -noreset vt7 -keeptty -verbose 3
Restart=on-failure
RestartSec=5
# Run as root — required to bypass logind drmSetMaster bug on JP6
User=root

[Install]
WantedBy=graphical.target
SVCEOF
    systemctl daemon-reload
    systemctl enable xorg-virtual.service
    echo "[${STEP}/${TOTAL}] Created and enabled xorg-virtual.service"

    # ------------------------------------------------------------------
    # Step 6: Ensure nvidia-drm.modeset=1 in boot params
    # ------------------------------------------------------------------
    STEP=$((STEP+1))
    EXTLINUX_CONF="/boot/extlinux/extlinux.conf"
    if [[ -f "$EXTLINUX_CONF" ]]; then
        if grep -q "nvidia-drm.modeset=1" "$EXTLINUX_CONF"; then
            echo "[${STEP}/${TOTAL}] nvidia-drm.modeset=1 already in extlinux.conf"
        else
            cp "$EXTLINUX_CONF" "${EXTLINUX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
            sed -i '/^\s*APPEND /s/$/ nvidia-drm.modeset=1/' "$EXTLINUX_CONF"
            echo "[${STEP}/${TOTAL}] Added nvidia-drm.modeset=1 to extlinux.conf"
        fi
    else
        echo "[${STEP}/${TOTAL}] WARNING: ${EXTLINUX_CONF} not found"
        echo "         Manually add 'nvidia-drm.modeset=1' to kernel boot params"
    fi
fi

# --- Common: ensure nvidia-drm loads at boot ---
MODULES_CONF="/etc/modules-load.d/nvidia-drm.conf"
if [[ -f "$MODULES_CONF" ]] && grep -q "nvidia-drm" "$MODULES_CONF"; then
    echo "[+] nvidia-drm already in ${MODULES_CONF}"
else
    echo "nvidia-drm" >> "$MODULES_CONF"
    echo "[+] Added nvidia-drm to ${MODULES_CONF}"
fi

echo ""
echo "=== Done! ==="
echo ""
echo "A reboot is REQUIRED:"
echo "  sudo reboot"
echo ""
echo "After reboot, verify:"
echo "  DISPLAY=:0 xrandr                                    # 1920x1080 screen"
echo "  DISPLAY=:0 eglinfo 2>&1 | head -15                   # EGL vendor: NVIDIA"
echo "  python3 -c \"import ctypes; c=ctypes.CDLL('libcuda.so.1'); print(c.cuInit(0))\"  # 0"
echo ""
if [[ "$JP_VERSION" -eq 6 ]]; then
    echo "NOTE: X runs via xorg-virtual.service (as root, before GDM)."
    echo "      GDM still provides login/session management."
    echo "      Real monitors hot-plug normally via DP."
    echo ""
    echo "      Status:  systemctl status xorg-virtual"
    echo "      Logs:    journalctl -u xorg-virtual"
fi

Make the script executable:

chmod +x setup-virtual-display.sh

Apply the Virtual Display Configuration #

Run the setup script and restart the display manager to apply the changes:

sudo ./setup-virtual-display.sh && sudo systemctl restart gdm

This script configures your system’s X server to create a virtual display. After the display manager restarts, you can connect via remote desktop applications and use the ZED camera as if a physical monitor were connected.

⚠️ Warning: This configuration will disable physical display output on HDMI and DisplayPort connectors. If you need to use a physical monitor directly, revert this configuration first (see below).

Reverting to Physical Display #

To restore physical display functionality (for HDMI or DisplayPort monitors), run:

sudo cp /etc/X11/xorg.conf.bak.original /etc/X11/xorg.conf && sudo systemctl restart gdm

This restores the original X server configuration and restarts the display manager.

How It Works #

The virtual display setup configures the X server to use a virtual framebuffer instead of requiring a physical display connection. This allows the system to:

  • Create a display session without a physical monitor
  • Enable GPU acceleration for graphics applications
  • Allow remote desktop connections to access the display
  • Run GUI applications that require a display environment

This is useful for headless robotic systems where you need to run ZED SDK applications with GUI components or perform development and debugging remotely.

Troubleshooting #

If you encounter issues after setting up the virtual display:

  • Remote desktop not connecting: Ensure your remote desktop server (VNC, NoMachine, etc.) is properly configured and running.
  • Need to switch back to physical display: Use the revert command shown above.
  • Display resolution issues: You may need to modify the script to adjust the virtual display resolution. The current script sets it to 1920x1080@60Hz, which is common for ZED SDK applications.