Skip to content

Tearing down a seat interrupts a standalone Apollo's stream (encoder restart) #23

Description

@vibesoftwarecoder

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:

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions