[PATCH 2/2] drivers: add support for async RTE FLOW APIs

ykanaujia at marvell.com ykanaujia at marvell.com
Wed Jul 15 10:45:20 CEST 2026


From: Yashaswini Kanaujia <ykanaujia at marvell.com>

added support for asynchronous rte_flow APIs
for cn10k device

Signed-off-by: Yashaswini Kanaujia <ykanaujia at marvell.com>
---
 drivers/common/cnxk/meson.build               |    1 +
 drivers/common/cnxk/roc_npc.h                 |   97 ++
 drivers/common/cnxk/roc_npc_flow_template.c   | 1326 ++++++++++++++++
 drivers/common/cnxk/roc_npc_mcam.c            |   45 +-
 drivers/common/cnxk/roc_npc_priv.h            |   98 ++
 drivers/common/cnxk/roc_npc_utils.c           |    4 +-
 .../common/cnxk/roc_platform_base_symbols.c   |   16 +
 drivers/net/cnxk/cn10k_ethdev.c               |   23 +
 drivers/net/cnxk/cn10k_flow.c                 |   43 +
 drivers/net/cnxk/cn10k_flow.h                 |    2 +
 drivers/net/cnxk/cn20k_ethdev.c               |   24 +-
 drivers/net/cnxk/cn20k_flow.c                 |   43 +
 drivers/net/cnxk/cn20k_flow.h                 |    2 +
 drivers/net/cnxk/cnxk_ethdev.h                |   44 +
 drivers/net/cnxk/cnxk_flow.c                  | 1338 ++++++++++++++++-
 drivers/net/cnxk/cnxk_flow.h                  |   77 +
 16 files changed, 3161 insertions(+), 22 deletions(-)
 create mode 100644 drivers/common/cnxk/roc_npc_flow_template.c

diff --git a/drivers/common/cnxk/meson.build b/drivers/common/cnxk/meson.build
index 3303ad9354..68374ee135 100644
--- a/drivers/common/cnxk/meson.build
+++ b/drivers/common/cnxk/meson.build
@@ -59,6 +59,7 @@ sources = files(
         'roc_npa_type.c',
         'roc_npc.c',
         'roc_npc_aging.c',
+        'roc_npc_flow_template.c',
         'roc_npc_mcam.c',
         'roc_npc_mcam_dump.c',
         'roc_npc_parse.c',
diff --git a/drivers/common/cnxk/roc_npc.h b/drivers/common/cnxk/roc_npc.h
index a7254f35ca..21fa493c74 100644
--- a/drivers/common/cnxk/roc_npc.h
+++ b/drivers/common/cnxk/roc_npc.h
@@ -473,6 +473,9 @@ int __roc_api roc_npc_mcam_alloc_entries(struct roc_npc *roc_npc, int ref_entry,
 int __roc_api roc_npc_mcam_ena_dis_entry(struct roc_npc *roc_npc, struct roc_npc_flow *mcam,
 					 bool enable);
 int __roc_api roc_npc_mcam_write_entry(struct roc_npc *roc_npc, struct roc_npc_flow *mcam);
+
+void __roc_api roc_npc_mcam_write_rx_finalize(struct roc_npc *roc_npc, struct roc_npc_flow *flow);
+
 void __roc_api roc_npc_defrag_mcam_banks(struct roc_npc *roc_npc);
 uint8_t __roc_api roc_npc_get_key_type(struct roc_npc *roc_npc, struct roc_npc_flow *flow);
 uint8_t __roc_api roc_npc_kex_key_type_config_get(struct roc_npc *roc_npc);
@@ -508,4 +511,98 @@ void __roc_api roc_npc_sdp_channel_get(struct roc_npc *roc_npc, uint16_t *chan_b
 int __roc_api roc_npc_mcam_get_stats(struct roc_npc *roc_npc, struct roc_npc_flow *flow,
 				     uint64_t *count);
 int __roc_api roc_npc_skip_size_pkind_get(struct roc_npc *roc_npc);
+
+/*
+ * Queue-based (async) template flow API.
+ */
+enum roc_npc_template_table_insertion_type {
+	/* HW MCAM entry allocated per rule at create time (match based). */
+	ROC_NPC_TEMPLATE_INSERTION_PATTERN,
+	/* MCAM range reserved up front; rules placed at an app-chosen index. */
+	ROC_NPC_TEMPLATE_INSERTION_INDEX_WITH_PATTERN,
+};
+
+struct roc_npc_flow_queue_attr {
+	uint32_t size; /**< Number of in-flight async ops the queue can hold. */
+};
+
+struct roc_npc_pattern_template_attr {
+	uint32_t relaxed_matching : 1;
+	uint32_t ingress : 1;
+	uint32_t egress : 1;
+	uint32_t transfer : 1;
+	uint32_t reserved : 28;
+};
+
+struct roc_npc_actions_template_attr {
+	uint32_t ingress : 1;
+	uint32_t egress : 1;
+	uint32_t transfer : 1;
+	uint32_t reserved : 29;
+};
+
+struct roc_npc_template_table_attr {
+	struct roc_npc_attr flow_attr; /**< Priority / ingress / egress. */
+	uint32_t transfer : 1;	       /**< roc_npc_attr has no transfer bit. */
+	uint32_t reserved : 31;
+	enum roc_npc_template_table_insertion_type insertion_type;
+	uint32_t nb_flows; /**< Max rules the table can hold. */
+	void *cookie;	   /**< Opaque consumer context, retrievable per rule. */
+};
+
+/** Result of one completed async operation, returned by roc_npc_flow_pull. */
+struct roc_npc_flow_op_result {
+	void *user_data; /**< user_data passed at async_create/destroy time. */
+	int rc;		 /**< 0 on success, negative errno on failure. */
+};
+
+/* Opaque template/table/flow handles owned by the roc layer. */
+struct roc_npc_pattern_template;
+struct roc_npc_actions_template;
+struct roc_npc_template_table;
+struct roc_npc_template_flow;
+
+int __roc_api roc_npc_flow_configure(struct roc_npc *roc_npc, uint16_t nb_queues,
+				     const struct roc_npc_flow_queue_attr *queue_attr[]);
+
+struct roc_npc_pattern_template *__roc_api roc_npc_pattern_template_create(
+	struct roc_npc *roc_npc, const struct roc_npc_pattern_template_attr *attr,
+	const struct roc_npc_item_info pattern[], int *errcode);
+int __roc_api roc_npc_pattern_template_destroy(struct roc_npc *roc_npc,
+					       struct roc_npc_pattern_template *tmpl);
+
+struct roc_npc_actions_template *__roc_api roc_npc_actions_template_create(
+	struct roc_npc *roc_npc, const struct roc_npc_actions_template_attr *attr,
+	const struct roc_npc_action actions[], const struct roc_npc_action masks[], int *errcode);
+int __roc_api roc_npc_actions_template_destroy(struct roc_npc *roc_npc,
+					       struct roc_npc_actions_template *tmpl);
+
+struct roc_npc_template_table *__roc_api roc_npc_template_table_create(
+	struct roc_npc *roc_npc, const struct roc_npc_template_table_attr *attr,
+	struct roc_npc_pattern_template *pattern_templates[], uint8_t nb_pattern_templates,
+	struct roc_npc_actions_template *actions_templates[], uint8_t nb_actions_templates,
+	int *errcode);
+int __roc_api roc_npc_template_table_destroy(struct roc_npc *roc_npc,
+					     struct roc_npc_template_table *table);
+
+struct roc_npc_template_flow *__roc_api roc_npc_async_flow_create(
+	struct roc_npc *roc_npc, uint32_t queue_id, struct roc_npc_template_table *table,
+	const struct roc_npc_item_info pattern[], const struct roc_npc_action actions[],
+	uint16_t dst_pf_func, uint64_t npc_default_action, void *user_data, int *errcode);
+int __roc_api roc_npc_async_flow_destroy(struct roc_npc *roc_npc, uint32_t queue_id,
+					 struct roc_npc_template_flow *flow, void *user_data);
+struct roc_npc_template_flow *__roc_api roc_npc_async_flow_create_by_index_with_pattern(
+	struct roc_npc *roc_npc, uint32_t queue_id, struct roc_npc_template_table *table,
+	uint32_t rule_index, const struct roc_npc_item_info pattern[],
+	const struct roc_npc_action actions[], void *user_data, int *errcode);
+int __roc_api roc_npc_async_flow_actions_update(struct roc_npc *roc_npc, uint32_t queue_id,
+						struct roc_npc_template_flow *flow,
+						const struct roc_npc_action actions[],
+						void *user_data);
+int __roc_api roc_npc_flow_push(struct roc_npc *roc_npc, uint32_t queue_id);
+int __roc_api roc_npc_flow_pull(struct roc_npc *roc_npc, uint32_t queue_id,
+				struct roc_npc_flow_op_result res[], uint16_t n_res);
+bool __roc_api roc_npc_async_flow_resolve(struct roc_npc *roc_npc, void *handle,
+					  struct roc_npc_flow **flow);
+void *__roc_api roc_npc_async_flow_table_cookie(struct roc_npc_template_flow *flow);
 #endif /* _ROC_NPC_H_ */
diff --git a/drivers/common/cnxk/roc_npc_flow_template.c b/drivers/common/cnxk/roc_npc_flow_template.c
new file mode 100644
index 0000000000..5c10cfe39a
--- /dev/null
+++ b/drivers/common/cnxk/roc_npc_flow_template.c
@@ -0,0 +1,1326 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(C) 2024 Marvell.
+ */
+
+#include "roc_api.h"
+#include "roc_priv.h"
+
+/* Size of an action's conf for the supported set; -1 = nested/unsupported. */
+static int
+roc_npc_action_conf_size(enum roc_npc_action_type type)
+{
+	switch (type) {
+	case ROC_NPC_ACTION_TYPE_END:
+	case ROC_NPC_ACTION_TYPE_VOID:
+	case ROC_NPC_ACTION_TYPE_FLAG:
+	case ROC_NPC_ACTION_TYPE_DROP:
+	case ROC_NPC_ACTION_TYPE_COUNT:
+	case ROC_NPC_ACTION_TYPE_PF:
+	case ROC_NPC_ACTION_TYPE_VLAN_STRIP:
+		return 0; /* no conf */
+	case ROC_NPC_ACTION_TYPE_MARK:
+		return sizeof(struct roc_npc_action_mark);
+	case ROC_NPC_ACTION_TYPE_VF:
+		return sizeof(struct roc_npc_action_vf);
+	case ROC_NPC_ACTION_TYPE_PORT_ID:
+		return sizeof(struct roc_npc_action_port_id);
+	case ROC_NPC_ACTION_TYPE_QUEUE:
+		return sizeof(struct roc_npc_action_queue);
+	case ROC_NPC_ACTION_TYPE_VLAN_INSERT:
+		return sizeof(struct roc_npc_action_of_set_vlan_vid);
+	case ROC_NPC_ACTION_TYPE_VLAN_ETHTYPE_INSERT:
+		return sizeof(struct roc_npc_action_of_push_vlan);
+	case ROC_NPC_ACTION_TYPE_VLAN_PCP_INSERT:
+		return sizeof(struct roc_npc_action_of_set_vlan_pcp);
+	case ROC_NPC_ACTION_TYPE_AGE:
+		return sizeof(struct roc_npc_action_age);
+	default:
+		return -1; /* RSS handled separately; others unsupported */
+	}
+}
+
+/* Deep-copy an item spec/last/mask payload; *dst = NULL when nothing to copy. */
+static int
+roc_npc_dup_item_field(const void *src, uint32_t sz, const void **dst)
+{
+	void *copy;
+
+	*dst = NULL;
+	if (src == NULL || sz == 0)
+		return 0;
+	copy = plt_zmalloc(sz, 0);
+	if (copy == NULL)
+		return -ENOMEM;
+	memcpy(copy, src, sz);
+	*dst = copy;
+	return 0;
+}
+
+/* Deep-copy an action conf; RSS packs queue[]/key[] into one blob. */
+static void *
+roc_npc_dup_action_conf(const struct roc_npc_action *act)
+{
+	if (act->conf == NULL)
+		return NULL;
+
+	if (act->type == ROC_NPC_ACTION_TYPE_RSS) {
+		const struct roc_npc_action_rss *rss = act->conf;
+		size_t rss_queue_bytes = (size_t)rss->queue_num * sizeof(uint16_t);
+		size_t rss_key_bytes = rss->key_len;
+		size_t rss_blob_bytes = sizeof(*rss);
+		struct roc_npc_action_rss *rss_copy;
+		uint8_t *rss_blob;
+
+		if ((rss_queue_bytes && rss->queue == NULL) || (rss_key_bytes && rss->key == NULL))
+			return NULL;
+
+		rss_blob = plt_zmalloc(rss_blob_bytes + rss_queue_bytes + rss_key_bytes, 0);
+		if (rss_blob == NULL)
+			return NULL;
+		rss_copy = (struct roc_npc_action_rss *)rss_blob;
+		*rss_copy = *rss;
+		if (rss_queue_bytes) {
+			memcpy(rss_blob + rss_blob_bytes, rss->queue, rss_queue_bytes);
+			rss_copy->queue = (const uint16_t *)(rss_blob + rss_blob_bytes);
+		} else {
+			rss_copy->queue = NULL;
+		}
+		if (rss_key_bytes) {
+			memcpy(rss_blob + rss_blob_bytes + rss_queue_bytes, rss->key,
+			       rss_key_bytes);
+			rss_copy->key =
+				(const uint8_t *)(rss_blob + rss_blob_bytes + rss_queue_bytes);
+		} else {
+			rss_copy->key = NULL;
+		}
+		return rss_copy;
+	}
+
+	{
+		int conf_size = roc_npc_action_conf_size(act->type);
+		void *copy;
+
+		if (conf_size <= 0)
+			return NULL;
+		copy = plt_zmalloc(conf_size, 0);
+		if (copy != NULL)
+			memcpy(copy, act->conf, conf_size);
+		return copy;
+	}
+}
+
+/* Free the deep-copied items/actions arrays retained for a rule. */
+static void
+roc_npc_template_flow_free_rule(struct roc_npc_template_flow *flow)
+{
+	uint16_t k;
+
+	if (flow->items != NULL) {
+		for (k = 0; k < flow->nb_items; k++) {
+			plt_free((void *)(uintptr_t)flow->items[k].spec);
+			plt_free((void *)(uintptr_t)flow->items[k].last);
+			plt_free((void *)(uintptr_t)flow->items[k].mask);
+		}
+
+		plt_free(flow->items);
+		flow->items = NULL;
+	}
+	if (flow->actions != NULL) {
+		for (k = 0; k < flow->nb_actions; k++)
+			plt_free((void *)(uintptr_t)flow->actions[k].conf);
+
+		plt_free(flow->actions);
+		flow->actions = NULL;
+	}
+}
+
+int
+roc_npc_flow_configure(struct roc_npc *roc_npc, uint16_t nb_queues,
+		       const struct roc_npc_flow_queue_attr *queue_attr[])
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_flow_queue *queues;
+	int rc = -ENOMEM;
+	uint16_t i;
+
+	if (nb_queues == 0)
+		return 0;
+
+	if (queue_attr == NULL)
+		return -EINVAL;
+
+	if (npc->flow_queues != NULL) {
+		/* Don't tear down live queues while async ops are in flight. */
+		for (i = 0; i < npc->nb_flow_queues; i++) {
+			if (npc->flow_queues[i].head != npc->flow_queues[i].tail)
+				return -EBUSY;
+		}
+	}
+
+	if (npc->flow_queues != NULL) {
+		for (i = 0; i < npc->nb_flow_queues; i++)
+			plt_free(npc->flow_queues[i].ops);
+
+		plt_free(npc->flow_queues);
+		npc->flow_queues = NULL;
+		npc->nb_flow_queues = 0;
+	}
+
+	queues = plt_zmalloc(nb_queues * sizeof(struct roc_npc_flow_queue), 0);
+	if (queues == NULL)
+		return -ENOMEM;
+
+	for (i = 0; i < nb_queues; i++) {
+		if (queue_attr[i] == NULL || queue_attr[i]->size == 0) {
+			rc = -EINVAL;
+			goto err_free;
+		}
+		queues[i].size = plt_align32pow2(queue_attr[i]->size);
+		queues[i].ops = plt_zmalloc(queues[i].size * sizeof(struct roc_npc_async_op), 0);
+		if (queues[i].ops == NULL) {
+			rc = -ENOMEM;
+			goto err_free;
+		}
+		queues[i].head = 0;
+		queues[i].pushed = 0;
+		queues[i].tail = 0;
+	}
+
+	npc->flow_queues = queues;
+	npc->nb_flow_queues = nb_queues;
+	return 0;
+
+err_free:
+	for (i = 0; i < nb_queues; i++)
+		plt_free(queues[i].ops);
+
+	plt_free(queues);
+	return rc;
+}
+
+struct roc_npc_pattern_template *
+roc_npc_pattern_template_create(struct roc_npc *roc_npc,
+				const struct roc_npc_pattern_template_attr *attr,
+				const struct roc_npc_item_info pattern[], int *errcode)
+{
+	struct roc_npc_pattern_template *tmpl = NULL;
+	int i, nb_items = 0, rc = -ENOMEM;
+	uint16_t j;
+
+	PLT_SET_USED(roc_npc);
+
+	if (attr == NULL || pattern == NULL) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+
+	while (pattern[nb_items].type != ROC_NPC_ITEM_TYPE_END)
+		nb_items++;
+
+	/* Bound template size to the fixed-size rule arrays. */
+	if (nb_items > ROC_NPC_ITEM_TYPE_END) {
+		*errcode = -ENOTSUP;
+		return NULL;
+	}
+
+	tmpl = plt_zmalloc(sizeof(*tmpl), 0);
+	if (tmpl == NULL)
+		goto err;
+
+	tmpl->attr = *attr;
+	tmpl->nb_items = nb_items;
+
+	tmpl->pattern = plt_zmalloc((nb_items + 1) * sizeof(struct roc_npc_item_info), 0);
+	if (tmpl->pattern == NULL)
+		goto err;
+
+	/* Reserve up to 3 payload copies per item. */
+	tmpl->copies = plt_zmalloc(3 * (nb_items + 1) * sizeof(void *), 0);
+	if (tmpl->copies == NULL)
+		goto err;
+
+	for (i = 0; i < nb_items; i++) {
+		enum roc_npc_item_type t = pattern[i].type;
+		uint32_t sz = pattern[i].size;
+
+		/* RAW is not flat-copyable. */
+		if (t == ROC_NPC_ITEM_TYPE_RAW) {
+			rc = -ENOTSUP;
+			goto err;
+		}
+
+		tmpl->pattern[i].type = t;
+		tmpl->pattern[i].size = sz;
+		/* Track each copy for the error path. */
+		if (roc_npc_dup_item_field(pattern[i].spec, sz, &tmpl->pattern[i].spec))
+			goto err;
+		if (tmpl->pattern[i].spec != NULL)
+			tmpl->copies[tmpl->nb_copies++] = (void *)(uintptr_t)tmpl->pattern[i].spec;
+		if (roc_npc_dup_item_field(pattern[i].last, sz, &tmpl->pattern[i].last))
+			goto err;
+		if (tmpl->pattern[i].last != NULL)
+			tmpl->copies[tmpl->nb_copies++] = (void *)(uintptr_t)tmpl->pattern[i].last;
+		if (roc_npc_dup_item_field(pattern[i].mask, sz, &tmpl->pattern[i].mask))
+			goto err;
+		if (tmpl->pattern[i].mask != NULL)
+			tmpl->copies[tmpl->nb_copies++] = (void *)(uintptr_t)tmpl->pattern[i].mask;
+	}
+	tmpl->pattern[nb_items].type = ROC_NPC_ITEM_TYPE_END;
+	tmpl->refcnt = 0;
+
+	return tmpl;
+
+err:
+	if (tmpl != NULL) {
+		for (j = 0; j < tmpl->nb_copies; j++)
+			plt_free(tmpl->copies[j]);
+		plt_free(tmpl->copies);
+		plt_free(tmpl->pattern);
+		plt_free(tmpl);
+	}
+	*errcode = rc;
+	return NULL;
+}
+
+int
+roc_npc_pattern_template_destroy(struct roc_npc *roc_npc, struct roc_npc_pattern_template *tmpl)
+{
+	uint16_t i;
+
+	PLT_SET_USED(roc_npc);
+
+	if (tmpl == NULL)
+		return 0;
+
+	if (tmpl->refcnt != 0)
+		return -EBUSY;
+
+	for (i = 0; i < tmpl->nb_copies; i++)
+		plt_free(tmpl->copies[i]);
+
+	plt_free(tmpl->copies);
+	plt_free(tmpl->pattern);
+	plt_free(tmpl);
+	return 0;
+}
+
+struct roc_npc_actions_template *
+roc_npc_actions_template_create(struct roc_npc *roc_npc,
+				const struct roc_npc_actions_template_attr *attr,
+				const struct roc_npc_action actions[],
+				const struct roc_npc_action masks[], int *errcode)
+{
+	struct roc_npc_actions_template *tmpl = NULL;
+	int i, nb_actions = 0, rc = -ENOMEM;
+	uint16_t j;
+
+	PLT_SET_USED(roc_npc);
+
+	if (attr == NULL || actions == NULL) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+
+	while (actions[nb_actions].type != ROC_NPC_ACTION_TYPE_END)
+		nb_actions++;
+
+	/* Bound template size to the fixed-size rule arrays. */
+	if (nb_actions > ROC_NPC_MAX_ACTION_COUNT - 1) {
+		*errcode = -ENOTSUP;
+		return NULL;
+	}
+
+	tmpl = plt_zmalloc(sizeof(*tmpl), 0);
+	if (tmpl == NULL)
+		goto err;
+
+	tmpl->attr = *attr;
+	tmpl->nb_actions = nb_actions;
+
+	tmpl->actions = plt_zmalloc((nb_actions + 1) * sizeof(struct roc_npc_action), 0);
+	if (tmpl->actions == NULL)
+		goto err;
+
+	tmpl->masks = plt_zmalloc((nb_actions + 1) * sizeof(struct roc_npc_action), 0);
+	if (tmpl->masks == NULL)
+		goto err;
+
+	/* Reserve up to 2 conf copies per action. */
+	tmpl->copies = plt_zmalloc(2 * (nb_actions + 1) * sizeof(void *), 0);
+	if (tmpl->copies == NULL)
+		goto err;
+
+	for (i = 0; i < nb_actions; i++) {
+		enum roc_npc_action_type t = actions[i].type;
+		int sz = roc_npc_action_conf_size(t);
+
+		if (sz < 0 && t != ROC_NPC_ACTION_TYPE_RSS) {
+			rc = -ENOTSUP;
+			goto err;
+		}
+
+		tmpl->actions[i].type = t;
+		tmpl->actions[i].rss_repte_pf_func = actions[i].rss_repte_pf_func;
+
+		/* A malformed RSS conf is invalid input, not an allocation failure. */
+		if (t == ROC_NPC_ACTION_TYPE_RSS && actions[i].conf != NULL) {
+			const struct roc_npc_action_rss *rss = actions[i].conf;
+
+			if ((rss->queue_num && rss->queue == NULL) ||
+			    (rss->key_len && rss->key == NULL)) {
+				rc = -EINVAL;
+				goto err;
+			}
+		}
+
+		tmpl->actions[i].conf = roc_npc_dup_action_conf(&actions[i]);
+		if (tmpl->actions[i].conf == NULL && actions[i].conf != NULL &&
+		    (sz > 0 || t == ROC_NPC_ACTION_TYPE_RSS))
+			goto err;
+		if (tmpl->actions[i].conf != NULL)
+			tmpl->copies[tmpl->nb_copies++] = (void *)(uintptr_t)tmpl->actions[i].conf;
+
+		if (masks != NULL) {
+			if (masks[i].type != t) {
+				rc = -EINVAL;
+				goto err;
+			}
+			tmpl->masks[i].type = t;
+			/* RSS masks cannot carry a nested pointer. */
+			if (t == ROC_NPC_ACTION_TYPE_RSS) {
+				if (masks[i].conf != NULL) {
+					rc = -ENOTSUP;
+					goto err;
+				}
+				tmpl->masks[i].conf = NULL;
+			} else {
+				tmpl->masks[i].conf = roc_npc_dup_action_conf(&masks[i]);
+				if (tmpl->masks[i].conf == NULL && masks[i].conf != NULL && sz > 0)
+					goto err;
+				if (tmpl->masks[i].conf != NULL)
+					tmpl->copies[tmpl->nb_copies++] =
+						(void *)(uintptr_t)tmpl->masks[i].conf;
+			}
+		}
+	}
+
+	tmpl->actions[nb_actions].type = ROC_NPC_ACTION_TYPE_END;
+	tmpl->masks[nb_actions].type = ROC_NPC_ACTION_TYPE_END;
+	tmpl->refcnt = 0;
+
+	return tmpl;
+
+err:
+	if (tmpl != NULL) {
+		for (j = 0; j < tmpl->nb_copies; j++)
+			plt_free(tmpl->copies[j]);
+		plt_free(tmpl->copies);
+		plt_free(tmpl->masks);
+		plt_free(tmpl->actions);
+		plt_free(tmpl);
+	}
+	*errcode = rc;
+	return NULL;
+}
+
+int
+roc_npc_actions_template_destroy(struct roc_npc *roc_npc, struct roc_npc_actions_template *tmpl)
+{
+	uint16_t i;
+
+	PLT_SET_USED(roc_npc);
+
+	if (tmpl == NULL)
+		return 0;
+
+	if (tmpl->refcnt != 0)
+		return -EBUSY;
+
+	for (i = 0; i < tmpl->nb_copies; i++)
+		plt_free(tmpl->copies[i]);
+
+	plt_free(tmpl->copies);
+	plt_free(tmpl->masks);
+	plt_free(tmpl->actions);
+	plt_free(tmpl);
+	return 0;
+}
+
+struct roc_npc_template_table *
+roc_npc_template_table_create(struct roc_npc *roc_npc,
+			      const struct roc_npc_template_table_attr *attr,
+			      struct roc_npc_pattern_template *pattern_templates[],
+			      uint8_t nb_pattern_templates,
+			      struct roc_npc_actions_template *actions_templates[],
+			      uint8_t nb_actions_templates, int *errcode)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_template_table *table;
+	uint32_t nb_flows;
+	uint8_t i;
+
+	if (attr == NULL) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+
+	nb_flows = attr->nb_flows;
+
+	table = plt_zmalloc(sizeof(*table), 0);
+	if (table == NULL)
+		goto enomem;
+
+	table->insertion_type = attr->insertion_type;
+	table->nb_flows = nb_flows;
+	table->nb_live = 0;
+	table->cookie = attr->cookie;
+	plt_spinlock_init(&table->lock);
+
+	table->flow_attr = attr->flow_attr;
+	table->transfer = attr->transfer;
+
+	if (attr->insertion_type == ROC_NPC_TEMPLATE_INSERTION_INDEX_WITH_PATTERN) {
+		int resp_count = 0;
+		uint32_t j;
+		int rc;
+
+		table->mcam_ids = plt_zmalloc(nb_flows * sizeof(int), 0);
+		if (table->mcam_ids == NULL)
+			goto free_index;
+		for (j = 0; j < nb_flows; j++)
+			table->mcam_ids[j] = -1;
+
+		table->flow_pool = plt_zmalloc(nb_flows * sizeof(struct roc_npc_template_flow), 0);
+		if (table->flow_pool == NULL)
+			goto free_index;
+
+		rc = roc_npc_mcam_alloc_entries(roc_npc, 0, table->mcam_ids, nb_flows,
+						NPC_MCAM_ANY_PRIO, &resp_count, false);
+		if (rc != 0 || resp_count < (int)nb_flows) {
+			for (j = 0; j < (uint32_t)resp_count; j++) {
+				if (table->mcam_ids[j] != -1)
+					roc_npc_mcam_free_entry(roc_npc, table->mcam_ids[j]);
+			}
+
+			*errcode = -ENOSPC;
+			goto free_index;
+		}
+
+		for (j = 0; j < nb_flows; j++) {
+			table->flow_pool[j].table = table;
+			table->flow_pool[j].nb_items = 0;
+			table->flow_pool[j].nb_actions = 0;
+		}
+	}
+	/* PATTERN: nothing reserved; each push allocates at the right priority. */
+
+	if (nb_pattern_templates != 0) {
+		table->pattern_templates =
+			plt_zmalloc(nb_pattern_templates * sizeof(*table->pattern_templates), 0);
+		if (table->pattern_templates == NULL)
+			goto free_table;
+	}
+	if (nb_actions_templates != 0) {
+		table->actions_templates =
+			plt_zmalloc(nb_actions_templates * sizeof(*table->actions_templates), 0);
+		if (table->actions_templates == NULL)
+			goto free_table;
+	}
+
+	for (i = 0; i < nb_pattern_templates; i++) {
+		table->pattern_templates[i] = pattern_templates[i];
+		table->pattern_templates[i]->refcnt++;
+	}
+	table->nb_pattern_templates = nb_pattern_templates;
+	for (i = 0; i < nb_actions_templates; i++) {
+		table->actions_templates[i] = actions_templates[i];
+		table->actions_templates[i]->refcnt++;
+	}
+	table->nb_actions_templates = nb_actions_templates;
+
+	/* Track the table on the port so a flush can reconcile it. */
+	TAILQ_INIT(&table->live_flows);
+	table->next = npc->flow_tables;
+	npc->flow_tables = table;
+
+	return table;
+
+free_index:
+	plt_free(table->mcam_ids);
+	plt_free(table->flow_pool);
+	table->mcam_ids = NULL;
+	table->flow_pool = NULL;
+	plt_free(table->pattern_templates);
+	plt_free(table->actions_templates);
+	plt_free(table);
+	if (*errcode == 0)
+		*errcode = -ENOMEM;
+	return NULL;
+
+free_table:
+	if (attr->insertion_type == ROC_NPC_TEMPLATE_INSERTION_INDEX_WITH_PATTERN) {
+		uint32_t j;
+
+		if (table->mcam_ids != NULL) {
+			for (j = 0; j < nb_flows; j++) {
+				if (table->mcam_ids[j] != -1)
+					roc_npc_mcam_free_entry(roc_npc, table->mcam_ids[j]);
+			}
+
+			plt_free(table->mcam_ids);
+		}
+		plt_free(table->flow_pool);
+	}
+	plt_free(table->pattern_templates);
+	plt_free(table->actions_templates);
+	plt_free(table);
+
+enomem:
+	*errcode = -ENOMEM;
+	return NULL;
+}
+
+int
+roc_npc_template_table_destroy(struct roc_npc *roc_npc, struct roc_npc_template_table *table)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_template_table **pp;
+	uint8_t i;
+
+	if (table == NULL)
+		return 0;
+
+	if (table->insertion_type == ROC_NPC_TEMPLATE_INSERTION_PATTERN) {
+		if (table->nb_live != 0)
+			return -EBUSY;
+	} else {
+		uint32_t j;
+
+		/* A live (enabled) index rule still owns its MCAM entry. */
+		if (!table->hw_released && table->flow_pool != NULL) {
+			for (j = 0; j < table->nb_flows; j++) {
+				if (table->flow_pool[j].flow.enable != 0)
+					return -EBUSY;
+			}
+		}
+
+		if (table->mcam_ids != NULL) {
+			/* Skip if a flush already freed the entries. */
+			if (!table->hw_released) {
+				for (j = 0; j < table->nb_flows; j++) {
+					int mcam_id = table->mcam_ids[j];
+
+					if (mcam_id != -1)
+						roc_npc_mcam_free_entry(roc_npc, mcam_id);
+				}
+			}
+			plt_free(table->mcam_ids);
+		}
+		plt_free(table->flow_pool);
+	}
+
+	for (i = 0; i < table->nb_pattern_templates; i++)
+		table->pattern_templates[i]->refcnt--;
+
+	for (i = 0; i < table->nb_actions_templates; i++)
+		table->actions_templates[i]->refcnt--;
+
+	/* Unlink from the port's table list. */
+	pp = &npc->flow_tables;
+	while (*pp != NULL) {
+		if (*pp == table) {
+			*pp = table->next;
+			break;
+		}
+		pp = &(*pp)->next;
+	}
+
+	plt_free(table->pattern_templates);
+	plt_free(table->actions_templates);
+	plt_free(table);
+	return 0;
+}
+
+struct roc_npc_template_flow *
+roc_npc_async_flow_create(struct roc_npc *roc_npc, uint32_t queue_id,
+			  struct roc_npc_template_table *table,
+			  const struct roc_npc_item_info pattern[],
+			  const struct roc_npc_action actions[], uint16_t dst_pf_func,
+			  uint64_t npc_default_action, void *user_data, int *errcode)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_template_flow *flow = NULL;
+	int nb_items = 0, nb_actions = 0;
+	struct roc_npc_flow_queue *q;
+	struct roc_npc_async_op *op;
+	int k;
+
+	if (table == NULL || pattern == NULL || actions == NULL || npc->flow_queues == NULL ||
+	    queue_id >= npc->nb_flow_queues) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+	if (table->insertion_type != ROC_NPC_TEMPLATE_INSERTION_PATTERN) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+
+	while (pattern[nb_items].type != ROC_NPC_ITEM_TYPE_END)
+		nb_items++;
+	while (actions[nb_actions].type != ROC_NPC_ACTION_TYPE_END)
+		nb_actions++;
+	if (nb_items > ROC_NPC_ITEM_TYPE_END || nb_actions > ROC_NPC_MAX_ACTION_COUNT - 1) {
+		*errcode = -ENOTSUP;
+		return NULL;
+	}
+
+	q = &npc->flow_queues[queue_id];
+	if (q->head - q->tail >= q->size) {
+		*errcode = -ENOSPC;
+		return NULL;
+	}
+
+	flow = plt_zmalloc(sizeof(*flow), 0);
+	if (flow == NULL) {
+		*errcode = -ENOMEM;
+		return NULL;
+	}
+	flow->table = table;
+	flow->nb_items = nb_items;
+	flow->nb_actions = nb_actions;
+	flow->items = plt_zmalloc((nb_items + 1) * sizeof(struct roc_npc_item_info), 0);
+	if (flow->items == NULL) {
+		*errcode = -ENOMEM;
+		goto err;
+	}
+
+	flow->actions = plt_zmalloc((nb_actions + 1) * sizeof(struct roc_npc_action), 0);
+	if (flow->actions == NULL) {
+		*errcode = -ENOMEM;
+		goto err;
+	}
+
+	/* Deep-copy each item so push() owns a self-contained rule. */
+	for (k = 0; k < nb_items; k++) {
+		uint32_t sz = pattern[k].size;
+
+		flow->items[k].type = pattern[k].type;
+		flow->items[k].size = sz;
+		if (roc_npc_dup_item_field(pattern[k].spec, sz, &flow->items[k].spec) ||
+		    roc_npc_dup_item_field(pattern[k].last, sz, &flow->items[k].last) ||
+		    roc_npc_dup_item_field(pattern[k].mask, sz, &flow->items[k].mask)) {
+			*errcode = -ENOMEM;
+			goto err;
+		}
+	}
+	flow->items[nb_items].type = ROC_NPC_ITEM_TYPE_END;
+
+	/* Deep-copy each action conf. */
+	for (k = 0; k < nb_actions; k++) {
+		int sz = roc_npc_action_conf_size(actions[k].type);
+
+		flow->actions[k].type = actions[k].type;
+		flow->actions[k].rss_repte_pf_func = actions[k].rss_repte_pf_func;
+
+		/* A malformed RSS conf (non-zero length with a NULL nested
+		 * pointer) is invalid input, not an allocation failure.
+		 */
+		if (actions[k].type == ROC_NPC_ACTION_TYPE_RSS && actions[k].conf != NULL) {
+			const struct roc_npc_action_rss *rss = actions[k].conf;
+
+			if ((rss->queue_num && rss->queue == NULL) ||
+			    (rss->key_len && rss->key == NULL)) {
+				*errcode = -EINVAL;
+				goto err;
+			}
+		}
+
+		flow->actions[k].conf = roc_npc_dup_action_conf(&actions[k]);
+		/* A NULL copy from a non-NULL conf now means the deep copy failed. */
+		if (flow->actions[k].conf == NULL && actions[k].conf != NULL &&
+		    (sz > 0 || actions[k].type == ROC_NPC_ACTION_TYPE_RSS)) {
+			*errcode = -ENOMEM;
+			goto err;
+		}
+	}
+	flow->actions[nb_actions].type = ROC_NPC_ACTION_TYPE_END;
+
+	/* Capture the rule attributes used by push() to program hardware. */
+	flow->attr = table->flow_attr;
+	if (table->transfer)
+		flow->attr.ingress = 1;
+	flow->dst_pf_func = dst_pf_func;
+	flow->npc_default_action = npc_default_action;
+	/* Capture the caller's RSS flow key so batched rules keep their own. */
+	flow->flowkey_cfg = roc_npc->flowkey_cfg_state;
+
+	op = &q->ops[q->head & (q->size - 1)];
+	op->flow = flow;
+	op->is_create = true;
+	op->is_update = false;
+	op->user_data = user_data;
+	op->done = false;
+	op->rc = 0;
+	q->head++;
+
+	return flow;
+
+err:
+	roc_npc_template_flow_free_rule(flow);
+	plt_free(flow);
+	return NULL;
+}
+
+int
+roc_npc_async_flow_destroy(struct roc_npc *roc_npc, uint32_t queue_id,
+			   struct roc_npc_template_flow *flow, void *user_data)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_flow_queue *q;
+	struct roc_npc_async_op *op;
+
+	if (flow == NULL || npc->flow_queues == NULL || queue_id >= npc->nb_flow_queues)
+		return -EINVAL;
+
+	q = &npc->flow_queues[queue_id];
+	if (q->head - q->tail >= q->size)
+		return -ENOSPC;
+
+	op = &q->ops[q->head & (q->size - 1)];
+	op->flow = flow;
+	op->is_create = false;
+	op->is_update = false;
+	op->user_data = user_data;
+	op->done = false;
+	op->rc = 0;
+	q->head++;
+
+	return 0;
+}
+
+/* Create a rule at an app-chosen index in an INDEX_WITH_PATTERN table. */
+struct roc_npc_template_flow *
+roc_npc_async_flow_create_by_index_with_pattern(struct roc_npc *roc_npc, uint32_t queue_id,
+						struct roc_npc_template_table *table,
+						uint32_t rule_index,
+						const struct roc_npc_item_info pattern[],
+						const struct roc_npc_action actions[],
+						void *user_data, int *errcode)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_template_flow *flow;
+	uint32_t mark_save, vtag_save;
+	struct roc_npc_flow_queue *q;
+	struct roc_npc_async_op *op;
+	struct roc_npc_attr attr;
+	int rc;
+
+	if (table == NULL || pattern == NULL || actions == NULL || npc->flow_queues == NULL ||
+	    queue_id >= npc->nb_flow_queues) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+	if (table->insertion_type != ROC_NPC_TEMPLATE_INSERTION_INDEX_WITH_PATTERN) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+	if (rule_index >= table->nb_flows) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+
+	/* Reject creates into a flushed table. */
+	if (table->hw_released) {
+		*errcode = -EINVAL;
+		return NULL;
+	}
+
+	q = &npc->flow_queues[queue_id];
+	if (q->head - q->tail >= q->size) {
+		*errcode = -ENOSPC;
+		return NULL;
+	}
+
+	/* App owns the index: use the reserved pool slot directly. */
+	flow = &table->flow_pool[rule_index];
+	flow->table = table;
+	flow->slot = rule_index;
+	memset(&flow->flow, 0, sizeof(flow->flow));
+
+	attr = table->flow_attr;
+	if (table->transfer)
+		attr.ingress = 1;
+
+	/* Snapshot MARK/VLAN refcounts before parsing. */
+	mark_save = npc->mark_actions;
+	vtag_save = npc->vtag_strip_actions;
+
+	rc = roc_npc_flow_parse(roc_npc, &attr, pattern, actions, &flow->flow);
+	if (rc != 0) {
+		npc->mark_actions = mark_save;
+		npc->vtag_strip_actions = vtag_save;
+		*errcode = rc;
+		return NULL;
+	}
+
+	roc_npc_mcam_write_rx_finalize(roc_npc, &flow->flow);
+	flow->flow.mcam_id = table->mcam_ids[rule_index];
+	flow->flow.enable = 1;
+
+	op = &q->ops[q->head & (q->size - 1)];
+	op->flow = flow;
+	op->is_create = true;
+	op->is_update = false;
+	op->user_data = user_data;
+	op->done = false;
+	op->rc = 0;
+	q->head++;
+
+	return flow;
+}
+
+/* Update a rule's actions in place and swap them in at push. */
+int
+roc_npc_async_flow_actions_update(struct roc_npc *roc_npc, uint32_t queue_id,
+				  struct roc_npc_template_flow *flow,
+				  const struct roc_npc_action actions[], void *user_data)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_item_info end_pattern[1];
+	struct roc_npc_template_table *table;
+	struct roc_npc_flow *fptr, tmpflow;
+	uint32_t mark_save, vtag_save;
+	struct roc_npc_flow_queue *q;
+	struct roc_npc_async_op *op;
+	struct roc_npc_attr attr;
+	uint16_t old_match_id;
+	bool old_vtag;
+	int rc;
+
+	if (flow == NULL || actions == NULL || npc->flow_queues == NULL ||
+	    queue_id >= npc->nb_flow_queues)
+		return -EINVAL;
+
+	table = flow->table;
+	if (table == NULL)
+		return -EINVAL;
+	/* PATTERN rules have roc_flow only after create commit. */
+	if (table->insertion_type == ROC_NPC_TEMPLATE_INSERTION_PATTERN && flow->roc_flow == NULL)
+		return -EINVAL;
+
+	/* Reject a second update until the pending one is flushed. */
+	if (flow->pending_update.pending)
+		return -EBUSY;
+
+	q = &npc->flow_queues[queue_id];
+	if (q->head - q->tail >= q->size)
+		return -ENOSPC;
+
+	end_pattern[0].type = ROC_NPC_ITEM_TYPE_END;
+
+	attr = table->flow_attr;
+	if (table->transfer)
+		attr.ingress = 1;
+
+	memset(&tmpflow, 0, sizeof(tmpflow));
+
+	/* Resolve the live flow before parsing the new actions. */
+	fptr = flow->roc_flow ? flow->roc_flow : &flow->flow;
+	old_match_id = (fptr->npc_action >> NPC_RX_ACT_MATCH_OFFSET) & NPC_RX_ACT_MATCH_MASK;
+	old_vtag = fptr->nix_intf == ROC_NPC_INTF_RX && fptr->vtag_action != 0;
+	mark_save = npc->mark_actions;
+	vtag_save = npc->vtag_strip_actions;
+
+	rc = roc_npc_flow_parse(roc_npc, &attr, end_pattern, actions, &tmpflow);
+	if (rc != 0) {
+		/* Undo any refcount bumps a partial parse made on failure. */
+		npc->mark_actions = mark_save;
+		npc->vtag_strip_actions = vtag_save;
+		return rc;
+	}
+
+	/* Stash the parsed update so push can commit or roll it back. */
+	flow->pending_update.npc_action = tmpflow.npc_action;
+	flow->pending_update.npc_action2 = tmpflow.npc_action2;
+	flow->pending_update.vtag_action = tmpflow.vtag_action;
+	flow->pending_update.recv_queue = tmpflow.recv_queue;
+	flow->pending_update.old_match_id = old_match_id != 0;
+	flow->pending_update.old_vtag = old_vtag;
+	flow->pending_update.new_match_id =
+		((tmpflow.npc_action >> NPC_RX_ACT_MATCH_OFFSET) & NPC_RX_ACT_MATCH_MASK) != 0;
+	flow->pending_update.new_vtag =
+		tmpflow.nix_intf == ROC_NPC_INTF_RX && tmpflow.vtag_action != 0;
+	flow->pending_update.pending = true;
+
+	op = &q->ops[q->head & (q->size - 1)];
+	op->flow = flow;
+	op->is_create = false;
+	op->is_update = true;
+	op->user_data = user_data;
+	op->done = false;
+	op->rc = 0;
+	q->head++;
+
+	return 0;
+}
+
+/* Commit one retained create to hardware. */
+static int
+roc_npc_flow_commit_create(struct roc_npc *roc_npc, struct roc_npc_template_flow *flow)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	uint32_t mark_save, vtag_save;
+	struct roc_npc_flow *rflow;
+	int errcode = 0;
+
+	/* Restore this rule's RSS flow key before programming (see create). */
+	roc_npc->flowkey_cfg_state = flow->flowkey_cfg;
+
+	/* Restore MARK/VLAN refcounts if create fails. */
+	mark_save = npc->mark_actions;
+	vtag_save = npc->vtag_strip_actions;
+
+	rflow = roc_npc_flow_create(roc_npc, &flow->attr, flow->items, flow->actions,
+				    flow->dst_pf_func, flow->npc_default_action, &errcode);
+	if (rflow == NULL) {
+		npc->mark_actions = mark_save;
+		npc->vtag_strip_actions = vtag_save;
+		return errcode ? errcode : -EINVAL;
+	}
+
+	flow->roc_flow = rflow;
+	return 0;
+}
+
+/* Apply a pending actions update and roll it back on failure. */
+static int
+roc_npc_flow_apply_pattern_update(struct roc_npc *roc_npc, struct npc *npc,
+				  struct roc_npc_template_flow *flow)
+{
+	struct roc_npc_flow *fptr = flow->roc_flow;
+	uint64_t save_action2, save_action, save_vtag;
+	uint32_t save_rq;
+	int rc;
+
+	if (fptr == NULL)
+		return -EINVAL;
+
+	save_action2 = fptr->npc_action2;
+	save_action = fptr->npc_action;
+	save_vtag = fptr->vtag_action;
+	save_rq = fptr->recv_queue;
+
+	fptr->npc_action = flow->pending_update.npc_action;
+	fptr->npc_action2 = flow->pending_update.npc_action2;
+	fptr->vtag_action = flow->pending_update.vtag_action;
+	fptr->recv_queue = flow->pending_update.recv_queue;
+	fptr->enable = 1;
+	roc_npc_mcam_write_rx_finalize(roc_npc, fptr);
+	rc = roc_npc_mcam_write_entry(roc_npc, fptr);
+	if (rc == 0) {
+		/* Drop the old action set's refcount contribution. */
+		if (flow->pending_update.old_match_id && npc->mark_actions > 0)
+			npc->mark_actions--;
+		if (flow->pending_update.old_vtag && npc->vtag_strip_actions > 0)
+			npc->vtag_strip_actions--;
+	} else {
+		/* Restore the live entry and undo the new contribution. */
+		fptr->npc_action = save_action;
+		fptr->npc_action2 = save_action2;
+		fptr->vtag_action = save_vtag;
+		fptr->recv_queue = save_rq;
+		if (flow->pending_update.new_match_id && npc->mark_actions > 0)
+			npc->mark_actions--;
+		if (flow->pending_update.new_vtag && npc->vtag_strip_actions > 0)
+			npc->vtag_strip_actions--;
+	}
+	return rc;
+}
+
+int
+roc_npc_flow_push(struct roc_npc *roc_npc, uint32_t queue_id)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_flow_queue *q;
+	uint32_t i;
+
+	if (npc->flow_queues == NULL || queue_id >= npc->nb_flow_queues)
+		return -EINVAL;
+
+	q = &npc->flow_queues[queue_id];
+
+	for (i = q->pushed; i != q->head; i++) {
+		struct roc_npc_async_op *op = &q->ops[i & (q->size - 1)];
+		struct roc_npc_template_flow *flow = op->flow;
+		struct roc_npc_template_table *table = flow->table;
+		int rc;
+
+		if (table->insertion_type == ROC_NPC_TEMPLATE_INSERTION_PATTERN) {
+			if (op->is_update) {
+				rc = roc_npc_flow_apply_pattern_update(roc_npc, npc, flow);
+				/* Clear the pending update after push. */
+				flow->pending_update.pending = false;
+			} else if (op->is_create) {
+				rc = roc_npc_flow_commit_create(roc_npc, flow);
+				/* Rule arrays are consumed on push. */
+				roc_npc_template_flow_free_rule(flow);
+				if (rc == 0) {
+					plt_spinlock_lock(&table->lock);
+					table->nb_live++;
+					TAILQ_INSERT_TAIL(&table->live_flows, flow, link);
+					plt_spinlock_unlock(&table->lock);
+				}
+				/* Keep the handle valid on failure. */
+			} else if (flow->roc_flow == NULL) {
+				/* Never committed (or a failed create): just free it. */
+				rc = 0;
+				roc_npc_template_flow_free_rule(flow);
+				plt_free(flow);
+			} else {
+				uint16_t match_id;
+				uint8_t nix_intf;
+				bool had_vtag;
+
+				/* Capture match_id and interface before destroy. */
+				match_id = (flow->roc_flow->npc_action >> NPC_RX_ACT_MATCH_OFFSET) &
+					   NPC_RX_ACT_MATCH_MASK;
+				nix_intf = flow->roc_flow->nix_intf;
+				had_vtag = flow->roc_flow->vtag_action != 0;
+
+				rc = roc_npc_flow_destroy(roc_npc, flow->roc_flow);
+				if (rc == 0 && match_id)
+					roc_npc_mark_actions_sub_return(roc_npc, 1);
+				if (rc == 0 && nix_intf == ROC_NPC_INTF_RX && had_vtag)
+					roc_npc_vtag_actions_sub_return(roc_npc, 1);
+				if (rc == 0) {
+					plt_spinlock_lock(&table->lock);
+					table->nb_live--;
+					TAILQ_REMOVE(&table->live_flows, flow, link);
+					plt_spinlock_unlock(&table->lock);
+					flow->roc_flow = NULL;
+					roc_npc_template_flow_free_rule(flow);
+					plt_free(flow);
+				}
+				/* Keep the handle on failure so the caller can retry. */
+			}
+		} else {
+			/* INDEX_WITH_PATTERN: entry fixed at the app-chosen index. */
+			struct roc_npc_flow *fptr = &flow->flow;
+			bool destroy = !op->is_create && !op->is_update;
+			uint16_t match_id = (fptr->npc_action >> NPC_RX_ACT_MATCH_OFFSET) &
+					    NPC_RX_ACT_MATCH_MASK;
+			uint64_t save_action2 = fptr->npc_action2;
+			uint64_t save_action = fptr->npc_action;
+			uint64_t save_vtag = fptr->vtag_action;
+			uint32_t save_rq = fptr->recv_queue;
+
+			/* Swap in the stashed update before writing hardware. */
+			if (op->is_update) {
+				fptr->npc_action = flow->pending_update.npc_action;
+				fptr->npc_action2 = flow->pending_update.npc_action2;
+				fptr->vtag_action = flow->pending_update.vtag_action;
+				fptr->recv_queue = flow->pending_update.recv_queue;
+			}
+
+			/* Set enable before writing hardware. */
+			fptr->enable = destroy ? 0 : 1;
+			if (op->is_update)
+				roc_npc_mcam_write_rx_finalize(roc_npc, fptr);
+			rc = roc_npc_mcam_write_entry(roc_npc, fptr);
+			if (rc != 0) {
+				/* Restore the previous enable state on failure. */
+				fptr->enable = destroy ? 1 : 0;
+				/* Roll back the swapped-in action words too. */
+				if (op->is_update) {
+					fptr->npc_action = save_action;
+					fptr->npc_action2 = save_action2;
+					fptr->vtag_action = save_vtag;
+					fptr->recv_queue = save_rq;
+				}
+			}
+
+			/* Commit or roll back the update refcount change. */
+			if (op->is_update) {
+				if (rc == 0) {
+					if (flow->pending_update.old_match_id &&
+					    npc->mark_actions > 0)
+						npc->mark_actions--;
+					if (flow->pending_update.old_vtag &&
+					    npc->vtag_strip_actions > 0)
+						npc->vtag_strip_actions--;
+				} else {
+					if (flow->pending_update.new_match_id &&
+					    npc->mark_actions > 0)
+						npc->mark_actions--;
+					if (flow->pending_update.new_vtag &&
+					    npc->vtag_strip_actions > 0)
+						npc->vtag_strip_actions--;
+				}
+				flow->pending_update.pending = false;
+			}
+
+			/* Release the COUNT counter on destroy. */
+			if (destroy && rc == 0 && fptr->use_ctr &&
+			    fptr->ctr_id != NPC_COUNTER_NONE) {
+				roc_npc_mcam_free_counter(roc_npc, fptr->ctr_id);
+				fptr->ctr_id = NPC_COUNTER_NONE;
+			}
+
+			/* Balance the mark_actions refcount for MARK/FLAG rules. */
+			if (destroy && rc == 0 && match_id)
+				roc_npc_mark_actions_sub_return(roc_npc, 1);
+
+			/* Balance the vtag_strip_actions refcount for RX VLAN strip rules. */
+			if (destroy && rc == 0 && fptr->nix_intf == ROC_NPC_INTF_RX &&
+			    fptr->vtag_action != 0)
+				roc_npc_vtag_actions_sub_return(roc_npc, 1);
+		}
+
+		op->rc = rc;
+		op->done = true;
+	}
+
+	q->pushed = q->head;
+	return 0;
+}
+
+int
+roc_npc_flow_pull(struct roc_npc *roc_npc, uint32_t queue_id, struct roc_npc_flow_op_result res[],
+		  uint16_t n_res)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_flow_queue *q;
+	uint16_t cnt = 0;
+
+	if (npc->flow_queues == NULL || queue_id >= npc->nb_flow_queues)
+		return -EINVAL;
+
+	q = &npc->flow_queues[queue_id];
+
+	/* Return ops that were pushed but not yet reported. */
+	while (q->tail != q->pushed && cnt < n_res) {
+		struct roc_npc_async_op *op = &q->ops[q->tail & (q->size - 1)];
+
+		res[cnt].user_data = op->user_data;
+		res[cnt].rc = op->rc;
+		cnt++;
+		q->tail++;
+	}
+
+	return cnt;
+}
+
+/* Resolve a candidate handle to a template flow or a sync flow. */
+bool
+roc_npc_async_flow_resolve(struct roc_npc *roc_npc, void *handle, struct roc_npc_flow **flow)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	struct roc_npc_template_flow *cand = handle;
+	struct roc_npc_template_table *table;
+	uint16_t i;
+
+	if (handle == NULL || flow == NULL)
+		return false;
+
+	for (table = npc->flow_tables; table != NULL; table = table->next) {
+		struct roc_npc_template_flow *match;
+
+		/* Integer range check: pointer comparison across objects is UB. */
+		if (table->flow_pool != NULL) {
+			uintptr_t p = (uintptr_t)cand;
+			uintptr_t base = (uintptr_t)table->flow_pool;
+			uintptr_t end = (uintptr_t)(table->flow_pool + table->nb_flows);
+
+			if (p >= base && p < end) {
+				*flow = &cand->flow;
+				return true;
+			}
+		}
+
+		plt_spinlock_lock(&table->lock);
+		/* clang-format off */
+		TAILQ_FOREACH(match, &table->live_flows, link) {
+			if (match == cand) {
+				plt_spinlock_unlock(&table->lock);
+				*flow = cand->roc_flow;
+				return true;
+			}
+		}
+		/* clang-format on */
+		plt_spinlock_unlock(&table->lock);
+	}
+
+	/* Handle still queued: a PATTERN rule not yet committed. */
+	for (i = 0; i < npc->nb_flow_queues; i++) {
+		struct roc_npc_flow_queue *q = &npc->flow_queues[i];
+		uint32_t j;
+
+		for (j = q->tail; j != q->head; j++) {
+			struct roc_npc_async_op *op = &q->ops[j & (q->size - 1)];
+
+			if (op->flow == cand) {
+				*flow = cand->roc_flow;
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+/* Return the table cookie used to recover update context. */
+void *
+roc_npc_async_flow_table_cookie(struct roc_npc_template_flow *flow)
+{
+	if (flow == NULL || flow->table == NULL)
+		return NULL;
+
+	return flow->table->cookie;
+}
+
+/* Reconcile template-table bookkeeping after flush or port close. */
+void
+npc_template_tables_flush_reconcile(struct npc *npc)
+{
+	struct roc_npc_template_table *table;
+	struct roc_npc_template_flow *flow;
+	struct roc_npc_flow *fptr;
+	uint16_t match_id;
+	uint32_t j;
+
+	for (table = npc->flow_tables; table != NULL; table = table->next) {
+		plt_spinlock_lock(&table->lock);
+		while ((flow = TAILQ_FIRST(&table->live_flows)) != NULL) {
+			TAILQ_REMOVE(&table->live_flows, flow, link);
+			/* roc_flow is already freed by free_all_resources. */
+			roc_npc_template_flow_free_rule(flow);
+			plt_free(flow);
+		}
+
+		/* INDEX_WITH_PATTERN rules live in flow_pool[]; clear stale state. */
+		if (table->flow_pool != NULL) {
+			for (j = 0; j < table->nb_flows; j++) {
+				fptr = &table->flow_pool[j].flow;
+
+				if (fptr->enable == 0)
+					continue;
+
+				match_id = (fptr->npc_action >> NPC_RX_ACT_MATCH_OFFSET) &
+					   NPC_RX_ACT_MATCH_MASK;
+				if (match_id && npc->mark_actions > 0)
+					npc->mark_actions--;
+
+				if (fptr->nix_intf == ROC_NPC_INTF_RX && fptr->vtag_action != 0 &&
+				    npc->vtag_strip_actions > 0)
+					npc->vtag_strip_actions--;
+
+				memset(fptr, 0, sizeof(*fptr));
+				fptr->ctr_id = NPC_COUNTER_NONE;
+			}
+		}
+
+		table->nb_live = 0;
+		table->hw_released = true;
+		plt_spinlock_unlock(&table->lock);
+	}
+}
diff --git a/drivers/common/cnxk/roc_npc_mcam.c b/drivers/common/cnxk/roc_npc_mcam.c
index 286cfd2dbd..34665f1bc3 100644
--- a/drivers/common/cnxk/roc_npc_mcam.c
+++ b/drivers/common/cnxk/roc_npc_mcam.c
@@ -714,7 +714,6 @@ npc_mcam_fetch_kex_cfg(struct npc *npc)
 	struct mbox *mbox = mbox_get(npc->mbox);
 	int rc = 0;
 
-
 	if (!roc_model_is_cn20k()) {
 		mbox_alloc_msg_npc_get_kex_cfg(mbox);
 		rc = mbox_process_msg(mbox, (void *)&kex_rsp);
@@ -786,6 +785,46 @@ npc_mcam_set_channel(struct roc_npc_flow *flow, struct npc_cn20k_mcam_write_entr
 	flow->mcam_mask[0] |= (uint64_t)mask;
 }
 
+/* Fold RQ into the action word and program the RX channel into kw[0],
+ * matching the RX steps the sync path does in npc_mcam_alloc_and_write().
+ */
+void
+roc_npc_mcam_write_rx_finalize(struct roc_npc *roc_npc, struct roc_npc_flow *flow)
+{
+	struct npc *npc = roc_npc_to_npc_priv(roc_npc);
+	uint16_t chan, mask;
+	uint64_t op;
+
+	if (flow->nix_intf != NIX_INTF_RX)
+		return;
+
+	/* Set the RQ in the action index field, but only for UCAST* ops that
+	 * use it as a receive queue. Clear then set so a re-finalize on an
+	 * async QUEUE update leaves no stale bits.
+	 */
+	op = flow->npc_action & 0xFULL;
+	if (op == NIX_RX_ACTIONOP_UCAST || op == NIX_RX_ACTIONOP_UCAST_IPSEC ||
+	    op == NIX_RX_ACTIONOP_UCAST_CPT) {
+		flow->npc_action &= ~GENMASK_ULL(39, 20);
+		flow->npc_action |= ((uint64_t)flow->recv_queue << 20) & GENMASK_ULL(39, 20);
+	}
+
+	/* Program the RX channel into kw[0]. */
+	chan = npc->channel;
+	mask = BIT_ULL(12) - 1;
+
+	flow->mcam_data[0] &= ~(GENMASK(11, 0));
+	flow->mcam_mask[0] &= ~(GENMASK(11, 0));
+
+	if (roc_model_runtime_is_cn10k() && !roc_npc_action_is_rx_inline(flow->npc_action)) {
+		chan = (chan & NIX_CHAN_CPT_X2P_MASK);
+		mask = (mask & NIX_CHAN_CPT_X2P_MASK);
+	}
+
+	flow->mcam_data[0] |= (uint64_t)chan;
+	flow->mcam_mask[0] |= (uint64_t)mask;
+}
+
 #define NPC_PF_FUNC_WIDTH    2
 #define NPC_KEX_PF_FUNC_MASK 0xFFFF
 
@@ -1430,5 +1469,9 @@ npc_flow_free_all_resources(struct npc *npc)
 			plt_free(flow);
 		}
 	}
+
+	/* Reconcile template-table bookkeeping for all teardown paths. */
+	npc_template_tables_flush_reconcile(npc);
+
 	return rc;
 }
diff --git a/drivers/common/cnxk/roc_npc_priv.h b/drivers/common/cnxk/roc_npc_priv.h
index c7e7d2d8f6..c6ab033d9f 100644
--- a/drivers/common/cnxk/roc_npc_priv.h
+++ b/drivers/common/cnxk/roc_npc_priv.h
@@ -427,6 +427,99 @@ struct npc_age_flow_entry {
 
 TAILQ_HEAD(npc_age_flow_list_head, npc_age_flow_entry);
 
+/* Queue-based async template flow engine types. */
+
+/* One in-flight async op in a queue ring. */
+struct roc_npc_async_op {
+	struct roc_npc_template_flow *flow; /**< Rule the op acts on. */
+	void *user_data;		    /**< Echoed back by flow_pull. */
+	bool is_create;			    /**< true = create, false = destroy. */
+	bool is_update;			    /**< true = in-place actions rewrite. */
+	bool done;			    /**< Set once pushed to hardware. */
+	int rc;				    /**< Result captured at push time. */
+};
+
+/* Fixed-size op ring; head/pushed/tail are free-running indices. */
+struct roc_npc_flow_queue {
+	struct roc_npc_async_op *ops; /**< size entries, power-of-two. */
+	uint32_t size;		      /**< Ring capacity (power of two). */
+	uint32_t head;		      /**< Next slot to enqueue into. */
+	uint32_t pushed;	      /**< Ops up to here are committed. */
+	uint32_t tail;		      /**< Next slot to return via pull. */
+};
+
+/* A deep-copied, retained rule awaiting (or holding) a hardware entry. */
+struct roc_npc_template_flow {
+	struct roc_npc_template_table *table; /**< Owning table. */
+	struct roc_npc_flow *roc_flow;	      /**< HW flow (PATTERN, once committed). */
+	struct roc_npc_item_info *items;      /**< Deep-copied merged pattern. */
+	struct roc_npc_action *actions;	      /**< Deep-copied merged actions. */
+	struct roc_npc_flow flow;	      /**< Embedded HW flow (INDEX path). */
+	struct roc_npc_attr attr;	      /**< Priority / ingress / egress. */
+	uint64_t npc_default_action;	      /**< Resolved port default action. */
+	uint32_t flowkey_cfg;		      /**< RSS flow key captured at create. */
+	uint32_t slot;			      /**< Pre-reserved index (INDEX path). */
+	uint16_t dst_pf_func;		      /**< Resolved destination pf_func. */
+	uint16_t nb_items;		      /**< Item count (excluding END). */
+	uint16_t nb_actions;		      /**< Action count (excluding END). */
+	/* Pending in-place actions update, applied at push. */
+	struct {
+		uint64_t npc_action;
+		uint64_t npc_action2;
+		uint64_t vtag_action;
+		uint32_t recv_queue;
+		bool pending;	   /**< An update is enqueued, awaiting push. */
+		bool old_match_id; /**< Old action set carried a mark/match_id. */
+		bool old_vtag;	   /**< Old action set carried RX vtag strip. */
+		bool new_match_id; /**< New action set carries a mark/match_id. */
+		bool new_vtag;	   /**< New action set carries RX vtag strip. */
+	} pending_update;
+	TAILQ_ENTRY(roc_npc_template_flow) link;
+};
+
+TAILQ_HEAD(roc_npc_template_flow_list, roc_npc_template_flow);
+
+/* Pattern template blueprint. */
+struct roc_npc_pattern_template {
+	struct roc_npc_pattern_template_attr attr;
+	struct roc_npc_item_info *pattern; /**< nb_items + END terminator. */
+	void **copies;			   /**< Tracked payload allocations. */
+	uint16_t nb_copies;		   /**< Entries used in copies[]. */
+	uint16_t nb_items;		   /**< Item count (excluding END). */
+	uint32_t refcnt;		   /**< Tables referencing this template. */
+};
+
+/* Actions template blueprint. */
+struct roc_npc_actions_template {
+	struct roc_npc_actions_template_attr attr;
+	struct roc_npc_action *actions; /**< nb_actions + END terminator. */
+	struct roc_npc_action *masks;	/**< Per-action masks (optional). */
+	void **copies;			/**< Tracked conf allocations. */
+	uint16_t nb_copies;		/**< Entries used in copies[]. */
+	uint16_t nb_actions;		/**< Action count (excluding END). */
+	uint32_t refcnt;		/**< Tables referencing this template. */
+};
+
+/* Template table and rule storage. */
+struct roc_npc_template_table {
+	int *mcam_ids;				 /**< Reserved entries (index). */
+	struct roc_npc_template_flow *flow_pool; /**< Per-index rule slots. */
+	struct roc_npc_pattern_template **pattern_templates;
+	struct roc_npc_actions_template **actions_templates;
+	struct roc_npc_template_flow_list live_flows; /**< Live pattern rules. */
+	struct roc_npc_template_table *next;	      /**< Port table list link. */
+	void *cookie;				      /**< Opaque consumer context. */
+	struct roc_npc_attr flow_attr;		      /**< Priority/ingress/egress. */
+	enum roc_npc_template_table_insertion_type insertion_type;
+	plt_spinlock_t lock;	      /**< Guards live_flows/nb_live. */
+	uint32_t nb_flows;	      /**< Max rules the table holds. */
+	uint32_t nb_live;	      /**< Live pattern rules count. */
+	uint32_t transfer;	      /**< Transfer (port) rule. */
+	uint8_t nb_pattern_templates; /**< pattern_templates entries. */
+	uint8_t nb_actions_templates; /**< actions_templates entries. */
+	bool hw_released;	      /**< Entries freed by a flush. */
+};
+
 struct npc {
 	struct mbox *mbox;			/* Mbox */
 	uint64_t keyx_supp_nmask[NPC_MAX_INTF]; /* nibble mask */
@@ -459,6 +552,10 @@ struct npc {
 	struct plt_bitmap *rss_grp_entries;
 	struct npc_flow_list ipsec_list;
 	uint8_t enable_debug;
+	/* Queue-based async template flow state owned by roc. */
+	struct roc_npc_flow_queue *flow_queues;
+	uint16_t nb_flow_queues;
+	struct roc_npc_template_table *flow_tables;
 };
 
 #define NPC_HASH_FIELD_LEN 16
@@ -533,6 +630,7 @@ int npc_mcam_fetch_hw_cap(struct npc *npc, uint8_t *npc_hw_cap);
 int npc_get_free_mcam_entry(struct mbox *mbox, struct roc_npc_flow *flow, struct npc *npc);
 void npc_delete_prio_list_entry(struct npc *npc, struct roc_npc_flow *flow);
 int npc_flow_free_all_resources(struct npc *npc);
+void npc_template_tables_flush_reconcile(struct npc *npc);
 const struct roc_npc_item_info *
 npc_parse_skip_void_and_any_items(const struct roc_npc_item_info *pattern);
 int npc_program_mcam(struct npc *npc, struct npc_parse_state *pst, bool mcam_alloc);
diff --git a/drivers/common/cnxk/roc_npc_utils.c b/drivers/common/cnxk/roc_npc_utils.c
index 8e83b8662d..7ae877aa98 100644
--- a/drivers/common/cnxk/roc_npc_utils.c
+++ b/drivers/common/cnxk/roc_npc_utils.c
@@ -1088,7 +1088,7 @@ npc_get_free_mcam_entry(struct mbox *mbox, struct roc_npc_flow *flow, struct npc
 					     flow->key_type);
 		if (rc) {
 			plt_npc_dbg("npc: failed to allocate MCAM entry.");
-			return rc;
+			goto err;
 		}
 
 		new_entry->flow = flow;
@@ -1096,7 +1096,7 @@ npc_get_free_mcam_entry(struct mbox *mbox, struct roc_npc_flow *flow, struct npc
 		rc = npc_alloc_mcam_by_ref_entry(mbox, flow, npc, &rsp_local);
 
 		if (rc)
-			return rc;
+			goto err;
 
 		new_entry->flow = flow;
 
diff --git a/drivers/common/cnxk/roc_platform_base_symbols.c b/drivers/common/cnxk/roc_platform_base_symbols.c
index 00746abc06..de8de910f6 100644
--- a/drivers/common/cnxk/roc_platform_base_symbols.c
+++ b/drivers/common/cnxk/roc_platform_base_symbols.c
@@ -487,6 +487,7 @@ RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_mcam_enable_all_entries)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_mcam_alloc_entry)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_mcam_ena_dis_entry)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_mcam_write_entry)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_mcam_write_rx_finalize)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_get_low_priority_mcam)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_profile_name_get)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_kex_capa_get)
@@ -505,6 +506,21 @@ RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_aged_flow_ctx_get)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_defrag_mcam_banks)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_get_key_type)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_flow_mcam_dump)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_flow_configure)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_pattern_template_create)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_pattern_template_destroy)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_actions_template_create)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_actions_template_destroy)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_template_table_create)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_template_table_destroy)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_async_flow_create)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_async_flow_destroy)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_async_flow_create_by_index_with_pattern)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_async_flow_actions_update)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_flow_push)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_flow_pull)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_async_flow_resolve)
+RTE_EXPORT_INTERNAL_SYMBOL(roc_npc_async_flow_table_cookie)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_re_ml_zeta_get)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_re_ml_zeta_put)
 RTE_EXPORT_INTERNAL_SYMBOL(roc_ree_queues_attach)
