Summary
RoutingFeedbackManager.UpdateDestinationImmediate removes a destination's route descriptors before re-deriving them, and does not restore them when the re-derivation yields no source. The destination's routing state is then gone, so a subsequent release finds nothing to release and the hardware is never torn down.
User-visible symptom: routing an "off"/clear source to an affected destination silently does nothing. The route stays up.
Regression in v3.0.0-rc.10. Not present in rc.5.
Why it only hits some destinations
RemoveRouteDescriptors matches on input port key:
.Where(rd => rd.Destination == destination &&
(string.IsNullOrEmpty(inputPortKey) || (rd.InputPort != null && rd.InputPort.Key == inputPortKey)))
The feedback manager always passes a concrete inputPort.Key. So:
- A destination configured without
destinationPortKey builds descriptors with a null InputPort (logged as dest:auto). They never match, and survive.
- A destination configured with
destinationPortKey builds descriptors carrying that port. They match, and are removed.
That is the whole difference between a destination that works and one that doesn't.
Observed
Three destinations, same room, same NVX fabric. display has no destinationPortKey; content and audio do.
Routing a source to audio, then clearing it:
11:03:31.003 Adding route descriptor: tx-zoom -> dsp-audio-in:anyVideoIn (Audio)
11:03:31.003 Adding route descriptor: tx-zoom -> dsp-audio-in:anyVideoIn (Video)
... route executes, NVX stream starts ...
11:03:31.611 [routingFeedbackManager] Updating destination dsp-audio-in with inputPort anyVideoIn
11:03:31.611 Removed 2 route descriptor(s) for 'dsp-audio-in':'anyVideoIn'
11:03:31.612 [routingFeedbackManager] Setting dsp-audio-in current Audio source to none
11:03:31.614 [routingFeedbackManager] Setting dsp-audio-in current Video source to none
11:03:33.689 Release route for 'dsp-audio-in':'anyVideoIn'
11:03:33.689 Removed 0 route descriptor(s) for 'dsp-audio-in':'anyVideoIn'
No switch is executed, no stream cleared. The decoder stays tuned.
The same clear on display works, because its descriptors were never removed:
11:03:23.146 Removed 2 route descriptor(s) for 'display':'auto'
11:03:23.146 [display] Releasing current route: tx-pc (Audio)
11:03:23.147 *** Executing switch: null to rx-display with signal type Audio ***
11:03:23.148 [rx-display] Clearing stream
11:03:23.250 activeRoutes: {"display":"none"}
Cause
RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptors(destination, inputPort.Key);
foreach (var signalType in new[] { eRoutingSignalType.Audio, eRoutingSignalType.Video })
{
if (!TryGetActiveSourcePort(firstTieLine, signalType, new HashSet<string>(), out var sourcePort))
continue; // bails - descriptors stay removed
var source = sourcePort?.ParentDevice as IRoutingOutputs;
destination.SetCurrentSource(signalType, source as IRoutingSource);
if (source == null)
continue; // bails - descriptors stay removed
var (route, _) = destination.GetRouteToSource(source, signalType, inputPort, sourcePort);
RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(route);
}
The removal is unconditional; both re-add paths are conditional. Any upstream walk that fails to resolve a source destroys state it cannot rebuild. In our case TryGetActiveSourcePort resolves to a null source while walking back through the NVX virtual matrix, roughly 600 ms after the route executed.
Suggested fix
Do not remove before you can replace. Either:
- derive the replacement descriptors first, and only remove the old ones once a replacement exists for that signal type; or
- remove per signal type, inside the loop, after
TryGetActiveSourcePort has succeeded.
Setting the current source to none when the walk fails seems reasonable on its own — the feedback genuinely is unknown. Deleting the route descriptors is the part that breaks teardown, because ReleaseRouteInternal depends on them to know what to switch off.
Worth noting this interacts with #1494: before that fix this call removed at most one descriptor, so the blast radius was smaller. The #1494 fix is correct; this call site just needs to stop assuming removal is free.
Workaround
Omit destinationPortKey from the destination so its descriptors are keyed auto and escape the match. Only viable when a single tie line feeds the sink, so the port still resolves unambiguously.
Environment
- Essentials
v3.0.0-rc.10 on a CP4N
PepperDash.Essentials.Plugins.Crestron.Nvx 4.0.0-feat-matrix-routing-videosync-status.4
- Destinations: LG display (no port key), Zoom codec
hdmiIn1, genericsink anyVideoIn
Summary
RoutingFeedbackManager.UpdateDestinationImmediateremoves a destination's route descriptors before re-deriving them, and does not restore them when the re-derivation yields no source. The destination's routing state is then gone, so a subsequent release finds nothing to release and the hardware is never torn down.User-visible symptom: routing an "off"/clear source to an affected destination silently does nothing. The route stays up.
Regression in
v3.0.0-rc.10. Not present in rc.5.Why it only hits some destinations
RemoveRouteDescriptorsmatches on input port key:The feedback manager always passes a concrete
inputPort.Key. So:destinationPortKeybuilds descriptors with a nullInputPort(logged asdest:auto). They never match, and survive.destinationPortKeybuilds descriptors carrying that port. They match, and are removed.That is the whole difference between a destination that works and one that doesn't.
Observed
Three destinations, same room, same NVX fabric.
displayhas nodestinationPortKey;contentandaudiodo.Routing a source to
audio, then clearing it:No switch is executed, no stream cleared. The decoder stays tuned.
The same clear on
displayworks, because its descriptors were never removed:Cause
The removal is unconditional; both re-add paths are conditional. Any upstream walk that fails to resolve a source destroys state it cannot rebuild. In our case
TryGetActiveSourcePortresolves to a null source while walking back through the NVX virtual matrix, roughly 600 ms after the route executed.Suggested fix
Do not remove before you can replace. Either:
TryGetActiveSourcePorthas succeeded.Setting the current source to none when the walk fails seems reasonable on its own — the feedback genuinely is unknown. Deleting the route descriptors is the part that breaks teardown, because
ReleaseRouteInternaldepends on them to know what to switch off.Worth noting this interacts with #1494: before that fix this call removed at most one descriptor, so the blast radius was smaller. The #1494 fix is correct; this call site just needs to stop assuming removal is free.
Workaround
Omit
destinationPortKeyfrom the destination so its descriptors are keyedautoand escape the match. Only viable when a single tie line feeds the sink, so the port still resolves unambiguously.Environment
v3.0.0-rc.10on a CP4NPepperDash.Essentials.Plugins.Crestron.Nvx4.0.0-feat-matrix-routing-videosync-status.4hdmiIn1,genericsinkanyVideoIn