diff options
author | Martin Braun <martin.braun@ettus.com> | 2019-04-24 18:23:31 -0700 |
---|---|---|
committer | Martin Braun <martin.braun@ettus.com> | 2019-11-26 11:49:14 -0800 |
commit | c97bdc6c94c98753215a90cf499af4bdf06db8e2 (patch) | |
tree | 67f623ae84acb045d145bd22036df60a1724b789 /host | |
parent | f0371292a43c3e4e3c68d8631c57d64ab10faf4c (diff) | |
download | uhd-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
Diffstat (limited to 'host')
-rw-r--r-- | host/include/uhd/rfnoc/node.hpp | 265 | ||||
-rw-r--r-- | host/include/uhd/rfnoc/node.ipp | 8 | ||||
-rw-r--r-- | host/lib/include/uhdlib/rfnoc/graph.hpp | 271 | ||||
-rw-r--r-- | host/lib/include/uhdlib/rfnoc/node_accessor.hpp | 5 | ||||
-rw-r--r-- | host/lib/rfnoc/CMakeLists.txt | 1 | ||||
-rw-r--r-- | host/lib/rfnoc/graph.cpp | 460 | ||||
-rw-r--r-- | host/lib/rfnoc/node.cpp | 312 | ||||
-rw-r--r-- | host/tests/CMakeLists.txt | 26 | ||||
-rw-r--r-- | host/tests/rfnoc_detailgraph_test.cpp | 215 | ||||
-rw-r--r-- | host/tests/rfnoc_graph_mock_nodes.hpp | 233 | ||||
-rw-r--r-- | host/tests/rfnoc_node_test.cpp | 58 | ||||
-rw-r--r-- | host/tests/rfnoc_propprop_test.cpp | 366 |
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(); +} |