Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libpisp for openSUSE:Factory checked 
in at 2026-09-10 16:40:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libpisp (Old)
 and      /work/SRC/openSUSE:Factory/.libpisp.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libpisp"

Thu Sep 10 16:40:05 2026 rev:3 rq:1376896 version:1.7.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/libpisp/libpisp.changes  2026-06-30 
17:41:17.245544232 +0200
+++ /work/SRC/openSUSE:Factory/.libpisp.new.1265/libpisp.changes        
2026-09-10 16:40:09.351794485 +0200
@@ -1,0 +2,10 @@
+Thu Sep 10 08:15:08 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 1.7.0
+  * gst: Add BGR output format support.
+  * gst: Honour GstVideoMeta strides on input buffers.
+  * gst: Fix R/B channel ordering for RGB output formats.
+  * tests: Add strided input and BGR test cases.
+  * tests: Drop BGR compensation for GStreamer RGB output.
+
+-------------------------------------------------------------------

Old:
----
  libpisp-1.6.0.tar.gz

New:
----
  libpisp-1.7.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libpisp.spec ++++++
--- /var/tmp/diff_new_pack.OcCGOV/_old  2026-09-10 16:40:10.350836396 +0200
+++ /var/tmp/diff_new_pack.OcCGOV/_new  2026-09-10 16:40:10.353836522 +0200
@@ -18,7 +18,7 @@
 %define soversion 1
 
 Name:           libpisp
-Version:        1.6.0
+Version:        1.7.0
 Release:        0
 Summary:        RPi ISP helper library
 Group:          System/Libraries

++++++ libpisp-1.6.0.tar.gz -> libpisp-1.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/libpisp-1.6.0/.github/workflows/camera-test-libpisp.yml 
new/libpisp-1.7.0/.github/workflows/camera-test-libpisp.yml
--- old/libpisp-1.6.0/.github/workflows/camera-test-libpisp.yml 2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/.github/workflows/camera-test-libpisp.yml 2026-08-07 
11:06:54.000000000 +0200
@@ -10,7 +10,7 @@
 jobs:
   trigger-camera-tests:
     runs-on: [self-hosted, camera-test-bridge]
