Summary
Tearing down a seat interrupts an in-progress stream on a standalone/console Apollo. The console Apollo tears down its encoder, re-enumerates the GPU, rebuilds its capture pipeline and creates a new encoder — roughly a one-second stall for whoever is watching.
This contradicts what CLAUDE.md promises under "Coexistence with a standalone Apollo":
MultiSeat is self-contained and non-destructive: it works out of the box whether or not the host already runs a standalone Apollo (e.g. for the main console account).
The three guarantees listed there (own binary, own port range, never kills a non-MultiSeat Apollo) all hold. They just do not cover this: we never touch the other Apollo's process, but we do disturb its capture.
Evidence
Measured on the reference host while a Moonlight client was connected to the console Apollo. Two seat teardowns, two encoder restarts, sub-second correlation each time:
| MultiSeat: seat torn down |
Console Apollo: encoder teardown |
08:45:13 |
08:45:12.674 |
09:00:35 |
09:00:34.484 |
Console Apollo log around the second one:
[09:00:34.484]: Info: Starting async encoder teardown
[09:00:34.544]: Info: Async encoder teardown complete
[09:00:35.108]: Info:
Device Description : NVIDIA GeForce RTX 3080
...
Capture size : 1920x1080
[09:00:35.109]: Info: Desktop resolution [1920x1080]
[09:00:35.138]: Info: Creating encoder [hevc_nvenc]
That is a full capture-pipeline rebuild, not a parameter change.
Mechanism
The console Apollo on this host is headless, so it runs on its own SudoVDA display (\.\DISPLAY30) captured via DXGI desktop duplication.
SeatManager.TeardownSeatInternalAsync calls _apolloManager.Stop(seat), and the seat's Apollo removes the SudoVDA display it created on the way out. SessionLauncher.LogoffSession follows shortly after. Either is a desktop topology change, and a topology change invalidates an existing DXGI duplication (DXGI_ERROR_ACCESS_LOST), which is exactly what would force the console Apollo to rebuild.
Worth noting VirtualDisplayManager.DestroyDisplayAsync is not the culprit — it is bookkeeping only and says so:
/// Release the virtual display assignment for a seat.
/// Does not destroy the underlying SudoVDA driver display.
What I did not establish
- Provisioning appears not to do this. Seats provisioned at
08:44:18 and 09:00:14 produced no encoder restart in the console Apollo. So on this evidence it is removal, not addition, that hurts — but that is two samples, not a controlled test.
- The precise trigger is inferred, not proven. I have not separated "seat Apollo exits and drops its display" from "RDP session logs off" — both happen within a second of each other in the teardown path, and either could be the topology change that matters.
- Unknown whether a seat's Apollo suffers the same thing when a different seat is torn down. If it does, this is not only a coexistence bug but a multi-seat one, and considerably more serious.
Why it matters
The whole point of the coexistence guarantees is that someone can run MultiSeat on a machine they also game on. A one-second freeze in the console player's stream every time a seat goes away is a poor experience, and it is invisible from the MultiSeat side — nothing logs it, and the operator tearing a seat down has no idea they just interrupted someone.
It also has a practical consequence for anyone developing on this project: routine seat churn during testing degrades a stream that someone else is using on the same box.
Possible directions
Not investigated, in rough order of appeal:
- Find out whether the removal can be made quieter — e.g. whether ordering the teardown so the RDP session logs off before the seat's Apollo exits produces one topology change instead of two.
- Warn rather than fix. Detect a connected client on a non-MultiSeat Apollo and log a warning (or refuse without a flag) when tearing down a seat. Cheap, honest, and removes the "invisible" part even if the stall stays.
- Establish whether seat-to-seat interference exists, since that would change the priority entirely.
Documentation should be corrected either way — the non-destructive claim needs this exception stated.
Summary
Tearing down a seat interrupts an in-progress stream on a standalone/console Apollo. The console Apollo tears down its encoder, re-enumerates the GPU, rebuilds its capture pipeline and creates a new encoder — roughly a one-second stall for whoever is watching.
This contradicts what
CLAUDE.mdpromises under "Coexistence with a standalone Apollo":The three guarantees listed there (own binary, own port range, never kills a non-MultiSeat Apollo) all hold. They just do not cover this: we never touch the other Apollo's process, but we do disturb its capture.
Evidence
Measured on the reference host while a Moonlight client was connected to the console Apollo. Two seat teardowns, two encoder restarts, sub-second correlation each time:
08:45:1308:45:12.67409:00:3509:00:34.484Console Apollo log around the second one:
That is a full capture-pipeline rebuild, not a parameter change.
Mechanism
The console Apollo on this host is headless, so it runs on its own SudoVDA display (
\.\DISPLAY30) captured via DXGI desktop duplication.SeatManager.TeardownSeatInternalAsynccalls_apolloManager.Stop(seat), and the seat's Apollo removes the SudoVDA display it created on the way out.SessionLauncher.LogoffSessionfollows shortly after. Either is a desktop topology change, and a topology change invalidates an existing DXGI duplication (DXGI_ERROR_ACCESS_LOST), which is exactly what would force the console Apollo to rebuild.Worth noting
VirtualDisplayManager.DestroyDisplayAsyncis not the culprit — it is bookkeeping only and says so:What I did not establish
08:44:18and09:00:14produced no encoder restart in the console Apollo. So on this evidence it is removal, not addition, that hurts — but that is two samples, not a controlled test.Why it matters
The whole point of the coexistence guarantees is that someone can run MultiSeat on a machine they also game on. A one-second freeze in the console player's stream every time a seat goes away is a poor experience, and it is invisible from the MultiSeat side — nothing logs it, and the operator tearing a seat down has no idea they just interrupted someone.
It also has a practical consequence for anyone developing on this project: routine seat churn during testing degrades a stream that someone else is using on the same box.
Possible directions
Not investigated, in rough order of appeal:
Documentation should be corrected either way — the non-destructive claim needs this exception stated.