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. Works with NoMachine/VNC for remote
# desktop access.
#
# 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 "nvidia" DDX driver
#
# HOW IT WORKS:
# -------------
# Uses the nvidia DDX Xorg driver with ConnectedMonitor + CustomEDID options
# to fake a connected 1920x1080 display on the DP port. This provides:
#   - Full NVIDIA EGL support (nvbufsurface, Argus camera can create EGLImages)
#   - CUDA-EGL interop (cuGraphicsEGLRegisterImage works)
#   - Hardware-accelerated X11/GLX rendering via NVIDIA GPU
#   - Proper DRI2 for remote desktop protocols (NoMachine, VNC, etc.)
#
# JP6-SPECIFIC NOTES:
# -------------------
# nvidia-drm.modeset=1 is NOT compatible with nvidia DDX driver — it causes
# "Failed to acquire modesetting permission". We remove it from boot params.
# The nvidia DDX handles modesetting directly without nvidia-drm KMS.
#
# EGL/CUDA/Argus work fine WITHOUT nvidia-drm.modeset=1 when using nvidia DDX,
# because applications access NVIDIA EGL/CUDA directly via the ICD, not through
# the kernel DRM modesetting path.
#
# 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                                    # 1920x1080 screen
#   DISPLAY=:0 eglinfo 2>&1 | head -15                   # EGL vendor: NVIDIA
#   python3 -c "import ctypes; c=ctypes.CDLL('libcuda.so.1'); print(c.cuInit(0))"  # 0
#   # Connect via NoMachine — should show full GNOME desktop
#
# TO REVERT:
#   sudo cp /etc/X11/xorg.conf.bak.original /etc/X11/xorg.conf  # or rm it
#   sudo rm -f /etc/X11/edid-1080p.bin
#   sudo rm -f /etc/udev/rules.d/99-jetson-drm-seat.rules
#   sudo systemctl enable seatd 2>/dev/null  # if you need seatd back
#   # For JP6: add back nvidia-drm.modeset=1 to /boot/extlinux/extlinux.conf if needed
#   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

# =========================================================================
# COMMON PATH (JP5 + JP6): nvidia DDX driver with ConnectedMonitor + CustomEDID
# =========================================================================

echo "Using nvidia DDX driver with connector ${DP_XORG}"
STEP=0
TOTAL_JP5=3
TOTAL_JP6=5

# ------------------------------------------------------------------
# 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}] Backed up xorg.conf"
else
    echo "[${STEP}] Backup: skipped (already exists or no file)"
fi

# ------------------------------------------------------------------
# Step 2: 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}] Installed EDID binary"

# ------------------------------------------------------------------
# Step 3: xorg.conf (nvidia DDX)
# ------------------------------------------------------------------
STEP=$((STEP+1))
cat > /etc/X11/xorg.conf << XORG_EOF
# Jetson JP${JP_VERSION} - 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}] Wrote xorg.conf (nvidia DDX)"

# ------------------------------------------------------------------
# JP6-SPECIFIC: Additional steps
# ------------------------------------------------------------------
if [[ "$JP_VERSION" -eq 6 ]]; then

    # ------------------------------------------------------------------
    # Step 4: Remove nvidia-drm.modeset=1 from 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
            cp "$EXTLINUX_CONF" "${EXTLINUX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
            sed -i 's| nvidia-drm\.modeset=1||g' "$EXTLINUX_CONF"
            echo "[${STEP}] Removed nvidia-drm.modeset=1 from extlinux.conf"
        else
            echo "[${STEP}] nvidia-drm.modeset=1 not present (OK)"
        fi
    else
        echo "[${STEP}] WARNING: ${EXTLINUX_CONF} not found"
    fi

    # ------------------------------------------------------------------
    # Step 5: Disable seatd if present (optional, helps avoid conflicts)
    # ------------------------------------------------------------------
    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}] Disabled seatd (avoids DRM master conflicts)"
    else
        echo "[${STEP}] seatd not present or already disabled"
    fi

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 "JP6 NOTES:"
    echo "  - nvidia DDX driver active (NOT modesetting)"
    echo "  - nvidia-drm.modeset=1 removed from boot (incompatible with nvidia DDX)"
    echo "  - EGL/CUDA/Argus work via nvidia DDX's own rendering path"
    echo "  - NoMachine/VNC will show full hardware-accelerated desktop"
    echo ""
fi
echo "Connect via NoMachine or VNC to access the desktop remotely."

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.