[libcamera-devel,PATCH/RFC,v2,3/4] ipa: Switch to the plain C API

Message ID 20191013001340.14266-4-laurent.pinchart@ideasonboard.com
State Accepted
Headers show
Series
  • Convert IPA API to plain C
Related show

Commit Message

Laurent Pinchart Oct. 13, 2019, 12:13 a.m. UTC
From: Jacopo Mondi <jacopo@jmondi.org>

Switch IPA communication to the plain C API. As the IPAInterface class
is easier to use for pipeline handlers than a plain C API, retain it and
an IPAContextWrapper that translate between the C++ and the C APIs.

On the IPA module side usage of IPAInterface may be desired for IPAs
implemented in C++ that want to link to libcamera. For those IPAs, a new
IPAInterfaceWrapper helper class is introduce to wrap the IPAInterface
implemented internally by the IPA module into an ipa_context,
ipa_context_ops and ipa_callback_ops.

Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 Documentation/Doxyfile.in                     |   1 +
 Documentation/meson.build                     |   2 +
 src/ipa/ipa_vimc.cpp                          |   6 +-
 src/ipa/libipa/ipa_interface_wrapper.cpp      | 151 ++++++++++++++++++
 src/ipa/libipa/ipa_interface_wrapper.h        |  45 ++++++
 src/ipa/libipa/meson.build                    |  11 ++
 src/ipa/meson.build                           |   3 +
 src/libcamera/include/ipa_context_wrapper.h   |  38 +++++
 src/libcamera/include/ipa_module.h            |   5 +-
 src/libcamera/include/meson.build             |   1 +
 src/libcamera/ipa_context_wrapper.cpp         | 131 +++++++++++++++
 src/libcamera/ipa_manager.cpp                 |  67 +++++++-
 src/libcamera/ipa_module.cpp                  |  23 +--
 src/libcamera/meson.build                     |   1 +
 .../proxy/worker/ipa_proxy_linux_worker.cpp   |   8 +-
 15 files changed, 473 insertions(+), 20 deletions(-)
 create mode 100644 src/ipa/libipa/ipa_interface_wrapper.cpp
 create mode 100644 src/ipa/libipa/ipa_interface_wrapper.h
 create mode 100644 src/ipa/libipa/meson.build
 create mode 100644 src/libcamera/include/ipa_context_wrapper.h
 create mode 100644 src/libcamera/ipa_context_wrapper.cpp

Patch

diff --git a/Documentation/Doxyfile.in b/Documentation/Doxyfile.in
index 5237cf60854f..018fd668a5a8 100644
--- a/Documentation/Doxyfile.in
+++ b/Documentation/Doxyfile.in
@@ -793,6 +793,7 @@  WARN_LOGFILE           =
 
 INPUT                  = "@TOP_SRCDIR@/include/ipa" \
 			 "@TOP_SRCDIR@/include/libcamera" \
+			 "@TOP_SRCDIR@/src/ipa/libipa" \
 			 "@TOP_SRCDIR@/src/libcamera" \
 			 "@TOP_BUILDDIR@/include/libcamera" \
 			 "@TOP_BUILDDIR@/src/libcamera"
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 4ff3fbeb0674..9136506f5d9c 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -24,6 +24,8 @@  if doxygen.found()
                       libcamera_ipa_api,
                       libcamera_headers,
                       libcamera_sources,
+                      libipa_headers,
+                      libipa_sources,
                   ],
                   output : 'api-html',
                   command : [doxygen, doxyfile],
diff --git a/src/ipa/ipa_vimc.cpp b/src/ipa/ipa_vimc.cpp
index 620dc25f456e..f43fc6ab6cd1 100644
--- a/src/ipa/ipa_vimc.cpp
+++ b/src/ipa/ipa_vimc.cpp
@@ -17,6 +17,8 @@ 
 #include <ipa/ipa_interface.h>
 #include <ipa/ipa_module_info.h>
 
