| Message ID | 20261015194335.287960-3-halfline@gmail.com |
|---|---|
| State | Superseded |
| Headers | show |
| Series |
|
| Related | show |
On Thu, Oct 15, 2026 at 03:43:33PM -0400, Ray Strode wrote: > From: Ray Strode <rstrode@redhat.com> > > The GPU debayer path centers the sensor image when output dimensions or > aspect ratios change. > > The project has no test that inspects rendered pixels, so a projection > error can shift the visible image without failing an automated check. > > This commit renders centered synthetic stripes through the GPU path and > compares each output against a centered crop of the native image. It > covers horizontal and vertical positioning, four Bayer formats, and > four output sizes. Doesn't qualify as a unit test. > Signed-off-by: Ray Strode <rstrode@redhat.com> > --- > test/meson.build | 16 ++- > test/software-isp-gpu-crop.cpp | 203 +++++++++++++++++++++++++++++++++ > 2 files changed, 218 insertions(+), 1 deletion(-) > create mode 100644 test/software-isp-gpu-crop.cpp > > diff --git a/test/meson.build b/test/meson.build > index 363a2829..644a8cba 100644 > --- a/test/meson.build > +++ b/test/meson.build > @@ -51,81 +51,95 @@ public_tests = [ > 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': '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']}, > ] > > +if softisp_enabled and mesa_works > + softisp_gpu_includes = include_directories('../src/libcamera/software_isp') > + internal_tests += [ > + {'name': 'software-isp-gpu-crop', 'sources': ['software-isp-gpu-crop.cpp'], > + 'dependencies': [libegl, libglesv2], > + 'include_directories': softisp_gpu_includes}, > + ] > +endif > + > internal_non_parallel_tests = [ > {'name': 'fence', 'sources': ['fence.cpp']}, > {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, > ] > > foreach test : public_tests > deps = [libcamera_public] > if 'dependencies' in test > deps += test['dependencies'] > endif > > exe = executable(test['name'], test['sources'], > dependencies : deps, > implicit_include_directories : false, > link_with : test_libraries, > include_directories : test_includes_public) > > test(test['name'], exe, should_fail : test.get('should_fail', false)) > endforeach > > foreach test : internal_tests > deps = [libcamera_private] > if 'dependencies' in test > deps += test['dependencies'] > endif > > + includes = [test_includes_internal] > + if 'include_directories' in test > + includes += test['include_directories'] > + endif > + > exe = executable(test['name'], test['sources'], > dependencies : deps, > implicit_include_directories : false, > link_with : test_libraries, > - include_directories : test_includes_internal) > + include_directories : includes) > > test(test['name'], exe, should_fail : test.get('should_fail', false)) > endforeach > > foreach test : internal_non_parallel_tests > deps = [libcamera_private] > if 'dependencies' in test > deps += test['dependencies'] > endif > > exe = executable(test['name'], test['sources'], > dependencies : deps, > implicit_include_directories : false, > link_with : test_libraries, > include_directories : test_includes_internal) > > test(test['name'], exe, > is_parallel : false, > should_fail : test.get('should_fail', false)) > endforeach > diff --git a/test/software-isp-gpu-crop.cpp b/test/software-isp-gpu-crop.cpp > new file mode 100644 > index 00000000..8d91c0a4 > --- /dev/null > +++ b/test/software-isp-gpu-crop.cpp > @@ -0,0 +1,203 @@ > +/* SPDX-License-Identifier: GPL-2.0-or-later */ > + > +#include <algorithm> > +#include <cmath> > +#include <iostream> > +#include <memory> > +#include <stdint.h> > +#include <string> > +#include <tuple> > +#include <utility> > +#include <vector> > + > +#include <libcamera/base/utils.h> > + > +#include <libcamera/camera_manager.h> > +#include <libcamera/formats.h> > +#include <libcamera/framebuffer.h> > +#include <libcamera/geometry.h> > +#include <libcamera/stream.h> > + > +#include "libcamera/internal/dma_buf_allocator.h" > +#include "libcamera/internal/egl.h" > +#include "libcamera/internal/framebuffer.h" > +#include "libcamera/internal/mapped_framebuffer.h" > +#include "libcamera/internal/software_isp/debayer_params.h" > +#include "libcamera/internal/software_isp/swstats_cpu.h" > + > +#include "debayer_egl.h" > +#include "test.h" > + > +using namespace libcamera; > + > +class GpuCropTest : public Test > +{ > +protected: > + int run() override > + { > + EGLDisplay display = eGL::probeDisplay(); > + if (display == EGL_NO_DISPLAY) > + return TestSkip; > + utils::scope_exit terminate([&] { eglTerminate(display); }); > + > + const char *extensions = eglQueryString(display, EGL_EXTENSIONS); > + if (!extensions || std::string(extensions).find("EGL_EXT_image_dma_buf_import") == std::string::npos) > + return TestSkip; > + > + DmaBufAllocator allocator(DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap | > + DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap | > + DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf); > + if (!allocator.isValid()) > + return TestSkip; > + > + CameraManager manager; > + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, > + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { > + for (bool vertical : { true, false }) { > + std::vector<uint8_t> reference; > + Size nativeSize; > + for (Size size : { Size(1920, 1080), Size(1280, 720), > + Size(640, 480), Size(512, 768) }) { > + std::vector<uint8_t> image; > + if (render(manager, allocator, display, format, size, > + vertical, &image, &nativeSize)) > + return TestFail; > + > + if (!checkCenter(image, size, vertical)) { > + std::cerr << format << " " << size > + << " stripe is not centered" << std::endl; > + return TestFail; > + } > + > + if (reference.empty()) { > + if (size != nativeSize) > + return TestFail; > + reference = std::move(image); > + continue; > + } > + > + /* Compare against a centered crop of the native render. */ > + double scale = std::max(double(size.width) / nativeSize.width, > + double(size.height) / nativeSize.height); > + double error = 0; > + for (unsigned int y = 0; y < size.height; y++) { > + int ry = std::lround(nativeSize.height / 2.0 + > + (y + 0.5 - size.height / 2.0) / scale - 0.5); > + for (unsigned int x = 0; x < size.width; x++) { > + int rx = std::lround(nativeSize.width / 2.0 + > + (x + 0.5 - size.width / 2.0) / scale - 0.5); > + unsigned int actual = (y * size.width + x) * 4; > + unsigned int expected = (ry * nativeSize.width + rx) * 4; > + for (unsigned int c = 0; c < 3; c++) > + error += std::abs(int(image[actual + c]) - reference[expected + c]); > + } > + } > + error /= size.width * size.height * 3; > + std::cout << format << " " << size << " " > + << (vertical ? "vertical" : "horizontal") > + << " stripe: mean pixel error " << error << std::endl; > + /* Allow interpolation and rounding at the stripe edges. */ > + if (error > 2.0) > + return TestFail; > + } > + } > + } > + return TestPass; > + } > + > +private: > + bool checkCenter(const std::vector<uint8_t> &image, const Size &size, bool vertical) > + { > + unsigned int length = vertical ? size.width : size.height; > + double sum = 0; > + unsigned int count = 0; > + for (unsigned int i = 0; i < length; i++) { > + unsigned int x = vertical ? i : size.width / 2; > + unsigned int y = vertical ? size.height / 2 : i; > + /* The synthetic scene is achromatic, independent of RGB ordering. */ > + if (image[(y * size.width + x) * 4 + 1] > 128) { > + sum += i + 0.5; > + count++; > + } > + } > + if (!count) > + return false; > + double center = sum / count; > + std::cout << (vertical ? "X" : "Y") << " center " << center > + << ", expected " << length / 2.0 << std::endl; > + return std::abs(center - length / 2.0) <= 1.0; > + } > + > + int render(CameraManager &manager, DmaBufAllocator &allocator, EGLDisplay display, > + PixelFormat format, const Size &size, bool vertical, > + std::vector<uint8_t> *image, Size *nativeSize) > + { > + auto stats = std::make_unique<SwStatsCpu>(manager); > + if (!stats->isValid()) > + return TestFail; > + DebayerEGL debayer(std::move(stats), manager, display); > + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; > + StreamConfiguration input, output; > + input.pixelFormat = format; > + input.size = { packed ? 1928U : 1924U, 1080 }; > + input.stride = debayer.preferredInputStride(format, input.size); > + input.bufferCount = 1; > + output.pixelFormat = formats::XBGR8888; > + output.size = size; > + unsigned int frameSize; > + std::tie(output.stride, frameSize) = debayer.strideAndFrameSize(output.pixelFormat, size); > + output.bufferCount = 1; > + *nativeSize = debayer.sizes(format, input.size).max; > + if (debayer.configure(input, { output }, false)) > + return TestFail; > + > + std::vector<std::unique_ptr<FrameBuffer>> inputs, outputs; > + if (allocator.exportBuffers(1, { input.stride * input.size.height }, &inputs) != 1 || > + allocator.exportBuffers(1, { frameSize }, &outputs) != 1) > + return TestFail; > + MappedFrameBuffer raw(inputs[0].get(), MappedFrameBuffer::MapFlag::Write); > + MappedFrameBuffer rgb(outputs[0].get(), MappedFrameBuffer::MapFlag::Read); > + if (!raw.isValid() || !rgb.isValid()) > + return TestFail; > + FrameMetadata &metadata = inputs[0]->_d()->metadata(); > + metadata.status = FrameMetadata::FrameSuccess; > + metadata.sequence = 0; > + metadata.timestamp = 0; > + > + { > + DmaSyncer sync(inputs[0]->planes()[0].fd, DmaSyncer::SyncType::Write); > + std::fill(raw.planes()[0].begin(), raw.planes()[0].end(), 0); > + /* A bright stripe centered on the sensor, with dark surroundings. */ > + for (unsigned int y = 0; y < input.size.height; y++) { > + for (unsigned int x = 0; x < input.size.width; x++) { > + unsigned int coordinate = vertical ? x : y; > + unsigned int middle = (vertical ? input.size.width : input.size.height) / 2; > + bool bright = coordinate >= middle - 64 && coordinate < middle + 64; > + uint8_t value = bright ? 224 : 32; > + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; > + /* RAW10 stores the eight high bits before each group's low bits. */ > + raw.planes()[0][y * input.stride + offset] = value; > + } > + } > + } > + > + /* stop() must also release a partially initialized context on failure. */ > + utils::scope_exit stop([&] { debayer.stop(); }); > + if (debayer.start()) > + return TestFail; > + bool ready = false; > + debayer.outputBufferReady.connect(this, [&](FrameBuffer *) { ready = true; }); > + debayer.process(0, inputs[0].get(), outputs[0].get(), DebayerParams{}); > + if (!ready || outputs[0]->metadata().status != FrameMetadata::FrameSuccess) > + return TestFail; > + > + DmaSyncer sync(outputs[0]->planes()[0].fd, DmaSyncer::SyncType::Read); > + image->resize(size.width * size.height * 4); > + for (unsigned int y = 0; y < size.height; y++) > + std::copy_n(rgb.planes()[0].data() + y * output.stride, size.width * 4, > + image->data() + y * size.width * 4); > + return TestPass; > + } > +}; > + > +TEST_REGISTER(GpuCropTest)
Hi, On 15-Oct-26 22:16, Laurent Pinchart wrote: > On Thu, Oct 15, 2026 at 03:43:33PM -0400, Ray Strode wrote: >> From: Ray Strode <rstrode@redhat.com> >> >> The GPU debayer path centers the sensor image when output dimensions or >> aspect ratios change. >> >> The project has no test that inspects rendered pixels, so a projection >> error can shift the visible image without failing an automated check. >> >> This commit renders centered synthetic stripes through the GPU path and >> compares each output against a centered crop of the native image. It >> covers horizontal and vertical positioning, four Bayer formats, and >> four output sizes. > > Doesn't qualify as a unit test. This still seems like a nice test to have though ? Can we maybe add some more end2end(ish) tests somewhere ? Even if users need to manually opt-in / specify some meson flags to get the extra tests this seems like something good to have ? Especially since we should be able to do this in CI, even if it with using / forcing LLVMpipe in MESA? Regards, Hans > >> Signed-off-by: Ray Strode <rstrode@redhat.com> >> --- >> test/meson.build | 16 ++- >> test/software-isp-gpu-crop.cpp | 203 +++++++++++++++++++++++++++++++++ >> 2 files changed, 218 insertions(+), 1 deletion(-) >> create mode 100644 test/software-isp-gpu-crop.cpp >> >> diff --git a/test/meson.build b/test/meson.build >> index 363a2829..644a8cba 100644 >> --- a/test/meson.build >> +++ b/test/meson.build >> @@ -51,81 +51,95 @@ public_tests = [ >> 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': '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']}, >> ] >> >> +if softisp_enabled and mesa_works >> + softisp_gpu_includes = include_directories('../src/libcamera/software_isp') >> + internal_tests += [ >> + {'name': 'software-isp-gpu-crop', 'sources': ['software-isp-gpu-crop.cpp'], >> + 'dependencies': [libegl, libglesv2], >> + 'include_directories': softisp_gpu_includes}, >> + ] >> +endif >> + >> internal_non_parallel_tests = [ >> {'name': 'fence', 'sources': ['fence.cpp']}, >> {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, >> ] >> >> foreach test : public_tests >> deps = [libcamera_public] >> if 'dependencies' in test >> deps += test['dependencies'] >> endif >> >> exe = executable(test['name'], test['sources'], >> dependencies : deps, >> implicit_include_directories : false, >> link_with : test_libraries, >> include_directories : test_includes_public) >> >> test(test['name'], exe, should_fail : test.get('should_fail', false)) >> endforeach >> >> foreach test : internal_tests >> deps = [libcamera_private] >> if 'dependencies' in test >> deps += test['dependencies'] >> endif >> >> + includes = [test_includes_internal] >> + if 'include_directories' in test >> + includes += test['include_directories'] >> + endif >> + >> exe = executable(test['name'], test['sources'], >> dependencies : deps, >> implicit_include_directories : false, >> link_with : test_libraries, >> - include_directories : test_includes_internal) >> + include_directories : includes) >> >> test(test['name'], exe, should_fail : test.get('should_fail', false)) >> endforeach >> >> foreach test : internal_non_parallel_tests >> deps = [libcamera_private] >> if 'dependencies' in test >> deps += test['dependencies'] >> endif >> >> exe = executable(test['name'], test['sources'], >> dependencies : deps, >> implicit_include_directories : false, >> link_with : test_libraries, >> include_directories : test_includes_internal) >> >> test(test['name'], exe, >> is_parallel : false, >> should_fail : test.get('should_fail', false)) >> endforeach >> diff --git a/test/software-isp-gpu-crop.cpp b/test/software-isp-gpu-crop.cpp >> new file mode 100644 >> index 00000000..8d91c0a4 >> --- /dev/null >> +++ b/test/software-isp-gpu-crop.cpp >> @@ -0,0 +1,203 @@ >> +/* SPDX-License-Identifier: GPL-2.0-or-later */ >> + >> +#include <algorithm> >> +#include <cmath> >> +#include <iostream> >> +#include <memory> >> +#include <stdint.h> >> +#include <string> >> +#include <tuple> >> +#include <utility> >> +#include <vector> >> + >> +#include <libcamera/base/utils.h> >> + >> +#include <libcamera/camera_manager.h> >> +#include <libcamera/formats.h> >> +#include <libcamera/framebuffer.h> >> +#include <libcamera/geometry.h> >> +#include <libcamera/stream.h> >> + >> +#include "libcamera/internal/dma_buf_allocator.h" >> +#include "libcamera/internal/egl.h" >> +#include "libcamera/internal/framebuffer.h" >> +#include "libcamera/internal/mapped_framebuffer.h" >> +#include "libcamera/internal/software_isp/debayer_params.h" >> +#include "libcamera/internal/software_isp/swstats_cpu.h" >> + >> +#include "debayer_egl.h" >> +#include "test.h" >> + >> +using namespace libcamera; >> + >> +class GpuCropTest : public Test >> +{ >> +protected: >> + int run() override >> + { >> + EGLDisplay display = eGL::probeDisplay(); >> + if (display == EGL_NO_DISPLAY) >> + return TestSkip; >> + utils::scope_exit terminate([&] { eglTerminate(display); }); >> + >> + const char *extensions = eglQueryString(display, EGL_EXTENSIONS); >> + if (!extensions || std::string(extensions).find("EGL_EXT_image_dma_buf_import") == std::string::npos) >> + return TestSkip; >> + >> + DmaBufAllocator allocator(DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap | >> + DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap | >> + DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf); >> + if (!allocator.isValid()) >> + return TestSkip; >> + >> + CameraManager manager; >> + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, >> + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { >> + for (bool vertical : { true, false }) { >> + std::vector<uint8_t> reference; >> + Size nativeSize; >> + for (Size size : { Size(1920, 1080), Size(1280, 720), >> + Size(640, 480), Size(512, 768) }) { >> + std::vector<uint8_t> image; >> + if (render(manager, allocator, display, format, size, >> + vertical, &image, &nativeSize)) >> + return TestFail; >> + >> + if (!checkCenter(image, size, vertical)) { >> + std::cerr << format << " " << size >> + << " stripe is not centered" << std::endl; >> + return TestFail; >> + } >> + >> + if (reference.empty()) { >> + if (size != nativeSize) >> + return TestFail; >> + reference = std::move(image); >> + continue; >> + } >> + >> + /* Compare against a centered crop of the native render. */ >> + double scale = std::max(double(size.width) / nativeSize.width, >> + double(size.height) / nativeSize.height); >> + double error = 0; >> + for (unsigned int y = 0; y < size.height; y++) { >> + int ry = std::lround(nativeSize.height / 2.0 + >> + (y + 0.5 - size.height / 2.0) / scale - 0.5); >> + for (unsigned int x = 0; x < size.width; x++) { >> + int rx = std::lround(nativeSize.width / 2.0 + >> + (x + 0.5 - size.width / 2.0) / scale - 0.5); >> + unsigned int actual = (y * size.width + x) * 4; >> + unsigned int expected = (ry * nativeSize.width + rx) * 4; >> + for (unsigned int c = 0; c < 3; c++) >> + error += std::abs(int(image[actual + c]) - reference[expected + c]); >> + } >> + } >> + error /= size.width * size.height * 3; >> + std::cout << format << " " << size << " " >> + << (vertical ? "vertical" : "horizontal") >> + << " stripe: mean pixel error " << error << std::endl; >> + /* Allow interpolation and rounding at the stripe edges. */ >> + if (error > 2.0) >> + return TestFail; >> + } >> + } >> + } >> + return TestPass; >> + } >> + >> +private: >> + bool checkCenter(const std::vector<uint8_t> &image, const Size &size, bool vertical) >> + { >> + unsigned int length = vertical ? size.width : size.height; >> + double sum = 0; >> + unsigned int count = 0; >> + for (unsigned int i = 0; i < length; i++) { >> + unsigned int x = vertical ? i : size.width / 2; >> + unsigned int y = vertical ? size.height / 2 : i; >> + /* The synthetic scene is achromatic, independent of RGB ordering. */ >> + if (image[(y * size.width + x) * 4 + 1] > 128) { >> + sum += i + 0.5; >> + count++; >> + } >> + } >> + if (!count) >> + return false; >> + double center = sum / count; >> + std::cout << (vertical ? "X" : "Y") << " center " << center >> + << ", expected " << length / 2.0 << std::endl; >> + return std::abs(center - length / 2.0) <= 1.0; >> + } >> + >> + int render(CameraManager &manager, DmaBufAllocator &allocator, EGLDisplay display, >> + PixelFormat format, const Size &size, bool vertical, >> + std::vector<uint8_t> *image, Size *nativeSize) >> + { >> + auto stats = std::make_unique<SwStatsCpu>(manager); >> + if (!stats->isValid()) >> + return TestFail; >> + DebayerEGL debayer(std::move(stats), manager, display); >> + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; >> + StreamConfiguration input, output; >> + input.pixelFormat = format; >> + input.size = { packed ? 1928U : 1924U, 1080 }; >> + input.stride = debayer.preferredInputStride(format, input.size); >> + input.bufferCount = 1; >> + output.pixelFormat = formats::XBGR8888; >> + output.size = size; >> + unsigned int frameSize; >> + std::tie(output.stride, frameSize) = debayer.strideAndFrameSize(output.pixelFormat, size); >> + output.bufferCount = 1; >> + *nativeSize = debayer.sizes(format, input.size).max; >> + if (debayer.configure(input, { output }, false)) >> + return TestFail; >> + >> + std::vector<std::unique_ptr<FrameBuffer>> inputs, outputs; >> + if (allocator.exportBuffers(1, { input.stride * input.size.height }, &inputs) != 1 || >> + allocator.exportBuffers(1, { frameSize }, &outputs) != 1) >> + return TestFail; >> + MappedFrameBuffer raw(inputs[0].get(), MappedFrameBuffer::MapFlag::Write); >> + MappedFrameBuffer rgb(outputs[0].get(), MappedFrameBuffer::MapFlag::Read); >> + if (!raw.isValid() || !rgb.isValid()) >> + return TestFail; >> + FrameMetadata &metadata = inputs[0]->_d()->metadata(); >> + metadata.status = FrameMetadata::FrameSuccess; >> + metadata.sequence = 0; >> + metadata.timestamp = 0; >> + >> + { >> + DmaSyncer sync(inputs[0]->planes()[0].fd, DmaSyncer::SyncType::Write); >> + std::fill(raw.planes()[0].begin(), raw.planes()[0].end(), 0); >> + /* A bright stripe centered on the sensor, with dark surroundings. */ >> + for (unsigned int y = 0; y < input.size.height; y++) { >> + for (unsigned int x = 0; x < input.size.width; x++) { >> + unsigned int coordinate = vertical ? x : y; >> + unsigned int middle = (vertical ? input.size.width : input.size.height) / 2; >> + bool bright = coordinate >= middle - 64 && coordinate < middle + 64; >> + uint8_t value = bright ? 224 : 32; >> + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; >> + /* RAW10 stores the eight high bits before each group's low bits. */ >> + raw.planes()[0][y * input.stride + offset] = value; >> + } >> + } >> + } >> + >> + /* stop() must also release a partially initialized context on failure. */ >> + utils::scope_exit stop([&] { debayer.stop(); }); >> + if (debayer.start()) >> + return TestFail; >> + bool ready = false; >> + debayer.outputBufferReady.connect(this, [&](FrameBuffer *) { ready = true; }); >> + debayer.process(0, inputs[0].get(), outputs[0].get(), DebayerParams{}); >> + if (!ready || outputs[0]->metadata().status != FrameMetadata::FrameSuccess) >> + return TestFail; >> + >> + DmaSyncer sync(outputs[0]->planes()[0].fd, DmaSyncer::SyncType::Read); >> + image->resize(size.width * size.height * 4); >> + for (unsigned int y = 0; y < size.height; y++) >> + std::copy_n(rgb.planes()[0].data() + y * output.stride, size.width * 4, >> + image->data() + y * size.width * 4); >> + return TestPass; >> + } >> +}; >> + >> +TEST_REGISTER(GpuCropTest) >
On Fri, Oct 16, 2026 at 04:39:02PM +0200, johannes.goede@oss.qualcomm.com wrote: > On 15-Oct-26 22:16, Laurent Pinchart wrote: > > On Thu, Oct 15, 2026 at 03:43:33PM -0400, Ray Strode wrote: > >> From: Ray Strode <rstrode@redhat.com> > >> > >> The GPU debayer path centers the sensor image when output dimensions or > >> aspect ratios change. > >> > >> The project has no test that inspects rendered pixels, so a projection > >> error can shift the visible image without failing an automated check. > >> > >> This commit renders centered synthetic stripes through the GPU path and > >> compares each output against a centered crop of the native image. It > >> covers horizontal and vertical positioning, four Bayer formats, and > >> four output sizes. > > > > Doesn't qualify as a unit test. > > This still seems like a nice test to have though ? > > Can we maybe add some more end2end(ish) tests somewhere ? We certainly need end-to-end tests. We have lc-compliance, but that's not a great match for this either. We need to start working on an imaging test suite, the same way Android splits camera tests into CTS (equivalent of lc-compliance) and ITS. > Even if users need to manually opt-in / specify some meson > flags to get the extra tests this seems like something good > to have ? I don't want to open to door to AI slop in unit tests. > Especially since we should be able to do this in CI, even if > it with using / forcing LLVMpipe in MESA? > > >> Signed-off-by: Ray Strode <rstrode@redhat.com> > >> --- > >> test/meson.build | 16 ++- > >> test/software-isp-gpu-crop.cpp | 203 +++++++++++++++++++++++++++++++++ > >> 2 files changed, 218 insertions(+), 1 deletion(-) > >> create mode 100644 test/software-isp-gpu-crop.cpp > >> > >> diff --git a/test/meson.build b/test/meson.build > >> index 363a2829..644a8cba 100644 > >> --- a/test/meson.build > >> +++ b/test/meson.build > >> @@ -51,81 +51,95 @@ public_tests = [ > >> 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': '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']}, > >> ] > >> > >> +if softisp_enabled and mesa_works > >> + softisp_gpu_includes = include_directories('../src/libcamera/software_isp') > >> + internal_tests += [ > >> + {'name': 'software-isp-gpu-crop', 'sources': ['software-isp-gpu-crop.cpp'], > >> + 'dependencies': [libegl, libglesv2], > >> + 'include_directories': softisp_gpu_includes}, > >> + ] > >> +endif > >> + > >> internal_non_parallel_tests = [ > >> {'name': 'fence', 'sources': ['fence.cpp']}, > >> {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, > >> ] > >> > >> foreach test : public_tests > >> deps = [libcamera_public] > >> if 'dependencies' in test > >> deps += test['dependencies'] > >> endif > >> > >> exe = executable(test['name'], test['sources'], > >> dependencies : deps, > >> implicit_include_directories : false, > >> link_with : test_libraries, > >> include_directories : test_includes_public) > >> > >> test(test['name'], exe, should_fail : test.get('should_fail', false)) > >> endforeach > >> > >> foreach test : internal_tests > >> deps = [libcamera_private] > >> if 'dependencies' in test > >> deps += test['dependencies'] > >> endif > >> > >> + includes = [test_includes_internal] > >> + if 'include_directories' in test > >> + includes += test['include_directories'] > >> + endif > >> + > >> exe = executable(test['name'], test['sources'], > >> dependencies : deps, > >> implicit_include_directories : false, > >> link_with : test_libraries, > >> - include_directories : test_includes_internal) > >> + include_directories : includes) > >> > >> test(test['name'], exe, should_fail : test.get('should_fail', false)) > >> endforeach > >> > >> foreach test : internal_non_parallel_tests > >> deps = [libcamera_private] > >> if 'dependencies' in test > >> deps += test['dependencies'] > >> endif > >> > >> exe = executable(test['name'], test['sources'], > >> dependencies : deps, > >> implicit_include_directories : false, > >> link_with : test_libraries, > >> include_directories : test_includes_internal) > >> > >> test(test['name'], exe, > >> is_parallel : false, > >> should_fail : test.get('should_fail', false)) > >> endforeach > >> diff --git a/test/software-isp-gpu-crop.cpp b/test/software-isp-gpu-crop.cpp > >> new file mode 100644 > >> index 00000000..8d91c0a4 > >> --- /dev/null > >> +++ b/test/software-isp-gpu-crop.cpp > >> @@ -0,0 +1,203 @@ > >> +/* SPDX-License-Identifier: GPL-2.0-or-later */ > >> + > >> +#include <algorithm> > >> +#include <cmath> > >> +#include <iostream> > >> +#include <memory> > >> +#include <stdint.h> > >> +#include <string> > >> +#include <tuple> > >> +#include <utility> > >> +#include <vector> > >> + > >> +#include <libcamera/base/utils.h> > >> + > >> +#include <libcamera/camera_manager.h> > >> +#include <libcamera/formats.h> > >> +#include <libcamera/framebuffer.h> > >> +#include <libcamera/geometry.h> > >> +#include <libcamera/stream.h> > >> + > >> +#include "libcamera/internal/dma_buf_allocator.h" > >> +#include "libcamera/internal/egl.h" > >> +#include "libcamera/internal/framebuffer.h" > >> +#include "libcamera/internal/mapped_framebuffer.h" > >> +#include "libcamera/internal/software_isp/debayer_params.h" > >> +#include "libcamera/internal/software_isp/swstats_cpu.h" > >> + > >> +#include "debayer_egl.h" > >> +#include "test.h" > >> + > >> +using namespace libcamera; > >> + > >> +class GpuCropTest : public Test > >> +{ > >> +protected: > >> + int run() override > >> + { > >> + EGLDisplay display = eGL::probeDisplay(); > >> + if (display == EGL_NO_DISPLAY) > >> + return TestSkip; > >> + utils::scope_exit terminate([&] { eglTerminate(display); }); > >> + > >> + const char *extensions = eglQueryString(display, EGL_EXTENSIONS); > >> + if (!extensions || std::string(extensions).find("EGL_EXT_image_dma_buf_import") == std::string::npos) > >> + return TestSkip; > >> + > >> + DmaBufAllocator allocator(DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap | > >> + DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap | > >> + DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf); > >> + if (!allocator.isValid()) > >> + return TestSkip; > >> + > >> + CameraManager manager; > >> + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, > >> + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { > >> + for (bool vertical : { true, false }) { > >> + std::vector<uint8_t> reference; > >> + Size nativeSize; > >> + for (Size size : { Size(1920, 1080), Size(1280, 720), > >> + Size(640, 480), Size(512, 768) }) { > >> + std::vector<uint8_t> image; > >> + if (render(manager, allocator, display, format, size, > >> + vertical, &image, &nativeSize)) > >> + return TestFail; > >> + > >> + if (!checkCenter(image, size, vertical)) { > >> + std::cerr << format << " " << size > >> + << " stripe is not centered" << std::endl; > >> + return TestFail; > >> + } > >> + > >> + if (reference.empty()) { > >> + if (size != nativeSize) > >> + return TestFail; > >> + reference = std::move(image); > >> + continue; > >> + } > >> + > >> + /* Compare against a centered crop of the native render. */ > >> + double scale = std::max(double(size.width) / nativeSize.width, > >> + double(size.height) / nativeSize.height); > >> + double error = 0; > >> + for (unsigned int y = 0; y < size.height; y++) { > >> + int ry = std::lround(nativeSize.height / 2.0 + > >> + (y + 0.5 - size.height / 2.0) / scale - 0.5); > >> + for (unsigned int x = 0; x < size.width; x++) { > >> + int rx = std::lround(nativeSize.width / 2.0 + > >> + (x + 0.5 - size.width / 2.0) / scale - 0.5); > >> + unsigned int actual = (y * size.width + x) * 4; > >> + unsigned int expected = (ry * nativeSize.width + rx) * 4; > >> + for (unsigned int c = 0; c < 3; c++) > >> + error += std::abs(int(image[actual + c]) - reference[expected + c]); > >> + } > >> + } > >> + error /= size.width * size.height * 3; > >> + std::cout << format << " " << size << " " > >> + << (vertical ? "vertical" : "horizontal") > >> + << " stripe: mean pixel error " << error << std::endl; > >> + /* Allow interpolation and rounding at the stripe edges. */ > >> + if (error > 2.0) > >> + return TestFail; > >> + } > >> + } > >> + } > >> + return TestPass; > >> + } > >> + > >> +private: > >> + bool checkCenter(const std::vector<uint8_t> &image, const Size &size, bool vertical) > >> + { > >> + unsigned int length = vertical ? size.width : size.height; > >> + double sum = 0; > >> + unsigned int count = 0; > >> + for (unsigned int i = 0; i < length; i++) { > >> + unsigned int x = vertical ? i : size.width / 2; > >> + unsigned int y = vertical ? size.height / 2 : i; > >> + /* The synthetic scene is achromatic, independent of RGB ordering. */ > >> + if (image[(y * size.width + x) * 4 + 1] > 128) { > >> + sum += i + 0.5; > >> + count++; > >> + } > >> + } > >> + if (!count) > >> + return false; > >> + double center = sum / count; > >> + std::cout << (vertical ? "X" : "Y") << " center " << center > >> + << ", expected " << length / 2.0 << std::endl; > >> + return std::abs(center - length / 2.0) <= 1.0; > >> + } > >> + > >> + int render(CameraManager &manager, DmaBufAllocator &allocator, EGLDisplay display, > >> + PixelFormat format, const Size &size, bool vertical, > >> + std::vector<uint8_t> *image, Size *nativeSize) > >> + { > >> + auto stats = std::make_unique<SwStatsCpu>(manager); > >> + if (!stats->isValid()) > >> + return TestFail; > >> + DebayerEGL debayer(std::move(stats), manager, display); > >> + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; > >> + StreamConfiguration input, output; > >> + input.pixelFormat = format; > >> + input.size = { packed ? 1928U : 1924U, 1080 }; > >> + input.stride = debayer.preferredInputStride(format, input.size); > >> + input.bufferCount = 1; > >> + output.pixelFormat = formats::XBGR8888; > >> + output.size = size; > >> + unsigned int frameSize; > >> + std::tie(output.stride, frameSize) = debayer.strideAndFrameSize(output.pixelFormat, size); > >> + output.bufferCount = 1; > >> + *nativeSize = debayer.sizes(format, input.size).max; > >> + if (debayer.configure(input, { output }, false)) > >> + return TestFail; > >> + > >> + std::vector<std::unique_ptr<FrameBuffer>> inputs, outputs; > >> + if (allocator.exportBuffers(1, { input.stride * input.size.height }, &inputs) != 1 || > >> + allocator.exportBuffers(1, { frameSize }, &outputs) != 1) > >> + return TestFail; > >> + MappedFrameBuffer raw(inputs[0].get(), MappedFrameBuffer::MapFlag::Write); > >> + MappedFrameBuffer rgb(outputs[0].get(), MappedFrameBuffer::MapFlag::Read); > >> + if (!raw.isValid() || !rgb.isValid()) > >> + return TestFail; > >> + FrameMetadata &metadata = inputs[0]->_d()->metadata(); > >> + metadata.status = FrameMetadata::FrameSuccess; > >> + metadata.sequence = 0; > >> + metadata.timestamp = 0; > >> + > >> + { > >> + DmaSyncer sync(inputs[0]->planes()[0].fd, DmaSyncer::SyncType::Write); > >> + std::fill(raw.planes()[0].begin(), raw.planes()[0].end(), 0); > >> + /* A bright stripe centered on the sensor, with dark surroundings. */ > >> + for (unsigned int y = 0; y < input.size.height; y++) { > >> + for (unsigned int x = 0; x < input.size.width; x++) { > >> + unsigned int coordinate = vertical ? x : y; > >> + unsigned int middle = (vertical ? input.size.width : input.size.height) / 2; > >> + bool bright = coordinate >= middle - 64 && coordinate < middle + 64; > >> + uint8_t value = bright ? 224 : 32; > >> + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; > >> + /* RAW10 stores the eight high bits before each group's low bits. */ > >> + raw.planes()[0][y * input.stride + offset] = value; > >> + } > >> + } > >> + } > >> + > >> + /* stop() must also release a partially initialized context on failure. */ > >> + utils::scope_exit stop([&] { debayer.stop(); }); > >> + if (debayer.start()) > >> + return TestFail; > >> + bool ready = false; > >> + debayer.outputBufferReady.connect(this, [&](FrameBuffer *) { ready = true; }); > >> + debayer.process(0, inputs[0].get(), outputs[0].get(), DebayerParams{}); > >> + if (!ready || outputs[0]->metadata().status != FrameMetadata::FrameSuccess) > >> + return TestFail; > >> + > >> + DmaSyncer sync(outputs[0]->planes()[0].fd, DmaSyncer::SyncType::Read); > >> + image->resize(size.width * size.height * 4); > >> + for (unsigned int y = 0; y < size.height; y++) > >> + std::copy_n(rgb.planes()[0].data() + y * output.stride, size.width * 4, > >> + image->data() + y * size.width * 4); > >> + return TestPass; > >> + } > >> +}; > >> + > >> +TEST_REGISTER(GpuCropTest)
diff --git a/test/meson.build b/test/meson.build index 363a2829..644a8cba 100644 --- a/test/meson.build +++ b/test/meson.build @@ -51,81 +51,95 @@ public_tests = [ 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': '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']}, ] +if softisp_enabled and mesa_works + softisp_gpu_includes = include_directories('../src/libcamera/software_isp') + internal_tests += [ + {'name': 'software-isp-gpu-crop', 'sources': ['software-isp-gpu-crop.cpp'], + 'dependencies': [libegl, libglesv2], + 'include_directories': softisp_gpu_includes}, + ] +endif + internal_non_parallel_tests = [ {'name': 'fence', 'sources': ['fence.cpp']}, {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, ] foreach test : public_tests deps = [libcamera_public] if 'dependencies' in test deps += test['dependencies'] endif exe = executable(test['name'], test['sources'], dependencies : deps, implicit_include_directories : false, link_with : test_libraries, include_directories : test_includes_public) test(test['name'], exe, should_fail : test.get('should_fail', false)) endforeach foreach test : internal_tests deps = [libcamera_private] if 'dependencies' in test deps += test['dependencies'] endif + includes = [test_includes_internal] + if 'include_directories' in test + includes += test['include_directories'] + endif + exe = executable(test['name'], test['sources'], dependencies : deps, implicit_include_directories : false, link_with : test_libraries, - include_directories : test_includes_internal) + include_directories : includes) test(test['name'], exe, should_fail : test.get('should_fail', false)) endforeach foreach test : internal_non_parallel_tests deps = [libcamera_private] if 'dependencies' in test deps += test['dependencies'] endif exe = executable(test['name'], test['sources'], dependencies : deps, implicit_include_directories : false, link_with : test_libraries, include_directories : test_includes_internal) test(test['name'], exe, is_parallel : false, should_fail : test.get('should_fail', false)) endforeach diff --git a/test/software-isp-gpu-crop.cpp b/test/software-isp-gpu-crop.cpp new file mode 100644 index 00000000..8d91c0a4 --- /dev/null +++ b/test/software-isp-gpu-crop.cpp @@ -0,0 +1,203 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <algorithm> +#include <cmath> +#include <iostream> +#include <memory> +#include <stdint.h> +#include <string> +#include <tuple> +#include <utility> +#include <vector> + +#include <libcamera/base/utils.h> + +#include <libcamera/camera_manager.h> +#include <libcamera/formats.h> +#include <libcamera/framebuffer.h> +#include <libcamera/geometry.h> +#include <libcamera/stream.h> + +#include "libcamera/internal/dma_buf_allocator.h" +#include "libcamera/internal/egl.h" +#include "libcamera/internal/framebuffer.h" +#include "libcamera/internal/mapped_framebuffer.h" +#include "libcamera/internal/software_isp/debayer_params.h" +#include "libcamera/internal/software_isp/swstats_cpu.h" + +#include "debayer_egl.h" +#include "test.h" + +using namespace libcamera; + +class GpuCropTest : public Test +{ +protected: + int run() override + { + EGLDisplay display = eGL::probeDisplay(); + if (display == EGL_NO_DISPLAY) + return TestSkip; + utils::scope_exit terminate([&] { eglTerminate(display); }); + + const char *extensions = eglQueryString(display, EGL_EXTENSIONS); + if (!extensions || std::string(extensions).find("EGL_EXT_image_dma_buf_import") == std::string::npos) + return TestSkip; + + DmaBufAllocator allocator(DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap | + DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap | + DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf); + if (!allocator.isValid()) + return TestSkip; + + CameraManager manager; + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { + for (bool vertical : { true, false }) { + std::vector<uint8_t> reference; + Size nativeSize; + for (Size size : { Size(1920, 1080), Size(1280, 720), + Size(640, 480), Size(512, 768) }) { + std::vector<uint8_t> image; + if (render(manager, allocator, display, format, size, + vertical, &image, &nativeSize)) + return TestFail; + + if (!checkCenter(image, size, vertical)) { + std::cerr << format << " " << size + << " stripe is not centered" << std::endl; + return TestFail; + } + + if (reference.empty()) { + if (size != nativeSize) + return TestFail; + reference = std::move(image); + continue; + } + + /* Compare against a centered crop of the native render. */ + double scale = std::max(double(size.width) / nativeSize.width, + double(size.height) / nativeSize.height); + double error = 0; + for (unsigned int y = 0; y < size.height; y++) { + int ry = std::lround(nativeSize.height / 2.0 + + (y + 0.5 - size.height / 2.0) / scale - 0.5); + for (unsigned int x = 0; x < size.width; x++) { + int rx = std::lround(nativeSize.width / 2.0 + + (x + 0.5 - size.width / 2.0) / scale - 0.5); + unsigned int actual = (y * size.width + x) * 4; + unsigned int expected = (ry * nativeSize.width + rx) * 4; + for (unsigned int c = 0; c < 3; c++) + error += std::abs(int(image[actual + c]) - reference[expected + c]); + } + } + error /= size.width * size.height * 3; + std::cout << format << " " << size << " " + << (vertical ? "vertical" : "horizontal") + << " stripe: mean pixel error " << error << std::endl; + /* Allow interpolation and rounding at the stripe edges. */ + if (error > 2.0) + return TestFail; + } + } + } + return TestPass; + } + +private: + bool checkCenter(const std::vector<uint8_t> &image, const Size &size, bool vertical) + { + unsigned int length = vertical ? size.width : size.height; + double sum = 0; + unsigned int count = 0; + for (unsigned int i = 0; i < length; i++) { + unsigned int x = vertical ? i : size.width / 2; + unsigned int y = vertical ? size.height / 2 : i; + /* The synthetic scene is achromatic, independent of RGB ordering. */ + if (image[(y * size.width + x) * 4 + 1] > 128) { + sum += i + 0.5; + count++; + } + } + if (!count) + return false; + double center = sum / count; + std::cout << (vertical ? "X" : "Y") << " center " << center + << ", expected " << length / 2.0 << std::endl; + return std::abs(center - length / 2.0) <= 1.0; + } + + int render(CameraManager &manager, DmaBufAllocator &allocator, EGLDisplay display, + PixelFormat format, const Size &size, bool vertical, + std::vector<uint8_t> *image, Size *nativeSize) + { + auto stats = std::make_unique<SwStatsCpu>(manager); + if (!stats->isValid()) + return TestFail; + DebayerEGL debayer(std::move(stats), manager, display); + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; + StreamConfiguration input, output; + input.pixelFormat = format; + input.size = { packed ? 1928U : 1924U, 1080 }; + input.stride = debayer.preferredInputStride(format, input.size); + input.bufferCount = 1; + output.pixelFormat = formats::XBGR8888; + output.size = size; + unsigned int frameSize; + std::tie(output.stride, frameSize) = debayer.strideAndFrameSize(output.pixelFormat, size); + output.bufferCount = 1; + *nativeSize = debayer.sizes(format, input.size).max; + if (debayer.configure(input, { output }, false)) + return TestFail; + + std::vector<std::unique_ptr<FrameBuffer>> inputs, outputs; + if (allocator.exportBuffers(1, { input.stride * input.size.height }, &inputs) != 1 || + allocator.exportBuffers(1, { frameSize }, &outputs) != 1) + return TestFail; + MappedFrameBuffer raw(inputs[0].get(), MappedFrameBuffer::MapFlag::Write); + MappedFrameBuffer rgb(outputs[0].get(), MappedFrameBuffer::MapFlag::Read); + if (!raw.isValid() || !rgb.isValid()) + return TestFail; + FrameMetadata &metadata = inputs[0]->_d()->metadata(); + metadata.status = FrameMetadata::FrameSuccess; + metadata.sequence = 0; + metadata.timestamp = 0; + + { + DmaSyncer sync(inputs[0]->planes()[0].fd, DmaSyncer::SyncType::Write); + std::fill(raw.planes()[0].begin(), raw.planes()[0].end(), 0); + /* A bright stripe centered on the sensor, with dark surroundings. */ + for (unsigned int y = 0; y < input.size.height; y++) { + for (unsigned int x = 0; x < input.size.width; x++) { + unsigned int coordinate = vertical ? x : y; + unsigned int middle = (vertical ? input.size.width : input.size.height) / 2; + bool bright = coordinate >= middle - 64 && coordinate < middle + 64; + uint8_t value = bright ? 224 : 32; + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; + /* RAW10 stores the eight high bits before each group's low bits. */ + raw.planes()[0][y * input.stride + offset] = value; + } + } + } + + /* stop() must also release a partially initialized context on failure. */ + utils::scope_exit stop([&] { debayer.stop(); }); + if (debayer.start()) + return TestFail; + bool ready = false; + debayer.outputBufferReady.connect(this, [&](FrameBuffer *) { ready = true; }); + debayer.process(0, inputs[0].get(), outputs[0].get(), DebayerParams{}); + if (!ready || outputs[0]->metadata().status != FrameMetadata::FrameSuccess) + return TestFail; + + DmaSyncer sync(outputs[0]->planes()[0].fd, DmaSyncer::SyncType::Read); + image->resize(size.width * size.height * 4); + for (unsigned int y = 0; y < size.height; y++) + std::copy_n(rgb.planes()[0].data() + y * output.stride, size.width * 4, + image->data() + y * size.width * 4); + return TestPass; + } +}; + +TEST_REGISTER(GpuCropTest)