From e35f639e0b3a03d797083357dcb72d380ceaaf9a Mon Sep 17 00:00:00 2001 From: Naushir Patuck Date: Thu, 30 Jul 2026 08:38:16 +0100 Subject: [PATCH 1/3] gst: Honour GstVideoMeta strides on input buffers Input strides were derived only from caps, so buffers with padded or non-default plane layouts were misinterpreted. Use the per-buffer GstVideoMeta strides and offsets on the memcpy path, programme the producer's strides for zero-copy where the layout is suitable, and fix the semi-planar copy path which assumed a planar U/V layout. Signed-off-by: Naushir Patuck --- src/gst/gstpispconvert.cpp | 159 ++++++++++++++++++++++++++++++++----- src/gst/gstpispconvert.h | 8 ++ 2 files changed, 147 insertions(+), 20 deletions(-) diff --git a/src/gst/gstpispconvert.cpp b/src/gst/gstpispconvert.cpp index f67af2a..db9fb84 100644 --- a/src/gst/gstpispconvert.cpp +++ b/src/gst/gstpispconvert.cpp @@ -375,6 +375,11 @@ static void gst_pisp_convert_init(GstPispConvert *self) 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++) { @@ -680,16 +685,16 @@ static void add_video_meta(GstBuffer *buffer, const char *pisp_format, bool rb_s strides); } -static void copy_planes(std::array src, guint src_stride, std::array dst, guint dst_stride, - guint width, guint height, const char *format) +static void copy_planes(std::array src, std::array src_stride, std::array dst, + std::array 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; @@ -698,12 +703,31 @@ static void copy_planes(std::array src, guint src_stride, std::arr 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; @@ -723,13 +747,13 @@ static void copy_planes(std::array src, guint src_stride, std::arr 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 */ @@ -746,19 +770,22 @@ static void copy_planes(std::array src, guint src_stride, std::arr 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 &mem, guint width, guint height, - guint gst_stride, guint hw_stride, const char *format) + const std::array &gst_stride, const std::array &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 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 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); } @@ -772,11 +799,46 @@ static void copy_pisp_to_buffer(const std::array &mem, GstBuffer * /* GstBuffer is always contiguous - planes calculated from offsets */ std::array dst = { map.data, nullptr, nullptr }; - copy_planes(const_cast &>(mem), hw_stride, dst, gst_stride, width, height, format); + copy_planes(const_cast &>(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 &stride = self->priv->in_meta_stride; + const std::array &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. */ @@ -863,7 +925,30 @@ static gboolean gst_pisp_convert_configure(GstPispConvert *self) 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); @@ -1048,6 +1133,38 @@ static GstFlowReturn gst_pisp_convert_chain(GstPad *pad [[maybe_unused]], GstObj 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 meta_stride = { self->priv->in_stride, 0, 0 }; + std::array 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) { @@ -1105,7 +1222,7 @@ static GstFlowReturn gst_pisp_convert_chain(GstPad *pad [[maybe_unused]], GstObj 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 dmabuf_input = gst_to_libpisp_buffer(inbuf); if (!dmabuf_input) @@ -1122,8 +1239,8 @@ static GstFlowReturn gst_pisp_convert_chain(GstPad *pad [[maybe_unused]], GstObj Buffer::Sync s(slice.at("pispbe-input"), Buffer::Sync::Access::ReadWrite); const auto &mem = s.Get(); copy_buffer_to_pisp(inbuf, const_cast &>(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"); } @@ -1296,6 +1413,8 @@ static gboolean gst_pisp_convert_stop(GstPispConvert *self) 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 --git a/src/gst/gstpispconvert.h b/src/gst/gstpispconvert.h index 54c65f9..41ac51a 100644 --- a/src/gst/gstpispconvert.h +++ b/src/gst/gstpispconvert.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -74,6 +75,13 @@ struct _GstPispConvertPrivate 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 in_meta_stride; + std::array 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]; From 9553c38c9e0791b2f11562eebd39b81a8328b4cf Mon Sep 17 00:00:00 2001 From: Naushir Patuck Date: Thu, 30 Jul 2026 09:03:10 +0100 Subject: [PATCH 2/3] gst: Add BGR output format support BGR maps to the PiSP RGB888 format with the R/B swap CSC applied, the same memory layout as the existing DRM RG24 mapping. This allows the swap path to be exercised by the conversion tests. Signed-off-by: Naushir Patuck --- src/gst/gstpispconvert.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gst/gstpispconvert.cpp b/src/gst/gstpispconvert.cpp index db9fb84..ceab319 100644 --- a/src/gst/gstpispconvert.cpp +++ b/src/gst/gstpispconvert.cpp @@ -32,7 +32,7 @@ GST_DEBUG_CATEGORY_STATIC(gst_pisp_convert_debug); #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,6 +66,7 @@ GST_ELEMENT_REGISTER_DEFINE(pispconvert, "pispconvert", GST_RANK_PRIMARY, GST_TY /* Bidirectional mapping between GstVideoFormat and PiSP format strings */ static const std::map gst_pisp_format_map = { { GST_VIDEO_FORMAT_RGB, "RGB888" }, + { GST_VIDEO_FORMAT_BGR, "RGB888" }, { GST_VIDEO_FORMAT_RGBx, "RGBX8888" }, { GST_VIDEO_FORMAT_BGRx, "RGBX8888" }, { GST_VIDEO_FORMAT_I420, "YUV420P" }, @@ -101,7 +102,7 @@ static const std::map drm_pisp_format_map = { * format as the PiSP format string does not encode the channel order. */ static bool require_rb_swap(GstVideoFormat format) { - return format == GST_VIDEO_FORMAT_BGRx; + return format == GST_VIDEO_FORMAT_BGR || format == GST_VIDEO_FORMAT_BGRx; } static bool require_rb_swap(const gchar *drm_format) From 7276d32e6eb3ecb0b29d6c1901ba29e2d3e09b5f Mon Sep 17 00:00:00 2001 From: Naushir Patuck Date: Thu, 30 Jul 2026 09:03:11 +0100 Subject: [PATCH 3/3] tests: Add strided input and BGR test cases The harness can now rewrite an input asset with per-plane strides for either GStreamer or convert mode. Add strided YUV420P, YUYV and RGB888 cases with a new packed-stride YUV444P reference, plus BGR output variants that exercise the R/B swap CSC. Signed-off-by: Naushir Patuck --- utils/test_convert.py | 191 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 6 deletions(-) diff --git a/utils/test_convert.py b/utils/test_convert.py index 1675ca1..54585df 100644 --- a/utils/test_convert.py +++ b/utils/test_convert.py @@ -39,10 +39,10 @@ def __init__( 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 @@ def __init__( "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 @@ def _pisp_to_gst_format(self, pisp_format): } 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: @@ -107,6 +219,20 @@ def run_gstreamer(self, input_file, output_file, input_format, output_format): gst_in_format = self._pisp_to_gst_format(in_fmt["format"]) gst_out_format = self._pisp_to_gst_format(out_fmt["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 = [ "gst-launch-1.0", @@ -118,12 +244,31 @@ def run_gstreamer(self, input_file, output_file, input_format, output_format): 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", + ] + + # 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}", @@ -150,7 +295,9 @@ def run_gstreamer(self, input_file, output_file, input_format, output_format): 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: @@ -160,6 +307,18 @@ def run_convert(self, input_file, output_file, input_format, output_format): 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, @@ -241,6 +400,23 @@ def run_test_case(self, test_case): 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( @@ -248,6 +424,8 @@ def run_test_case(self, test_case): test_case["output_file"], test_case["input_format"], test_case["output_format"], + input_strides, + test_case.get("file_format"), ) else: success = self.run_convert( @@ -255,6 +433,7 @@ def run_test_case(self, test_case): test_case["output_file"], test_case["input_format"], test_case["output_format"], + input_strides, ) if not success: