diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 3077c6bb..cc7da299 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -489,6 +489,34 @@ int PipelineHandlerIPU3::configure(Camera *camera, CameraConfiguration *c)
 	V4L2DeviceFormat outputFormat;
 	int ret;
 
+	/*
+	 * Pass the requested stream size to the CIO2 unit and get back the
+	 * adjusted format to be propagated to the ImgU output devices.
+	 */
+	const Size &sensorSize = config->cio2Format().size;
+	V4L2DeviceFormat cio2Format;
+	ret = cio2->configure(sensorSize, config->combinedTransform_, &cio2Format);
+	if (ret)
+		return ret;
+
+	IPACameraSensorInfo sensorInfo;
+	cio2->sensor()->sensorInfo(&sensorInfo);
+	data->cropRegion_ = sensorInfo.analogCrop;
+
+	/*
+	 * If the ImgU gets configured, its driver seems to expect that
+	 * buffers will be queued to its outputs, as otherwise the next
+	 * capture session that uses the ImgU fails when queueing
+	 * buffers to its input.
+	 *
+	 * If no ImgU configuration has been computed, it means only a RAW
+	 * stream has been requested: return here to skip the ImgU configuration
+	 * part.
+	 */
+	ImgUDevice::PipeConfig imguConfig = config->imguConfig();
+	if (imguConfig.isNull())
+		return 0;
+
 	/*
 	 * FIXME: enabled links in one ImgU pipe interfere with capture
 	 * operations on the other one. This can be easily triggered by
@@ -531,34 +559,6 @@ int PipelineHandlerIPU3::configure(Camera *camera, CameraConfiguration *c)
 	if (ret)
 		return ret;
 
-	/*
-	 * Pass the requested stream size to the CIO2 unit and get back the
-	 * adjusted format to be propagated to the ImgU output devices.
-	 */
-	const Size &sensorSize = config->cio2Format().size;
-	V4L2DeviceFormat cio2Format;
-	ret = cio2->configure(sensorSize, config->combinedTransform_, &cio2Format);
-	if (ret)
-		return ret;
-
-	IPACameraSensorInfo sensorInfo;
-	cio2->sensor()->sensorInfo(&sensorInfo);
-	data->cropRegion_ = sensorInfo.analogCrop;
-
-	/*
-	 * If the ImgU gets configured, its driver seems to expect that
-	 * buffers will be queued to its outputs, as otherwise the next
-	 * capture session that uses the ImgU fails when queueing
-	 * buffers to its input.
-	 *
-	 * If no ImgU configuration has been computed, it means only a RAW
-	 * stream has been requested: return here to skip the ImgU configuration
-	 * part.
-	 */
-	ImgUDevice::PipeConfig imguConfig = config->imguConfig();
-	if (imguConfig.isNull())
-		return 0;
-
 	ret = imgu->configure(imguConfig, &cio2Format);
 	if (ret)
 		return ret;
