aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Braun <martin.braun@ettus.com>2019-04-24 18:23:31 -0700
committerMartin Braun <martin.braun@ettus.com>2019-11-26 11:49:14 -0800
commitc97bdc6c94c98753215a90cf499af4bdf06db8e2 (patch)
tree67f623ae84acb045d145bd22036df60a1724b789
parentf0371292a43c3e4e3c68d8631c57d64ab10faf4c (diff)
downloaduhd-c97bdc6c94c98753215a90cf499af4bdf06db8e2.tar.gz
uhd-c97bdc6c94c98753215a90cf499af4bdf06db8e2.tar.bz2
uhd-c97bdc6c94c98753215a90cf499af4bdf06db8e2.zip
rfnoc: Add property propagation, Boost.Graph storage
- Adds a detail::graph_t class, which handles the propagation - Adds methods to node_t to aid with propagation - Adds unit tests - Adds dynamic property forwarding: Nodes are now able to forward properties they don't know about by providing a forwarding policy. A good example is the FIFO block which simply forwards most properties verbatim. - node: Temporarily disabling consistency check at init
-rw-r--r--host/include/uhd/rfnoc/node.hpp265
-rw-r--r--host/include/uhd/rfnoc/node.ipp8
-rw-r--r--host/lib/include/uhdlib/rfnoc/graph.hpp271
-rw-r--r--host/lib/include/uhdlib/rfnoc/node_accessor.hpp5
-rw-r--r--host/lib/rfnoc/CMakeLists.txt1
-rw-r--r--host/lib/rfnoc/graph.cpp460
-rw-r--r--host/lib/rfnoc/node.cpp312
-rw-r--r--host/tests/CMakeLists.txt26
-rw-r--r--host/tests/rfnoc_detailgraph_test.cpp215
-rw-r--r--host/tests/rfnoc_graph_mock_nodes.hpp233
-rw-r--r--host/tests/rfnoc_node_test.cpp58
-rw-r--r--host/tests/rfnoc_propprop_test.cpp366
12 files changed, 2171 insertions, 49 deletions
diff --git a/host/include/uhd/rfnoc/node.hpp b/host/include/uhd/rfnoc/node.hpp
index 5f8a15f4f..1e634ecea 100644
--- a/host/include/uhd/rfnoc/node.hpp
+++ b/host/include/uhd/rfnoc/node.hpp
@@ -8,11 +8,12 @@
#define INCLUDED_LIBUHD_RFNOC_NODE_HPP
#include <uhd/rfnoc/property.hpp>
+#include <uhd/rfnoc/dirtifier.hpp>
#include <uhd/utils/scope_exit.hpp>
#include <uhd/utils/log.hpp>
+#include <boost/graph/adjacency_list.hpp>
#include <unordered_map>
#include <unordered_set>
-#include <boost/optional.hpp>
#include <functional>
#include <memory>
#include <mutex>
@@ -32,6 +33,27 @@ class UHD_API node_t
{
public:
using resolver_fn_t = std::function<void(void)>;
+ using resolve_callback_t = std::function<void(void)>;
+
+ //! Types of property/action forwarding for those not defined by the block itself
+ enum class forwarding_policy_t {
+ //! Forward the property/action to the opposite port with the same index
+ //(e.g., if it comes from input port 0, forward it to output port 0).
+ ONE_TO_ONE,
+ //! Fan-out forwarding: Forward to all opposite ports
+ ONE_TO_FAN,
+ //! Forward the property to all input ports
+ ONE_TO_ALL_IN,
+ //! Forward the property to all output ports
+ ONE_TO_ALL_OUT,
+ //! Forward the property to all ports
+ ONE_TO_ALL,
+ //! Property propagation ends here
+ DROP
+ };
+
+
+ node_t();
/******************************************
* Basic Operations
@@ -121,44 +143,77 @@ protected:
/******************************************
* Internal Registration Functions
******************************************/
+ using prop_ptrs_t = std::unordered_set<property_base_t*>;
/*! Register a property for this block
*
* \param prop A reference to the property
+ * \param clean_callback A callback that gets executed whenever this property
+ * is dirty and gets marked clean
*
* \throws uhd::key_error if another property with the same ID and source
* type is already registered
*/
- void register_property(property_base_t* prop);
+ void register_property(
+ property_base_t* prop, resolve_callback_t&& clean_callback = nullptr);
- /*! Add a resolver function to this block. A resolver function is used to
- * reconcile state changes in the block, and is triggered by a
- * user or other upstream/downstream blocks. A block may have multiple
- * resolvers.
+ /*! Add a resolver function to this block.
+ *
+ * A resolver function is used to reconcile state changes in the block, and
+ * is triggered by a user or other upstream/downstream blocks. A block may
+ * have multiple resolvers.
*
- * NOTE: Multiple resolvers may share properties for reading and a
- * resolver may read multiple properties
- * NOTE: The framework will perform run-time validation to
- * ensure read/write property access is not violated. This means the
- * resolver function can only read values that are specified in the
- * \p inputs list, and only write values that are specified in the
- * \p outputs list.
+ * Notes on resolvers:
+ * - Multiple resolvers may share properties for reading and a resolver may
+ * read multiple properties
+ * - A resolver may assume the properties are in a consistent state before
+ * it executes, but it must leave the properties in a consistent state
+ * when it completes.
+ * - The framework will perform run-time validation to ensure read/write
+ * property access is not violated. All properties can be read during
+ * execution, but only properties in the \p outputs list can be written
+ * to.
*
- * \param inputs The properties that this resolver will read
+ * \param inputs The properties that will cause this resolver to run
* \param outputs The properties that this resolver will write to
* \param resolver_fn The resolver function
+ * \throws uhd::runtime_error if any of the properties listed is not
+ * registered
*/
- void add_property_resolver(std::set<property_base_t*>&& inputs,
- std::set<property_base_t*>&& outputs,
- resolver_fn_t&& resolver_fn);
+ void add_property_resolver(
+ prop_ptrs_t&& inputs, prop_ptrs_t&& outputs, resolver_fn_t&& resolver_fn);
+
+ /**************************************************************************
+ * Property forwarding
+ *************************************************************************/
+ /*! Set a property forwarding policy for dynamic properties
+ *
+ * Whenever this node is asked to handle a property that is not registered,
+ * this is how the node knows what to do with the property. For example, the
+ * FIFO block controller will almost always want to pass on properties to
+ * the next block.
+ *
+ * This method can be called more than once, and it will overwrite previous
+ * policies. However, once a property has been registered with this block,
+ * the policy is set.
+ * Typically, this function should only ever be called from within the
+ * constructor.
+ *
+ * \param policy The policy that is applied (see also forwarding_policy_t).
+ * \param prop_id The property ID that this forwarding policy is applied to.
+ * If \p prop_id is not given, it will apply to all properties,
+ * unless a different policy was given with a matching ID.
+ */
+ void set_prop_forwarding_policy(
+ forwarding_policy_t policy, const std::string& prop_id = "");
/*! Handle a request to perform an action. The default action handler
* ignores user action and forwards port actions.
*
* \param handler The function that is called to handle the action
*/
- //void register_action_handler(std::function<
- //void(const action_info& info, const res_source_info& src)
+ // void register_action_handler(std::function<
+ // void(const action_info& info, const res_source_info& src)
//> handler);
/******************************************
@@ -167,7 +222,11 @@ protected:
// TBW
//
+ //! A dirtifyer object, useful for properties that always need updating.
+ static dirtifier_t ALWAYS_DIRTY;
+
private:
+ friend class node_accessor_t;
/*! Return a reference to a property, if it exists.
*
@@ -177,12 +236,148 @@ private:
res_source_info src_info, const std::string& id) const;
/*! RAII-Style property access
+ *
+ * Returns an object which will grant temporary \p access to the property
+ * \p prop until the returned object goes out of scope.
*/
uhd::utils::scope_exit::uptr _request_property_access(
property_base_t* prop, property_base_t::access_t access) const;
+ /*! Return a set of properties that match a predicate
+ *
+ * Will return an empty set if none match.
+ */
+ template <typename PredicateType>
+ prop_ptrs_t filter_props(PredicateType&& predicate)
+ {
+ prop_ptrs_t filtered_props{};
+ for (const auto& type_prop_pair : _props) {
+ for (const auto& prop : type_prop_pair.second) {
+ if (predicate(prop)) {
+ filtered_props.insert(prop);
+ }
+ }
+ }
+
+ return filtered_props;
+ }
+
+ /*! Set up a new, unknown edge property
+ *
+ * This function is called when forward_edge_property() receives a new
+ * property it doesn't know about. Using the policy set in
+ * set_prop_forwarding_policy(), we figure our which resolvers to set, and
+ * install them.
+ */
+ property_base_t* inject_edge_property(
+ property_base_t* blueprint, res_source_info new_src_info);
+
+ /*! This will run all the resolvers once to put the block into a valid
+ * state. It will execute the following algorithm:
+ *
+ * - Iterate through all resolvers
+ * - For all resolvers, mark its output properties as RWLOCKED (the
+ * assumption is that all other properties are RO).
+ * - Run the resolver. If the default values were inconsistent, this can
+ * cause a uhd::resolve_error.
+ * - Reset the properties to RO and continue with the next resolver.
+ * - When all resolvers have been run, mark all properties as clean.
+ *
+ * \throws uhd::resolve_error if the default values were inconsistent
+ */
+ void init_props();
+
+ /*! This will find dirty properties, and call their respective resolvers.
+ *
+ * It will execute the following algorithm:
+ * - Create set D of all dirty properties
+ * - Create empty set W
+ * - Create a set R of resolvers which have the dirty properties in their
+ * input list
+ * - For ever resolver:
+ * - For all outputs, set the access mode to RWLOCKED if it's in W, or to
+ * RW if it's not
+ * - Run the resolver
+ * - Reset the access modes on all outputs to RO
+ * - Add all outputs to W
+ *
+ * The assumption is that the properties are internally consistent before
+ * this function was called.
+ *
+ * Note: This does not mark any properties as cleaned! All modified outputs
+ * will be marked dirty.
+ *
+ * \throws uhd::resolve_error if the properties could not be resolved. This
+ * typically indicates that the resolvers were set up inconsistently.
+ */
+ void resolve_props();
+
+ /*! This will trigger a graph-wide property resolution
+ */
+ void resolve_all();
+
+ /*! Mark all properties as clean
+ *
+ * When dirty properties have a clean-callback registered, that will also
+ * get triggered.
+ */
+ void clean_props();
+
+ /*! Sets a callback that the framework can call when it needs to trigger a
+ * property resolution.
+ */
+ void set_resolve_all_callback(resolve_callback_t&& resolver)
+ {
+ _resolve_all_cb = resolver;
+ }
+
+ /*! Forward the value of an edge property into this node
+ *
+ * Note that \p incoming_prop is a reference to the neighbouring node's
+ * property. That means if incoming_prop.get_src_info().type == OUTPUT_EDGE,
+ * then this will update a property on this node with the same ID, port
+ * number, but one that has source type INPUT_EDGE.
+ *
+ * This method is meant to be called by the framework during resolution of
+ * properties, and shouldn't be called by the class itself.
+ *
+ * If this method is called with an unknown property, a new dynamic property
+ * is created. Then, the forwarding policy is looked up to make a decision
+ * what to do next:
+ * - forwarding_policy_t::DROP: Nothing happens.
+ * - forwarding_policy_t::ONE_TO_ONE: A new property on the opposite
+ * port is created if it doesn't yet exist. A resolver is registered that
+ * copies the value from one property to another.
+ * If there is no opposite port, then we continue as if the policy had
+ * been DROP.
+ * - forwarding_policy_t::ONE_TO_ALL_IN: New properties on all input
+ * ports are created if they don't yet exist. A resolver is created that
+ * copies from this new property to all inputs.
+ * - forwarding_policy_t::ONE_TO_ALL_OUT: Same as before, except the
+ * property is forwarded to the outputs.
+ * - forwarding_policy_t::ONE_TO_ALL: Same as before, except the
+ * property is forwarded to all ports.
+ *
+ * \param incoming_prop Pointer to the other node's property that is being
+ * forwarded. We read the value from that property, and
+ * check the types match.
+ * \param incoming_port The port on which this property is incoming.
+ *
+ * \throws uhd::type_error if the properties do not have the same type
+ */
+ void forward_edge_property(
+ property_base_t* incoming_prop, const size_t incoming_port);
+
+ /**************************************************************************
+ * Private helpers
+ *************************************************************************/
+ //! Return true if this node has a port that matches \p port_info
+ bool _has_port(const res_source_info& port_info) const;
+
/****** Attributes *******************************************************/
- //! Mutex to lock access to the property registry
+ //! Mutex to lock access to the property registry. Note: This is not the
+ // global property mutex, this only write-protects access to the property-
+ // related containers in this class.
mutable std::mutex _prop_mutex;
//! Stores a reference to every registered property (Property Registry)
@@ -191,13 +386,35 @@ private:
std::hash<size_t> >
_props;
- using property_resolver_t = std::tuple<std::set<property_base_t*>,
- std::set<property_base_t*>,
- resolver_fn_t> ;
+ //! Stores a clean callback for some properties
+ std::unordered_map<property_base_t*, resolve_callback_t> _clean_cb_registry;
+
+ using property_resolver_t = std::tuple<prop_ptrs_t, prop_ptrs_t, resolver_fn_t>;
//! Stores the list of property resolvers
std::vector<property_resolver_t> _prop_resolvers;
-}; // class node_t
+ //! A callback that can be called to notify the graph manager that something
+ // has changed, and that a property resolution needs to be performed.
+ resolve_callback_t _resolve_all_cb = [this]() {
+ resolve_props();
+ clean_props();
+ };
+
+ //! This is permanent storage for all properties that don't get stored
+ // explicitly.
+ //
+ // Dynamic properties include properties defined in the block descriptor
+ // file, as well as new properties that get passed in during property
+ // propagation.
+ std::unordered_set<std::unique_ptr<property_base_t>> _dynamic_props;
+
+ //! Forwarding policy for specific properties
+ //
+ // The entry with the empty-string-key is the default policy.
+ std::unordered_map<std::string, forwarding_policy_t> _prop_fwd_policies{{
+ "", forwarding_policy_t::ONE_TO_ONE}};
+
+}; // class node_t
}} /* namespace uhd::rfnoc */
diff --git a/host/include/uhd/rfnoc/node.ipp b/host/include/uhd/rfnoc/node.ipp
index 7de69063c..62db98243 100644
--- a/host/include/uhd/rfnoc/node.ipp
+++ b/host/include/uhd/rfnoc/node.ipp
@@ -55,7 +55,9 @@ void node_t::set_property(
prop_ptr->set(val);
}
- // resolve_all() TODO
+ // Now trigger a property resolution. If other properties depend on this one,
+ // they will be updated.
+ resolve_all();
}
template <typename prop_data_t>
@@ -63,7 +65,9 @@ const prop_data_t& node_t::get_property(const std::string& id, const size_t inst
{
res_source_info src_info{res_source_info::USER, instance};
- // resolve_all() TODO
+ // First, trigger a property resolution to make sure this property is
+ // updated (if necessary) before reading it out
+ resolve_all();
auto prop_ptr = _assert_prop<prop_data_t>(
_find_property(src_info, id), get_unique_id(), id);
diff --git a/host/lib/include/uhdlib/rfnoc/graph.hpp b/host/lib/include/uhdlib/rfnoc/graph.hpp
new file mode 100644
index 000000000..f9fb7ac41
--- /dev/null
+++ b/host/lib/include/uhdlib/rfnoc/graph.hpp
@@ -0,0 +1,271 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#ifndef INCLUDED_LIBUHD_GRAPH_HPP
+#define INCLUDED_LIBUHD_GRAPH_HPP
+
+#include <uhd/rfnoc/node.hpp>
+#include <boost/graph/adjacency_list.hpp>
+#include <tuple>
+#include <memory>
+
+namespace uhd { namespace rfnoc {
+
+/*! A container that holds information about a graph edge
+ */
+struct graph_edge_t
+{
+ enum edge_t {
+ STATIC, ///< A static connection between two blocks in the FPGA
+ DYNAMIC, ///< A user (dynamic) connection between two blocks in the FPGA
+ RX_STREAM, ///< A connection from an FPGA block to a software RX streamer
+ TX_STREAM ///< A connection from a software TX streamer and an FPGA block
+ };
+
+ graph_edge_t() = default;
+
+ graph_edge_t(const size_t src_port_,
+ const size_t dst_port_,
+ const edge_t edge_,
+ const bool ppa)
+ : src_port(src_port_)
+ , dst_port(dst_port_)
+ , edge(edge_)
+ , property_propagation_active(ppa)
+ {
+ }
+
+ //! The block ID of the source block for this edge
+ std::string src_blockid;
+ //! The port number of the source block for this edge
+ size_t src_port = 0;
+ //! The block ID of the destination block for this edge
+ std::string dst_blockid;
+ //! The port number of the destination block for this edge
+ size_t dst_port = 0;
+ //! The type of edge
+ edge_t edge = DYNAMIC;
+ //! When true, the framework will use this edge for property propagation
+ bool property_propagation_active = true;
+
+ bool operator==(const graph_edge_t& rhs) const
+ {
+ return std::tie(src_blockid,
+ src_port,
+ dst_blockid,
+ dst_port,
+ edge,
+ property_propagation_active)
+ == std::tie(rhs.src_blockid,
+ rhs.src_port,
+ rhs.dst_blockid,
+ rhs.dst_port,
+ rhs.edge,
+ rhs.property_propagation_active);
+ }
+};
+
+
+namespace detail {
+
+//! Container for the logical graph within an uhd::rfnoc_graph
+class graph_t
+{
+public:
+ using uptr = std::unique_ptr<graph_t>;
+ //! A shorthand for a pointer to a node
+ using node_ref_t = uhd::rfnoc::node_t*;
+
+ using graph_edge_t = uhd::rfnoc::graph_edge_t;
+
+ /*! Add a connection to the graph
+ *
+ * After this function returns, the nodes will be considered connected
+ * along the ports specified in \p edge_info.
+ *
+ * \param src_node A reference to the source node
+ * \param dst_node A reference to the destination node
+ * \param edge_info Information about the type of edge
+ */
+ void connect(node_ref_t src_node, node_ref_t dst_node, graph_edge_t edge_info);
+
+ //void disconnect(node_ref_t src_node,
+ //node_ref_t dst_node,
+ //const size_t src_port,
+ //const size_t dst_port);
+ //
+
+ /*! Run initial checks for graph
+ *
+ * This method can be called anytime, but it's intended to be called when
+ * the graph has been committed. It will run checks on the graph and run a
+ * property propagation.
+ *
+ * \throws uhd::resolve_error if the properties fail to resolve.
+ */
+ void initialize();
+
+
+private:
+ friend class graph_accessor_t;
+
+ /**************************************************************************
+ * Graph-related types
+ *************************************************************************/
+ // Naming conventions:
+ // - 'vertex' and 'node' are generally ambiguous in a graph context, but
+ // we'll use vertex for BGL related items, and node for RFNoC nodes
+ // - We may use CamelCase occasionally if it fits the BGL examples and/or
+ // reference designs, in case someone needs to learn BGL to understand
+ // this code
+
+ struct vertex_property_t
+ {
+ enum { num = 4000 };
+ typedef boost::vertex_property_tag kind;
+ };
+ using RfnocVertexProperty = boost::property<vertex_property_t, node_ref_t>;
+
+ struct edge_property_t
+ {
+ enum { num = 4001 };
+ typedef boost::edge_property_tag kind;
+ };
+ using RfnocEdgeProperty = boost::property<edge_property_t, graph_edge_t>;
+
+ /*! The type of the BGL graph we're using
+ *
+ * - It is bidirectional because we need to access both in_edges and
+ * out_edges
+ * - All container types are according to the BGL manual recommendations for
+ * this kind of graph
+ */
+ using rfnoc_graph_t = boost::adjacency_list<boost::vecS,
+ boost::vecS,
+ boost::bidirectionalS,
+ RfnocVertexProperty,
+ RfnocEdgeProperty>;
+
+ using vertex_list_t = std::list<rfnoc_graph_t::vertex_descriptor>;
+
+ template <bool forward_edges_only = true>
+ struct ForwardBackwardEdgePredicate
+ {
+ ForwardBackwardEdgePredicate() {} // Default ctor is required
+ ForwardBackwardEdgePredicate(rfnoc_graph_t& graph) : _graph(&graph) {}
+
+ template <typename Edge>
+ bool operator()(const Edge& e) const
+ {
+ graph_edge_t edge_info = boost::get(edge_property_t(), *_graph, e);
+ return edge_info.property_propagation_active == forward_edges_only;
+ }
+
+ private:
+ // Don't make any attribute const, because default assignment operator
+ // is also required
+ rfnoc_graph_t* _graph;
+ };
+
+ using ForwardEdgePredicate = ForwardBackwardEdgePredicate<true>;
+ using BackEdgePredicate = ForwardBackwardEdgePredicate<false>;
+
+ //! Vertex predicate, only selects nodes with dirty props
+ struct DirtyNodePredicate;
+
+ //! Vertex predicate, returns specific existing nodes
+ struct FindNodePredicate;
+
+ /**************************************************************************
+ * Other private types
+ *************************************************************************/
+ using node_map_t = std::map<node_ref_t, rfnoc_graph_t::vertex_descriptor>;
+
+ /**************************************************************************
+ * The Algorithm
+ *************************************************************************/
+ /*! Implementation of the property propagation algorithm
+ */
+ void resolve_all_properties();
+
+ /**************************************************************************
+ * Private graph helpers
+ *************************************************************************/
+ template <typename VertexContainerType>
+ std::vector<node_ref_t> _vertices_to_nodes(VertexContainerType&& vertex_container)
+ {
+ std::vector<node_ref_t> result{};
+ result.reserve(vertex_container.size());
+ for (const auto& vertex_descriptor : vertex_container) {
+ result.push_back(boost::get(vertex_property_t(), _graph, vertex_descriptor));
+ }
+ return result;
+ }
+
+ /*! Returns a list of all nodes that have dirty properties.
+ */
+ vertex_list_t _find_dirty_nodes();
+
+ /*! Returns nodes in topologically sorted order
+ *
+ *
+ * \throws uhd::runtime_error if the graph was not sortable
+ */
+ vertex_list_t _get_topo_sorted_nodes();
+
+ /*! Add a node, but only if it's not already in the graph.
+ *
+ * If it's already there, do nothing.
+ */
+ void _add_node(node_ref_t node);
+
+ /*! Find the neighbouring node for \p origin based on \p port_info
+ *
+ * This function will check port_info to identify the port number and the
+ * direction (input or output) from \p port_info. It will then return a
+ * reference to the node that is attached to the node \p origin if such a
+ * node exists, and the edge info.
+ *
+ * If port_info.type == res_source_info::INPUT_EDGE, then port_info.instance
+ * will equal the return value's dst_port value.
+ *
+ * \returns A valid reference to the neighbouring node, or nullptr if no
+ * such node exists, and the corresponding edge info.
+ */
+ std::pair<node_ref_t, graph_edge_t> _find_neighbour(
+ rfnoc_graph_t::vertex_descriptor origin, res_source_info port_info);
+
+ /*! Forward all edge properties from this node (\p origin) to the
+ * neighbouring ones
+ *
+ */
+ void _forward_edge_props(rfnoc_graph_t::vertex_descriptor origin);
+
+ /*! Check that the edge properties on both sides of the edge are equal
+ *
+ * \returns false if edge properties are not consistent
+ */
+ bool _assert_edge_props_consistent(rfnoc_graph_t::edge_descriptor edge);
+
+ /**************************************************************************
+ * Attributes
+ *************************************************************************/
+ //! Storage for the actual graph
+ rfnoc_graph_t _graph;
+
+ //! Map to do a lookup node_ref_t -> vertex descriptor.
+ //
+ // This is technically redundant, but helps us check quickly and easily if
+ // a node is already in the graph, and to yank out the appropriate node
+ // descriptor without having to traverse the graph. The rfnoc_graph_t is not
+ // efficient for lookups of vertices.
+ node_map_t _node_map;
+};
+
+
+}}} /* namespace uhd::rfnoc::detail */
+
+#endif /* INCLUDED_LIBUHD_GRAPH_HPP */
diff --git a/host/lib/include/uhdlib/rfnoc/node_accessor.hpp b/host/lib/include/uhdlib/rfnoc/node_accessor.hpp
index 26e6a5607..554cc8f4f 100644
--- a/host/lib/include/uhdlib/rfnoc/node_accessor.hpp
+++ b/host/lib/include/uhdlib/rfnoc/node_accessor.hpp
@@ -72,9 +72,10 @@ public:
*
* See node_t::forward_edge_property() for details.
*/
- void forward_edge_property(node_t* dst_node, property_base_t* incoming_prop)
+ void forward_edge_property(
+ node_t* dst_node, const size_t dst_port, property_base_t* incoming_prop)
{
- dst_node->forward_edge_property(incoming_prop);
+ dst_node->forward_edge_property(incoming_prop, dst_port);
}
};
diff --git a/host/lib/rfnoc/CMakeLists.txt b/host/lib/rfnoc/CMakeLists.txt
index bc3e7309f..9b14f3456 100644
--- a/host/lib/rfnoc/CMakeLists.txt
+++ b/host/lib/rfnoc/CMakeLists.txt
@@ -20,6 +20,7 @@ LIBUHD_APPEND_SOURCES(
${CMAKE_CURRENT_SOURCE_DIR}/block_id.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ctrl_iface.cpp
${CMAKE_CURRENT_SOURCE_DIR}/graph_impl.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/graph.cpp
${CMAKE_CURRENT_SOURCE_DIR}/legacy_compat.cpp
${CMAKE_CURRENT_SOURCE_DIR}/noc_block_base.cpp
${CMAKE_CURRENT_SOURCE_DIR}/node_ctrl_base.cpp
diff --git a/host/lib/rfnoc/graph.cpp b/host/lib/rfnoc/graph.cpp
new file mode 100644
index 000000000..f90f70b43
--- /dev/null
+++ b/host/lib/rfnoc/graph.cpp
@@ -0,0 +1,460 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/exception.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhdlib/rfnoc/graph.hpp>
+#include <uhdlib/rfnoc/node_accessor.hpp>
+#include <boost/graph/topological_sort.hpp>
+#include <boost/graph/filtered_graph.hpp>
+#include <utility>
+
+using namespace uhd::rfnoc;
+using namespace uhd::rfnoc::detail;
+
+namespace {
+
+const std::string LOG_ID = "RFNOC::GRAPH::DETAIL";
+
+/*! Helper function to pretty-print edge info
+ */
+std::string print_edge(
+ graph_t::node_ref_t src, graph_t::node_ref_t dst, graph_t::graph_edge_t edge_info)
+{
+ return src->get_unique_id() + ":" + std::to_string(edge_info.src_port) + " -> "
+ + dst->get_unique_id() + ":" + std::to_string(edge_info.dst_port);
+}
+
+/*! Return a list of dirty properties from a node
+ */
+auto get_dirty_props(graph_t::node_ref_t node_ref)
+{
+ using namespace uhd::rfnoc;
+ node_accessor_t node_accessor{};
+ return node_accessor.filter_props(node_ref, [](property_base_t* prop) {
+ return prop->is_dirty()
+ && prop->get_src_info().type != res_source_info::FRAMEWORK;
+ });
+}
+
+/*! Check that \p new_edge_info does not conflict with \p existing_edge_info
+ *
+ * \throws uhd::rfnoc_error if it does.
+ */
+void assert_edge_new(const graph_t::graph_edge_t& new_edge_info,
+ const graph_t::graph_edge_t& existing_edge_info)
+{
+ if (existing_edge_info == new_edge_info) {
+ UHD_LOG_INFO(LOG_ID,
+ "Ignoring repeated call to connect "
+ << new_edge_info.src_blockid << ":" << new_edge_info.src_port << " -> "
+ << new_edge_info.dst_blockid << ":" << new_edge_info.dst_port);
+ return;
+ } else if (existing_edge_info.src_port == new_edge_info.src_port
+ && existing_edge_info.src_blockid == new_edge_info.src_blockid
+ && existing_edge_info.dst_port == new_edge_info.dst_port
+ && existing_edge_info.dst_blockid == new_edge_info.dst_blockid) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Caught attempt to modify properties of edge "
+ << existing_edge_info.src_blockid << ":" << existing_edge_info.src_port
+ << " -> " << existing_edge_info.dst_blockid << ":"
+ << existing_edge_info.dst_port);
+ throw uhd::rfnoc_error("Caught attempt to modify properties of edge!");
+ } else if (new_edge_info.src_blockid == existing_edge_info.src_blockid
+ && new_edge_info.src_port == existing_edge_info.src_port) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Attempting to reconnect output port " << existing_edge_info.src_blockid
+ << ":" << existing_edge_info.src_port);
+ throw uhd::rfnoc_error("Attempting to reconnect output port!");
+ } else if (new_edge_info.dst_blockid == existing_edge_info.dst_blockid
+ && new_edge_info.dst_port == existing_edge_info.dst_port) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Attempting to reconnect output port " << existing_edge_info.dst_blockid
+ << ":" << existing_edge_info.dst_port);
+ throw uhd::rfnoc_error("Attempting to reconnect input port!");
+ }
+}
+
+} // namespace
+
+/*! Graph-filtering predicate to find dirty nodes only
+ */
+struct graph_t::DirtyNodePredicate
+{
+ DirtyNodePredicate() {} // Default ctor is required
+ DirtyNodePredicate(graph_t::rfnoc_graph_t& graph) : _graph(&graph) {}
+
+ template <typename Vertex>
+ bool operator()(const Vertex& v) const
+ {
+ return !get_dirty_props(boost::get(graph_t::vertex_property_t(), *_graph, v))
+ .empty();
+ }
+
+private:
+ // Don't make any attribute const, because default assignment operator
+ // is also required
+ graph_t::rfnoc_graph_t* _graph;
+};
+
+/******************************************************************************
+ * Public API calls
+ *****************************************************************************/
+void graph_t::connect(node_ref_t src_node, node_ref_t dst_node, graph_edge_t edge_info)
+{
+ node_accessor_t node_accessor{};
+ UHD_LOG_TRACE(LOG_ID,
+ "Connecting block " << src_node->get_unique_id() << ":" << edge_info.src_port
+ << " -> " << dst_node->get_unique_id() << ":"
+ << edge_info.dst_port);
+
+ // Correctly populate edge_info
+ edge_info.src_blockid = src_node->get_unique_id();
+ edge_info.dst_blockid = dst_node->get_unique_id();
+
+ // Set resolver callbacks:
+ node_accessor.set_resolve_all_callback(
+ src_node, [this]() { this->resolve_all_properties(); });
+ node_accessor.set_resolve_all_callback(
+ dst_node, [this]() { this->resolve_all_properties(); });
+
+ // Add nodes to graph, if not already in there:
+ _add_node(src_node);
+ _add_node(dst_node);
+ // Find vertex descriptors
+ auto src_vertex_desc = _node_map.at(src_node);
+ auto dst_vertex_desc = _node_map.at(dst_node);
+
+ // Check if connection exists
+ // This can be optimized: Edges can appear in both out_edges and in_edges,
+ // and we could skip double-checking them.
+ auto out_edge_range = boost::out_edges(src_vertex_desc, _graph);
+ for (auto edge_it = out_edge_range.first; edge_it != out_edge_range.second;
+ ++edge_it) {
+ assert_edge_new(edge_info, boost::get(edge_property_t(), _graph, *edge_it));
+ }
+ auto in_edge_range = boost::in_edges(dst_vertex_desc, _graph);
+ for (auto edge_it = in_edge_range.first; edge_it != in_edge_range.second; ++edge_it) {
+ assert_edge_new(edge_info, boost::get(edge_property_t(), _graph, *edge_it));
+ }
+
+ // Create edge
+ auto edge_descriptor =
+ boost::add_edge(src_vertex_desc, dst_vertex_desc, edge_info, _graph);
+ UHD_ASSERT_THROW(edge_descriptor.second);
+
+ // Now make sure we didn't add an unintended cycle
+ try {
+ _get_topo_sorted_nodes();
+ } catch (const uhd::rfnoc_error&) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Adding edge " << src_node->get_unique_id() << ":" << edge_info.src_port
+ << " -> " << dst_node->get_unique_id() << ":"
+ << edge_info.dst_port
+ << " without disabling property_propagation_active will lead "
+ "to unresolvable graph!");
+ boost::remove_edge(edge_descriptor.first, _graph);
+ throw uhd::rfnoc_error(
+ "Adding edge without disabling property_propagation_active will lead "
+ "to unresolvable graph!");
+ }
+}
+
+void graph_t::initialize()
+{
+ UHD_LOG_DEBUG(LOG_ID, "Initializing graph.");
+ resolve_all_properties();
+}
+
+
+/******************************************************************************
+ * Private methods to be called by friends
+ *****************************************************************************/
+void graph_t::resolve_all_properties()
+{
+ if (boost::num_vertices(_graph) == 0) {
+ return;
+ }
+ node_accessor_t node_accessor{};
+
+ // First, find the node on which we'll start.
+ auto initial_dirty_nodes = _find_dirty_nodes();
+ if (initial_dirty_nodes.size() > 1) {
+ UHD_LOGGER_WARNING(LOG_ID)
+ << "Found " << initial_dirty_nodes.size()
+ << " dirty nodes in initial search (expected one or zero). "
+ "Property propagation may resolve this.";
+ for (auto& vertex : initial_dirty_nodes) {
+ node_ref_t node = boost::get(vertex_property_t(), _graph, vertex);
+ UHD_LOG_WARNING(LOG_ID, "Dirty: " << node->get_unique_id());
+ }
+ }
+ if (initial_dirty_nodes.empty()) {
+ UHD_LOG_DEBUG(LOG_ID,
+ "In resolve_all_properties(): No dirty properties found. Starting on "
+ "arbitrary node.");
+ initial_dirty_nodes.push_back(*boost::vertices(_graph).first);
+ }
+ UHD_ASSERT_THROW(!initial_dirty_nodes.empty());
+ auto initial_node = initial_dirty_nodes.front();
+
+ // Now get all nodes in topologically sorted order, and the appropriate
+ // iterators.
+ auto topo_sorted_nodes = _get_topo_sorted_nodes();
+ auto node_it = topo_sorted_nodes.begin();
+ auto begin_it = topo_sorted_nodes.begin();
+ auto end_it = topo_sorted_nodes.end();
+ while (*node_it != initial_node) {
+ // We know *node_it must be == initial_node at some point, because
+ // otherwise, initial_dirty_nodes would have been empty
+ node_it++;
+ }
+
+ // Start iterating over nodes
+ bool forward_dir = true;
+ int num_iterations = 0;
+ // If all edge properties were known at the beginning, a single iteration
+ // would suffice. However, usually during the first time the property
+ // propagation is run, blocks create new (dynamic) edge properties that
+ // default to dirty. If we had a way of knowing when that happens, we could
+ // dynamically increase the number of iterations during the loop. For now,
+ // we simply hard-code the number of iterations to 2 so that we catch that
+ // case without any additional complications.
+ constexpr int MAX_NUM_ITERATIONS = 2;
+ while (true) {
+ node_ref_t current_node = boost::get(vertex_property_t(), _graph, *node_it);
+ UHD_LOG_TRACE(
+ LOG_ID, "Now resolving next node: " << current_node->get_unique_id());
+
+ // On current node, call local resolution. This may cause other
+ // properties to become dirty.
+ node_accessor.resolve_props(current_node);
+
+ // Forward all edge props in all directions from current node. We make
+ // sure to skip properties if the edge is flagged as
+ // !property_propagation_active
+ _forward_edge_props(*node_it);
+
+ // Now mark all properties on this node as clean
+ node_accessor.clean_props(current_node);
+
+ // The rest of the code in this loop is to figure out who's the next
+ // node. First, increment (or decrement) iterator:
+ if (forward_dir) {
+ node_it++;
+ // If we're at the end, flip the direction
+ if (node_it == end_it) {
+ forward_dir = false;
+ // Back off from the sentinel:
+ node_it--;
+ }
+ }
+ if (!forward_dir) {
+ if (topo_sorted_nodes.size() > 1) {
+ node_it--;
+ // If we're back at the front, flip direction
+ if (node_it == begin_it) {
+ forward_dir = true;
+ }
+ } else {
+ forward_dir = true;
+ }
+ }
+ // If we're going forward, and the next node is the initial node,
+ // we've gone full circle (one full iteration).
+ if (forward_dir && (*node_it == initial_node)) {
+ num_iterations++;
+ if (num_iterations == MAX_NUM_ITERATIONS) {
+ UHD_LOG_TRACE(LOG_ID,
+ "Terminating graph resolution after iteration " << num_iterations);
+ break;
+ }
+ }
+ }
+
+ // Post-iteration sanity checks:
+ // First, we make sure that there are no dirty properties left. If there are,
+ // that means our algorithm couldn't converge and we have a problem.
+ auto remaining_dirty_nodes = _find_dirty_nodes();
+ if (!remaining_dirty_nodes.empty()) {
+ UHD_LOG_ERROR(LOG_ID, "The following properties could not be resolved:");
+ for (auto& vertex : remaining_dirty_nodes) {
+ node_ref_t node = boost::get(vertex_property_t(), _graph, vertex);
+ const std::string node_id = node->get_unique_id();
+ auto dirty_props = get_dirty_props(node);
+ for (auto& prop : dirty_props) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Dirty: " << node_id << "[" << prop->get_src_info().to_string() << " "
+ << prop->get_id() << "]");
+ }
+ }
+ throw uhd::resolve_error("Could not resolve properties.");
+ }
+
+ // Second, go through edges marked !property_propagation_active and make
+ // sure that they match up
+ BackEdgePredicate back_edge_filter(_graph);
+ auto e_iterators =
+ boost::edges(boost::filtered_graph<rfnoc_graph_t, BackEdgePredicate>(
+ _graph, back_edge_filter));
+ bool back_edges_valid = true;
+ for (auto e_it = e_iterators.first; e_it != e_iterators.second; ++e_it) {
+ back_edges_valid = back_edges_valid && _assert_edge_props_consistent(*e_it);
+ }
+ if (!back_edges_valid) {
+ throw uhd::resolve_error(
+ "Error during property resultion: Back-edges inconsistent!");
+ }
+}
+
+/******************************************************************************
+ * Private methods
+ *****************************************************************************/
+graph_t::vertex_list_t graph_t::_find_dirty_nodes()
+{
+ // Create a view on the graph that doesn't include the back-edges
+ DirtyNodePredicate vertex_filter(_graph);
+ boost::filtered_graph<rfnoc_graph_t, boost::keep_all, DirtyNodePredicate> fg(
+ _graph, boost::keep_all(), vertex_filter);
+
+ auto v_iterators = boost::vertices(fg);
+ return vertex_list_t(v_iterators.first, v_iterators.second);
+}
+
+graph_t::vertex_list_t graph_t::_get_topo_sorted_nodes()
+{
+ // Create a view on the graph that doesn't include the back-edges
+ ForwardEdgePredicate edge_filter(_graph);
+ boost::filtered_graph<rfnoc_graph_t, ForwardEdgePredicate> fg(_graph, edge_filter);
+
+ // Topo-sort and return
+ vertex_list_t sorted_nodes;
+ try {
+ boost::topological_sort(fg, std::front_inserter(sorted_nodes));
+ } catch (boost::not_a_dag&) {
+ throw uhd::rfnoc_error("Cannot resolve graph because it has at least one cycle!");
+ }
+ return sorted_nodes;
+}
+
+void graph_t::_add_node(node_ref_t new_node)
+{
+ if (_node_map.count(new_node)) {
+ return;
+ }
+
+ _node_map.emplace(new_node, boost::add_vertex(new_node, _graph));
+}
+
+
+void graph_t::_forward_edge_props(graph_t::rfnoc_graph_t::vertex_descriptor origin)
+{
+ node_accessor_t node_accessor{};
+ node_ref_t origin_node = boost::get(vertex_property_t(), _graph, origin);
+
+ auto edge_props = node_accessor.filter_props(origin_node, [](property_base_t* prop) {
+ return (prop->get_src_info().type == res_source_info::INPUT_EDGE
+ || prop->get_src_info().type == res_source_info::OUTPUT_EDGE);
+ });
+ UHD_LOG_TRACE(LOG_ID,
+ "Forwarding up to " << edge_props.size() << " edge properties from node "
+ << origin_node->get_unique_id());
+
+ for (auto prop : edge_props) {
+ auto neighbour_node_info = _find_neighbour(origin, prop->get_src_info());
+ if (neighbour_node_info.first != nullptr
+ && neighbour_node_info.second.property_propagation_active) {
+ const size_t neighbour_port = prop->get_src_info().type
+ == res_source_info::INPUT_EDGE
+ ? neighbour_node_info.second.src_port
+ : neighbour_node_info.second.dst_port;
+ node_accessor.forward_edge_property(
+ neighbour_node_info.first, neighbour_port, prop);
+ }
+ }
+}
+
+bool graph_t::_assert_edge_props_consistent(rfnoc_graph_t::edge_descriptor edge)
+{
+ node_ref_t src_node =
+ boost::get(vertex_property_t(), _graph, boost::source(edge, _graph));
+ node_ref_t dst_node =
+ boost::get(vertex_property_t(), _graph, boost::target(edge, _graph));
+ graph_edge_t edge_info = boost::get(edge_property_t(), _graph, edge);
+
+ // Helper function to get properties as maps
+ auto get_prop_map = [](const size_t port,
+ res_source_info::source_t edge_type,
+ node_ref_t node) {
+ node_accessor_t node_accessor{};
+ // Create a set of all properties
+ auto props_set = node_accessor.filter_props(
+ node, [port, edge_type, node](property_base_t* prop) {
+ return prop->get_src_info().instance == port
+ && prop->get_src_info().type == edge_type;
+ });
+ std::unordered_map<std::string, property_base_t*> prop_map;
+ for (auto prop_it = props_set.begin(); prop_it != props_set.end(); ++prop_it) {
+ prop_map.emplace((*prop_it)->get_id(), *prop_it);
+ }
+
+ return prop_map;
+ };
+
+ // Create two maps ID -> prop_ptr, so we have an easier time comparing them
+ auto src_prop_map =
+ get_prop_map(edge_info.src_port, res_source_info::OUTPUT_EDGE, src_node);
+ auto dst_prop_map =
+ get_prop_map(edge_info.dst_port, res_source_info::INPUT_EDGE, dst_node);
+
+ // Now iterate through all properties, and make sure they match
+ bool props_match = true;
+ for (auto src_prop_it = src_prop_map.begin(); src_prop_it != src_prop_map.end();
+ ++src_prop_it) {
+ auto src_prop = src_prop_it->second;
+ auto dst_prop = dst_prop_map.at(src_prop->get_id());
+ if (!src_prop->equal(dst_prop)) {
+ UHD_LOG_ERROR(LOG_ID,
+ "Edge property " << src_prop->get_id() << " inconsistent on edge "
+ << print_edge(src_node, dst_node, edge_info));
+ props_match = false;
+ }
+ }
+
+ return props_match;
+}
+
+std::pair<graph_t::node_ref_t, graph_t::graph_edge_t> graph_t::_find_neighbour(
+ rfnoc_graph_t::vertex_descriptor origin, res_source_info port_info)
+{
+ if (port_info.type == res_source_info::INPUT_EDGE) {
+ auto it_range = boost::in_edges(origin, _graph);
+ for (auto it = it_range.first; it != it_range.second; ++it) {
+ graph_edge_t edge_info = boost::get(edge_property_t(), _graph, *it);
+ if (edge_info.dst_port == port_info.instance) {
+ return {
+ boost::get(vertex_property_t(), _graph, boost::source(*it, _graph)),
+ edge_info};
+ }
+ }
+ return {nullptr, {}};
+ }
+ if (port_info.type == res_source_info::OUTPUT_EDGE) {
+ auto it_range = boost::out_edges(origin, _graph);
+ for (auto it = it_range.first; it != it_range.second; ++it) {
+ graph_edge_t edge_info = boost::get(edge_property_t(), _graph, *it);
+ if (edge_info.src_port == port_info.instance) {
+ return {
+ boost::get(vertex_property_t(), _graph, boost::target(*it, _graph)),
+ edge_info};
+ }
+ }
+ return {nullptr, {}};
+ }
+
+ UHD_THROW_INVALID_CODE_PATH();
+}
+
diff --git a/host/lib/rfnoc/node.cpp b/host/lib/rfnoc/node.cpp
index 68ba5e283..0b724b889 100644
--- a/host/lib/rfnoc/node.cpp
+++ b/host/lib/rfnoc/node.cpp
@@ -4,12 +4,23 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
+#include <uhd/exception.hpp>
#include <uhd/rfnoc/node.hpp>
+#include <uhd/utils/log.hpp>
#include <uhdlib/rfnoc/prop_accessor.hpp>
#include <boost/format.hpp>
+#include <algorithm>
+#include <iostream>
using namespace uhd::rfnoc;
+dirtifier_t node_t::ALWAYS_DIRTY{};
+
+
+node_t::node_t()
+{
+ register_property(&ALWAYS_DIRTY);
+}
std::string node_t::get_unique_id() const
{
@@ -35,7 +46,7 @@ std::vector<std::string> node_t::get_property_ids() const
}
/*** Protected methods *******************************************************/
-void node_t::register_property(property_base_t* prop)
+void node_t::register_property(property_base_t* prop, resolve_callback_t&& clean_callback)
{
std::lock_guard<std::mutex> _l(_prop_mutex);
@@ -60,11 +71,13 @@ void node_t::register_property(property_base_t* prop)
}
_props[src_type].push_back(prop);
+ if (clean_callback) {
+ _clean_cb_registry[prop] = std::move(clean_callback);
+ }
}
-void node_t::add_property_resolver(std::set<property_base_t*>&& inputs,
- std::set<property_base_t*>&& outputs,
- resolver_fn_t&& resolver_fn)
+void node_t::add_property_resolver(
+ prop_ptrs_t&& inputs, prop_ptrs_t&& outputs, resolver_fn_t&& resolver_fn)
{
std::lock_guard<std::mutex> _l(_prop_mutex);
@@ -88,14 +101,21 @@ void node_t::add_property_resolver(std::set<property_base_t*>&& inputs,
}
// All good, we can store it
- _prop_resolvers.push_back(std::make_tuple(
- std::forward<std::set<property_base_t*>>(inputs),
- std::forward<std::set<property_base_t*>>(outputs),
- std::forward<resolver_fn_t>(resolver_fn)));
+ _prop_resolvers.push_back(std::make_tuple(std::forward<prop_ptrs_t>(inputs),
+ std::forward<prop_ptrs_t>(outputs),
+ std::forward<resolver_fn_t>(resolver_fn)));
+}
+
+
+void node_t::set_prop_forwarding_policy(
+ forwarding_policy_t policy, const std::string& prop_id)
+{
+ _prop_fwd_policies[prop_id] = policy;
}
/*** Private methods *********************************************************/
-property_base_t* node_t::_find_property(res_source_info src_info, const std::string& id) const
+property_base_t* node_t::_find_property(
+ res_source_info src_info, const std::string& id) const
{
for (const auto& type_prop_pair : _props) {
if (type_prop_pair.first != src_info.type) {
@@ -117,3 +137,277 @@ uhd::utils::scope_exit::uptr node_t::_request_property_access(
return prop_accessor_t{}.get_scoped_prop_access(*prop, access);
}
+
+property_base_t* node_t::inject_edge_property(
+ property_base_t* blueprint, res_source_info new_src_info)
+{
+ // Check if a property already exists which matches the new property
+ // requirements. If so, we can return early:
+ auto new_prop = _find_property(new_src_info, blueprint->get_id());
+ if (new_prop) {
+ return new_prop;
+ }
+
+ // We need to create a new property and stash it away:
+ new_prop = [&]() -> property_base_t* {
+ auto prop = blueprint->clone(new_src_info);
+ auto ptr = prop.get();
+ _dynamic_props.emplace(std::move(prop));
+ return ptr;
+ }();
+ register_property(new_prop);
+
+ // Collect some info on how to do the forwarding:
+ const auto fwd_policy = [&](const std::string& id) {
+ if (_prop_fwd_policies.count(id)) {
+ return _prop_fwd_policies.at(id);
+ }
+ return _prop_fwd_policies.at("");
+ }(new_prop->get_id());
+ const size_t port_idx = new_prop->get_src_info().instance;
+ const auto port_type = new_prop->get_src_info().type;
+ UHD_ASSERT_THROW(port_type == res_source_info::INPUT_EDGE
+ || port_type == res_source_info::OUTPUT_EDGE);
+
+ // Now comes the hard part: Figure out which other properties need to be
+ // created, and which resolvers need to be instantiated
+ if (fwd_policy == forwarding_policy_t::ONE_TO_ONE) {
+ // Figure out if there's an opposite port
+ const auto opposite_port_type = res_source_info::invert_edge(port_type);
+ if (_has_port({opposite_port_type, port_idx})) {
+ // Make sure that the other side's property exists:
+ // This is a safe recursion, because we've already created and
+ // registered this property.
+ auto opposite_prop =
+ inject_edge_property(new_prop, {opposite_port_type, port_idx});
+ // Now add a resolver that will always forward the value from this
+ // property to the other one.
+ add_property_resolver(
+ {new_prop}, {opposite_prop}, [new_prop, opposite_prop]() {
+ prop_accessor_t{}.forward<false>(new_prop, opposite_prop);
+ });
+ }
+ }
+ if (fwd_policy == forwarding_policy_t::ONE_TO_FAN) {
+ const auto opposite_port_type = res_source_info::invert_edge(port_type);
+ const size_t num_ports = opposite_port_type == res_source_info::INPUT_EDGE
+ ? get_num_input_ports()
+ : get_num_output_ports();
+ for (size_t i = 0; i < num_ports; i++) {
+ auto opposite_prop =
+ inject_edge_property(new_prop, {opposite_port_type, i});
+ // Now add a resolver that will always forward the value from this
+ // property to the other one.
+ add_property_resolver(
+ {new_prop}, {opposite_prop}, [new_prop, opposite_prop]() {
+ prop_accessor_t{}.forward<false>(new_prop, opposite_prop);
+ });
+ }
+ }
+ if (fwd_policy == forwarding_policy_t::ONE_TO_ALL
+ || fwd_policy == forwarding_policy_t::ONE_TO_ALL_IN) {
+ // Loop through all other ports, make sure those properties exist
+ for (size_t other_port_idx = 0; other_port_idx < get_num_input_ports();
+ other_port_idx++) {
+ if (port_type == res_source_info::INPUT_EDGE && other_port_idx == port_idx) {
+ continue;
+ }
+ inject_edge_property(new_prop, {res_source_info::INPUT_EDGE, other_port_idx});
+ }
+ // Now add a dynamic resolver that will update all input properties.
+ // In order to keep this code simple, we bypass the write list and
+ // get access via the prop_accessor.
+ add_property_resolver({new_prop}, {/* empty */}, [this, new_prop, port_idx]() {
+ for (size_t other_port_idx = 0; other_port_idx < get_num_input_ports();
+ other_port_idx++) {
+ if (other_port_idx == port_idx) {
+ continue;
+ }
+ auto prop =
+ _find_property({res_source_info::INPUT_EDGE, other_port_idx},
+ new_prop->get_id());
+ if (prop) {
+ prop_accessor_t{}.forward<false>(new_prop, prop);
+ }
+ }
+ });
+ }
+ if (fwd_policy == forwarding_policy_t::ONE_TO_ALL
+ || fwd_policy == forwarding_policy_t::ONE_TO_ALL_OUT) {
+ // Loop through all other ports, make sure those properties exist
+ for (size_t other_port_idx = 0; other_port_idx < get_num_output_ports();
+ other_port_idx++) {
+ if (port_type == res_source_info::OUTPUT_EDGE && other_port_idx == port_idx) {
+ continue;
+ }
+ inject_edge_property(new_prop, {res_source_info::OUTPUT_EDGE, other_port_idx});
+ }
+ // Now add a dynamic resolver that will update all input properties.
+ // In order to keep this code simple, we bypass the write list and
+ // get access via the prop_accessor.
+ add_property_resolver({new_prop}, {/* empty */}, [this, new_prop, port_idx]() {
+ for (size_t other_port_idx = 0; other_port_idx < get_num_input_ports();
+ other_port_idx++) {
+ if (other_port_idx == port_idx) {
+ continue;
+ }
+ auto prop =
+ _find_property({res_source_info::OUTPUT_EDGE, other_port_idx},
+ new_prop->get_id());
+ if (prop) {
+ prop_accessor_t{}.forward<false>(new_prop, prop);
+ }
+ }
+ });
+ }
+
+ return new_prop;
+}
+
+
+void node_t::init_props()
+{
+ std::lock_guard<std::mutex> _l(_prop_mutex);
+
+ prop_accessor_t prop_accessor{};
+
+ for (auto& resolver_tuple : _prop_resolvers) {
+ // 1) Set all outputs to RWLOCKED
+ auto& outputs = std::get<1>(resolver_tuple);
+ for (auto& output : outputs) {
+ prop_accessor.set_access(output, property_base_t::RWLOCKED);
+ }
+
+ // 2) Run the resolver
+ try {
+ std::get<2>(resolver_tuple)();
+ } catch (const uhd::resolve_error& ex) {
+ UHD_LOGGER_WARNING(get_unique_id())
+ << "Failed to initialize node. Most likely cause: Inconsistent default "
+ "values. Resolver threw this error: "
+ << ex.what();
+ //throw uhd::runtime_error(std::string("Failed to initialize node ") + get_unique_id());
+ }
+
+ // 3) Set outputs back to RO
+ for (auto& output : outputs) {
+ prop_accessor.set_access(output, property_base_t::RO);
+ }
+ }
+
+ // 4) Mark properties as clean
+ clean_props();
+}
+
+
+void node_t::resolve_props()
+{
+ prop_accessor_t prop_accessor{};
+ const prop_ptrs_t dirty_props =
+ filter_props([](property_base_t* prop) { return prop->is_dirty(); });
+ prop_ptrs_t written_props{};
+ UHD_LOG_TRACE(get_unique_id(),
+ "Locally resolving " << dirty_props.size() << " dirty properties.");
+
+ // Helper to determine if any element from inputs is in dirty_props
+ auto in_dirty_props = [&dirty_props](const prop_ptrs_t inputs) {
+ return std::any_of(
+ inputs.cbegin(), inputs.cend(), [&dirty_props](property_base_t* prop) {
+ return dirty_props.count(prop) != 1;
+ });
+ };
+
+ for (auto& resolver_tuple : _prop_resolvers) {
+ auto& inputs = std::get<0>(resolver_tuple);
+ auto& outputs = std::get<1>(resolver_tuple);
+ if (in_dirty_props(inputs)) {
+ continue;
+ }
+
+ // Enable outputs
+ std::vector<uhd::utils::scope_exit::uptr> access_holder;
+ access_holder.reserve(outputs.size());
+ for (auto& output : outputs) {
+ access_holder.emplace_back(prop_accessor.get_scoped_prop_access(*output,
+ written_props.count(output) ? property_base_t::access_t::RWLOCKED
+ : property_base_t::access_t::RW));
+ }
+
+ // Run resolver
+ std::get<2>(resolver_tuple)();
+
+ // Take note of outputs
+ written_props.insert(outputs.cbegin(), outputs.cend());
+
+ // RW or RWLOCKED gets released here as access_holder goes out of scope.
+ }
+}
+
+void node_t::resolve_all()
+{
+ _resolve_all_cb();
+}
+
+
+void node_t::clean_props()
+{
+ prop_accessor_t prop_accessor{};
+ for (const auto& type_prop_pair : _props) {
+ for (const auto& prop : type_prop_pair.second) {
+ if (prop->is_dirty() && _clean_cb_registry.count(prop)) {
+ _clean_cb_registry.at(prop)();
+ }
+ prop_accessor.mark_clean(*prop);
+ }
+ }
+}
+
+
+void node_t::forward_edge_property(
+ property_base_t* incoming_prop, const size_t incoming_port)
+{
+ UHD_ASSERT_THROW(
+ incoming_prop->get_src_info().type == res_source_info::INPUT_EDGE
+ || incoming_prop->get_src_info().type == res_source_info::OUTPUT_EDGE);
+ UHD_LOG_TRACE(get_unique_id(),
+ "Incoming edge property: `" << incoming_prop->get_id() << "`, source info: "
+ << incoming_prop->get_src_info().to_string());
+
+ // The source type of my local prop (it's the opposite of the source type
+ // of incoming_prop)
+ const auto prop_src_type =
+ res_source_info::invert_edge(incoming_prop->get_src_info().type);
+ // Set of local properties that match incoming_prop. It can be an empty set,
+ // or, if the node is misconfigured, a set with more than one entry. Or, if
+ // all is as expected, it's a set with a single entry.
+ auto local_prop_set = filter_props([prop_src_type, incoming_prop, incoming_port](
+ property_base_t* prop) -> bool {
+ return prop->get_src_info().type == prop_src_type
+ && prop->get_src_info().instance == incoming_port
+ && prop->get_id() == incoming_prop->get_id();
+ });
+
+ // If there is no such property, we're forwarding a new property
+ if (local_prop_set.empty()) {
+ UHD_LOG_TRACE(get_unique_id(),
+ "Received unknown incoming edge prop: " << incoming_prop->get_id());
+ local_prop_set.emplace(
+ inject_edge_property(incoming_prop, {prop_src_type, incoming_port}));
+ }
+ // There must be either zero results, or one
+ UHD_ASSERT_THROW(local_prop_set.size() == 1);
+
+ auto local_prop = *local_prop_set.begin();
+
+ prop_accessor_t prop_accessor{};
+ prop_accessor.forward<false>(incoming_prop, local_prop);
+}
+
+bool node_t::_has_port(const res_source_info& port_info) const
+{
+ return (port_info.type == res_source_info::INPUT_EDGE
+ && port_info.instance <= get_num_input_ports())
+ || (port_info.type == res_source_info::OUTPUT_EDGE
+ && port_info.instance <= get_num_output_ports());
+}
+
diff --git a/host/tests/CMakeLists.txt b/host/tests/CMakeLists.txt
index eab27833b..551b57bb9 100644
--- a/host/tests/CMakeLists.txt
+++ b/host/tests/CMakeLists.txt
@@ -208,6 +208,32 @@ UHD_ADD_NONAPI_TEST(
${CMAKE_SOURCE_DIR}/lib/utils/pathslib.cpp
)
+add_executable(rfnoc_propprop_test
+ rfnoc_propprop_test.cpp
+ ${CMAKE_SOURCE_DIR}/lib/rfnoc/graph.cpp
+)
+target_link_libraries(rfnoc_propprop_test uhd ${Boost_LIBRARIES})
+UHD_ADD_TEST(rfnoc_propprop_test rfnoc_propprop_test)
+UHD_INSTALL(TARGETS
+ rfnoc_propprop_test
+ RUNTIME
+ DESTINATION ${PKG_LIB_DIR}/tests
+ COMPONENT tests
+)
+
+add_executable(rfnoc_detailgraph_test
+ rfnoc_detailgraph_test.cpp
+ ${CMAKE_SOURCE_DIR}/lib/rfnoc/graph.cpp
+)
+target_link_libraries(rfnoc_detailgraph_test uhd ${Boost_LIBRARIES})
+UHD_ADD_TEST(rfnoc_detailgraph_test rfnoc_detailgraph_test)
+UHD_INSTALL(TARGETS
+ rfnoc_detailgraph_test
+ RUNTIME
+ DESTINATION ${PKG_LIB_DIR}/tests
+ COMPONENT tests
+)
+
########################################################################
# demo of a loadable module
########################################################################
diff --git a/host/tests/rfnoc_detailgraph_test.cpp b/host/tests/rfnoc_detailgraph_test.cpp
new file mode 100644
index 000000000..6273430e6
--- /dev/null
+++ b/host/tests/rfnoc_detailgraph_test.cpp
@@ -0,0 +1,215 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include <uhd/rfnoc/node.hpp>
+#include <uhd/utils/log.hpp>
+#include <uhdlib/rfnoc/node_accessor.hpp>
+#include <uhdlib/rfnoc/prop_accessor.hpp>
+#include <uhdlib/rfnoc/graph.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+#include "rfnoc_graph_mock_nodes.hpp"
+
+using uhd::rfnoc::detail::graph_t;
+using namespace uhd::rfnoc;
+
+namespace uhd { namespace rfnoc { namespace detail {
+
+/*! Helper class to access internals of detail::graph
+ *
+ * This is basically a cheat code to get around the 'private' part of graph_t.
+ */
+class graph_accessor_t
+{
+public:
+ using vertex_descriptor = graph_t::rfnoc_graph_t::vertex_descriptor;
+
+ graph_accessor_t(graph_t* graph_ptr) : _graph_ptr(graph_ptr)
+ { /* nop */
+ }
+
+ graph_t::rfnoc_graph_t& get_graph()
+ {
+ return _graph_ptr->_graph;
+ }
+
+ template <typename VertexIterator>
+ graph_t::node_ref_t get_node_ref_from_iterator(VertexIterator it)
+ {
+ return boost::get(graph_t::vertex_property_t(), get_graph(), *it);
+ }
+
+ auto find_neighbour(vertex_descriptor origin, res_source_info port_info)
+ {
+ return _graph_ptr->_find_neighbour(origin, port_info);
+ }
+
+ auto find_dirty_nodes()
+ {
+ return _graph_ptr->_find_dirty_nodes();
+ }
+
+ auto get_topo_sorted_nodes()
+ {
+ return _graph_ptr->_vertices_to_nodes(_graph_ptr->_get_topo_sorted_nodes());
+ }
+private:
+ graph_t* _graph_ptr;
+};
+
+}}};
+
+BOOST_AUTO_TEST_CASE(test_graph)
+{
+ graph_t graph{};
+ uhd::rfnoc::detail::graph_accessor_t graph_accessor(&graph);
+ node_accessor_t node_accessor{};
+
+ auto& bgl_graph = graph_accessor.get_graph();
+
+ // Define some mock nodes:
+ // Source radio
+ mock_radio_node_t mock_rx_radio(0);
+ // Sink radio
+ mock_radio_node_t mock_tx_radio(1);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_rx_radio);
+ node_accessor.init_props(&mock_tx_radio);
+
+ // In this simple graph, all connections are identical from an edge info
+ // perspective, so we're lazy and share an edge_info object:
+ uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
+ edge_info.src_port = 0;
+ edge_info.dst_port = 0;
+ edge_info.property_propagation_active = true;
+ edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;
+
+ // Now create the graph:
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
+
+ // A whole bunch of low-level checks first:
+ BOOST_CHECK_EQUAL(boost::num_vertices(bgl_graph), 2);
+ auto vertex_iterators = boost::vertices(bgl_graph);
+ auto vertex_iterator = vertex_iterators.first;
+ auto rx_descriptor = *vertex_iterator;
+ graph_t::node_ref_t node_ref =
+ graph_accessor.get_node_ref_from_iterator(vertex_iterator++);
+ BOOST_CHECK_EQUAL(node_ref->get_unique_id(), mock_rx_radio.get_unique_id());
+ auto tx_descriptor = *vertex_iterator;
+ node_ref = graph_accessor.get_node_ref_from_iterator(vertex_iterator++);
+ BOOST_CHECK_EQUAL(node_ref->get_unique_id(), mock_tx_radio.get_unique_id());
+ BOOST_CHECK(vertex_iterator == vertex_iterators.second);
+
+ auto rx_neighbour_info =
+ graph_accessor.find_neighbour(rx_descriptor, {res_source_info::OUTPUT_EDGE, 0});
+ BOOST_REQUIRE(rx_neighbour_info.first);
+ BOOST_CHECK_EQUAL(
+ rx_neighbour_info.first->get_unique_id(), mock_tx_radio.get_unique_id());
+ BOOST_CHECK(std::tie(rx_neighbour_info.second.src_port,
+ rx_neighbour_info.second.dst_port,
+ rx_neighbour_info.second.property_propagation_active)
+ == std::tie(edge_info.src_port,
+ edge_info.dst_port,
+ edge_info.property_propagation_active));
+
+ auto tx_neighbour_info =
+ graph_accessor.find_neighbour(tx_descriptor, {res_source_info::INPUT_EDGE, 0});
+ BOOST_REQUIRE(tx_neighbour_info.first);
+ BOOST_CHECK_EQUAL(
+ tx_neighbour_info.first->get_unique_id(), mock_rx_radio.get_unique_id());
+ BOOST_CHECK(std::tie(tx_neighbour_info.second.src_port,
+ tx_neighbour_info.second.dst_port,
+ tx_neighbour_info.second.property_propagation_active)
+ == std::tie(edge_info.src_port,
+ edge_info.dst_port,
+ edge_info.property_propagation_active));
+
+ auto rx_upstream_neighbour_info =
+ graph_accessor.find_neighbour(rx_descriptor, {res_source_info::INPUT_EDGE, 0});
+ BOOST_CHECK(rx_upstream_neighbour_info.first == nullptr);
+ auto tx_downstream_neighbour_info =
+ graph_accessor.find_neighbour(tx_descriptor, {res_source_info::OUTPUT_EDGE, 0});
+ BOOST_CHECK(tx_downstream_neighbour_info.first == nullptr);
+ auto rx_wrongport_neighbour_info =
+ graph_accessor.find_neighbour(rx_descriptor, {res_source_info::OUTPUT_EDGE, 1});
+ BOOST_CHECK(rx_wrongport_neighbour_info.first == nullptr);
+ auto tx_wrongport_neighbour_info =
+ graph_accessor.find_neighbour(tx_descriptor, {res_source_info::INPUT_EDGE, 1});
+ BOOST_CHECK(tx_wrongport_neighbour_info.first == nullptr);
+
+ // Check there are no dirty nodes (init_props() will clean them all)
+ BOOST_CHECK_EQUAL(graph_accessor.find_dirty_nodes().empty(), true);
+
+ auto topo_sorted_nodes = graph_accessor.get_topo_sorted_nodes();
+ BOOST_CHECK_EQUAL(topo_sorted_nodes.size(), 2);
+ BOOST_CHECK_EQUAL(
+ topo_sorted_nodes.at(0)->get_unique_id(), mock_rx_radio.get_unique_id());
+
+ // Now initialize the graph (will force a call to resolve_all_properties())
+ graph.initialize();
+
+ // This will be ignored
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
+ BOOST_CHECK_EQUAL(boost::num_vertices(bgl_graph), 2);
+
+ // Now attempt illegal connections (they must all fail)
+ edge_info.src_port = 1;
+ edge_info.dst_port = 0;
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error);
+ edge_info.src_port = 0;
+ edge_info.dst_port = 1;
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error);
+ edge_info.src_port = 0;
+ edge_info.dst_port = 0;
+ edge_info.property_propagation_active = false;
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error);
+}
+
+BOOST_AUTO_TEST_CASE(test_graph_unresolvable)
+{
+ graph_t graph{};
+ node_accessor_t node_accessor{};
+
+ // Define some mock nodes:
+ // Source radio
+ mock_radio_node_t mock_rx_radio(0);
+ // Sink radio
+ mock_radio_node_t mock_tx_radio(1);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_rx_radio);
+ node_accessor.init_props(&mock_tx_radio);
+
+ // In this simple graph, all connections are identical from an edge info
+ // perspective, so we're lazy and share an edge_info object:
+ uhd::rfnoc::detail::graph_t::graph_edge_t edge_info(
+ 0, 0, graph_t::graph_edge_t::DYNAMIC, true);
+
+ // Now create the graph and commit:
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
+ graph.initialize();
+
+ // Now set a property that will cause the graph to fail to resolve:
+ BOOST_REQUIRE_THROW(mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0),
+ uhd::resolve_error);
+
+ // Now we add a back-edge
+ edge_info.src_port = 0;
+ edge_info.dst_port = 0;
+ edge_info.property_propagation_active = false;
+ graph.connect(&mock_tx_radio, &mock_rx_radio, edge_info);
+ UHD_LOG_INFO("TEST", "Testing back edge error path");
+ mock_tx_radio.disable_samp_out_resolver = true;
+ // The set_property would be valid if we hadn't futzed with the back-edge
+ BOOST_REQUIRE_THROW(mock_tx_radio.set_property<double>("master_clock_rate", 200e6, 0),
+ uhd::resolve_error);
+ UHD_LOG_INFO("TEST", "^^^ Expected ERROR here.");
+}
diff --git a/host/tests/rfnoc_graph_mock_nodes.hpp b/host/tests/rfnoc_graph_mock_nodes.hpp
new file mode 100644
index 000000000..85e667ebd
--- /dev/null
+++ b/host/tests/rfnoc_graph_mock_nodes.hpp
@@ -0,0 +1,233 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#ifndef INCLUDED_LIBUHD_TESTS_MOCK_NODES_HPP
+#define INCLUDED_LIBUHD_TESTS_MOCK_NODES_HPP
+
+#include <uhd/rfnoc/node.hpp>
+
+using namespace uhd::rfnoc;
+
+constexpr int MAX_DECIM = 512;
+constexpr double DEFAULT_RATE = 1e9;
+constexpr int DEFAULT_DECIM = 1;
+
+/*! Mock Radio node
+ *
+ * - "Full Duplex"
+ * - Has two master clock rates: 100e6 and 200e6
+ * - RSSI is a read-only prop that always needs updating
+ */
+class mock_radio_node_t : public node_t
+{
+public:
+ mock_radio_node_t(const size_t radio_idx) : _radio_idx(radio_idx)
+ {
+ register_property(&_samp_rate_in);
+ register_property(&_samp_rate_out);
+ register_property(&_master_clock_rate);
+ register_property(&_rssi);
+
+ // Resolver for the input rate: We don't actually try and be clever, we
+ // always reset the rate back to the TX rate.
+ add_property_resolver({&_samp_rate_in},
+ {&_samp_rate_in},
+ [& samp_rate_in = _samp_rate_in,
+ &master_clock_rate = _master_clock_rate,
+ this]() {
+ UHD_LOG_INFO(get_unique_id(), " Calling resolver for `samp_rate_in'...");
+ samp_rate_in = master_clock_rate.get();
+ });
+ add_property_resolver({&_samp_rate_out},
+ {&_samp_rate_out},
+ [this]() {
+ UHD_LOG_INFO(get_unique_id(), " Calling resolver for `samp_rate_out'...");
+ if (this->disable_samp_out_resolver) {
+ _samp_rate_out = this->force_samp_out_value;
+ UHD_LOG_DEBUG(get_unique_id(),
+ "Forcing samp_rate_out to " << _samp_rate_out.get());
+ return;
+ }
+ this->_samp_rate_out = this->_master_clock_rate.get();
+ });
+ add_property_resolver({&_master_clock_rate},
+ {&_master_clock_rate, &_samp_rate_in, &_samp_rate_out},
+ [& samp_rate_out = _samp_rate_out,
+ &samp_rate_in = _samp_rate_in,
+ &master_clock_rate = _master_clock_rate,
+ this]() {
+ UHD_LOG_INFO(get_unique_id(), " Calling resolver for `master_clock_rate'...");
+ if (_master_clock_rate.get() > 150e6) {
+ _master_clock_rate = 200e6;
+ } else {
+ _master_clock_rate = 100e6;
+ }
+ _samp_rate_in = _master_clock_rate.get();
+ if (!this->disable_samp_out_resolver) {
+ _samp_rate_out = _master_clock_rate.get();
+ } else {
+ _samp_rate_out = this->force_samp_out_value;
+ UHD_LOG_DEBUG(get_unique_id(),
+ "Forcing samp_rate_out to " << _samp_rate_out.get());
+ }
+ });
+ // By depending on ALWAYS_DIRTY, this property is always updated:
+ add_property_resolver({&ALWAYS_DIRTY},
+ {&_rssi},
+ [this]() {
+ UHD_LOG_INFO(get_unique_id(), " Calling resolver for `rssi'...");
+ rssi_resolver_count++;
+ _rssi = static_cast<double>(rssi_resolver_count);
+ });
+ }
+
+ std::string get_unique_id() const { return "MOCK_RADIO" + std::to_string(_radio_idx); }
+
+ size_t get_num_input_ports() const
+ {
+ return 1;
+ }
+
+ size_t get_num_output_ports() const
+ {
+ return 1;
+ }
+
+ // Some public attributes that help debugging
+ size_t rssi_resolver_count = 0;
+ bool disable_samp_out_resolver = false;
+ double force_samp_out_value = 23e6;
+
+private:
+ const size_t _radio_idx;
+
+ property_t<double> _samp_rate_in{
+ "samp_rate", 200e6, {res_source_info::INPUT_EDGE}};
+ property_t<double> _samp_rate_out{
+ "samp_rate", 200e6, {res_source_info::OUTPUT_EDGE}};
+ property_t<double> _master_clock_rate{
+ "master_clock_rate", 200e6, {res_source_info::USER}};
+ property_t<double> _rssi{"rssi", 0, {res_source_info::USER}};
+};
+
+/*! Mock DDC node
+ *
+ * - Single channel
+ * - Does simple coercion of decimation
+ * - Keeps output and input rates consistent with decimation
+ */
+class mock_ddc_node_t : public node_t
+{
+public:
+ mock_ddc_node_t()
+ {
+ register_property(&_samp_rate_in);
+ register_property(&_samp_rate_out);
+ register_property(&_decim);
+
+ // Resolver for _decim: This gets executed when the user directly
+ // modifies _decim. The desired behaviour is to coerce it first, then
+ // keep the input rate constant, and re-calculate the output rate.
+ add_property_resolver({&_decim},
+ {&_decim, &_samp_rate_out},
+ [& decim = _decim,
+ &samp_rate_out = _samp_rate_out,
+ &samp_rate_in = _samp_rate_in]() {
+ UHD_LOG_INFO("MOCK DDC", "Calling resolver for `decim'...");
+ decim = coerce_decim(decim.get());
+ samp_rate_out = samp_rate_in.get() / decim.get();
+ });
+ // Resolver for the input rate: We try and match decim so that the output
+ // rate is not modified. If decim needs to be coerced, only then the
+ // output rate is modified.
+ add_property_resolver({&_samp_rate_in},
+ {&_decim, &_samp_rate_out},
+ [& decim = _decim,
+ &samp_rate_out = _samp_rate_out,
+ &samp_rate_in = _samp_rate_in]() {
+ UHD_LOG_INFO("MOCK DDC", "Calling resolver for `samp_rate_in'...");
+ decim = coerce_decim(int(samp_rate_in.get() / samp_rate_out.get()));
+ samp_rate_out = samp_rate_in.get() / decim.get();
+ });
+ // Resolver for the output rate: Like the previous one, but flipped.
+ add_property_resolver({&_samp_rate_out},
+ {&_decim, &_samp_rate_in},
+ [& decim = _decim,
+ &samp_rate_out = _samp_rate_out,
+ &samp_rate_in = _samp_rate_in]() {
+ UHD_LOG_INFO("MOCK DDC", "Calling resolver for `samp_rate_out'...");
+ decim = coerce_decim(int(samp_rate_in.get() / samp_rate_out.get()));
+ samp_rate_in = samp_rate_out.get() * decim.get();
+ });
+ }
+
+ std::string get_unique_id() const { return "MOCK_DDC"; }
+
+ size_t get_num_input_ports() const
+ {
+ return 1;
+ }
+
+ size_t get_num_output_ports() const
+ {
+ return 1;
+ }
+
+ // Simplified coercer: Let's pretend like we can hit all even rates or 1
+ // for all rates <= MAX_DECIM
+ static int coerce_decim(const int requested_decim)
+ {
+ if (requested_decim <= 1) {
+ return 1;
+ }
+ return std::min(requested_decim - (requested_decim % 2), MAX_DECIM);
+ }
+
+
+ // We make the properties global so we can inspect them, but that's not what
+ // your supposed to do. However, we do keep the underscore notation, since that's
+ // what they be called if they were in the class like they're supposed to.
+ property_t<double> _samp_rate_in{
+ "samp_rate", DEFAULT_RATE, {res_source_info::INPUT_EDGE}};
+ property_t<double> _samp_rate_out{
+ "samp_rate", DEFAULT_RATE, {res_source_info::OUTPUT_EDGE}};
+ property_t<int> _decim{"decim", DEFAULT_DECIM, {res_source_info::USER}};
+
+private:
+ // This is where you normally put the properties
+};
+
+
+/*! FIFO
+ *
+ * Not much here -- we use it to test dynamic prop forwarding.
+ */
+class mock_fifo_t : public node_t
+{
+public:
+ mock_fifo_t(const size_t num_ports) : _num_ports(num_ports)
+ {
+ set_prop_forwarding_policy(forwarding_policy_t::ONE_TO_ONE);
+ }
+
+ std::string get_unique_id() const { return "MOCK_FIFO"; }
+
+ size_t get_num_input_ports() const
+ {
+ return _num_ports;
+ }
+
+ size_t get_num_output_ports() const
+ {
+ return _num_ports;
+ }
+
+
+private:
+ const size_t _num_ports;
+};
+
+#endif /* INCLUDED_LIBUHD_TESTS_MOCK_NODES_HPP */
diff --git a/host/tests/rfnoc_node_test.cpp b/host/tests/rfnoc_node_test.cpp
index 046f8ab33..15597de16 100644
--- a/host/tests/rfnoc_node_test.cpp
+++ b/host/tests/rfnoc_node_test.cpp
@@ -5,6 +5,7 @@
//
#include <uhd/rfnoc/node.hpp>
+#include <uhdlib/rfnoc/node_accessor.hpp>
#include <boost/test/unit_test.hpp>
#include <iostream>
@@ -16,13 +17,17 @@ public:
test_node_t(size_t num_inputs, size_t num_outputs)
: _num_input_ports(num_inputs), _num_output_ports(num_outputs)
{
- register_property(&_double_prop_user);
+ register_property(&_double_prop_user, [this]() {
+ std::cout << "Calling clean callback for user prop" << std::endl;
+ this->user_prop_cb_called = true;
+ });
register_property(&_double_prop_in);
register_property(&_double_prop_out);
// A property with a simple 1:1 dependency
- add_property_resolver(
- {&_double_prop_user}, {&_double_prop_out}, []() { std::cout << "foo" << std::endl; });
+ add_property_resolver({&_double_prop_user}, {&_double_prop_out}, []() {
+ std::cout << "Executing user prop resolver" << std::endl;
+ });
}
//! Register a property for the second time, with the goal of triggering an
@@ -33,35 +38,46 @@ public:
}
//! Register an identical property for the first time, with the goal of
- //triggering an exception
+ // triggering an exception
void double_register_input()
{
- property_t<double> double_prop_in{"double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}};
+ property_t<double> double_prop_in{
+ "double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}};
register_property(&double_prop_in);
}
//! This should throw an error because the property in the output isn't
// registered
- void add_unregistered_resolver_in() {
+ void add_unregistered_resolver_in()
+ {
property_t<double> temp{"temp", 0.0, {res_source_info::INPUT_EDGE, 5}};
- add_property_resolver(
- {&temp}, {}, []() { std::cout << "foo" << std::endl; });
+ add_property_resolver({&temp}, {}, []() { std::cout << "foo" << std::endl; });
}
//! This should throw an error because the property in the output isn't
// registered
- void add_unregistered_resolver_out() {
+ void add_unregistered_resolver_out()
+ {
property_t<double> temp{"temp", 0.0, {res_source_info::INPUT_EDGE, 5}};
add_property_resolver(
{&_double_prop_user}, {&temp}, []() { std::cout << "foo" << std::endl; });
}
- size_t get_num_input_ports() const { return _num_input_ports; }
- size_t get_num_output_ports() const { return _num_output_ports; }
+ size_t get_num_input_ports() const
+ {
+ return _num_input_ports;
+ }
+ size_t get_num_output_ports() const
+ {
+ return _num_output_ports;
+ }
+
+ bool user_prop_cb_called = false;
private:
property_t<double> _double_prop_user{"double_prop", 0.0, {res_source_info::USER}};
- property_t<double> _double_prop_in{"double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}};
+ property_t<double> _double_prop_in{
+ "double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}};
property_t<double> _double_prop_out{
"double_prop", 0.0, {res_source_info::OUTPUT_EDGE, 1}};
@@ -106,3 +122,21 @@ BOOST_AUTO_TEST_CASE(test_node_prop_access)
BOOST_CHECK_EQUAL(TN1.get_property<double>("double_prop"), 4.2);
}
+BOOST_AUTO_TEST_CASE(test_node_accessor)
+{
+ test_node_t TN1(2, 3);
+ node_accessor_t node_accessor{};
+
+ auto user_props = node_accessor.filter_props(&TN1, [](property_base_t* prop) {
+ return (prop->get_src_info().type == res_source_info::USER);
+ });
+
+ BOOST_CHECK_EQUAL(user_props.size(), 1);
+ BOOST_CHECK_EQUAL((*user_props.begin())->get_id(), "double_prop");
+ BOOST_CHECK((*user_props.begin())->get_src_info().type == res_source_info::USER);
+
+ BOOST_CHECK(!TN1.user_prop_cb_called);
+ node_accessor.init_props(&TN1);
+ BOOST_CHECK(TN1.user_prop_cb_called);
+
+}
diff --git a/host/tests/rfnoc_propprop_test.cpp b/host/tests/rfnoc_propprop_test.cpp
new file mode 100644
index 000000000..1b9b94b05
--- /dev/null
+++ b/host/tests/rfnoc_propprop_test.cpp
@@ -0,0 +1,366 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#include "rfnoc_graph_mock_nodes.hpp"
+#include <uhd/utils/log.hpp>
+#include <uhdlib/rfnoc/graph.hpp>
+#include <uhdlib/rfnoc/node_accessor.hpp>
+#include <uhdlib/rfnoc/prop_accessor.hpp>
+#include <boost/test/unit_test.hpp>
+#include <iostream>
+
+/*! Mock invalid node
+ *
+ * This block has an output prop that is always twice the input prop. This block
+ * is invalid because the defaults don't work.
+ */
+class mock_invalid_node1_t : public node_t
+{
+public:
+ mock_invalid_node1_t()
+ {
+ register_property(&_in);
+ register_property(&_out);
+
+ add_property_resolver({&_in}, {&_out}, [this]() { _out = _in * 2; });
+ add_property_resolver({&_out}, {&_in}, [this]() { _in = _out / 2; });
+ }
+
+ std::string get_unique_id() const
+ {
+ return "MOCK_INVALID_NODE1";
+ }
+
+ size_t get_num_input_ports() const
+ {
+ return 1;
+ }
+
+ size_t get_num_output_ports() const
+ {
+ return 1;
+ }
+
+private:
+ property_t<double> _in{"in", 1.0, {res_source_info::INPUT_EDGE}};
+ // This has an invalid default value: It would have to be 2.0 for the block
+ // to be able to initialize
+ property_t<double> _out{"out", 1.0 /* SIC */, {res_source_info::OUTPUT_EDGE}};
+};
+
+/*! Mock invalid node
+ *
+ * This block will write conflicting values to the output at resolution time.
+ */
+class mock_invalid_node2_t : public node_t
+{
+public:
+ mock_invalid_node2_t()
+ {
+ register_property(&_in);
+ register_property(&_out);
+
+ add_property_resolver({&_in}, {&_out}, [this]() {
+ UHD_LOG_INFO("MOCK2", "Calling resolver 1/2 for _out");
+ _out = _in * 2.0;
+ });
+ // If this->factor != 2.0, then this resolver will contradict the
+ // previous one:
+ add_property_resolver({&_in}, {&_out}, [this]() {
+ UHD_LOG_INFO("MOCK2", "Calling resolver 2/2 for _out");
+ _out = _in * this->factor;
+ });
+ add_property_resolver({&_out}, {&_in}, [this]() {
+ UHD_LOG_INFO("MOCK2", "Calling resolver for _in");
+ _in = _out / 2.0;
+ });
+ }
+
+ void mark_in_dirty()
+ {
+ prop_accessor_t prop_accessor{};
+ auto access_lock = prop_accessor.get_scoped_prop_access(_in, property_base_t::RW);
+ double old_val = _in.get();
+ _in.set(old_val * 2.0);
+ _in.set(old_val);
+ }
+
+ size_t get_num_input_ports() const
+ {
+ return 1;
+ }
+
+ size_t get_num_output_ports() const
+ {
+ return 1;
+ }
+
+ std::string get_unique_id() const
+ {
+ return "MOCK_INVALID_NODE2";
+ }
+
+ // When we change this, we break resolver #2.
+ double factor = 2.0;
+
+private:
+ property_t<double> _in{"in", 1.0, {res_source_info::INPUT_EDGE}};
+ property_t<double> _out{"out", 2.0, {res_source_info::OUTPUT_EDGE}};
+};
+
+// Do some sanity checks on the mock just so we don't get surprised later
+BOOST_AUTO_TEST_CASE(test_mock)
+{
+ BOOST_CHECK_EQUAL(1, mock_ddc_node_t::coerce_decim(1));
+ BOOST_CHECK_EQUAL(2, mock_ddc_node_t::coerce_decim(2));
+ BOOST_CHECK_EQUAL(512, mock_ddc_node_t::coerce_decim(1212));
+ BOOST_CHECK_EQUAL(512, mock_ddc_node_t::coerce_decim(513));
+ BOOST_CHECK_EQUAL(2, mock_ddc_node_t::coerce_decim(3));
+
+ mock_ddc_node_t mock{};
+ BOOST_CHECK(mock._decim.is_dirty());
+ BOOST_CHECK(mock._samp_rate_out.is_dirty());
+ BOOST_CHECK(mock._samp_rate_in.is_dirty());
+ BOOST_CHECK_EQUAL(mock._decim.get(), DEFAULT_DECIM);
+ BOOST_CHECK_EQUAL(mock._samp_rate_out.get(), DEFAULT_RATE);
+ BOOST_CHECK_EQUAL(mock._samp_rate_in.get(), DEFAULT_RATE);
+}
+
+BOOST_AUTO_TEST_CASE(test_init_and_resolve)
+{
+ mock_ddc_node_t mock_ddc{};
+ mock_radio_node_t mock_radio(0);
+ node_accessor_t node_accessor{};
+
+ node_accessor.init_props(&mock_ddc);
+ node_accessor.init_props(&mock_radio);
+
+ BOOST_CHECK(!mock_ddc._decim.is_dirty());
+ BOOST_CHECK(!mock_ddc._samp_rate_out.is_dirty());
+ BOOST_CHECK(!mock_ddc._samp_rate_in.is_dirty());
+ BOOST_CHECK_EQUAL(mock_ddc._decim.get(), DEFAULT_DECIM);
+ BOOST_CHECK_EQUAL(mock_ddc._samp_rate_out.get(), DEFAULT_RATE);
+ BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get(), DEFAULT_RATE);
+
+ BOOST_CHECK_EQUAL(mock_ddc.get_property<int>("decim", 0), DEFAULT_DECIM);
+
+ mock_ddc.set_property("decim", 2, 0);
+ BOOST_CHECK(!mock_ddc._decim.is_dirty());
+ node_accessor.resolve_props(&mock_ddc);
+
+ BOOST_CHECK_EQUAL(mock_ddc.get_property<int>("decim", 0), 2);
+ BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get(), DEFAULT_RATE);
+ BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get() / 2, mock_ddc._samp_rate_out.get());
+}
+
+BOOST_AUTO_TEST_CASE(test_failures)
+{
+ node_accessor_t node_accessor{};
+
+ UHD_LOG_INFO("TEST", "We expect an ERROR log message next:");
+ mock_invalid_node1_t mock1{};
+ // BOOST_REQUIRE_THROW(
+ // node_accessor.init_props(&mock1),
+ // uhd::runtime_error);
+
+ mock_invalid_node2_t mock2{};
+ node_accessor.init_props(&mock2);
+ mock2.factor = 1.0;
+ mock2.mark_in_dirty();
+ BOOST_REQUIRE_THROW(node_accessor.resolve_props(&mock2), uhd::resolve_error);
+}
+
+BOOST_AUTO_TEST_CASE(test_graph_resolve_ddc_radio)
+{
+ node_accessor_t node_accessor{};
+ uhd::rfnoc::detail::graph_t graph{};
+ // Define some mock nodes:
+ mock_ddc_node_t mock_ddc{};
+ // Source radio
+ mock_radio_node_t mock_rx_radio(0);
+ // Sink radio
+ mock_radio_node_t mock_tx_radio(1);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_ddc);
+ node_accessor.init_props(&mock_tx_radio);
+ node_accessor.init_props(&mock_rx_radio);
+
+ // In this simple graph, all connections are identical from an edge info
+ // perspective, so we're lazy and share an edge_info object:
+ uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
+ edge_info.src_port = 0;
+ edge_info.dst_port = 0;
+ edge_info.property_propagation_active = true;
+ edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;
+
+ // Now create the graph and commit:
+ graph.connect(&mock_rx_radio, &mock_ddc, edge_info);
+ graph.connect(&mock_ddc, &mock_tx_radio, edge_info);
+ graph.initialize();
+ BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 1);
+
+ mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0);
+ BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 2);
+
+ UHD_LOG_INFO("TEST", "Now tempting DDC to invalid prop value...");
+ mock_ddc.set_property<int>("decim", 42, 0);
+ // It will bounce back:
+ BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 2);
+}
+
+
+BOOST_AUTO_TEST_CASE(test_graph_catch_invalid_graph)
+{
+ node_accessor_t node_accessor{};
+ uhd::rfnoc::detail::graph_t graph{};
+ // Define some mock nodes:
+ // Source radio
+ mock_radio_node_t mock_rx_radio(0);
+ // Sink radio
+ mock_radio_node_t mock_tx_radio(1);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_tx_radio);
+ node_accessor.init_props(&mock_rx_radio);
+ mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0);
+
+ // In this simple graph, all connections are identical from an edge info
+ // perspective, so we're lazy and share an edge_info object:
+ uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
+ edge_info.src_port = 0;
+ edge_info.dst_port = 0;
+ edge_info.property_propagation_active = true;
+ edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;
+
+ // Now create the graph and commit:
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
+ BOOST_REQUIRE_THROW(graph.initialize(), uhd::resolve_error);
+ UHD_LOG_INFO("TEST", "^^^ Expected an error message.");
+}
+
+BOOST_AUTO_TEST_CASE(test_graph_ro_prop)
+{
+ node_accessor_t node_accessor{};
+ uhd::rfnoc::detail::graph_t graph{};
+ // Define some mock nodes:
+ // Source radio
+ mock_radio_node_t mock_rx_radio(0);
+ // Sink radio
+ mock_radio_node_t mock_tx_radio(1);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_tx_radio);
+ node_accessor.init_props(&mock_rx_radio);
+ BOOST_CHECK_EQUAL(mock_tx_radio.rssi_resolver_count, 1);
+ BOOST_CHECK_EQUAL(mock_rx_radio.rssi_resolver_count, 1);
+
+ // In this simple graph, all connections are identical from an edge info
+ // perspective, so we're lazy and share an edge_info object:
+ uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
+ edge_info.src_port = 0;
+ edge_info.dst_port = 0;
+ edge_info.property_propagation_active = true;
+ edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;
+
+ // Now create the graph and commit:
+ graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
+ graph.initialize();
+
+ const size_t rx_rssi_resolver_count = mock_rx_radio.rssi_resolver_count;
+ UHD_LOG_DEBUG("TEST", "RX RSSI: " << mock_rx_radio.get_property<double>("rssi"));
+ // The next value must match the value in graph.cpp
+ constexpr size_t MAX_NUM_ITERATIONS = 2;
+ BOOST_CHECK_EQUAL(
+ rx_rssi_resolver_count + MAX_NUM_ITERATIONS, mock_rx_radio.rssi_resolver_count);
+}
+
+BOOST_AUTO_TEST_CASE(test_graph_double_connect)
+{
+ node_accessor_t node_accessor{};
+ using uhd::rfnoc::detail::graph_t;
+ graph_t graph{};
+ using edge_t = graph_t::graph_edge_t;
+ // Define some mock nodes:
+ mock_radio_node_t mock_rx_radio0(0);
+ mock_radio_node_t mock_rx_radio1(1);
+ mock_radio_node_t mock_tx_radio0(2);
+ mock_radio_node_t mock_tx_radio1(3);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_tx_radio0);
+ node_accessor.init_props(&mock_tx_radio1);
+ node_accessor.init_props(&mock_rx_radio0);
+ node_accessor.init_props(&mock_rx_radio1);
+
+ graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true});
+ // Twice is also OK:
+ UHD_LOG_INFO("TEST", "Testing double-connect with same edges");
+ graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true});
+ UHD_LOG_INFO("TEST", "Testing double-connect with same edges, different attributes");
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, false}),
+ uhd::rfnoc_error);
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::STATIC, false}),
+ uhd::rfnoc_error);
+ UHD_LOG_INFO("TEST", "Testing double-connect output port, new dest node");
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio0, &mock_tx_radio1, {0, 0, edge_t::DYNAMIC, true}),
+ uhd::rfnoc_error);
+ UHD_LOG_INFO("TEST", "Testing double-connect input port, new source node");
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio1, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true}),
+ uhd::rfnoc_error);
+ // Add another valid connection
+ graph.connect(&mock_rx_radio1, &mock_tx_radio1, {0, 0, edge_t::DYNAMIC, true});
+ UHD_LOG_INFO("TEST", "Testing double-connect output port, existing dest node");
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio0, &mock_tx_radio1, {0, 0, edge_t::DYNAMIC, true}),
+ uhd::rfnoc_error);
+ UHD_LOG_INFO("TEST", "Testing double-connect input port, existing source node");
+ BOOST_REQUIRE_THROW(
+ graph.connect(&mock_rx_radio1, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true}),
+ uhd::rfnoc_error);
+}
+
+BOOST_AUTO_TEST_CASE(test_graph_crisscross_fifo)
+{
+ node_accessor_t node_accessor{};
+ uhd::rfnoc::detail::graph_t graph{};
+ // Define some mock nodes:
+ // Source radios
+ mock_radio_node_t mock_rx_radio0(0); // -> 2
+ mock_radio_node_t mock_rx_radio1(1); // -> 3
+ // Sink radios
+ mock_radio_node_t mock_tx_radio0(2);
+ mock_radio_node_t mock_tx_radio1(3);
+ // FIFO
+ mock_fifo_t mock_fifo(2);
+
+ // These init calls would normally be done by the framework
+ node_accessor.init_props(&mock_rx_radio0);
+ node_accessor.init_props(&mock_rx_radio1);
+ node_accessor.init_props(&mock_tx_radio0);
+ node_accessor.init_props(&mock_tx_radio1);
+ node_accessor.init_props(&mock_fifo);
+
+ mock_rx_radio0.set_property<double>("master_clock_rate", 200e6, 0);
+ mock_rx_radio1.set_property<double>("master_clock_rate", 100e6, 0);
+ mock_tx_radio0.set_property<double>("master_clock_rate", 100e6, 0);
+ mock_tx_radio1.set_property<double>("master_clock_rate", 200e6, 0);
+
+ using graph_edge_t = uhd::rfnoc::detail::graph_t::graph_edge_t;
+
+ // Now create the graph and commit:
+ graph.connect(&mock_rx_radio0, &mock_fifo, {0, 0, graph_edge_t::DYNAMIC, true});
+ graph.connect(&mock_rx_radio1, &mock_fifo, {0, 1, graph_edge_t::DYNAMIC, true});
+ // Notice how we swap the TX radios
+ graph.connect(&mock_fifo, &mock_tx_radio0, {1, 0, graph_edge_t::DYNAMIC, true});
+ graph.connect(&mock_fifo, &mock_tx_radio1, {0, 0, graph_edge_t::DYNAMIC, true});
+ UHD_LOG_INFO("TEST", "Now testing criss-cross prop resolution");
+ graph.initialize();
+}