-    environment: ${{ github.event.pull_request.head.repo.full_name == 
github.repository && '' || 'camera-test' }}
+    environment: ${{ github.event.pull_request.head.repo.full_name != 
github.repository && 'camera-test' || '' }}
     timeout-minutes: 120
     steps:
       - name: Checkout camera_tester
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/LICENSES/CC0-1.0.txt 
new/libpisp-1.7.0/LICENSES/CC0-1.0.txt
--- old/libpisp-1.6.0/LICENSES/CC0-1.0.txt      2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/LICENSES/CC0-1.0.txt      1970-01-01 01:00:00.000000000 
+0100
@@ -1,119 +0,0 @@
-Creative Commons Legal Code
-
-CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES
-NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE
-AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
-ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE
-OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS
-LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE 
INFORMATION
-OR WORKS PROVIDED HEREUNDER.
-
-Statement of Purpose
-
-The laws of most jurisdictions throughout the world automatically confer 
exclusive
-Copyright and Related Rights (defined below) upon the creator and subsequent
-owner(s) (each and all, an "owner") of an original work of authorship and/or
-a database (each, a "Work").
-
-Certain owners wish to permanently relinquish those rights to a Work for the
-purpose of contributing to a commons of creative, cultural and scientific
-works ("Commons") that the public can reliably and without fear of later claims
-of infringement build upon, modify, incorporate in other works, reuse and
-redistribute as freely as possible in any form whatsoever and for any purposes,
-including without limitation commercial purposes. These owners may contribute
-to the Commons to promote the ideal of a free culture and the further 
production
-of creative, cultural and scientific works, or to gain reputation or greater
-distribution for their Work in part through the use and efforts of others.
-
-For these and/or other purposes and motivations, and without any expectation
-of additional consideration or compensation, the person associating CC0 with
-a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
-and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
-and publicly distribute the Work under its terms, with knowledge of his or
-her Copyright and Related Rights in the Work and the meaning and intended
-legal effect of CC0 on those rights.
-
-1. Copyright and Related Rights. A Work made available under CC0 may be 
protected
-by copyright and related or neighboring rights ("Copyright and Related 
Rights").
-Copyright and Related Rights include, but are not limited to, the following:
-
-i. the right to reproduce, adapt, distribute, perform, display, communicate,
-and translate a Work;
-
-      ii. moral rights retained by the original author(s) and/or performer(s);
-
-iii. publicity and privacy rights pertaining to a person's image or likeness
-depicted in a Work;
-
-iv. rights protecting against unfair competition in regards to a Work, subject
-to the limitations in paragraph 4(a), below;
-
-v. rights protecting the extraction, dissemination, use and reuse of data
-in a Work;
-
-vi. database rights (such as those arising under Directive 96/9/EC of the
-European Parliament and of the Council of 11 March 1996 on the legal protection
-of databases, and under any national implementation thereof, including any
-amended or successor version of such directive); and
-
-vii. other similar, equivalent or corresponding rights throughout the world
-based on applicable law or treaty, and any national implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention of,
-applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
-unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
-and Related Rights and associated claims and causes of action, whether now
-known or unknown (including existing as well as future claims and causes of
-action), in the Work (i) in all territories worldwide, (ii) for the maximum
-duration provided by applicable law or treaty (including future time 
extensions),
-(iii) in any current or future medium and for any number of copies, and (iv)
-for any purpose whatsoever, including without limitation commercial, 
advertising
-or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the
-benefit of each member of the public at large and to the detriment of 
Affirmer's
-heirs and successors, fully intending that such Waiver shall not be subject
-to revocation, rescission, cancellation, termination, or any other legal or
-equitable action to disrupt the quiet enjoyment of the Work by the public
-as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason be
-judged legally invalid or ineffective under applicable law, then the Waiver
-shall be preserved to the maximum extent permitted taking into account 
Affirmer's
-express Statement of Purpose. In addition, to the extent the Waiver is so
-judged Affirmer hereby grants to each affected person a royalty-free, non
-transferable, non sublicensable, non exclusive, irrevocable and unconditional
-license to exercise Affirmer's Copyright and Related Rights in the Work (i)
-in all territories worldwide, (ii) for the maximum duration provided by 
applicable
-law or treaty (including future time extensions), (iii) in any current or
-future medium and for any number of copies, and (iv) for any purpose 
whatsoever,
-including without limitation commercial, advertising or promotional purposes
-(the "License"). The License shall be deemed effective as of the date CC0
-was applied by Affirmer to the Work. Should any part of the License for any
-reason be judged legally invalid or ineffective under applicable law, such
-partial invalidity or ineffectiveness shall not invalidate the remainder of
-the License, and in such case Affirmer hereby affirms that he or she will
-not (i) exercise any of his or her remaining Copyright and Related Rights
-in the Work or (ii) assert any associated claims and causes of action with
-respect to the Work, in either case contrary to Affirmer's express Statement
-of Purpose.
-
-   4. Limitations and Disclaimers.
-
-a. No trademark or patent rights held by Affirmer are waived, abandoned, 
surrendered,
-licensed or otherwise affected by this document.
-
-b. Affirmer offers the Work as-is and makes no representations or warranties
-of any kind concerning the Work, express, implied, statutory or otherwise,
-including without limitation warranties of title, merchantability, fitness
-for a particular purpose, non infringement, or the absence of latent or other
-defects, accuracy, or the present or absence of errors, whether or not 
discoverable,
-all to the greatest extent permissible under applicable law.
-
-c. Affirmer disclaims responsibility for clearing rights of other persons
-that may apply to the Work or any use thereof, including without limitation
-any person's Copyright and Related Rights in the Work. Further, Affirmer 
disclaims
-responsibility for obtaining any necessary consents, permissions or other
-rights required for any use of the Work.
-
-d. Affirmer understands and acknowledges that Creative Commons is not a party
-to this document and has no duty or obligation with respect to this CC0 or
-use of the Work.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/meson.build 
new/libpisp-1.7.0/meson.build
--- old/libpisp-1.6.0/meson.build       2026-06-29 18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/meson.build       2026-08-07 11:06:54.000000000 +0200
@@ -1,9 +1,9 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 project('libpisp', 'c', 'cpp',
         meson_version : '>= 0.58.0',
-        version : '1.6.0',
+        version : '1.7.0',
         default_options : [
             'werror=true',
             'warning_level=2',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/meson_options.txt 
new/libpisp-1.7.0/meson_options.txt
--- old/libpisp-1.6.0/meson_options.txt 2026-06-29 18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/meson_options.txt 2026-08-07 11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 option('logging', type : 'feature', value : 'auto')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/examples/meson.build 
new/libpisp-1.7.0/src/examples/meson.build
--- old/libpisp-1.6.0/src/examples/meson.build  2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/src/examples/meson.build  2026-08-07 11:06:54.000000000 
+0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2025, Raspberry Pi Ltd
 
 opts_dep = dependency('cxxopts', fallback : ['cxxopts', 'cxxopts_dep'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/gst/gstpispconvert.cpp 
new/libpisp-1.7.0/src/gst/gstpispconvert.cpp
--- old/libpisp-1.6.0/src/gst/gstpispconvert.cpp        2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/gst/gstpispconvert.cpp        2026-08-07 
11:06:54.000000000 +0200
@@ -32,7 +32,7 @@
 #define GST_CAT_DEFAULT gst_pisp_convert_debug
 
 /* Supported GStreamer formats */
-#define PISP_FORMATS "{ RGB, RGBx, BGRx, I420, YV12, Y42B, Y444, YUY2, UYVY, 
NV12, NV12_128C8, NV12_10LE32_128C8 }"
+#define PISP_FORMATS "{ RGB, BGR, RGBx, BGRx, I420, YV12, Y42B, Y444, YUY2, 
UYVY, NV12, NV12_128C8, NV12_10LE32_128C8 }"
 /* Supported DRM fourccs */
 #define PISP_DRM_FORMATS                                                       
                                        \
        "{ RG24, XB24, XR24, YU12, YV12, YU16, YU24, YUYV, UYVY, NV12, 
NV12:0x0700000000000004, P030:0x0700000000000004 }"
@@ -66,8 +66,9 @@
 /* Bidirectional mapping between GstVideoFormat and PiSP format strings */
 static const std::map<GstVideoFormat, std::string> gst_pisp_format_map = {
        { GST_VIDEO_FORMAT_RGB, "RGB888" },
+       { GST_VIDEO_FORMAT_BGR, "RGB888" },
        { GST_VIDEO_FORMAT_RGBx, "RGBX8888" },
-       { GST_VIDEO_FORMAT_BGRx, "XRGB8888" },
+       { GST_VIDEO_FORMAT_BGRx, "RGBX8888" },
        { GST_VIDEO_FORMAT_I420, "YUV420P" },
        { GST_VIDEO_FORMAT_YV12, "YVU420P" },
        { GST_VIDEO_FORMAT_Y42B, "YUV422P" },
@@ -84,7 +85,7 @@
        { "RG24", "RGB888" },
        { "BG24", "RGB888" },
        { "XB24", "RGBX8888" },
-       { "XR24", "XRGB8888" },
+       { "XR24", "RGBX8888" },
        { "YU12", "YUV420P" },
        { "YV12", "YVU420P" },
        { "YU16", "YUV422P" },
@@ -96,20 +97,33 @@
        { "P030:0x0700000000000004", "YUV420SP10_COL128" },
 };
 
+/* Formats storing channels in B,G,R memory order need an R/B swap relative to
+ * the hardware's native R,G,B channel output. Keyed on the GStreamer/DRM
+ * format as the PiSP format string does not encode the channel order. */
+static bool require_rb_swap(GstVideoFormat format)
+{
+       return format == GST_VIDEO_FORMAT_BGR || format == 
GST_VIDEO_FORMAT_BGRx;
+}
+
+static bool require_rb_swap(const gchar *drm_format)
+{
+       return drm_format && (g_str_equal(drm_format, "XR24") || 
g_str_equal(drm_format, "RG24"));
+}
+
 static const char *gst_format_to_pisp(GstVideoFormat format)
 {
        auto it = gst_pisp_format_map.find(format);
        return it != gst_pisp_format_map.end() ? it->second.c_str() : nullptr;
 }
 
-static GstVideoFormat pisp_to_gst_video_format(const char *pisp_format)
+static GstVideoFormat pisp_to_gst_video_format(const char *pisp_format, bool 
rb_swap)
 {
        if (!pisp_format)
                return GST_VIDEO_FORMAT_UNKNOWN;
 
        for (const auto &[gst_fmt, pisp_fmt] : gst_pisp_format_map)
        {
-               if (g_str_equal(pisp_format, pisp_fmt.c_str()))
+               if (g_str_equal(pisp_format, pisp_fmt.c_str()) && rb_swap == 
require_rb_swap(gst_fmt))
                        return gst_fmt;
        }
        return GST_VIDEO_FORMAT_UNKNOWN;
@@ -153,7 +167,7 @@
 
 /* Configure colour space conversion blocks for the backend */
 static uint32_t configure_colour_conversion(libpisp::BackEnd *backend, const 
char *in_format, const char *in_colorspace,
-                                                                               
        const char *out_format, const char *out_colorspace,
+                                                                               
        const char *out_format, bool out_rb_swap, const char *out_colorspace,
                                                                                
        unsigned int output_index)
 {
        uint32_t rgb_enables = 0;
@@ -175,8 +189,7 @@
                backend->SetCsc(output_index, csc);
                rgb_enables |= PISP_BE_RGB_ENABLE_CSC(output_index);
        }
-       else if (g_str_equal(out_format, "RGB888") || g_str_equal(out_format, 
"RGBX8888") ||
-                        g_str_equal(out_format, "XRGB8888"))
+       else if (out_rb_swap)
        {
                /* R/B channel swap to match GStreamer/DRM byte ordering */
                pisp_be_ccm_config csc = {};
@@ -363,6 +376,11 @@
        self->priv->configured = FALSE;
        self->priv->dmabuf_allocator = gst_dmabuf_allocator_new();
        self->priv->use_dmabuf_input = FALSE;
+       self->priv->in_has_meta = FALSE;
+       self->priv->in_n_mem = 0;
+       self->priv->in_meta_stride = { 0, 0, 0 };
+       self->priv->in_meta_offset = { 0, 0, 0 };
+       self->priv->force_memcpy_input = FALSE;
 
        for (unsigned int i = 0; i < PISP_NUM_OUTPUTS; i++)
        {
@@ -371,6 +389,7 @@
                self->priv->out_stride[i] = 0;
                self->priv->out_hw_stride[i] = 0;
                self->priv->out_format[i] = nullptr;
+               self->priv->out_rb_swap[i] = false;
                self->priv->output_enabled[i] = FALSE;
                self->priv->use_dmabuf_output[i] = FALSE;
                self->priv->output_pool[i] = nullptr;
@@ -447,6 +466,7 @@
                gst_structure_get_int(out_structure, "width", (gint 
*)&self->priv->out_width[index]);
                gst_structure_get_int(out_structure, "height", (gint 
*)&self->priv->out_height[index]);
                self->priv->out_format[index] = drm_format_to_pisp(drm_format);
+               self->priv->out_rb_swap[index] = require_rb_swap(drm_format);
                self->priv->out_stride[index] = 0;
 
                GstVideoColorimetry colorimetry = {};
@@ -470,6 +490,7 @@
                self->priv->out_height[index] = 
GST_VIDEO_INFO_HEIGHT(&out_info);
                self->priv->out_stride[index] = 
GST_VIDEO_INFO_PLANE_STRIDE(&out_info, 0);
                self->priv->out_format[index] = 
gst_format_to_pisp(GST_VIDEO_INFO_FORMAT(&out_info));
+               self->priv->out_rb_swap[index] = 
require_rb_swap(GST_VIDEO_INFO_FORMAT(&out_info));
                self->priv->out_colorspace[index] = 
colorimetry_to_pisp(&GST_VIDEO_INFO_COLORIMETRY(&out_info));
                GST_INFO_OBJECT(self, "Output%u format: pisp=%s, colorspace=%s 
(matrix=%d, range=%d)", index,
                                                self->priv->out_format[index], 
self->priv->out_colorspace[index],
@@ -637,9 +658,10 @@
 }
 
 /* Attach GstVideoMeta with the correct hardware stride to a dmabuf output 
buffer */
-static void add_video_meta(GstBuffer *buffer, const char *pisp_format, guint 
width, guint height, guint hw_stride)
+static void add_video_meta(GstBuffer *buffer, const char *pisp_format, bool 
rb_swap, guint width, guint height,
+                                                  guint hw_stride)
 {
-       GstVideoFormat gst_fmt = pisp_to_gst_video_format(pisp_format);
+       GstVideoFormat gst_fmt = pisp_to_gst_video_format(pisp_format, rb_swap);
        if (gst_fmt == GST_VIDEO_FORMAT_UNKNOWN)
                return;
 
@@ -664,16 +686,16 @@
                                                                   strides);
 }
 
-static void copy_planes(std::array<uint8_t *, 3> src, guint src_stride, 
std::array<uint8_t *, 3> dst, guint dst_stride,
-                                               guint width, guint height, 
const char *format)
+static void copy_planes(std::array<uint8_t *, 3> src, std::array<guint, 3> 
src_stride, std::array<uint8_t *, 3> dst,
+                                               std::array<guint, 3> 
dst_stride, guint width, guint height, const char *format)
 {
-       GST_DEBUG("copy_planes: %ux%u, src_stride=%u, dst_stride=%u, 
format=%s", width, height, src_stride, dst_stride,
-                         format);
+       GST_DEBUG("copy_planes: %ux%u, src_stride=%u, dst_stride=%u, 
format=%s", width, height, src_stride[0],
+                         dst_stride[0], format);
 
        /* YUV420SP_COL128 (NV12 column 128) - special tiled format */
        if (strncmp(format, "YUV420SP_COL128", 15) == 0 || strncmp(format, 
"YUV420SP10_COL128", 17) == 0)
        {
-               guint y_size = GST_VIDEO_TILE_X_TILES(src_stride) * 128 * 
GST_VIDEO_TILE_Y_TILES(src_stride) * 8;
+               guint y_size = GST_VIDEO_TILE_X_TILES(src_stride[0]) * 128 * 
GST_VIDEO_TILE_Y_TILES(src_stride[0]) * 8;
                memcpy(dst[0], src[0], y_size);
 
                uint8_t *src_uv = src[1] ? src[1] : src[0] + y_size;
@@ -682,12 +704,31 @@
                return;
        }
 
+       /* Semi-planar YUV formats: interleaved UV plane at half height 
(YUV420SP) */
+       if (is_yuv_format(format) && strstr(format, "SP") != nullptr)
+       {
+               /* Copy Y plane line by line */
+               for (guint y = 0; y < height; ++y)
+                       memcpy(dst[0] + y * dst_stride[0], src[0] + y * 
src_stride[0], width);
+
+               guint src_uv_stride = src_stride[1] ? src_stride[1] : 
src_stride[0];
+               guint dst_uv_stride = dst_stride[1] ? dst_stride[1] : 
dst_stride[0];
+
+               /* Calculate plane pointers if not explicitly provided (single 
contiguous buffer) */
+               uint8_t *src_uv = src[1] ? src[1] : src[0] + src_stride[0] * 
height;
+               uint8_t *dst_uv = dst[1] ? dst[1] : dst[0] + dst_stride[0] * 
height;
+
+               for (guint y = 0; y < height / 2; ++y)
+                       memcpy(dst_uv + y * dst_uv_stride, src_uv + y * 
src_uv_stride, width);
+               return;
+       }
+
        /* Planar YUV formats: YUV420P, YVU420P, YUV422P, YUV444P */
        if (is_yuv_format(format) && strstr(format, "P") != nullptr)
        {
                /* Copy Y plane line by line */
                for (guint y = 0; y < height; ++y)
-                       memcpy(dst[0] + y * dst_stride, src[0] + y * 
src_stride, width);
+                       memcpy(dst[0] + y * dst_stride[0], src[0] + y * 
src_stride[0], width);
 
                /* Determine UV subsampling */
                guint uv_width, uv_height;
@@ -707,13 +748,13 @@
                        uv_height = height;
                }
 
-               guint src_uv_stride = (uv_width == width) ? src_stride : 
src_stride / 2;
-               guint dst_uv_stride = (uv_width == width) ? dst_stride : 
dst_stride / 2;
+               guint src_uv_stride = src_stride[1] ? src_stride[1] : (uv_width 
== width) ? src_stride[0] : src_stride[0] / 2;
+               guint dst_uv_stride = dst_stride[1] ? dst_stride[1] : (uv_width 
== width) ? dst_stride[0] : dst_stride[0] / 2;
 
                /* Calculate plane pointers if not explicitly provided (single 
contiguous buffer) */
-               uint8_t *src_u = src[1] ? src[1] : src[0] + src_stride * height;
+               uint8_t *src_u = src[1] ? src[1] : src[0] + src_stride[0] * 
height;
                uint8_t *src_v = src[2] ? src[2] : src_u + src_uv_stride * 
uv_height;
-               uint8_t *dst_u = dst[1] ? dst[1] : dst[0] + dst_stride * height;
+               uint8_t *dst_u = dst[1] ? dst[1] : dst[0] + dst_stride[0] * 
height;
                uint8_t *dst_v = dst[2] ? dst[2] : dst_u + dst_uv_stride * 
uv_height;
 
                /* Copy U and V planes */
@@ -730,19 +771,22 @@
        guint line_stride = width * bytes_per_pixel;
 
        for (guint y = 0; y < height; ++y)
-               memcpy(dst[0] + y * dst_stride, src[0] + y * src_stride, 
line_stride);
+               memcpy(dst[0] + y * dst_stride[0], src[0] + y * src_stride[0], 
line_stride);
 }
 
 static void copy_buffer_to_pisp(GstBuffer *gstbuf, std::array<uint8_t *, 3> 
&mem, guint width, guint height,
-                                                               guint 
gst_stride, guint hw_stride, const char *format)
+                                                               const 
std::array<guint, 3> &gst_stride, const std::array<gsize, 3> &gst_offset,
+                                                               guint 
hw_stride, const char *format)
 {
        GstMapInfo map;
        gst_buffer_map(gstbuf, &map, GST_MAP_READ);
 
-       /* GstBuffer is always contiguous - planes calculated from offsets */
-       std::array<uint8_t *, 3> src = { map.data, nullptr, nullptr };
+       /* Plane pointers from the GstVideoMeta offsets; zero offsets leave the
+        * pointer unset so copy_planes derives a contiguous layout. */
+       std::array<uint8_t *, 3> src = { map.data + gst_offset[0], 
gst_offset[1] ? map.data + gst_offset[1] : nullptr,
+                                                                        
gst_offset[2] ? map.data + gst_offset[2] : nullptr };
 
-       copy_planes(src, gst_stride, mem, hw_stride, width, height, format);
+       copy_planes(src, gst_stride, mem, { hw_stride, 0, 0 }, width, height, 
format);
 
        gst_buffer_unmap(gstbuf, &map);
 }
@@ -756,12 +800,47 @@
        /* GstBuffer is always contiguous - planes calculated from offsets */
        std::array<uint8_t *, 3> dst = { map.data, nullptr, nullptr };
 
-       copy_planes(const_cast<std::array<uint8_t *, 3> &>(mem), hw_stride, 
dst, gst_stride, width, height, format);
+       copy_planes(const_cast<std::array<uint8_t *, 3> &>(mem), { hw_stride, 
0, 0 }, dst, { gst_stride, 0, 0 }, width,
+                               height, format);
 
        gst_buffer_unmap(gstbuf, &map);
 }
 
 /*
+ * Check whether the input buffer layout described by the GstVideoMeta can be
+ * consumed directly by the hardware. Zero-copy needs hardware-aligned strides
+ * and, for a single memory block, planes contiguous at those strides (the
+ * V4L2 device derives plane offsets from the configured stride and height).
+ */
+static gboolean zero_copy_layout_ok(GstPispConvert *self)
+{
+       const std::array<guint, 3> &stride = self->priv->in_meta_stride;
+       const std::array<gsize, 3> &offset = self->priv->in_meta_offset;
+       const char *format = self->priv->in_format;
+       guint height = self->priv->in_height;
+
+       if (offset[0] != 0 || stride[0] % PISP_BACK_END_OUTPUT_MIN_ALIGN)
+               return FALSE;
+
+       /* Per-plane memory blocks: offsets fall on memory boundaries */
+       if (self->priv->in_n_mem > 1)
+               return TRUE;
+
+       if (is_yuv_format(format) && strstr(format, "SP") != nullptr)
+               return offset[1] == (gsize)stride[0] * height;
+
+       if (is_yuv_format(format) && strstr(format, "P") != nullptr)
+       {
+               guint uv_height = strstr(format, "420") != nullptr ? height / 2 
: height;
+               return stride[1] == stride[2] && offset[1] == (gsize)stride[0] 
* height &&
+                          offset[2] == offset[1] + (gsize)stride[1] * 
uv_height;
+       }
+
+       /* Packed single-plane formats */
+       return TRUE;
+}
+
+/*
  * Configure the PiSP backend for the current input/output settings.
  */
 static gboolean gst_pisp_convert_configure(GstPispConvert *self)
@@ -847,7 +926,30 @@
                        GST_ERROR_OBJECT(self, "Failed to get input format");
                        return FALSE;
                }
-               libpisp::compute_stride(input_cfg);
+
+               /* For zero-copy input the hardware must read with the 
producer's
+                * actual strides (from GstVideoMeta) rather than our computed 
ones.
+                * COL128 formats keep their tile-encoded stride semantics. */
+               self->priv->force_memcpy_input = FALSE;
+               gboolean col128 = strstr(self->priv->in_format, "COL128") != 
nullptr;
+
+               if (self->priv->use_dmabuf_input && self->priv->in_has_meta && 
!col128)
+               {
+                       if (zero_copy_layout_ok(self))
+                       {
+                               input_cfg.stride = 
self->priv->in_meta_stride[0];
+                               input_cfg.stride2 = 
self->priv->in_meta_stride[1];
+                       }
+                       else
+                       {
+                               GST_INFO_OBJECT(self, "Input buffer layout 
unsuitable for zero-copy, using memcpy path");
+                               self->priv->force_memcpy_input = TRUE;
+                               libpisp::compute_stride(input_cfg);
+                       }
+               }
+               else
+                       libpisp::compute_stride(input_cfg);
+
                self->priv->in_hw_stride = input_cfg.stride;
                self->priv->backend->SetInputFormat(input_cfg);
 
@@ -889,7 +991,8 @@
 
                        global.rgb_enables |= 
configure_colour_conversion(self->priv->backend.get(), self->priv->in_format,
                                                                                
                                          self->priv->in_colorspace, 
self->priv->out_format[i],
-                                                                               
                                          self->priv->out_colorspace[i], i);
+                                                                               
                                          self->priv->out_rb_swap[i], 
self->priv->out_colorspace[i],
+                                                                               
                                          i);
 
                        GST_INFO_OBJECT(self, "Output%d: %ux%u %s (stride: 
gst=%u hw=%u) colorspace %s", i,
                                                        
self->priv->out_width[i], self->priv->out_height[i], self->priv->out_format[i],
@@ -1031,6 +1134,38 @@
        GstFlowReturn ret = GST_FLOW_OK;
        GstBuffer *outbuf[PISP_NUM_OUTPUTS] = { nullptr, nullptr };
 
+       /* Pick up the buffer's actual plane layout from GstVideoMeta, falling
+        * back to the caps-derived stride with contiguous planes. */
+       GstVideoMeta *meta = gst_buffer_get_video_meta(inbuf);
+       std::array<guint, 3> meta_stride = { self->priv->in_stride, 0, 0 };
+       std::array<gsize, 3> meta_offset = { 0, 0, 0 };
+       if (meta)
+       {
+               for (guint p = 0; p < MIN(meta->n_planes, 3u); p++)
+               {
+                       meta_stride[p] = meta->stride[p];
+                       meta_offset[p] = meta->offset[p];
+               }
+               GST_LOG_OBJECT(self,
+                                          "Input meta: planes=%u 
strides=%u/%u/%u offsets=%" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT
+                                          "/%" G_GSIZE_FORMAT " buffer size=%" 
G_GSIZE_FORMAT " n_mem=%u",
+                                          meta->n_planes, meta_stride[0], 
meta_stride[1], meta_stride[2], meta_offset[0], meta_offset[1],
+                                          meta_offset[2], 
gst_buffer_get_size(inbuf), gst_buffer_n_memory(inbuf));
+       }
+
+       /* The zero-copy input configuration depends on the buffer layout */
+       if (self->priv->configured && self->priv->use_dmabuf_input &&
+               (meta_stride != self->priv->in_meta_stride || meta_offset != 
self->priv->in_meta_offset))
+       {
+               GST_INFO_OBJECT(self, "Input buffer layout changed, 
reconfiguring");
+               self->priv->configured = FALSE;
+       }
+
+       self->priv->in_has_meta = meta != nullptr;
+       self->priv->in_meta_stride = meta_stride;
+       self->priv->in_meta_offset = meta_offset;
+       self->priv->in_n_mem = gst_buffer_n_memory(inbuf);
+
        /* Configure on first buffer if not already configured */
        if (!self->priv->configured)
        {
@@ -1088,7 +1223,7 @@
                slice.emplace(node_name, buffers[index]);
 
        /* Prepare input: copy to slice buffer (memcpy path) or get dmabuf 
(zero-copy path) */
-       if (input_is_dmabuf && self->priv->use_dmabuf_input)
+       if (input_is_dmabuf && self->priv->use_dmabuf_input && 
!self->priv->force_memcpy_input)
        {
                std::optional<Buffer> dmabuf_input = 
gst_to_libpisp_buffer(inbuf);
                if (!dmabuf_input)
@@ -1105,8 +1240,8 @@
                Buffer::Sync s(slice.at("pispbe-input"), 
Buffer::Sync::Access::ReadWrite);
                const auto &mem = s.Get();
                copy_buffer_to_pisp(inbuf, const_cast<std::array<uint8_t *, 3> 
&>(mem), self->priv->in_width,
-                                                       self->priv->in_height, 
self->priv->in_stride, self->priv->in_hw_stride,
-                                                       self->priv->in_format);
+                                                       self->priv->in_height, 
self->priv->in_meta_stride, self->priv->in_meta_offset,
+                                                       
self->priv->in_hw_stride, self->priv->in_format);
                GST_DEBUG_OBJECT(self, "Using memcpy input path");
        }
 
@@ -1135,8 +1270,8 @@
                                goto cleanup;
                        }
 
-                       add_video_meta(outbuf[i], self->priv->out_format[i], 
self->priv->out_width[i], self->priv->out_height[i],
-                                                  
self->priv->out_hw_stride[i]);
+                       add_video_meta(outbuf[i], self->priv->out_format[i], 
self->priv->out_rb_swap[i], self->priv->out_width[i],
+                                                  self->priv->out_height[i], 
self->priv->out_hw_stride[i]);
 
                        GST_DEBUG_OBJECT(self, "Using zero-copy output%d path", 
i);
                }
