diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index f7363244e1d2d0ff..0e7555c716b36749 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -190,6 +190,7 @@ private:
 	static constexpr unsigned int IPU3_BUFFER_COUNT = 4;
 	static constexpr unsigned int IPU3_MAX_STREAMS = 3;
 
+	int updateStreams();
 	void adjustStream(StreamConfiguration &cfg, bool scale);
 
 	/*
@@ -256,6 +257,51 @@ IPU3CameraConfiguration::IPU3CameraConfiguration(Camera *camera,
 	data_ = data;
 }
 
+int IPU3CameraConfiguration::updateStreams()
+{
+	std::set<const IPU3Stream *> availableStreams = {
+		&data_->outStream_,
+		&data_->vfStream_,
+		&data_->rawStream_,
+	};
+
+	/* Pick the stream most suitable for the requested configuration. */
+	std::vector<const IPU3Stream *> streams;
+	for (unsigned int i = 0; i < config_.size(); ++i) {
+		const StreamConfiguration &cfg = config_[i];
+		const IPU3Stream *stream;
+
+		/*
+		 * Only the raw stream can support Bayer formats.
+		 */
+		if (cfg.pixelFormat.modifier() == IPU3_FORMAT_MOD_PACKED)
+			stream = &data_->rawStream_;
+		/*
+		 * Output stream can't scale so can only be used if the size
+		 * matches the size of the sensor.
+		 *
+		 * NOTE: It can crop but is not supported.
+		 */
+		else if (cfg.size == sensorFormat_.size)
+			stream = &data_->outStream_;
+		/*
+		 * Pick the view finder stream last as it may scale.
+		 */
+		else
+			stream = &data_->vfStream_;
+
+		if (availableStreams.find(stream) == availableStreams.end())
+			return -EINVAL;
+
+		streams.push_back(stream);
+		availableStreams.erase(stream);
+	}
+
+	streams_ = streams;
+
+	return 0;
+}
+
 void IPU3CameraConfiguration::adjustStream(StreamConfiguration &cfg, bool scale)
 {
 	/* The only pixel format the driver supports is NV12. */
@@ -342,40 +388,16 @@ CameraConfiguration::Status IPU3CameraConfiguration::validate()
 	if (!sensorFormat_.size.width || !sensorFormat_.size.height)
 		sensorFormat_.size = sensor->resolution();
 
-	/*
-	 * Verify and update all configuration entries, and assign a stream to
-	 * each of them. The viewfinder stream can scale, while the output
-	 * stream can crop only, so select the output stream when the requested
-	 * resolution is equal to the sensor resolution, and the viewfinder
-	 * stream otherwise.
-	 */
-	std::set<const IPU3Stream *> availableStreams = {
-		&data_->outStream_,
-		&data_->vfStream_,
-		&data_->rawStream_,
-	};
 
-	streams_.clear();
-	streams_.reserve(config_.size());
+	/* Assign streams to each configuration entry. */
+	if (updateStreams())
+		return Invalid;
 
+	/* Verify and adjust configuration if needed. */
 	for (unsigned int i = 0; i < config_.size(); ++i) {
 		StreamConfiguration &cfg = config_[i];
-		const PixelFormat pixelFormat = cfg.pixelFormat;
-		const Size size = cfg.size;
-		const IPU3Stream *stream;
-
-		if (cfg.pixelFormat.modifier() == IPU3_FORMAT_MOD_PACKED)
-			stream = &data_->rawStream_;
-		else if (cfg.size == sensorFormat_.size)
-			stream = &data_->outStream_;
-		else
-			stream = &data_->vfStream_;
-
-		if (availableStreams.find(stream) == availableStreams.end())
-			stream = *availableStreams.begin();
-
-		LOG(IPU3, Debug)
-			<< "Assigned '" << stream->name_ << "' to stream " << i;
+		const StreamConfiguration org = cfg;
+		const IPU3Stream *stream = streams_[i];
 
 		if (stream->raw_) {
 			const auto &itFormat =
@@ -392,15 +414,12 @@ CameraConfiguration::Status IPU3CameraConfiguration::validate()
 
 		cfg.bufferCount = IPU3_BUFFER_COUNT;
 
-		if (cfg.pixelFormat != pixelFormat || cfg.size != size) {
+		if (cfg.pixelFormat != org.pixelFormat || cfg.size != org.size) {
 			LOG(IPU3, Debug)
 				<< "Stream " << i << " configuration adjusted to "
 				<< cfg.toString();
 			status = Adjusted;
 		}
-
-		streams_.push_back(stream);
-		availableStreams.erase(stream);
 	}
 
 	return status;