@@ -660,36 +660,39 @@ int PipelineHandlerIPU3::exportFrameBuffers(Camera *camera, Stream *stream,
 int PipelineHandlerIPU3::allocateBuffers(Camera *camera)
 {
 	IPU3CameraData *data = cameraData(camera);
+	CIO2Device *cio2 = &data->cio2_;
 	ImgUDevice *imgu = data->imgu_;
 	unsigned int bufferCount;
 	int ret;
 
-	bufferCount = std::max({
-		data->outStream_.configuration().bufferCount,
-		data->vfStream_.configuration().bufferCount,
-		data->rawStream_.configuration().bufferCount,
-	});
+	if (cio2->needsImgu()) {
+		bufferCount = std::max({
+			data->outStream_.configuration().bufferCount,
+			data->vfStream_.configuration().bufferCount,
+			data->rawStream_.configuration().bufferCount,
+		});
 
-	ret = imgu->allocateBuffers(bufferCount);
-	if (ret < 0)
-		return ret;
+		ret = imgu->allocateBuffers(bufferCount);
+		if (ret < 0)
+			return ret;
 
-	/* Map buffers to the IPA. */
-	unsigned int ipaBufferId = 1;
+		/* Map buffers to the IPA. */
+		unsigned int ipaBufferId = 1;
 
-	for (const std::unique_ptr<FrameBuffer> &buffer : imgu->paramBuffers_) {
-		buffer->setCookie(ipaBufferId++);
-		ipaBuffers_.emplace_back(buffer->cookie(), buffer->planes());
-	}
+		for (const std::unique_ptr<FrameBuffer> &buffer : imgu->paramBuffers_) {
+			buffer->setCookie(ipaBufferId++);
+			ipaBuffers_.emplace_back(buffer->cookie(), buffer->planes());
+		}
 
-	for (const std::unique_ptr<FrameBuffer> &buffer : imgu->statBuffers_) {
-		buffer->setCookie(ipaBufferId++);
-		ipaBuffers_.emplace_back(buffer->cookie(), buffer->planes());
-	}
+		for (const std::unique_ptr<FrameBuffer> &buffer : imgu->statBuffers_) {
+			buffer->setCookie(ipaBufferId++);
+			ipaBuffers_.emplace_back(buffer->cookie(), buffer->planes());
+		}
 
-	data->ipa_->mapBuffers(ipaBuffers_);
+		data->ipa_->mapBuffers(ipaBuffers_);
+		data->frameInfos_.init(imgu->paramBuffers_, imgu->statBuffers_);
+	}
 
-	data->frameInfos_.init(imgu->paramBuffers_, imgu->statBuffers_);
 	data->frameInfos_.bufferAvailable.connect(
 		data, &IPU3CameraData::queuePendingRequests);
 
@@ -699,17 +702,20 @@ int PipelineHandlerIPU3::allocateBuffers(Camera *camera)
 int PipelineHandlerIPU3::freeBuffers(Camera *camera)
 {
 	IPU3CameraData *data = cameraData(camera);
+	CIO2Device *cio2 = &data->cio2_;
 
 	data->frameInfos_.clear();
 
-	std::vector<unsigned int> ids;
-	for (IPABuffer &ipabuf : ipaBuffers_)
-		ids.push_back(ipabuf.id);
+	if (cio2->needsImgu()) {
+		std::vector<unsigned int> ids;
+		for (IPABuffer &ipabuf : ipaBuffers_)
+			ids.push_back(ipabuf.id);
 
-	data->ipa_->unmapBuffers(ids);
-	ipaBuffers_.clear();
+		data->ipa_->unmapBuffers(ids);
+		data->imgu_->freeBuffers();
 
-	data->imgu_->freeBuffers();
+		ipaBuffers_.clear();
+	}
 
 	return 0;
 }
@@ -736,10 +742,6 @@ int PipelineHandlerIPU3::start(Camera *camera, [[maybe_unused]] const ControlLis
 	if (ret)
 		return ret;
 
-	ret = data->ipa_->start();
-	if (ret)
-		goto error;
-
 	data->delayedCtrls_->reset();
 
 	/*
@@ -750,16 +752,24 @@ int PipelineHandlerIPU3::start(Camera *camera, [[maybe_unused]] const ControlLis
 	if (ret)
 		goto error;
 
-	ret = imgu->start();
-	if (ret)
-		goto error;
+	if (cio2->needsImgu()) {
+		ret = data->ipa_->start();
+		if (ret)
+			goto error;
+
+		ret = imgu->start();
+		if (ret)
+			goto error;
+	}
 
 	return 0;
 
 error:
-	imgu->stop();
 	cio2->stop();
-	data->ipa_->stop();
+	if (cio2->needsImgu()) {
+		imgu->stop();
+		data->ipa_->stop();
+	}
 	freeBuffers(camera);
 	LOG(IPU3, Error) << "Failed to start camera " << camera->id();
 
@@ -773,9 +783,11 @@ void PipelineHandlerIPU3::stopDevice(Camera *camera)
 
 	data->cancelPendingRequests();
 
-	data->ipa_->stop();
+	if (data->cio2_.needsImgu()) {
+		data->ipa_->stop();
+		ret |= data->imgu_->stop();
+	}
 
-	ret |= data->imgu_->stop();
 	ret |= data->cio2_.stop();
 	if (ret)
 		LOG(IPU3, Warning) << "Failed to stop camera " << camera->id();
@@ -806,7 +818,7 @@ void IPU3CameraData::queuePendingRequests()
 	while (!pendingRequests_.empty()) {
 		Request *request = pendingRequests_.front();
 
-		IPU3Frames::Info *info = frameInfos_.create(request, true);
+		IPU3Frames::Info *info = frameInfos_.create(request, cio2_.needsImgu());
 		if (!info)
 			break;
 
@@ -829,7 +841,8 @@ void IPU3CameraData::queuePendingRequests()
 
 		info->rawBuffer = rawBuffer;
 
-		ipa_->queueRequest(info->id, request->controls());
+		if (cio2_.needsImgu())
+			ipa_->queueRequest(info->id, request->controls());
 
 		pendingRequests_.pop();
 		processingRequests_.push(request);
@@ -1070,10 +1083,6 @@ int PipelineHandlerIPU3::registerCameras()
 		if (ret)
 			continue;
 
-		ret = data->loadIPA();
-		if (ret)
-			continue;
-
 		/* Initialize the camera properties. */
 		data->properties_ = cio2->sensor()->properties();
 
@@ -1104,36 +1113,44 @@ int PipelineHandlerIPU3::registerCameras()
 					   << cio2->sensor()->id()
 					   << ". Assume rotation 0";
 
-		/**
-		 * \todo Dynamically assign ImgU and output devices to each
-		 * stream and camera; as of now, limit support to two cameras
-		 * only, and assign imgu0 to the first one and imgu1 to the
-		 * second.
-		 */
-		data->imgu_ = numCameras ? &imgu1_ : &imgu0_;
-
 		/*
 		 * Connect video devices' 'bufferReady' signals to their
 		 * slot to implement the image processing pipeline.
-		 *
-		 * Frames produced by the CIO2 unit are passed to the
-		 * associated ImgU input where they get processed and
-		 * returned through the ImgU main and secondary outputs.
 		 */
 		data->cio2_.bufferReady().connect(data.get(),
-					&IPU3CameraData::cio2BufferReady);
+						  &IPU3CameraData::cio2BufferReady);
 		data->cio2_.bufferAvailable.connect(
 			data.get(), &IPU3CameraData::queuePendingRequests);
-		data->imgu_->input_->bufferReady.connect(&data->cio2_,
-					&CIO2Device::tryReturnBuffer);
-		data->imgu_->output_->bufferReady.connect(data.get(),
-					&IPU3CameraData::imguOutputBufferReady);
-		data->imgu_->viewfinder_->bufferReady.connect(data.get(),
-					&IPU3CameraData::imguOutputBufferReady);
-		data->imgu_->param_->bufferReady.connect(data.get(),
-					&IPU3CameraData::paramBufferReady);
-		data->imgu_->stat_->bufferReady.connect(data.get(),
-					&IPU3CameraData::statBufferReady);
+
+		if (cio2->needsImgu()) {
+			ret = data->loadIPA();
+			if (ret)
+				continue;
+
+			/**
+			 * \todo Dynamically assign ImgU and output devices to each
+			 * stream and camera; as of now, limit support to two cameras
+			 * only, and assign imgu0 to the first one and imgu1 to the
+			 * second.
+			 */
+			data->imgu_ = numCameras ? &imgu1_ : &imgu0_;
+
+			/*
+			* Frames produced by the CIO2 unit are passed to the
+			* associated ImgU input where they get processed and
+			* returned through the ImgU main and secondary outputs.
+			*/
+			data->imgu_->input_->bufferReady.connect(&data->cio2_,
+						&CIO2Device::tryReturnBuffer);
+			data->imgu_->output_->bufferReady.connect(data.get(),
+						&IPU3CameraData::imguOutputBufferReady);
+			data->imgu_->viewfinder_->bufferReady.connect(data.get(),
+						&IPU3CameraData::imguOutputBufferReady);
+			data->imgu_->param_->bufferReady.connect(data.get(),
+						&IPU3CameraData::paramBufferReady);
+			data->imgu_->stat_->bufferReady.connect(data.get(),
+						&IPU3CameraData::statBufferReady);
+		}
 
 		/* Create and register the Camera instance. */
 		const std::string &cameraId = cio2->sensor()->id();
@@ -1330,10 +1347,15 @@ void IPU3CameraData::cio2BufferReady(FrameBuffer *buffer)
 
 	info->effectiveSensorControls = delayedCtrls_->get(buffer->metadata().sequence);
 
-	if (request->findBuffer(&rawStream_))
+	if (request->findBuffer(&rawStream_)) {
 		pipe()->completeBuffer(request, buffer);
 
-	ipa_->fillParamsBuffer(info->id, info->paramBuffer->cookie());
+		if (frameInfos_.tryComplete(info))
+			pipe()->completeRequest(request);
+	}
+
+	if (cio2_.needsImgu())
+		ipa_->fillParamsBuffer(info->id, info->paramBuffer->cookie());
 }
 
 void IPU3CameraData::paramBufferReady(FrameBuffer *buffer)
