diff --git a/Documentation/runtime_configuration.rst b/Documentation/runtime_configuration.rst
index a71b715a1..1d93ad3a8 100644
--- a/Documentation/runtime_configuration.rst
+++ b/Documentation/runtime_configuration.rst
@@ -46,6 +46,7 @@ file structure:
         supported_devices:
           - driver: # driver name, e.g. `mxc-isi`
             software_isp: # true/false
+        enable_raw_ipa: # true/false
     software_isp:
       copy_input_buffer: # true/false
       measure:
@@ -81,6 +82,7 @@ Configuration file example
          supported_devices:
            - driver: mxc-isi
              software_isp: true
+         enable_raw_ipa: true
      software_isp:
        copy_input_buffer: false
        measure:
@@ -157,6 +159,14 @@ pipelines.simple.supported_devices.driver, pipelines.simple.supported_devices.so
 
    Example `software_isp` value: ``true``
 
+pipelines.simple.enable_raw_ipa
+   Whether image processing algorithms are enabled also with raw-only
+   output. When enabled, statistics is gathered for raw-only output,
+   image processing algorithms are run on it, metadata is produced and
+   exposure is adjusted.
+
+   Example value: ``true``
+
 software_isp.copy_input_buffer
    Define whether input buffers should be copied into standard (cached)
    memory in software ISP. This is done by default to prevent very slow
diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index 3e420242b..b4c57dbf5 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -67,7 +67,8 @@ public:
 	int configure(const StreamConfiguration &inputCfg,
 		      const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 		      const ipa::softisp::IPAConfigInfo &configInfo,
-		      ControlInfoMap *ipaControls);
+		      ControlInfoMap *ipaControls,
+		      bool debayerEnabled);
 
 	int exportBuffers(const Stream *stream, unsigned int count,
 			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index 35c29ceca..dd1d69a5b 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -427,6 +427,7 @@ public:
 	V4L2Subdevice *subdev(const MediaEntity *entity);
 	std::shared_ptr<MediaDevice> converter() { return converter_; }
 	bool swIspEnabled() const { return swIspEnabled_; }
+	bool rawIpaEnabled() const { return rawIpaEnabled_; }
 
 protected:
 	int queueRequestDevice(Camera *camera, Request *request) override;
@@ -460,6 +461,7 @@ private:
 
 	std::shared_ptr<MediaDevice> converter_;
 	bool swIspEnabled_;
+	bool rawIpaEnabled_ = true;
 };
 
 /* -----------------------------------------------------------------------------
@@ -968,6 +970,11 @@ void SimpleCameraData::imageBufferReady(FrameBuffer *buffer)
 
 		conversionQueue_.pop();
 		return;
+	} else if (pipe->rawIpaEnabled()) {
+		/* \todo Make the processing asynchronous? */
+		swIsp_->process(request->sequence(), buffer, nullptr);
+		pipe->completeBuffer(request, buffer);
+		return;
 	}
 
 	/* Otherwise simply complete the request. */
@@ -1591,7 +1598,8 @@ int SimplePipelineHandler::configure(Camera *camera, CameraConfiguration *c)
 			data->rawStream_ = &data->streams_[i];
 	}
 
-	if (outputCfgs.empty())
+	if (outputCfgs.empty() &&
+	    (data->converter_ || !data->rawStream_ || !rawIpaEnabled_))
 		return 0;
 
 	StreamConfiguration inputCfg;
@@ -1605,7 +1613,12 @@ int SimplePipelineHandler::configure(Camera *camera, CameraConfiguration *c)
 	} else {
 		ipa::softisp::IPAConfigInfo configInfo;
 		configInfo.sensorControls = data->sensor_->controls();
-		return data->swIsp_->configure(inputCfg, outputCfgs, configInfo, &data->controlInfo_);
+		return data->swIsp_->configure(
+			inputCfg,
+			outputCfgs,
+			configInfo,
+			&data->controlInfo_,
+			data->useConversion_);
 	}
 }
 
@@ -1698,6 +1711,12 @@ int SimplePipelineHandler::start(Camera *camera, [[maybe_unused]] const ControlL
 		if (!data->rawStream_)
 			for (std::unique_ptr<FrameBuffer> &buffer : data->conversionBuffers_)
 				video->queueBuffer(buffer.get());
+	} else if (data->swIsp_) {
+		ret = data->swIsp_->startIpa();
+		if (ret < 0) {
+			stop(camera);
+			return ret;
+		}
 	}
 
 	return 0;
@@ -1720,6 +1739,8 @@ void SimplePipelineHandler::stopDevice(Camera *camera)
 			data->converter_->stop();
 		else if (data->swIsp_)
 			data->swIsp_->stop();
+	} else if (data->swIsp_) {
+		data->swIsp_->stopIpa();
 	}
 
 	video->streamOff();
