[v14,4/7] libcamera: pipeline: Add test pattern for VirtualPipelineHandler
diff mbox series

Message ID 20240930063342.3014837-5-chenghaoyang@google.com
State Superseded
Headers show
Series
  • Add VirtualPipelineHandler
Related show

Commit Message

Harvey Yang Sept. 30, 2024, 6:29 a.m. UTC
From: Konami Shu <konamiz@google.com>

Add a test pattern generator class hierarchy for the Virtual
pipeline handler.

Implement two types of test patterns: color bars and diagonal lines
generator and use them in the Virtual pipeline handler.

A shifting mechanism is enabled. For each frame, the image is shifted to
the left by 1 pixel. It drops FPS though.

Add a dependency for libyuv to the build system to generate images
in NV12 format from the test pattern.

Signed-off-by: Konami Shu <konamiz@google.com>
Co-developed-by: Harvey Yang <chenghaoyang@chromium.org>
Co-developed-by: Yunke Cao <yunkec@chromium.org>
Co-developed-by: Tomasz Figa <tfiga@chromium.org>
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/android/meson.build                       |  19 ---
 .../pipeline/virtual/frame_generator.h        |  29 ++++
 src/libcamera/pipeline/virtual/meson.build    |   3 +
 .../virtual/test_pattern_generator.cpp        | 137 ++++++++++++++++++
 .../pipeline/virtual/test_pattern_generator.h |  53 +++++++
 src/libcamera/pipeline/virtual/virtual.cpp    |  41 +++++-
 src/libcamera/pipeline/virtual/virtual.h      |   5 +
 src/meson.build                               |  19 +++
 8 files changed, 284 insertions(+), 22 deletions(-)
 create mode 100644 src/libcamera/pipeline/virtual/frame_generator.h
 create mode 100644 src/libcamera/pipeline/virtual/test_pattern_generator.cpp
 create mode 100644 src/libcamera/pipeline/virtual/test_pattern_generator.h

Comments

Kieran Bingham Oct. 3, 2024, 10:05 a.m. UTC | #1
Quoting Harvey Yang (2024-09-30 07:29:45)
> From: Konami Shu <konamiz@google.com>
> 
> Add a test pattern generator class hierarchy for the Virtual
> pipeline handler.
> 
> Implement two types of test patterns: color bars and diagonal lines
> generator and use them in the Virtual pipeline handler.
> 
> A shifting mechanism is enabled. For each frame, the image is shifted to
> the left by 1 pixel. It drops FPS though.

I guess we could solve the throughput here by generating a double width
image, handling the stride accordingly and just changing the offset when
calling ARGBtoNV12 ...

But no need for that now. I guess other implementations here could be
done on top.