diff --git a/drivers/net/cnxk/cn10k_ethdev.c b/drivers/net/cnxk/cn10k_ethdev.c
index 23a2341c8b..bcf4e23286 100644
--- a/drivers/net/cnxk/cn10k_ethdev.c
+++ b/drivers/net/cnxk/cn10k_ethdev.c
@@ -5,6 +5,7 @@
 #include "cn10k_flow.h"
 #include "cn10k_rx.h"
 #include "cn10k_tx.h"
+#include "cnxk_flow.h"
 
 static uint16_t
 nix_rx_offload_flags(struct rte_eth_dev *eth_dev)
@@ -916,8 +917,27 @@ npc_flow_ops_override(void)
 	cnxk_flow_ops.create = cn10k_flow_create;
 	cnxk_flow_ops.destroy = cn10k_flow_destroy;
 	cnxk_flow_ops.info_get = cn10k_flow_info_get;
+
+	cnxk_flow_ops.configure = cnxk_flow_configure;
+	cnxk_flow_ops.pattern_template_create = cnxk_flow_pattern_template_create;
+	cnxk_flow_ops.pattern_template_destroy = cnxk_flow_pattern_template_destroy;
+	cnxk_flow_ops.actions_template_create = cnxk_flow_actions_template_create;
+	cnxk_flow_ops.actions_template_destroy = cnxk_flow_actions_template_destroy;
+	cnxk_flow_ops.template_table_create = cnxk_flow_template_table_create;
+	cnxk_flow_ops.template_table_destroy = cnxk_flow_template_table_destroy;
 }
 
+/* Fast-path (async) flow ops. Installed per-port in cn10k_nix_probe(). */
+static const struct rte_flow_fp_ops cn10k_flow_fp_ops = {
+	.async_create = cnxk_flow_async_create,
+	.async_create_by_index = cnxk_flow_async_create_by_index,
+	.async_create_by_index_with_pattern = cnxk_flow_async_create_by_index_with_pattern,
+	.async_actions_update = cnxk_flow_async_actions_update,
+	.async_destroy = cnxk_flow_async_destroy,
+	.push = cn10k_flow_push,
+	.pull = cnxk_flow_pull,
+};
+
 static int
 cn10k_nix_remove(struct rte_pci_device *pci_dev)
 {
@@ -966,6 +986,9 @@ cn10k_nix_probe(struct rte_pci_driver *pci_drv, struct rte_pci_device *pci_dev)
 
 	dev = cnxk_eth_pmd_priv(eth_dev);
 
+	/* Install fast-path (async) flow ops for this port. */
+	eth_dev->flow_fp_ops = &cn10k_flow_fp_ops;
+
 	/* DROP_RE is not supported with inline IPSec for CN10K A0 and
 	 * when vector mode is enabled.
 	 */
diff --git a/drivers/net/cnxk/cn10k_flow.c b/drivers/net/cnxk/cn10k_flow.c
index b95fb83f08..02bc002507 100644
--- a/drivers/net/cnxk/cn10k_flow.c
+++ b/drivers/net/cnxk/cn10k_flow.c
@@ -48,6 +48,49 @@ cn10k_flow_info_get(struct rte_eth_dev *dev, struct rte_flow_port_info *port_inf
 	return cnxk_flow_info_get_common(dev, port_info, queue_info, err);
 }
 
+int
+cn10k_flow_push(struct rte_eth_dev *eth_dev, uint32_t queue, struct rte_flow_error *error)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	bool reset_rx = false;
+	int rc;
+
+	/* Flush the pending async ops to hardware first. */
+	rc = cnxk_flow_push(eth_dev, queue, error);
+	if (rc)
+		return rc;
+
+	/* Async push, unlike sync create/destroy, does not toggle the MARK and
+	 * VLAN-strip RX offloads; reconcile each with its refcount so the Rx
+	 * function matches the live rules.
+	 */
+	if (roc_npc_mark_actions_get(npc) &&
+	    !(dev->rx_offload_flags & NIX_RX_OFFLOAD_MARK_UPDATE_F)) {
+		dev->rx_offload_flags |= NIX_RX_OFFLOAD_MARK_UPDATE_F;
+		reset_rx = true;
+	} else if (!roc_npc_mark_actions_get(npc) &&
+		   (dev->rx_offload_flags & NIX_RX_OFFLOAD_MARK_UPDATE_F)) {
+		dev->rx_offload_flags &= ~NIX_RX_OFFLOAD_MARK_UPDATE_F;
+		reset_rx = true;
+	}
+
+	if (roc_npc_vtag_actions_get(npc) &&
+	    !(dev->rx_offload_flags & NIX_RX_OFFLOAD_VLAN_STRIP_F)) {
+		dev->rx_offload_flags |= NIX_RX_OFFLOAD_VLAN_STRIP_F;
+		reset_rx = true;
+	} else if (!roc_npc_vtag_actions_get(npc) &&
+		   (dev->rx_offload_flags & NIX_RX_OFFLOAD_VLAN_STRIP_F)) {
+		dev->rx_offload_flags &= ~NIX_RX_OFFLOAD_VLAN_STRIP_F;
+		reset_rx = true;
+	}
+
+	if (reset_rx)
+		cn10k_eth_set_rx_function(eth_dev);
+
+	return 0;
+}
+
 int
 cn10k_flow_destroy(struct rte_eth_dev *eth_dev, struct rte_flow *rte_flow,
 		   struct rte_flow_error *error)
