Implementation of basic doca based offloads.  Only drop and simple
forwarding actions between peer doca ports are supported.

Signed-off-by: Eli Britstein <[email protected]>
---
 lib/automake.mk                 |    2 +
 lib/dpif-offload-doca-netdev.c  | 1266 +++++++++++++++++++++++++++++++
 lib/dpif-offload-doca-private.h |   55 ++
 lib/dpif-offload-doca.c         |  104 +++
 lib/netdev-doca.c               |   14 +-
 lib/netdev-doca.h               |    7 +
 6 files changed, 1446 insertions(+), 2 deletions(-)
 create mode 100644 lib/dpif-offload-doca-netdev.c
 create mode 100644 lib/dpif-offload-doca.c

diff --git a/lib/automake.mk b/lib/automake.mk
index 7006839cd..7dcc8244a 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -457,6 +457,8 @@ if DOCA_NETDEV
 lib_libopenvswitch_la_SOURCES += \
        lib/doca-pipe-group.c \
        lib/doca-pipe-group.h \
+       lib/dpif-offload-doca.c \
+       lib/dpif-offload-doca-netdev.c \
        lib/dpif-offload-doca-private.h \
        lib/netdev-doca.c \
        lib/netdev-doca.h
diff --git a/lib/dpif-offload-doca-netdev.c b/lib/dpif-offload-doca-netdev.c
new file mode 100644
index 000000000..c1765a17e
--- /dev/null
+++ b/lib/dpif-offload-doca-netdev.c
@@ -0,0 +1,1266 @@
+/*
+ * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <netinet/in.h>
+
+#include <doca_flow.h>
+
+#include "cmap.h"
+#include "doca-pipe-group.h"
+#include "dpif-netdev.h"
+#include "dpif-offload.h"
+#include "dpif-offload-doca-private.h"
+#include "netdev-provider.h"
+#include "odp-util.h"
+#include "ovs-doca.h"
+#include "ovs-rcu.h"
+#include "packets.h"
+#include "uuid.h"
+
+#include "openvswitch/match.h"
+#include "openvswitch/vlog.h"
+
+VLOG_DEFINE_THIS_MODULE(dpif_offload_doca_netdev);
+static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(600, 600);
+
+/* Thread-safety
+ * =============
+ *
+ * Below API is NOT thread safe in following terms:
+ *
+ *  - The caller must be sure that 'netdev' will not be destructed/deallocated.
+ *
+ *  - The caller must be sure that 'netdev' configuration will not be changed.
+ *    For example, simultaneous call of 'netdev_reconfigure()' for the same
+ *    'netdev' is forbidden.
+ *
+ * For current implementation all above restrictions are fulfilled by
+ * read-locking the datapath 'port_rwlock' in lib/dpif-netdev.c. */
+
+/* A mapping from pmd_id to flow_reference. */
+struct pmd_id_to_flow_ref_data {
+    struct cmap_node node;
+    void *flow_reference;
+    unsigned pmd_id;
+};
+
+struct pmd_data {
+    struct cmap pmd_id_to_flow_ref;
+    struct ovs_mutex map_lock;
+};
+
+struct doca_offload_handle {
+    struct doca_entry_handle e;
+};
+
+/* A mapping from ufid to doca offload handle. */
+struct ufid_to_doh_data {
+    struct cmap_node node;
+    ovs_u128 ufid;
+    struct doca_offload_handle doh;
+    OVSRCU_TYPE(struct pmd_data *) pmd_mapping;
+    struct dpif_flow_stats stats;
+    struct dpif_flow_stats baseline_stats;
+    struct netdev *physdev;
+    struct ovs_mutex lock;
+    unsigned int creation_tid;
+    bool dead;
+};
+
+struct doca_offload_netdev_data {
+    struct cmap ufid_to_doh;
+    uint64_t *doh_counters;
+    struct ovs_mutex map_lock;
+};
+
+struct doca_match_headers {
+    struct doca_flow_header_format *hdr_spec, *hdr_mask;
+    enum doca_flow_l2_meta *l2_spec, *l2_mask;
+    enum doca_flow_l3_meta *l3_spec, *l3_mask;
+    enum doca_flow_l4_meta *l4_spec, *l4_mask;
+    uint8_t *frag_mask;
+};
+
+struct doca_vars {
+    struct doca_offload *offload;
+    struct netdev *netdev;
+
+    struct ovs_doca_flow_match *dmatch;
+    struct ovs_doca_flow_match *dmatch_masks;
+    struct doca_flow_fwd *fwd;
+    struct doca_flow_monitor *monitor;
+
+    odp_port_t vport;
+    enum doca_pipe_group_type group_type;
+    uint32_t priority;
+};
+
+static struct pmd_data *
+pmd_data_init(void)
+{
+    struct pmd_data *mapping = xmalloc(sizeof *mapping);
+
+    ovs_mutex_init(&mapping->map_lock);
+    cmap_init(&mapping->pmd_id_to_flow_ref);
+    return mapping;
+}
+
+static void
+pmd_data_associate(struct pmd_data *mapping, unsigned pmd_id,
+                   void *flow_reference)
+{
+    struct pmd_id_to_flow_ref_data *pmd_data = xmalloc(sizeof *pmd_data);
+
+    pmd_data->flow_reference = flow_reference;
+    pmd_data->pmd_id = pmd_id;
+
+    ovs_mutex_lock(&mapping->map_lock);
+    cmap_insert(&mapping->pmd_id_to_flow_ref, &pmd_data->node,
+                hash_int(pmd_id, 0));
+    ovs_mutex_unlock(&mapping->map_lock);
+}
+
+static void
+pmd_data_disassociate(struct pmd_data *mapping, unsigned pmd_id)
+{
+    struct pmd_id_to_flow_ref_data *data;
+    size_t hash = hash_int(pmd_id, 0);
+
+    ovs_mutex_lock(&mapping->map_lock);
+
+    CMAP_FOR_EACH_WITH_HASH_PROTECTED (data, node, hash,
+                                       &mapping->pmd_id_to_flow_ref) {
+        if (data->pmd_id == pmd_id) {
+            cmap_remove(&mapping->pmd_id_to_flow_ref, &data->node, hash);
+            ovsrcu_postpone(free, data);
+            break;
+        }
+    }
+
+    ovs_mutex_unlock(&mapping->map_lock);
+}
+
+static struct pmd_id_to_flow_ref_data*
+pmd_data_get(const struct ufid_to_doh_data *doh_data, unsigned pmd_id)
+{
+    struct pmd_id_to_flow_ref_data *data;
+    size_t hash = hash_int(pmd_id, 0);
+    struct pmd_data *mapping;
+
+    mapping = ovsrcu_get(struct pmd_data *, &doh_data->pmd_mapping);
+    if (!mapping) {
+        return NULL;
+    }
+
+    CMAP_FOR_EACH_WITH_HASH (data, node, hash,
+                             &mapping->pmd_id_to_flow_ref) {
+        if (data->pmd_id == pmd_id) {
+            return data;
+        }
+    }
+
+    return NULL;
+}
+
+static bool
+pmd_data_update(const struct ufid_to_doh_data *doh_data, unsigned pmd_id,
+                void *new_flow_reference, void **previous_flow_reference)
+{
+    struct pmd_id_to_flow_ref_data *data;
+
+    data = pmd_data_get(doh_data, pmd_id);
+    if (!data) {
+        return false;
+    }
+
+    *previous_flow_reference = data->flow_reference;
+    data->flow_reference = new_flow_reference;
+    return true;
+}
+
+static bool
+pmd_data_find_and_delete(const struct ufid_to_doh_data *doh_data,
+                         unsigned pmd_id, void *flow_reference)
+{
+    struct pmd_data *mapping = ovsrcu_get(struct pmd_data *,
+                                          &doh_data->pmd_mapping);
+    struct pmd_id_to_flow_ref_data *data;
+    size_t hash = hash_int(pmd_id, 0);
+
+    ovs_assert(mapping);
+    ovs_mutex_lock(&mapping->map_lock);
+
+    CMAP_FOR_EACH_WITH_HASH_PROTECTED (data, node, hash,
+                                       &mapping->pmd_id_to_flow_ref) {
+        if (data->pmd_id == pmd_id && data->flow_reference == flow_reference) {
+            cmap_remove(&mapping->pmd_id_to_flow_ref, &data->node, hash);
+            ovsrcu_postpone(free, data);
+
+            ovs_mutex_unlock(&mapping->map_lock);
+            return true;
+        }
+    }
+
+    ovs_mutex_unlock(&mapping->map_lock);
+    return false;
+}
+
+static void
+pmd_data_cleanup(struct doca_offload *offload, struct pmd_data *mapping)
+{
+    struct pmd_id_to_flow_ref_data *data;
+
+    if (!mapping) {
+        return;
+    }
+
+    ovs_mutex_lock(&mapping->map_lock);
+
+    CMAP_FOR_EACH (data, node, &mapping->pmd_id_to_flow_ref) {
+        cmap_remove(&mapping->pmd_id_to_flow_ref, &data->node,
+                    hash_int(data->pmd_id, 0));
+
+        doca_offload_unreference(offload, data->pmd_id, data->flow_reference);
+        ovsrcu_postpone(free, data);
+    }
+
+    ovs_mutex_unlock(&mapping->map_lock);
+}
+
+static void
+pmd_data_destroy(struct pmd_data *mapping)
+{
+    if (mapping) {
+        ovs_mutex_destroy(&mapping->map_lock);
+        cmap_destroy(&mapping->pmd_id_to_flow_ref);
+        free(mapping);
+    }
+}
+
+static int
+offload_data_init(struct netdev *netdev, unsigned int offload_thread_count)
+{
+    struct doca_offload_netdev_data *data;
+
+    data = xzalloc(sizeof *data);
+    ovs_mutex_init(&data->map_lock);
+    cmap_init(&data->ufid_to_doh);
+    data->doh_counters = xcalloc(1 + offload_thread_count,
+                                 sizeof *data->doh_counters);
+
+    ovsrcu_set(&netdev->hw_info.offload_data, (void *) data);
+    atomic_store_relaxed(&netdev->hw_info.post_process_api_supported, true);
+
+    return 0;
+}
+
+static void
+offload_data_destroy__(struct doca_offload_netdev_data *data)
+{
+    ovs_mutex_destroy(&data->map_lock);
+    free(data->doh_counters);
+    free(data);
+}
+
+static void
+offload_data_destroy(struct netdev *netdev)
+{
+    struct doca_offload_netdev_data *data;
+    struct ufid_to_doh_data *node;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+    if (!data) {
+        return;
+    }
+
+    if (!cmap_is_empty(&data->ufid_to_doh)) {
+        VLOG_ERR("Incomplete flush: %s contains doh elements",
+                 netdev_get_name(netdev));
+    }
+
+    CMAP_FOR_EACH (node, node, &data->ufid_to_doh) {
+        ovsrcu_postpone(free, node);
+    }
+
+    cmap_destroy(&data->ufid_to_doh);
+    ovsrcu_postpone(offload_data_destroy__, data);
+
+    ovsrcu_set(&netdev->hw_info.offload_data, NULL);
+}
+
+static void
+offload_data_lock(struct netdev *netdev)
+    OVS_NO_THREAD_SAFETY_ANALYSIS
+{
+    struct doca_offload_netdev_data *data;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+    if (!data) {
+        return;
+    }
+
+    ovs_mutex_lock(&data->map_lock);
+}
+
+static void
+offload_data_unlock(struct netdev *netdev)
+    OVS_NO_THREAD_SAFETY_ANALYSIS
+{
+    struct doca_offload_netdev_data *data;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+    if (!data) {
+        return;
+    }
+
+    ovs_mutex_unlock(&data->map_lock);
+}
+
+static struct cmap *
+offload_data_map(struct netdev *netdev)
+{
+    struct doca_offload_netdev_data *data;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+
+    return data ? &data->ufid_to_doh : NULL;
+}
+
+static bool
+offload_data_maps(struct netdev *netdev, struct cmap **ufid_map)
+{
+    struct doca_offload_netdev_data *data;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+
+    if (!data) {
+        return false;
+    }
+
+    *ufid_map = &data->ufid_to_doh;
+    return true;
+}
+
+/* Find doh_data with @ufid. */
+static struct ufid_to_doh_data *
+ufid_to_doh_data_find(struct netdev *netdev, const ovs_u128 *ufid)
+{
+    size_t hash = hash_bytes(ufid, sizeof *ufid, 0);
+    struct ufid_to_doh_data *data;
+    struct cmap *map = offload_data_map(netdev);
+
+    if (!map) {
+        return NULL;
+    }
+
+    CMAP_FOR_EACH_WITH_HASH (data, node, hash, map) {
+        if (ovs_u128_equals(*ufid, data->ufid)) {
+            return data;
+        }
+    }
+
+    return NULL;
+}
+
+static struct ufid_to_doh_data *
+ufid_to_doh_data_find_pmd_and_update(struct netdev *netdev,
+                                          const ovs_u128 *ufid,
+                                          unsigned pmd_id, bool *found_pmd,
+                                          void *new_flow_reference,
+                                          void **previous_flow_reference)
+{
+    struct ufid_to_doh_data *data = ufid_to_doh_data_find(netdev,
+                                                                    ufid);
+    if (data) {
+        *found_pmd = pmd_data_update(data, pmd_id, new_flow_reference,
+                                     previous_flow_reference);
+    } else {
+        *found_pmd = false;
+    }
+
+    return data;
+}
+
+static void
+ufid_to_doh_set(struct ufid_to_doh_data *data,
+                struct netdev *physdev,
+                struct pmd_data *pmd_mapping)
+{
+    if (data->physdev) {
+        ovs_assert(data->physdev == physdev);
+    } else {
+        data->physdev = netdev_ref(physdev);
+    }
+
+    ovsrcu_set(&data->pmd_mapping, pmd_mapping);
+}
+
+static int
+ufid_to_doh_associate(struct ufid_to_doh_data *data,
+                      const ovs_u128 *ufid,
+                      struct netdev *physdev,
+                      struct pmd_data *pmd_mapping)
+{
+    size_t hash = hash_bytes(ufid, sizeof *ufid, 0);
+    struct cmap *map;
+
+    if (!offload_data_maps(physdev, &map)) {
+        return ENODEV;
+    }
+
+    /* We already checked before that no one inserted a
+     * doh for this ufid.  As the ufid dispatch is per
+     * thread id, this ufid would have been serviced by
+     * this thread and sync is implicit. */
+
+    data->ufid = *ufid;
+    data->creation_tid = doca_offload_thread_id();
+    ovs_mutex_init(&data->lock);
+    ufid_to_doh_set(data, physdev, pmd_mapping);
+
+    offload_data_lock(physdev);
+    cmap_insert(map, CONST_CAST(struct cmap_node *, &data->node), hash);
+    offload_data_unlock(physdev);
+
+    return 0;
+}
+
+static void
+doh_data_unref(struct ufid_to_doh_data *data)
+{
+    struct pmd_data *pmd_mapping = ovsrcu_get(struct pmd_data *,
+                                              &data->pmd_mapping);
+
+    pmd_data_destroy(pmd_mapping);
+    ovs_mutex_destroy(&data->lock);
+    free(data);
+}
+
+static void
+ufid_to_doh_disassociate(struct ufid_to_doh_data *data)
+    OVS_REQUIRES(data->lock)
+{
+    size_t hash = hash_bytes(&data->ufid, sizeof data->ufid, 0);
+    struct pmd_data *pmd_mapping;
+    struct cmap *map;
+
+    if (!offload_data_maps(data->physdev, &map)) {
+        return;
+    }
+
+    offload_data_lock(data->physdev);
+    cmap_remove(map, CONST_CAST(struct cmap_node *, &data->node), hash);
+    offload_data_unlock(data->physdev);
+
+    netdev_close(data->physdev);
+
+    /* There should be no more users before removing the hw flow. */
+    pmd_mapping = ovsrcu_get(struct pmd_data *, &data->pmd_mapping);
+    ovs_assert(!pmd_mapping || !cmap_count(&pmd_mapping->pmd_id_to_flow_ref));
+
+    ovsrcu_postpone(doh_data_unref, data);
+}
+
+static int
+doh_data_destroy(struct doca_offload *offload,
+                 struct ufid_to_doh_data *doh_data,
+                 bool force_destroy,
+                 bool modification)
+{
+    unsigned int tid = doca_offload_thread_id();
+    struct doca_offload_netdev_data *data;
+    struct doca_offload_handle *doh;
+    struct pmd_data *pmd_mapping;
+    struct netdev *physdev;
+    int ret;
+
+    if (!modification) {
+        ovs_mutex_lock(&doh_data->lock);
+    }
+
+    pmd_mapping = ovsrcu_get(struct pmd_data *, &doh_data->pmd_mapping);
+
+    /* Only delete the flow from HW if no PMDs are using it, and it's not a
+     * forceful destroy. */
+    if (doh_data->dead
+        || (!force_destroy && pmd_mapping
+            && cmap_count(&pmd_mapping->pmd_id_to_flow_ref))) {
+        if (!modification) {
+            ovs_mutex_unlock(&doh_data->lock);
+        }
+
+        return 0;
+    }
+
+    if (force_destroy) {
+        pmd_data_cleanup(offload, pmd_mapping);
+    }
+
+    doh_data->dead = true;
+
+    doh = &doh_data->doh;
+    physdev = doh_data->physdev;
+
+    ret = doca_pipe_group_del_deh(physdev, tid, &doh->e);
+    if (ret) {
+        goto out;
+    }
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &physdev->hw_info.offload_data);
+    data->doh_counters[tid]--;
+
+    if (!modification) {
+        ufid_to_doh_disassociate(doh_data);
+    }
+
+out:
+    if (!modification) {
+        ovs_mutex_unlock(&doh_data->lock);
+    }
+
+    return ret;
+}
+
+static int
+deh_add(struct doca_vars *vars,
+        struct doca_entry_handle *deh)
+{
+    unsigned int tid = doca_offload_thread_id();
+    struct doca_offload_netdev_data *data;
+    doca_error_t rv;
+
+    rv = doca_pipe_group_add_deh(vars->netdev, tid, vars->vport,
+                                 vars->group_type, vars->priority,
+                                 vars->dmatch, vars->dmatch_masks, NULL, NULL,
+                                 NULL, vars->monitor, vars->fwd, deh);
+    if (rv) {
+        return rv;
+    }
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &vars->netdev->hw_info.offload_data);
+    data->doh_counters[tid]++;
+
+    return 0;
+}
+
+static void
+metadata_match_ignore(struct match *match)
+{
+    if (match->wc.masks.dp_hash &&
+        (match->flow.dp_hash & match->wc.masks.dp_hash) == 0) {
+        match->wc.masks.dp_hash = 0;
+    }
+
+    if (match->wc.masks.skb_priority &&
+        (match->flow.skb_priority & match->wc.masks.skb_priority) == 0) {
+        match->wc.masks.skb_priority = 0;
+    }
+
+    if (match->wc.masks.ct_state &&
+        (match->flow.ct_state & match->wc.masks.ct_state) == 0) {
+        match->wc.masks.ct_state = 0;
+    }
+
+    if (match->wc.masks.ct_zone &&
+        (match->flow.ct_zone & match->wc.masks.ct_zone) == 0) {
+        match->wc.masks.ct_zone = 0;
+    }
+
+    if (match->wc.masks.ct_mark &&
+        (match->flow.ct_mark & match->wc.masks.ct_mark) == 0) {
+        match->wc.masks.ct_mark = 0;
+    }
+
+    for (int i = 0; i < 3; i++) {
+        if (match->wc.masks.ct_label.u32[i] &&
+            (match->flow.ct_label.u32[i] &
+             match->wc.masks.ct_label.u32[i]) == 0) {
+            match->wc.masks.ct_label.u32[i] = 0;
+        }
+    }
+}
+
+static void
+doca_match_headers_set(struct ovs_doca_flow_match *dmatch,
+                       struct ovs_doca_flow_match *dmatch_masks,
+                       bool inner,
+                       struct doca_match_headers *dmh)
+{
+    memset(dmh, 0, sizeof *dmh);
+
+    if (inner) {
+        dmh->hdr_spec = &dmatch->d.inner;
+        dmh->hdr_mask = &dmatch_masks->d.inner;
+        dmh->l2_spec = &dmatch->d.parser_meta.inner_l2_type;
+        dmh->l2_mask = &dmatch_masks->d.parser_meta.inner_l2_type;
+        dmh->l3_spec = &dmatch->d.parser_meta.inner_l3_type;
+        dmh->l3_mask = &dmatch_masks->d.parser_meta.inner_l3_type;
+        dmh->l4_spec = &dmatch->d.parser_meta.inner_l4_type;
+        dmh->l4_mask = &dmatch_masks->d.parser_meta.inner_l4_type;
+        dmh->frag_mask = &dmatch_masks->d.parser_meta.inner_ip_fragmented;
+    } else {
+        dmh->hdr_spec = &dmatch->d.outer;
+        dmh->hdr_mask = &dmatch_masks->d.outer;
+        dmh->l2_spec = &dmatch->d.parser_meta.outer_l2_type;
+        dmh->l2_mask = &dmatch_masks->d.parser_meta.outer_l2_type;
+        dmh->l3_spec = &dmatch->d.parser_meta.outer_l3_type;
+        dmh->l3_mask = &dmatch_masks->d.parser_meta.outer_l3_type;
+        dmh->l4_spec = &dmatch->d.parser_meta.outer_l4_type;
+        dmh->l4_mask = &dmatch_masks->d.parser_meta.outer_l4_type;
+        dmh->frag_mask = &dmatch_masks->d.parser_meta.outer_ip_fragmented;
+    }
+}
+
+static int
+parse_flow_match(struct doca_vars *vars,
+                 struct match *match,
+                 struct doca_match_headers *dmh,
+                 const ovs_u128 *ufid)
+{
+#define copy_match_field(dfield, mfield)                               \
+    ({                                                                 \
+        const uint8_t *_mspec = (const uint8_t *) &match->flow.mfield; \
+        uint8_t *_dmatch = (uint8_t *) &dmh->hdr_spec->dfield;         \
+        uint8_t *_mmask = (uint8_t *) &match->wc.masks.mfield;         \
+        size_t _size = sizeof dmh->hdr_spec->dfield;                   \
+        bool _field_on = false;                                        \
+                                                                       \
+        memcpy(&dmh->hdr_mask->dfield, _mmask, _size);                 \
+        for (int _i = 0; _i < _size; _i++) {                           \
+            _dmatch[_i] = _mspec[_i] & _mmask[_i];                     \
+            _field_on |= !!_mmask[_i];                                 \
+            _mmask[_i] = 0;                                            \
+        }                                                              \
+                                                                       \
+        _field_on;                                                     \
+    })
+
+    match->wc.masks.in_port.odp_port = 0;
+    vars->vport = ODPP_NONE;
+
+    /* recirc id must be zero. */
+    if (match->wc.masks.recirc_id & match->flow.recirc_id) {
+        return EOPNOTSUPP;
+    }
+
+    vars->group_type = DOCA_PIPE_GROUP_TYPE_NONE;
+    match->wc.masks.recirc_id = 0;
+    match->wc.masks.packet_type = 0;
+
+    vars->dmatch->d.parser_meta.port_id =
+        netdev_doca_get_port_id(vars->netdev);
+    memset(&vars->dmatch_masks->d.parser_meta.port_id, 0xFF,
+           sizeof vars->dmatch_masks->d.parser_meta.port_id);
+
+    /* Eth. */
+    copy_match_field(eth.src_mac, dl_src);
+    copy_match_field(eth.dst_mac, dl_dst);
+    copy_match_field(eth.type, dl_type);
+    vars->priority = DOCA_OFFLOAD_PRIORITY_LOW;
+
+    /* VLAN. */
+    if (match->wc.masks.vlans[0].tci) {
+        if (match->flow.vlans[0].tci) {
+            *dmh->l2_spec = DOCA_FLOW_L2_META_SINGLE_VLAN;
+            match->wc.masks.vlans[0].tci &= ~htons(VLAN_CFI);
+            copy_match_field(eth_vlan[0].tci, vlans[0].tci);
+            dmh->hdr_spec->l2_valid_headers = DOCA_FLOW_L2_VALID_HEADER_VLAN_0;
+            dmh->hdr_mask->l2_valid_headers = DOCA_FLOW_L2_VALID_HEADER_VLAN_0;
+        } else {
+            *dmh->l2_spec = DOCA_FLOW_L2_META_NO_VLAN;
+        }
+    }
+
+    *dmh->l2_mask = UINT32_MAX;
+    memset(&match->wc.masks.vlans[0], 0, sizeof match->wc.masks.vlans[0]);
+
+    if (match->flow.dl_type == htons(ETH_TYPE_IP)) {
+        /* IPv4. */
+        if (copy_match_field(ip4.src_ip, nw_src) |
+            copy_match_field(ip4.dst_ip, nw_dst) |
+            copy_match_field(ip4.next_proto, nw_proto) |
+            copy_match_field(ip4.ttl, nw_ttl) |
+            copy_match_field(ip4.dscp_ecn, nw_tos)) {
+            dmh->hdr_spec->l3_type = DOCA_FLOW_L3_TYPE_IP4;
+            dmh->hdr_mask->l3_type = DOCA_FLOW_L3_TYPE_IP4;
+        }
+
+        *dmh->l3_spec = DOCA_FLOW_L3_META_IPV4;
+        *dmh->l3_mask = DOCA_FLOW_L3_META_IPV4;
+
+        /* DOCA API does not support differentiation between first/later
+         * frags.  Reject both. */
+        *dmh->frag_mask = UINT8_MAX;
+        match->wc.masks.nw_frag = 0;
+        vars->priority = DOCA_OFFLOAD_PRIORITY_HIGH;
+    } else if (match->flow.dl_type == htons(ETH_TYPE_IPV6)) {
+        /* IPv6. */
+        if (copy_match_field(ip6.src_ip, ipv6_src) |
+            copy_match_field(ip6.dst_ip, ipv6_dst) |
+            copy_match_field(ip6.next_proto, nw_proto) |
+            copy_match_field(ip6.hop_limit, nw_ttl) |
+            copy_match_field(ip6.traffic_class, nw_tos)) {
+            dmh->hdr_spec->l3_type = DOCA_FLOW_L3_TYPE_IP6;
+            dmh->hdr_mask->l3_type = DOCA_FLOW_L3_TYPE_IP6;
+        }
+
+        *dmh->l3_spec = DOCA_FLOW_L3_META_IPV6;
+        *dmh->l3_mask = DOCA_FLOW_L3_META_IPV6;
+
+        /* DOCA API does not support differentiation between first/later
+         * frags.  Reject both. */
+        *dmh->frag_mask = UINT8_MAX;
+        match->wc.masks.nw_frag = 0;
+        vars->priority = DOCA_OFFLOAD_PRIORITY_MED;
+    }
+
+    if (match->flow.nw_proto == IPPROTO_TCP) {
+        if (copy_match_field(tcp.l4_port.src_port, tp_src) |
+            copy_match_field(tcp.l4_port.dst_port, tp_dst) |
+            copy_match_field(tcp.flags, tcp_flags)) {
+            dmh->hdr_spec->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_TCP;
+            dmh->hdr_mask->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_TCP;
+        }
+
+        *dmh->l4_spec = DOCA_FLOW_L4_META_TCP;
+        *dmh->l4_mask = DOCA_FLOW_L4_META_TCP;
+    } else if (match->flow.nw_proto == IPPROTO_UDP) {
+        if (copy_match_field(udp.l4_port.src_port, tp_src) |
+            copy_match_field(udp.l4_port.dst_port, tp_dst)) {
+            dmh->hdr_spec->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_UDP;
+            dmh->hdr_mask->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_UDP;
+        }
+
+        *dmh->l4_spec = DOCA_FLOW_L4_META_UDP;
+        *dmh->l4_mask = DOCA_FLOW_L4_META_UDP;
+    } else if (match->flow.nw_proto == IPPROTO_ICMP) {
+        if (copy_match_field(icmp.type, tp_src) |
+            copy_match_field(icmp.code, tp_dst)) {
+            dmh->hdr_spec->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_ICMP;
+            dmh->hdr_mask->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_ICMP;
+        }
+
+        *dmh->l4_spec = DOCA_FLOW_L4_META_ICMP;
+        *dmh->l4_mask = DOCA_FLOW_L4_META_ICMP;
+    } else if (match->flow.nw_proto == IPPROTO_ICMPV6) {
+        if (copy_match_field(icmp.type, tp_src) |
+            copy_match_field(icmp.code, tp_dst)) {
+            dmh->hdr_spec->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_ICMP6;
+            dmh->hdr_mask->l4_type_ext = DOCA_FLOW_L4_TYPE_EXT_ICMP6;
+        }
+
+        *dmh->l4_spec = DOCA_FLOW_L4_META_ICMP;
+        *dmh->l4_mask = DOCA_FLOW_L4_META_ICMP;
+    }
+
+    /* Ignore ==0 metadata matches on recirc 0. */
+    if (match->flow.recirc_id == 0) {
+        metadata_match_ignore(match);
+    }
+
+    if (!is_all_zeros(&match->wc.masks, sizeof match->wc.masks)) {
+        struct ds s = DS_EMPTY_INITIALIZER;
+
+        match_format(match, NULL, &s, OFP_DEFAULT_PRIORITY);
+        VLOG_DBG_RL(&rl, "%s: matches of ufid "UUID_FMT" are not supported: "
+                    "%s", netdev_get_name(vars->netdev),
+                    UUID_ARGS((struct uuid *) ufid), ds_cstr(&s));
+        ds_destroy(&s);
+        return EOPNOTSUPP;
+    }
+
+    return 0;
+
+#undef copy_match_field
+}
+
+static int
+get_output_port_id(struct doca_vars *vars,
+                   const struct nlattr *nla,
+                   uint16_t *port_id)
+{
+    struct netdev *outdev;
+    odp_port_t port;
+
+    port = nl_attr_get_odp_port(nla);
+    outdev = doca_offload_get_netdev(vars->offload, port);
+    if (!outdev) {
+        VLOG_DBG_RL(&rl, "Cannot find netdev for odp port %"PRIu32, port);
+        return ENODEV;
+    }
+
+    if (!dpif_offload_netdev_same_offload(vars->netdev, outdev)) {
+        VLOG_DBG_RL(&rl, "%s: Output to port \'%s\' cannot be offloaded",
+                    netdev_get_name(vars->netdev), netdev_get_name(outdev));
+        return EOPNOTSUPP;
+    }
+
+    *port_id = netdev_doca_get_port_id(outdev);
+    if (*port_id == DPDK_ETH_PORT_ID_INVALID) {
+        VLOG_DBG_RL(&rl, "%s: Can't get port_id for %s",
+                    netdev_get_name(vars->netdev), netdev_get_name(outdev));
+        return EOPNOTSUPP;
+    }
+
+    return 0;
+}
+
+static int
+parse_flow_actions(struct doca_vars *vars,
+                   struct nlattr *nl_actions,
+                   size_t nl_actions_len)
+{
+    struct nlattr *nla;
+    size_t left;
+
+    NL_ATTR_FOR_EACH_UNSAFE (nla, left, nl_actions, nl_actions_len) {
+        if (nl_attr_type(nla) == OVS_ACTION_ATTR_OUTPUT) {
+            int error;
+
+            if (left > NLA_ALIGN(nla->nla_len)) {
+                VLOG_DBG_RL(&rl, "Unsupported non-last output action");
+                return EOPNOTSUPP;
+            }
+
+            vars->fwd->type = DOCA_FLOW_FWD_PORT;
+            error = get_output_port_id(vars, nla, &vars->fwd->port_id);
+            if (error) {
+                return error;
+            }
+        } else if (nl_attr_type(nla) == OVS_ACTION_ATTR_DROP) {
+            if (left > NLA_ALIGN(nla->nla_len)) {
+                VLOG_DBG_RL(&rl, "Unsupported non-last drop action");
+                return EOPNOTSUPP;
+            }
+
+            vars->fwd->type = DOCA_FLOW_FWD_DROP;
+        } else {
+            VLOG_DBG_RL(&rl, "Unsupported action type %d", nl_attr_type(nla));
+            return EOPNOTSUPP;
+        }
+    }
+
+    if (nl_actions_len == 0) {
+        VLOG_DBG_RL(&rl, "No actions provided");
+        return EOPNOTSUPP;
+    }
+
+    vars->monitor->counter_type = DOCA_FLOW_RESOURCE_TYPE_NON_SHARED;
+
+    return 0;
+}
+
+static struct ufid_to_doh_data *
+doca_offload_add_flow(struct doca_offload *offload,
+                      struct pmd_data *pmd_mapping, struct netdev *netdev,
+                      struct match *match, struct nlattr *nl_actions,
+                      size_t actions_len, const ovs_u128 *ufid,
+                      struct ufid_to_doh_data *old_doh_data)
+{
+    struct ovs_doca_flow_match dmatch, dmatch_masks;
+    struct ufid_to_doh_data *doh_data = NULL;
+    struct doca_flow_monitor monitor;
+    struct doca_offload_handle *doh;
+    struct doca_match_headers dmh;
+    struct doca_flow_fwd fwd;
+    struct doca_vars vars = {
+        .offload = offload,
+        .netdev = netdev,
+        .dmatch = &dmatch,
+        .dmatch_masks = &dmatch_masks,
+        .fwd = &fwd,
+        .monitor = &monitor,
+    };
+
+    memset(&dmatch, 0, sizeof dmatch);
+    memset(&dmatch_masks, 0, sizeof dmatch_masks);
+    memset(&fwd, 0, sizeof fwd);
+    memset(&monitor, 0, sizeof monitor);
+
+    doh_data = old_doh_data ? old_doh_data : xzalloc(sizeof *doh_data);
+    if (!doh_data) {
+        return NULL;
+    }
+
+    memset(&doh_data->doh, 0, sizeof doh_data->doh);
+    doh = &doh_data->doh;
+
+    doca_match_headers_set(&dmatch, &dmatch_masks, false, &dmh);
+
+    if (parse_flow_match(&vars, match, &dmh, ufid)) {
+        VLOG_DBG_RL(&rl, "%s: matches of ufid "UUID_FMT" are not supported",
+                    netdev_get_name(netdev), UUID_ARGS((struct uuid *) ufid));
+        if (!old_doh_data) {
+            free(doh_data);
+        }
+
+        return NULL;
+    }
+
+    if (parse_flow_actions(&vars, nl_actions, actions_len)) {
+        VLOG_DBG_RL(&rl, "%s: actions of ufid "UUID_FMT" are not supported",
+                    netdev_get_name(netdev), UUID_ARGS((struct uuid *) ufid));
+        goto err_actions;
+    }
+
+    if (deh_add(&vars, &doh->e)) {
+        goto err_deh_add;
+    }
+
+    if (old_doh_data) {
+        ovs_mutex_lock(&doh_data->lock);
+        ufid_to_doh_set(doh_data, netdev, pmd_mapping);
+        doh_data->dead = false;
+        ovs_mutex_unlock(&doh_data->lock);
+    } else if (ufid_to_doh_associate(doh_data, ufid, netdev, pmd_mapping)) {
+        goto err_associate;
+    }
+
+    VLOG_DBG("%s: installed flow (%p/%p) by ufid "UUID_FMT,
+             netdev_get_name(netdev), doh->e.mctx, doh->e.entry,
+             UUID_ARGS((struct uuid *) ufid));
+    return doh_data;
+
+err_associate:
+    doh_data_destroy(offload, doh_data, false, true);
+    free(doh_data);
+    return NULL;
+err_deh_add:
+err_actions:
+    if (!old_doh_data) {
+        free(doh_data);
+    }
+
+    return NULL;
+}
+
+struct get_netdev_odp_aux {
+    struct netdev *netdev;
+    odp_port_t odp_port;
+};
+
+int
+doca_netdev_flow_put(struct doca_offload *offload, unsigned pmd_id,
+                     void *flow_reference, struct netdev *netdev,
+                     struct match *match, struct nlattr *actions,
+                     size_t actions_len, const ovs_u128 *ufid,
+                     odp_port_t orig_in_port OVS_UNUSED,
+                     void **previous_flow_reference,
+                     struct dpif_flow_stats *stats)
+{
+    struct ufid_to_doh_data *old_doh_data;
+    struct ufid_to_doh_data *doh_data;
+    struct pmd_data *pmd_mapping;
+    bool modification = false;
+    bool pmd_exists;
+    int ret;
+
+    if (stats) {
+        memset(stats, 0, sizeof *stats);
+    }
+
+    /* If an old doh_data exists for this pmd_id, it means it's a flow
+     * modification.  Here destroy the old doh_data first before adding a
+     * new one.  Keep the stats and pmd_mapping for the newly created rule. */
+    old_doh_data = ufid_to_doh_data_find_pmd_and_update(
+        netdev, ufid, pmd_id, &pmd_exists, flow_reference,
+        previous_flow_reference);
+    doh_data = old_doh_data;
+
+    if (old_doh_data && old_doh_data->doh.e.entry && pmd_exists) {
+        memcpy(&old_doh_data->baseline_stats, &old_doh_data->stats,
+               sizeof old_doh_data->baseline_stats);
+        modification = true;
+        pmd_mapping = ovsrcu_get(struct pmd_data *,
+                                 &old_doh_data->pmd_mapping);
+        ovs_mutex_lock(&old_doh_data->lock);
+        ovsrcu_set(&old_doh_data->pmd_mapping, NULL);
+
+        ret = doh_data_destroy(offload, old_doh_data, false, true);
+        ovs_mutex_unlock(&old_doh_data->lock);
+        if (ret) {
+            return ret;
+        }
+    } else if (!old_doh_data) {
+        pmd_mapping = pmd_data_init();
+        pmd_data_associate(pmd_mapping, pmd_id, flow_reference);
+        *previous_flow_reference = NULL;
+    } else /* if (old_doh_data) */ {
+        pmd_mapping = ovsrcu_get(struct pmd_data *,
+                                 &old_doh_data->pmd_mapping);
+
+        pmd_data_associate(pmd_mapping, pmd_id, flow_reference);
+        *previous_flow_reference = NULL;
+    }
+
+    if (modification || !old_doh_data) {
+        doh_data = doca_offload_add_flow(offload, pmd_mapping, netdev, match,
+                                         actions, actions_len, ufid,
+                                         old_doh_data);
+        if (!doh_data) {
+            if (modification) {
+                ovs_mutex_lock(&old_doh_data->lock);
+                ufid_to_doh_disassociate(old_doh_data);
+                ovs_mutex_unlock(&old_doh_data->lock);
+            }
+
+            /* Clean up existing mappings, except for the current pmd_id one,
+             * as this is handled through the callback. */
+            pmd_data_disassociate(pmd_mapping, pmd_id);
+            pmd_data_cleanup(offload, pmd_mapping);
+            ovsrcu_postpone(pmd_data_destroy, pmd_mapping);
+            return EINVAL;
+        }
+    }
+
+    if (modification) {
+        memcpy(&doh_data->stats, &old_doh_data->baseline_stats,
+               sizeof doh_data->stats);
+    }
+
+    if (stats) {
+        *stats = doh_data->stats;
+    }
+
+    return 0;
+}
+
+int
+doca_netdev_flow_del(struct doca_offload *offload, struct netdev *netdev,
+                     unsigned pmd_id, const ovs_u128 *ufid,
+                     void *flow_reference, struct dpif_flow_stats *stats)
+{
+    struct ufid_to_doh_data *doh_data;
+
+    doh_data = ufid_to_doh_data_find(netdev, ufid);
+    if (!doh_data || !doh_data->doh.e.entry) {
+        return ENOENT;
+    }
+
+    if (!pmd_data_find_and_delete(doh_data, pmd_id, flow_reference)) {
+        return ENOENT;
+    }
+
+    if (stats) {
+        memset(stats, 0, sizeof *stats);
+    }
+
+    return doh_data_destroy(offload, doh_data, false, false);
+}
+
+int
+doca_netdev_offload_init(struct netdev *netdev,
+                         unsigned int offload_thread_count)
+{
+    return offload_data_init(netdev, offload_thread_count);
+}
+
+void
+doca_netdev_offload_uninit(struct netdev *netdev)
+{
+    offload_data_destroy(netdev);
+}
+
+int
+doca_netdev_flow_get(struct netdev *netdev, struct match *match OVS_UNUSED,
+                     struct nlattr **actions OVS_UNUSED, const ovs_u128 *ufid,
+                     struct dpif_flow_stats *stats,
+                     struct dpif_flow_attrs *attrs,
+                     struct ofpbuf *buf OVS_UNUSED)
+{
+    struct doca_flow_resource_query query;
+    struct ufid_to_doh_data *doh_data;
+    int ret = 0;
+
+    attrs->dp_extra_info = NULL;
+
+    doh_data = ufid_to_doh_data_find(netdev, ufid);
+    if (doh_data) {
+        attrs->offloaded = true;
+    }
+
+    if (!doh_data || !doh_data->doh.e.entry ||
+        doh_data->dead || ovs_mutex_trylock(&doh_data->lock)) {
+        if (doh_data) {
+            attrs->dp_layer = "doca";
+            /* The node might be in the process of being deleted.
+             * It should be fine to read from it as actual freeing is done
+             * after an RCU grace period. */
+            memcpy(stats, &doh_data->stats, sizeof *stats);
+            return 0;
+        }
+
+        return ENOENT;
+    }
+
+    /* Check again whether the data is dead, as it could have been
+     * updated while the lock was not yet taken.  The first check above
+     * was only to avoid unnecessary locking if possible. */
+    if (doh_data->dead) {
+        ret = ENOENT;
+        goto out;
+    }
+
+    attrs->offloaded = true;
+    attrs->dp_layer = "doca";
+
+    ret = doca_flow_resource_query_entry(doh_data->doh.e.entry, &query);
+    if (ret) {
+        VLOG_DBG_RL(&rl, "%s: Failed to query ufid "UUID_FMT" entry: %p",
+                    netdev_get_name(netdev), UUID_ARGS((struct uuid *) ufid),
+                    doh_data->doh.e.entry);
+        goto out;
+    }
+
+    if (doh_data->stats.n_packets != query.counter.total_pkts) {
+        doh_data->stats.used = time_msec();
+    }
+
+    doh_data->stats.n_packets = query.counter.total_pkts +
+        doh_data->baseline_stats.n_packets;
+    doh_data->stats.n_bytes = query.counter.total_bytes +
+        doh_data->baseline_stats.n_bytes;
+    memcpy(stats, &doh_data->stats, sizeof *stats);
+out:
+    ovs_mutex_unlock(&doh_data->lock);
+    return ret;
+}
+
+static void
+doca_netdev_flow_flush__(struct doca_offload *offload, struct netdev *netdev)
+{
+    struct cmap *map = offload_data_map(netdev);
+    unsigned int tid = doca_offload_thread_id();
+    struct ufid_to_doh_data *data;
+
+    if (!map) {
+        return;
+    }
+
+    CMAP_FOR_EACH (data, node, map) {
+        if (data->creation_tid == tid) {
+            doh_data_destroy(offload, data, true, false);
+        }
+    }
+}
+
+struct flush_related_aux {
+    struct doca_offload *offload;
+    struct netdev *netdev;
+};
+
+static bool
+flush_related_cb(struct netdev *related,
+                 odp_port_t odp_port OVS_UNUSED,
+                 void *aux_)
+{
+    struct doca_offload_esw_data *offload_data;
+    struct flush_related_aux *aux = aux_;
+
+    if (aux->netdev == related) {
+        return false;
+    }
+
+    offload_data = netdev_doca_get_esw_offload_data(related);
+    /* Representors are related. */
+    if (netdev_doca_is_esw_mgr(aux->netdev) &&
+        offload_data == netdev_doca_get_esw_offload_data(aux->netdev)) {
+        doca_netdev_flow_flush__(aux->offload, related);
+    }
+
+    return false;
+}
+
+int
+doca_netdev_flow_flush(struct doca_offload *offload, struct netdev *netdev)
+{
+    struct flush_related_aux aux = {
+        .offload = offload,
+        .netdev = netdev,
+    };
+
+    doca_netdev_flow_flush__(offload, netdev);
+    doca_offload_traverse_ports(offload, flush_related_cb, &aux);
+
+    return 0;
+}
+
+int
+doca_netdev_hw_miss_packet_recover(struct doca_offload *offload OVS_UNUSED,
+                                   struct netdev *netdev OVS_UNUSED,
+                                   unsigned pmd_id OVS_UNUSED,
+                                   struct dp_packet *packet OVS_UNUSED,
+                                   void **flow_reference OVS_UNUSED)
+{
+    return 0;
+}
+
+uint64_t
+doca_netdev_flow_count(struct netdev *netdev,
+                       unsigned int offload_thread_count)
+{
+    struct doca_offload_netdev_data *data;
+    uint64_t total = 0;
+    unsigned int tid;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+    if (!data) {
+        return 0;
+    }
+
+    for (tid = 0; tid < 1 + offload_thread_count; tid++) {
+        total += data->doh_counters[tid];
+    }
+
+    return total;
+}
+
+uint64_t
+doca_netdev_flow_count_by_thread(struct netdev *netdev, unsigned int tid)
+{
+    struct doca_offload_netdev_data *data;
+
+    data = (struct doca_offload_netdev_data *)
+        ovsrcu_get(void *, &netdev->hw_info.offload_data);
+    if (!data) {
+        return 0;
+    }
+
+    return data->doh_counters[tid];
+}
+
+void
+doca_netdev_offload_start(struct netdev *netdev)
+{
+    doca_pipe_group_init(netdev);
+}
+
+void
+doca_netdev_offload_stop(struct netdev *netdev)
+{
+    doca_pipe_group_uninit(netdev);
+}
diff --git a/lib/dpif-offload-doca-private.h b/lib/dpif-offload-doca-private.h
index 94907f298..c6fbb0a81 100644
--- a/lib/dpif-offload-doca-private.h
+++ b/lib/dpif-offload-doca-private.h
@@ -19,11 +19,66 @@
 
 #include <config.h>
 
