ArmNN
 20.02
Graph.cpp
Go to the documentation of this file.
1 //
2 // Copyright © 2017 Arm Ltd. All rights reserved.
3 // SPDX-License-Identifier: MIT
4 //
5 
6 #include "Graph.hpp"
7 #include "SubgraphView.hpp"
8 #include "LayersFwd.hpp"
9 
11 
12 #include <armnn/BackendId.hpp>
13 #include <armnn/Logging.hpp>
14 #include <armnn/TypesUtils.hpp>
15 #include <armnn/Utils.hpp>
16 
17 #include <boost/polymorphic_cast.hpp>
18 #include <boost/assert.hpp>
19 #include <boost/format.hpp>
20 
21 #include <unordered_map>
22 #include <DotSerializer.hpp>
23 #include <sstream>
24 
25 namespace armnn
26 {
27 
28 Graph::Graph(const Graph& other)
29 : m_LayersInOrder(other.m_LayersInOrder)
30 {
31  std::unordered_map<const Layer*, Layer*> otherToClonedMap;
32 
33  for (auto&& otherLayer : other.m_Layers)
34  {
35  Layer* const layer = otherLayer->Clone(*this);
36  otherToClonedMap.emplace(otherLayer, layer);
37  }
38 
39  // Copies slot connections.
40  for (auto&& otherLayer : other.m_Layers)
41  {
42  Layer* const thisLayer = otherToClonedMap[otherLayer];
43 
44  auto outputSlot = thisLayer->BeginOutputSlots();
45  for (auto&& otherOutputSlot : otherLayer->GetOutputSlots())
46  {
47  for (auto&& otherInputSlot : otherOutputSlot.GetConnections())
48  {
49  const Layer& otherTgtLayer = otherInputSlot->GetOwningLayer();
50  Layer* const thisTgtLayer = otherToClonedMap[&otherTgtLayer];
51 
52  InputSlot& inputSlot = thisTgtLayer->GetInputSlot(otherInputSlot->GetSlotIndex());
53  outputSlot->Connect(inputSlot);
54  }
55  outputSlot->SetTensorInfo(otherOutputSlot.GetTensorInfo());
56  ++outputSlot;
57  }
58  }
59 }
60 
62 {
63  if (m_Layers.empty())
64  {
65  ARMNN_LOG(info) << "\n Graph is empty.\n";
66  return Status::Success;
67  }
68  ARMNN_LOG(info) << "\n";
69  ARMNN_LOG(info) << "Walking Pattern: \n";
70 
71  for (auto&& it : TopologicalSort())
72  {
73  ARMNN_LOG(info) << it->GetName() << ":" << GetLayerTypeAsCString(it->GetType())
74  << ":" << it->GetBackendId().Get();
75  }
76  ARMNN_LOG(info) << "\n\n";
77 
78  return Status::Success;
79 }
80 
81 Status Graph::SerializeToDot(std::ostream& stream)
82 {
83  {
84  DotGraph graph(stream, "Optimized");
85 
86  {
87  // Default node attributes:
88  DotDefaults nodes(stream, "node");
89  nodes.GetAttributeSet()
90  .AddAttribute("shape", "record");
91  }
92 
93  {
94  // Default edge attributes:
95  DotDefaults edges(stream, "edge");
96  edges.GetAttributeSet()
97  .AddAttribute("fontsize", 8)
98  .AddAttribute("fontcolor", "blue")
99  .AddAttribute("fontname", "arial-bold");
100  }
101 
102  // First declares the nodes.
103  for (auto&& layer : m_Layers)
104  {
105  DotNode node(stream, layer->GetGuid(), GetLayerTypeAsCString(layer->GetType()));
106  // Extracts the layer parameters.
107  ParameterStringifyFunction extractParams = [&node](const std::string & name, const std::string & value){
108  node.GetContents().AddContent(name + " : " + value);
109  };
110  layer->SerializeLayerParameters(extractParams);
111  }
112 
113  // Second declares the edges.
114  for (auto&& layer : m_Layers)
115  {
116  LayerGuid toId = layer->GetGuid();
117 
118  for (unsigned int i=0;i<layer->GetNumInputSlots(); i++)
119  {
120  OutputSlot* outputSlot = static_cast<OutputSlot*>(layer->GetInputSlot(i).GetConnection());
121  LayerGuid fromId = outputSlot->GetOwningLayer().GetGuid();
122  DotEdge edge(stream, fromId, toId);
123 
124  // Now print the tensor shape on the edge.
125  {
126  // Constructs the label attribute with HTML markup.
127  std::stringstream ss;
128  ss << "< " << outputSlot->GetTensorInfo().GetShape() << " >";
129  edge.GetAttributeSet().AddAttribute("label", ss);
130  }
131  }
132  }
133  }
134 
135  if (stream.bad())
136  {
137  return Status::Failure;
138  }
139  return Status::Success;
140 }
141 
143 {
144  // Layers must be sorted in topological order
145  BOOST_ASSERT(m_LayersInOrder);
146 
147  std::unordered_set<const ITensorHandle*> preallocatedTensors;
148  std::unordered_map<const ITensorHandle*, unsigned int> handleReferenceCounts;
149 
150  // Finds the first TensorHandle ancestor of a SubTensorHandle. If the ITensorHandle provided
151  // is a TensorHandle, the function just returns it
152  auto TraceSubTensorHandleAncestry = [](ITensorHandle* const subTensorHandle)
153  {
154  ITensorHandle* ancestor = subTensorHandle;
155  while (ancestor && ancestor->GetParent())
156  {
157  ancestor = ancestor->GetParent();
158  }
159  return ancestor;
160  };
161 
162  // Checks whether a TensorHandle has been pre-allocated
163  auto IsPreallocated = [&](ITensorHandle* const tensorHandle)
164  {
165  return tensorHandle && preallocatedTensors.find(tensorHandle) != preallocatedTensors.end();
166  };
167 
168  // Constant tensor handles need to last from the beginning of execution till the end,
169  // therefore we pre-allocate them upfront
170  for (auto&& layer : m_Layers)
171  {
172  if (layer->GetType() == LayerType::Constant)
173  {
174  for (auto&& slot = layer->BeginOutputSlots(); slot != layer->EndOutputSlots(); ++slot)
175  {
176  ITensorHandle *tensorHandle = TraceSubTensorHandleAncestry(slot->GetOutputHandler().GetData());
177 
178  if (tensorHandle && !IsPreallocated(tensorHandle))
179  {
180  tensorHandle->Allocate();
181  preallocatedTensors.insert(tensorHandle);
182  }
183  }
184  }
185  }
186 
187  // Iterate over the network in topological order
188  for (auto&& layer : m_Layers)
189  {
190  // Count the amount of times each output slot references a certain buffer (ITensorHandle).
191  // The first time we encounter a new tensor handle, we start managing its lifetime.
192  for (auto&& slot = layer->BeginOutputSlots(); slot != layer->EndOutputSlots(); ++slot)
193  {
194  ITensorHandle *tensorHandle = TraceSubTensorHandleAncestry(slot->GetOutputHandler().GetData());
195 
196  if (tensorHandle && !IsPreallocated(tensorHandle))
197  {
198  unsigned int numConnections = slot->GetNumConnections();
199  if (handleReferenceCounts.find(tensorHandle) == handleReferenceCounts.end())
200  {
201  handleReferenceCounts[tensorHandle] = numConnections;
202  tensorHandle->Manage();
203  if (handleReferenceCounts[tensorHandle] == 0u)
204  {
205  // if nobody consumes this tensor we call Allocate()
206  tensorHandle->Allocate();
207  }
208  }
209  else
210  {
211  handleReferenceCounts[tensorHandle] += numConnections;
212  }
213  }
214  }
215 
216  // Loop through the input slots in the same layer and decrement the reference counter associated
217  // to each tensor handle we encounter. Once it reaches zero, we end the lifetime of the tensor handle
218  for (auto&& slot = layer->BeginInputSlots(); slot != layer->EndInputSlots(); ++slot)
219  {
220  ITensorHandle *tensorHandle = TraceSubTensorHandleAncestry(
221  slot->GetConnectedOutputSlot()->GetOutputHandler().GetData());
222 
223  if (tensorHandle && !IsPreallocated(tensorHandle))
224  {
225  --handleReferenceCounts[tensorHandle];
226 
227  if (handleReferenceCounts[tensorHandle] == 0u)
228  {
229  // Stop managing lifetime of tensor handle
230  tensorHandle->Allocate();
231  handleReferenceCounts.erase(tensorHandle);
232  }
233  }
234  }
235  }
236 
237  return Status::Success;
238 }
239 
241 {
242  if (!m_LayersInOrder)
243  {
244  // Resets layer order.
245  for (auto&& it : m_Layers)
246  {
247  it->ResetPriority();
248  }
249 
250  auto compareLayerPriority = [](const LayerList::value_type& layerA, const LayerList::value_type& layerB)
251  {
252  return layerA->GetPriority() < layerB->GetPriority();
253  };
254 
255  m_Layers.sort(compareLayerPriority);
256 
257  m_LayersInOrder = true;
258  }
259 
260  return *this;
261 }
262 
263 void Graph::AddCompatibilityLayers(std::map<BackendId, std::unique_ptr<IBackendInternal>>& backends,
264  TensorHandleFactoryRegistry& registry)
265 {
266  // Returns true if the given layer could potentially need an intermediate copy/import layer (depending on its
267  // connections to other layers).
268  auto MayNeedCompatibilityLayer = [](const Layer& layer)
269  {
270  // All layers should have been associated with a valid compute device at this point.
271  BOOST_ASSERT(layer.GetBackendId() != Compute::Undefined);
272  // Does not need another compatibility layer if a copy or import layer is already present.
273  return layer.GetType() != LayerType::MemCopy &&
274  layer.GetType() != LayerType::MemImport;
275  };
276 
277  auto IsCompatibilityStrategy = [](EdgeStrategy strategy)
278  {
279  return strategy == EdgeStrategy::CopyToTarget ||
280  strategy == EdgeStrategy::ExportToTarget;
281  };
282 
283  ForEachLayer([this, &backends, &registry, MayNeedCompatibilityLayer, IsCompatibilityStrategy](Layer* srcLayer)
284  {
285  BOOST_ASSERT(srcLayer);
286 
287  if (!MayNeedCompatibilityLayer(*srcLayer))
288  {
289  // The current layer does not need copy layers, move to the next one
290  return;
291  }
292 
293  const std::vector<OutputSlot>& srcOutputSlots = srcLayer->GetOutputSlots();
294  for (unsigned int srcOutputIndex = 0; srcOutputIndex < srcOutputSlots.size(); srcOutputIndex++)
295  {
296  OutputSlot& srcOutputSlot = srcLayer->GetOutputSlot(srcOutputIndex);
297  const std::vector<InputSlot*> srcConnections = srcOutputSlot.GetConnections();
298  const std::vector<EdgeStrategy> srcEdgeStrategies = srcOutputSlot.GetEdgeStrategies();
299  for (unsigned int srcConnectionIndex = 0; srcConnectionIndex < srcConnections.size(); srcConnectionIndex++)
300  {
301  InputSlot* dstInputSlot = srcConnections[srcConnectionIndex];
302  BOOST_ASSERT(dstInputSlot);
303 
304  EdgeStrategy strategy = srcEdgeStrategies[srcConnectionIndex];
305  BOOST_ASSERT_MSG(strategy != EdgeStrategy::Undefined,
306  "Undefined memory strategy found while adding copy layers for compatibility");
307 
308  const Layer& dstLayer = dstInputSlot->GetOwningLayer();
309  if (MayNeedCompatibilityLayer(dstLayer) &&
310  IsCompatibilityStrategy(strategy))
311  {
312  // A copy layer is needed in between the source and destination layers.
313  // Record the operation rather than attempting to modify the graph as we go.
314  // (invalidating iterators)
315  const std::string compLayerName = boost::str(boost::format("[ %1% (%2%) -> %3% (%4%) ]")
316  % srcLayer->GetName()
317  % srcOutputIndex
318  % dstLayer.GetName()
319  % dstInputSlot->GetSlotIndex());
320 
321  Layer* compLayer = nullptr;
322  if (strategy == EdgeStrategy::CopyToTarget)
323  {
324  compLayer = InsertNewLayer<MemCopyLayer>(*dstInputSlot, compLayerName.c_str());
325  }
326  else
327  {
328  BOOST_ASSERT_MSG(strategy == EdgeStrategy::ExportToTarget, "Invalid edge strategy found.");
329  compLayer = InsertNewLayer<MemImportLayer>(*dstInputSlot, compLayerName.c_str());
330  }
331 
332  compLayer->SetBackendId(dstLayer.GetBackendId());
333 
334  OutputSlot& compOutputSlot = compLayer->GetOutputSlot(0);
335  auto backendIt = backends.find(dstLayer.GetBackendId());
336  if (backendIt != backends.end() &&
337  backendIt->second &&
338  backendIt->second->SupportsTensorAllocatorAPI())
339  {
340  auto backend = backendIt->second.get();
341  auto tensorHandleFactoryIds = backend->GetHandleFactoryPreferences();
342  bool found = false;
343 
344  for (auto preference : tensorHandleFactoryIds)
345  {
346  auto factory = registry.GetFactory(preference);
347  if (factory)
348  {
349  auto srcPref = srcOutputSlot.GetTensorHandleFactoryId();
350  auto srcFactory = registry.GetFactory(srcPref);
351 
352  if (srcFactory)
353  {
354  bool canExportImport =
355  (factory->GetImportFlags() & srcFactory->GetExportFlags()) != 0;
356 
357  if (factory->SupportsMapUnmap() || canExportImport)
358  {
359  compOutputSlot.SetTensorHandleFactory(preference);
360  found = true;
361  break;
362  }
363  }
364  }
365  }
366 
367  if (!found)
368  {
370  }
371  }
372  else
373  {
375  }
376 
377  // The output strategy of a compatibility layer is always DirectCompatibility.
379 
380  // Recalculate the connection index on the previous layer as we have just inserted into it.
381  const std::vector<InputSlot*>& newSourceConnections = srcOutputSlot.GetConnections();
382  long newSrcConnectionIndex = std::distance(newSourceConnections.begin(),
383  std::find(newSourceConnections.begin(),
384  newSourceConnections.end(),
385  &compLayer->GetInputSlot(0)));
386 
387  // The input strategy of a compatibility layer is always DirectCompatibilty.
388  srcOutputSlot.SetEdgeStrategy(boost::numeric_cast<unsigned int>(newSrcConnectionIndex),
390  }
391  }
392  }
393  });
394 }
395 
397 {
398  BOOST_ASSERT(substituteLayer != nullptr);
399 
400  ReplaceSubgraphConnections(subgraph, substituteLayer);
401  EraseSubgraphLayers(subgraph);
402 }
403 
404 void Graph::SubstituteSubgraph(SubgraphView& subgraph, const SubgraphView& substituteSubgraph)
405 {
406  // Look through each layer in the new subgraph and add any that are not already a member of this graph
407  substituteSubgraph.ForEachLayer([this](Layer* layer)
408  {
409  if (std::find(std::begin(m_Layers), std::end(m_Layers), layer) == std::end(m_Layers))
410  {
411  layer->Reparent(*this, m_Layers.end());
412  m_LayersInOrder = false;
413  }
414  });
415 
416  ReplaceSubgraphConnections(subgraph, substituteSubgraph);
417  EraseSubgraphLayers(subgraph);
418  TopologicalSort();
419 }
420 
421 void Graph::ReplaceSubgraphConnections(const SubgraphView& subgraph, IConnectableLayer* substituteLayer)
422 {
423  BOOST_ASSERT(substituteLayer != nullptr);
424 
425  // Create a new sub-graph with only the given layer, using
426  // the given sub-graph as a reference of which parent graph to use
427  SubgraphView substituteSubgraph(substituteLayer);
428  ReplaceSubgraphConnections(subgraph, substituteSubgraph);
429 }
430 
431 void Graph::ReplaceSubgraphConnections(const SubgraphView& subgraph, const SubgraphView& substituteSubgraph)
432 {
433  BOOST_ASSERT_MSG(!substituteSubgraph.GetLayers().empty(), "New sub-graph used for substitution must not be empty");
434 
435  const SubgraphView::Layers& substituteSubgraphLayers = substituteSubgraph.GetLayers();
436  std::for_each(substituteSubgraphLayers.begin(), substituteSubgraphLayers.end(), [&](Layer* layer)
437  {
438  IgnoreUnused(layer);
439  BOOST_ASSERT_MSG(std::find(m_Layers.begin(), m_Layers.end(), layer) != m_Layers.end(),
440  "Substitute layer is not a member of graph");
441  });
442 
443  const SubgraphView::InputSlots& subgraphInputSlots = subgraph.GetInputSlots();
444  const SubgraphView::OutputSlots& subgraphOutputSlots = subgraph.GetOutputSlots();
445 
446  unsigned int subgraphNumInputSlots = boost::numeric_cast<unsigned int>(subgraphInputSlots.size());
447  unsigned int subgraphNumOutputSlots = boost::numeric_cast<unsigned int>(subgraphOutputSlots.size());
448 
449  const SubgraphView::InputSlots& substituteSubgraphInputSlots = substituteSubgraph.GetInputSlots();
450  const SubgraphView::OutputSlots& substituteSubgraphOutputSlots = substituteSubgraph.GetOutputSlots();
451 
452  BOOST_ASSERT(subgraphNumInputSlots == substituteSubgraphInputSlots.size());
453  BOOST_ASSERT(subgraphNumOutputSlots == substituteSubgraphOutputSlots.size());
454 
455  // Disconnect the sub-graph and replace it with the substitute sub-graph
456 
457  // Step 1: process input slots
458  for (unsigned int inputSlotIdx = 0; inputSlotIdx < subgraphNumInputSlots; ++inputSlotIdx)
459  {
460  InputSlot* subgraphInputSlot = subgraphInputSlots.at(inputSlotIdx);
461  BOOST_ASSERT(subgraphInputSlot);
462 
463  IOutputSlot* connectedOutputSlot = subgraphInputSlot->GetConnection();
464  BOOST_ASSERT(connectedOutputSlot);
465  connectedOutputSlot->Disconnect(*subgraphInputSlot);
466 
467  IInputSlot* substituteInputSlot = substituteSubgraphInputSlots.at(inputSlotIdx);
468  BOOST_ASSERT(substituteInputSlot);
469  connectedOutputSlot->Connect(*substituteInputSlot);
470  }
471 
472  // Step 2: process output slots
473  for(unsigned int outputSlotIdx = 0; outputSlotIdx < subgraphNumOutputSlots; ++outputSlotIdx)
474  {
475  OutputSlot* subgraphOutputSlot = subgraphOutputSlots.at(outputSlotIdx);
476  BOOST_ASSERT(subgraphOutputSlot);
477 
478  OutputSlot* substituteOutputSlot = substituteSubgraphOutputSlots.at(outputSlotIdx);
479  BOOST_ASSERT(substituteOutputSlot);
480  subgraphOutputSlot->MoveAllConnections(*substituteOutputSlot);
481  }
482 }
483 
484 void Graph::EraseSubgraphLayers(SubgraphView &subgraph)
485 {
486  for (auto layer : subgraph.GetLayers())
487  {
488  EraseLayer(layer);
489  }
490  subgraph.Clear();
491 }
492 
494 {
495  for (auto&& layer : TopologicalSort())
496  {
497  for (auto&& input : layer->GetInputSlots())
498  {
499  const IOutputSlot* source = input.GetConnectedOutputSlot();
500  if (source == NULL)
501  {
502  std::ostringstream message;
503  message << "Input not connected on "
504  << GetLayerTypeAsCString(layer->GetType())
505  << " layer \""
506  << layer->GetName()
507  << "\"";
508  throw LayerValidationException(message.str());
509  }
510 
511  if (!source->IsTensorInfoSet())
512  {
513  throw LayerValidationException("All inputs must have the TensorInfo set at this point.");
514  }
515  }
516  layer->ValidateTensorShapesFromInputs();
517  }
518 }
519 
520 } // namespace armnn
const std::vector< InputSlot * > & GetConnections() const
Definition: Layer.hpp:125
Interface for a layer that is connectable to other layers via InputSlots and OutputSlots.
Definition: INetwork.hpp:61
NodeContent & AddContent(const std::string &content)
void SetEdgeStrategy(unsigned int connectionIndex, EdgeStrategy strategy)
Definition: Layer.cpp:177
No strategy has been defined. Used internally to verify integrity of optimizations.
const TensorShape & GetShape() const
Definition: Tensor.hpp:88
Status SerializeToDot(std::ostream &stream)
Definition: Graph.cpp:81
const std::vector< EdgeStrategy > & GetEdgeStrategies() const
Definition: Layer.hpp:126
virtual void Reparent(Graph &dest, std::list< Layer *>::const_iterator iterator)=0
Layer & GetOwningLayer() const
Definition: Layer.hpp:115
Source backends tensor data can be exported to destination backend tensor without copy...
void EraseLayer(Iterator pos)
Deletes the layer at the specified position.
Definition: Graph.hpp:442
std::vector< OutputSlot * > OutputSlots
DotAttributeSet & GetAttributeSet()
virtual void Allocate()=0
Indicate to the memory manager that this resource is no longer active.
virtual Layer * Clone(Graph &graph) const =0
Creates a dynamically-allocated copy of this layer.
#define ARMNN_LOG(severity)
Definition: Logging.hpp:163
virtual void Manage()=0
Indicate to the memory manager that this resource is active.
Copyright (c) 2020 ARM Limited.
void IgnoreUnused(Ts &&...)
const IOutputSlot * GetConnection() const override
Definition: Layer.hpp:199
Destination backend can work directly with tensors on source backend.
The SubgraphView class represents a subgraph of a Graph.
const InputSlot & GetInputSlot(unsigned int index) const override
Get a const input slot handle by slot index.
Definition: Layer.hpp:310
void ForEachLayer(Func func) const
Definition: Graph.hpp:39
char const * GetLayerTypeAsCString(LayerType type)
DotAttributeSet & AddAttribute(const std::string &name, const std::stringstream &value)
NodeContent & GetContents()
An output connection slot for a layer.
Definition: INetwork.hpp:37
unsigned int GetSlotIndex() const
Definition: Layer.hpp:53
virtual void Disconnect(IInputSlot &slot)=0
virtual ITensorHandle * GetParent() const =0
Get the parent tensor if this is a subtensor.
Status
enumeration
Definition: Types.hpp:26
std::enable_if_t< std::is_unsigned< Source >::value &&std::is_unsigned< Dest >::value, Dest > numeric_cast(Source source)
Definition: NumericCast.hpp:33
Layer & GetOwningLayer() const
Definition: Layer.hpp:52
const BackendId & GetBackendId() const
Definition: Layer.hpp:263
const std::vector< OutputSlot > & GetOutputSlots() const
Definition: Layer.hpp:232
std::vector< InputSlot * > InputSlots
void SubstituteSubgraph(SubgraphView &subgraph, IConnectableLayer *substituteLayer)
Substitutes the given sub-graph with either a new layer or a new sub-graph.
Definition: Graph.cpp:396
void SetTensorHandleFactory(const ITensorHandleFactory::FactoryId &id)
Definition: Layer.cpp:167
const InputSlots & GetInputSlots() const
std::vector< OutputSlot >::iterator BeginOutputSlots()
Definition: Layer.hpp:239
const OutputSlots & GetOutputSlots() const
void InferTensorInfos()
Definition: Graph.cpp:493
ITensorHandleFactory * GetFactory(ITensorHandleFactory::FactoryId id) const
Find a TensorHandleFactory by Id Returns nullptr if not found.
virtual bool IsTensorInfoSet() const =0
const Layers & GetLayers() const
const OutputSlot & GetOutputSlot(unsigned int index=0) const override
Get the const output slot handle by slot index.
Definition: Layer.hpp:312
DotAttributeSet & GetAttributeSet()
const char * GetName() const override
Returns the name of the layer.
Definition: Layer.hpp:305
ITensorHandleFactory::FactoryId GetTensorHandleFactoryId() const
Definition: Layer.cpp:172
Graph & TopologicalSort()
Sorts layers in topological order and return this.
Definition: Graph.hpp:173
virtual int Connect(IInputSlot &destination)=0
void ForEachLayer(Func func) const
std::list< Layer * > Layers
std::function< void(const std::string &name, const std::string &value)> ParameterStringifyFunction
Status AllocateDynamicBuffers()
Allocates memory for all tensors under output tensor handers of each layer.
Definition: Graph.cpp:142
const TensorInfo & GetTensorInfo() const override
Definition: Layer.cpp:63
void AddCompatibilityLayers(std::map< BackendId, std::unique_ptr< class IBackendInternal >> &backends, TensorHandleFactoryRegistry &registry)
Modifies the graph in-place, removing edges connecting layers using different compute devices...
Definition: Graph.cpp:263
Status Print() const
Definition: Graph.cpp:61
static const FactoryId LegacyFactoryId
void MoveAllConnections(OutputSlot &destination)
Moves all connections to another OutputSlot.
Definition: Layer.cpp:112
An input connection slot for a layer.
Definition: INetwork.hpp:24
LayerGuid GetGuid() const final
Returns the unique id of the layer.
Definition: Layer.hpp:316