diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 0eb741bb..eac9b83a 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -14,9 +14,12 @@ namespace PepperDash.Essentials.Core.Routing public class RoutingFeedbackManager : EssentialsDevice { /// - /// Maps midpoint device keys to the set of sink device keys that are downstream + /// Maps midpoint device keys to the set of downstream sink input ports, derived from the + /// static tie-line topology. Because it is built from topology rather than a sink's currently + /// reported input, sinks that never report an input (i.e. + /// stays null, such as codecs fed from an external matrix) are still mapped and updated. /// - private Dictionary> midpointToSinksMap; + private Dictionary> midpointToSinkInputsMap; /// /// Debounce timers for each sink device to prevent rapid successive updates @@ -42,30 +45,39 @@ namespace PepperDash.Essentials.Core.Routing } /// - /// Builds a map of which sink devices are downstream of each midpoint device - /// for performance optimization in HandleMidpointUpdate + /// Builds a map of which sink input ports are downstream of each midpoint device + /// for performance optimization in HandleMidpointUpdate. + /// The map is derived from the static tie-line topology (every sink input port is traced + /// upstream), so it does not depend on a sink having already reported its current input. /// private void BuildMidpointSinkMap() { - midpointToSinksMap = new Dictionary>(); + midpointToSinkInputsMap = new Dictionary>(); var sinks = DeviceManager.AllDevices.OfType(); - var midpoints = DeviceManager.AllDevices.OfType(); foreach (var sink in sinks) { - if (sink.CurrentInputPort == null) + // Trace from every input port on the sink (static topology) rather than only the + // currently-selected input. Sinks that never report an input still get mapped. + var inputPorts = (sink as IRoutingInputs)?.InputPorts; + if (inputPorts == null) continue; - // Find all upstream midpoints for this sink - var upstreamMidpoints = GetUpstreamMidpoints(sink); - - foreach (var midpointKey in upstreamMidpoints) + foreach (var inputPort in inputPorts) { - if (!midpointToSinksMap.ContainsKey(midpointKey)) - midpointToSinksMap[midpointKey] = new HashSet(); + var upstreamMidpoints = GetUpstreamMidpointsForInput(inputPort); - midpointToSinksMap[midpointKey].Add(sink.Key); + foreach (var midpointKey in upstreamMidpoints) + { + if (!midpointToSinkInputsMap.TryGetValue(midpointKey, out var inputs)) + { + inputs = new HashSet(); + midpointToSinkInputsMap[midpointKey] = inputs; + } + + inputs.Add(inputPort); + } } } @@ -73,24 +85,25 @@ namespace PepperDash.Essentials.Core.Routing Serilog.Events.LogEventLevel.Information, "Built midpoint-to-sink map with {count} midpoints", this, - midpointToSinksMap.Count + midpointToSinkInputsMap.Count ); } /// - /// Gets all upstream midpoint device keys for a given sink + /// Gets all upstream midpoint device keys reachable from a specific sink input port + /// by walking the static tie-line topology. /// - private HashSet GetUpstreamMidpoints(IRoutingSinkWithFeedback sink) + private HashSet GetUpstreamMidpointsForInput(RoutingInputPort inputPort) { var result = new HashSet(); var visited = new HashSet(); - if (sink.CurrentInputPort == null) + if (inputPort == null) return result; var tieLine = TieLineCollection.Default.FirstOrDefault(tl => - tl.DestinationPort.Key == sink.CurrentInputPort.Key && - tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key); + tl.DestinationPort.Key == inputPort.Key && + tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); if (tieLine == null) return result; @@ -171,22 +184,34 @@ namespace PepperDash.Essentials.Core.Routing try { // Only update affected sinks (performance optimization) - if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys)) + if (midpointToSinkInputsMap != null && midpointToSinkInputsMap.TryGetValue(midpoint.Key, out var affectedInputPorts)) { Debug.LogMessage( Serilog.Events.LogEventLevel.Debug, - "Midpoint {midpoint} changed, updating {count} downstream sinks", + "Midpoint {midpoint} changed, updating {count} downstream sink inputs", this, midpoint.Key, - affectedSinkKeys.Count + affectedInputPorts.Count ); - foreach (var sinkKey in affectedSinkKeys) + // Avoid redundant updates when a feedback-reporting sink has several mapped inputs. + var updatedSinkKeys = new HashSet(); + + foreach (var inputPort in affectedInputPorts) { - if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithFeedback sink) - { - UpdateDestination(sink, sink.CurrentInputPort); - } + if (!(inputPort.ParentDevice is IRoutingSinkWithFeedback sink)) + continue; + + // Sinks that report their input drive updates off the currently-selected input. + // Sinks that never report an input (CurrentInputPort == null, e.g. a codec fed + // from an external matrix) fall back to the static topology input port so that + // matrix route changes still propagate. + var portToUse = sink.CurrentInputPort ?? inputPort; + + if (sink.CurrentInputPort != null && !updatedSinkKeys.Add(sink.Key)) + continue; + + UpdateDestination(sink, portToUse); } } else