Initial commit: JCD3198 second HDMI driver setup for Fedora

Enables the MCT T6 USB Station (0711:5601) chip on the J5 Create JCD3198
dock, which drives the 4K30 HDMI port with no native Linux kernel driver.

Stack: EVDI 1.14.16 (kernel module + libevdi) → t6evdi userspace daemon
(mobilepixels-linux-driver, patched for EVDI 1.14 API) → MCT T6 over USB.

Includes DKMS registration for evdi.ko (auto-rebuilds on kernel updates)
and a Mutter D-Bus layout script that restores monitor positions by serial
number after each daemon restart, working around the incrementing DVI-I-N
connector name assigned by EVDI on every reconnect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 18:36:35 -05:00
commit 5dbbb34096
5 changed files with 318 additions and 0 deletions

207
README.md Normal file
View File

@@ -0,0 +1,207 @@
# J5 Create JCD3198 — Second HDMI (4K30) Driver on Linux
## Problem
The JCD3198 dock has two HDMI outputs:
| Port | Chip | Linux support |
|------|------|--------------|
| 4K60 HDMI | DP alt mode via USB-C | Native — works out of the box |
| 4K30 HDMI | MCT T6 USB Station (`0711:5601`) | Requires userspace driver |
The T6 chip has no mainline Linux kernel driver. It presents as USB class `ff-00-00`
(vendor-specific) with no driver bound. A userspace daemon bridges it through the EVDI
virtual display framework.
## System
| Item | Value |
|------|-------|
| Machine | LG Gram 17Z90Q (`jake@gram`) |
| OS | Fedora 43, GNOME Wayland |
| Left monitor | LG FHD — vendor `GSM`, serial `0x01010101` (via T6 / EVDI) |
| Right monitor | Acer SA270 — vendor `ACR`, serial `0x13102591` (via DP alt mode, stable) |
## Architecture
```
GNOME Wayland compositor
evdi.ko — virtual DRM device (card0, connector DVI-I-N)
│ libevdi.so
t6evdi — reads frames from EVDI, encodes JPEG, sends over USB bulk transfer
│ libusb-1.0
MCT T6 chip (USB 0711:5601) → 4K30 HDMI port
```
## Installed Files
| Repo path | Installed to | Purpose |
|-----------|-------------|---------|
| `bin/fix-monitor-layout` | `/usr/local/bin/fix-monitor-layout` | Python script — applies monitor layout via Mutter D-Bus |
| `bin/t6evdi-apply-layout` | `/usr/local/bin/t6evdi-apply-layout` | Shell wrapper — called by systemd after t6evdi starts |
| `systemd/t6evdi.service` | `/etc/systemd/system/t6evdi.service` | Systemd service — starts daemon, auto-restarts on crash |
| `systemd/evdi.conf` | `/etc/modules-load.d/evdi.conf` | Loads evdi.ko at boot |
The `t6evdi` binary itself lives at `/usr/local/bin/t6evdi` (built from source, not in this repo).
## First-Time Build
### 1. Dependencies
```bash
sudo dnf install -y libusb1-devel turbojpeg-devel libdrm-devel git \
bison elfutils-libelf-devel flex openssl-devel python3-dbus
# kernel-devel must match the running kernel exactly.
# If not available via dnf, fetch from Koji:
KVER=$(uname -r)
VER=$(echo $KVER | cut -d- -f1)
REL=$(echo $KVER | cut -d- -f2)
sudo rpm -i "https://kojipkgs.fedoraproject.org/packages/kernel/$VER/$REL/x86_64/kernel-devel-$KVER.rpm"
```
### 2. Build EVDI (kernel module + library)
EVDI 1.14.16 supports kernels through 6.20. The top-level Makefile passes
`-Werror=sign-compare` which trips on Fedora kernel headers — patch it before building.
```bash
git clone --depth=1 https://github.com/DisplayLink/evdi.git
cd evdi
```
In `Makefile`, replace the `module:` recipe with:
```makefile
module:
CFLAGS="-isystem./include -isystem./include/uapi -Wextra -Wall -Wno-sign-compare -Wno-error $(CFLAGS)" $(MAKE) -C module $(MFLAGS)
```
Then build and install:
```bash
make module && make library
sudo make install # installs evdi.ko + libevdi.so
sudo cp library/evdi_lib.h /usr/include/
sudo ldconfig
sudo modprobe evdi
echo "evdi" | sudo tee /etc/modules-load.d/evdi.conf
```
### 3. Register EVDI with DKMS
DKMS rebuilds `evdi.ko` automatically after each kernel update (`AUTOINSTALL=yes`).
```bash
sudo cp -r module /usr/src/evdi-1.14.16
sudo cp module/dkms.conf /usr/src/evdi-1.14.16/
sudo dkms add evdi/1.14.16
sudo dkms build evdi/1.14.16
sudo dkms install evdi/1.14.16
```
### 4. Build the T6 userspace daemon
Source: https://github.com/rfxDarth/mobilepixels-linux-driver
Supports `0711:5601` alongside the MobilePixels devices it was written for.
One API fix required — `evdi_connect` was renamed `evdi_connect2` in EVDI 1.14+:
```bash
git clone --depth=1 https://github.com/rfxDarth/mobilepixels-linux-driver.git
cd mobilepixels-linux-driver/evdi_t6_1
# Edit main.c ~line 1117:
# evdi_connect(..., 0, 0) → evdi_connect2(..., 0, 0)
make
sudo cp T6evdi /usr/local/bin/t6evdi
```
### 5. Install service and layout scripts
```bash
sudo cp bin/fix-monitor-layout bin/t6evdi-apply-layout /usr/local/bin/
sudo chmod +x /usr/local/bin/fix-monitor-layout /usr/local/bin/t6evdi-apply-layout
sudo cp systemd/t6evdi.service /etc/systemd/system/
sudo cp systemd/evdi.conf /etc/modules-load.d/
sudo systemctl daemon-reload
sudo systemctl enable --now t6evdi.service
```
## After a Kernel Update
DKMS rebuilds `evdi.ko` automatically — nothing to do. If it fails:
```bash
sudo dkms build evdi/1.14.16 -k $(uname -r)
sudo dkms install evdi/1.14.16 -k $(uname -r)
# If kernel-devel isn't in repos, fetch it from Koji (see step 1)
```
## How the Monitor Layout Fix Works
**Problem:** The DRM connector name for the EVDI display (`DVI-I-N`) increments every
time t6evdi restarts. GNOME's `monitors.xml` matches saved layouts by connector name,
so every restart lands the LG monitor on the wrong side.
**Solution:** `fix-monitor-layout` calls the Mutter `DisplayConfig` D-Bus API directly,
matching monitors by **serial number** rather than connector name. It calls
`ApplyMonitorsConfig` with `method=2` (persistent), which also rewrites `monitors.xml`
correctly for the current connector.
`t6evdi-apply-layout` is the shell wrapper called by systemd. It sleeps 6 seconds to
give GNOME time to enumerate the new display, then runs `fix-monitor-layout` as `jake`
via `runuser` with the correct session bus address.
Run the fix manually (without restarting the daemon):
```bash
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus /usr/local/bin/fix-monitor-layout
```
## Troubleshooting
**Second monitor black after restart:**
```bash
sudo systemctl restart t6evdi.service
# Layout fix fires automatically ~6 seconds later
```
**Monitors on wrong sides (layout fix didn't run):**
```bash
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus /usr/local/bin/fix-monitor-layout
```
**Check daemon health:**
```bash
systemctl status t6evdi
journalctl -u t6evdi -n 30
sudo dmesg | grep evdi | tail -10
```
Healthy log output looks like:
```
monitor state =1
EDID_ret= 128
edid edid_size = 256
set mode finish
mct_dpms_handler: dpms_mode = 0
```
**EDID_CheckSum error loop:** No monitor connected to the 4K30 HDMI port. Daemon
retries indefinitely — plug in the monitor and it will proceed.
**DKMS status:**
```bash
dkms status evdi
```
## Known Limitations
- Refresh rate capped at 75Hz at 1080p (T6 chip / USB bandwidth constraint)
- UID 1000 is hardcoded in `t6evdi-apply-layout` — update if jake's UID changes
- The 6-second sleep in `t6evdi-apply-layout` is a heuristic; increase it if GNOME is
slow to enumerate the display under load
- Layout fix only runs on service start, not on login. If the display is already
connected but mis-positioned at login, run `fix-monitor-layout` manually
- The t6evdi codebase is immature — do not use `make install` from the mobilepixels repo

85
bin/fix-monitor-layout Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Re-apply dual-monitor layout after t6evdi restarts.
Matches monitors by serial number (not connector name), so it works
regardless of what DRM connector name t6evdi assigns on each restart.
LG FHD (GSM, serial 0x01010101) → left (x=0)
Acer SA270 (ACR, serial 0x13102591) → right (x=1920), primary
"""
import sys
import dbus
BUS = 'org.gnome.Mutter.DisplayConfig'
PATH = '/org/gnome/Mutter/DisplayConfig'
LG_SERIAL = '0x01010101'
ACER_SERIAL = '0x13102591'
def find_mode(modes, width=1920, height=1080, rate=74.973):
"""Return mode ID matching resolution+rate, falling back to resolution only."""
for mode_id, mw, mh, mr, *_ in modes:
if int(mw) == width and int(mh) == height and abs(float(mr) - rate) < 0.5:
return str(mode_id)
for mode_id, mw, mh, *_ in modes:
if int(mw) == width and int(mh) == height:
return str(mode_id)
return None
def main():
try:
bus = dbus.SessionBus()
except dbus.exceptions.DBusException as e:
print(f"ERROR: cannot connect to session bus: {e}", file=sys.stderr)
sys.exit(1)
proxy = bus.get_object(BUS, PATH)
iface = dbus.Interface(proxy, BUS)
serial, monitors, _, _ = iface.GetCurrentState()
connectors = {}
mode_ids = {}
for (connector, vendor, product, mon_serial), modes, props in monitors:
s = str(mon_serial)
if s in (LG_SERIAL, ACER_SERIAL):
connectors[s] = str(connector)
mode_ids[s] = find_mode(modes)
errors = []
for label, key in [('LG FHD', LG_SERIAL), ('Acer SA270', ACER_SERIAL)]:
if key not in connectors:
errors.append(f"{label} not detected")
elif mode_ids[key] is None:
errors.append(f"no 1920x1080 mode on {label}")
if errors:
print(f"ERROR: {'; '.join(errors)}", file=sys.stderr)
sys.exit(1)
logical = [
(0, 0, 1.0, 0, False,
[(connectors[LG_SERIAL], mode_ids[LG_SERIAL], {})]),
(1920, 0, 1.0, 0, True,
[(connectors[ACER_SERIAL], mode_ids[ACER_SERIAL], {})]),
]
iface.ApplyMonitorsConfig(
dbus.UInt32(serial),
dbus.UInt32(2), # 2 = persistent
logical,
{},
signature='uua(iiduba(ssa{sv}))a{sv}'
)
print(
f"Applied: {connectors[LG_SERIAL]} (LG FHD) at x=0, "
f"{connectors[ACER_SERIAL]} (Acer SA270) at x=1920 [primary]"
)
if __name__ == '__main__':
main()

12
bin/t6evdi-apply-layout Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Called by t6evdi.service after startup.
# Waits for GNOME to enumerate the new display, then uses the Mutter
# DisplayConfig DBus API to apply the correct monitor positions by serial
# number — independent of whatever connector name t6evdi assigns.
sleep 6
exec runuser -u jake -- env \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
XDG_RUNTIME_DIR=/run/user/1000 \
/usr/local/bin/fix-monitor-layout

1
systemd/evdi.conf Normal file
View File

@@ -0,0 +1 @@
evdi

13
systemd/t6evdi.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=MCT T6 USB display daemon (JCD3198 second HDMI)
After=systemd-udev-settle.service
[Service]
Type=simple
ExecStart=/usr/local/bin/t6evdi
ExecStartPost=/usr/local/bin/t6evdi-apply-layout
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target