diff --git a/drivers/net/cnxk/cn10k_flow.h b/drivers/net/cnxk/cn10k_flow.h
index c9e8c86e96..c0d239c20f 100644
--- a/drivers/net/cnxk/cn10k_flow.h
+++ b/drivers/net/cnxk/cn10k_flow.h
@@ -16,6 +16,8 @@ int cn10k_flow_destroy(struct rte_eth_dev *dev, struct rte_flow *flow,
 int cn10k_flow_info_get(struct rte_eth_dev *dev, struct rte_flow_port_info *port_info,
 			struct rte_flow_queue_info *queue_info, struct rte_flow_error *err);
 
+int cn10k_flow_push(struct rte_eth_dev *dev, uint32_t queue, struct rte_flow_error *error);
+
 #define CNXK_NPC_COUNTERS_MAX 512
 
 #endif /* __CN10K_FLOW_H__ */
diff --git a/drivers/net/cnxk/cn20k_ethdev.c b/drivers/net/cnxk/cn20k_ethdev.c
index f08079f612..9fac027650 100644
--- a/drivers/net/cnxk/cn20k_ethdev.c
+++ b/drivers/net/cnxk/cn20k_ethdev.c
@@ -5,7 +5,7 @@
 #include "cn20k_flow.h"
 #include "cn20k_rx.h"
 #include "cn20k_tx.h"
-
+#include "cnxk_flow.h"
 static uint16_t
 nix_rx_offload_flags(struct rte_eth_dev *eth_dev)
 {
@@ -962,8 +962,27 @@ npc_flow_ops_override(void)
 	cnxk_flow_ops.create = cn20k_flow_create;
 	cnxk_flow_ops.destroy = cn20k_flow_destroy;
 	cnxk_flow_ops.info_get = cn20k_flow_info_get;
+
+	cnxk_flow_ops.configure = cnxk_flow_configure;
+	cnxk_flow_ops.pattern_template_create = cnxk_flow_pattern_template_create;
+	cnxk_flow_ops.pattern_template_destroy = cnxk_flow_pattern_template_destroy;
+	cnxk_flow_ops.actions_template_create = cnxk_flow_actions_template_create;
+	cnxk_flow_ops.actions_template_destroy = cnxk_flow_actions_template_destroy;
+	cnxk_flow_ops.template_table_create = cnxk_flow_template_table_create;
+	cnxk_flow_ops.template_table_destroy = cnxk_flow_template_table_destroy;
 }
 
+/* Fast-path (async) flow ops. Installed per-port in cn20k_nix_probe(). */
+static const struct rte_flow_fp_ops cn20k_flow_fp_ops = {
+	.async_create = cnxk_flow_async_create,
+	.async_create_by_index = cnxk_flow_async_create_by_index,
+	.async_create_by_index_with_pattern = cnxk_flow_async_create_by_index_with_pattern,
+	.async_actions_update = cnxk_flow_async_actions_update,
+	.async_destroy = cnxk_flow_async_destroy,
+	.push = cn20k_flow_push,
+	.pull = cnxk_flow_pull,
+};
+
 static int
 cn20k_nix_remove(struct rte_pci_device *pci_dev)
 {
@@ -1012,6 +1031,9 @@ cn20k_nix_probe(struct rte_pci_driver *pci_drv, struct rte_pci_device *pci_dev)
 
 	dev = cnxk_eth_pmd_priv(eth_dev);
 
+	/* Install fast-path (async) flow ops for this port. */
+	eth_dev->flow_fp_ops = &cn20k_flow_fp_ops;
+
 	/* Register up msg callbacks for PTP information */
 	roc_nix_ptp_info_cb_register(&dev->nix, cn20k_nix_ptp_info_update_cb);
 
diff --git a/drivers/net/cnxk/cn20k_flow.c b/drivers/net/cnxk/cn20k_flow.c
index fd50f516ee..ff6e220103 100644
--- a/drivers/net/cnxk/cn20k_flow.c
+++ b/drivers/net/cnxk/cn20k_flow.c
@@ -48,6 +48,49 @@ cn20k_flow_info_get(struct rte_eth_dev *dev, struct rte_flow_port_info *port_inf
 	return cnxk_flow_info_get_common(dev, port_info, queue_info, err);
 }
 
+int
+cn20k_flow_push(struct rte_eth_dev *eth_dev, uint32_t queue, struct rte_flow_error *error)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	bool reset_rx = false;
+	int rc;
+
+	/* Flush the pending async ops to hardware first. */
+	rc = cnxk_flow_push(eth_dev, queue, error);
+	if (rc)
+		return rc;
+
+	/* Async push, unlike sync create/destroy, does not toggle the MARK and
+	 * VLAN-strip RX offloads; reconcile each with its refcount so the Rx
+	 * function matches the live rules.
+	 */
+	if (roc_npc_mark_actions_get(npc) &&
+	    !(dev->rx_offload_flags & NIX_RX_OFFLOAD_MARK_UPDATE_F)) {
+		dev->rx_offload_flags |= NIX_RX_OFFLOAD_MARK_UPDATE_F;
+		reset_rx = true;
+	} else if (!roc_npc_mark_actions_get(npc) &&
+		   (dev->rx_offload_flags & NIX_RX_OFFLOAD_MARK_UPDATE_F)) {
+		dev->rx_offload_flags &= ~NIX_RX_OFFLOAD_MARK_UPDATE_F;
+		reset_rx = true;
+	}
+
+	if (roc_npc_vtag_actions_get(npc) &&
+	    !(dev->rx_offload_flags & NIX_RX_OFFLOAD_VLAN_STRIP_F)) {
+		dev->rx_offload_flags |= NIX_RX_OFFLOAD_VLAN_STRIP_F;
+		reset_rx = true;
+	} else if (!roc_npc_vtag_actions_get(npc) &&
+		   (dev->rx_offload_flags & NIX_RX_OFFLOAD_VLAN_STRIP_F)) {
+		dev->rx_offload_flags &= ~NIX_RX_OFFLOAD_VLAN_STRIP_F;
+		reset_rx = true;
+	}
+
+	if (reset_rx)
+		cn20k_eth_set_rx_function(eth_dev);
+
+	return 0;
+}
+
 int
 cn20k_flow_destroy(struct rte_eth_dev *eth_dev, struct rte_flow *rte_flow,
 		   struct rte_flow_error *error)
diff --git a/drivers/net/cnxk/cn20k_flow.h b/drivers/net/cnxk/cn20k_flow.h
index 1e4bd6214a..ed8a2aa1f3 100644
--- a/drivers/net/cnxk/cn20k_flow.h
+++ b/drivers/net/cnxk/cn20k_flow.h
@@ -16,6 +16,8 @@ int cn20k_flow_destroy(struct rte_eth_dev *dev, struct rte_flow *flow,
 int cn20k_flow_info_get(struct rte_eth_dev *dev, struct rte_flow_port_info *port_info,
 			struct rte_flow_queue_info *queue_info, struct rte_flow_error *err);
 
+int cn20k_flow_push(struct rte_eth_dev *dev, uint32_t queue, struct rte_flow_error *error);
+
 #define CNXK_NPC_COUNTERS_MAX 512
 
 #endif /* __CN20K_FLOW_H__ */
diff --git a/drivers/net/cnxk/cnxk_ethdev.h b/drivers/net/cnxk/cnxk_ethdev.h
index fc66135c66..342d249cf4 100644
--- a/drivers/net/cnxk/cnxk_ethdev.h
+++ b/drivers/net/cnxk/cnxk_ethdev.h
@@ -328,6 +328,50 @@ struct cnxk_macsec_sess {
 };
 TAILQ_HEAD(cnxk_macsec_sess_list, cnxk_macsec_sess);
 
+/* Pattern template: owns deep copies tracked in copies[]. */
+struct cnxk_flow_pattern_template {
+	struct rte_flow_pattern_template_attr attr;
+	struct rte_flow_item *pattern;
+	/* Mirrored roc handle for table-level ownership/lifecycle in roc. */
+	struct roc_npc_pattern_template *roc_tmpl;
+	uint16_t nb_items;
+	void **copies;
+	uint16_t nb_copies;
+	uint32_t refcnt;
+};
+
+/* Actions template: owns deep copies tracked in copies[]. */
+struct cnxk_flow_actions_template {
+	struct rte_flow_actions_template_attr attr;
+	struct rte_flow_action *actions;
+	struct rte_flow_action *masks;
+	/* Mirrored roc handle for table-level ownership/lifecycle in roc. */
+	struct roc_npc_actions_template *roc_tmpl;
+	uint16_t nb_actions;
+	void **copies;
+	uint16_t nb_copies;
+	uint32_t refcnt;
+};
+
+/* Template table wrapper. The roc layer owns rule storage, MCAM reservation
+ * and rule lifecycle; cnxk keeps rte templates for per-rule translation.
+ */
+struct cnxk_flow_table {
+	/* roc engine table handle: storage, MCAM, rule lifecycle. */
+	struct roc_npc_template_table *roc_table;
+
+	struct cnxk_flow_pattern_template **pattern_templates;
+	struct cnxk_flow_actions_template **actions_templates;
+	uint8_t nb_pattern_templates;
+	uint8_t nb_actions_templates;
+
+	uint32_t nb_flows;
+	struct roc_npc_attr flow_attr;
+	/* roc_npc_attr has no transfer field; replay it into each rule attr. */
+	bool transfer;
+	enum rte_flow_table_insertion_type insertion_type;
+};
+
 struct cnxk_eth_dev {
 	/* ROC NIX */
 	struct roc_nix nix;
diff --git a/drivers/net/cnxk/cnxk_flow.c b/drivers/net/cnxk/cnxk_flow.c
index c1c48eb7ab..1562b7389e 100644
--- a/drivers/net/cnxk/cnxk_flow.c
+++ b/drivers/net/cnxk/cnxk_flow.c
@@ -8,6 +8,7 @@
 
 #define TNL_DCP_MATCH_ID 5
 #define NRML_MATCH_ID	 1
+
 const struct cnxk_rte_flow_term_info term[] = {
 	[RTE_FLOW_ITEM_TYPE_ETH] = {ROC_NPC_ITEM_TYPE_ETH, sizeof(struct rte_flow_item_eth)},
 	[RTE_FLOW_ITEM_TYPE_VLAN] = {ROC_NPC_ITEM_TYPE_VLAN, sizeof(struct rte_flow_item_vlan)},
@@ -49,8 +50,7 @@ const struct cnxk_rte_flow_term_info term[] = {
 	[RTE_FLOW_ITEM_TYPE_REPRESENTED_PORT] = {ROC_NPC_ITEM_TYPE_REPRESENTED_PORT,
 						 sizeof(struct rte_flow_item_ethdev)},
 	[RTE_FLOW_ITEM_TYPE_PPPOES] = {ROC_NPC_ITEM_TYPE_PPPOES,
-				       sizeof(struct rte_flow_item_pppoe)}
-};
+				       sizeof(struct rte_flow_item_pppoe)}};
 
 static int
 npc_rss_action_validate(struct rte_eth_dev *eth_dev, const struct rte_flow_attr *attr,
@@ -239,7 +239,7 @@ append_rss_action(struct cnxk_eth_dev *dev, struct roc_npc_action *in_actions, u
 
 	/* Add RSS action */
 	rss_conf->queue_num = nb_rxq;
-	queue_arr = calloc(1, rss_conf->queue_num * sizeof(uint16_t));
+	queue_arr = plt_zmalloc(rss_conf->queue_num * sizeof(uint16_t), 0);
 	if (!queue_arr) {
 		plt_err("Failed to allocate memory for rss queue");
 		rc = -ENOMEM;
@@ -266,10 +266,12 @@ append_rss_action(struct cnxk_eth_dev *dev, struct roc_npc_action *in_actions, u
 	while (free_allocs[j] != 0)
 		j++;
 	free_allocs[j] = (uint64_t)rss_conf;
+	j++;
+	free_allocs[j] = (uint64_t)queue_arr;
 
 	return 0;
 free_rss:
-	rte_free(rss_conf);
+	plt_free(rss_conf);
 fail:
 	return rc;
 }
@@ -1070,22 +1072,18 @@ cnxk_flow_query_common(struct rte_eth_dev *eth_dev, struct rte_flow *flow,
 		       const struct rte_flow_action *action, void *data,
 		       struct rte_flow_error *error, bool is_rep)
 {
-	struct roc_npc_flow *in_flow = (struct roc_npc_flow *)flow;
 	struct rte_flow_query_count *query = data;
+	struct roc_npc_flow *in_flow;
 	struct cnxk_rep_dev *rep_dev;
-	struct cnxk_eth_dev *dev;
-	struct roc_npc *npc;
 	const char *errmsg = NULL;
+	struct cnxk_eth_dev *dev;
 	int errcode = ENOTSUP;
+	struct roc_npc *npc;
 	int rc;
 
-	if (action->type != RTE_FLOW_ACTION_TYPE_COUNT) {
-		errmsg = "Only COUNT is supported in query";
-		goto err_exit;
-	}
-
-	if (in_flow->ctr_id == NPC_COUNTER_NONE) {
-		errmsg = "Counter is not available";
+	if (flow == NULL) {
+		errmsg = "Invalid flow handle";
+		errcode = EINVAL;
 		goto err_exit;
 	}
 
@@ -1098,6 +1096,29 @@ cnxk_flow_query_common(struct rte_eth_dev *eth_dev, struct rte_flow *flow,
 		npc = &rep_dev->parent_dev->npc;
 	}
 
+	/* Let the roc layer resolve async/template handles to the underlying
+	 * roc_npc_flow (NULL until committed); else treat as a sync flow.
+	 */
+	in_flow = (struct roc_npc_flow *)flow;
+	if (!is_rep)
+		roc_npc_async_flow_resolve(npc, flow, &in_flow);
+
+	if (in_flow == NULL) {
+		errmsg = "Flow is not committed yet";
+		errcode = EINVAL;
+		goto err_exit;
+	}
+
+	if (action->type != RTE_FLOW_ACTION_TYPE_COUNT) {
+		errmsg = "Only COUNT is supported in query";
+		goto err_exit;
+	}
+
+	if (in_flow->ctr_id == NPC_COUNTER_NONE) {
+		errmsg = "Counter is not available";
+		goto err_exit;
+	}
+
 	if (in_flow->use_pre_alloc) {
 		rc = roc_npc_inl_mcam_read_counter(in_flow->ctr_id, &query->hits);
 	} else {
@@ -1149,8 +1170,8 @@ cnxk_flow_isolate(struct rte_eth_dev *eth_dev __rte_unused, int enable __rte_unu
 	 * entry for this port.
 	 */
 
-	rte_flow_error_set(error, ENOTSUP, RTE_FLOW_ERROR_TYPE_UNSPECIFIED,
-			   NULL, "Flow isolation not supported");
+	rte_flow_error_set(error, ENOTSUP, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+			   "Flow isolation not supported");
 
 	return -rte_errno;
 }
@@ -1159,8 +1180,9 @@ int
 cnxk_flow_dev_dump_common(struct rte_eth_dev *eth_dev, struct rte_flow *flow, FILE *file,
 			  struct rte_flow_error *error, bool is_rep)
 {
+	struct roc_npc_flow *in_flow = NULL;
+	struct cnxk_eth_dev *dev = NULL;
 	struct cnxk_rep_dev *rep_dev;
-	struct cnxk_eth_dev *dev;
 	struct roc_npc *npc;
 
 	/* is_rep set for operation performed via representor ports */
@@ -1179,7 +1201,17 @@ cnxk_flow_dev_dump_common(struct rte_eth_dev *eth_dev, struct rte_flow *flow, FI
 	}
 
 	if (flow != NULL) {
-		roc_npc_flow_mcam_dump(file, npc, (struct roc_npc_flow *)flow);
+		/* Resolve async/template handles to the underlying roc_npc_flow;
+		 * else treat as a sync flow.
+		 */
+		if (is_rep || !roc_npc_async_flow_resolve(npc, flow, &in_flow))
+			in_flow = (struct roc_npc_flow *)flow;
+
+		if (in_flow == NULL)
+			return rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_HANDLE, NULL,
+						  "flow is not committed yet");
+
+		roc_npc_flow_mcam_dump(file, npc, in_flow);
 		return 0;
 	}
 
@@ -1187,6 +1219,1276 @@ cnxk_flow_dev_dump_common(struct rte_eth_dev *eth_dev, struct rte_flow *flow, FI
 
 	return 0;
 }
+/* Configure async flow queues for the port. */
+int
+cnxk_flow_configure(struct rte_eth_dev *eth_dev, const struct rte_flow_port_attr *port_attr,
+		    uint16_t nb_queue, const struct rte_flow_queue_attr *queue_attr[],
+		    struct rte_flow_error *err)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	const struct roc_npc_flow_queue_attr **rptrs = NULL;
+	struct roc_npc_flow_queue_attr *rattr = NULL;
+	struct roc_npc *npc = &dev->npc;
+	uint16_t i;
+	int rc;
+
+	RTE_SET_USED(port_attr);
+
+	/* Translate the rte queue attributes into roc_npc types; the roc
+	 * engine validates nb_queue == 0 and a NULL queue_attr.
+	 */
+	if (nb_queue != 0 && queue_attr != NULL) {
+		rattr = plt_zmalloc(nb_queue * sizeof(*rattr), 0);
+		if (rattr == NULL)
+			return rte_flow_error_set(err, ENOMEM, RTE_FLOW_ERROR_TYPE_UNSPECIFIED,
+						  NULL, "Failed to allocate flow queues");
+
+		rptrs = plt_zmalloc(nb_queue * sizeof(*rptrs), 0);
+		if (rptrs == NULL) {
+			plt_free(rattr);
+			return rte_flow_error_set(err, ENOMEM, RTE_FLOW_ERROR_TYPE_UNSPECIFIED,
+						  NULL, "Failed to allocate flow queues");
+		}
+		for (i = 0; i < nb_queue; i++) {
+			if (queue_attr[i] != NULL) {
+				rattr[i].size = queue_attr[i]->size;
+				rptrs[i] = &rattr[i];
+			} else {
+				rptrs[i] = NULL;
+			}
+		}
+	}
+
+	rc = roc_npc_flow_configure(npc, nb_queue, rptrs);
+
+	plt_free(rattr);
+	plt_free(rptrs);
+
+	if (rc)
+		return rte_flow_error_set(err, -rc, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "Failed to configure flow queues");
+	return 0;
+}
+
+/* Deep-copy a spec/last/mask/conf payload into template-owned memory. */
+static int
+cnxk_flow_dup_conf(void **copies, uint16_t *nb_copies, const void *src, size_t sz, const void **dst)
+{
+	void *copy;
+
+	if (src == NULL || sz == 0) {
+		*dst = NULL;
+		return 0;
+	}
+	copy = plt_zmalloc(sz, 0);
+	if (copy == NULL)
+		return -ENOMEM;
+	memcpy(copy, src, sz);
+	copies[(*nb_copies)++] = copy;
+	*dst = copy;
+	return 0;
+}
+
+struct rte_flow_pattern_template *
+cnxk_flow_pattern_template_create(struct rte_eth_dev *eth_dev,
+				  const struct rte_flow_pattern_template_attr *attr,
+				  const struct rte_flow_item pattern[],
+				  struct rte_flow_error *error)
+{
+	const char *errmsg = "Failed to allocate pattern template";
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct cnxk_flow_pattern_template *tmpl = NULL;
+	struct roc_npc_pattern_template_attr roc_attr;
+	struct roc_npc_item_info *roc_pattern = NULL;
+	int i, nb_items = 0, errcode = ENOMEM;
+	struct roc_npc *npc = &dev->npc;
+	int roc_err;
+
+	if (attr == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "NULL template attr");
+		return NULL;
+	}
+
+	if (pattern == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "NULL pattern");
+		return NULL;
+	}
+
+	while (pattern[nb_items].type != RTE_FLOW_ITEM_TYPE_END)
+		nb_items++;
+
+	/* Reject oversized templates; rules are built into fixed-size stack
+	 * arrays (combined_items[ROC_NPC_ITEM_TYPE_END + 1]).
+	 */
+	if (nb_items > ROC_NPC_ITEM_TYPE_END) {
+		errcode = ENOTSUP;
+		errmsg = "too many pattern items for hardware";
+		goto err_nomem;
+	}
+
+	tmpl = plt_zmalloc(sizeof(*tmpl), 0);
+	if (tmpl == NULL)
+		goto err_nomem;
+
+	tmpl->attr = *attr;
+	tmpl->nb_items = nb_items;
+
+	tmpl->pattern = plt_zmalloc((nb_items + 1) * sizeof(struct rte_flow_item), 0);
+	if (tmpl->pattern == NULL)
+		goto err_free;
+
+	/* At most 3 payload copies (spec/last/mask) per item. */
+	tmpl->copies = plt_zmalloc(3 * (nb_items + 1) * sizeof(void *), 0);
+	if (tmpl->copies == NULL)
+		goto err_free;
+
+	for (i = 0; i < nb_items; i++) {
+		enum rte_flow_item_type t = pattern[i].type;
+		size_t sz = (t < (int)RTE_DIM(term)) ? term[t].item_size : 0;
+
+		/* Reject unrepresentable item types (sz 0), which would drop
+		 * spec/last/mask. VOID/ANY legitimately carry no payload.
+		 */
+		if (t != RTE_FLOW_ITEM_TYPE_VOID && t != RTE_FLOW_ITEM_TYPE_ANY && sz == 0) {
+			errcode = ENOTSUP;
+			errmsg = "unsupported pattern item type in template";
+			goto err_free;
+		}
+
+		/* Reject items with nested variable-length payloads; a flat
+		 * memcpy would retain a dangling pointer into caller memory.
+		 */
+		if (t == RTE_FLOW_ITEM_TYPE_RAW) {
+			errcode = ENOTSUP;
+			errmsg = "Item type with nested pointers is not supported "
+				 "in pattern template (e.g. RAW)";
+			goto err_free;
+		}
+
+		tmpl->pattern[i].type = t;
+		if (cnxk_flow_dup_conf(tmpl->copies, &tmpl->nb_copies, pattern[i].spec, sz,
+				       &tmpl->pattern[i].spec) ||
+		    cnxk_flow_dup_conf(tmpl->copies, &tmpl->nb_copies, pattern[i].last, sz,
+				       &tmpl->pattern[i].last) ||
+		    cnxk_flow_dup_conf(tmpl->copies, &tmpl->nb_copies, pattern[i].mask, sz,
+				       &tmpl->pattern[i].mask))
+			goto err_free;
+	}
+	tmpl->pattern[nb_items].type = RTE_FLOW_ITEM_TYPE_END;
+	tmpl->roc_tmpl = NULL;
+	tmpl->refcnt = 0;
+
+	roc_pattern = plt_zmalloc((nb_items + 1) * sizeof(*roc_pattern), 0);
+	if (roc_pattern == NULL)
+		goto err_free;
+
+	for (i = 0; i < nb_items; i++) {
+		enum rte_flow_item_type t = tmpl->pattern[i].type;
+		size_t sz = (t < (int)RTE_DIM(term)) ? term[t].item_size : 0;
+
+		roc_pattern[i].type =
+			(t < (int)RTE_DIM(term)) ? term[t].item_type : ROC_NPC_ITEM_TYPE_VOID;
+		roc_pattern[i].size = sz;
+		roc_pattern[i].spec = tmpl->pattern[i].spec;
+		roc_pattern[i].last = tmpl->pattern[i].last;
+		roc_pattern[i].mask = tmpl->pattern[i].mask;
+	}
+	roc_pattern[nb_items].type = ROC_NPC_ITEM_TYPE_END;
+
+	memset(&roc_attr, 0, sizeof(roc_attr));
+	roc_attr.relaxed_matching = attr->relaxed_matching;
+	roc_attr.ingress = attr->ingress;
+	roc_attr.egress = attr->egress;
+	roc_attr.transfer = attr->transfer;
+
+	roc_err = 0;
+	tmpl->roc_tmpl = roc_npc_pattern_template_create(npc, &roc_attr, roc_pattern, &roc_err);
+	if (tmpl->roc_tmpl == NULL) {
+		errcode = (roc_err < 0) ? -roc_err : ENOMEM;
+		errmsg = "Failed to create ROC pattern template";
+		goto err_free;
+	}
+
+	plt_free(roc_pattern);
+
+	return (struct rte_flow_pattern_template *)tmpl;
+
+err_free:
+	plt_free(roc_pattern);
+	if (tmpl != NULL) {
+		uint16_t j;
+		if (tmpl->roc_tmpl != NULL)
+			roc_npc_pattern_template_destroy(npc, tmpl->roc_tmpl);
+		for (j = 0; j < tmpl->nb_copies; j++)
+			plt_free(tmpl->copies[j]);
+		plt_free(tmpl->copies);
+		plt_free(tmpl->pattern);
+		plt_free(tmpl);
+	}
+err_nomem:
+	rte_flow_error_set(error, errcode, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL, errmsg);
+	return NULL;
+}
+
+int
+cnxk_flow_pattern_template_destroy(struct rte_eth_dev *eth_dev,
+				   struct rte_flow_pattern_template *templ,
+				   struct rte_flow_error *error)
+{
+	struct cnxk_flow_pattern_template *tmpl = (struct cnxk_flow_pattern_template *)templ;
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	uint16_t i;
+	int rc;
+
+	if (tmpl == NULL)
+		return 0;
+
+	if (tmpl->refcnt != 0)
+		return rte_flow_error_set(error, EBUSY, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "pattern template still in use by a table");
+
+	rc = roc_npc_pattern_template_destroy(npc, tmpl->roc_tmpl);
+	if (rc)
+		return rte_flow_error_set(error, -rc, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "failed to destroy ROC pattern template");
+
+	for (i = 0; i < tmpl->nb_copies; i++)
+		plt_free(tmpl->copies[i]);
+	plt_free(tmpl->copies);
+	plt_free(tmpl->pattern);
+	plt_free(tmpl);
+	return 0;
+}
+/* Size of an action's conf for the v1-supported set; -1 = unsupported. */
+static int
+cnxk_flow_action_conf_size(enum rte_flow_action_type type)
+{
+	switch (type) {
+	case RTE_FLOW_ACTION_TYPE_VOID:
+	case RTE_FLOW_ACTION_TYPE_DROP:
+	case RTE_FLOW_ACTION_TYPE_PF:
+	case RTE_FLOW_ACTION_TYPE_FLAG:
+	case RTE_FLOW_ACTION_TYPE_OF_POP_VLAN:
+	case RTE_FLOW_ACTION_TYPE_VXLAN_DECAP:
+		return 0; /* no conf */
+	case RTE_FLOW_ACTION_TYPE_QUEUE:
+		return sizeof(struct rte_flow_action_queue);
+	case RTE_FLOW_ACTION_TYPE_VF:
+		return sizeof(struct rte_flow_action_vf);
+	case RTE_FLOW_ACTION_TYPE_MARK:
+		return sizeof(struct rte_flow_action_mark);
+	case RTE_FLOW_ACTION_TYPE_COUNT:
+		return sizeof(struct rte_flow_action_count);
+	case RTE_FLOW_ACTION_TYPE_OF_PUSH_VLAN:
+		return sizeof(struct rte_flow_action_of_push_vlan);
+	case RTE_FLOW_ACTION_TYPE_OF_SET_VLAN_VID:
+		return sizeof(struct rte_flow_action_of_set_vlan_vid);
+	case RTE_FLOW_ACTION_TYPE_OF_SET_VLAN_PCP:
+		return sizeof(struct rte_flow_action_of_set_vlan_pcp);
+	case RTE_FLOW_ACTION_TYPE_AGE:
+		return sizeof(struct rte_flow_action_age);
+	case RTE_FLOW_ACTION_TYPE_REPRESENTED_PORT:
+	case RTE_FLOW_ACTION_TYPE_PORT_REPRESENTOR:
+		return sizeof(struct rte_flow_action_ethdev);
+	case RTE_FLOW_ACTION_TYPE_PORT_ID:
+		return sizeof(struct rte_flow_action_port_id);
+	default:
+		return -1;
+	}
+}
+
+/* Deep-copy an action conf. RSS nests queue[]/key[] pointers, so pack them
+ * into one blob and fix the pointers; other confs use a flat memcpy.
+ */
+static void *
+cnxk_flow_dup_action_conf(const struct rte_flow_action *act)
+{
+	if (act->conf == NULL)
+		return NULL;
+
+	if (act->type == RTE_FLOW_ACTION_TYPE_RSS) {
+		const struct rte_flow_action_rss *rss = act->conf;
+		size_t rss_blob_bytes = sizeof(*rss);
+		size_t rss_queue_bytes = (size_t)rss->queue_num * sizeof(uint16_t);
+		size_t rss_key_bytes = rss->key_len;
+		struct rte_flow_action_rss *rss_copy;
+		uint8_t *rss_blob;
+
+		/* Reject a non-zero length paired with a NULL pointer, which
+		 * would make the memcpy below dereference NULL.
+		 */
+		if ((rss_queue_bytes && rss->queue == NULL) || (rss_key_bytes && rss->key == NULL))
+			return NULL;
+
+		rss_blob = plt_zmalloc(rss_blob_bytes + rss_queue_bytes + rss_key_bytes, 0);
+		if (rss_blob == NULL)
+			return NULL;
+		rss_copy = (struct rte_flow_action_rss *)rss_blob;
+		*rss_copy = *rss;
+		if (rss_queue_bytes) {
+			memcpy(rss_blob + rss_blob_bytes, rss->queue, rss_queue_bytes);
+			rss_copy->queue = (const uint16_t *)(rss_blob + rss_blob_bytes);
+		} else {
+			rss_copy->queue = NULL;
+		}
+		if (rss_key_bytes) {
+			memcpy(rss_blob + rss_blob_bytes + rss_queue_bytes, rss->key,
+			       rss_key_bytes);
+			rss_copy->key =
+				(const uint8_t *)(rss_blob + rss_blob_bytes + rss_queue_bytes);
+		} else {
+			rss_copy->key = NULL;
+		}
+		return rss_copy;
+	}
+
+	{
+		int conf_size = cnxk_flow_action_conf_size(act->type);
+		void *copy;
+
+		if (conf_size <= 0)
+			return NULL;
+		copy = plt_zmalloc(conf_size, 0);
+		if (copy != NULL)
+			memcpy(copy, act->conf, conf_size);
+		return copy;
+	}
+}
+
+/* Map a supported rte_flow action type to its roc_npc action type, to mirror
+ * the actions template into the roc engine for lifecycle/refcount ownership.
+ */
+static int
+cnxk_flow_roc_action_type(enum rte_flow_action_type type, enum roc_npc_action_type *roc_type)
+{
+	switch (type) {
+	case RTE_FLOW_ACTION_TYPE_VOID:
+	case RTE_FLOW_ACTION_TYPE_VXLAN_DECAP:
+		*roc_type = ROC_NPC_ACTION_TYPE_VOID;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_DROP:
+		*roc_type = ROC_NPC_ACTION_TYPE_DROP;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_PF:
+		*roc_type = ROC_NPC_ACTION_TYPE_PF;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_VF:
+		*roc_type = ROC_NPC_ACTION_TYPE_VF;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_FLAG:
+		*roc_type = ROC_NPC_ACTION_TYPE_FLAG;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_MARK:
+		*roc_type = ROC_NPC_ACTION_TYPE_MARK;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_COUNT:
+		*roc_type = ROC_NPC_ACTION_TYPE_COUNT;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_QUEUE:
+		*roc_type = ROC_NPC_ACTION_TYPE_QUEUE;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_RSS:
+		*roc_type = ROC_NPC_ACTION_TYPE_RSS;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_OF_POP_VLAN:
+		*roc_type = ROC_NPC_ACTION_TYPE_VLAN_STRIP;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_OF_PUSH_VLAN:
+		*roc_type = ROC_NPC_ACTION_TYPE_VLAN_ETHTYPE_INSERT;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_OF_SET_VLAN_VID:
+		*roc_type = ROC_NPC_ACTION_TYPE_VLAN_INSERT;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_OF_SET_VLAN_PCP:
+		*roc_type = ROC_NPC_ACTION_TYPE_VLAN_PCP_INSERT;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_AGE:
+		*roc_type = ROC_NPC_ACTION_TYPE_AGE;
+		return 0;
+	case RTE_FLOW_ACTION_TYPE_PORT_ID:
+	case RTE_FLOW_ACTION_TYPE_REPRESENTED_PORT:
+	case RTE_FLOW_ACTION_TYPE_PORT_REPRESENTOR:
+		*roc_type = ROC_NPC_ACTION_TYPE_PORT_ID;
+		return 0;
+	default:
+		return -1;
+	}
+}
+
+struct rte_flow_actions_template *
+cnxk_flow_actions_template_create(struct rte_eth_dev *eth_dev,
+				  const struct rte_flow_actions_template_attr *attr,
+				  const struct rte_flow_action actions[],
+				  const struct rte_flow_action masks[],
+				  struct rte_flow_error *error)
+{
+	const char *errmsg = "Failed to allocate actions template";
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct cnxk_flow_actions_template *tmpl = NULL;
+	struct roc_npc_actions_template_attr roc_attr;
+	struct roc_npc_action *roc_actions = NULL;
+	int i, nb_actions = 0, errcode = ENOMEM;
+	struct roc_npc *npc = &dev->npc;
+	int roc_err = 0;
+
+	if (attr == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "NULL template attr");
+		return NULL;
+	}
+
+	if (actions == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "NULL actions");
+		return NULL;
+	}
+
+	while (actions[nb_actions].type != RTE_FLOW_ACTION_TYPE_END)
+		nb_actions++;
+
+	/* Reject oversized templates; rules are built into fixed-size stack
+	 * arrays (combined_actions[ROC_NPC_MAX_ACTION_COUNT] plus END).
+	 */
+	if (nb_actions > ROC_NPC_MAX_ACTION_COUNT - 1) {
+		errcode = ENOTSUP;
+		errmsg = "too many actions for hardware";
+		goto err;
+	}
+
+	tmpl = plt_zmalloc(sizeof(*tmpl), 0);
+	if (tmpl == NULL)
+		goto err;
+
+	tmpl->attr = *attr;
+	tmpl->nb_actions = nb_actions;
+
+	tmpl->actions = plt_zmalloc((nb_actions + 1) * sizeof(struct rte_flow_action), 0);
+	if (tmpl->actions == NULL)
+		goto err;
+
+	tmpl->masks = plt_zmalloc((nb_actions + 1) * sizeof(struct rte_flow_action), 0);
+	if (tmpl->masks == NULL)
+		goto err;
+
+	/* up to 2 conf copies (action + mask) per action */
+	tmpl->copies = plt_zmalloc(2 * (nb_actions + 1) * sizeof(void *), 0);
+	if (tmpl->copies == NULL)
+		goto err;
+
+	for (i = 0; i < nb_actions; i++) {
+		enum rte_flow_action_type t = actions[i].type;
+		int sz = cnxk_flow_action_conf_size(t);
+
+		if (sz < 0 && t != RTE_FLOW_ACTION_TYPE_RSS) {
+			errcode = ENOTSUP;
+			errmsg = "Unsupported action in template "
+				 "(supports VOID/DROP/QUEUE/PF/VF/MARK/FLAG/COUNT/RSS/"
+				 "OF_POP_VLAN/OF_PUSH_VLAN/OF_SET_VLAN_VID/"
+				 "OF_SET_VLAN_PCP/AGE/REPRESENTED_PORT/"
+				 "PORT_REPRESENTOR/PORT_ID/VXLAN_DECAP)";
+			goto err;
+		}
+
+		tmpl->actions[i].type = t;
+
+		if (t == RTE_FLOW_ACTION_TYPE_RSS) {
+			/* RSS has nested pointers — use deep-copy helper. */
+			void *copy = cnxk_flow_dup_action_conf(&actions[i]);
+
+			if (copy == NULL && actions[i].conf != NULL) {
+				const struct rte_flow_action_rss *rss = actions[i].conf;
+
+				/* Malformed conf vs allocation failure: report a
+				 * precise errno instead of the default ENOMEM.
+				 */
+				if ((rss->queue_num && rss->queue == NULL) ||
+				    (rss->key_len && rss->key == NULL)) {
+					errcode = EINVAL;
+					errmsg = "Malformed RSS action conf in template";
+				}
+				goto err;
+			}
+			tmpl->actions[i].conf = copy;
+			if (copy)
+				tmpl->copies[tmpl->nb_copies++] = copy;
+		} else {
+			if (cnxk_flow_dup_conf(tmpl->copies, &tmpl->nb_copies, actions[i].conf, sz,
+					       &tmpl->actions[i].conf))
+				goto err;
+		}
+
+		if (masks != NULL) {
+			if (masks[i].type != t) {
+				errcode = EINVAL;
+				errmsg = "actions/masks type mismatch in template";
+				goto err;
+			}
+			tmpl->masks[i].type = t;
+			if (t == RTE_FLOW_ACTION_TYPE_RSS) {
+				/* RSS nests pointers and cannot be copied by the flat
+				 * helper; reject a non-NULL RSS mask.
+				 */
+				if (masks[i].conf != NULL) {
+					errcode = ENOTSUP;
+					errmsg = "Non-NULL RSS mask conf is not "
+						 "supported in template";
+					goto err;
+				}
+				tmpl->masks[i].conf = NULL;
+			} else if (cnxk_flow_dup_conf(tmpl->copies, &tmpl->nb_copies, masks[i].conf,
+						      sz > 0 ? sz : 0, &tmpl->masks[i].conf)) {
+				goto err;
+			}
+		}
+	}
+
+	tmpl->actions[nb_actions].type = RTE_FLOW_ACTION_TYPE_END;
+	tmpl->masks[nb_actions].type = RTE_FLOW_ACTION_TYPE_END;
+	tmpl->roc_tmpl = NULL;
+	tmpl->refcnt = 0;
+
+	/* Mirror only action types into the roc engine for template lifecycle
+	 * and refcount ownership; per-rule conf translation stays in cnxk.
+	 */
+	roc_actions = plt_zmalloc((nb_actions + 1) * sizeof(*roc_actions), 0);
+	if (roc_actions == NULL)
+		goto err;
+
+	for (i = 0; i < nb_actions; i++) {
+		enum roc_npc_action_type rt;
+
+		if (cnxk_flow_roc_action_type(tmpl->actions[i].type, &rt)) {
+			errcode = ENOTSUP;
+			errmsg = "Unsupported action in template";
+			goto err;
+		}
+		roc_actions[i].type = rt;
+		roc_actions[i].conf = NULL;
+	}
+	roc_actions[nb_actions].type = ROC_NPC_ACTION_TYPE_END;
+
+	memset(&roc_attr, 0, sizeof(roc_attr));
+	roc_attr.ingress = attr->ingress;
+	roc_attr.egress = attr->egress;
+	roc_attr.transfer = attr->transfer;
+
+	tmpl->roc_tmpl =
+		roc_npc_actions_template_create(npc, &roc_attr, roc_actions, NULL, &roc_err);
+	plt_free(roc_actions);
+	roc_actions = NULL;
+	if (tmpl->roc_tmpl == NULL) {
+		errcode = (roc_err < 0) ? -roc_err : ENOMEM;
+		errmsg = "Failed to create ROC actions template";
+		goto err;
+	}
+
+	return (struct rte_flow_actions_template *)tmpl;
+
+err:
+	plt_free(roc_actions);
+	if (tmpl != NULL) {
+		uint16_t j;
+		if (tmpl->roc_tmpl != NULL)
+			roc_npc_actions_template_destroy(npc, tmpl->roc_tmpl);
+		for (j = 0; j < tmpl->nb_copies; j++)
+			plt_free(tmpl->copies[j]);
+		plt_free(tmpl->copies);
+		plt_free(tmpl->masks);
+		plt_free(tmpl->actions);
+		plt_free(tmpl);
+	}
+	rte_flow_error_set(error, errcode, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL, errmsg);
+	return NULL;
+}
+
+int
+cnxk_flow_actions_template_destroy(struct rte_eth_dev *eth_dev,
+				   struct rte_flow_actions_template *templ,
+				   struct rte_flow_error *error)
+{
+	struct cnxk_flow_actions_template *tmpl = (struct cnxk_flow_actions_template *)templ;
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	uint16_t i;
+	int rc;
+
+	if (tmpl == NULL)
+		return 0;
+
+	if (tmpl->refcnt != 0)
+		return rte_flow_error_set(error, EBUSY, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "actions template still in use by a table");
+
+	rc = roc_npc_actions_template_destroy(npc, tmpl->roc_tmpl);
+	if (rc)
+		return rte_flow_error_set(error, -rc, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "failed to destroy ROC actions template");
+
+	for (i = 0; i < tmpl->nb_copies; i++)
+		plt_free(tmpl->copies[i]);
+	plt_free(tmpl->copies);
+	plt_free(tmpl->masks);
+	plt_free(tmpl->actions);
+	plt_free(tmpl);
+	return 0;
+}
+
+struct rte_flow_template_table *
+cnxk_flow_template_table_create(struct rte_eth_dev *eth_dev,
+				const struct rte_flow_template_table_attr *table_attr,
+				struct rte_flow_pattern_template *pattern_templates[],
+				uint8_t nb_pattern_templates,
+				struct rte_flow_actions_template *actions_templates[],
+				uint8_t nb_actions_templates, struct rte_flow_error *error)
+{
+	struct roc_npc_pattern_template **roc_pattern_templates = NULL;
+	struct roc_npc_actions_template **roc_actions_templates = NULL;
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc_template_table_attr roc_attr;
+	enum rte_flow_table_insertion_type itype;
+	const struct rte_flow_attr *fattr;
+	struct roc_npc *npc = &dev->npc;
+	struct cnxk_flow_table *table;
+	uint32_t nb_flows;
+	int errcode = 0;
+	uint8_t i;
+
+	if (table_attr == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "NULL table_attr");
+		return NULL;
+	}
+
+	itype = table_attr->insertion_type;
+	fattr = &table_attr->flow_attr;
+	nb_flows = table_attr->nb_flows;
+
+	if (itype == RTE_FLOW_TABLE_INSERTION_TYPE_INDEX) {
+		rte_flow_error_set(error, ENOTSUP, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "pure-index insertion not supported; NPC is match-based");
+		return NULL;
+	}
+
+	/* Validate the template handles before allocating anything. */
+	for (i = 0; i < nb_pattern_templates; i++) {
+		if (pattern_templates[i] == NULL) {
+			rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					   "invalid template handle");
+			return NULL;
+		}
+	}
+	for (i = 0; i < nb_actions_templates; i++) {
+		if (actions_templates[i] == NULL) {
+			rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					   "invalid template handle");
+			return NULL;
+		}
+	}
+
+	table = plt_zmalloc(sizeof(*table), 0);
+	if (table == NULL)
+		goto enomem;
+
+	table->insertion_type = itype;
+	table->nb_flows = nb_flows;
+
+	memset(&table->flow_attr, 0, sizeof(table->flow_attr));
+	table->flow_attr.priority = fattr->priority;
+	table->flow_attr.ingress = fattr->ingress;
+	table->flow_attr.egress = fattr->egress;
+	table->transfer = fattr->transfer;
+
+	if (nb_pattern_templates != 0) {
+		table->pattern_templates =
+			plt_zmalloc(nb_pattern_templates * sizeof(*table->pattern_templates), 0);
+		if (table->pattern_templates == NULL)
+			goto free_table;
+		roc_pattern_templates =
+			plt_zmalloc(nb_pattern_templates * sizeof(*roc_pattern_templates), 0);
+		if (roc_pattern_templates == NULL)
+			goto free_table;
+	}
+	if (nb_actions_templates != 0) {
+		table->actions_templates =
+			plt_zmalloc(nb_actions_templates * sizeof(*table->actions_templates), 0);
+		if (table->actions_templates == NULL)
+			goto free_table;
+		roc_actions_templates =
+			plt_zmalloc(nb_actions_templates * sizeof(*roc_actions_templates), 0);
+		if (roc_actions_templates == NULL)
+			goto free_table;
+	}
+
+	/* Hand the table to the roc engine, which owns rule storage, MCAM
+	 * reservation and the rule lifecycle; cnxk keeps per-rule translation.
+	 */
+	memset(&roc_attr, 0, sizeof(roc_attr));
+	roc_attr.flow_attr = table->flow_attr;
+	roc_attr.transfer = table->transfer;
+	roc_attr.nb_flows = nb_flows;
+	roc_attr.insertion_type = (itype == RTE_FLOW_TABLE_INSERTION_TYPE_INDEX_WITH_PATTERN) ?
+					  ROC_NPC_TEMPLATE_INSERTION_INDEX_WITH_PATTERN :
+					  ROC_NPC_TEMPLATE_INSERTION_PATTERN;
+	/* Stash this wrapper as the table cookie so async_actions_update can
+	 * recover the rte templates from just the flow handle.
+	 */
+	roc_attr.cookie = table;
+
+	for (i = 0; i < nb_pattern_templates; i++) {
+		table->pattern_templates[i] =
+			(struct cnxk_flow_pattern_template *)pattern_templates[i];
+		roc_pattern_templates[i] = table->pattern_templates[i]->roc_tmpl;
+		if (roc_pattern_templates[i] == NULL)
+			goto bad_handle;
+	}
+	for (i = 0; i < nb_actions_templates; i++) {
+		table->actions_templates[i] =
+			(struct cnxk_flow_actions_template *)actions_templates[i];
+		roc_actions_templates[i] = table->actions_templates[i]->roc_tmpl;
+		if (roc_actions_templates[i] == NULL)
+			goto bad_handle;
+	}
+
+	/* clang-format off */
+	table->roc_table = roc_npc_template_table_create(npc, &roc_attr,
+			roc_pattern_templates, nb_pattern_templates,
+			roc_actions_templates, nb_actions_templates, &errcode);
+	/* clang-format on */
+	plt_free(roc_pattern_templates);
+	plt_free(roc_actions_templates);
+	roc_pattern_templates = NULL;
+	roc_actions_templates = NULL;
+	if (table->roc_table == NULL) {
+		plt_free(table->pattern_templates);
+		plt_free(table->actions_templates);
+		plt_free(table);
+		rte_flow_error_set(error, errcode < 0 ? -errcode : ENOMEM,
+				   RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "could not create template table");
+		return NULL;
+	}
+
+	for (i = 0; i < nb_pattern_templates; i++)
+		table->pattern_templates[i]->refcnt++;
+	table->nb_pattern_templates = nb_pattern_templates;
+	for (i = 0; i < nb_actions_templates; i++)
+		table->actions_templates[i]->refcnt++;
+	table->nb_actions_templates = nb_actions_templates;
+
+	return (struct rte_flow_template_table *)table;
+
+bad_handle:
+	plt_free(roc_pattern_templates);
+	plt_free(roc_actions_templates);
+	plt_free(table->pattern_templates);
+	plt_free(table->actions_templates);
+	plt_free(table);
+	rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+			   "invalid template handle");
+	return NULL;
+
+free_table:
+	plt_free(roc_pattern_templates);
+	plt_free(roc_actions_templates);
+	plt_free(table->pattern_templates);
+	plt_free(table->actions_templates);
+	plt_free(table);
+
+enomem:
+	rte_flow_error_set(error, ENOMEM, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL, "out of memory");
+	return NULL;
+}
+
+int
+cnxk_flow_template_table_destroy(struct rte_eth_dev *eth_dev,
+				 struct rte_flow_template_table *tbl_handle,
+				 struct rte_flow_error *error)
+{
+	struct cnxk_flow_table *table = (struct cnxk_flow_table *)tbl_handle;
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	uint8_t i;
+	int rc;
+
+	if (table == NULL)
+		return 0;
+
+	/* The roc engine guards teardown: a busy table returns -EBUSY and
+	 * nothing is freed, so the cnxk wrapper and refcounts stay intact.
+	 */
+	rc = roc_npc_template_table_destroy(npc, table->roc_table);
+	if (rc == -EBUSY)
+		return rte_flow_error_set(error, EBUSY, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "table still has live flows; destroy them first");
+	if (rc)
+		return rte_flow_error_set(error, -rc, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "failed to destroy template table");
+
+	for (i = 0; i < table->nb_pattern_templates; i++)
+		table->pattern_templates[i]->refcnt--;
+	for (i = 0; i < table->nb_actions_templates; i++)
+		table->actions_templates[i]->refcnt--;
+
+	plt_free(table->pattern_templates);
+	plt_free(table->actions_templates);
+	plt_free(table);
+	return 0;
+}
+
+/* Reject per-rule pattern/actions arrays whose types/order do not mirror the
+ * chosen templates; the merge takes the type from the template but the
+ * spec/conf from the per-rule array. A NULL array uses template defaults.
+ */
+static int
+cnxk_flow_check_items(const struct cnxk_flow_pattern_template *pt,
+		      const struct rte_flow_item items[])
+{
+	uint16_t k;
+
+	if (items == NULL)
+		return 0;
+	for (k = 0; k < pt->nb_items; k++)
+		if (items[k].type != pt->pattern[k].type)
+			return -EINVAL;
+	if (items[pt->nb_items].type != RTE_FLOW_ITEM_TYPE_END)
+		return -EINVAL;
+	return 0;
+}
+
+static int
+cnxk_flow_check_actions(const struct cnxk_flow_actions_template *at,
+			const struct rte_flow_action actions[])
+{
+	uint16_t k;
+
+	if (actions == NULL)
+		return 0;
+	for (k = 0; k < at->nb_actions; k++)
+		if (actions[k].type != at->actions[k].type)
+			return -EINVAL;
+	if (actions[at->nb_actions].type != RTE_FLOW_ACTION_TYPE_END)
+		return -EINVAL;
+	return 0;
+}
+
+/* Enqueue a PATTERN create: merge the per-rule spec with the templates,
+ * translate to roc_npc types, and hand to the roc engine (programmed at push).
+ */
+struct rte_flow *
+cnxk_flow_async_create(struct rte_eth_dev *eth_dev, uint32_t queue,
+		       const struct rte_flow_op_attr *attr,
+		       struct rte_flow_template_table *tbl_handle,
+		       const struct rte_flow_item items[], uint8_t pattern_template_index,
+		       const struct rte_flow_action actions[], uint8_t action_template_index,
+		       void *user_data, struct rte_flow_error *error)
+{
+	struct cnxk_flow_table *table = (struct cnxk_flow_table *)tbl_handle;
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct cnxk_flow_pattern_template *pt;
+	struct cnxk_flow_actions_template *at;
+	struct roc_npc_template_flow *flow;
+	struct roc_npc *npc = &dev->npc;
+
+	uint64_t free_allocs[ROC_NPC_MAX_ACTION_COUNT + ROC_NPC_ITEM_TYPE_END + 1] = {0};
+	struct roc_npc_item_info in_pattern[ROC_NPC_ITEM_TYPE_END + 1] = {0};
+	struct rte_flow_action combined_actions[ROC_NPC_MAX_ACTION_COUNT];
+	struct roc_npc_action in_actions[ROC_NPC_MAX_ACTION_COUNT] = {0};
+	struct rte_flow_item combined_items[ROC_NPC_ITEM_TYPE_END + 1];
+	struct roc_npc_action_sample in_sample = {0};
+	struct rte_flow_attr rte_attr;
+	struct roc_npc_attr in_attr;
+	uint16_t dst_pf_func = 0;
+	uint64_t def_action = 0;
+	int errcode = 0;
+	uint16_t k;
+	int rc, j;
+
+	RTE_SET_USED(attr);
+
+	if (table == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "bad table");
+		return NULL;
+	}
+	if (table->insertion_type != RTE_FLOW_TABLE_INSERTION_TYPE_PATTERN) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "table is index-insertion; use create_by_index_with_pattern");
+		return NULL;
+	}
+	if (pattern_template_index >= table->nb_pattern_templates ||
+	    action_template_index >= table->nb_actions_templates) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "template index out of range");
+		return NULL;
+	}
+
+	pt = table->pattern_templates[pattern_template_index];
+	at = table->actions_templates[action_template_index];
+
+	if (cnxk_flow_check_items(pt, items) != 0 || cnxk_flow_check_actions(at, actions) != 0) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "pattern/actions do not match the selected templates");
+		return NULL;
+	}
+
+	/* Merge: spec comes from the per-rule call, last/mask from the template. */
+	for (k = 0; k < pt->nb_items; k++) {
+		combined_items[k].type = pt->pattern[k].type;
+		combined_items[k].spec = items ? items[k].spec : NULL;
+		combined_items[k].last = pt->pattern[k].last;
+		combined_items[k].mask = pt->pattern[k].mask;
+	}
+	combined_items[pt->nb_items].type = RTE_FLOW_ITEM_TYPE_END;
+
+	/* Merge: per-rule conf overrides the template conf. */
+	for (k = 0; k < at->nb_actions; k++) {
+		combined_actions[k].type = at->actions[k].type;
+		combined_actions[k].conf =
+			(actions && actions[k].conf) ? actions[k].conf : at->actions[k].conf;
+	}
+	combined_actions[at->nb_actions].type = RTE_FLOW_ACTION_TYPE_END;
+
+	memset(&rte_attr, 0, sizeof(rte_attr));
+	rte_attr.priority = table->flow_attr.priority;
+	rte_attr.ingress = table->flow_attr.ingress;
+	rte_attr.egress = table->flow_attr.egress;
+	rte_attr.transfer = table->transfer;
+
+	/* Translate into roc_npc types. The RSS flow key is stashed in
+	 * npc->flowkey_cfg_state so the engine can restore it at push().
+	 */
+	memset(&in_attr, 0, sizeof(in_attr));
+	rc = cnxk_map_flow_data(eth_dev, &rte_attr, combined_items, combined_actions, &in_attr,
+				in_pattern, in_actions, &in_sample, &npc->flowkey_cfg_state,
+				&dst_pf_func, &def_action, false, free_allocs, 0);
+	if (rc == 0)
+		flow = roc_npc_async_flow_create(npc, queue, table->roc_table, in_pattern,
+						 in_actions, dst_pf_func, def_action, user_data,
+						 &errcode);
+	else
+		flow = NULL;
+
+	for (j = 0; j < (int)RTE_DIM(free_allocs) && free_allocs[j]; j++)
+		plt_free((void *)free_allocs[j]);
+
+	if (rc != 0) {
+		rte_flow_error_set(error, rc < 0 ? -rc : rc, RTE_FLOW_ERROR_TYPE_ACTION_NUM, NULL,
+				   "Failed to map flow data");
+		return NULL;
+	}
+	if (flow == NULL) {
+		rte_flow_error_set(error, errcode < 0 ? -errcode : EINVAL,
+				   RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "async flow create failed");
+		return NULL;
+	}
+
+	return (struct rte_flow *)flow;
+}
+
+/* Enqueue a destroy operation. */
+int
+cnxk_flow_async_destroy(struct rte_eth_dev *eth_dev, uint32_t queue,
+			const struct rte_flow_op_attr *attr, struct rte_flow *flow, void *user_data,
+			struct rte_flow_error *error)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	int rc;
+
+	RTE_SET_USED(attr);
+
+	rc = roc_npc_async_flow_destroy(npc, queue, (struct roc_npc_template_flow *)flow,
+					user_data);
+	if (rc)
+		return rte_flow_error_set(error, -rc, RTE_FLOW_ERROR_TYPE_HANDLE, NULL,
+					  "bad queue or flow handle");
+	return 0;
+}
+
+/* Flush all pending async operations to hardware in one batch. */
+int
+cnxk_flow_push(struct rte_eth_dev *eth_dev, uint32_t queue, struct rte_flow_error *error)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	int rc;
+
+	rc = roc_npc_flow_push(npc, queue);
+	if (rc)
+		return rte_flow_error_set(error, -rc, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "bad queue");
+	return 0;
+}
+
+/* Return results for completed ops back to the application. */
+int
+cnxk_flow_pull(struct rte_eth_dev *eth_dev, uint32_t queue, struct rte_flow_op_result res[],
+	       uint16_t n_res, struct rte_flow_error *error)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct roc_npc *npc = &dev->npc;
+	uint16_t done = 0;
+
+	/* Drain roc engine results in fixed-size batches, translating each
+	 * roc op result into the rte op result without a per-pull allocation.
+	 */
+	while (done < n_res) {
+		struct roc_npc_flow_op_result rres[64];
+		uint16_t batch = RTE_MIN((uint16_t)(n_res - done), (uint16_t)RTE_DIM(rres));
+		int got = roc_npc_flow_pull(npc, queue, rres, batch);
+		uint16_t k;
+
+		if (got < 0) {
+			if (done != 0)
+				break;
+			return rte_flow_error_set(error, -got, RTE_FLOW_ERROR_TYPE_UNSPECIFIED,
+						  NULL, "bad queue");
+		}
+
+		for (k = 0; k < (uint16_t)got; k++) {
+			res[done + k].status =
+				(rres[k].rc == 0) ? RTE_FLOW_OP_SUCCESS : RTE_FLOW_OP_ERROR;
+			res[done + k].user_data = rres[k].user_data;
+		}
+		done += (uint16_t)got;
+
+		if (got < batch)
+			break;
+	}
+
+	return done;
+}
+
+struct rte_flow *
+cnxk_flow_async_create_by_index(struct rte_eth_dev *eth_dev, uint32_t queue,
+				const struct rte_flow_op_attr *attr,
+				struct rte_flow_template_table *table, uint32_t rule_index,
+				const struct rte_flow_action actions[],
+				uint8_t action_template_index, void *user_data,
+				struct rte_flow_error *error)
+{
+	RTE_SET_USED(eth_dev);
+	RTE_SET_USED(queue);
+	RTE_SET_USED(attr);
+	RTE_SET_USED(table);
+	RTE_SET_USED(rule_index);
+	RTE_SET_USED(actions);
+	RTE_SET_USED(action_template_index);
+	RTE_SET_USED(user_data);
+
+	rte_flow_error_set(error, ENOTSUP, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+			   "index-only insertion needs a match key; NPC is match-based");
+	return NULL;
+}
+
+/* Like async_create but with an app-specified index; the app must avoid
+ * concurrent creates to the same index and use a valid table index.
+ */
+struct rte_flow *
+/* clang-format off */
+cnxk_flow_async_create_by_index_with_pattern(struct rte_eth_dev *eth_dev,
+		uint32_t queue, const struct rte_flow_op_attr *attr,
+		struct rte_flow_template_table *tbl_handle, uint32_t rule_index,
+		const struct rte_flow_item items[], uint8_t pattern_template_index,
+		const struct rte_flow_action actions[],
+		uint8_t action_template_index, void *user_data,
+		struct rte_flow_error *error)
+/* clang-format on */
+{
+	struct cnxk_flow_table *table = (struct cnxk_flow_table *)tbl_handle;
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct cnxk_flow_pattern_template *pt;
+	struct cnxk_flow_actions_template *at;
+	struct roc_npc_template_flow *flow;
+	struct roc_npc *npc = &dev->npc;
+
+	uint64_t free_allocs[ROC_NPC_MAX_ACTION_COUNT + ROC_NPC_ITEM_TYPE_END + 1] = {0};
+	struct roc_npc_item_info in_pattern[ROC_NPC_ITEM_TYPE_END + 1] = {0};
+	struct rte_flow_action combined_actions[ROC_NPC_MAX_ACTION_COUNT];
+	struct roc_npc_action in_actions[ROC_NPC_MAX_ACTION_COUNT] = {0};
+	struct rte_flow_item combined_items[ROC_NPC_ITEM_TYPE_END + 1];
+	struct roc_npc_action_sample in_sample = {0};
+	struct rte_flow_attr rte_attr;
+	struct roc_npc_attr in_attr;
+	uint32_t flowkey_cfg = 0;
+	uint16_t dst_pf_func = 0;
+	uint64_t def_action = 0;
+	int errcode = 0;
+	uint16_t k;
+	int rc, j;
+
+	RTE_SET_USED(attr);
+
+	if (table == NULL) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "bad table");
+		return NULL;
+	}
+	if (table->insertion_type != RTE_FLOW_TABLE_INSERTION_TYPE_INDEX_WITH_PATTERN) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "table is not index-with-pattern insertion");
+		return NULL;
+	}
+	if (pattern_template_index >= table->nb_pattern_templates ||
+	    action_template_index >= table->nb_actions_templates) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "template index out of range");
+		return NULL;
+	}
+	if (rule_index >= table->nb_flows) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "rule_index out of range");
+		return NULL;
+	}
+
+	pt = table->pattern_templates[pattern_template_index];
+	at = table->actions_templates[action_template_index];
+
+	if (cnxk_flow_check_items(pt, items) != 0 || cnxk_flow_check_actions(at, actions) != 0) {
+		rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				   "pattern/actions do not match the selected templates");
+		return NULL;
+	}
+
+	for (k = 0; k < pt->nb_items; k++) {
+		combined_items[k].type = pt->pattern[k].type;
+		combined_items[k].spec = items ? items[k].spec : NULL;
+		combined_items[k].last = pt->pattern[k].last;
+		combined_items[k].mask = pt->pattern[k].mask;
+	}
+	combined_items[pt->nb_items].type = RTE_FLOW_ITEM_TYPE_END;
+
+	for (k = 0; k < at->nb_actions; k++) {
+		combined_actions[k].type = at->actions[k].type;
+		combined_actions[k].conf =
+			(actions && actions[k].conf) ? actions[k].conf : at->actions[k].conf;
+	}
+	combined_actions[at->nb_actions].type = RTE_FLOW_ACTION_TYPE_END;
+
+	memset(&rte_attr, 0, sizeof(rte_attr));
+	rte_attr.priority = table->flow_attr.priority;
+	rte_attr.ingress = table->flow_attr.ingress;
+	rte_attr.egress = table->flow_attr.egress;
+	rte_attr.transfer = table->transfer;
+
+	memset(&in_attr, 0, sizeof(in_attr));
+	rc = cnxk_map_flow_data(eth_dev, &rte_attr, combined_items, combined_actions, &in_attr,
+				in_pattern, in_actions, &in_sample, &flowkey_cfg, &dst_pf_func,
+				&def_action, false, free_allocs, 0);
+	if (rc == 0)
+		/* clang-format off */
+		flow = roc_npc_async_flow_create_by_index_with_pattern(npc, queue,
+				table->roc_table, rule_index, in_pattern, in_actions,
+				user_data, &errcode);
+	/* clang-format on */
+	else
+		flow = NULL;
+
+	for (j = 0; j < (int)RTE_DIM(free_allocs) && free_allocs[j]; j++)
+		plt_free((void *)free_allocs[j]);
+
+	if (rc != 0) {
+		rte_flow_error_set(error, rc < 0 ? -rc : rc, RTE_FLOW_ERROR_TYPE_ACTION_NUM, NULL,
+				   "Failed to map flow data");
+		return NULL;
+	}
+	if (flow == NULL) {
+		rte_flow_error_set(error, errcode < 0 ? -errcode : EINVAL,
+				   RTE_FLOW_ERROR_TYPE_HANDLE, NULL, "flow parse failed");
+		return NULL;
+	}
+
+	return (struct rte_flow *)flow;
+}
+
+/* Update the actions of an existing flow, keeping key/mask/mcam_id/enable. */
+int
+cnxk_flow_async_actions_update(struct rte_eth_dev *eth_dev, uint32_t queue,
+			       const struct rte_flow_op_attr *attr, struct rte_flow *flow,
+			       const struct rte_flow_action actions[],
+			       uint8_t action_template_index, void *user_data,
+			       struct rte_flow_error *error)
+{
+	struct cnxk_eth_dev *dev = cnxk_eth_pmd_priv(eth_dev);
+	struct cnxk_flow_actions_template *at;
+	struct roc_npc *npc = &dev->npc;
+	struct cnxk_flow_table *table;
+
+	uint64_t free_allocs[ROC_NPC_MAX_ACTION_COUNT + ROC_NPC_ITEM_TYPE_END + 1] = {0};
+	struct roc_npc_item_info in_pattern[ROC_NPC_ITEM_TYPE_END + 1] = {0};
+	struct rte_flow_action combined_actions[ROC_NPC_MAX_ACTION_COUNT];
+	struct roc_npc_action in_actions[ROC_NPC_MAX_ACTION_COUNT] = {0};
+	struct roc_npc_action_sample in_sample = {0};
+	struct rte_flow_item end_pattern[1];
+	struct rte_flow_attr rte_attr;
+	struct roc_npc_attr in_attr;
+	uint32_t flowkey_cfg = 0;
+	uint16_t dst_pf_func = 0;
+	uint64_t def_action = 0;
+	uint16_t k;
+	int rc, j;
+
+	RTE_SET_USED(attr);
+
+	if (flow == NULL)
+		return rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_HANDLE, NULL,
+					  "bad flow handle");
+
+	/* Recover the cnxk table context from the cookie the engine kept for
+	 * this rule's table.
+	 */
+	table = roc_npc_async_flow_table_cookie((struct roc_npc_template_flow *)flow);
+	if (table == NULL)
+		return rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_HANDLE, NULL,
+					  "flow handle has no associated table");
+	if (action_template_index >= table->nb_actions_templates)
+		return rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "actions template index out of range");
+
+	at = table->actions_templates[action_template_index];
+
+	if (cnxk_flow_check_actions(at, actions) != 0)
+		return rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_ACTION, NULL,
+					  "actions do not match the selected template");
+
+	for (k = 0; k < at->nb_actions; k++) {
+		combined_actions[k].type = at->actions[k].type;
+		combined_actions[k].conf =
+			(actions && actions[k].conf) ? actions[k].conf : at->actions[k].conf;
+	}
+	combined_actions[at->nb_actions].type = RTE_FLOW_ACTION_TYPE_END;
+
+	end_pattern[0].type = RTE_FLOW_ITEM_TYPE_END;
+
+	memset(&rte_attr, 0, sizeof(rte_attr));
+	rte_attr.priority = table->flow_attr.priority;
+	rte_attr.ingress = table->flow_attr.ingress;
+	rte_attr.egress = table->flow_attr.egress;
+	rte_attr.transfer = table->transfer;
+
+	memset(&in_attr, 0, sizeof(in_attr));
+	rc = cnxk_map_flow_data(eth_dev, &rte_attr, end_pattern, combined_actions, &in_attr,
+				in_pattern, in_actions, &in_sample, &flowkey_cfg, &dst_pf_func,
+				&def_action, false, free_allocs, 0);
+	if (rc == 0)
+		/* clang-format off */
+		rc = roc_npc_async_flow_actions_update(npc, queue,
+				(struct roc_npc_template_flow *)flow, in_actions, user_data);
+	/* clang-format on */
+
+	for (j = 0; j < (int)RTE_DIM(free_allocs) && free_allocs[j]; j++)
+		plt_free((void *)free_allocs[j]);
+
+	if (rc != 0)
+		return rte_flow_error_set(error, rc < 0 ? -rc : EINVAL, RTE_FLOW_ERROR_TYPE_ACTION,
+					  NULL, "failed to update flow actions");
+
+	return 0;
+}
 
 static int
 cnxk_flow_dev_dump(struct rte_eth_dev *eth_dev, struct rte_flow *flow, FILE *file,
diff --git a/drivers/net/cnxk/cnxk_flow.h b/drivers/net/cnxk/cnxk_flow.h
index 2986ea81d1..2202f4f3b8 100644
--- a/drivers/net/cnxk/cnxk_flow.h
+++ b/drivers/net/cnxk/cnxk_flow.h
@@ -48,4 +48,81 @@ int cnxk_flow_dev_dump_common(struct rte_eth_dev *eth_dev, struct rte_flow *flow
 			      struct rte_flow_error *error, bool is_rep);
 int cnxk_mtr_destroy(struct rte_eth_dev *eth_dev, uint32_t mtr_id);
 
+int cnxk_flow_configure(struct rte_eth_dev *eth_dev, const struct rte_flow_port_attr *port_attr,
+			uint16_t nb_queue, const struct rte_flow_queue_attr *queue_attr[],
+			struct rte_flow_error *err);
+
+/* clang-format off */
+struct rte_flow_pattern_template *cnxk_flow_pattern_template_create(struct rte_eth_dev *eth_dev,
+		const struct rte_flow_pattern_template_attr *attr,
+		const struct rte_flow_item pattern[],
+		struct rte_flow_error *error);
+/* clang-format on */
+
+int cnxk_flow_pattern_template_destroy(struct rte_eth_dev *eth_dev,
+				       struct rte_flow_pattern_template *templ,
+				       struct rte_flow_error *error);
+
+/* clang-format off */
+struct rte_flow_actions_template *cnxk_flow_actions_template_create(struct rte_eth_dev *eth_dev,
+		const struct rte_flow_actions_template_attr *attr,
+		const struct rte_flow_action actions[],
+		const struct rte_flow_action masks[],
+		struct rte_flow_error *error);
+/* clang-format on */
+
+int cnxk_flow_actions_template_destroy(struct rte_eth_dev *eth_dev,
+				       struct rte_flow_actions_template *templ,
+				       struct rte_flow_error *error);
+
+/* clang-format off */
+struct rte_flow_template_table *cnxk_flow_template_table_create(struct rte_eth_dev *eth_dev,
+		const struct rte_flow_template_table_attr *table_attr,
+		struct rte_flow_pattern_template *pattern_templates[],
+		uint8_t nb_pattern_templates,
+		struct rte_flow_actions_template *actions_templates[],
+		uint8_t nb_actions_templates,
+		struct rte_flow_error *error);
+/* clang-format on */
+
+int cnxk_flow_template_table_destroy(struct rte_eth_dev *eth_dev,
+				     struct rte_flow_template_table *tbl_handle,
+				     struct rte_flow_error *error);
+struct rte_flow *
+cnxk_flow_async_create(struct rte_eth_dev *eth_dev, uint32_t queue,
+		       const struct rte_flow_op_attr *attr, struct rte_flow_template_table *table,
+		       const struct rte_flow_item items[], uint8_t pattern_template_index,
+		       const struct rte_flow_action actions[], uint8_t action_template_index,
+		       void *user_data, struct rte_flow_error *error);
+int cnxk_flow_async_destroy(struct rte_eth_dev *eth_dev, uint32_t queue,
+			    const struct rte_flow_op_attr *attr, struct rte_flow *flow,
+			    void *user_data, struct rte_flow_error *error);
+
+int cnxk_flow_push(struct rte_eth_dev *eth_dev, uint32_t queue, struct rte_flow_error *error);
+int cnxk_flow_pull(struct rte_eth_dev *eth_dev, uint32_t queue, struct rte_flow_op_result res[],
+		   uint16_t n_res, struct rte_flow_error *error);
+
+struct rte_flow *cnxk_flow_async_create_by_index(struct rte_eth_dev *eth_dev, uint32_t queue,
+						 const struct rte_flow_op_attr *attr,
+						 struct rte_flow_template_table *table,
+						 uint32_t rule_index,
+						 const struct rte_flow_action actions[],
+						 uint8_t action_template_index, void *user_data,
+						 struct rte_flow_error *error);
+
+/* clang-format off */
+struct rte_flow *cnxk_flow_async_create_by_index_with_pattern(struct rte_eth_dev *eth_dev,
+		uint32_t queue, const struct rte_flow_op_attr *attr,
+		struct rte_flow_template_table *tbl_handle, uint32_t rule_index,
+		const struct rte_flow_item items[], uint8_t pattern_template_index,
+		const struct rte_flow_action actions[],
+		uint8_t action_template_index, void *user_data,
+		struct rte_flow_error *error);
+/* clang-format on */
+
+int cnxk_flow_async_actions_update(struct rte_eth_dev *eth_dev, uint32_t queue,
+				   const struct rte_flow_op_attr *attr, struct rte_flow *flow,
+				   const struct rte_flow_action actions[],
+				   uint8_t action_template_index, void *user_data,
+				   struct rte_flow_error *error);
 #endif /* __CNXK_RTE_FLOW_H__ */
-- 
2.43.0



More information about the dev mailing list