+#include "dp-packet.h"
+#include "netlink-protocol.h"
 #include "refmap.h"
 
+#include "openvswitch/match.h"
+#include "openvswitch/types.h"
+
+/* Forward declarations of private structures. */
+struct doca_offload;
+struct netdev;
+
 struct doca_offload_esw_data {
     struct refmap *group_rfm;
     struct refmap *group_mask_rfm;
 };
 
+enum {
+    DOCA_OFFLOAD_PRIORITY_HIGH,
+    DOCA_OFFLOAD_PRIORITY_MED,
+    DOCA_OFFLOAD_PRIORITY_LOW,
+};
+
+/* DPIF offload doca implementation-specific functions.  These should only be
+ * used by the associated netdev offload provider, i.e.,
+ * dpif-offload-doca-netdev. */
+unsigned int doca_offload_thread_id(void);
+void doca_offload_unreference(struct doca_offload *offload, unsigned pmd_id,
+                              void *flow_reference);
+struct netdev *doca_offload_get_netdev(const struct doca_offload *,
+                                       odp_port_t port_no);
+void doca_offload_traverse_ports(
+    const struct doca_offload *offload,
+    bool (*cb)(struct netdev *, odp_port_t, void *), void *aux);
+
+/* dpif-offload-doca-netdev specific offload functions.  These should only be
+ * used by the associated dpif offload provider, i.e., dpif-offload-doca. */
+int doca_netdev_offload_init(struct netdev *,
+                             unsigned int offload_thread_count);
+void doca_netdev_offload_uninit(struct netdev *);
+int doca_netdev_flow_flush(struct doca_offload *, struct netdev *);
+uint64_t doca_netdev_flow_count(struct netdev *,
+                                unsigned int offload_thread_count);
+uint64_t doca_netdev_flow_count_by_thread(struct netdev *, unsigned int tid);
+int doca_netdev_hw_miss_packet_recover(struct doca_offload *, struct netdev *,
+                                       unsigned pmd_id, struct dp_packet *,
+                                       void **flow_reference);
+int doca_netdev_flow_put(struct doca_offload *, unsigned pmd_id,
+                         void *flow_reference, struct netdev *, struct match *,
+                         struct nlattr *actions, size_t actions_len,
+                         const ovs_u128 *ufid, odp_port_t orig_in_port,
+                         void **previous_flow_reference,
+                         struct dpif_flow_stats *);
+int doca_netdev_flow_del(struct doca_offload *, struct netdev *,
+                         unsigned pmd_id, const ovs_u128 *ufid,
+                         void *flow_reference, struct dpif_flow_stats *);
+int doca_netdev_flow_get(struct netdev *, struct match *,
+                         struct nlattr **actions, const ovs_u128 *ufid,
+                         struct dpif_flow_stats *, struct dpif_flow_attrs *,
+                         struct ofpbuf *buf);
+void doca_netdev_offload_start(struct netdev *netdev);
+void doca_netdev_offload_stop(struct netdev *netdev);
+
 #endif /* DPIF_OFFLOAD_DOCA_PRIVATE_H */
