This project turns a USB-connected Daylight DC-1 into a genuine additional Hyprland output. Hyprland owns a separate headless monitor and workspace; its pixels are sent losslessly to the DC-1 over an ADB USB tunnel. It is not a VNC desktop and does not mirror an existing monitor.
For low latency, the native sender captures the headless output directly through ext-image-copy-capture-v1. It converts only compositor-reported damaged regions to grayscale and retains only the newest image generation, so receiver backpressure drops stale frames instead of accumulating display lag. There is no intermediate process, FIFO, recorder, video encoder, or container.
sender/ Native Linux/Wayland capture and transport
receiver/ Android application and native renderer
bin/ Display lifecycle and receiver-install helpers
tests/ Sender protocol integration tests
Makefile Root build entry point for both components
The sender and receiver deliberately live in one repository because changes to the transport protocol generally require coordinated updates on both sides.
- Linux with systemd user services and Hyprland
- A compositor implementing
ext-image-copy-capture-v1(the current backend is tested on Hyprland) - A Daylight DC-1 with USB debugging enabled
- USB data cable; no Wi-Fi connection is required
- x86-64 host for the prebuilt bundle, or any Linux architecture capable of building the sender from source
Run the dependency check before building:
bin/daylight-doctorIt checks host commands and libraries, the pinned Android SDK components, Java, and optional device connectivity.
On Arch Linux/Omarchy, install the equivalent of:
sudo pacman -S --needed base-devel cmake jdk21-openjdk jq lz4 pkgconf wayland wayland-protocols android-toolsHyprland and systemd are normally already present on Omarchy. On Debian/Ubuntu, the package names are typically:
sudo apt install build-essential cmake openjdk-21-jdk jq adb liblz4-dev libwayland-dev wayland-protocols pkg-configThe distribution must provide recent staging Wayland protocol XML files under /usr/share/wayland-protocols/staging/. Package names vary, so use bin/daylight-doctor as the authoritative check.
Install Android Studio or Google's command-line tools, set ANDROID_SDK_ROOT, and install these pinned components:
export ANDROID_SDK_ROOT=/path/to/Android/Sdk
export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH"
sdkmanager \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0" \
"ndk;26.3.11579264" \
"cmake;3.22.1"Set JAVA_HOME to JDK 17 or newer; JDK 21 is the tested choice. The Gradle wrapper is included, so a system Gradle installation is unnecessary.
Build the Linux sender:
make sender
make testBuild the Android receiver:
export ANDROID_HOME=/path/to/Android/Sdk
export JAVA_HOME=/path/to/jdk-21
make receiverThe Android SDK must provide API 34, Build Tools 34.0.0, NDK 26.3.11579264, and CMake 3.22.1. Three variants are available:
| Target | Purpose | APK |
|---|---|---|
make receiver |
Debugging native and Kotlin code | receiver/app/build/outputs/apk/debug/app-debug.apk |
make receiver-profile |
Optimized native code with Android profiling enabled | receiver/app/build/outputs/apk/profile/app-profile.apk |
make receiver-release |
Optimized, non-debuggable daily-use build | receiver/app/build/outputs/apk/release/app-release.apk |
Without signing environment variables, release/profile APKs use Android's local debug key so variants can replace one another during development. Public releases should use a persistent private signing key; see “Release artifacts.”
Build both components with make. Build outputs are ignored by Git and can be removed with make clean.
Verify downloads and extract the host bundle:
sha256sum -c SHA256SUMS
tar -xzf daylight-display-0.1.0-linux-x86_64.tar.gz
cd daylight-display-0.1.0
bin/daylight-doctor --runtime
bin/install-receiver release
bin/daylight-display startThe host bundle contains the portable sender binary and receiver APK; it does not require the Android SDK or compiler. Runtime dependencies such as ADB, Hyprland, liblz4, Wayland, jq, and systemd are still required.
Enable USB debugging on the DC-1, authorize the Linux host, and confirm that adb devices -l reports the tablet in the device state. Then install the locally built receiver:
make install-receiver-release
# or, after building/downloading an APK:
bin/install-receiver releasebin/daylight-display install-receiver also installs release by default. Pass debug or profile to either helper to select another already-built variant.
Android refuses an update when the installed app and new APK use different signing keys. During development, uninstall com.daylight.mirror once before switching from any differently signed build. That uninstall removes the receiver app's local data.
Start and manage the display:
bin/daylight-display start
bin/daylight-display status
bin/daylight-display logs
bin/daylight-display stopCommon display settings can be passed directly when starting or restarting:
bin/daylight-display restart \
--resolution 1280x960 \
--refresh-rate 60 \
--brightness 10 \
--keyframe-interval 300 \
--damage-full-threshold 75 \
--scale 1 \
--workspace 9Change receiver brightness without interrupting the display:
bin/daylight-display brightness 40
bin/daylight-display brightness 0 # backlight offRun bin/daylight-display --help for every option.
List the native and performance-oriented 4:3 resolution presets with:
bin/daylight-display resolutionsArbitrary positive WIDTHxHEIGHT values remain supported; the presets range from the native 1600×1200 mode down to 640×480.
The default output is DAYLIGHT-1, 1280×960 at 60 fps, scale 1, brightness 10, with workspace 9 assigned to it. It keeps only one unacknowledged frame in flight to minimize cursor latency and sends a full recovery keyframe every 300 transmitted frames (about five seconds at 60 fps); reconnects always force an immediate keyframe. Changed content is tracked in 64×64 tiles: sparse, fragmented changes use a packed multi-tile payload instead of one wasteful bounding rectangle. The receiver preserves those tiles through its render handoff and performs separate GPU texture uploads when they cover less than half of their bounding rectangle and require no more than 64 calls; otherwise it uses one bounding upload. Updates covering at least 75% of the image adaptively become full frames. Set --damage-full-threshold 0 to disable that adaptive cutoff. Capture is paced by the headless output's refresh rather than a recorder-side FPS filter, avoiding accumulated frames and catch-up bursts. The receiver's fixed device port 8888 is tunneled to host port 18888.
Override settings with DAYLIGHT_WIDTH, DAYLIGHT_HEIGHT, DAYLIGHT_FPS, DAYLIGHT_BRIGHTNESS (0–255; zero disables the backlight), DAYLIGHT_MAX_INFLIGHT, DAYLIGHT_KEYFRAME_INTERVAL, DAYLIGHT_DAMAGE_FULL_THRESHOLD, DAYLIGHT_PORT, DAYLIGHT_WORKSPACE, or DAYLIGHT_SERIAL. To restore the sharper mode, use DAYLIGHT_WIDTH=1600 DAYLIGHT_HEIGHT=1200 bin/daylight-display restart. If a workload produces too much motion or CPU use, use DAYLIGHT_FPS=30 bin/daylight-display restart.
The launcher runs as a transient user systemd service so it survives the terminal that started it. The output and ADB tunnel are created at runtime and removed on stop. No file under ~/.config/hypr or /usr/share/omarchy is modified.
bin/daylight-display logs reports host averages for capture-to-ACK frame age, pixel processing, LZ4 compression, ACK backpressure waits, socket writes, and receiver ACK round trips. The sender waits for transport capacity before selecting the newest captured generation, preventing an already-processed frame from becoming stale behind an in-flight frame. The Android DaylightMirror log tag reports payload reads, decompression, delta application, GPU texture upload, buffer swap, decode-to-presentation time, tiled-upload frame and call counts, and uploaded pixels as a percentage of the equivalent bounding rectangles. Use the profile receiver when collecting traces and the release receiver when comparing normal performance.
The native sender is built with -march=native and link-time optimization. Rebuild it on the host where it will run; override CFLAGS if a portable binary is required.
Run all host protocol tests with:
make testThe tests open a loopback receiver, validate resolution/brightness control packets, decompress payloads, acknowledge frames, and reconstruct the expected images. The tiled test specifically covers variable-length tile headers, bounds, packed tile payloads, multi-tile reconstruction, and adaptive full-frame fallback. Android builds also run Gradle's lint checks as part of profile/release assembly.
Build all versioned artifacts and SHA-256 checksums with:
export ANDROID_SDK_ROOT=/path/to/Android/Sdk
export JAVA_HOME=/path/to/jdk-21
make releaseOutputs under dist/ are:
daylight-display-VERSION-linux-x86_64.tar.gz: portable x86-64-v2 sender, helpers, README, and APKdaylight-display-VERSION-source.tar.gz: reproducible source archivedaylight-mirror-VERSION.apk: standalone optimized receiverSHA256SUMS: artifact checksums
The archives use sorted paths, normalized ownership, SOURCE_DATE_EPOCH, and timestamp-free gzip output. Override SOURCE_DATE_EPOCH to reproduce a specific build timestamp.
For a distribution-signed APK, provide all four values before make release:
export DAYLIGHT_KEYSTORE_FILE=/secure/path/release.jks
export DAYLIGHT_KEYSTORE_PASSWORD='...'
export DAYLIGHT_KEY_ALIAS='...'
export DAYLIGHT_KEY_PASSWORD='...'
make releaseNever commit the keystore or passwords. Without these values the artifact is development-signed and suitable for local installation, not an official public release.
The project is available under the MIT License. Bundled LZ4 source retains its own upstream license notices.
adb devices -l shows unauthorized or no device:
- Unlock the DC-1 and accept its USB-debugging authorization prompt.
- Confirm the cable supports data, reconnect it, then run
adb kill-server && adb start-server. - Select USB debugging again in Android developer options if authorization was revoked.
The receiver remains on “waiting”:
- Run
bin/daylight-display statusand inspectbin/daylight-display logs. - Verify
adb reverse --listcontains device port8888mapped to the configured host port. - Restart with
bin/daylight-display restart; reconnects force a recovery keyframe.
The headless output is missing or applications open on the wrong monitor:
- Run
hyprctl monitors alland confirmDAYLIGHT-1exists. - Confirm the requested workspace is not reserved by another monitor rule.
- Stop and restart the helper. It creates only runtime state and does not edit Hyprland configuration files.
Installation fails with INSTALL_FAILED_UPDATE_INCOMPATIBLE:
- The installed APK uses a different signing key. Uninstall
com.daylight.mirror, then reinstall. Uninstalling removes local app data.
Performance is poor during full-screen animation:
- Try
--resolution 1024x768or--refresh-rate 30. - Sparse editor/cursor changes use tiled transport; genuinely full-screen motion must still send large frames.
- Use sender logs and the Android
DaylightMirrorlog tag to distinguish capture, transport, decode, upload, and display synchronization costs.
Run a one-hour background stability session while using the display normally:
bin/daylight-stability start
bin/daylight-stability statusThe monitor samples sender and receiver memory, CPU counters, connection state, and DC-1 temperatures every ten seconds. Results and a generated REPORT.md are written beneath stability-runs/, which is ignored by Git. Use bin/daylight-stability stop to finish early; stopping the monitor does not stop the display. Duration and cadence can be changed with --duration and --interval.