Summary
An AudioVideo route adds two RouteDescriptors to RouteDescriptorCollection.DefaultCollection — one Audio, one Video — but RemoveRouteDescriptor removes only one. Every route change to an AudioVideo destination therefore orphans a descriptor, and each subsequent release pops a progressively older one.
The consequences are that a destination's release acts on a stale route, its output ports' InUseTracker counts never return to zero, and routed-source feedback derived from the collection lags reality.
Observed on v3.0.0-rc.6, CP4N.
Mechanism
RunRouteRequest builds the pair (Extensions.cs#L240-L249):
var audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Audio);
...
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Video);
AddRouteDescriptor dedups on (Source, Destination, SignalType, InputPort), so both are stored (RouteDescriptorCollection.cs#L55-L60).
But the lookup used on release ignores SignalType entirely and returns the first match (RouteDescriptorCollection.cs#L89):
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
RemoveRouteDescriptor removes that single descriptor, and ReleaseRouteInternal releases only it. Since RouteDescriptors is a List<>, FirstOrDefault returns the oldest surviving entry — so the lag grows in insertion order rather than staying at one.
The in-use count is the same bug seen from the other end. ReleaseRoutes removes a user keyed by signal type (RouteDescriptor.cs#L151):
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
Two descriptors add destination-Audio and destination-Video; releasing one removes one. The other user is never removed, so the port is permanently marked in use.
Reproduction
Room with an AudioVideo destination (display) and a separate Audio-only destination (dsp-audio-in) fed from the same switcher. Cycle through six sources.
The Audio-only destination is the control group: it has exactly one descriptor, and it is correct on every single press. The AudioVideo destination drifts.
| Press |
display actually on |
display released |
dsp-audio-in released |
| appleTv |
zoom |
zoom ✅ |
zoom ✅ |
| iptv |
appletv |
zoom ❌ |
appletv ✅ |
| signage |
iptv |
appletv ❌ |
iptv ✅ |
| zoom |
wireless |
appletv ❌ |
wireless ✅ |
| appleTv |
zoom |
iptv ❌ |
zoom ✅ |
Accumulation is visible directly in the log. While the source is unchanged, one of the pair is already present, so it is a net +1 against one removal:
Adding route descriptor: zoom-source -> display:hdmiIn1 (Audio)
Route from zoom-source to display:hdmiIn1 (Video) already exists in this collection
On a source change neither exists, so it is a net +2 against one removal:
Adding route descriptor: appletv-source -> display:hdmiIn1 (Audio)
Adding route descriptor: appletv-source -> display:hdmiIn1 (Video)
Matching in-use counts from the same presses — the AudioVideo port never reaches 0, the Audio-only port is clean every time:
Port dmLiteOut1 releasing. Count=1 ... routing. Count=1 ... routing. Count=2
Port auxOut1 releasing. Count=0 ... routing. Count=1
Impact
The executed switches are always correct, so AV lands on the right input and the fault is not audible or visible in the room. What breaks is the bookkeeping built on it:
- A destination's "currently routed source" is wrong after the second source change. This is the same Audio/Video split behind the routed-source label flashing on an NVX bench.
InUseTracker counts oscillate and never reach zero, so anything gating on "is this port free" sees a port that is never free.
ReleaseRoute(clearRoute: true) clears the stale route's switch rather than the current one.
Suggested fix
RemoveRouteDescriptor should remove every descriptor for the destination (optionally filtered by input port key) and release each, rather than only the first. The caller's intent at ReleaseRouteInternal is "release whatever is currently routed to this destination", which is all of them.
Returning a single RouteDescriptor is part of the signature, so this likely wants an overload or a return of the released set — ReleaseRouteInternal uses the returned SignalType afterwards to decide which ICurrentSources entries to clear, and that logic would want the union.
Filtering the lookup by SignalType instead would fix the in-use counts but leave the caller still releasing one of the two, so the stale-descriptor half would remain.
Summary
An
AudioVideoroute adds twoRouteDescriptors toRouteDescriptorCollection.DefaultCollection— oneAudio, oneVideo— butRemoveRouteDescriptorremoves only one. Every route change to an AudioVideo destination therefore orphans a descriptor, and each subsequent release pops a progressively older one.The consequences are that a destination's release acts on a stale route, its output ports'
InUseTrackercounts never return to zero, and routed-source feedback derived from the collection lags reality.Observed on v3.0.0-rc.6, CP4N.
Mechanism
RunRouteRequestbuilds the pair (Extensions.cs#L240-L249):AddRouteDescriptordedups on(Source, Destination, SignalType, InputPort), so both are stored (RouteDescriptorCollection.cs#L55-L60).But the lookup used on release ignores
SignalTypeentirely and returns the first match (RouteDescriptorCollection.cs#L89):RemoveRouteDescriptorremoves that single descriptor, andReleaseRouteInternalreleases only it. SinceRouteDescriptorsis aList<>,FirstOrDefaultreturns the oldest surviving entry — so the lag grows in insertion order rather than staying at one.The in-use count is the same bug seen from the other end.
ReleaseRoutesremoves a user keyed by signal type (RouteDescriptor.cs#L151):Two descriptors add
destination-Audioanddestination-Video; releasing one removes one. The other user is never removed, so the port is permanently marked in use.Reproduction
Room with an
AudioVideodestination (display) and a separateAudio-only destination (dsp-audio-in) fed from the same switcher. Cycle through six sources.The Audio-only destination is the control group: it has exactly one descriptor, and it is correct on every single press. The AudioVideo destination drifts.
displayactually ondisplayreleaseddsp-audio-inreleasedAccumulation is visible directly in the log. While the source is unchanged, one of the pair is already present, so it is a net +1 against one removal:
On a source change neither exists, so it is a net +2 against one removal:
Matching in-use counts from the same presses — the AudioVideo port never reaches 0, the Audio-only port is clean every time:
Impact
The executed switches are always correct, so AV lands on the right input and the fault is not audible or visible in the room. What breaks is the bookkeeping built on it:
InUseTrackercounts oscillate and never reach zero, so anything gating on "is this port free" sees a port that is never free.ReleaseRoute(clearRoute: true)clears the stale route's switch rather than the current one.Suggested fix
RemoveRouteDescriptorshould remove every descriptor for the destination (optionally filtered by input port key) and release each, rather than only the first. The caller's intent atReleaseRouteInternalis "release whatever is currently routed to this destination", which is all of them.Returning a single
RouteDescriptoris part of the signature, so this likely wants an overload or a return of the released set —ReleaseRouteInternaluses the returnedSignalTypeafterwards to decide whichICurrentSourcesentries to clear, and that logic would want the union.Filtering the lookup by
SignalTypeinstead would fix the in-use counts but leave the caller still releasing one of the two, so the stale-descriptor half would remain.