This is our fork of Apollo (itself a Sunshine fork), built and released as ApolloVibe and
consumed by MultiSeat. MultiSeat installs it to
C:\Program Files\ApolloVibe\, deliberately separate from any standalone C:\Program Files\Apollo\.
C++ / CMake / Ninja, MSVC on Windows. The web UI is Vite.
| remote | url | what it is |
|---|---|---|
origin |
vibesoftwarecoder/ApolloVibe |
ours — releases are published here |
upstream |
ClassicOldSong/Apollo |
Apollo proper, by ClassicOldSong |
logabell |
logabell/Apollo |
the microphone-passthrough fork |
Work on master. As of 2026-09-04 master and apollovibe-dev were collapsed into one
branch and are identical. master is the default branch, the branch releases are built from,
and the only branch GitHub reads issue templates from. apollovibe-dev is kept as a redundant
pointer for now; it carries nothing master lacks.
⛔ Do not merge logabell/feature/microphone-passthrough. That work is already absorbed;
merging it again re-applies changes that are in the tree.
ClassicOldSong is not Nonary. Two different people, two different projects — Nonary
(Chase Payne) writes libvirtualdisplay and Vibeshine. Conflating them has produced wrong
conclusions before.
✅ The old two-branch split is gone (merge a68da22a). master previously carried 13 commits
of ours that a fast-forward would have destroyed — the ApolloVibe rebrand, version/update URLs, mic
lineage, AMD/BOOST fixes. Those are now on the single branch along with everything from
apollovibe-dev.
git rev-list --left-right --count in BOTH directions before any merge advice.
The rule outlived the specific divergence that produced it.
upstream/master has been dormant since 2026-05-21 and we are 0 behind it. If ClassicOldSong
resumes, merge often — the rebrand touches user-facing strings all over the tree, so conflicts scale
with how far behind you let it drift.
⛔ Do not merge logabell/master either — it is 14 commits BEHIND ClassicOldSong, so it drags
the tree backwards. logabell/feature/microphone-passthrough is 785 behind: stale, ignore it.
Related forks under vibesoftwarecoder: moonlight-common-c, moonlight-common-c-mic; and
MoonlightVibe-Development tracks logabell/moonlight-qt-mic.
Requires MSYS2 — the batch files run ninja through MSYS bash. Default C:\msys64,
overridable with MSYS2_ROOT. All four scripts are tracked (72618f92) and derive the repo root
from %~dp0, so they are portable.
build_apollovibe.bat :: ninja -j4 sunshine -> build/sunshine.exe (~187s)
build_apollovibe_rebase.bat :: cmake configure + build, for after a rebase
package_apollovibe.bat <ver> :: stages build/{sunshine.exe,assets,tools} into the release zipBuild output is gitignored (38d1f013): the zip, _repackage/, build_apollovibe.log.
C:\Users\<username>\... and would have published a username to a
public fork. They were fixed before being committed, and reading them first is what caught it.
Always read build tooling before committing it to a public repo — absolute local paths hide
there.
stop MultiSeatService :: tears down seats and releases the exe lock
back up C:\Program Files\ApolloVibe\sunshine.exe
copy the new sunshine.exe
start MultiSeatService
For a commit touching only src/, sunshine.exe is the only file that needs copying. The
standalone C:\Program Files\Apollo\Sunshine.exe keeps running throughout, which is correct.
Verify the deploy by a log line that only exists in the new build — a stale binary otherwise looks
identical.
build_apollovibe.bat only compiles. CPack is NSIS-only here (CPACK_BINARY_ZIP=OFF), so the
portable zip is assembled by hand — and two releases shipped sunshine.exe only, with assets/
and tools/ missing.
The symptom is nasty because it looks like a MultiSeat bug: Apollo exits at startup, MultiSeat's seat provisioning fails, and the user reports "loopback RDP error" (this was GitHub issue #5).
package_apollovibe.ps1 now hard-fails if assets/apps.json, assets/web/index.html or
tools/sunshinesvc.exe are missing. Use it; never hand-zip a release.
The release asset MultiSeat downloads is named apollovibe-v<ver>-windows-x64.zip, and the prereq
script currently pins v2026.6.1-multiseat.1. Changing the tag means changing
install-prerequisites.ps1 too.
These were learned the hard way while chasing MultiSeat issues; each one cost real time.
// src/main.cpp
if (config::parse(argc, argv)) { return 0; } // ~line 162 — exits here
auto log_deinit_guard = logging::init(...); // ~line 166 — logging starts HERESo a config failure exits with no log file at all. If Apollo dies leaving nothing, look at config, not at the encoder.
// src/logging.cpp
if (min_log_level >= 1) { av_log_set_level(AV_LOG_QUIET); }
else { av_log_set_level(AV_LOG_DEBUG); }verbose=0, debug=1, info=2. debug is not enough — anything ≥ 1 discards every FFmpeg
message. Since h264_amf, the QSV encoders and the software encoders are all FFmpeg encoders, an
encoder that fails to open prints Creating encoder [...] and then nothing.
This is the single most useful setting when diagnosing an encoder.
make_synced_session returns std::nullopt when make_encode_device yields null, without
logging — while every failure path inside the D3D11 encode-device setup does log. So a log that
stops right after Creating encoder is not evidence of a crash.
| the process | what it means |
|---|---|
| exits within seconds | the encoder failed to open, and the reason was discarded. Set the log level to verbose to recover it |
| stays alive, one CPU core at 100% | the encoder opened fine and the encode loop is spinning |
The second case was measured on 2026-09-05 (MultiSeat issue #24, an AMD host). At verbose the log
showed h264_amf configuring, then Frame 1: IDR Keyframe with a complete AUD/SPS/PPS/IDR set —
so the encoder worked. What followed was one SubmitInput, no second frame, and QueryOutput()
repeating with zero errors, filling 30 MB of log in seconds and pinning one core. That is a
hang, not a failed encoder, and it reads identically at info level.
It is literally "assets" (cmake/compile_definitions/common.cmake), so apply_config() resolves
assets/apps.json against the working directory. MultiSeat runs each seat's Apollo with the
per-seat directory as its working dir, which is why it creates assets and tools junctions there.
Break the junction and the seat's Apollo dies before logging.
display_base.cpp filters adapters by adapter_name, then enumerates that adapter's outputs and
requires AttachedToDesktop plus a successful DXGI duplication test. The GPU follows the display
— you cannot capture display X and encode on GPU Y.
Warning: SudoVDA: display was added but never became nameable/active after 5100ms
— RDP indirect display likely still owns the topology
Measured with a client attached: in a seat's RDP session the display namespace is entirely
RdpIdd_IndirectDisplay — 16 adapters, zero SudoVDA. The seat's display reports 1000 Hz, which
is RdpIdd's synthetic rate and breaks frame pacing in older games. ChangeDisplaySettingsEx to 60 Hz
returns DISP_CHANGE_SUCCESSFUL and changes nothing.
So a seat streams its RDP surface. That works, and it is not a broken stream — but it is why seats have no real refresh rate.
This machine is the reference host and runs live seats. These are not style preferences; each one follows a real incident.
- ⛔ The host is headless and the user is never physically at it. Any procedure that says "watch the screen" or "listen to the speakers" cannot be run. Reach it over RustDesk or through logs.
- ⛔ Never provision or tear down a seat while anything is streaming. Check first, and check it correctly — Moonlight's video is UDP, so established TCP connections prove nothing. Use per-process GPU encode utilisation, which also names which Apollo is busy.
- ⛔ Reboots are the user's call. Stage them and say so; never run
Restart-Computer. - ⛔ This fork is public. Read anything before committing it, especially build tooling — these
scripts once carried
C:\Users\<username>\...and would have published a username. ⚠️ Deploying a newsunshine.exeis manual, and a stale binary looks identical. Verify with a log line only the new build emits.⚠️ Validate an instrument before trusting what it says. A probe that returns the same answer no matter what you change is broken, not informative — run it where you know the answer first.
Fuller versions of these, and the incidents behind them, are in this project's memory.
MultiSeat is the main consumer, so a change here is not proven until a seat uses it:
# in the MultiSeat repo
.\scripts\smoke-seat.ps1 -Account <seat> # provision, assert, tear down, restore⛔ Never provision or tear down a seat while someone is streaming. Teardown makes a standalone console Apollo rebuild its encoder (~700 ms stall, measured five times). Check first with per-process GPU encode utilisation — not established TCP connections, since Moonlight's video is UDP:
(Get-Counter '\GPU Engine(*engtype_VideoEncode)\Utilization Percentage').CounterSamples |
Where-Object CookedValue -gt 1