| Message ID | 20261015194335.287960-3-halfline@gmail.com |
|---|---|
| State | New |
| 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)
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)