diff --git a/lib/dpif-offload-doca.c b/lib/dpif-offload-doca.c
new file mode 100644
index 000000000..f80f16b9f
--- /dev/null
+++ b/lib/dpif-offload-doca.c
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "dpif-offload.h"
+#include "dpif-offload-provider.h"
+#include "dpif-offload-doca-private.h"
+#include "ovs-rcu.h"
+
+/* dpif offload interface for the doca offload implementation. */
+struct doca_offload {
+    struct dpif_offload offload;
+
+    atomic_count next_offload_thread_id;
+    atomic_bool offload_thread_shutdown;
+    struct doca_offload_thread *offload_threads;
+
+    dpif_offload_flow_unreference_cb *unreference_cb;
+
+    /* Configuration specific variables. */
+    struct ovsthread_once once_enable; /* Track first-time enablement. */
+    unsigned int offload_thread_count; /* Number of offload threads. */
+};
+
+DECLARE_EXTERN_PER_THREAD_DATA(unsigned int, doca_offload_thread_id);
+DEFINE_EXTERN_PER_THREAD_DATA(doca_offload_thread_id, OVSTHREAD_ID_UNSET);
+
+unsigned int
+doca_offload_thread_id(void)
+{
+    unsigned int id = *doca_offload_thread_id_get();
+
+    if (OVS_UNLIKELY(id == OVSTHREAD_ID_UNSET)) {
+        /* Offload threads get their ID set at initialization, here
+         * only the RCU thread might need initialization. */
+        ovs_assert(!strncmp(get_subprogram_name(), "urcu", strlen("urcu")));
+
+        /* RCU will compete with other threads for shared object access.
+         * Reclamation functions using a thread ID must be thread-safe.
+         * For that end, and because RCU must consider all potential shared
+         * objects anyway, its thread-id can be whichever, so return 0. */
+        id = 0;
+        *doca_offload_thread_id_get() = id;
+    }
+
+    return id;
+}
+
+void
+doca_offload_traverse_ports(const struct doca_offload *offload,
+                            bool (*cb)(struct netdev *, odp_port_t, void *),
+                            void *aux)
+{
+    struct dpif_offload_port *port;
+
+    DPIF_OFFLOAD_PORT_FOR_EACH (port, &offload->offload) {
+        if (cb(port->netdev, port->port_no, aux)) {
+            break;
+        }
+    }
+}
+
+static struct netdev *
+doca_offload_get_netdev__(const struct dpif_offload *offload,
+                          odp_port_t port_no)
+{
+    struct dpif_offload_port *port;
+
+    port = dpif_offload_port_mgr_find_by_odp_port(offload, port_no);
+    if (!port) {
+        return NULL;
+    }
+
+    return port->netdev;
+}
+
+struct netdev *
+doca_offload_get_netdev(const struct doca_offload *offload, odp_port_t port_no)
+{
+    return doca_offload_get_netdev__(&offload->offload, port_no);
+}
+
+void
+doca_offload_unreference(struct doca_offload *offload, unsigned pmd_id,
+                         void *flow_reference)
+{
+    if (offload->unreference_cb) {
+        offload->unreference_cb(pmd_id, flow_reference);
+    }
+}
diff --git a/lib/netdev-doca.c b/lib/netdev-doca.c
index 6ff85338a..98ff50f49 100644
--- a/lib/netdev-doca.c
+++ b/lib/netdev-doca.c
@@ -41,8 +41,11 @@
 #include <doca_rdma_bridge.h>
 
 #include "coverage.h"
