new file mode 100644
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026, Red Hat Inc.
+ *
+ * Sensor crop tests
+ */
+
+#include <iostream>
+
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/software_isp/debayer_geometry.h"
+
+#include "test.h"
+
+using namespace libcamera;
+
+class DebayerGeometryTest : public Test
+{
+protected:
+ int run() override
+ {
+ if (testCrop())
+ return TestFail;
+
+ return TestPass;
+ }
+
+private:
+ int testCrop()
+ {
+ static const struct {
+ Size input;
+ Size native;
+ Size output;
+ Rectangle expected;
+ } cases[] = {
+ { { 1920, 1080 }, { 1920, 1080 }, { 1920, 1080 }, { 0, 0, 1920, 1080 } },
+ { { 1928, 1092 }, { 1920, 1092 }, { 1920, 1092 }, { 4, 0, 1920, 1092 } },
+ /* Output resolution changes must not shrink the sensor window. */
+ { { 1928, 1092 }, { 1920, 1092 }, { 1920, 1080 }, { 4, 6, 1920, 1080 } },
+ { { 1928, 1092 }, { 1920, 1092 }, { 1280, 720 }, { 4, 6, 1920, 1080 } },
+ { { 1928, 1092 }, { 1920, 1092 }, { 640, 480 }, { 236, 0, 1456, 1092 } },
+ { { 1928, 1092 }, { 1920, 1092 }, { 512, 768 }, { 600, 0, 728, 1092 } },
+ { { 1928, 1092 }, { 1920, 1092 }, { 1280, 640 }, { 4, 66, 1920, 960 } },
+ /* A smaller Bayer border leaves an odd top coordinate. */
+ { { 1928, 1092 }, { 1924, 1092 }, { 1280, 720 }, { 2, 5, 1924, 1082 } },
+ };
+
+ for (const auto &entry : cases) {
+ Rectangle crop = debayerCrop(entry.input, entry.native, entry.output);
+ if (crop != entry.expected) {
+ std::cerr << "Crop " << entry.input << " -> " << entry.output
+ << " is " << crop << ", expected " << entry.expected << std::endl;
+ return TestFail;
+ }
+ }
+
+ return TestPass;
+ }
+};
+
+TEST_REGISTER(DebayerGeometryTest)
@@ -25,60 +25,61 @@ endif
subdir('libtest')
subdir('camera')
subdir('controls')
subdir('gstreamer')
subdir('ipa')
subdir('ipc')
subdir('log')
subdir('media_device')
subdir('process')
subdir('py')
subdir('serialization')
subdir('stream')
subdir('v4l2_compat')
subdir('v4l2_subdevice')
subdir('v4l2_videodevice')
public_tests = [
{'name': 'color-space', 'sources': ['color-space.cpp']},
{'name': 'geometry', 'sources': ['geometry.cpp']},
{'name': 'public-api', 'sources': ['public-api.cpp']},
{'name': 'signal', 'sources': ['signal.cpp']},
{'name': 'transform', 'sources': ['transform.cpp']},
]
internal_tests = [
{'name': 'bayer-format', 'sources': ['bayer-format.cpp']},
{'name': 'byte-stream-buffer', 'sources': ['byte-stream-buffer.cpp']},
{'name': 'camera-sensor', 'sources': ['camera-sensor.cpp']},
+ {'name': 'debayer-geometry', 'sources': ['debayer-geometry.cpp']},
{'name': 'delayed_controls', 'sources': ['delayed_controls.cpp']},
{'name': 'event', 'sources': ['event.cpp']},
{'name': 'event-dispatcher', 'sources': ['event-dispatcher.cpp']},
{'name': 'event-thread', 'sources': ['event-thread.cpp']},
{'name': 'file', 'sources': ['file.cpp']},
{'name': 'flags', 'sources': ['flags.cpp']},
{'name': 'hotplug-cameras', 'sources': ['hotplug-cameras.cpp']},
{'name': 'matrix', 'sources': ['matrix.cpp']},
{'name': 'message', 'sources': ['message.cpp']},
{'name': 'object', 'sources': ['object.cpp']},
{'name': 'object-delete', 'sources': ['object-delete.cpp']},
{'name': 'object-invoke', 'sources': ['object-invoke.cpp']},
{'name': 'pixel-format', 'sources': ['pixel-format.cpp']},
{'name': 'shared-fd', 'sources': ['shared-fd.cpp']},
{'name': 'signal-threads', 'sources': ['signal-threads.cpp']},
{'name': 'threads', 'sources': 'threads.cpp', 'dependencies': [libthreads]},
{'name': 'timer', 'sources': ['timer.cpp']},
{'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true},
{'name': 'timer-thread', 'sources': ['timer-thread.cpp']},
{'name': 'unique-fd', 'sources': ['unique-fd.cpp']},
{'name': 'utils', 'sources': ['utils.cpp']},
{'name': 'value-node', 'sources': ['value-node.cpp']},
{'name': 'vector', 'sources': ['vector.cpp']},
{'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']},
]
internal_non_parallel_tests = [
{'name': 'fence', 'sources': ['fence.cpp']},
{'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']},
]