@@ -35,6 +35,7 @@ subdir('media_device')
subdir('process')
subdir('py')
subdir('serialization')
+subdir('software_isp')
subdir('stream')
subdir('v4l2_compat')
subdir('v4l2_subdevice')
new file mode 100644
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026, Red Hat Inc.
+ *
+ * CPU debayer crop configuration tests
+ */
+
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include <libcamera/camera_manager.h>
+#include <libcamera/formats.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/formats.h"
+#include "libcamera/internal/software_isp/swstats_cpu.h"
+
+#include "debayer_cpu.h"
+#include "test.h"
+
+using namespace libcamera;
+
+class DebayerCropTest : public Test
+{
+protected:
+ int run() override
+ {
+ CameraManager manager;
+ auto stats = std::make_unique<SwStatsCpu>(manager);
+ if (!stats->isValid())
+ return TestFail;
+
+ DebayerCpu debayer(std::move(stats), manager);
+ static const struct {
+ PixelFormat input;
+ int rgbShift;
+ int bgrShift;
+ } cases[] = {
+ { formats::SBGGR8, 0, 1 },
+ { formats::SGBRG8, 1, 0 },
+ { formats::SGRBG8, 0, 1 },
+ { formats::SRGGB8, 1, 0 },
+ { formats::SBGGR10, 0, 1 },
+ { formats::SGBRG10, 1, 0 },
+ { formats::SGRBG10, 0, 1 },
+ { formats::SRGGB10, 1, 0 },
+ { formats::SBGGR12, 0, 1 },
+ { formats::SGBRG12, 1, 0 },
+ { formats::SGRBG12, 0, 1 },
+ { formats::SRGGB12, 1, 0 },
+ /* Packed kernels use each Bayer order without a phase shift. */
+ { formats::SBGGR10_CSI2P, 0, 0 },
+ { formats::SGBRG10_CSI2P, 0, 0 },
+ { formats::SGRBG10_CSI2P, 0, 0 },
+ { formats::SRGGB10_CSI2P, 0, 0 },
+ { formats::SBGGR12_CSI2P, 0, 0 },
+ { formats::SGBRG12_CSI2P, 0, 0 },
+ { formats::SGRBG12_CSI2P, 0, 0 },
+ { formats::SRGGB12_CSI2P, 0, 0 },
+ };
+
+ for (const auto &entry : cases) {
+ StreamConfiguration input;
+ input.pixelFormat = entry.input;
+ input.size = { 1928, 1092 };
+ input.stride = PixelFormatInfo::info(entry.input).stride(input.size.width, 0);
+
+ for (PixelFormat format : { formats::RGB888, formats::BGR888,
+ formats::XRGB8888, formats::XBGR8888 }) {
+ StreamConfiguration output;
+ output.pixelFormat = format;
+ output.size = { 1280, 720 };
+ std::tie(output.stride, output.frameSize) =
+ debayer.strideAndFrameSize(format, output.size);
+ const std::vector<std::reference_wrapper<const StreamConfiguration>> outputs{ output };
+ if (debayer.configure(input, outputs, false)) {
+ std::cerr << "Unable to configure " << entry.input << " -> " << format
+ << std::endl;
+ return TestFail;
+ }
+
+ int shift = (format == formats::RGB888 || format == formats::XRGB8888)
+ ? entry.rgbShift
+ : entry.bgrShift;
+ Rectangle expected(324 + shift, 186, 1280, 720);
+ if (debayer.inputCrop() != expected) {
+ std::cerr << "Crop for " << entry.input << " -> " << format << " is "
+ << debayer.inputCrop() << ", expected " << expected << std::endl;
+ return TestFail;
+ }
+ }
+ }
+
+ return TestPass;
+ }
+};
+
+TEST_REGISTER(DebayerCropTest)
new file mode 100644
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: CC0-1.0
+
+if not softisp_enabled
+ subdir_done()
+endif
+
+software_isp_tests = [
+ {'name': 'debayer-crop', 'sources': ['debayer-crop.cpp']},
+]
+
+foreach test : software_isp_tests
+ exe = executable(test['name'], test['sources'],
+ dependencies : libcamera_private,
+ implicit_include_directories : false,
+ link_with : test_libraries,
+ include_directories : [test_includes_internal,
+ '../../src/libcamera/software_isp/'])
+
+ test(test['name'], exe, suite : 'software_isp')
+endforeach