+#include "doca-pipe-group.h"
 #include "dp-packet.h"
 #include "dpif-netdev.h"
+#include "dpif-offload.h"
+#include "dpif-offload-doca-private.h"
 #include "netdev-provider.h"
 #include "ovs-doca.h"
 #include "ovs-thread.h"
@@ -111,7 +114,7 @@ netdev_doca_get_esw_mgr_port_id(const struct netdev *netdev)
     return dev->esw_mgr_port_id;
 }
 
-static dpdk_port_t
+dpdk_port_t
 netdev_doca_get_port_id(const struct netdev *netdev)
 {
     struct netdev_doca *dev = netdev_doca_cast(netdev);
@@ -630,6 +633,10 @@ netdev_doca_egress_entry_uninit(struct netdev *netdev)
 static void
 netdev_doca_slowpath_esw_uninit(struct netdev *netdev)
 {
+    if (dpif_offload_enabled()) {
+        doca_netdev_offload_stop(netdev);
+    }
+
     netdev_doca_root_pipe_uninit(netdev);
     netdev_doca_pre_miss_rules_uninit(netdev);
     netdev_doca_pre_miss_pipe_uninit(netdev);
@@ -665,6 +672,9 @@ netdev_doca_slowpath_esw_init(struct netdev *netdev)
     ESW_INIT_CMD(netdev_doca_pre_miss_pipe_init);
     ESW_INIT_CMD(netdev_doca_pre_miss_rules_init);
     ESW_INIT_CMD(netdev_doca_root_pipe_init);
+    if (dpif_offload_enabled()) {
+        doca_netdev_offload_start(netdev);
+    }
 
     return 0;
 }
@@ -1223,7 +1233,7 @@ netdev_doca_class_init(void)
 
 /* Extract the PCI part from 'devargs' to rte_pci.
  * Return EINVAL for error or 0 for success. */
-static int
+int
 netdev_doca_parse_dpdk_devargs_pci(const char *devargs,
                                    struct rte_pci_addr *rte_pci)
 {
diff --git a/lib/netdev-doca.h b/lib/netdev-doca.h
index 0b16d7717..02433dae8 100644
--- a/lib/netdev-doca.h
+++ b/lib/netdev-doca.h
@@ -121,4 +121,11 @@ netdev_doca_get_esw_offload_data(struct netdev *netdev);
 bool
 netdev_doca_is_esw_mgr(const struct netdev *netdev);
 
+uint16_t
+netdev_doca_get_port_id(const struct netdev *netdev);
+
+int
+netdev_doca_parse_dpdk_devargs_pci(const char *devargs,
+                                   struct rte_pci_addr *rte_pci);
+
 #endif /* NETDEV_DOCA_H */
-- 
2.43.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to