diff --git a/src/libcamera/converter/converter_v4l2_m2m.cpp b/src/libcamera/converter/converter_v4l2_m2m.cpp
index ffba0434e..b705b56e8 100644
--- a/src/libcamera/converter/converter_v4l2_m2m.cpp
+++ b/src/libcamera/converter/converter_v4l2_m2m.cpp
@@ -818,6 +818,7 @@ bool V4L2M2MConverter::supportsRequests()
 static std::initializer_list<std::string> compatibles = {
 	"mtk-mdp",
 	"pxp",
+	"qcom-camss-ope",
 };
 
 REGISTER_CONVERTER("v4l2_m2m", V4L2M2MConverter, compatibles)
diff --git a/src/libcamera/pipeline/camss/camss.cpp b/src/libcamera/pipeline/camss/camss.cpp
index 90d7b681e..51951ab20 100644
--- a/src/libcamera/pipeline/camss/camss.cpp
+++ b/src/libcamera/pipeline/camss/camss.cpp
@@ -38,6 +38,7 @@
 #include "camss_csi.h"
 #include "camss_frames.h"
 #include "camss_isp.h"
+#include "camss_isp_ope.h"
 #include "camss_isp_soft.h"
 
 namespace libcamera {
@@ -571,10 +572,13 @@ bool PipelineHandlerCamss::match(DeviceEnumerator *enumerator)
 		data->delayedCtrls_ =
 			std::make_unique<DelayedControls>(sensor->device(), params);
 
-		data->isp_ = std::make_unique<CamssIspSoft>(this, sensor, &data->controlInfo_);
-		if (!data->isp_->isValid()) {
-			LOG(Camss, Error) << "Failed to create software ISP";
-			continue;
+		data->isp_ = CamssIspOpe::match(this, enumerator, sensor, &data->controlInfo_);
+		if (data->isp_ == nullptr) {
+			data->isp_ = std::make_unique<CamssIspSoft>(this, sensor, &data->controlInfo_);
+			if (!data->isp_->isValid()) {
+				LOG(Camss, Error) << "Failed to create software ISP";
+				continue;
+			}
 		}
 
 		data->isp_->inputBufferReady.connect(data->csi_.get(),
diff --git a/src/libcamera/pipeline/camss/camss_isp_ope.cpp b/src/libcamera/pipeline/camss/camss_isp_ope.cpp
new file mode 100644
index 000000000..7d9a831cb
--- /dev/null
+++ b/src/libcamera/pipeline/camss/camss_isp_ope.cpp
@@ -0,0 +1,229 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Qualcomm CAMSS OPE ISP class
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include "camss_isp_ope.h"
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/controls.h>
+#include <libcamera/geometry.h>
+#include <libcamera/request.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/converter.h"
+#include "libcamera/internal/device_enumerator.h"
+#include "libcamera/internal/media_device.h"
+#include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/v4l2_subdevice.h"
+#include "libcamera/internal/v4l2_videodevice.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Camss)
+
+/**
+ * \class CamssIspOpe
+ * \brief CAMSS ISP class for the Offline Processing Engine (OPE) ISP
+ */
+
+/**
+ * \brief Constructs CamssIspOpe object
+ * \param[in] pipe The pipeline handler in use
+ * \param[in] sensor Pointer to the CameraSensor instance owned by the pipeline
+ * \param[out] ControlInfoMap to which to add ISP provided controls
+ */
+CamssIspOpe::CamssIspOpe([[maybe_unused]] PipelineHandler *pipe, const CameraSensor *sensor, [[maybe_unused]] ControlInfoMap *ispControls)
+	: sensor_(sensor)
+{
+#if 0
+	swIsp_ = std::make_unique<SoftwareIsp>(pipe, sensor, ispControls);
+
+	swIsp_->ispStatsReady.connect(this,
+				      [&](uint32_t frame, uint32_t bufferId) {
+						statsReady.emit(frame, bufferId);
+				      });
+	swIsp_->metadataReady.connect(this,
+				      [&](uint32_t frame, const ControlList &metadata) {
+						metadataReady.emit(frame, metadata);
+				      });
+	swIsp_->setSensorControls.connect(this,
+					  [&](const ControlList &sensorControls) {
+						setSensorControls.emit(sensorControls);
+					  });
+#endif
+}
+
+CamssIspOpe::~CamssIspOpe() = default;
+
+bool CamssIspOpe::isValid()
+{
+	return true;
+}
+
+StreamConfiguration CamssIspOpe::generateConfiguration(const StreamConfiguration &raw) const
+{
+	/* Converters support the same size-ranges for all output formats */
+	std::vector<PixelFormat> pixelFormats = converter_->formats(raw.pixelFormat);
+	SizeRange sizes = converter_->sizes(raw.size);
+
+	if (sizes.max.isNull() || pixelFormats.empty())
+		return {};
+
+	std::vector<SizeRange> sizesVector = { sizes };
+	std::map<PixelFormat, std::vector<SizeRange>> formats;
+
+	for (unsigned int i = 0; i < pixelFormats.size(); i++)
+		formats[pixelFormats[i]] = sizesVector;
+
+	StreamConfiguration cfg{ StreamFormats{ formats } };
+	cfg.size = sizes.max;
+	cfg.pixelFormat = pixelFormats[0];
+	cfg.bufferCount = kBufferCount;
+
+	return cfg;
+}
+
+namespace {
+
+/*
+ * \todo copy-pasted from src/libcamera/pipeline/simple/simple.cpp turn this
+ * into a member of SizeRange ?
+ */
+static Size adjustSize(const Size &requestedSize, const SizeRange &supportedSizes)
+{
+	ASSERT(supportedSizes.min <= supportedSizes.max);
+
+	if (supportedSizes.min == supportedSizes.max)
+		return supportedSizes.max;
+
+	unsigned int hStep = supportedSizes.hStep;
+	unsigned int vStep = supportedSizes.vStep;
+
+	if (hStep == 0)
+		hStep = supportedSizes.max.width - supportedSizes.min.width;
+	if (vStep == 0)
+		vStep = supportedSizes.max.height - supportedSizes.min.height;
+
+	Size adjusted = requestedSize.boundedTo(supportedSizes.max)
+				.expandedTo(supportedSizes.min);
+
+	return adjusted.shrunkBy(supportedSizes.min)
+		.alignedDownTo(hStep, vStep)
+		.grownBy(supportedSizes.min);
+}
+
+} /* namespace */
+
+StreamConfiguration CamssIspOpe::validate(const StreamConfiguration &raw, const StreamConfiguration &req) const
+{
+	StreamConfiguration cfg;
+
+	std::vector<PixelFormat> formats = converter_->formats(raw.pixelFormat);
+	SizeRange sizes = converter_->sizes(raw.size);
+
+	cfg.size = adjustSize(req.size, sizes);
+
+	if (cfg.size.isNull() || formats.empty())
+		return {};
+
+	for (unsigned int i = 0; i < formats.size(); i++) {
+		if (formats[i] == req.pixelFormat)
+			cfg.pixelFormat = req.pixelFormat;
+	}
+
+	if (!cfg.pixelFormat.isValid())
+		cfg.pixelFormat = formats[0];
+
+	std::tie(cfg.stride, cfg.frameSize) =
+		converter_->strideAndFrameSize(cfg.pixelFormat, cfg.size);
+
+	cfg.bufferCount = std::max(kBufferCount, req.bufferCount);
+	cfg.setStream(const_cast<Stream *>(&outStream_));
+
+	return cfg;
+}
+
+int CamssIspOpe::configure(const StreamConfiguration &inputCfg,
+			   const StreamConfiguration &outputCfg)
+{
+	std::vector<std::reference_wrapper<const StreamConfiguration>> outputCfgs;
+	outputCfgs.push_back(outputCfg);
+
+	int ret = converter_->configure(inputCfg, outputCfgs);
+	if (ret)
+		return ret;
+
+	converter_->inputBufferReady.connect(this,
+					     [&](FrameBuffer *f) { inputBufferReady.emit(f); });
+	converter_->outputBufferReady.connect(this,
+					      [&](FrameBuffer *f) {
+						      // HACK FIXME
+						      metadataReady.emit(f->metadata().sequence, {});
+						      outputBufferReady.emit(f);
+					      });
+
+	return 0;
+}
+
+int CamssIspOpe::exportOutputBuffers(const Stream *stream, unsigned int count,
+				     std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	return converter_->exportBuffers(stream, count, buffers);
+}
+
+void CamssIspOpe::queueBuffers(Request *request, FrameBuffer *input)
+{
+	std::map<const Stream *, FrameBuffer *> outputs;
+	for (const auto &[stream, outbuffer] : request->buffers()) {
+		if (stream == &outStream_)
+			outputs[stream] = outbuffer;
+	}
+
+	//	swIsp_->queueRequest(request->sequence(), request->controls());
+	converter_->queueBuffers(input, outputs);
+}
+
+void CamssIspOpe::processStats([[maybe_unused]] const uint32_t frame, [[maybe_unused]] const uint32_t bufferId,
+			       [[maybe_unused]] const ControlList &sensorControls)
+{
+	//	swIsp_->processStats(frame, bufferId, sensorControls);
+}
+
+int CamssIspOpe::start()
+{
+	return converter_->start();
+}
+
+void CamssIspOpe::stop()
+{
+	converter_->stop();
+}
+
+std::unique_ptr<CamssIspOpe> CamssIspOpe::match(PipelineHandler *pipe,
+						DeviceEnumerator *enumerator,
+						const CameraSensor *sensor,
+						ControlInfoMap *ispControls)
+{
+	std::unique_ptr<CamssIspOpe> ope = std::make_unique<CamssIspOpe>(pipe, sensor, ispControls);
+
+	DeviceMatch opeDm("qcom-camss-ope");
+
+	ope->opeMediaDev_ = pipe->acquireMediaDevice(enumerator, opeDm);
+	if (!ope->opeMediaDev_)
+		return nullptr;
+
+	ope->converter_ = ConverterFactoryBase::create(ope->opeMediaDev_);
+	if (!ope->converter_)
+		return nullptr;
+
+	LOG(Camss, Info) << "Using OPE for " << sensor->entity()->name();
+
+	return ope;
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/camss/camss_isp_ope.h b/src/libcamera/pipeline/camss/camss_isp_ope.h
new file mode 100644
index 000000000..4324e4c0a
--- /dev/null
+++ b/src/libcamera/pipeline/camss/camss_isp_ope.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Qualcomm CAMSS OPE ISP class
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "camss_isp.h"
+
+namespace libcamera {
+
+class CameraSensor;
+class ControlInfoMap;
+class Converter;
+class DeviceEnumerator;
+class MediaDevice;
+class PipelineHandler;
+class SoftwareIsp;
+
+class CamssIspOpe : public CamssIsp
+{
+public:
+	static std::unique_ptr<CamssIspOpe> match(PipelineHandler *pipe,
+						  DeviceEnumerator *enumerator,
+						  const CameraSensor *sensor,
+						  ControlInfoMap *ispControls);
+
+	CamssIspOpe(PipelineHandler *pipe, const CameraSensor *sensor, ControlInfoMap *ispControls);
+	~CamssIspOpe() override;
+
+	bool isValid() override;
+
+	StreamConfiguration generateConfiguration(const StreamConfiguration &raw) const override;
+	StreamConfiguration validate(const StreamConfiguration &raw, const StreamConfiguration &req) const override;
+	int configure(const StreamConfiguration &inputCfg,
+		      const StreamConfiguration &outputCfg) override;
+
+	int exportOutputBuffers(const Stream *stream, unsigned int count,
+				std::vector<std::unique_ptr<FrameBuffer>> *buffers) override;
+	void queueBuffers(Request *request, FrameBuffer *input) override;
+
+	void processStats(const uint32_t frame, const uint32_t bufferId,
+			  const ControlList &sensorControls) override;
+
+	int start() override;
+	void stop() override;
+
+private:
+	std::shared_ptr<MediaDevice> opeMediaDev_;
+	std::unique_ptr<Converter> converter_;
+	const CameraSensor *sensor_;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/camss/meson.build b/src/libcamera/pipeline/camss/meson.build
index 047559789..36478cf7d 100644
--- a/src/libcamera/pipeline/camss/meson.build
+++ b/src/libcamera/pipeline/camss/meson.build
@@ -5,5 +5,6 @@ libcamera_internal_sources += files([
     'camss_csi.cpp',
     'camss_frames.cpp',
     'camss_isp.cpp',
+    'camss_isp_ope.cpp',
     'camss_isp_soft.cpp',
 ])