> Add a dependency for libyuv to the build system to generate images
> in NV12 format from the test pattern.
> 
> Signed-off-by: Konami Shu <konamiz@google.com>
> Co-developed-by: Harvey Yang <chenghaoyang@chromium.org>
> Co-developed-by: Yunke Cao <yunkec@chromium.org>
> Co-developed-by: Tomasz Figa <tfiga@chromium.org>
> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>  src/android/meson.build                       |  19 ---
>  .../pipeline/virtual/frame_generator.h        |  29 ++++
>  src/libcamera/pipeline/virtual/meson.build    |   3 +
>  .../virtual/test_pattern_generator.cpp        | 137 ++++++++++++++++++
>  .../pipeline/virtual/test_pattern_generator.h |  53 +++++++
>  src/libcamera/pipeline/virtual/virtual.cpp    |  41 +++++-
>  src/libcamera/pipeline/virtual/virtual.h      |   5 +
>  src/meson.build                               |  19 +++
>  8 files changed, 284 insertions(+), 22 deletions(-)
>  create mode 100644 src/libcamera/pipeline/virtual/frame_generator.h
>  create mode 100644 src/libcamera/pipeline/virtual/test_pattern_generator.cpp
>  create mode 100644 src/libcamera/pipeline/virtual/test_pattern_generator.h
> 
> diff --git a/src/android/meson.build b/src/android/meson.build
> index 68646120..6341ee8b 100644
> --- a/src/android/meson.build
> +++ b/src/android/meson.build
> @@ -15,25 +15,6 @@ foreach dep : android_deps
>      endif
>  endforeach
>  
> -libyuv_dep = dependency('libyuv', required : false)
> -
> -# Fallback to a subproject if libyuv isn't found, as it's typically not
> -# provided by distributions.
> -if not libyuv_dep.found()
> -    cmake = import('cmake')
> -
> -    libyuv_vars = cmake.subproject_options()
> -    libyuv_vars.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
> -    libyuv_vars.set_override_option('cpp_std', 'c++17')
> -    libyuv_vars.append_compile_args('cpp',
> -         '-Wno-sign-compare',
> -         '-Wno-unused-variable',
> -         '-Wno-unused-parameter')
> -    libyuv_vars.append_link_args('-ljpeg')
> -    libyuv = cmake.subproject('libyuv', options : libyuv_vars)
> -    libyuv_dep = libyuv.dependency('yuv')
> -endif
> -
>  android_deps += [libyuv_dep]
>  
>  android_hal_sources = files([
> diff --git a/src/libcamera/pipeline/virtual/frame_generator.h b/src/libcamera/pipeline/virtual/frame_generator.h
> new file mode 100644
> index 00000000..4ff41dd8
> --- /dev/null
> +++ b/src/libcamera/pipeline/virtual/frame_generator.h
> @@ -0,0 +1,29 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Google Inc.
> + *
> + * frame_generator.h - Virtual cameras helper to generate frames
> + */
> +
> +#pragma once
> +
> +#include <libcamera/framebuffer.h>
> +#include <libcamera/geometry.h>
> +
> +namespace libcamera {
> +
> +class FrameGenerator
> +{
> +public:
> +       virtual ~FrameGenerator() = default;
> +
> +       virtual void configure(const Size &size) = 0;
> +
> +       virtual int generateFrame(const Size &size,
> +                                 const FrameBuffer *buffer) = 0;
> +
> +protected:
> +       FrameGenerator() {}
> +};
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build
> index ada1b335..0c537777 100644
> --- a/src/libcamera/pipeline/virtual/meson.build
> +++ b/src/libcamera/pipeline/virtual/meson.build
> @@ -1,5 +1,8 @@
>  # SPDX-License-Identifier: CC0-1.0
>  
>  libcamera_internal_sources += files([
> +    'test_pattern_generator.cpp',
>      'virtual.cpp',
>  ])
> +
> +libcamera_deps += [libyuv_dep]
> diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp
> new file mode 100644
> index 00000000..c5a93d37
> --- /dev/null
> +++ b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp
> @@ -0,0 +1,137 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Google Inc.
> + *
> + * test_pattern_generator.cpp - Derived class of FrameGenerator for
> + * generating test patterns
> + */
> +
> +#include "test_pattern_generator.h"
> +
> +#include <libcamera/base/log.h>
> +
> +#include "libcamera/internal/mapped_framebuffer.h"
> +
> +#include <libyuv/convert_from_argb.h>
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(Virtual)
> +
> +static const unsigned int kARGBSize = 4;
> +
> +int TestPatternGenerator::generateFrame(const Size &size,
> +                                       const FrameBuffer *buffer)
> +{
> +       MappedFrameBuffer mappedFrameBuffer(buffer,
> +                                           MappedFrameBuffer::MapFlag::Write);
> +
> +       auto planes = mappedFrameBuffer.planes();
> +
> +       shiftLeft(size);
> +
> +       /* Convert the template_ to the frame buffer */
> +       int ret = libyuv::ARGBToNV12(template_.get(), size.width * kARGBSize,

Do we need to do anything here to make sure we match the stride of the
output buffer if it comes from an external source?

Maybe that's not something we need to deal with currently.

> +                                    planes[0].begin(), size.width,
> +                                    planes[1].begin(), size.width,
> +                                    size.width, size.height);
> +       if (ret != 0)
> +               LOG(Virtual, Error) << "ARGBToNV12() failed with " << ret;
> +
> +       return ret;
> +}
> +
> +void TestPatternGenerator::shiftLeft(const Size &size)
> +{
> +       /* Store the first column temporarily */
> +       auto firstColumn = std::make_unique<uint8_t[]>(size.height * kARGBSize);
> +       for (size_t h = 0; h < size.height; h++) {
> +               unsigned int index = h * size.width * kARGBSize;
> +               unsigned int index1 = h * kARGBSize;
> +               firstColumn[index1] = template_[index];
> +               firstColumn[index1 + 1] = template_[index + 1];
> +               firstColumn[index1 + 2] = template_[index + 2];
> +               firstColumn[index1 + 3] = 0x00;
> +       }
> +
> +       /* Overwrite template_ */
> +       uint8_t *buf = template_.get();
> +       for (size_t h = 0; h < size.height; h++) {
> +               for (size_t w = 0; w < size.width - 1; w++) {
> +                       /* Overwrite with the pixel on the right */
> +                       unsigned int index = (h * size.width + w + 1) * kARGBSize;
> +                       *buf++ = template_[index]; /* B */
> +                       *buf++ = template_[index + 1]; /* G */
> +                       *buf++ = template_[index + 2]; /* R */
> +                       *buf++ = 0x00; /* A */
> +               }
> +               /* Overwrite the new last column with the original first column */
> +               unsigned int index1 = h * kARGBSize;
> +               *buf++ = firstColumn[index1]; /* B */
> +               *buf++ = firstColumn[index1 + 1]; /* G */
> +               *buf++ = firstColumn[index1 + 2]; /* R */
> +               *buf++ = 0x00; /* A */
> +       }

I could imagine ways we could try to optimise this or do different
things - but I don't think performance is key here right away - so I'm
fine with the above.

> +}
> +
> +void ColorBarsGenerator::configure(const Size &size)
> +{
> +       constexpr uint8_t kColorBar[8][3] = {
> +               /*  R,    G,    B */
> +               { 0xff, 0xff, 0xff }, /* White */
> +               { 0xff, 0xff, 0x00 }, /* Yellow */
> +               { 0x00, 0xff, 0xff }, /* Cyan */
> +               { 0x00, 0xff, 0x00 }, /* Green */
> +               { 0xff, 0x00, 0xff }, /* Magenta */
> +               { 0xff, 0x00, 0x00 }, /* Red */
> +               { 0x00, 0x00, 0xff }, /* Blue */
> +               { 0x00, 0x00, 0x00 }, /* Black */
> +       };
> +
> +       template_ = std::make_unique<uint8_t[]>(
> +               size.width * size.height * kARGBSize);
> +
> +       unsigned int colorBarWidth = size.width / std::size(kColorBar);
> +
> +       uint8_t *buf = template_.get();
> +       for (size_t h = 0; h < size.height; h++) {
> +               for (size_t w = 0; w < size.width; w++) {
> +                       /* Repeat when the width is exceed */
> +                       unsigned int index = (w / colorBarWidth) % std::size(kColorBar);
> +
> +                       *buf++ = kColorBar[index][2]; /* B */
> +                       *buf++ = kColorBar[index][1]; /* G */
> +                       *buf++ = kColorBar[index][0]; /* R */
> +                       *buf++ = 0x00; /* A */
> +               }
> +       }
> +}
> +
> +void DiagonalLinesGenerator::configure(const Size &size)
> +{
> +       constexpr uint8_t kColorBar[8][3] = {

[8][3] ?

> +               /*  R,    G,    B */
> +               { 0xff, 0xff, 0xff }, /* White */
> +               { 0x00, 0x00, 0x00 }, /* Black */
> +       };
> +
> +       template_ = std::make_unique<uint8_t[]>(
> +               size.width * size.height * kARGBSize);
> +
> +       unsigned int lineWidth = size.width / 10;
> +
> +       uint8_t *buf = template_.get();
> +       for (size_t h = 0; h < size.height; h++) {
> +               for (size_t w = 0; w < size.width; w++) {
> +                       /* Repeat when the width is exceed */
> +                       int index = ((w + h) / lineWidth) % 2;
> +
> +                       *buf++ = kColorBar[index][2]; /* B */
> +                       *buf++ = kColorBar[index][1]; /* G */
> +                       *buf++ = kColorBar[index][0]; /* R */
> +                       *buf++ = 0x00; /* A */
> +               }
> +       }
> +}
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.h b/src/libcamera/pipeline/virtual/test_pattern_generator.h
> new file mode 100644
> index 00000000..2ff1e40e
> --- /dev/null
> +++ b/src/libcamera/pipeline/virtual/test_pattern_generator.h
> @@ -0,0 +1,53 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2024, Google Inc.
> + *
> + * test_pattern_generator.h - Derived class of FrameGenerator for
> + * generating test patterns
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/framebuffer.h>
> +#include <libcamera/geometry.h>
> +
> +#include "frame_generator.h"
> +
> +namespace libcamera {
> +
> +enum class TestPattern : char {
> +       ColorBars = 0,
> +       DiagonalLines = 1,
> +};
> +
> +class TestPatternGenerator : public FrameGenerator
> +{
> +public:
> +       int generateFrame(const Size &size, const FrameBuffer *buffer) override;
> +
> +protected:
> +       /* Buffer of test pattern template */
> +       std::unique_ptr<uint8_t[]> template_;
> +
> +private:
> +       /* Shift the buffer by 1 pixel left each frame */
> +       void shiftLeft(const Size &size);
> +};
> +
> +class ColorBarsGenerator : public TestPatternGenerator
> +{
> +public:
> +       /* Generate a template buffer of the color bar test pattern. */
> +       void configure(const Size &size) override;
> +};
> +
> +class DiagonalLinesGenerator : public TestPatternGenerator
> +{
> +public:
> +       /* Generate a template buffer of the diagonal lines test pattern. */
> +       void configure(const Size &size) override;
> +};
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
> index d1584f59..2b258492 100644
> --- a/src/libcamera/pipeline/virtual/virtual.cpp
> +++ b/src/libcamera/pipeline/virtual/virtual.cpp
> @@ -34,6 +34,7 @@
>  #include "libcamera/internal/camera.h"
>  #include "libcamera/internal/dma_buf_allocator.h"
>  #include "libcamera/internal/formats.h"
> +#include "libcamera/internal/framebuffer.h"
>  #include "libcamera/internal/pipeline_handler.h"
>  
>  namespace libcamera {
> @@ -93,6 +94,8 @@ private:
>                 return static_cast<VirtualCameraData *>(camera->_d());
>         }
>  
> +       void initFrameGenerator(Camera *camera);
> +
>         DmaBufAllocator dmaBufAllocator_;
>  };
>  
> @@ -227,8 +230,10 @@ int PipelineHandlerVirtual::configure(Camera *camera,
>                                       CameraConfiguration *config)
>  {
>         VirtualCameraData *data = cameraData(camera);
> -       for (auto [i, c] : utils::enumerate(*config))
> +       for (auto [i, c] : utils::enumerate(*config)) {
>                 c.setStream(&data->streamConfigs_[i].stream);
> +               data->streamConfigs_[i].frameGenerator->configure(c.size);
> +       }
>  
>         return 0;
>  }
> @@ -264,8 +269,24 @@ void PipelineHandlerVirtual::stopDevice([[maybe_unused]] Camera *camera)
>  int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
>                                                Request *request)
>  {
> -       for (auto it : request->buffers())
> -               completeBuffer(request, it.second);
> +       VirtualCameraData *data = cameraData(camera);
> +
> +       for (auto const &[stream, buffer] : request->buffers()) {
> +               bool found = false;
> +               /* map buffer and fill test patterns */
> +               for (auto &streamConfig : data->streamConfigs_) {
> +                       if (stream == &streamConfig.stream) {
> +                               found = true;
> +                               if (streamConfig.frameGenerator->generateFrame(
> +                                           stream->configuration().size, buffer))
> +                                       buffer->_d()->cancel();
> +
> +                               completeBuffer(request, buffer);
> +                               break;
> +                       }
> +               }
> +               ASSERT(found);
> +       }
>  
>         request->metadata().set(controls::SensorTimestamp, currentTimestamp());
>         completeRequest(request);
> @@ -307,11 +328,25 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator
>  
>         const std::string id = "Virtual0";
>         std::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);
> +
> +       initFrameGenerator(camera.get());
> +
>         registerCamera(std::move(camera));
>  
>         return true;
>  }
>  
> +void PipelineHandlerVirtual::initFrameGenerator(Camera *camera)
> +{
> +       auto data = cameraData(camera);
> +       for (auto &streamConfig : data->streamConfigs_) {
> +               if (data->testPattern_ == TestPattern::DiagonalLines)
> +                       streamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();
> +               else
> +                       streamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();
> +       }
> +}
> +
>  REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, "virtual")
>  
>  } /* namespace libcamera */
> diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
> index 4df70a13..9b79ac09 100644
> --- a/src/libcamera/pipeline/virtual/virtual.h
> +++ b/src/libcamera/pipeline/virtual/virtual.h
> @@ -15,6 +15,8 @@
>  #include "libcamera/internal/camera.h"
>  #include "libcamera/internal/pipeline_handler.h"
>  
> +#include "test_pattern_generator.h"
> +
>  namespace libcamera {
>  
>  class VirtualCameraData : public Camera::Private
> @@ -28,6 +30,7 @@ public:
>         };
>         struct StreamConfig {
>                 Stream stream;
> +               std::unique_ptr<FrameGenerator> frameGenerator;
>         };
>  
>         VirtualCameraData(PipelineHandler *pipe,
> @@ -35,6 +38,8 @@ public:
>  
>         ~VirtualCameraData() = default;
>  
> +       TestPattern testPattern_ = TestPattern::ColorBars;
> +
>         const std::vector<Resolution> supportedResolutions_;
>         Size maxResolutionSize_;
>         Size minResolutionSize_;
> diff --git a/src/meson.build b/src/meson.build
> index 165a77bb..91bea775 100644
> --- a/src/meson.build
> +++ b/src/meson.build
> @@ -27,6 +27,25 @@ else
>      ipa_sign_module = false
>  endif
>  
> +libyuv_dep = dependency('libyuv', required : false)
> +
> +# Fallback to a subproject if libyuv isn't found, as it's typically not
> +# provided by distributions.
> +if not libyuv_dep.found()
> +    cmake = import('cmake')
> +
> +    libyuv_vars = cmake.subproject_options()
> +    libyuv_vars.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
> +    libyuv_vars.set_override_option('cpp_std', 'c++17')
> +    libyuv_vars.append_compile_args('cpp',
> +         '-Wno-sign-compare',
> +         '-Wno-unused-variable',
> +         '-Wno-unused-parameter')
> +    libyuv_vars.append_link_args('-ljpeg')
> +    libyuv = cmake.subproject('libyuv', options : libyuv_vars)
> +    libyuv_dep = libyuv.dependency('yuv')
> +endif
> +

I'm not yet sure in my mind if this should be in the top level where
other dependencies are handled, or if here is fine. As this is the
highest common layer for both android and libcamera - I think this is
fine.

It would have been nicer to split this meson change out to a distinct
patch but it's fine now.

I'm going to say it's fine to move here, and if we decide differently
later - it's easy to move.


With at least that [8][3] fixed, which I think is the only 'incorrect'
thing, then you can add:

Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>


>  # libcamera must be built first as a dependency to the other components.
>  subdir('libcamera')
>  
> -- 
> 2.46.1.824.gd892dcdcdd-goog
>
Kieran Bingham Oct. 3, 2024, 10:33 a.m. UTC | #2
Quoting Harvey Yang (2024-09-30 07:29:45)
> From: Konami Shu <konamiz@google.com>

I'm getting email bounces that konamiz@google.com is not a valid email
address.

--
Kieran
Harvey Yang Oct. 4, 2024, 9:49 a.m. UTC | #3
Hi Kieran,

On Thu, Oct 3, 2024 at 6:33 PM Kieran Bingham
<kieran.bingham@ideasonboard.com> wrote:
>
> Quoting Harvey Yang (2024-09-30 07:29:45)
> > From: Konami Shu <konamiz@google.com>
>
> I'm getting email bounces that konamiz@google.com is not a valid email
> address.

Yeah Konami was our intern last year, and after she left the email address
is invalid... (Until she comes back one day perhaps haha)

I'd like to keep her credit though.

BR,
Harvey

>
> --
> Kieran
Kieran Bingham Oct. 4, 2024, 9:58 a.m. UTC | #4
Quoting Cheng-Hao Yang (2024-10-04 10:49:15)
> Hi Kieran,
> 
> On Thu, Oct 3, 2024 at 6:33 PM Kieran Bingham
> <kieran.bingham@ideasonboard.com> wrote:
> >
> > Quoting Harvey Yang (2024-09-30 07:29:45)
> > > From: Konami Shu <konamiz@google.com>
> >
> > I'm getting email bounces that konamiz@google.com is not a valid email
> > address.
> 
> Yeah Konami was our intern last year, and after she left the email address
> is invalid... (Until she comes back one day perhaps haha)
> 
> I'd like to keep her credit though.

Certainly! That's fine with me.
--
Kieran


> 
> BR,
> Harvey
> 
> >
> > --
> > Kieran
Harvey Yang Oct. 4, 2024, 10 a.m. UTC | #5
Hi Kieran,

On Thu, Oct 3, 2024 at 6:05 PM Kieran Bingham
<kieran.bingham@ideasonboard.com> wrote:
>
> Quoting Harvey Yang (2024-09-30 07:29:45)
> > From: Konami Shu <konamiz@google.com>
> >
> > Add a test pattern generator class hierarchy for the Virtual
> > pipeline handler.
> >
> > Implement two types of test patterns: color bars and diagonal lines
> > generator and use them in the Virtual pipeline handler.
> >
> > A shifting mechanism is enabled. For each frame, the image is shifted to
> > the left by 1 pixel. It drops FPS though.
>
> I guess we could solve the throughput here by generating a double width
> image, handling the stride accordingly and just changing the offset when
> calling ARGBtoNV12 ...
>
> But no need for that now. I guess other implementations here could be
> done on top.
>
> > Add a dependency for libyuv to the build system to generate images
> > in NV12 format from the test pattern.
> >
> > Signed-off-by: Konami Shu <konamiz@google.com>
> > Co-developed-by: Harvey Yang <chenghaoyang@chromium.org>
> > Co-developed-by: Yunke Cao <yunkec@chromium.org>
> > Co-developed-by: Tomasz Figa <tfiga@chromium.org>
> > Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > ---
> >  src/android/meson.build                       |  19 ---
> >  .../pipeline/virtual/frame_generator.h        |  29 ++++
> >  src/libcamera/pipeline/virtual/meson.build    |   3 +
> >  .../virtual/test_pattern_generator.cpp        | 137 ++++++++++++++++++
> >  .../pipeline/virtual/test_pattern_generator.h |  53 +++++++
> >  src/libcamera/pipeline/virtual/virtual.cpp    |  41 +++++-
> >  src/libcamera/pipeline/virtual/virtual.h      |   5 +
> >  src/meson.build                               |  19 +++
> >  8 files changed, 284 insertions(+), 22 deletions(-)
> >  create mode 100644 src/libcamera/pipeline/virtual/frame_generator.h
> >  create mode 100644 src/libcamera/pipeline/virtual/test_pattern_generator.cpp
> >  create mode 100644 src/libcamera/pipeline/virtual/test_pattern_generator.h
> >
> > diff --git a/src/android/meson.build b/src/android/meson.build
> > index 68646120..6341ee8b 100644
> > --- a/src/android/meson.build
> > +++ b/src/android/meson.build
> > @@ -15,25 +15,6 @@ foreach dep : android_deps
> >      endif
> >  endforeach
> >
> > -libyuv_dep = dependency('libyuv', required : false)
> > -
> > -# Fallback to a subproject if libyuv isn't found, as it's typically not
> > -# provided by distributions.
> > -if not libyuv_dep.found()
> > -    cmake = import('cmake')
> > -
> > -    libyuv_vars = cmake.subproject_options()
> > -    libyuv_vars.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
> > -    libyuv_vars.set_override_option('cpp_std', 'c++17')
> > -    libyuv_vars.append_compile_args('cpp',
> > -         '-Wno-sign-compare',
> > -         '-Wno-unused-variable',
> > -         '-Wno-unused-parameter')
> > -    libyuv_vars.append_link_args('-ljpeg')
> > -    libyuv = cmake.subproject('libyuv', options : libyuv_vars)
> > -    libyuv_dep = libyuv.dependency('yuv')
> > -endif
> > -
> >  android_deps += [libyuv_dep]
> >
> >  android_hal_sources = files([
> > diff --git a/src/libcamera/pipeline/virtual/frame_generator.h b/src/libcamera/pipeline/virtual/frame_generator.h
> > new file mode 100644
> > index 00000000..4ff41dd8
> > --- /dev/null
> > +++ b/src/libcamera/pipeline/virtual/frame_generator.h
> > @@ -0,0 +1,29 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2024, Google Inc.
> > + *
> > + * frame_generator.h - Virtual cameras helper to generate frames
> > + */
> > +
> > +#pragma once
> > +
> > +#include <libcamera/framebuffer.h>
> > +#include <libcamera/geometry.h>
> > +
> > +namespace libcamera {
> > +
> > +class FrameGenerator
> > +{
> > +public:
> > +       virtual ~FrameGenerator() = default;
> > +
> > +       virtual void configure(const Size &size) = 0;
> > +
> > +       virtual int generateFrame(const Size &size,
> > +                                 const FrameBuffer *buffer) = 0;
> > +
> > +protected:
> > +       FrameGenerator() {}
> > +};
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build
> > index ada1b335..0c537777 100644
> > --- a/src/libcamera/pipeline/virtual/meson.build
> > +++ b/src/libcamera/pipeline/virtual/meson.build
> > @@ -1,5 +1,8 @@
> >  # SPDX-License-Identifier: CC0-1.0
> >
> >  libcamera_internal_sources += files([
> > +    'test_pattern_generator.cpp',
> >      'virtual.cpp',
> >  ])
> > +
> > +libcamera_deps += [libyuv_dep]
> > diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp
> > new file mode 100644
> > index 00000000..c5a93d37
> > --- /dev/null
> > +++ b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp
> > @@ -0,0 +1,137 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2024, Google Inc.
> > + *
> > + * test_pattern_generator.cpp - Derived class of FrameGenerator for
> > + * generating test patterns
> > + */
> > +
> > +#include "test_pattern_generator.h"
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +#include "libcamera/internal/mapped_framebuffer.h"
> > +
> > +#include <libyuv/convert_from_argb.h>
> > +
> > +namespace libcamera {
> > +
> > +LOG_DECLARE_CATEGORY(Virtual)
> > +
> > +static const unsigned int kARGBSize = 4;
> > +
> > +int TestPatternGenerator::generateFrame(const Size &size,
> > +                                       const FrameBuffer *buffer)
> > +{
> > +       MappedFrameBuffer mappedFrameBuffer(buffer,
> > +                                           MappedFrameBuffer::MapFlag::Write);
> > +
> > +       auto planes = mappedFrameBuffer.planes();
> > +
> > +       shiftLeft(size);
> > +
> > +       /* Convert the template_ to the frame buffer */
> > +       int ret = libyuv::ARGBToNV12(template_.get(), size.width * kARGBSize,
>
> Do we need to do anything here to make sure we match the stride of the
> output buffer if it comes from an external source?
>
> Maybe that's not something we need to deal with currently.
>
> > +                                    planes[0].begin(), size.width,
> > +                                    planes[1].begin(), size.width,
> > +                                    size.width, size.height);
> > +       if (ret != 0)
> > +               LOG(Virtual, Error) << "ARGBToNV12() failed with " << ret;
> > +
> > +       return ret;
> > +}
> > +
> > +void TestPatternGenerator::shiftLeft(const Size &size)
> > +{
> > +       /* Store the first column temporarily */
> > +       auto firstColumn = std::make_unique<uint8_t[]>(size.height * kARGBSize);
> > +       for (size_t h = 0; h < size.height; h++) {
> > +               unsigned int index = h * size.width * kARGBSize;
> > +               unsigned int index1 = h * kARGBSize;
> > +               firstColumn[index1] = template_[index];
> > +               firstColumn[index1 + 1] = template_[index + 1];
> > +               firstColumn[index1 + 2] = template_[index + 2];
> > +               firstColumn[index1 + 3] = 0x00;
> > +       }
> > +
> > +       /* Overwrite template_ */
> > +       uint8_t *buf = template_.get();
> > +       for (size_t h = 0; h < size.height; h++) {
> > +               for (size_t w = 0; w < size.width - 1; w++) {
> > +                       /* Overwrite with the pixel on the right */
> > +                       unsigned int index = (h * size.width + w + 1) * kARGBSize;
> > +                       *buf++ = template_[index]; /* B */
> > +                       *buf++ = template_[index + 1]; /* G */
> > +                       *buf++ = template_[index + 2]; /* R */
> > +                       *buf++ = 0x00; /* A */
> > +               }
> > +               /* Overwrite the new last column with the original first column */
> > +               unsigned int index1 = h * kARGBSize;
> > +               *buf++ = firstColumn[index1]; /* B */
> > +               *buf++ = firstColumn[index1 + 1]; /* G */
> > +               *buf++ = firstColumn[index1 + 2]; /* R */
> > +               *buf++ = 0x00; /* A */
> > +       }
>
> I could imagine ways we could try to optimise this or do different
> things - but I don't think performance is key here right away - so I'm
> fine with the above.
>
> > +}
> > +
> > +void ColorBarsGenerator::configure(const Size &size)
> > +{
> > +       constexpr uint8_t kColorBar[8][3] = {
> > +               /*  R,    G,    B */
> > +               { 0xff, 0xff, 0xff }, /* White */
> > +               { 0xff, 0xff, 0x00 }, /* Yellow */
> > +               { 0x00, 0xff, 0xff }, /* Cyan */
> > +               { 0x00, 0xff, 0x00 }, /* Green */
> > +               { 0xff, 0x00, 0xff }, /* Magenta */
> > +               { 0xff, 0x00, 0x00 }, /* Red */
> > +               { 0x00, 0x00, 0xff }, /* Blue */
> > +               { 0x00, 0x00, 0x00 }, /* Black */
> > +       };
> > +
> > +       template_ = std::make_unique<uint8_t[]>(
> > +               size.width * size.height * kARGBSize);
> > +
> > +       unsigned int colorBarWidth = size.width / std::size(kColorBar);
> > +
> > +       uint8_t *buf = template_.get();
> > +       for (size_t h = 0; h < size.height; h++) {
> > +               for (size_t w = 0; w < size.width; w++) {
> > +                       /* Repeat when the width is exceed */
> > +                       unsigned int index = (w / colorBarWidth) % std::size(kColorBar);
> > +
> > +                       *buf++ = kColorBar[index][2]; /* B */
> > +                       *buf++ = kColorBar[index][1]; /* G */
> > +                       *buf++ = kColorBar[index][0]; /* R */
> > +                       *buf++ = 0x00; /* A */
> > +               }
> > +       }
> > +}
> > +
> > +void DiagonalLinesGenerator::configure(const Size &size)
> > +{
> > +       constexpr uint8_t kColorBar[8][3] = {
>
> [8][3] ?

Ah, updated to [2][3].

>
> > +               /*  R,    G,    B */
> > +               { 0xff, 0xff, 0xff }, /* White */
> > +               { 0x00, 0x00, 0x00 }, /* Black */
> > +       };
> > +
> > +       template_ = std::make_unique<uint8_t[]>(
> > +               size.width * size.height * kARGBSize);
> > +
> > +       unsigned int lineWidth = size.width / 10;
> > +
> > +       uint8_t *buf = template_.get();
> > +       for (size_t h = 0; h < size.height; h++) {
> > +               for (size_t w = 0; w < size.width; w++) {
> > +                       /* Repeat when the width is exceed */
> > +                       int index = ((w + h) / lineWidth) % 2;
> > +
> > +                       *buf++ = kColorBar[index][2]; /* B */
> > +                       *buf++ = kColorBar[index][1]; /* G */
> > +                       *buf++ = kColorBar[index][0]; /* R */
> > +                       *buf++ = 0x00; /* A */
> > +               }
> > +       }
> > +}
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.h b/src/libcamera/pipeline/virtual/test_pattern_generator.h
> > new file mode 100644
> > index 00000000..2ff1e40e
> > --- /dev/null
> > +++ b/src/libcamera/pipeline/virtual/test_pattern_generator.h
> > @@ -0,0 +1,53 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2024, Google Inc.
> > + *
> > + * test_pattern_generator.h - Derived class of FrameGenerator for
> > + * generating test patterns
> > + */
> > +
> > +#pragma once
> > +
> > +#include <memory>
> > +
> > +#include <libcamera/framebuffer.h>
> > +#include <libcamera/geometry.h>
> > +
> > +#include "frame_generator.h"
> > +
> > +namespace libcamera {
> > +
> > +enum class TestPattern : char {
> > +       ColorBars = 0,
> > +       DiagonalLines = 1,
> > +};
> > +
> > +class TestPatternGenerator : public FrameGenerator
> > +{
> > +public:
> > +       int generateFrame(const Size &size, const FrameBuffer *buffer) override;
> > +
> > +protected:
> > +       /* Buffer of test pattern template */
> > +       std::unique_ptr<uint8_t[]> template_;
> > +
> > +private:
> > +       /* Shift the buffer by 1 pixel left each frame */
> > +       void shiftLeft(const Size &size);
> > +};
> > +
> > +class ColorBarsGenerator : public TestPatternGenerator
> > +{
> > +public:
> > +       /* Generate a template buffer of the color bar test pattern. */
> > +       void configure(const Size &size) override;
> > +};
> > +
> > +class DiagonalLinesGenerator : public TestPatternGenerator
> > +{
> > +public:
> > +       /* Generate a template buffer of the diagonal lines test pattern. */
> > +       void configure(const Size &size) override;
> > +};
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
> > index d1584f59..2b258492 100644
> > --- a/src/libcamera/pipeline/virtual/virtual.cpp
> > +++ b/src/libcamera/pipeline/virtual/virtual.cpp
> > @@ -34,6 +34,7 @@
> >  #include "libcamera/internal/camera.h"
> >  #include "libcamera/internal/dma_buf_allocator.h"
> >  #include "libcamera/internal/formats.h"
> > +#include "libcamera/internal/framebuffer.h"
> >  #include "libcamera/internal/pipeline_handler.h"
> >
> >  namespace libcamera {
> > @@ -93,6 +94,8 @@ private:
> >                 return static_cast<VirtualCameraData *>(camera->_d());
> >         }
> >
> > +       void initFrameGenerator(Camera *camera);
> > +
> >         DmaBufAllocator dmaBufAllocator_;
> >  };
> >
> > @@ -227,8 +230,10 @@ int PipelineHandlerVirtual::configure(Camera *camera,
> >                                       CameraConfiguration *config)
> >  {
> >         VirtualCameraData *data = cameraData(camera);
> > -       for (auto [i, c] : utils::enumerate(*config))
> > +       for (auto [i, c] : utils::enumerate(*config)) {
> >                 c.setStream(&data->streamConfigs_[i].stream);
> > +               data->streamConfigs_[i].frameGenerator->configure(c.size);
> > +       }
> >
> >         return 0;
> >  }
> > @@ -264,8 +269,24 @@ void PipelineHandlerVirtual::stopDevice([[maybe_unused]] Camera *camera)
> >  int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
> >                                                Request *request)
> >  {
> > -       for (auto it : request->buffers())
> > -               completeBuffer(request, it.second);
> > +       VirtualCameraData *data = cameraData(camera);
> > +
> > +       for (auto const &[stream, buffer] : request->buffers()) {
> > +               bool found = false;
> > +               /* map buffer and fill test patterns */
> > +               for (auto &streamConfig : data->streamConfigs_) {
> > +                       if (stream == &streamConfig.stream) {
> > +                               found = true;
> > +                               if (streamConfig.frameGenerator->generateFrame(
> > +                                           stream->configuration().size, buffer))
> > +                                       buffer->_d()->cancel();
> > +
> > +                               completeBuffer(request, buffer);
> > +                               break;
> > +                       }
> > +               }
> > +               ASSERT(found);
> > +       }
> >
> >         request->metadata().set(controls::SensorTimestamp, currentTimestamp());
> >         completeRequest(request);
> > @@ -307,11 +328,25 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator
> >
> >         const std::string id = "Virtual0";
> >         std::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);
> > +
> > +       initFrameGenerator(camera.get());
> > +
> >         registerCamera(std::move(camera));
> >
> >         return true;
> >  }
> >
> > +void PipelineHandlerVirtual::initFrameGenerator(Camera *camera)
> > +{
> > +       auto data = cameraData(camera);
> > +       for (auto &streamConfig : data->streamConfigs_) {
> > +               if (data->testPattern_ == TestPattern::DiagonalLines)
> > +                       streamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();
> > +               else
> > +                       streamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();
> > +       }
> > +}
> > +
> >  REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, "virtual")
> >
> >  } /* namespace libcamera */
> > diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
> > index 4df70a13..9b79ac09 100644
> > --- a/src/libcamera/pipeline/virtual/virtual.h
> > +++ b/src/libcamera/pipeline/virtual/virtual.h
> > @@ -15,6 +15,8 @@
> >  #include "libcamera/internal/camera.h"
> >  #include "libcamera/internal/pipeline_handler.h"
> >
> > +#include "test_pattern_generator.h"
> > +
> >  namespace libcamera {
> >
> >  class VirtualCameraData : public Camera::Private
> > @@ -28,6 +30,7 @@ public:
> >         };
> >         struct StreamConfig {
> >                 Stream stream;
> > +               std::unique_ptr<FrameGenerator> frameGenerator;
> >         };
> >
> >         VirtualCameraData(PipelineHandler *pipe,
> > @@ -35,6 +38,8 @@ public:
> >
> >         ~VirtualCameraData() = default;
> >
> > +       TestPattern testPattern_ = TestPattern::ColorBars;
> > +
> >         const std::vector<Resolution> supportedResolutions_;
> >         Size maxResolutionSize_;
> >         Size minResolutionSize_;
> > diff --git a/src/meson.build b/src/meson.build
> > index 165a77bb..91bea775 100644
> > --- a/src/meson.build
> > +++ b/src/meson.build
> > @@ -27,6 +27,25 @@ else
> >      ipa_sign_module = false
> >  endif
> >
> > +libyuv_dep = dependency('libyuv', required : false)
> > +
> > +# Fallback to a subproject if libyuv isn't found, as it's typically not
> > +# provided by distributions.
> > +if not libyuv_dep.found()
> > +    cmake = import('cmake')
> > +
> > +    libyuv_vars = cmake.subproject_options()
> > +    libyuv_vars.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
> > +    libyuv_vars.set_override_option('cpp_std', 'c++17')
> > +    libyuv_vars.append_compile_args('cpp',
> > +         '-Wno-sign-compare',
> > +         '-Wno-unused-variable',
> > +         '-Wno-unused-parameter')
> > +    libyuv_vars.append_link_args('-ljpeg')
> > +    libyuv = cmake.subproject('libyuv', options : libyuv_vars)
> > +    libyuv_dep = libyuv.dependency('yuv')
> > +endif
> > +
>
> I'm not yet sure in my mind if this should be in the top level where
> other dependencies are handled, or if here is fine. As this is the
> highest common layer for both android and libcamera - I think this is
> fine.
>
> It would have been nicer to split this meson change out to a distinct
> patch but it's fine now.
>
> I'm going to say it's fine to move here, and if we decide differently
> later - it's easy to move.
>
>
> With at least that [8][3] fixed, which I think is the only 'incorrect'
> thing, then you can add:
>
> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>

Thanks Kieran, true that there are some optimization that we
can work on, while maybe save the work for now, until we
really need them.



>
>
> >  # libcamera must be built first as a dependency to the other components.
> >  subdir('libcamera')
> >
> > --
> > 2.46.1.824.gd892dcdcdd-goog
> >

Patch
diff mbox series

diff --git a/src/android/meson.build b/src/android/meson.build
index 68646120..6341ee8b 100644
--- a/src/android/meson.build
+++ b/src/android/meson.build
@@ -15,25 +15,6 @@  foreach dep : android_deps
     endif
 endforeach
 
-libyuv_dep = dependency('libyuv', required : false)
-
-# Fallback to a subproject if libyuv isn't found, as it's typically not
-# provided by distributions.
-if not libyuv_dep.found()
-    cmake = import('cmake')
-
-    libyuv_vars = cmake.subproject_options()
-    libyuv_vars.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
-    libyuv_vars.set_override_option('cpp_std', 'c++17')
-    libyuv_vars.append_compile_args('cpp',
-         '-Wno-sign-compare',
-         '-Wno-unused-variable',
-         '-Wno-unused-parameter')
-    libyuv_vars.append_link_args('-ljpeg')
-    libyuv = cmake.subproject('libyuv', options : libyuv_vars)
-    libyuv_dep = libyuv.dependency('yuv')
-endif
-
 android_deps += [libyuv_dep]
 
 android_hal_sources = files([
diff --git a/src/libcamera/pipeline/virtual/frame_generator.h b/src/libcamera/pipeline/virtual/frame_generator.h
new file mode 100644
index 00000000..4ff41dd8
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/frame_generator.h
@@ -0,0 +1,29 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Google Inc.
+ *
+ * frame_generator.h - Virtual cameras helper to generate frames
+ */
+
+#pragma once
+
+#include <libcamera/framebuffer.h>
+#include <libcamera/geometry.h>
+
+namespace libcamera {
+
+class FrameGenerator
+{
+public:
+	virtual ~FrameGenerator() = default;
+
+	virtual void configure(const Size &size) = 0;
+
+	virtual int generateFrame(const Size &size,
+				  const FrameBuffer *buffer) = 0;
+
+protected:
+	FrameGenerator() {}
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build
index ada1b335..0c537777 100644
--- a/src/libcamera/pipeline/virtual/meson.build
+++ b/src/libcamera/pipeline/virtual/meson.build
@@ -1,5 +1,8 @@ 
 # SPDX-License-Identifier: CC0-1.0
 
 libcamera_internal_sources += files([
+    'test_pattern_generator.cpp',
     'virtual.cpp',
 ])
+
+libcamera_deps += [libyuv_dep]
diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp
new file mode 100644
index 00000000..c5a93d37
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp
@@ -0,0 +1,137 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Google Inc.
+ *
+ * test_pattern_generator.cpp - Derived class of FrameGenerator for
+ * generating test patterns
+ */
+
+#include "test_pattern_generator.h"
+
+#include <libcamera/base/log.h>
+
+#include "libcamera/internal/mapped_framebuffer.h"
+
+#include <libyuv/convert_from_argb.h>
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Virtual)
+
+static const unsigned int kARGBSize = 4;
+
+int TestPatternGenerator::generateFrame(const Size &size,
+					const FrameBuffer *buffer)
+{
+	MappedFrameBuffer mappedFrameBuffer(buffer,
+					    MappedFrameBuffer::MapFlag::Write);
+
+	auto planes = mappedFrameBuffer.planes();
+
+	shiftLeft(size);
+
+	/* Convert the template_ to the frame buffer */
+	int ret = libyuv::ARGBToNV12(template_.get(), size.width * kARGBSize,
+				     planes[0].begin(), size.width,
+				     planes[1].begin(), size.width,
+				     size.width, size.height);
+	if (ret != 0)
+		LOG(Virtual, Error) << "ARGBToNV12() failed with " << ret;
+
+	return ret;
+}
+
+void TestPatternGenerator::shiftLeft(const Size &size)
+{
+	/* Store the first column temporarily */
+	auto firstColumn = std::make_unique<uint8_t[]>(size.height * kARGBSize);
+	for (size_t h = 0; h < size.height; h++) {
+		unsigned int index = h * size.width * kARGBSize;
+		unsigned int index1 = h * kARGBSize;
+		firstColumn[index1] = template_[index];
+		firstColumn[index1 + 1] = template_[index + 1];
+		firstColumn[index1 + 2] = template_[index + 2];
+		firstColumn[index1 + 3] = 0x00;
+	}
+
+	/* Overwrite template_ */
+	uint8_t *buf = template_.get();
+	for (size_t h = 0; h < size.height; h++) {
+		for (size_t w = 0; w < size.width - 1; w++) {
+			/* Overwrite with the pixel on the right */
+			unsigned int index = (h * size.width + w + 1) * kARGBSize;
+			*buf++ = template_[index]; /* B */
+			*buf++ = template_[index + 1]; /* G */
+			*buf++ = template_[index + 2]; /* R */
+			*buf++ = 0x00; /* A */
+		}
+		/* Overwrite the new last column with the original first column */
+		unsigned int index1 = h * kARGBSize;
+		*buf++ = firstColumn[index1]; /* B */
+		*buf++ = firstColumn[index1 + 1]; /* G */
+		*buf++ = firstColumn[index1 + 2]; /* R */
+		*buf++ = 0x00; /* A */
+	}
+}
+
+void ColorBarsGenerator::configure(const Size &size)
+{
+	constexpr uint8_t kColorBar[8][3] = {
+		/*  R,    G,    B */
+		{ 0xff, 0xff, 0xff }, /* White */
+		{ 0xff, 0xff, 0x00 }, /* Yellow */
+		{ 0x00, 0xff, 0xff }, /* Cyan */
+		{ 0x00, 0xff, 0x00 }, /* Green */
+		{ 0xff, 0x00, 0xff }, /* Magenta */
+		{ 0xff, 0x00, 0x00 }, /* Red */
+		{ 0x00, 0x00, 0xff }, /* Blue */
+		{ 0x00, 0x00, 0x00 }, /* Black */
+	};
+
+	template_ = std::make_unique<uint8_t[]>(
+		size.width * size.height * kARGBSize);
+
+	unsigned int colorBarWidth = size.width / std::size(kColorBar);
+
+	uint8_t *buf = template_.get();
+	for (size_t h = 0; h < size.height; h++) {
+		for (size_t w = 0; w < size.width; w++) {
+			/* Repeat when the width is exceed */
+			unsigned int index = (w / colorBarWidth) % std::size(kColorBar);
+
+			*buf++ = kColorBar[index][2]; /* B */
+			*buf++ = kColorBar[index][1]; /* G */
+			*buf++ = kColorBar[index][0]; /* R */
+			*buf++ = 0x00; /* A */
+		}
+	}
+}
+
+void DiagonalLinesGenerator::configure(const Size &size)
+{
+	constexpr uint8_t kColorBar[8][3] = {
+		/*  R,    G,    B */
+		{ 0xff, 0xff, 0xff }, /* White */
+		{ 0x00, 0x00, 0x00 }, /* Black */
+	};
+
+	template_ = std::make_unique<uint8_t[]>(
+		size.width * size.height * kARGBSize);
+
+	unsigned int lineWidth = size.width / 10;
+
+	uint8_t *buf = template_.get();
+	for (size_t h = 0; h < size.height; h++) {
+		for (size_t w = 0; w < size.width; w++) {
+			/* Repeat when the width is exceed */
+			int index = ((w + h) / lineWidth) % 2;
+
+			*buf++ = kColorBar[index][2]; /* B */
+			*buf++ = kColorBar[index][1]; /* G */
+			*buf++ = kColorBar[index][0]; /* R */
+			*buf++ = 0x00; /* A */
+		}
+	}
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.h b/src/libcamera/pipeline/virtual/test_pattern_generator.h
new file mode 100644
index 00000000..2ff1e40e
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/test_pattern_generator.h
@@ -0,0 +1,53 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Google Inc.
+ *
+ * test_pattern_generator.h - Derived class of FrameGenerator for
+ * generating test patterns
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <libcamera/framebuffer.h>
+#include <libcamera/geometry.h>
+
+#include "frame_generator.h"
+
+namespace libcamera {
+
+enum class TestPattern : char {
+	ColorBars = 0,
+	DiagonalLines = 1,
+};
+
+class TestPatternGenerator : public FrameGenerator
+{
+public:
+	int generateFrame(const Size &size, const FrameBuffer *buffer) override;
+
+protected:
+	/* Buffer of test pattern template */
+	std::unique_ptr<uint8_t[]> template_;
+
+private:
+	/* Shift the buffer by 1 pixel left each frame */
+	void shiftLeft(const Size &size);
+};
+
+class ColorBarsGenerator : public TestPatternGenerator
+{
+public:
+	/* Generate a template buffer of the color bar test pattern. */
+	void configure(const Size &size) override;
+};
+
+class DiagonalLinesGenerator : public TestPatternGenerator
+{
+public:
+	/* Generate a template buffer of the diagonal lines test pattern. */
+	void configure(const Size &size) override;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
index d1584f59..2b258492 100644
--- a/src/libcamera/pipeline/virtual/virtual.cpp
+++ b/src/libcamera/pipeline/virtual/virtual.cpp
@@ -34,6 +34,7 @@ 
 #include "libcamera/internal/camera.h"
 #include "libcamera/internal/dma_buf_allocator.h"
 #include "libcamera/internal/formats.h"
+#include "libcamera/internal/framebuffer.h"
 #include "libcamera/internal/pipeline_handler.h"
 
 namespace libcamera {
@@ -93,6 +94,8 @@  private:
 		return static_cast<VirtualCameraData *>(camera->_d());
 	}
 
+	void initFrameGenerator(Camera *camera);
+
 	DmaBufAllocator dmaBufAllocator_;
 };
 
@@ -227,8 +230,10 @@  int PipelineHandlerVirtual::configure(Camera *camera,
 				      CameraConfiguration *config)
 {
 	VirtualCameraData *data = cameraData(camera);
-	for (auto [i, c] : utils::enumerate(*config))
+	for (auto [i, c] : utils::enumerate(*config)) {
 		c.setStream(&data->streamConfigs_[i].stream);
+		data->streamConfigs_[i].frameGenerator->configure(c.size);
+	}
 
 	return 0;
 }
@@ -264,8 +269,24 @@  void PipelineHandlerVirtual::stopDevice([[maybe_unused]] Camera *camera)
 int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
 					       Request *request)
 {
-	for (auto it : request->buffers())
-		completeBuffer(request, it.second);
+	VirtualCameraData *data = cameraData(camera);
+
+	for (auto const &[stream, buffer] : request->buffers()) {
+		bool found = false;
+		/* map buffer and fill test patterns */
+		for (auto &streamConfig : data->streamConfigs_) {
+			if (stream == &streamConfig.stream) {
+				found = true;
+				if (streamConfig.frameGenerator->generateFrame(
+					    stream->configuration().size, buffer))
+					buffer->_d()->cancel();
+
+				completeBuffer(request, buffer);
+				break;
+			}
+		}
+		ASSERT(found);
+	}
 
 	request->metadata().set(controls::SensorTimestamp, currentTimestamp());
 	completeRequest(request);
@@ -307,11 +328,25 @@  bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator
 
 	const std::string id = "Virtual0";
 	std::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);
+
+	initFrameGenerator(camera.get());
+
 	registerCamera(std::move(camera));
 
 	return true;
 }
 
+void PipelineHandlerVirtual::initFrameGenerator(Camera *camera)
+{
+	auto data = cameraData(camera);
+	for (auto &streamConfig : data->streamConfigs_) {
+		if (data->testPattern_ == TestPattern::DiagonalLines)
+			streamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();
+		else
+			streamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();
+	}
+}
+
 REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, "virtual")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
index 4df70a13..9b79ac09 100644
--- a/src/libcamera/pipeline/virtual/virtual.h
+++ b/src/libcamera/pipeline/virtual/virtual.h
@@ -15,6 +15,8 @@ 
 #include "libcamera/internal/camera.h"
 #include "libcamera/internal/pipeline_handler.h"
 
+#include "test_pattern_generator.h"
+
 namespace libcamera {
 
 class VirtualCameraData : public Camera::Private
@@ -28,6 +30,7 @@  public:
 	};
 	struct StreamConfig {
 		Stream stream;
+		std::unique_ptr<FrameGenerator> frameGenerator;
 	};
 
 	VirtualCameraData(PipelineHandler *pipe,
@@ -35,6 +38,8 @@  public:
 
 	~VirtualCameraData() = default;
 
+	TestPattern testPattern_ = TestPattern::ColorBars;
+
 	const std::vector<Resolution> supportedResolutions_;
 	Size maxResolutionSize_;
 	Size minResolutionSize_;
diff --git a/src/meson.build b/src/meson.build
index 165a77bb..91bea775 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -27,6 +27,25 @@  else
     ipa_sign_module = false
 endif
 
+libyuv_dep = dependency('libyuv', required : false)
+
+# Fallback to a subproject if libyuv isn't found, as it's typically not
+# provided by distributions.
+if not libyuv_dep.found()
+    cmake = import('cmake')
+
+    libyuv_vars = cmake.subproject_options()
+    libyuv_vars.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
+    libyuv_vars.set_override_option('cpp_std', 'c++17')
+    libyuv_vars.append_compile_args('cpp',
+         '-Wno-sign-compare',
+         '-Wno-unused-variable',
+         '-Wno-unused-parameter')
+    libyuv_vars.append_link_args('-ljpeg')
+    libyuv = cmake.subproject('libyuv', options : libyuv_vars)
+    libyuv_dep = libyuv.dependency('yuv')
+endif
+
 # libcamera must be built first as a dependency to the other components.
 subdir('libcamera')