+#include "libipa/ipa_interface_wrapper.h"
+
 #include "log.h"
 
 namespace libcamera {
@@ -108,9 +110,9 @@  const struct IPAModuleInfo ipaModuleInfo = {
 	LICENSE,
 };
 
-IPAInterface *ipaCreate()
+struct ipa_context *ipaCreate()
 {
-	return new IPAVimc();
+	return new IPAInterfaceWrapper(new IPAVimc());
 }
 };
 
diff --git a/src/ipa/libipa/ipa_interface_wrapper.cpp b/src/ipa/libipa/ipa_interface_wrapper.cpp
new file mode 100644
index 000000000000..d7c4905e72ee
--- /dev/null
+++ b/src/ipa/libipa/ipa_interface_wrapper.cpp
@@ -0,0 +1,151 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * ipa_interface_wrapper.cpp - Image Processing Algorithm interface wrapper
+ */
+
+#include "ipa_interface_wrapper.h"
+
+#include <unistd.h>
+
+#include <ipa/ipa_interface.h>
+
+/**
+ * \file ipa_interface_wrapper.h
+ * \brief Image Processing Algorithm interface wrapper
+ */
+
+namespace libcamera {
+
+/**
+ * \class IPAInterfaceWrapper
+ * \brief Wrap an IPAInterface and expose it as an ipa_context
+ *
+ * This class implements the ipa_context API based on a provided IPAInterface.
+ * It helps IPAs that implement the IPAInterface API to provide the external
+ * ipa_context API.
+ *
+ * To use the wrapper, an IPA module simple creates a new instance of its
+ * IPAInterface implementation, and passes it to the constructor of the
+ * IPAInterfaceWrapper. As IPAInterfaceWrapper inherits from ipa_context, the
+ * constructed wrapper can then be directly returned from the IPA module's
+ * ipaCreate() function.
+ *
+ * \code{.cpp}
+ * class MyIPA : public IPAInterface
+ * {
+ * 	...
+ * };
+ *
+ * struct ipa_context *ipaCreate()
+ * {
+ * 	return new IPAInterfaceWrapper(new MyIPA());
+ * }
+ * \endcode
+ */
+
+/**
+ * \brief Construct an IPAInterfaceWrapper wrapping \a interface
+ * \param[in] interface The interface to wrap
+ */
+IPAInterfaceWrapper::IPAInterfaceWrapper(IPAInterface *interface)
+	: ipa_(interface), callbacks_(nullptr), cb_ctx_(nullptr)
+{
+	ops = &operations_;
+
+	ipa_->queueFrameAction.connect(this, &IPAInterfaceWrapper::queueFrameAction);
+}
+
+void IPAInterfaceWrapper::destroy(struct ipa_context *_ctx)
+{
+	IPAInterfaceWrapper *ctx = static_cast<IPAInterfaceWrapper *>(_ctx);
+
+	delete ctx->ipa_;
+	delete ctx;
+}
+
+void IPAInterfaceWrapper::init(struct ipa_context *_ctx)
+{
+	IPAInterfaceWrapper *ctx = static_cast<IPAInterfaceWrapper *>(_ctx);
+
+	ctx->ipa_->init();
+}
+
+void IPAInterfaceWrapper::register_callbacks(struct ipa_context *_ctx,
+					     const struct ipa_callback_ops *callbacks,
+					     void *cb_ctx)
+{
+	IPAInterfaceWrapper *ctx = static_cast<IPAInterfaceWrapper *>(_ctx);
+
+	ctx->callbacks_ = callbacks;
+	ctx->cb_ctx_ = cb_ctx;
+}
+
+void IPAInterfaceWrapper::configure(struct ipa_context *_ctx)
+{
+}
+
+void IPAInterfaceWrapper::map_buffers(struct ipa_context *_ctx,
+				      const struct ipa_buffer *_buffers,
+				      size_t num_buffers)
+{
+	IPAInterfaceWrapper *ctx = static_cast<IPAInterfaceWrapper *>(_ctx);
+	std::vector<IPABuffer> buffers(num_buffers);
+
+	for (unsigned int i = 0; i < num_buffers; ++i) {
+		const struct ipa_buffer &_buffer = _buffers[i];
+		IPABuffer &buffer = buffers[i];
+		std::vector<Plane> &planes = buffer.memory.planes();
+
+		buffer.id = _buffer.id;
+
+		planes.resize(_buffer.num_planes);
+		for (unsigned int j = 0; j < _buffer.num_planes; ++j) {
+			planes[j].setDmabuf(_buffer.planes[j].dmabuf,
+					    _buffer.planes[j].length);
+			/** \todo Create a Dmabuf class to implement RAII. */
+			::close(_buffer.planes[j].dmabuf);
+		}
+	}
+
+	ctx->ipa_->mapBuffers(buffers);
+}
+
+void IPAInterfaceWrapper::unmap_buffers(struct ipa_context *_ctx,
+					const unsigned int *_ids,
+					size_t num_buffers)
+{
+	IPAInterfaceWrapper *ctx = static_cast<IPAInterfaceWrapper *>(_ctx);
+	std::vector<unsigned int> ids(_ids, _ids + num_buffers);
+	ctx->ipa_->unmapBuffers(ids);
+}
+
+void IPAInterfaceWrapper::process_event(struct ipa_context *_ctx)
+{
+}
+
+void IPAInterfaceWrapper::queueFrameAction(unsigned int frame,
+					   const IPAOperationData &data)
+{
+	if (callbacks_)
+		callbacks_->queue_frame_action(cb_ctx_, frame);
+}
+
+#ifndef __DOXYGEN__
+/*
+ * This construct confuses Doygen and makes it believe that all members of the
+ * operations is a member of IPAInterfaceWrapper. It must thus be hidden.
+ */
+const struct ipa_context_ops IPAInterfaceWrapper::operations_ = {
+	.destroy = &IPAInterfaceWrapper::destroy,
+	.init = &IPAInterfaceWrapper::init,
+	.register_callbacks = &IPAInterfaceWrapper::register_callbacks,
+	.configure = &IPAInterfaceWrapper::configure,
+	.map_buffers = &IPAInterfaceWrapper::map_buffers,
+	.unmap_buffers = &IPAInterfaceWrapper::unmap_buffers,
+	.process_event = &IPAInterfaceWrapper::process_event,
+};
+#endif
+
+}; /* namespace libcamera */
diff --git a/src/ipa/libipa/ipa_interface_wrapper.h b/src/ipa/libipa/ipa_interface_wrapper.h
new file mode 100644
index 000000000000..a7c70cdc2d83
--- /dev/null
+++ b/src/ipa/libipa/ipa_interface_wrapper.h
@@ -0,0 +1,45 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * ipa_interface_wrapper.h - Image Processing Algorithm interface wrapper
+ */
+#ifndef __LIBCAMERA_IPA_INTERFACE_WRAPPER_H__
+#define __LIBCAMERA_IPA_INTERFACE_WRAPPER_H__
+
+#include <ipa/ipa_interface.h>
+
+namespace libcamera {
+
+class IPAInterfaceWrapper : public ipa_context
+{
+public:
+	IPAInterfaceWrapper(IPAInterface *interface);
+
+private:
+	static void destroy(struct ipa_context *ctx);
+	static void init(struct ipa_context *ctx);
+	static void register_callbacks(struct ipa_context *ctx,
+				       const struct ipa_callback_ops *callbacks,
+				       void *cb_ctx);
+	static void configure(struct ipa_context *ctx);
+	static void map_buffers(struct ipa_context *ctx,
+				const struct ipa_buffer *c_buffers,
+				size_t num_buffers);
+	static void unmap_buffers(struct ipa_context *ctx,
+				  const unsigned int *ids,
+				  size_t num_buffers);
+	static void process_event(struct ipa_context *ctx);
+
+	static const struct ipa_context_ops operations_;
+
+	void queueFrameAction(unsigned int frame, const IPAOperationData &data);
+
+	IPAInterface *ipa_;
+	const struct ipa_callback_ops *callbacks_;
+	void *cb_ctx_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_IPA_INTERFACE_WRAPPER_H__ */
diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
new file mode 100644
index 000000000000..03b046e2203e
--- /dev/null
+++ b/src/ipa/libipa/meson.build
@@ -0,0 +1,11 @@ 
+libipa_headers = files([
+    'ipa_interface_wrapper.h',
+])
+
+libipa_sources = files([
+    'ipa_interface_wrapper.cpp',
+])
+
+libipa = static_library('ipa', libipa_sources,
+                        include_directories : ipa_includes,
+                        dependencies : libcamera_dep)
diff --git a/src/ipa/meson.build b/src/ipa/meson.build
index 4f2a45771201..421803243e32 100644
--- a/src/ipa/meson.build
+++ b/src/ipa/meson.build
@@ -10,11 +10,14 @@  ipa_includes = [
     libcamera_internal_includes,
 ]
 
+subdir('libipa')
+
 foreach t : ipa_vimc_sources
     ipa = shared_module(t[0], 'ipa_vimc.cpp',
                         name_prefix : '',
                         include_directories : ipa_includes,
                         dependencies : libcamera_dep,
+                        link_with : libipa,
                         install : true,
                         install_dir : ipa_install_dir,
                         cpp_args : '-DLICENSE="' + t[1] + '"')
diff --git a/src/libcamera/include/ipa_context_wrapper.h b/src/libcamera/include/ipa_context_wrapper.h
new file mode 100644
index 000000000000..f758bb07abea
--- /dev/null
+++ b/src/libcamera/include/ipa_context_wrapper.h
@@ -0,0 +1,38 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * ipa_context_wrapper.h - Image Processing Algorithm context wrapper
+ */
+#ifndef __LIBCAMERA_IPA_CONTEXT_WRAPPER_H__
+#define __LIBCAMERA_IPA_CONTEXT_WRAPPER_H__
+
+#include <ipa/ipa_interface.h>
+
+namespace libcamera {
+
+class IPAContextWrapper final : public IPAInterface
+{
+public:
+	IPAContextWrapper(struct ipa_context *context);
+	~IPAContextWrapper();
+
+	int init() override;
+	void configure(const std::map<unsigned int, IPAStream> &streamConfig,
+		       const std::map<unsigned int, V4L2ControlInfoMap> &entityControls) override;
+
+	void mapBuffers(const std::vector<IPABuffer> &buffers) override;
+	void unmapBuffers(const std::vector<unsigned int> &ids) override;
+
+	virtual void processEvent(const IPAOperationData &data) override;
+
+private:
+	static void queue_frame_action(void *ctx, unsigned int frame);
+	static const struct ipa_callback_ops callbacks_;
+
+	struct ipa_context *ctx_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_IPA_CONTEXT_WRAPPER_H__ */
diff --git a/src/libcamera/include/ipa_module.h b/src/libcamera/include/ipa_module.h
index 97737587ab3a..2028b76a1913 100644
--- a/src/libcamera/include/ipa_module.h
+++ b/src/libcamera/include/ipa_module.h
@@ -7,7 +7,6 @@ 
 #ifndef __LIBCAMERA_IPA_MODULE_H__
 #define __LIBCAMERA_IPA_MODULE_H__
 
-#include <memory>
 #include <string>
 
 #include <ipa/ipa_interface.h>
@@ -30,7 +29,7 @@  public:
 
 	bool load();
 
-	std::unique_ptr<IPAInterface> createInstance();
+	struct ipa_context *createContext();
 
 	bool match(PipelineHandler *pipe,
 		   uint32_t minVersion, uint32_t maxVersion) const;
@@ -45,7 +44,7 @@  private:
 	bool loaded_;
 
 	void *dlHandle_;
-	typedef IPAInterface *(*IPAIntfFactory)(void);
+	typedef struct ipa_context *(*IPAIntfFactory)(void);
 	IPAIntfFactory ipaCreate_;
 
 	int loadIPAModuleInfo();
diff --git a/src/libcamera/include/meson.build b/src/libcamera/include/meson.build
index 2c74d29bd925..f9b7e9b09a60 100644
--- a/src/libcamera/include/meson.build
+++ b/src/libcamera/include/meson.build
@@ -7,6 +7,7 @@  libcamera_headers = files([
     'device_enumerator_udev.h',
     'event_dispatcher_poll.h',
     'formats.h',
+    'ipa_context_wrapper.h',
     'ipa_manager.h',
     'ipa_module.h',
     'ipa_proxy.h',
diff --git a/src/libcamera/ipa_context_wrapper.cpp b/src/libcamera/ipa_context_wrapper.cpp
new file mode 100644
index 000000000000..355911791d43
--- /dev/null
+++ b/src/libcamera/ipa_context_wrapper.cpp
@@ -0,0 +1,131 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * ipa_context_wrapper.cpp - Image Processing Algorithm context wrapper
+ */
+
+#include "ipa_context_wrapper.h"
+
+#include <libcamera/controls.h>
+
+/**
+ * \file ipa_context_wrapper.h
+ * \brief Image Processing Algorithm context wrapper
+ */
+
+namespace libcamera {
+
+/**
+ * \class IPAContextWrapper
+ * \brief Wrap an ipa_context and expose it as an IPAInterface
+ *
+ * The IPAContextWrapper class wraps an ipa_context, provided by an IPA module, and
+ * exposes an IPAInterface. This mechanism is used for IPAs that are not
+ * isolated in a separate process to allow direct calls from pipeline handler
+ * using the IPAInterface API instead of the lower-level ipa_context API.
+ *
+ * The IPAInterface methods are converted to the ipa_context API by serialising
+ * all C++ arguments into plain C structures or byte arrays that contain no
+ * pointer, as required by the ipa_context API.
+ */
+
+/**
+ * \brief Construct an IPAContextWrapper instance that wraps the \a context
+ * \param[in] context The IPA module context
+ *
+ * Ownership of the \a context is passed to the IPAContextWrapper. The context remains
+ * valid for the whole lifetime of the wrapper and is destroyed automatically
+ * with it.
+ */
+IPAContextWrapper::IPAContextWrapper(struct ipa_context *context)
+	: ctx_(context)
+{
+}
+
+IPAContextWrapper::~IPAContextWrapper()
+{
+	if (ctx_)
+		ctx_->ops->destroy(ctx_);
+}
+
+int IPAContextWrapper::init()
+{
+	if (!ctx_)
+		return 0;
+
+	ctx_->ops->register_callbacks(ctx_, &IPAContextWrapper::callbacks_, this);
+	ctx_->ops->init(ctx_);
+
+	return 0;
+}
+
+void IPAContextWrapper::configure(const std::map<unsigned int, IPAStream> &streamConfig,
+				  const std::map<unsigned int, V4L2ControlInfoMap> &entityControls)
+{
+	if (!ctx_)
+		return;
+
+	ctx_->ops->configure(ctx_);
+}
+
+void IPAContextWrapper::mapBuffers(const std::vector<IPABuffer> &buffers)
+{
+	if (!ctx_)
+		return;
+
+	struct ipa_buffer c_buffers[buffers.size()];
+
+	for (unsigned int i = 0; i < buffers.size(); ++i) {
+		struct ipa_buffer &c_buffer = c_buffers[i];
+		const IPABuffer &buffer = buffers[i];
+		const std::vector<Plane> &planes = buffer.memory.planes();
+
+		c_buffer.id = buffer.id;
+		c_buffer.num_planes = planes.size();
+
+		for (unsigned int j = 0; j < planes.size(); ++j) {
+			const Plane &plane = planes[j];
+			c_buffer.planes[j].dmabuf = plane.dmabuf();
+			c_buffer.planes[j].length = plane.length();
+		}
+	}
+
+	ctx_->ops->map_buffers(ctx_, c_buffers, buffers.size());
+}
+
+void IPAContextWrapper::unmapBuffers(const std::vector<unsigned int> &ids)
+{
+	if (!ctx_)
+		return;
+
+	ctx_->ops->unmap_buffers(ctx_, ids.data(), ids.size());
+}
+
+void IPAContextWrapper::processEvent(const IPAOperationData &data)
+{
+	if (!ctx_)
+		return;
+
+	ctx_->ops->process_event(ctx_);
+}
+
+void IPAContextWrapper::queue_frame_action(void *ctx, unsigned int frame)
+{
+	IPAContextWrapper *_this = static_cast<IPAContextWrapper *>(ctx);
+	IPAOperationData data;
+
+	_this->queueFrameAction.emit(frame, data);
+}
+
+#ifndef __DOXYGEN__
+/*
+ * This construct confuses Doygen and makes it believe that all members of the
+ * operations is a member of IPAInterfaceWrapper. It must thus be hidden.
+ */
+const struct ipa_callback_ops IPAContextWrapper::callbacks_ = {
+	.queue_frame_action = &IPAContextWrapper::queue_frame_action,
+};
+#endif
+
+} /* namespace libcamera */
diff --git a/src/libcamera/ipa_manager.cpp b/src/libcamera/ipa_manager.cpp
index f3180c0739cb..47b131d75da4 100644
--- a/src/libcamera/ipa_manager.cpp
+++ b/src/libcamera/ipa_manager.cpp
@@ -12,6 +12,7 @@ 
 #include <string.h>
 #include <sys/types.h>
 
+#include "ipa_context_wrapper.h"
 #include "ipa_module.h"
 #include "ipa_proxy.h"
 #include "log.h"
@@ -30,6 +31,66 @@  LOG_DEFINE_CATEGORY(IPAManager)
 /**
  * \class IPAManager
  * \brief Manager for IPA modules
+ *
+ * The IPA module manager discovers IPA modules from disk, queries and loads
+ * them, and creates IPA contexts. It supports isolation of the modules in a
+ * separate process with IPC communication and offers a unified IPAInterface
+ * view of the IPA contexts to pipeline handlers regardless of whether the
+ * modules are isolated or loaded in the same process.
+ *
+ * Module isolation is based on the module licence. Open-source modules are
+ * loaded without isolation, while closed-source module are forcefully isolated.
+ * The isolation mechanism ensures that no code from a closed-source module is
+ * ever run in the libcamera process.
+ *
+ * To create an IPA context, pipeline handlers call the IPAManager::ipaCreate()
+ * method. For a directly loaded module, the manager calls the module's
+ * ipaCreate() function directly and wraps the returned context in an
+ * IPAContextWrapper that exposes an IPAInterface.
+ *
+ * ~~~~
+ * +---------------+
+ * |   Pipeline    |
+ * |    Handler    |
+ * +---------------+
+ *         |
+ *         v
+ * +---------------+                   +---------------+
+ * |      IPA      |                   |  Open Source  |
+ * |   Interface   |                   |  IPA Module   |
+ * | - - - - - - - |                   | - - - - - - - |
+ * |  IPA Context  |  ipa_context_ops  |  ipa_context  |
+ * |    Wrapper    | ----------------> |               |
+ * +---------------+                   +---------------+
+ * ~~~~
+ *
+ * For an isolated module, the manager instantiates an IPAProxy which spawns a
+ * new process for an IPA proxy worker. The worker loads the IPA module and
+ * creates the IPA context. The IPAProxy alse exposes an IPAInterface.
+ *
+ * ~~~~
+ * +---------------+                   +---------------+
+ * |   Pipeline    |                   | Closed Source |
+ * |    Handler    |                   |  IPA Module   |
+ * +---------------+                   | - - - - - - - |
+ *         |                           |  ipa_context  |
+ *         v                           |               |
+ * +---------------+                   +---------------+
+ * |      IPA      |           ipa_context_ops ^
+ * |   Interface   |                           |
+ * | - - - - - - - |                   +---------------+
+ * |   IPA Proxy   |     operations    |   IPA Proxy   |
+ * |               | ----------------> |    Worker     |
+ * +---------------+      over IPC     +---------------+
+ * ~~~~
+ *
+ * The IPAInterface implemented by the IPAContextWrapper or IPAProxy is
+ * returned to the pipeline handler, and all interactions with the IPA context
+ * go the same interface regardless of process isolation.
+ *
+ * In all cases the data passed to the IPAInterface methods is serialised to
+ * Plain Old Data, either for the purpose of passing it to the IPA context
+ * plain C API, or to transmit the data to the isolated process through IPC.
  */
 
 IPAManager::IPAManager()
@@ -199,7 +260,11 @@  std::unique_ptr<IPAInterface> IPAManager::createIPA(PipelineHandler *pipe,
 	if (!m->load())
 		return nullptr;
 
-	return m->createInstance();
+	struct ipa_context *ctx = m->createContext();
+	if (!ctx)
+		return nullptr;
+
+	return utils::make_unique<IPAContextWrapper>(ctx);
 }
 
 } /* namespace libcamera */
diff --git a/src/libcamera/ipa_module.cpp b/src/libcamera/ipa_module.cpp
index 99d308efd47b..2c355ea8b5e5 100644
--- a/src/libcamera/ipa_module.cpp
+++ b/src/libcamera/ipa_module.cpp
@@ -385,13 +385,13 @@  const std::string &IPAModule::path() const
 /**
  * \brief Load the IPA implementation factory from the shared object
  *
- * The IPA module shared object implements an IPAInterface class to be used
+ * The IPA module shared object implements an ipa_context object to be used
  * by pipeline handlers. This method loads the factory function from the
- * shared object. Later, createInstance() can be called to instantiate the
- * IPAInterface.
+ * shared object. Later, createContext() can be called to instantiate the
+ * ipa_context.
  *
  * This method only needs to be called successfully once, after which
- * createInstance() can be called as many times as IPAInterface instances are
+ * createContext() can be called as many times as ipa_context instances are
  * needed.
  *
  * Calling this function on an invalid module (as returned by isValid()) is
@@ -433,24 +433,25 @@  bool IPAModule::load()
 }
 
 /**
- * \brief Instantiate an IPAInterface
+ * \brief Instantiate an IPA context
  *
- * After loading the IPA module with load(), this method creates an
- * instance of the IPA module interface.
+ * After loading the IPA module with load(), this method creates an instance of
+ * the IPA module context. Ownership of the context is passed to the caller, and
+ * the context shall be destroyed by calling the \ref ipa_context_ops::destroy
+ * "ipa_context::ops::destroy()" function.
  *
  * Calling this function on a module that has not yet been loaded, or an
  * invalid module (as returned by load() and isValid(), respectively) is
  * an error.
  *
- * \return The IPA implementation as a new IPAInterface instance on success,
- * or nullptr on error
+ * \return The IPA context on success, or nullptr on error
  */
-std::unique_ptr<IPAInterface> IPAModule::createInstance()
+struct ipa_context *IPAModule::createContext()
 {
 	if (!valid_ || !loaded_)
 		return nullptr;
 
-	return std::unique_ptr<IPAInterface>(ipaCreate_());
+	return ipaCreate_();
 }
 
 /**
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index d329820b9582..775ceff9dace 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -14,6 +14,7 @@  libcamera_sources = files([
     'event_notifier.cpp',
     'formats.cpp',
     'geometry.cpp',
+    'ipa_context_wrapper.cpp',
     'ipa_interface.cpp',
     'ipa_manager.cpp',
     'ipa_module.cpp',
diff --git a/src/libcamera/proxy/worker/ipa_proxy_linux_worker.cpp b/src/libcamera/proxy/worker/ipa_proxy_linux_worker.cpp
index a10761e52b32..07380c16e2d5 100644
--- a/src/libcamera/proxy/worker/ipa_proxy_linux_worker.cpp
+++ b/src/libcamera/proxy/worker/ipa_proxy_linux_worker.cpp
@@ -72,9 +72,9 @@  int main(int argc, char **argv)
 	}
 	socket.readyRead.connect(&readyRead);
 
-	std::unique_ptr<IPAInterface> ipa = ipam->createInstance();
-	if (!ipa) {
-		LOG(IPAProxyLinuxWorker, Error) << "Failed to create IPA interface";
+	struct ipa_context *ipac = ipam->createContext();
+	if (!ipac) {
+		LOG(IPAProxyLinuxWorker, Error) << "Failed to create IPA context";
 		return EXIT_FAILURE;
 	}
 
@@ -85,5 +85,7 @@  int main(int argc, char **argv)
 	while (1)
 		dispatcher->processEvents();
 
+	ipac->ops->destroy(ipac);
+
 	return 0;
 }