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

<[email protected]>
Newsgroups org.dpdk.dev
Message-ID <[email protected]>
From: Yashaswini Kanaujia <[email protected]>

added support for asynchronous rte_flow APIs
for cn10k device

Signed-off-by: Yashaswini Kanaujia <[email protected]>
---
 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.