@@ -1279,6 +1414,8 @@
        self->priv->media_dev_path = nullptr;
        self->priv->configured = FALSE;
        self->priv->use_dmabuf_input = FALSE;
+       self->priv->in_has_meta = FALSE;
+       self->priv->force_memcpy_input = FALSE;
 
        return TRUE;
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/gst/gstpispconvert.h 
new/libpisp-1.7.0/src/gst/gstpispconvert.h
--- old/libpisp-1.6.0/src/gst/gstpispconvert.h  2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/src/gst/gstpispconvert.h  2026-08-07 11:06:54.000000000 
+0200
@@ -7,6 +7,7 @@
 
 #pragma once
 
+#include <array>
 #include <gst/gst.h>
 #include <map>
 #include <memory>
@@ -74,6 +75,13 @@
        const char *in_format;
        const char *in_colorspace;
 
+       /* Input buffer layout from GstVideoMeta (falls back to caps-derived 
stride) */
+       gboolean in_has_meta;
+       guint in_n_mem;
+       std::array<guint, 3> in_meta_stride;
+       std::array<gsize, 3> in_meta_offset;
+       gboolean force_memcpy_input; // Buffer layout unsuitable for zero-copy
+
        /* Output format info - arrays for dual outputs */
        guint out_width[PISP_NUM_OUTPUTS];
        guint out_height[PISP_NUM_OUTPUTS];
