diff --git a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
index 42c9caa0..214dba8f 100644
--- a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
+++ b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
@@ -16,6 +16,7 @@
 #include <libcamera/file_descriptor.h>
 #include <libcamera/formats.h>
 #include <libcamera/ipa/raspberrypi.h>
+#include <libcamera/ipa/raspberrypi_wrapper.h>
 #include <libcamera/logging.h>
 #include <libcamera/property_ids.h>
 #include <libcamera/request.h>
@@ -35,6 +36,8 @@
 #include "dma_heaps.h"
 #include "staggered_ctrl.h"
 
+#include "libcamera/internal/ipa_proxy_raspberrypi.h"
+
 namespace libcamera {
 
 LOG_DEFINE_CATEGORY(RPI)
@@ -296,7 +299,7 @@ public:
 	int loadIPA();
 	int configureIPA();
 
-	void queueFrameAction(unsigned int frame, const IPAOperationData &action);
+	void queueFrameAction(unsigned int frame, const RPiActionParams &action);
 
 	/* bufferComplete signal handlers. */
 	void unicamBufferDequeue(FrameBuffer *buffer);
@@ -307,6 +310,8 @@ public:
 	void handleStreamBuffer(FrameBuffer *buffer, const RPiStream *stream);
 	void handleState();
 
+	std::unique_ptr<IPAProxyRPi> ipa_;
+
 	CameraSensor *sensor_;
 	/* Array of Unicam and ISP device streams and associated buffers/streams. */
 	RPiDevice<Unicam, 2> unicam_;
@@ -1094,7 +1099,9 @@ void RPiCameraData::frameStarted(uint32_t sequence)
 
 int RPiCameraData::loadIPA()
 {
-	ipa_ = IPAManager::createIPA(pipe_, 1, 1);
+	std::unique_ptr<IPAProxy> ptr = IPAManager::createIPA(pipe_, 1, 1);
+	ipa_ = std::unique_ptr<IPAProxyRPi>{static_cast<IPAProxyRPi*>(std::move(ptr).release())};
+
 	if (!ipa_)
 		return -ENOENT;
 
@@ -1110,8 +1117,8 @@ int RPiCameraData::loadIPA()
 int RPiCameraData::configureIPA()
 {
 	std::map<unsigned int, IPAStream> streamConfig;
-	std::map<unsigned int, const ControlInfoMap &> entityControls;
-	IPAOperationData ipaConfig = {};
+	std::map<unsigned int, const ControlInfoMap> entityControls;
+	RPiConfigureParams ipaConfig;
 
 	/* Get the device format to pass to the IPA. */
 	V4L2DeviceFormat sensorFormat;
@@ -1136,8 +1143,11 @@ int RPiCameraData::configureIPA()
 			return -ENOMEM;
 
 		/* Allow the IPA to mmap the LS table via the file descriptor. */
-		ipaConfig.operation = RPI_IPA_CONFIG_LS_TABLE;
-		ipaConfig.data = { static_cast<unsigned int>(lsTable_.fd()) };
+		RPiConfigurePayload payload;
+		payload.op_ = RPI_IPA_CONFIG_LS_TABLE;
+		payload.lsTableHandle_ = lsTable_;
+		payload.lsTableHandleStatic_ = lsTable_.fd();
+		ipaConfig.payload_.push_back(payload);
 	}
 
 	CameraSensorInfo sensorInfo = {};
@@ -1148,60 +1158,66 @@ int RPiCameraData::configureIPA()
 	}
 
 	/* Ready the IPA - it must know about the sensor resolution. */
-	IPAOperationData result;
+	RPiConfigureParams results;
 
 	ipa_->configure(sensorInfo, streamConfig, entityControls, ipaConfig,
-			&result);
+			&results);
 