@@ -1763,6 +1784,8 @@ int SimplePipelineHandler::queueRequestDevice(Camera *camera, Request *request)
 		data->conversionQueue_.push({ request, std::move(buffers) });
 		if (data->swIsp_)
 			data->swIsp_->queueRequest(request->sequence(), request->controls());
+	} else if (data->swIsp_ && rawIpaEnabled_) {
+		data->swIsp_->queueRequest(request->sequence(), request->controls());
 	}
 
 	return 0;
@@ -1882,9 +1905,8 @@ bool SimplePipelineHandler::matchDevice(std::shared_ptr<MediaDevice> media,
 
 	swIspEnabled_ = info.swIspEnabled;
 	const GlobalConfiguration &configuration = cameraManager()->_d()->configuration();
-	for (const ValueNode &entry :
-	     configuration.configuration()["pipelines"]["simple"]["supported_devices"]
-		     .asList()) {
+	auto &simpleConfiguration = configuration.configuration()["pipelines"]["simple"];
+	for (const ValueNode &entry : simpleConfiguration["supported_devices"].asList()) {
 		auto name = entry["driver"].get<std::string>();
 		if (name == info.driver) {
 			swIspEnabled_ = entry["software_isp"].get<bool>().value_or(swIspEnabled_);
@@ -1903,6 +1925,10 @@ bool SimplePipelineHandler::matchDevice(std::shared_ptr<MediaDevice> media,
 		 */
 		ASSERT(!converter_);
 		numStreams = 2;
+
+		auto enableRawIpa = simpleConfiguration["enable_raw_ipa"].get<bool>();
+		if (enableRawIpa)
+			rawIpaEnabled_ = enableRawIpa.value();
 	}
 
 	/* Locate the sensors. */
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index 11e0c49ed..594125f8f 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -25,6 +25,7 @@
 
 #include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/mapped_framebuffer.h"
 #include "libcamera/internal/software_isp/debayer_params.h"
 
 #include "debayer_cpu.h"
@@ -280,19 +281,34 @@ uint32_t SoftwareIsp::preferredInputStride(const PixelFormat &inputFormat, const
  * \param[in] outputCfgs The output configurations
  * \param[in] configInfo The IPA configuration data, received from the pipeline handler
  * \param[out] ipaControls The IPA controls to update
+ * \param[in] debayerEnabled Whether debayering should be performed in addition
+ *   to stats and IPA processing
  * \return 0 on success, a negative errno on failure
  */
 int SoftwareIsp::configure(const StreamConfiguration &inputCfg,
 			   const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 			   const ipa::softisp::IPAConfigInfo &configInfo,
-			   ControlInfoMap *ipaControls)
+			   ControlInfoMap *ipaControls,
+			   bool debayerEnabled)
 {
-	ASSERT(ipa_ && debayer_);
+	ASSERT(ipa_ && (!debayerEnabled || debayer_));
+
+	int ret;
+	if (!debayerEnabled) {
+		ret = stats_->configure(inputCfg);
+		if (ret < 0)
+			return ret;
+		stats_->setWindow(Rectangle(inputCfg.size));
+	}
 
-	int ret = ipa_->configure(configInfo, ipaControls);
+	ret = ipa_->configure(configInfo, ipaControls);
 	if (ret < 0)
 		return ret;
 
+	if (!debayerEnabled) {
+		return 0;
+	}
+
 	ret = debayer_->configure(inputCfg, outputCfgs, ccmEnabled_);
 	if (ret < 0)
 		return ret;
@@ -452,13 +468,25 @@ void SoftwareIsp::stopIpa()
  * \brief Passes the input framebuffer to the ISP worker to process
  * \param[in] frame The frame number
  * \param[in] input The input framebuffer
- * \param[out] output The framebuffer to write the processed frame to
+ * \param[out] output The framebuffer to write the processed frame to; if
+ *   nullptr then debayering is skipped and stats are processed synchronously
  */
 void SoftwareIsp::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output)
 {
 	ipa_->computeParams(frame);
-	debayer_->invokeMethod(&Debayer::process,
-			       ConnectionTypeQueued, frame, input, output, debayerParams_);
+
+	if (output) {
+		debayer_->invokeMethod(&Debayer::process,
+				       ConnectionTypeQueued, frame, input, output, debayerParams_);
+	} else {
+		/* Compute stats, which are otherwise computed in debayering */
+		if (frame % SwStatsCpu::kStatPerNumFrames) {
+			stats_->finishFrame(frame, 0);
+		} else {
+			MappedFrameBuffer inputMapped(input, MappedFrameBuffer::MapFlag::Read);
+			stats_->processFrame(frame, 0, inputMapped);
+		}
+	}
 }
 
 void SoftwareIsp::saveIspParams([[maybe_unused]] uint32_t frame)
