feat: switch host from intel VirtualHeads to dummy driver for headless boot

- Add 20-dummy-headless.conf (Driver dummy, 1920x1080)
- Remove 20-intel-virtual.conf (intel driver required HDMI at boot)
- Update install.sh: installs dummy driver, saves EDID, updates GRUB
- Update README: document dummy driver + kernel EDID fallback approach
- hdmi-fallback.sh now detects DUMMY0 or VIRTUAL1

Rationale: intel VirtualHeads only worked when HDMI was connected at boot.
Dummy driver creates DUMMY0 unconditionally, working with or without cable.
Kernel EDID fallback (video=... drm.edid_firmware=...) is insurance if dummy fails.
This commit is contained in:
Deploy Bot
2026-05-15 18:12:05 +01:00
parent a0b2b5ebf7
commit 00c36b8d7e
5 changed files with 273 additions and 131 deletions

148
README.md
View File

@@ -1,6 +1,6 @@
# RDtop — RustDesk Headless Display Setup # RDtop — RustDesk Headless Display Setup
**Автоматический виртуальный дисплей для RustDesk без подключенного HDMI монитора. Работает на Intel iGPU и CPU-only VPS.** **Автоматический виртуальный дисплей для RustDesk без подключенного HDMI монитора. Работает на любой Linux машине (Intel iGPU, AMD, NVIDIA, CPU-only VPS).**
--- ---
@@ -10,8 +10,6 @@
2. [Решение](#решение) 2. [Решение](#решение)
3. [Файлы проекта](#файлы-проекта) 3. [Файлы проекта](#файлы-проекта)
4. [Установка](#установка) 4. [Установка](#установка)
- [Вариант А: Десктоп с Intel iGPU](#вариант-а-десктоп-с-intel-igpu)
- [Вариант Б: CPU-only VPS](#вариант-б-cpu-only-vps)
5. [Как это работает](#как-это-работает) 5. [Как это работает](#как-это-работает)
6. [Проверка](#проверка) 6. [Проверка](#проверка)
7. [Troubleshooting](#troubleshooting) 7. [Troubleshooting](#troubleshooting)
@@ -34,8 +32,8 @@
| Способ | Результат | | Способ | Результат |
|--------|-----------| |--------|-----------|
| `Option "VirtualHeads" "1"` (intel driver) | **Работает!** Но требует `Driver "intel"` + `VirtualHeads` в `xorg.conf`. | | `Option "VirtualHeads" "1"` (intel driver) | **Работает только с кабелем на момент загрузки.** Без HDMI — VIRTUAL1 не создается. |
| modesetting driver | Создает VIRTUAL1 через randr, но после reboot пропадает без `intel` config. | | modesetting driver | Создает VIRTUAL1 через randr, но после reboot пропадает. |
| `NoOutputInitialSize` | Требует пересборки xorg или патчей. | | `NoOutputInitialSize` | Требует пересборки xorg или патчей. |
| Dummy plug (аппаратный) | Работает, но нужно покупать. | | Dummy plug (аппаратный) | Работает, но нужно покупать. |
@@ -43,24 +41,37 @@
## Решение ## Решение
### Хост (Intel iGPU): VIRTUAL1 = primary, HDMI1 = clone ### Хост: Dummy driver + kernel EDID fallback
Работает **без кабеля HDMI на момент загрузки** — dummy driver не зависит от физических выходов.
``` ```
+------------------------------------------------------------------------+ +------------------------------------------------------------------------+
| X11 Screen 0: 1920x1080 | | Xorg Dummy Driver (1920×1080) |
| +-------------------------+ |
| | DUMMY0 = primary | ← RustDesk --server захватывает |
| | 1920×1080 @ 60Hz | |
| +-------------------------+ |
| | | |
| +-------------------------+ +------------------------------------+ | | При подключении HDMI кабеля: |
| | VIRTUAL1 | | HDMI1 | | | скрипт hdmi-fallback.sh клонирует HDMI1 на DUMMY0 |
| | [PRIMARY] 1920×1080 | | [CLONE --same-as VIRTUAL1] | | | При отключении: HDMI1 off, DUMMY0 остается primary |
| | Всегда активен | | При отключении → off | |
| +-------------------------+ +------------------------------------+ |
| |
| RustDesk: всегда захватывает PRIMARY (VIRTUAL1) |
+------------------------------------------------------------------------+ +------------------------------------------------------------------------+
``` ```
### Kernel EDID fallback (дополнительно)
Параметры ядра:
```
video=HDMI-A-1:1920x1080@60 drm.edid_firmware=HDMI-A-1:edid/samsung.bin
```
Это заставляет ядро `i915` инициализировать HDMI-A-1 с сохраненным EDID даже без кабеля. Если dummy driver не сработает (редкий случай), ядро все равно создаст виртуальный framebuffer.
### VPS (CPU-only): Dummy driver + Xorg + RustDesk ### VPS (CPU-only): Dummy driver + Xorg + RustDesk
Тот же dummy driver, но без GPU. Работает на любом VPS.
``` ```
+------------------------------------------------------------------------+ +------------------------------------------------------------------------+
| Xorg Dummy Driver (1920×1080) | | Xorg Dummy Driver (1920×1080) |
@@ -78,10 +89,10 @@
| Файл репозитория | Назначение | Системный путь | | Файл репозитория | Назначение | Системный путь |
|------------------|------------|----------------| |------------------|------------|----------------|
| `bin/hdmi-fallback.sh` | Мониторинг HDMI-A-1 и авто-fallback на VIRTUAL1 | `~/.local/bin/hdmi-fallback.sh` | | `bin/hdmi-fallback.sh` | Мониторинг HDMI-A-1 и авто-fallback на DUMMY0/VIRTUAL1 | `~/.local/bin/hdmi-fallback.sh` |
| `bin/vps-start-xorg.sh` | Стартер Xorg с dummy driver на VPS | `/usr/local/bin/start-xorg-dummy.sh` | | `bin/vps-start-xorg.sh` | Стартер Xorg с dummy driver на VPS | `/usr/local/bin/start-xorg-dummy.sh` |
| `config/x11/90-fallback.conf` | Xorg fallback: Virtual 1920 1080 для modesetting | `/etc/X11/xorg.conf.d/90-fallback.conf` | | `config/x11-host/20-dummy-headless.conf` | Dummy driver headless config | `/etc/X11/xorg.conf.d/20-dummy-headless.conf` |
| `config/x11-host/20-intel-virtual.conf` | Intel driver + VirtualHeads=1 | `/etc/X11/xorg.conf.d/20-intel-virtual.conf` | | `config/x11-host/20-intel-virtual.conf.bak` | Backup старого Intel config (не используется) | — |
| `config/systemd/hdmi-fallback.service` | systemd user unit (host) | `~/.config/systemd/user/hdmi-fallback.service` | | `config/systemd/hdmi-fallback.service` | systemd user unit (host) | `~/.config/systemd/user/hdmi-fallback.service` |
| `config/vps/xorg-dummy.service` | systemd system unit (VPS Xorg) | `/etc/systemd/system/xorg-dummy.service` | | `config/vps/xorg-dummy.service` | systemd system unit (VPS Xorg) | `/etc/systemd/system/xorg-dummy.service` |
| `config/vps/rustdesk-dummy.service` | systemd system unit (VPS RustDesk) | `/etc/systemd/system/rustdesk-dummy.service` | | `config/vps/rustdesk-dummy.service` | systemd system unit (VPS RustDesk) | `/etc/systemd/system/rustdesk-dummy.service` |
@@ -91,15 +102,15 @@
## Установка ## Установка
### Вариант А: Десктоп с Intel iGPU ### Хост (Desktop с GPU)
```bash ```bash
bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)" bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)"
``` ```
После установки **перезагрузить компьютер** или перезапустить GDM: После установки **перезагрузить компьютер**:
```bash ```bash
sudo systemctl restart gdm sudo reboot
``` ```
**Вручную:** **Вручную:**
@@ -107,24 +118,33 @@ sudo systemctl restart gdm
# 1. Клонируем # 1. Клонируем
git clone https://git.softuniq.eu/NW/RDtop.git ~/RDtop && cd ~/RDtop git clone https://git.softuniq.eu/NW/RDtop.git ~/RDtop && cd ~/RDtop
# 2. Intel driver + VirtualHeads # 2. Dummy driver config (главный способ)
sudo cp config/x11-host/20-intel-virtual.conf /etc/X11/xorg.conf.d/ sudo cp config/x11-host/20-dummy-headless.conf /etc/X11/xorg.conf.d/
# 3. Xorg fallback (резерв) # 3. Удалить старые конфиги если были
sudo cp config/x11/90-fallback.conf /etc/X11/xorg.conf.d/ sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf
sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
# 4. Скрипт мониторинга # 4. Сохранить EDID для kernel fallback (опционально)
sudo mkdir -p /lib/firmware/edid
sudo cp /sys/class/drm/card0-HDMI-A-1/edid /lib/firmware/edid/samsung.bin
# 5. Обновить GRUB для kernel EDID fallback
sudo sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=".*"|GRUB_CMDLINE_LINUX_DEFAULT="quiet splash video=HDMI-A-1:1920x1080@60 drm.edid_firmware=HDMI-A-1:edid/samsung.bin"|' /etc/default/grub
sudo update-grub
# 6. Скрипт мониторинга
chmod +x bin/hdmi-fallback.sh chmod +x bin/hdmi-fallback.sh
cp bin/hdmi-fallback.sh ~/.local/bin/ cp bin/hdmi-fallback.sh ~/.local/bin/
# 5. systemd unit # 7. systemd unit
mkdir -p ~/.config/systemd/user mkdir -p ~/.config/systemd/user
cp config/systemd/hdmi-fallback.service ~/.config/systemd/user/ cp config/systemd/hdmi-fallback.service ~/.config/systemd/user/
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now hdmi-fallback.service systemctl --user enable --now hdmi-fallback.service
# 6. Перезагрузка # 8. Перезагрузка
sudo systemctl restart gdm sudo reboot
``` ```
### Вариант Б: CPU-only VPS ### Вариант Б: CPU-only VPS
@@ -159,13 +179,13 @@ xrandr --listmonitors
### Хост (Intel iGPU) ### Хост (Intel iGPU)
1. **Xorg конфиг** `20-intel-virtual.conf` указывает `Driver "intel"` + `Option "VirtualHeads" "1"`. 1. **Dummy driver** (`Driver "dummy"`) создает DUMMY0 сразу при старте Xorg, **независимо от HDMI**.
2. **Intel driver** создает VIRTUAL1 как software output (проверено на Alder Lake-N). 2. **Kernel EDID fallback** (`drm.edid_firmware`) — если dummy каким-то образом не сработает, ядро `i915` инициализирует HDMI-A-1 с сохраненным EDID.
3. **Скрипт** `hdmi-fallback.sh` при старте: 3. **Скрипт** `hdmi-fallback.sh` при старте:
- Всегда активирует VIRTUAL1 как primary - Всегда активирует DUMMY0 как primary
- Если HDMI connected — делает HDMI1 clone (`--same-as`) - Если HDMI connected — делает HDMI1 clone (`--same-as`)
- Если HDMI disconnected — выключает HDMI1, VIRTUAL1 остается primary - Если HDMI disconnected — выключает HDMI1, DUMMY0 остается primary
4. **RustDesk** захватывает VIRTUAL1 (primary), а не HDMI1. 4. **RustDesk** захватывает DUMMY0 (primary).
### VPS (CPU-only) ### VPS (CPU-only)
@@ -178,18 +198,27 @@ xrandr --listmonitors
## Проверка ## Проверка
### Хост ### Хост (после reboot без HDMI)
```bash ```bash
# После перезагрузки # Должно показать DUMMY0 как primary
xrandr --listmonitors xrandr --listmonitors
# Должно быть: # Ожидаемый вывод:
# 0: +*VIRTUAL1 1920x... VIRTUAL1 [PRIMARY] # 0: +*DUMMY0 1920/508x1080/286+0+0 DUMMY0 [PRIMARY]
# 1: +HDMI1 1920x... HDMI1 [CLONE]
# Сервис # Если HDMI подключен:
# 1: +HDMI-1 1920/508x1080/286+0+0 HDMI-1 [CLONE]
```
### Сервис
```bash
systemctl --user status hdmi-fallback.service systemctl --user status hdmi-fallback.service
``` ```
### Логи
```bash
cat ~/.local/logs/drm-hotplug.log
```
### VPS ### VPS
```bash ```bash
export DISPLAY=:0 export DISPLAY=:0
@@ -199,23 +228,25 @@ xrandr --listmonitors
# RustDesk # RustDesk
ps aux | grep rustdesk ps aux | grep rustdesk
# /usr/lib/rustdesk/rustdesk --server должен быть запущен # /usr/lib/rustdesk/rustdesk --server должен быть запущен
``` ```
--- ---
## Troubleshooting ## Troubleshooting
### VIRTUAL1 не появляется после reboot ### DUMMY0 не появляется после reboot
```bash ```bash
# Проверить intel driver: # Проверить dummy driver:
grep "Loading.*intel" /var/log/Xorg.0.log grep "Loading.*dummy" /var/log/Xorg.0.log
# Должно быть: (II) Loading /usr/lib/xorg/modules/drivers/intel_drv.so # Должно быть: (II) Loading /usr/lib/xorg/modules/drivers/dummy_drv.so
# Если modesetting загружается вместо intel: # Проверить конфиг:
# Убедиться что 20-intel-virtual.conf существует: ls /etc/X11/xorg.conf.d/20-dummy-headless.conf
ls /etc/X11/xorg.conf.d/20-intel-virtual.conf # Должен существовать
# Если intel driver загружается вместо dummy:
# Убедиться что нет других конфликтующих xorg.conf: # Убедиться что нет других конфликтующих xorg.conf:
ls /etc/X11/xorg.conf 2>/dev/null || echo "OK: no xorg.conf" ls /etc/X11/xorg.conf 2>/dev/null || echo "OK: no xorg.conf"
``` ```
@@ -225,11 +256,11 @@ ls /etc/X11/xorg.conf 2>/dev/null || echo "OK: no xorg.conf"
```bash ```bash
# Проверить primary: # Проверить primary:
xrandr --verbose | grep primary xrandr --verbose | grep primary
# Должно быть: VIRTUAL1 connected primary (или DUMMY0) # Должно быть: DUMMY0 connected primary
# Если primary = HDMI1 — переключить: # Если primary не DUMMY0:
xrandr --output VIRTUAL1 --mode "1920x1080_60.00" --primary xrandr --output DUMMY0 --mode "1920x1080" --primary
xrandr --output HDMI1 --auto --same-as VIRTUAL1 xrandr --output HDMI-1 --auto --same-as DUMMY0
``` ```
### VPS: RustDesk не видит DUMMY0 ### VPS: RustDesk не видит DUMMY0
@@ -249,11 +280,16 @@ ls /tmp/.X11-unix/X0
```bash ```bash
# Хост # Хост
sudo rm -f /etc/X11/xorg.conf.d/20-dummy-headless.conf
sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf
sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
systemctl --user disable --now hdmi-fallback.service systemctl --user disable --now hdmi-fallback.service
rm -f ~/.local/bin/hdmi-fallback.sh rm -f ~/.local/bin/hdmi-fallback.sh
# Вернуть стандартный GRUB:
sudo sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=".*"|GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"|' /etc/default/grub
sudo update-grub
# VPS # VPS
systemctl disable --now xorg-dummy.service systemctl disable --now xorg-dummy.service
systemctl disable --now rustdesk-dummy.service systemctl disable --now rustdesk-dummy.service
@@ -265,13 +301,17 @@ sudo rm -f /etc/systemd/system/rustdesk-dummy.service
## Технические детали ## Технические детали
### Почему intel driver, а не modesetting? ### Почему dummy driver, а не intel с VirtualHeads?
На Alder Lake-N (46d2) modesetting driver создает VIRTUAL1 только при определенных условиях и не гарантирует его после reboot. Intel driver с `VirtualHeads=1` создает VIRTUAL1 стабильно. `intel` driver + `VirtualHeads=1` работает **только если HDMI подключен при загрузке**. Без кабеля — ядро не создает connector/CRTC для HDMI-A-1, и `VirtualHeads` не помогает. Dummy driver не зависит от физических выходов.
### Почему kernel EDID fallback?
Параметры `video=HDMI-A-1:1920x1080@60` и `drm.edid_firmware=HDMI-A-1:edid/samsung.bin` заставляют ядро `i915` инициализировать connector с фиктивным EDID. Это "страховка": если dummy driver по какой-то причине не загрузится (например, обновление Xorg удалит его), ядро все равно создаст выход.
### Почему клон, а не extended desktop? ### Почему клон, а не extended desktop?
`--same-as` клонирует framebuffer. При disconnect HDMI clone просто перестает рендериться, но primary (VIRTUAL1) не затрагивается — Xorg не теряет framebuffer. `--same-as` клонирует framebuffer. При disconnect HDMI clone просто перестает рендериться, но primary (DUMMY0) не затрагивается — Xorg не теряет framebuffer.
### Dummy driver на VPS ### Dummy driver на VPS

View File

@@ -0,0 +1,23 @@
Section "Device"
Identifier "DummyHeadless"
Driver "dummy"
Option "ConstantDPI" "true"
VideoRam 256000
EndSection
Section "Monitor"
Identifier "DummyMonitor"
HorizSync 28-80
VertRefresh 48-75
EndSection
Section "Screen"
Identifier "DummyScreen"
Device "DummyHeadless"
Monitor "DummyMonitor"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1920x1080" "1280x720"
EndSubSection
EndSection

View File

@@ -1,5 +0,0 @@
Section "Device"
Identifier "Intel Graphics"
Driver "intel"
Option "VirtualHeads" "1"
EndSection

View File

@@ -0,0 +1,9 @@
# BACKUP of original Intel VirtualHeads config
# Disabled in favour of dummy headless config (20-dummy-headless.conf)
# which works without HDMI connected at boot.
#
# Section "Device"
# Identifier "Intel Graphics"
# Driver "intel"
# Option "VirtualHeads" "1"
# EndSection

View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# RDtop Installer — RustDesk headless display setup # RDtop Installer — RustDesk headless display setup (dummy driver)
# Usage: bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)" # Usage: bash -c "$(curl -fsSL https://git.softuniq.eu/NW/RDtop/raw/branch/main/install.sh)"
set -euo pipefail set -euo pipefail
@@ -8,9 +8,10 @@ echo "Installing RustDesk headless display fallback..."
# Config paths # Config paths
REPO_URL="https://git.softuniq.eu/NW/RDtop" REPO_URL="https://git.softuniq.eu/NW/RDtop"
X11_CONF="/etc/X11/xorg.conf.d/90-fallback.conf" X11_CONF="/etc/X11/xorg.conf.d/20-dummy-headless.conf"
BIN_DST="$HOME/.local/bin/hdmi-fallback.sh" BIN_DST="$HOME/.local/bin/hdmi-fallback.sh"
SYSTEMD_DST="$HOME/.config/systemd/user/hdmi-fallback.service" SYSTEMD_DST="$HOME/.config/systemd/user/hdmi-fallback.service"
EDID_DST="/lib/firmware/edid/samsung.bin"
LOG="$HOME/.local/logs/rdtop-install.log" LOG="$HOME/.local/logs/rdtop-install.log"
mkdir -p "$(dirname "$LOG")" mkdir -p "$(dirname "$LOG")"
@@ -24,130 +25,203 @@ if [ "$EUID" -eq 0 ]; then
exit 1 exit 1
fi fi
if ! xset q >/dev/null 2>&1; then # --- Check sudo works ---
log "WARNING: No X11 display found. You need to run this from a graphical session." if ! sudo -n true 2>/dev/null; then
echo "This script requires sudo privileges. Please enter your password when prompted."
fi fi
# --- Check if dummy driver exists or can be installed --- # --- Install dummy driver ---
if [ -f /usr/lib/xorg/modules/drivers/dummy_drv.so ]; then if [ -f /usr/lib/xorg/modules/drivers/dummy_drv.so ]; then
log "Dummy driver already present" log "Dummy driver already present"
else else
log "Dummy driver not found. Will try to install xserver-xorg-video-dummy..." log "Installing xserver-xorg-video-dummy..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq xserver-xorg-video-dummy || log "WARNING: apt failed, dummy not installed (VIRTUAL1 should still work)" sudo apt-get update -qq && sudo apt-get install -y -qq xserver-xorg-video-dummy || true
else else
log "WARNING: apt-get not found, skipping dummy driver install" log "WARNING: apt-get not found, cannot install dummy driver"
exit 1
fi fi
fi fi
# --- Create X11 fallback config --- # --- Remove old conflicting configs ---
log "Creating X11 fallback config at $X11_CONF" log "Removing old Intel X11 configs"
echo "retrowest" | sudo -S bash -c 'cat > /etc/X11/xorg.conf.d/90-fallback.conf' <<'EOF' sudo rm -f /etc/X11/xorg.conf.d/20-intel-virtual.conf
# Fallback Xorg config: guarantee a framebuffer even without HDMI sudo rm -f /etc/X11/xorg.conf.d/90-fallback.conf
# --- Copy EDID firmware ---
log "Saving EDID firmware"
if [ -f /sys/class/drm/card0-HDMI-A-1/edid ]; then
sudo mkdir -p /lib/firmware/edid
sudo cp /sys/class/drm/card0-HDMI-A-1/edid "$EDID_DST"
sudo chmod 644 "$EDID_DST"
log "EDID saved to $EDID_DST"
else
log "WARNING: No EDID found at /sys/class/drm/card0-HDMI-A-1/edid"
fi
# --- Create dummy headless X11 config ---
log "Creating dummy headless X11 config at $X11_CONF"
cat <<'XORG' | sudo tee "$X11_CONF" >/dev/null
Section "Device" Section "Device"
Identifier "AutoDevice" Identifier "DummyHeadless"
Driver "modesetting" Driver "dummy"
Option "ConstantDPI" "true"
VideoRam 256000
EndSection EndSection
Section "Monitor" Section "Monitor"
Identifier "AutoMonitor" Identifier "DummyMonitor"
Option "DPMS" "false" HorizSync 28-80
VertRefresh 48-75
EndSection EndSection
Section "Screen" Section "Screen"
Identifier "AutoScreen" Identifier "DummyScreen"
Device "AutoDevice" Device "DummyHeadless"
Monitor "AutoMonitor" Monitor "DummyMonitor"
DefaultDepth 24 DefaultDepth 24
SubSection "Display" SubSection "Display"
Depth 24 Depth 24
Virtual 1920 1080 Modes "1920x1080" "1280x720"
Modes "1920x1080"
EndSubSection EndSubSection
EndSection EndSection
EOF XORG
sudo chmod 644 "$X11_CONF"
sudo chmod 644 /etc/X11/xorg.conf.d/90-fallback.conf
log "X11 config installed" log "X11 config installed"
# --- Update GRUB for kernel EDID fallback ---
log "Updating GRUB cmdline for EDID fallback"
if [ -f /etc/default/grub ]; then
sudo sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=".*"|GRUB_CMDLINE_LINUX_DEFAULT="quiet splash video=HDMI-A-1:1920x1080@60 drm.edid_firmware=HDMI-A-1:edid/samsung.bin"|' /etc/default/grub
sudo update-grub >/dev/null 2>&1 || log "WARNING: update-grub failed"
log "GRUB updated"
else
log "WARNING: /etc/default/grub not found"
fi
# --- Install fallback script --- # --- Install fallback script ---
log "Installing hdmi-fallback.sh to ~/.local/bin/" log "Installing hdmi-fallback.sh to ~/.local/bin/"
mkdir -p "$HOME/.local/bin" mkdir -p "$HOME/.local/bin"
cat > "$BIN_DST" <<'SCRIPT' cat > "$BIN_DST" <<'SCRIPT'
#!/bin/bash #!/bin/bash
# Universal HDMI Fallback: DUMMY0 (dummy driver) or VIRTUAL1 (intel)
# Works both with HDMI connected and disconnected at boot
LOG="$HOME/.local/logs/drm-hotplug.log" LOG="$HOME/.local/logs/drm-hotplug.log"
mkdir -p "$(dirname "$LOG")" mkdir -p "$HOME/.local/logs"
export DISPLAY=:0 export DISPLAY=:0
log() { log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG" >&1 echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG" >/dev/null
} }
log "=== Starting VIRTUAL1-primary fallback monitor ===" # Find available primary output (DUMMY0 or VIRTUAL1)
find_primary_output() {
MODE="1920x1080_60.00" if xrandr 2>/dev/null | grep -q "^DUMMY0"; then
if ! xrandr 2>/dev/null | grep -q "$MODE"; then echo "DUMMY0"
xrandr --newmode "$MODE" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 2>/dev/null || true elif xrandr 2>/dev/null | grep -q "^VIRTUAL1"; then
fi echo "VIRTUAL1"
xrandr --addmode VIRTUAL1 "$MODE" 2>/dev/null || true else
echo ""
setup_virtual_primary() { fi
log "Setting up VIRTUAL1 as primary..."
xrandr --output VIRTUAL1 --mode "$MODE" --primary 2>&1 && log "VIRTUAL1 primary OK" || log "VIRTUAL1 primary failed"
} }
enable_hdmi_clone() { # Find HDMI output name (HDMI-1, HDMI1, etc)
log "Enabling HDMI1 as clone of VIRTUAL1" find_hdmi_output() {
xrandr --output HDMI1 --auto --same-as VIRTUAL1 2>&1 && log "HDMI1 cloned OK" || log "HDMI1 clone failed" xrandr 2>/dev/null | grep -E "^HDMI-[0-9] connected" | awk '{print $1}' | head -1
} }
disable_hdmi() { # Set up mode for virtual output
log "HDMI1 disconnected, keeping VIRTUAL1 primary" setup_mode() {
xrandr --output HDMI1 --off 2>&1 && log "HDMI1 off OK" || true local OUTPUT="$1"
local MODE_NAME="1920x1080"
if [ "$OUTPUT" = "VIRTUAL1" ]; then
local MODELINE="1920x1080_60.00"
if ! xrandr 2>/dev/null | grep -q "$MODELINE"; then
xrandr --newmode "$MODELINE" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 2>/dev/null || true
fi
xrandr --addmode VIRTUAL1 "$MODELINE" 2>/dev/null || true
MODE_NAME="$MODELINE"
fi
echo "$MODE_NAME"
} }
log "Start monitor (user=$USER, display=$DISPLAY)"
CONNECTOR=""
for p in /sys/class/drm/card1-HDMI-A-1 /sys/class/drm/card0-HDMI-A-1; do for p in /sys/class/drm/card1-HDMI-A-1 /sys/class/drm/card0-HDMI-A-1; do
[ -e "$p/status" ] && { CONNECTOR="$p"; break; } [ -e "$p/status" ] && { CONNECTOR="$p"; break; }
done done
[ -z "$CONNECTOR" ] && CONNECTOR=$(find /sys/class/drm -maxdepth 1 -name "*HDMI-A-1" -type d | head -1) [ -z "$CONNECTOR" ] && CONNECTOR=$(find /sys/class/drm -maxdepth 1 -name "*HDMI-A-1" -type d | head -1)
if [ -z "$CONNECTOR" ] || [ ! -e "$CONNECTOR/status" ]; then if [ -z "$CONNECTOR" ] || [ ! -e "$CONNECTOR/status" ]; then
log "ERROR: No HDMI-A-1 found" log "WARNING: No HDMI-A-1 connector found, running headless only"
setup_virtual_primary fi
# Initial state
LAST_STATE=$(cat "$CONNECTOR/status" 2>/dev/null || echo unknown)
log "Initial HDMI: $LAST_STATE"
# Detect outputs
PRIMARY=$(find_primary_output)
HDMI=$(find_hdmi_output)
if [ -z "$PRIMARY" ]; then
log "ERROR: No virtual output found (DUMMY0 or VIRTUAL1)"
exit 1 exit 1
fi fi
log "Monitoring: $CONNECTOR" log "Primary: $PRIMARY, HDMI: ${HDMI:-none}"
setup_virtual_primary MODE=$(setup_mode "$PRIMARY")
HDMI_STATE=$(cat "$CONNECTOR/status" 2>/dev/null || echo "unknown")
log "Initial HDMI state: $HDMI_STATE"
if [ "$HDMI_STATE" = "connected" ]; then # Initial setup
enable_hdmi_clone if [ "$LAST_STATE" = "connected" ] && [ -n "$HDMI" ]; then
log "HDMI connected, setting clone: $PRIMARY primary + $HDMI same-as"
xrandr --output "$PRIMARY" --mode "$MODE" --primary 2>/dev/null || true
xrandr --output "$HDMI" --auto --same-as "$PRIMARY" 2>/dev/null || true
else else
disable_hdmi log "HDMI disconnected or not found, keeping $PRIMARY primary"
xrandr --output "$PRIMARY" --mode "$MODE" --primary 2>/dev/null || true
[ -n "$HDMI" ] && xrandr --output "$HDMI" --off 2>/dev/null || true
fi fi
log "Initial setup complete. Monitors:" log "Setup complete. Monitors:"
xrandr --listmonitors 2>&1 | while read line; do log " $line"; done xrandr --listmonitors 2>/dev/null | while read -r line; do log " $line"; done
LAST_STATE="$HDMI_STATE"
# Main loop
while true; do while true; do
CURRENT=$(cat "$CONNECTOR/status" 2>/dev/null || echo "unknown") CURRENT=$(cat "$CONNECTOR/status" 2>/dev/null || echo unknown)
if [ "$CURRENT" != "$LAST_STATE" ]; then if [ "$CURRENT" != "$LAST_STATE" ]; then
log "HDMI changed: $LAST_STATE -> $CURRENT" log "HDMI changed: $LAST_STATE -> $CURRENT"
setup_virtual_primary
if [ "$CURRENT" = "connected" ]; then # Re-detect
enable_hdmi_clone PRIMARY=$(find_primary_output)
else HDMI=$(find_hdmi_output)
disable_hdmi MODE=$(setup_mode "$PRIMARY")
if [ -z "$PRIMARY" ]; then
log "ERROR: No virtual output detected after HDMI change"
sleep 5
continue
fi fi
log "Monitor layout after change:"
xrandr --listmonitors 2>&1 | while read line; do log " $line"; done # Always make PRIMARY primary
xrandr --output "$PRIMARY" --mode "$MODE" --primary 2>/dev/null || true
if [ "$CURRENT" = "connected" ] && [ -n "$HDMI" ]; then
log "HDMI connected, cloning $HDMI to $PRIMARY"
xrandr --output "$HDMI" --auto --same-as "$PRIMARY" 2>/dev/null || true
else
log "HDMI disconnected, keeping $PRIMARY primary"
[ -n "$HDMI" ] && xrandr --output "$HDMI" --off 2>/dev/null || true
fi
LAST_STATE="$CURRENT" LAST_STATE="$CURRENT"
fi fi
sleep 2 sleep 2
done done
SCRIPT SCRIPT
@@ -160,7 +234,7 @@ log "Installing systemd user unit"
mkdir -p "$HOME/.config/systemd/user" mkdir -p "$HOME/.config/systemd/user"
cat > "$SYSTEMD_DST" <<'UNIT' cat > "$SYSTEMD_DST" <<'UNIT'
[Unit] [Unit]
Description=HDMI to VIRTUAL1 Fallback Monitor Description=HDMI to Virtual Fallback Monitor
After=graphical-session.target After=graphical-session.target
[Service] [Service]
@@ -175,8 +249,8 @@ WantedBy=graphical-session.target
UNIT UNIT
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now hdmi-fallback.service systemctl --user enable --now hdmi-fallback.service 2>/dev/null || true
log "Systemd unit enabled and started" log "Systemd unit enabled"
# --- Summary --- # --- Summary ---
echo "" echo ""
@@ -185,15 +259,16 @@ echo "RDtop installation complete!"
echo "============================================" echo "============================================"
echo "" echo ""
echo "Next steps:" echo "Next steps:"
echo "1. Reboot or re-login to apply X11 config" echo "1. REBOOT now for changes to take effect:"
echo " sudo systemctl restart gdm" echo " sudo reboot"
echo "" echo ""
echo "2. After reboot, verify VIRTUAL1 is primary:" echo "2. After reboot without HDMI, check:"
echo " xrandr --listmonitors" echo " xrandr --listmonitors"
echo "" echo ""
echo "3. Check service status:" echo "3. Service status:"
echo " systemctl --user status hdmi-fallback.service" echo " systemctl --user status hdmi-fallback.service"
echo "" echo ""
echo "4. To verify: unplug HDMI cable, VIRTUAL1 will stay active" echo "4. View logs:"
echo " cat ~/.local/logs/drm-hotplug.log"
echo "" echo ""
log "Install complete" log "Install complete"