-	if (result.operation & RPI_IPA_CONFIG_STAGGERED_WRITE) {
-		/*
-		 * Setup our staggered control writer with the sensor default
-		 * gain and exposure delays.
-		 */
-		if (!staggeredCtrl_) {
-			staggeredCtrl_.init(unicam_[Unicam::Image].dev(),
-					    { { V4L2_CID_ANALOGUE_GAIN, result.data[0] },
-					      { V4L2_CID_EXPOSURE, result.data[1] } });
-			sensorMetadata_ = result.data[2];
-		}
+	for (RPiConfigurePayload &result : results.payload_) {
+		if (result.op_ == RPI_IPA_CONFIG_STAGGERED_WRITE) {
 
-		/* Configure the H/V flip controls based on the sensor rotation. */
-		ControlList ctrls(unicam_[Unicam::Image].dev()->controls());
-		int32_t rotation = sensor_->properties().get(properties::Rotation);
-		ctrls.set(V4L2_CID_HFLIP, static_cast<int32_t>(!!rotation));
-		ctrls.set(V4L2_CID_VFLIP, static_cast<int32_t>(!!rotation));
-		unicam_[Unicam::Image].dev()->setControls(&ctrls);
-	}
+			/*
+			 * Setup our staggered control writer with the sensor default
+			 * gain and exposure delays.
+			 */
+			if (!staggeredCtrl_) {
+				staggeredCtrl_.init(unicam_[Unicam::Image].dev(),
+						{ { V4L2_CID_ANALOGUE_GAIN, result.staggeredWriteResult_.gainDelay_ },
+						{ V4L2_CID_EXPOSURE, result.staggeredWriteResult_.exposureDelay_ } });
+				sensorMetadata_ = result.staggeredWriteResult_.sensorMetadata_;
+			}
 
-	if (result.operation & RPI_IPA_CONFIG_SENSOR) {
-		const ControlList &ctrls = result.controls[0];
-		if (!staggeredCtrl_.set(ctrls))
-			LOG(RPI, Error) << "V4L2 staggered set failed";
+			/* Configure the H/V flip controls based on the sensor rotation. */
+			ControlList ctrls(unicam_[Unicam::Image].dev()->controls());
+			int32_t rotation = sensor_->properties().get(properties::Rotation);
+			ctrls.set(V4L2_CID_HFLIP, static_cast<int32_t>(!!rotation));
+			ctrls.set(V4L2_CID_VFLIP, static_cast<int32_t>(!!rotation));
+			unicam_[Unicam::Image].dev()->setControls(&ctrls);
+		}
+
+		if (result.op_ == RPI_IPA_CONFIG_SENSOR) {
+			const ControlList &ctrls = result.controls_;
+			if (!staggeredCtrl_.set(ctrls))
+				LOG(RPI, Error) << "V4L2 staggered set failed";
+		}
 	}
 
 	return 0;
 }
 
 void RPiCameraData::queueFrameAction([[maybe_unused]] unsigned int frame,
-				     const IPAOperationData &action)
+				     const RPiActionParams &action)
 {
 	/*
 	 * The following actions can be handled when the pipeline handler is in
 	 * a stopped state.
 	 */
-	switch (action.operation) {
+	switch (action.op_) {
 	case RPI_IPA_ACTION_V4L2_SET_STAGGERED: {
-		const ControlList &controls = action.controls[0];
+		const ControlList &controls = action.controls_;
 		if (!staggeredCtrl_.set(controls))
 			LOG(RPI, Error) << "V4L2 staggered set failed";
 		goto done;
 	}
 
 	case RPI_IPA_ACTION_V4L2_SET_ISP: {
-		ControlList controls = action.controls[0];
+		ControlList controls = action.controls_;
 		isp_[Isp::Input].dev()->setControls(&controls);
 		goto done;
 	}
+
+	default:
+		break;
 	}
 
 	if (state_ == State::Stopped)
@@ -1211,20 +1227,20 @@ void RPiCameraData::queueFrameAction([[maybe_unused]] unsigned int frame,
 	 * The following actions must not be handled when the pipeline handler
 	 * is in a stopped state.
 	 */
-	switch (action.operation) {
+	switch (action.op_) {
 	case RPI_IPA_ACTION_STATS_METADATA_COMPLETE: {
-		unsigned int bufferId = action.data[0];
+		unsigned int bufferId = action.statsComplete_.bufferId_;
 		FrameBuffer *buffer = isp_[Isp::Stats].getBuffers()->at(bufferId).get();
 
 		handleStreamBuffer(buffer, &isp_[Isp::Stats]);
 		/* Fill the Request metadata buffer with what the IPA has provided */
-		requestQueue_.front()->metadata() = std::move(action.controls[0]);
+		requestQueue_.front()->metadata() = std::move(action.statsComplete_.controls_);
 		state_ = State::IpaComplete;
 		break;
 	}
 
 	case RPI_IPA_ACTION_EMBEDDED_COMPLETE: {
-		unsigned int bufferId = action.data[0];
+		unsigned int bufferId = action.bufferId_;
 		FrameBuffer *buffer = unicam_[Unicam::Embedded].getBuffers()->at(bufferId).get();
 		handleStreamBuffer(buffer, &unicam_[Unicam::Embedded]);
 		break;
@@ -1232,20 +1248,17 @@ void RPiCameraData::queueFrameAction([[maybe_unused]] unsigned int frame,
 
 	case RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME:
 	case RPI_IPA_ACTION_RUN_ISP: {
-		unsigned int bufferId = action.data[0];
+		unsigned int bufferId = action.bufferId_;
 		FrameBuffer *buffer = unicam_[Unicam::Image].getBuffers()->at(bufferId).get();
 
-		LOG(RPI, Debug) << "Input re-queue to ISP, buffer id " << buffer->cookie()
-				<< ", timestamp: " << buffer->metadata().timestamp;
-
 		isp_[Isp::Input].dev()->queueBuffer(buffer);
-		dropFrame_ = (action.operation == RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME) ? true : false;
+		dropFrame_ = (action.op_ == RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME) ? true : false;
 		ispOutputCount_ = 0;
 		break;
 	}
 
 	default:
-		LOG(RPI, Error) << "Unknown action " << action.operation;
+		LOG(RPI, Error) << "Unknown action " << action.op_;
 		break;
 	}
 
@@ -1345,10 +1358,10 @@ void RPiCameraData::ispOutputDequeue(FrameBuffer *buffer)
 
 	/* If this is a stats output, hand it to the IPA now. */
 	if (stream == &isp_[Isp::Stats]) {
-		IPAOperationData op;
-		op.operation = RPI_IPA_EVENT_SIGNAL_STAT_READY;
-		op.data = { RPiIpaMask::STATS | buffer->cookie() };
-		ipa_->processEvent(op);
+		RPiEventParams ev;
+		ev.ev_ = RPI_IPA_EVENT_SIGNAL_STAT_READY;
+		ev.bufferId_ = { RPiIpaMask::STATS | buffer->cookie() };
+		ipa_->processEvent(ev);
 	}
 
 	handleState();
@@ -1491,7 +1504,7 @@ void RPiCameraData::checkRequestCompleted()
 void RPiCameraData::tryRunPipeline()
 {
 	FrameBuffer *bayerBuffer, *embeddedBuffer;
-	IPAOperationData op;
+	RPiEventParams ev;
 
 	/* If any of our request or buffer queues are empty, we cannot proceed. */
 	if (state_ != State::Idle || requestQueue_.empty() ||
@@ -1546,9 +1559,9 @@ void RPiCameraData::tryRunPipeline()
 	 * queue the ISP output buffer listed in the request to start the HW
 	 * pipeline.
 	 */
-	op.operation = RPI_IPA_EVENT_QUEUE_REQUEST;
-	op.controls = { request->controls() };
-	ipa_->processEvent(op);
+	ev.ev_ = RPI_IPA_EVENT_QUEUE_REQUEST;
+	ev.controls_ = { request->controls() };
+	ipa_->processEvent(ev);
 
 	/* Queue up any ISP buffers passed into the request. */
 	for (auto &stream : isp_) {
@@ -1567,10 +1580,10 @@ void RPiCameraData::tryRunPipeline()
 			<< " Bayer buffer id: " << bayerBuffer->cookie()
 			<< " Embedded buffer id: " << embeddedBuffer->cookie();
 
-	op.operation = RPI_IPA_EVENT_SIGNAL_ISP_PREPARE;
-	op.data = { RPiIpaMask::EMBEDDED_DATA | embeddedBuffer->cookie(),
-		    RPiIpaMask::BAYER_DATA | bayerBuffer->cookie() };
-	ipa_->processEvent(op);
+	ev.ev_ = RPI_IPA_EVENT_SIGNAL_ISP_PREPARE;
+	ev.ispPrepare_.embeddedbufferId_ = RPiIpaMask::EMBEDDED_DATA | embeddedBuffer->cookie();
+	ev.ispPrepare_.bayerbufferId_    = RPiIpaMask::BAYER_DATA | bayerBuffer->cookie();
+	ipa_->processEvent(ev);
 }
 
 void RPiCameraData::tryFlushQueues()
