Documentation · Building · Features
Where each part of Nightjar lives, which files change together and which test proves a change. It is written for people and coding agents changing the client.
Nightjar is developed inside the Rubylight repository, in its client folder, and mirrored from there. That repository's tools/code_map.py answers three questions from the tree and its history: index <folder> prints lines, tests and the header sentence of each file, with <file> lists the files that changed in the same commits, and hot <folder> lists fix commits per file. Its check verifies the paths and names on this page, so correct the page in the change that moves the code.
All Rust is one Cargo workspace in protocol/.
| Crate | Folder | Holds |
|---|---|---|
rubylight-protocol |
protocol/core/ |
Wire formats shared with the host, no_std: each extension message in a module of its own, every message id in ids.rs. |
rubylight-traverse |
protocol/traverse/ |
Internet play, shared with the host: DHT records, STUN, PCP and NAT-PMP, hole punching and the tunnel that reach a paired PC from outside its network. |
nightjar-core |
protocol/client/ |
The streaming engine: pairing and HTTPS, RTSP, control, video and audio receive, input, USB forwarding. |
nightjar-policy |
protocol/policy/ |
Decisions without I/O or a clock: bitrate, the quality ladder, the recovery budget, the overlay, the stream menu. Both apps call it, so they decide alike. |
nightjar-desktop |
protocol/desktop/ |
The Windows, Linux and macOS app. |
nightjar-android |
protocol/android/ |
libnightjar_core.so, the JNI side of the Android app's Rust engine. |
rubylight-protocol-ffi |
protocol/ffi/ |
A C interface to rubylight-protocol for the Android app's C code. |
nightjar-cli |
protocol/cli/ |
A headless client for tests. |
nightjar-difftest |
protocol/difftest/ |
Runs the engine and the patched moonlight-common-c on the same packets and compares their output byte for byte. |
A feature lands in both apps in one change. What they share goes into nightjar-policy or nightjar-core; an app only draws it and binds it to its platform.
| Step | Where |
|---|---|
| Find and pair a PC, list apps, launch | protocol/client/src/http/; what the host offers is Feature, HostCapabilities |
| Internet play, guests | protocol/client/src/internet.rs and protocol/client/src/http/internet_play.rs (over rubylight-traverse); joining another device's game in protocol/client/src/http/session_guests.rs |
| RTSP | protocol/client/src/rtsp/, protocol/client/src/sdp.rs |
| Control | protocol/client/src/control/: session.rs is the state machine without a socket, driver.rs runs it on ENet, messages.rs and events.rs are what is sent and received |
| Start | protocol/client/src/session/mod.rs, which also defines the VideoDecoder an app implements |
| Video | receive_thread, receive_batch read the socket; VideoReceiver decrypts; RtpQueue, add_packet, reconstruct_frame order packets and repair loss with ReedSolomon; Depacketizer builds frames; FrameQueue, decoder_thread, decode_loop hand them to the app's decoder |
| Audio | protocol/client/src/audio/, protocol/client/src/session/audio.rs |
| Input, microphone | protocol/client/src/input/ (a physical touchpad's packets in input/touchpad.rs) |
| Native API, files, folders | protocol/client/src/http/native.rs, files.rs and folders.rs beside it |
| Part | protocol/desktop/src/ |
|---|---|
| Entry, command line | main.rs holds the app's state (App) and its loop; cli.rs |
| What the app does | app.rs (saving, shutdown), app/launcher.rs (the launcher's events and frames), app/command.rs (carries out what the UI returns), app/launch.rs (starting a stream), app/session.rs (a running stream and its end), app/overlay.rs (statistics, toasts and the stream menu over the picture), app/renderer.rs (the window's renderer) |
| UI | gui.rs (egui on SDL3), ui.rs and ui/: pages only draw and return commands. Settings page: ui/settings.rs |
| PCs and apps | hosts.rs: discovery and app lists on worker threads, results as events (hosts/events.rs applies them); hosts/pairing.rs, hosts/power.rs (wake, sleep, updates), hosts/native.rs (the native API's reads), hosts/covers.rs |
| Stream | stream/mod.rs (session worker), stream/connect.rs (launch), stream/negotiate.rs (mode, bitrate, codecs, HDR), stream/pipeline.rs (picture loop, decoder order), stream/present.rs and stream/present/ (the video texture; stream/present/pacer.rs decides when a picture is shown), stream/input.rs, stream/recovery.rs, stream/guests.rs, stream/touchpad/ |
| Decode | media.rs (FFmpeg, Opus), vulkan_video.rs, amd_decode.rs, linux_video.rs, dmabuf.rs, stream/pyrowave.rs, stream/pyrowave_gpu.rs |
| Windows only | stream/direct.rs, stream/d3d11.rs, stream/amf_scaler.rs, stream/wasapi.rs, stream/microphone.rs |
| Linux only | dmabuf.rs, vulkan_video.rs, stream/wl_scanout.rs, stream/wl_presentation.rs, stream/drm_vrr.rs, stream/planes_gpu.rs, kms/ with stream/kms_scanout.rs (DRM/KMS output without a desktop), stream/touchpad/evdev.rs |
| macOS only | metal/, stream/macos_video_check.rs |
| Settings, text | config.rs, strings.rs, strings/ |
cfg(unix) includes macOS: Linux-only code takes target_os = "linux". A changed shader .spv needs its Metal translation from protocol/desktop/src/metal/generate.py.
Gradle builds one module, app/, in the flavors nonRoot and root. Its tasks buildRustEngine and prepareNativePatches build nightjar-android for each ABI and apply app/src/main/jni/moonlight-core/patches/ to moonlight-common-c.
A stream runs on one of two engines. The Rust engine is nightjar-core behind RustBridge and protocol/android/src/lib.rs; the C engine is moonlight-common-c behind MoonBridge and app/src/main/jni/moonlight-core/. The Rust side calls the same MoonBridge callbacks the C side does (protocol/android/src/bridge.rs, listener.rs, renderers.rs), so decoders and input see one interface. StreamEngine records which one ran. A protocol change must hold on both.
| Part | app/src/main/java/com/limelight/ |
|---|---|
| Stream activity | Game.java, with GameConnection.java, GameRecovery.java, GameVideoOutput.java, GameTouchInput.java, GameOverlays.java, StreamGuests.java |
| Connection | nvstream/NvConnection.java, nvstream/http/ (serverinfo, pairing, HostCapabilities), nvstream/jni/ |
| Video | binding/video/, led by binding/video/MediaCodecDecoderRenderer.java |
| Input, audio | binding/input/ControllerHandler.java, binding/input/driver/, binding/input/touch/, binding/input/virtual_controller/, binding/audio/ |
| Settings | preferences/PreferenceConfiguration.java, preferences/StreamSettings.java |
| PCs, library, UI | computers/, console/, ui/, grid/ |
| Native API, files | nativeapi/, files/ |
Native code beside the engines, in app/src/main/jni/: pyrowave-renderer/, vulkan/, evdev_reader/, window-dataspace/.
Desktop:
- A field of
Settingswith a doc comment and its default. A stored name is never renamed: an old value is read with a serde alias or inmigrate. Per-PC and per-game values areOverrides. - A row in
settings_rows, in the section the setting belongs to. - Its words: the English literal inside
tr,trf,enis the key, and every table inprotocol/desktop/src/strings/needs an entry for it. A test fails for a missing one. - Tests in
protocol/desktop/src/ui/settings_tests.rs.
Android:
- The preference in
app/src/main/res/xml/preferences.xml, its texts inapp/src/main/res/values/strings.xmland each translatedstrings.xml. - The key and its reading in
PreferenceConfiguration. Keys are never renamed and retired keys never return:SettingsKeysTest. - Its help text in
scripts/generate-settings.py, then run the script. It writesdocs/settings.md, and the documentation check fails when that file is out of date.
A decision both apps make from the setting goes into protocol/policy/src/.
- The id in
ids.rsand a module inprotocol/core/src/. - The engine:
protocol/client/src/control/messages.rsto send,events.rsto receive. It is sent only to a host that announced it. - Android:
protocol/android/src/lib.rsand itsRustBridgemethod; on the C engine the bridge inapp/src/main/jni/moonlight-core/, with a patch in itspatches/folder where moonlight-common-c itself must change. - Desktop: the handler in
protocol/desktop/src/stream/. - Tests:
protocol/client/tests/control_messages_contract.rsandMoonBridgeControlMessagesTest.
Add it to Feature with the evidence that proves it (a serverinfo field, a DESCRIBE line, a native API capability merged by with_native), and to the Java HostCapabilities. Both read the recorded replies in app/src/test/resources/protocol/capabilities/, checked by protocol/client/tests/capabilities_contract.rs. Gate on the feature, never on a host's name.
| Change | Test |
|---|---|
| Engine | cargo test -p nightjar-core in protocol/. protocol/client/tests/ runs whole sessions on loopback (session_loopback.rs, control_loopback.rs, host_interop.rs) and holds the contracts. |
| Bytes from the network | The property tests protocol/core/src/fuzz.rs, protocol/client/src/fuzz_tests.rs and the fuzz_tests.rs under http/ and rtsp/. PROPTEST_CASES raises the count. |
| Same result as moonlight-common-c | cargo test -p nightjar-difftest |
| A policy | Its module tests, and recorded traces under protocol/policy/tests/traces/ replayed by protocol/policy/tests/traces.rs |
| Desktop UI | protocol/desktop/src/ui/tests.rs draws pages and dialogs without a window; protocol/desktop/src/shot.rs rasterises them for screenshots. |
| Android | gradlew :app:testNonRootDebugUnitTest --tests <class> (JUnit and Robolectric, under app/src/test/java/). The unit tests do not compile the JNI code: gradlew :app:externalNativeBuildNonRootDebug does. scripts/test-native.sh runs the C tests, scripts/emulator-smoke.py drives the app on an emulator. |
| Documentation | node scripts/check-doc-links.mjs, and scripts/generate-settings.py leaving docs/settings.md unchanged |
Building has the toolchains each of these needs.
The fix history repeats a few shapes. Check a change against them before calling it done.
- One app only. A setting, a text or a behaviour that reached Android or desktop and not the other.
withon the file you changed shows what the other side touched last time. - One language only. A new text needs every table in
protocol/desktop/src/strings/and every translatedstrings.xml. - Gated by name or model. A feature follows what the host announced and what the device's decoder reports at run time, not a host brand, a vendor or an Android version.
- Recovery. A reconnect, a network change or a stopped stream meets state the first launch set. The cases live in
protocol/desktop/src/stream/recovery.rs,protocol/policy/src/recovery.rsandGameRecoveryTest. - One engine only on Android: a change in the Rust engine that the C engine's patches do not have, or the reverse.
- Linux code compiled for macOS.
cfg(unix)is both; the macOS build runs only in CI.