@@ -81,6 +89,7 @@
        guint out_hw_stride[PISP_NUM_OUTPUTS]; // Hardware buffer stride
        const char *out_format[PISP_NUM_OUTPUTS];
        const char *out_colorspace[PISP_NUM_OUTPUTS];
+       bool out_rb_swap[PISP_NUM_OUTPUTS]; // Memory stores B,G,R channel order
        gboolean output_enabled[PISP_NUM_OUTPUTS]; // Track which outputs are 
active
 
        /* dmabuf support */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/gst/meson.build 
new/libpisp-1.7.0/src/gst/meson.build
--- old/libpisp-1.6.0/src/gst/meson.build       2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/src/gst/meson.build       2026-08-07 11:06:54.000000000 
+0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2026, Raspberry Pi Ltd
 
 gst_dep = dependency('gstreamer-1.0', version : '>= 1.14', required : 
get_option('gstreamer'))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/helpers/meson.build 
new/libpisp-1.7.0/src/helpers/meson.build
--- old/libpisp-1.6.0/src/helpers/meson.build   2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/src/helpers/meson.build   2026-08-07 11:06:54.000000000 
+0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2025, Raspberry Pi Ltd
 
 pisp_sources += files([
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/libpisp/backend/meson.build 
new/libpisp-1.7.0/src/libpisp/backend/meson.build
--- old/libpisp-1.6.0/src/libpisp/backend/meson.build   2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/libpisp/backend/meson.build   2026-08-07 
11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 config_install_dir = get_option('prefix') / get_option('datadir') / 'libpisp'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/libpisp/backend/tiling/meson.build 
new/libpisp-1.7.0/src/libpisp/backend/tiling/meson.build
--- old/libpisp-1.6.0/src/libpisp/backend/tiling/meson.build    2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/libpisp/backend/tiling/meson.build    2026-08-07 
11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 backend_sources += files([
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/libpisp/common/meson.build 
new/libpisp-1.7.0/src/libpisp/common/meson.build
--- old/libpisp-1.6.0/src/libpisp/common/meson.build    2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/libpisp/common/meson.build    2026-08-07 
11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 pisp_sources += files([
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/libpisp/frontend/meson.build 
new/libpisp-1.7.0/src/libpisp/frontend/meson.build
--- old/libpisp-1.6.0/src/libpisp/frontend/meson.build  2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/libpisp/frontend/meson.build  2026-08-07 
11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 frontend_sources = files([
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/libpisp/meson.build 
new/libpisp-1.7.0/src/libpisp/meson.build
--- old/libpisp-1.6.0/src/libpisp/meson.build   2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/src/libpisp/meson.build   2026-08-07 11:06:54.000000000 
+0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 subdir('common')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/libpisp/variants/meson.build 
new/libpisp-1.7.0/src/libpisp/variants/meson.build
--- old/libpisp-1.6.0/src/libpisp/variants/meson.build  2026-06-29 
18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/libpisp/variants/meson.build  2026-08-07 
11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 pisp_sources += files([
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/src/meson.build 
new/libpisp-1.7.0/src/meson.build
--- old/libpisp-1.6.0/src/meson.build   2026-06-29 18:07:15.000000000 +0200
+++ new/libpisp-1.7.0/src/meson.build   2026-08-07 11:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-# SPDX-License-Identifier: CC0-1.0
+# SPDX-License-Identifier: BSD-2-Clause
 # Copyright (C) 2023, Raspberry Pi Ltd
 
 pisp_sources = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libpisp-1.6.0/utils/test_convert.py 
new/libpisp-1.7.0/utils/test_convert.py
--- old/libpisp-1.6.0/utils/test_convert.py     2026-06-29 18:07:15.000000000 
+0200
+++ new/libpisp-1.7.0/utils/test_convert.py     2026-08-07 11:06:54.000000000 
+0200
@@ -39,10 +39,10 @@
         self.test_cases = [
             {
                 "input_file": "conv_yuv420_4056x3040_4056s.yuv",
-                "output_file": "out_4056x3050_12168s_rgb888.rgb",
+                "output_file": "out_4056x3040_12168s_rgb888.rgb",
                 "input_format": "4056:3040:4056:YUV420P",
                 "output_format": "4056:3040:12168:RGB888",
-                "reference_file": "ref_4056x3050_12168s_rgb888.rgb",
+                "reference_file": "ref_4056x3040_12168s_rgb888.rgb",
                 "skip_gst": False,
             },
             {
@@ -61,6 +61,61 @@
                 "reference_file": "ref_4000x3000_4032s.yuv",
                 "skip_gst": True,
             },
+            # Strided inputs: the harness rewrites the input with padded rows,
+            # passing the strides to rawvideoparse (exercising pispconvert's
+            # GstVideoMeta stride handling) or to the convert utility via the
+            # format string. Convert mode is skipped automatically if the
+            # strides do not follow its luma/chroma derivation rules.
+            {
+                "input_file": "conv_yuv420_4056x3040_4056s.yuv",
+                "output_file": "out_4056x3040_strided_rgb888.rgb",
+                "input_format": "4056:3040:4056:YUV420P",
+                "output_format": "4056:3040:12168:RGB888",
+                "input_strides": [4160, 2080, 2080],
+                "reference_file": "ref_4056x3040_12168s_rgb888.rgb",
+            },
+            {
+                "input_file": "conv_800x600_1200s_422_yuyv.yuv",
+                "output_file": "out_1600x1200_strided_422p.yuv",
+                "input_format": "800:600:1600:YUYV",
+                "output_format": "1600:1200:1600:YUV422P",
+                "input_strides": [1728],
+                "reference_file": "ref_1600x1200_1600_422p.yuv",
+            },
+            # RGB input with its native padded stride (2432 vs 800*3=2400).
+            # The packed-stride output reference allows this to run in both
+            # modes, unlike the 4032-stride variant above.
+            {
+                "input_file": "conv_rgb888_800x600_2432s.rgb",
+                "output_file": "out_4000x3000_4000s.yuv",
+                "input_format": "800:600:2432:RGB888",
+                "output_format": "4000:3000:4000:YUV444P",
+                "input_strides": [2432],
+                "reference_file": "ref_4000x3000_4000s.yuv",
+            },
+            # BGR output variants exercise the pispconvert R/B swap CSC.
+            # videoconvert repacks the file back to RGB so the RGB888
+            # references can be reused; a wrong channel order in the element
+            # shows up as a swapped file. GStreamer only.
+            {
+                "input_file": "conv_yuv420_4056x3040_4056s.yuv",
+                "output_file": "out_4056x3040_bgr_rgb888.rgb",
+                "input_format": "4056:3040:4056:YUV420P",
+                "output_format": "4056:3040:12168:BGR",
+                "file_format": "RGB",
+                "reference_file": "ref_4056x3040_12168s_rgb888.rgb",
+                "skip_convert": True,
+            },
+            {
+                "input_file": "conv_yuv420_4056x3040_4056s.yuv",
+                "output_file": "out_4056x3040_strided_bgr_rgb888.rgb",
+                "input_format": "4056:3040:4056:YUV420P",
+                "output_format": "4056:3040:12168:BGR",
+                "input_strides": [4160, 2080, 2080],
+                "file_format": "RGB",
+                "reference_file": "ref_4056x3040_12168s_rgb888.rgb",
+                "skip_convert": True,
+            },
             # Add more test cases here as needed
         ]
 
@@ -89,7 +144,64 @@
         }
         return format_map.get(pisp_format, pisp_format)
 
-    def run_gstreamer(self, input_file, output_file, input_format, 
output_format):
+    def _plane_geometry(self, fmt):
+        """Per-plane (rows, row_bytes, stride) for a parsed format dict."""
+        w, h, s = fmt["width"], fmt["height"], fmt["stride"]
+        name = fmt["format"]
+        if name in ("YUV420P", "YVU420P"):
+            return [(h, w, s), (h // 2, w // 2, s // 2), (h // 2, w // 2, s // 
2)]
+        if name == "YUV422P":
+            return [(h, w, s), (h, w // 2, s // 2), (h, w // 2, s // 2)]
+        if name == "YUV444P":
+            return [(h, w, s), (h, w, s), (h, w, s)]
+        if name in ("YUYV", "UYVY"):
+            return [(h, w * 2, s)]
+        if name == "RGB888":
+            return [(h, w * 3, s)]
+        raise ValueError(f"Unsupported format for strided input: {name}")
+
+    def _strided_input_path(self, input_file):
+        """Output-dir path for the strided rewrite of an input file."""
+        base = os.path.basename(input_file).removeprefix("conv_")
+        return os.path.join(self.output_dir or ".", "conv_strided_" + base)
+
+    def _make_strided_input(self, src_path, in_fmt, strides, dst_path):
+        """Rewrite src_path with the given per-plane strides, zero-padding each
+        row (including a final row the source file may have left unpadded).
+        Returns the plane offsets of the new file."""
+        planes = self._plane_geometry(in_fmt)
+        if len(strides) != len(planes):
+            raise ValueError("gst_input_strides needs one stride per plane")
+
+        with open(src_path, "rb") as f:
+            data = f.read()
+
+        out = bytearray()
+        offsets = []
+        pos = 0
+        for (rows, row_bytes, src_stride), dst_stride in zip(planes, strides):
+            if dst_stride < row_bytes:
+                raise ValueError(
+                    f"Stride {dst_stride} smaller than row size {row_bytes}"
+                )
+            offsets.append(len(out))
+            for _ in range(rows):
+                row = data[pos : pos + row_bytes]
+                out += row + b"\x00" * (dst_stride - len(row))
+                pos += src_stride
+        with open(dst_path, "wb") as f:
+            f.write(out)
+        return offsets
+
+    def run_gstreamer(
+        self,
+        input_file,
+        output_file,
+        input_format,
+        output_format,
+        input_strides=None,
+        file_format=None,
+    ):
         """Run GStreamer pipeline with pispconvert."""
         # Use input directory if specified
         if self.input_dir:
@@ -106,8 +218,20 @@
         # Convert to GStreamer format names
         gst_in_format = self._pisp_to_gst_format(in_fmt["format"])
         gst_out_format = self._pisp_to_gst_format(out_fmt["format"])
-        # pispconvert swaps R/B for RGB, use BGR file output to match convert 
reference
-        gst_file_format = "BGR" if gst_out_format == "RGB" else gst_out_format
+
+        # Rewrite the input with explicit strides and tell rawvideoparse about
+        # them, so pispconvert receives buffers with non-default GstVideoMeta
+        parse_props = []
+        if input_strides:
+            strided_file = self._strided_input_path(input_file)
+            offsets = self._make_strided_input(
+                input_file, in_fmt, input_strides, strided_file
+            )
+            input_file = strided_file
+            parse_props = [
+                f"plane-strides=<{','.join(str(s) for s in input_strides)}>",
+                f"plane-offsets=<{','.join(str(o) for o in offsets)}>",
+            ]
 
         # Build GStreamer pipeline
         pipeline = [
@@ -120,16 +244,31 @@
             f"height={in_fmt['height']}",
             f"format={gst_in_format.lower()}",
             "framerate=30/1",
-            "!",
-            "video/x-raw,colorimetry=1:4:0:0",
+            *parse_props,
+        ]
+
+        # BT.601 colorimetry is only valid for YUV inputs; RGB keeps the
+        # rawvideoparse default (identity matrix)
+        if not in_fmt["format"].startswith("RGB"):
+            pipeline += ["!", "video/x-raw,colorimetry=1:4:0:0"]
+
+        pipeline += [
             "!",
             "pispconvert",
             "!",
             
f"video/x-raw,format={gst_out_format},width={out_fmt['width']},height={out_fmt['height']},colorimetry=1:4:0:0",
-            "!",
-            "videoconvert",
-            "!",
-            
f"video/x-raw,format={gst_file_format},width={out_fmt['width']},height={out_fmt['height']}",
+        ]
+
+        # Repack to the requested file format so a common reference can be used
+        if file_format and file_format != gst_out_format:
+            pipeline += [
+                "!",
+                "videoconvert",
+                "!",
+                
f"video/x-raw,format={file_format},width={out_fmt['width']},height={out_fmt['height']}",
+            ]
+
+        pipeline += [
             "!",
             "filesink",
             f"location={output_file}",
@@ -156,7 +295,9 @@
             print(f"stderr: {e.stderr}")
             return False
 
-    def run_convert(self, input_file, output_file, input_format, 
output_format):
+    def run_convert(
+        self, input_file, output_file, input_format, output_format, 
input_strides=None
+    ):
         """Run the convert utility with the specified parameters."""
         # Use input directory if specified
         if self.input_dir:
@@ -166,6 +307,18 @@
         if self.output_dir:
             output_file = os.path.join(self.output_dir, output_file)
 
+        # Rewrite the input with explicit strides and adjust the format
+        # string accordingly (the caller has checked expressibility)
+        if input_strides:
+            in_fmt = self._parse_format(input_format)
+            strided_file = self._strided_input_path(input_file)
+            self._make_strided_input(input_file, in_fmt, input_strides, 
strided_file)
+            input_file = strided_file
+            input_format = (
+                f"{in_fmt['width']}:{in_fmt['height']}:"
+                f"{input_strides[0]}:{in_fmt['format']}"
+            )
+
         cmd = [
             self.convert_binary,
             input_file,
@@ -247,6 +400,23 @@
             print("SKIPPED: Test case marked as skip_gst=True")
             return None  # Return None to indicate skipped
 
+        # Skip convert test if marked to skip
+        if not self.use_gstreamer and test_case.get("skip_convert", False):
+            print("SKIPPED: Test case marked as skip_convert=True")
+            return None  # Return None to indicate skipped
+
+        input_strides = test_case.get("input_strides")
+
+        # The convert utility derives chroma strides from the luma stride, so
+        # skip strides its format string cannot express
+        if not self.use_gstreamer and input_strides:
+            in_fmt = self._parse_format(test_case["input_format"])
+            in_fmt["stride"] = input_strides[0]
+            expected = [plane[2] for plane in self._plane_geometry(in_fmt)]
+            if input_strides != expected:
+                print("SKIPPED: strides not expressible by the convert 
utility")
+                return None
+
         # Run the convert utility or GStreamer pipeline
         if self.use_gstreamer:
             success = self.run_gstreamer(
@@ -254,6 +424,8 @@
                 test_case["output_file"],
                 test_case["input_format"],
                 test_case["output_format"],
+                input_strides,
+                test_case.get("file_format"),
             )
         else:
             success = self.run_convert(
@@ -261,6 +433,7 @@
                 test_case["output_file"],
                 test_case["input_format"],
                 test_case["output_format"],
+                input_strides,
             )
 
         if not success:

Reply via email to