From 7b21371083c509da1c7cfd301e920688ae51d309 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 3 May 2026 12:24:43 -0400 Subject: [PATCH 01/35] Reapply "fix: replace ZM_COLOUR system with AVPixelFormat for format dispatch" This reverts commit 60b6b37c60796453a7d308e5f14be5c9da130c08. --- src/zm_camera.cpp | 7 +- src/zm_camera.h | 11 +- src/zm_ffmpeg.cpp | 42 +-- src/zm_ffmpeg_camera.cpp | 9 +- src/zm_image.cpp | 552 ++++++++++++++-------------- src/zm_image.h | 6 +- src/zm_libvlc_camera.cpp | 9 +- src/zm_libvnc_camera.cpp | 9 +- src/zm_local_camera.cpp | 62 ++-- src/zm_monitor.cpp | 48 ++- src/zm_mpeg.cpp | 36 +- src/zm_pixformat.h | 154 ++++++++ src/zm_remote_camera_rtsp.cpp | 9 +- src/zm_rgb.h | 19 +- tests/CMakeLists.txt | 1 + tests/zm_pixformat.cpp | 201 ++++++++++ web/lang/en_gb.php | 1 + web/skins/classic/views/monitor.php | 1 + 18 files changed, 772 insertions(+), 405 deletions(-) create mode 100644 src/zm_pixformat.h create mode 100644 tests/zm_pixformat.cpp diff --git a/src/zm_camera.cpp b/src/zm_camera.cpp index cdd66527f32..7c3ee460b02 100644 --- a/src/zm_camera.cpp +++ b/src/zm_camera.cpp @@ -41,6 +41,7 @@ Camera::Camera( height(p_height), colours(p_colours), subpixelorder(p_subpixelorder), + pixelFormat(zm_pixformat_from_colours(p_colours, p_subpixelorder)), brightness(p_brightness), hue(p_hue), colour(p_colour), @@ -63,9 +64,9 @@ Camera::Camera( mLastAudioDTS(AV_NOPTS_VALUE), bytes(0), mIsPrimed(false) { - linesize = width * colours; + linesize = FFALIGN(av_image_get_linesize(pixelFormat, width, 0), 32); pixels = width * height; - imagesize = static_cast(height) * linesize; + imagesize = av_image_get_buffer_size(pixelFormat, width, height, 32); Debug(2, "New camera id: %d width: %d line size: %d height: %d colours: %d subpixelorder: %d capture: %d, size: %llu", monitor->Id(), width, linesize, height, colours, subpixelorder, capture, imagesize); @@ -101,7 +102,7 @@ AVStream *Camera::getVideoStream() { mVideoStream->time_base = (AVRational) {1, 1000000}; // microseconds as base frame rate mVideoStream->codecpar->width = width; mVideoStream->codecpar->height = height; - mVideoStream->codecpar->format = GetFFMPEGPixelFormat(colours, subpixelorder); + mVideoStream->codecpar->format = pixelFormat; mVideoStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; mVideoStream->codecpar->codec_id = AV_CODEC_ID_NONE; Debug(1, "Allocating avstream %p %p %d", mVideoStream, mVideoStream->codecpar, mVideoStream->codecpar->codec_id); diff --git a/src/zm_camera.h b/src/zm_camera.h index 21e46cf2e9c..dbfaa447fcc 100644 --- a/src/zm_camera.h +++ b/src/zm_camera.h @@ -21,6 +21,7 @@ #define ZM_CAMERA_H #include "zm_image.h" +#include "zm_pixformat.h" #include #include @@ -42,8 +43,9 @@ class Camera { uint16_t width; uint16_t height; unsigned int linesize; - unsigned int colours; - unsigned int subpixelorder; + unsigned int colours; // DEPRECATED: use pixelFormat + unsigned int subpixelorder; // DEPRECATED: use pixelFormat + AVPixelFormat pixelFormat; unsigned int pixels; unsigned long long imagesize; int brightness; @@ -98,8 +100,9 @@ class Camera { unsigned int Width() const { return width; } unsigned int LineSize() const { return linesize; } unsigned int Height() const { return height; } - unsigned int Colours() const { return colours; } - unsigned int SubpixelOrder() const { return subpixelorder; } + unsigned int Colours() const { return colours; } // DEPRECATED + unsigned int SubpixelOrder() const { return subpixelorder; } // DEPRECATED + AVPixelFormat PixelFormat() const { return pixelFormat; } unsigned int Pixels() const { return pixels; } unsigned long long ImageSize() const { return imagesize; } unsigned int Bytes() const { return bytes; }; diff --git a/src/zm_ffmpeg.cpp b/src/zm_ffmpeg.cpp index eecb0664221..b9e97a4a7cc 100644 --- a/src/zm_ffmpeg.cpp +++ b/src/zm_ffmpeg.cpp @@ -20,6 +20,7 @@ #include "zm_ffmpeg.h" #include "zm_logger.h" +#include "zm_pixformat.h" #include "zm_rgb.h" #include "zm_utils.h" @@ -327,46 +328,9 @@ void FFMPEGDeInit() { bInit = false; } +// DEPRECATED: use zm_pixformat_from_colours() from zm_pixformat.h instead. enum _AVPIXELFORMAT GetFFMPEGPixelFormat(unsigned int p_colours, unsigned p_subpixelorder) { - enum _AVPIXELFORMAT pf; - - Debug(8,"Colours: %d SubpixelOrder: %d",p_colours,p_subpixelorder); - - switch (p_colours) { - case ZM_COLOUR_RGB24: - if(p_subpixelorder == ZM_SUBPIX_ORDER_BGR) { - /* BGR subpixel order */ - pf = AV_PIX_FMT_BGR24; - } else { - /* Assume RGB subpixel order */ - pf = AV_PIX_FMT_RGB24; - } - break; - case ZM_COLOUR_RGB32: - if (p_subpixelorder == ZM_SUBPIX_ORDER_ARGB) { - /* ARGB subpixel order */ - pf = AV_PIX_FMT_ARGB; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - pf = AV_PIX_FMT_ABGR; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - pf = AV_PIX_FMT_BGRA; - } else { - /* Assume RGBA subpixel order */ - pf = AV_PIX_FMT_RGBA; - } - break; - case ZM_COLOUR_GRAY8: - pf = AV_PIX_FMT_GRAY8; - break; - default: - Panic("Unexpected colours: %d", p_colours); - pf = AV_PIX_FMT_GRAY8; /* Just to shush gcc variable may be unused warning */ - break; - } - - return pf; + return zm_pixformat_from_colours(p_colours, p_subpixelorder); } #if LIBAVUTIL_VERSION_CHECK(56, 0, 0, 17, 100) diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index c39f4924a22..4ec504b32a8 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -94,15 +94,18 @@ FfmpegCamera::FfmpegCamera( /* Has to be located inside the constructor so other components such as zma * will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; imagePixFormat = AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGB; imagePixFormat = AV_PIX_FMT_RGB24; - } else if ( colours == ZM_COLOUR_GRAY8 ) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; imagePixFormat = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected colours: %d", colours); } diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 3fbb458f59c..da047c6bd01 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -361,14 +361,40 @@ bool Image::Assign(const AVFrame *frame) { } zm_dump_video_frame(frame, "source frame in Image::Assign"); - // Desired format AVPixelFormat format = (AVPixelFormat)AVPixFormat(); + AVPixelFormat src_fmt = static_cast(frame->format); + + // If source and destination format + dimensions match, do a direct plane + // copy instead of running through sws_scale. This avoids the overhead of + // the swscale pipeline for identity conversions (e.g. YUVJ422P→YUVJ422P). + if (src_fmt == format + && frame->width == static_cast(width) + && frame->height == static_cast(height)) { + const char *fmt_name = av_get_pix_fmt_name(format); + Debug(4, "Same format %s %dx%d, using av_image_copy", + fmt_name ? fmt_name : "unknown", width, height); + av_frame_ptr temp_frame{av_frame_alloc()}; + if (!temp_frame) { + Error("Unable to allocate destination frame"); + return false; + } + PopulateFrame(temp_frame.get()); + temp_frame->pts = frame->pts; + // u_buffer/v_buffer are not members of Image on master; av_image_copy + // reads the planes directly from temp_frame->data, so we don't need to + // mirror them onto the Image either. + av_image_copy(temp_frame->data, temp_frame->linesize, + (const uint8_t **)frame->data, frame->linesize, + format, width, height); + update_function_pointers(); + return true; + } + sws_convert_context = sws_getCachedContext( sws_convert_context, - frame->width, frame->height, (AVPixelFormat)frame->format, + frame->width, frame->height, src_fmt, width, height, format, SWS_BICUBIC, - //SWS_POINT | SWS_BITEXACT, nullptr, nullptr, nullptr); if (sws_convert_context == nullptr) { Error("Unable to create conversion context"); @@ -691,11 +717,8 @@ uint8_t* Image::WriteBuffer( const unsigned int p_colours, const unsigned int p_subpixelorder) { - if ( p_colours != ZM_COLOUR_GRAY8 - && - p_colours != ZM_COLOUR_RGB24 - && - p_colours != ZM_COLOUR_RGB32 ) { + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + if (p_pixfmt == AV_PIX_FMT_NONE) { Error("WriteBuffer called with unexpected colours: %d", p_colours); return nullptr; } @@ -707,7 +730,23 @@ uint8_t* Image::WriteBuffer( if ( p_width != width || p_height != height || p_colours != colours || p_subpixelorder != subpixelorder ) { - unsigned int newsize = (p_width * p_height) * p_colours; + // Derive size/linesize from the AVPixelFormat. Using p_width * p_colours + // is wrong for planar formats: with the GRAY8/YUV420P alias collision, + // p_colours=1 for YUV420P/YUV422P, but those formats need ~1.5x/2x more + // bytes for the chroma planes. + int av_size = av_image_get_buffer_size(p_pixfmt, p_width, p_height, 32); + if (av_size < 0) { + Error("WriteBuffer: av_image_get_buffer_size failed for fmt=%d %ux%u", + p_pixfmt, p_width, p_height); + return nullptr; + } + int av_linesize = av_image_get_linesize(p_pixfmt, p_width, 0); + if (av_linesize < 0) { + Error("WriteBuffer: av_image_get_linesize failed for fmt=%d width=%u", + p_pixfmt, p_width); + return nullptr; + } + unsigned int newsize = static_cast(av_size); if ( buffer == nullptr ) { AllocImgBuffer(newsize); @@ -727,8 +766,9 @@ uint8_t* Image::WriteBuffer( width = p_width; height = p_height; colours = p_colours; - linesize = p_width * p_colours; + linesize = static_cast(av_linesize); subpixelorder = p_subpixelorder; + imagePixFormat = p_pixfmt; pixels = height*width; size = newsize; } // end if need to re-alloc buffer @@ -741,21 +781,32 @@ void Image::AssignDirect(const AVFrame *frame) { height = frame->height; buffer = frame->data[0]; linesize = frame->linesize[0]; - allocation = size = av_image_get_buffer_size(static_cast(frame->format), frame->width, frame->height, 32); - switch(static_cast(frame->format)) { - case AV_PIX_FMT_RGBA: - subpixelorder = ZM_SUBPIX_ORDER_RGBA; - colours = ZM_COLOUR_RGB32; - break; - case AV_PIX_FMT_YUV420P: - colours = ZM_COLOUR_GRAY8; - break; - default: - Debug(1, "Unimplemented format"); - break; + imagePixFormat = static_cast(frame->format); + + // av_image_get_buffer_size returns int (negative on error). Assigning a + // negative value into the unsigned size/allocation members would wrap to + // huge and break later bounds-dependent code, so check first. + int av_size = av_image_get_buffer_size(imagePixFormat, frame->width, frame->height, 32); + bool fmt_ok = (av_size >= 0) + && zm_colours_from_pixformat(imagePixFormat, colours, subpixelorder); + if (!fmt_ok) { + Error("AssignDirect: unsupported pixel format %d on frame %dx%d (av_size=%d)", + frame->format, frame->width, frame->height, av_size); + // Leave the Image in an explicit invalid state — don't keep stale + // size/linesize/colours that don't match imagePixFormat. + imagePixFormat = AV_PIX_FMT_NONE; + colours = 0; + subpixelorder = 0; + size = 0; + allocation = 0; + linesize = 0; + pixels = 0; + } else { + allocation = size = static_cast(av_size); + pixels = width * height; } + holdbuffer = true; buffertype = ZM_BUFTYPE_DONTFREE; - pixels = width * height; } @@ -781,7 +832,7 @@ void Image::AssignDirect( return; } - if ( p_colours != ZM_COLOUR_GRAY8 && p_colours != ZM_COLOUR_RGB24 && p_colours != ZM_COLOUR_RGB32 ) { + if (zm_pixformat_from_colours(p_colours, p_subpixelorder) == AV_PIX_FMT_NONE) { Error("Attempt to directly assign buffer with unexpected colours per pixel: %d", p_colours); return; } @@ -819,6 +870,7 @@ void Image::AssignDirect( colours = p_colours; linesize = width * colours; subpixelorder = p_subpixelorder; + imagePixFormat = zm_pixformat_from_colours(colours, subpixelorder); pixels = width * height; size = new_buffer_size; update_function_pointers(); @@ -848,7 +900,7 @@ void Image::Assign( return; } - if ( p_colours != ZM_COLOUR_GRAY8 && p_colours != ZM_COLOUR_RGB24 && p_colours != ZM_COLOUR_RGB32 ) { + if (zm_pixformat_from_colours(p_colours, p_subpixelorder) == AV_PIX_FMT_NONE) { Error("Attempt to assign buffer with unexpected colours per pixel: %d", p_colours); return; } @@ -872,6 +924,7 @@ void Image::Assign( pixels = width*height; colours = p_colours; subpixelorder = p_subpixelorder; + imagePixFormat = zm_pixformat_from_colours(colours, subpixelorder); size = new_size; } @@ -888,11 +941,7 @@ void Image::Assign(const Image &image) { return; } - if ( image.colours != ZM_COLOUR_GRAY8 - && - image.colours != ZM_COLOUR_RGB24 - && - image.colours != ZM_COLOUR_RGB32 ) { + if (image.imagePixFormat == AV_PIX_FMT_NONE) { Error("Attempt to assign image with unexpected colours per pixel: %d", image.colours); return; } @@ -919,6 +968,7 @@ void Image::Assign(const Image &image) { pixels = width*height; colours = image.colours; subpixelorder = image.subpixelorder; + imagePixFormat = image.imagePixFormat; size = new_size; linesize = image.linesize; update_function_pointers(); @@ -947,13 +997,15 @@ Image *Image::HighlightEdges( unsigned int p_subpixelorder, const Box *limits ) { - if ( colours != ZM_COLOUR_GRAY8 ) { + if ( imagePixFormat != AV_PIX_FMT_GRAY8 ) { Panic("Attempt to highlight image edges when colours = %d", colours); } /* Convert the colour's RGBA subpixel order into the image's subpixel order */ colour = rgb_convert(colour, p_subpixelorder); + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + /* Create a new image of the target format */ Image *high_image = new Image(width, height, p_colours, p_subpixelorder); uint8_t* high_buff = high_image->WriteBuffer(width, height, p_colours, p_subpixelorder); @@ -966,7 +1018,7 @@ Image *Image::HighlightEdges( unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; - if ( p_colours == ZM_COLOUR_GRAY8 ) { + if ( p_pixfmt == AV_PIX_FMT_GRAY8 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { const uint8_t* p = buffer + (y * linesize) + lo_x; uint8_t* phigh = high_buff + (y * linesize) + lo_x; @@ -986,7 +1038,7 @@ Image *Image::HighlightEdges( } } } - } else if ( p_colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(p_pixfmt) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { const uint8_t* p = buffer + (y * linesize) + lo_x; uint8_t* phigh = high_buff + (((y * linesize) + lo_x) * 3); @@ -1008,7 +1060,7 @@ Image *Image::HighlightEdges( } } } - } else if ( p_colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(p_pixfmt) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { const uint8_t* p = buffer + (y * linesize) + lo_x; Rgb* phigh = (Rgb*)(high_buff + (((y * linesize) + lo_x) * 4)); @@ -1084,6 +1136,7 @@ bool Image::WriteRaw(const std::string &filename) const { bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsigned int p_subpixelorder) { unsigned int new_width, new_height, new_colours, new_subpixelorder; + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); if (!readjpg_dcinfo) { readjpg_dcinfo = new jpeg_decompress_struct; @@ -1131,22 +1184,20 @@ bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsign height = new_height; } - switch (p_colours) { - case ZM_COLOUR_GRAY8: + if (p_pixfmt == AV_PIX_FMT_GRAY8) { readjpg_dcinfo->out_color_space = JCS_GRAYSCALE; new_colours = ZM_COLOUR_GRAY8; new_subpixelorder = ZM_SUBPIX_ORDER_NONE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(p_pixfmt)) { #ifdef JCS_EXTENSIONS new_colours = ZM_COLOUR_RGB32; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGRA) { + if (p_pixfmt == AV_PIX_FMT_BGRA) { readjpg_dcinfo->out_color_space = JCS_EXT_BGRX; new_subpixelorder = ZM_SUBPIX_ORDER_BGRA; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ARGB) { + } else if (p_pixfmt == AV_PIX_FMT_ARGB) { readjpg_dcinfo->out_color_space = JCS_EXT_XRGB; new_subpixelorder = ZM_SUBPIX_ORDER_ARGB; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (p_pixfmt == AV_PIX_FMT_ABGR) { readjpg_dcinfo->out_color_space = JCS_EXT_XBGR; new_subpixelorder = ZM_SUBPIX_ORDER_ABGR; } else { @@ -1154,20 +1205,22 @@ bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsign readjpg_dcinfo->out_color_space = JCS_EXT_RGBX; new_subpixelorder = ZM_SUBPIX_ORDER_RGBA; } - break; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a RGB32 buffer, reading into a RGB24 buffer instead."); + new_colours = ZM_COLOUR_RGB24; + readjpg_dcinfo->out_color_space = JCS_RGB; + new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif - case ZM_COLOUR_RGB24: - default: + } else { + /* Assume RGB24/BGR24 */ new_colours = ZM_COLOUR_RGB24; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGR) { + if (p_pixfmt == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS readjpg_dcinfo->out_color_space = JCS_EXT_BGR; new_subpixelorder = ZM_SUBPIX_ORDER_BGR; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a BGR24 buffer, reading into a RGB24 buffer instead."); - cinfo->out_color_space = JCS_RGB; + readjpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif } else { @@ -1182,8 +1235,7 @@ bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsign readjpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; } - break; - } // end switch p_colours + } // end format dispatch if (WriteBuffer(new_width, new_height, new_colours, new_subpixelorder) == nullptr) { Error("Failed requesting writeable buffer for reading JPEG image."); @@ -1231,7 +1283,7 @@ bool Image::WriteJpeg(const std::string &filename, SystemTimePoint timestamp, bool on_blocking_abort) const { - if (config.colour_jpeg_files && (colours == ZM_COLOUR_GRAY8)) { + if (config.colour_jpeg_files && (imagePixFormat == AV_PIX_FMT_GRAY8)) { Image temp_image(*this); temp_image.Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); return temp_image.WriteJpeg(filename, quality_override, timestamp, on_blocking_abort); @@ -1297,28 +1349,25 @@ bool Image::WriteJpeg(const std::string &filename, cinfo->image_width = width; /* image width and height, in pixels */ cinfo->image_height = height; - switch (colours) { - case ZM_COLOUR_GRAY8: + if (imagePixFormat == AV_PIX_FMT_GRAY8) { cinfo->input_components = 1; cinfo->in_color_space = JCS_GRAYSCALE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(imagePixFormat)) { #ifdef JCS_EXTENSIONS cinfo->input_components = 4; - if (subpixelorder == ZM_SUBPIX_ORDER_RGBA) { + if (imagePixFormat == AV_PIX_FMT_RGBA) { cinfo->in_color_space = JCS_EXT_RGBX; - } else if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) { + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { cinfo->in_color_space = JCS_EXT_BGRX; - } else if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) { + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { cinfo->in_color_space = JCS_EXT_XRGB; - } else if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { cinfo->in_color_space = JCS_EXT_XBGR; } else { Warning("Unknown subpixelorder %d", subpixelorder); /* Assume RGBA */ cinfo->in_color_space = JCS_EXT_RGBX; } - break; #else Error("libjpeg-turbo is required for JPEG encoding directly from RGB32 source"); jpeg_abort_compress(cinfo); @@ -1327,10 +1376,10 @@ bool Image::WriteJpeg(const std::string &filename, fclose(outfile); return false; #endif - case ZM_COLOUR_RGB24: - default: + } else { + /* Assume RGB24/BGR24 */ cinfo->input_components = 3; - if (subpixelorder == ZM_SUBPIX_ORDER_BGR) { + if (imagePixFormat == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS cinfo->in_color_space = JCS_EXT_BGR; #else @@ -1341,7 +1390,7 @@ bool Image::WriteJpeg(const std::string &filename, fclose(outfile); return false; #endif - } else if (subpixelorder == ZM_SUBPIX_ORDER_YUV420P) { + } else if (zm_is_yuv420(imagePixFormat)) { cinfo->in_color_space = JCS_YCbCr; } else { /* Assume RGB */ @@ -1354,8 +1403,7 @@ bool Image::WriteJpeg(const std::string &filename, */ cinfo->in_color_space = JCS_RGB; } - break; - } // end switch(colours) + } // end format dispatch jpeg_set_defaults(cinfo); jpeg_set_quality(cinfo, quality, FALSE); @@ -1398,7 +1446,7 @@ bool Image::WriteJpeg(const std::string &filename, jpeg_write_marker(cinfo, EXIF_CODE, (const JOCTET *) exiftimes, sizeof(exiftimes)); } - if (subpixelorder == ZM_SUBPIX_ORDER_YUV420P) { + if (zm_is_yuv420(imagePixFormat)) { std::vector tmprowbuf(width * 3); JSAMPROW row_pointer = &tmprowbuf[0]; /* pointer to a single row */ while (cinfo->next_scanline < cinfo->image_height) { @@ -1435,6 +1483,7 @@ bool Image::WriteJpeg(const std::string &filename, bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p_colours, unsigned int p_subpixelorder) { unsigned int new_width, new_height, new_colours, new_subpixelorder; + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); if (!decodejpg_dcinfo) { decodejpg_dcinfo = new jpeg_decompress_struct; @@ -1473,22 +1522,20 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p width, height, new_width, new_height); } - switch (p_colours) { - case ZM_COLOUR_GRAY8: + if (p_pixfmt == AV_PIX_FMT_GRAY8) { decodejpg_dcinfo->out_color_space = JCS_GRAYSCALE; new_colours = ZM_COLOUR_GRAY8; new_subpixelorder = ZM_SUBPIX_ORDER_NONE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(p_pixfmt)) { #ifdef JCS_EXTENSIONS new_colours = ZM_COLOUR_RGB32; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGRA) { + if (p_pixfmt == AV_PIX_FMT_BGRA) { decodejpg_dcinfo->out_color_space = JCS_EXT_BGRX; new_subpixelorder = ZM_SUBPIX_ORDER_BGRA; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ARGB) { + } else if (p_pixfmt == AV_PIX_FMT_ARGB) { decodejpg_dcinfo->out_color_space = JCS_EXT_XRGB; new_subpixelorder = ZM_SUBPIX_ORDER_ARGB; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (p_pixfmt == AV_PIX_FMT_ABGR) { decodejpg_dcinfo->out_color_space = JCS_EXT_XBGR; new_subpixelorder = ZM_SUBPIX_ORDER_ABGR; } else { @@ -1496,20 +1543,22 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p decodejpg_dcinfo->out_color_space = JCS_EXT_RGBX; new_subpixelorder = ZM_SUBPIX_ORDER_RGBA; } - break; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a RGB32 buffer, reading into a RGB24 buffer instead."); + new_colours = ZM_COLOUR_RGB24; + decodejpg_dcinfo->out_color_space = JCS_RGB; + new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif - case ZM_COLOUR_RGB24: - default: + } else { + /* Assume RGB24/BGR24 */ new_colours = ZM_COLOUR_RGB24; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGR) { + if (p_pixfmt == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS decodejpg_dcinfo->out_color_space = JCS_EXT_BGR; new_subpixelorder = ZM_SUBPIX_ORDER_BGR; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a BGR24 buffer, reading into a RGB24 buffer instead."); - cinfo->out_color_space = JCS_RGB; + decodejpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif } else { @@ -1524,8 +1573,7 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p decodejpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; } - break; - } // end switch + } // end format dispatch if (WriteBuffer(new_width, new_height, new_colours, new_subpixelorder) == nullptr) { Error("Failed requesting writeable buffer for reading JPEG image."); @@ -1547,7 +1595,7 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p } bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_override) const { - if ( config.colour_jpeg_files && (colours == ZM_COLOUR_GRAY8) ) { + if ( config.colour_jpeg_files && (imagePixFormat == AV_PIX_FMT_GRAY8) ) { Image temp_image(*this); temp_image.Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); return temp_image.EncodeJpeg(outbuffer, outbuffer_size, quality_override); @@ -1572,37 +1620,34 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov cinfo->image_width = width; /* image width and height, in pixels */ cinfo->image_height = height; - switch ( colours ) { - case ZM_COLOUR_GRAY8: + if (imagePixFormat == AV_PIX_FMT_GRAY8) { cinfo->input_components = 1; cinfo->in_color_space = JCS_GRAYSCALE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(imagePixFormat)) { #ifdef JCS_EXTENSIONS cinfo->input_components = 4; - if ( subpixelorder == ZM_SUBPIX_ORDER_RGBA ) { + if (imagePixFormat == AV_PIX_FMT_RGBA) { cinfo->in_color_space = JCS_EXT_RGBX; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { cinfo->in_color_space = JCS_EXT_BGRX; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_ARGB ) { + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { cinfo->in_color_space = JCS_EXT_XRGB; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_ABGR ) { + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { cinfo->in_color_space = JCS_EXT_XBGR; } else { Warning("unknown subpixelorder %d", subpixelorder); /* Assume RGBA */ cinfo->in_color_space = JCS_EXT_RGBX; } - break; #else Error("libjpeg-turbo is required for JPEG encoding directly from RGB32 source"); jpeg_abort_compress(cinfo); return false; #endif - case ZM_COLOUR_RGB24: - default: + } else { + /* Assume RGB24/BGR24 */ cinfo->input_components = 3; - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR ) { + if (imagePixFormat == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS cinfo->in_color_space = JCS_EXT_BGR; #else @@ -1621,8 +1666,7 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov */ cinfo->in_color_space = JCS_RGB; } - break; - } // end switch + } // end format dispatch jpeg_set_defaults(cinfo); jpeg_set_quality(cinfo, quality, FALSE); @@ -1709,13 +1753,25 @@ void Image::Overlay( const Image &image ) { width, height, image.width, image.height); } - if ( colours == image.colours && subpixelorder != image.subpixelorder ) { - Warning("Attempt to overlay images of same format but with different subpixel order %d != %d.", - subpixelorder, image.subpixelorder); - } - - /* Grayscale on top of grayscale - complete */ - if ( colours == ZM_COLOUR_GRAY8 && image.colours == ZM_COLOUR_GRAY8 ) { + // Pre-AVPixelFormat the (colours, subpixelorder) pair was the canonical + // format identifier and a colours-match-but-subpixelorder-mismatch warning + // was meaningful. With imagePixFormat now canonical the old check fires + // false positives whenever GRAY8 (colours=1, subpixelorder=NONE=2) is + // overlaid onto YUV420P (colours=1 due to the GRAY8/YUV420P alias + // collision, subpixelorder=YUV420P=11) — the dispatch below handles this + // case correctly via zm_bytes_per_pixel(...) == 1. Only warn if the + // AVPixelFormat actually matches but the ZM metadata diverges, which would + // indicate a real format-tracking bug. + if (imagePixFormat == image.imagePixFormat + && (colours != image.colours || subpixelorder != image.subpixelorder)) { + Warning("Overlay: imagePixFormat matches (%s) but ZM (colours,subpixelorder) " + "diverges: (%u,%u) vs (%u,%u) — stale metadata?", + av_get_pix_fmt_name(imagePixFormat), + colours, subpixelorder, image.colours, image.subpixelorder); + } + + /* Grayscale/YUV420 on top of grayscale/YUV420 - complete */ + if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { const uint8_t* const max_ptr = buffer+size; const uint8_t* psrc = image.buffer; uint8_t* pdest = buffer; @@ -1728,8 +1784,8 @@ void Image::Overlay( const Image &image ) { psrc++; } - /* RGB24 on top of grayscale - convert to same format first - complete */ - } else if ( colours == ZM_COLOUR_GRAY8 && image.colours == ZM_COLOUR_RGB24 ) { + /* RGB24 on top of grayscale/YUV420 - convert to same format first - complete */ + } else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_is_rgb24(image.imagePixFormat) ) { Colourise(image.colours, image.subpixelorder); const uint8_t* const max_ptr = buffer+size; @@ -1746,15 +1802,15 @@ void Image::Overlay( const Image &image ) { psrc += 3; } - /* RGB32 on top of grayscale - convert to same format first - complete */ - } else if ( colours == ZM_COLOUR_GRAY8 && image.colours == ZM_COLOUR_RGB32 ) { + /* RGB32 on top of grayscale/YUV420 - convert to same format first - complete */ + } else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_is_rgb32(image.imagePixFormat) ) { Colourise(image.colours, image.subpixelorder); const Rgb* const max_ptr = (Rgb*)(buffer+size); const Rgb* prsrc = (Rgb*)image.buffer; Rgb* prdest = (Rgb*)buffer; - if ( subpixelorder == ZM_SUBPIX_ORDER_RGBA || subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { + if ( imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA ) { /* RGB\BGR\RGBA\BGRA subpixel order - Alpha byte is last */ while ( prdest < max_ptr) { if ( RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc) ) { @@ -1774,8 +1830,8 @@ void Image::Overlay( const Image &image ) { } } - /* Grayscale on top of RGB24 - complete */ - } else if ( colours == ZM_COLOUR_RGB24 && image.colours == ZM_COLOUR_GRAY8 ) { + /* Grayscale/YUV420 on top of RGB24 - complete */ + } else if ( zm_is_rgb24(imagePixFormat) && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { const uint8_t* const max_ptr = buffer+size; const uint8_t* psrc = image.buffer; uint8_t* pdest = buffer; @@ -1789,7 +1845,7 @@ void Image::Overlay( const Image &image ) { } /* RGB24 on top of RGB24 - not complete. need to take care of different subpixel orders */ - } else if ( colours == ZM_COLOUR_RGB24 && image.colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) && zm_is_rgb24(image.imagePixFormat) ) { const uint8_t* const max_ptr = buffer+size; const uint8_t* psrc = image.buffer; uint8_t* pdest = buffer; @@ -1805,16 +1861,16 @@ void Image::Overlay( const Image &image ) { } /* RGB32 on top of RGB24 - TO BE DONE */ - } else if ( colours == ZM_COLOUR_RGB24 && image.colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb24(imagePixFormat) && zm_is_rgb32(image.imagePixFormat) ) { Error("Overlay of RGB32 on top of RGB24 is not supported."); - /* Grayscale on top of RGB32 - complete */ - } else if ( colours == ZM_COLOUR_RGB32 && image.colours == ZM_COLOUR_GRAY8 ) { + /* Grayscale/YUV420 on top of RGB32 - complete */ + } else if ( zm_is_rgb32(imagePixFormat) && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { const Rgb* const max_ptr = (Rgb*)(buffer+size); Rgb* prdest = (Rgb*)buffer; const uint8_t* psrc = image.buffer; - if ( subpixelorder == ZM_SUBPIX_ORDER_RGBA || subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { + if ( imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA ) { /* RGBA\BGRA subpixel order - Alpha byte is last */ while ( prdest < max_ptr ) { if ( *psrc ) { @@ -1836,16 +1892,16 @@ void Image::Overlay( const Image &image ) { } /* RGB24 on top of RGB32 - TO BE DONE */ - } else if ( colours == ZM_COLOUR_RGB32 && image.colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb32(imagePixFormat) && zm_is_rgb24(image.imagePixFormat) ) { Error("Overlay of RGB24 on top of RGB32 is not supported."); /* RGB32 on top of RGB32 - not complete. need to take care of different subpixel orders */ - } else if ( colours == ZM_COLOUR_RGB32 && image.colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) && zm_is_rgb32(image.imagePixFormat) ) { const Rgb* const max_ptr = (Rgb*)(buffer+size); Rgb* prdest = (Rgb*)buffer; const Rgb* prsrc = (Rgb*)image.buffer; - if ( image.subpixelorder == ZM_SUBPIX_ORDER_RGBA || image.subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { + if ( image.imagePixFormat == AV_PIX_FMT_RGBA || image.imagePixFormat == AV_PIX_FMT_BGRA ) { /* RGB\BGR\RGBA\BGRA subpixel order - Alpha byte is last */ while ( prdest < max_ptr ) { if ( RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc) ) { @@ -1887,7 +1943,7 @@ void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned unsigned int hi_x = (lo_x+image.width)-1; unsigned int hi_y = (lo_y+image.height-1); - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { const uint8_t *psrc = image.buffer; for ( unsigned int y = lo_y; y <= hi_y; y++ ) { uint8_t *pdest = &buffer[(y*width)+lo_x]; @@ -1895,7 +1951,7 @@ void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { const uint8_t *psrc = image.buffer; for ( unsigned int y = lo_y; y <= hi_y; y++ ) { uint8_t *pdest = &buffer[colours*((y*width)+lo_x)]; @@ -1905,7 +1961,7 @@ void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { const Rgb *psrc = (Rgb*)(image.buffer); for ( unsigned int y = lo_y; y <= hi_y; y++ ) { Rgb *pdest = (Rgb*)&buffer[((y*width)+lo_x)<<2]; @@ -2076,37 +2132,28 @@ bool Image::Delta(const Image &image, Image* targetimage) const { TimePoint start = std::chrono::steady_clock::now(); #endif - switch ( colours ) { - case ZM_COLOUR_RGB24: - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR ) { - /* BGR subpixel order */ - (*delta8_bgr)(buffer, image.buffer, pdiff, pixels); - } else { - /* Assume RGB subpixel order */ - (*delta8_rgb)(buffer, image.buffer, pdiff, pixels); - } - break; - case ZM_COLOUR_RGB32: - if ( subpixelorder == ZM_SUBPIX_ORDER_ARGB ) { - /* ARGB subpixel order */ - (*delta8_argb)(buffer, image.buffer, pdiff, pixels); - } else if(subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - (*delta8_abgr)(buffer, image.buffer, pdiff, pixels); - } else if(subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - (*delta8_bgra)(buffer, image.buffer, pdiff, pixels); - } else { - /* Assume RGBA subpixel order */ - (*delta8_rgba)(buffer, image.buffer, pdiff, pixels); - } - break; - case ZM_COLOUR_GRAY8: + if (imagePixFormat == AV_PIX_FMT_BGR24) { + /* BGR subpixel order */ + (*delta8_bgr)(buffer, image.buffer, pdiff, pixels); + } else if (zm_is_rgb24(imagePixFormat)) { + /* Assume RGB subpixel order */ + (*delta8_rgb)(buffer, image.buffer, pdiff, pixels); + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { + /* ARGB subpixel order */ + (*delta8_argb)(buffer, image.buffer, pdiff, pixels); + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { + /* ABGR subpixel order */ + (*delta8_abgr)(buffer, image.buffer, pdiff, pixels); + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { + /* BGRA subpixel order */ + (*delta8_bgra)(buffer, image.buffer, pdiff, pixels); + } else if (zm_is_rgb32(imagePixFormat)) { + /* Assume RGBA subpixel order */ + (*delta8_rgba)(buffer, image.buffer, pdiff, pixels); + } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { (*delta8_gray8)(buffer, image.buffer, pdiff, pixels); - break; - default: - Panic("Delta called with unexpected colours: %d",colours); - break; + } else { + Panic("Delta called with unexpected colours: %d", colours); } #ifdef ZM_IMAGE_PROFILING @@ -2162,13 +2209,13 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour unsigned int i = 0; for ( unsigned int y = 0; y < height; y++ ) { - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int x = 0; x < width; x++, ptr++ ) { if ( p_bitmask[i] ) *ptr = pixel_bw_col; i++; } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int x = 0; x < width; x++, ptr += colours ) { if ( p_bitmask[i] ) { RED_PTR_RGBA(ptr) = pixel_r_col; @@ -2177,7 +2224,7 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour } i++; } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for ( unsigned int x = 0; x < width; x++, ptr += colours ) { Rgb *temp_ptr = (Rgb*)ptr; if ( p_bitmask[i] ) @@ -2228,7 +2275,7 @@ void Image::Annotate( for (const std::string &line : lines) { uint32 x = x0; - if (colours == ZM_COLOUR_GRAY8) { + if (zm_bytes_per_pixel(imagePixFormat) == 1) { uint8 *ptr = &buffer[(y * width) + x0]; for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { @@ -2250,7 +2297,7 @@ void Image::Annotate( break; } } - } else if (colours == ZM_COLOUR_RGB24) { + } else if (zm_is_rgb24(imagePixFormat)) { constexpr uint8 bytesPerPixel = 3; uint8 *ptr = &buffer[((y * width) + x0) * bytesPerPixel]; @@ -2282,7 +2329,7 @@ void Image::Annotate( break; } } - } else if (colours == ZM_COLOUR_RGB32) { + } else if (zm_is_rgb32(imagePixFormat)) { constexpr uint8 bytesPerPixel = 4; Rgb *ptr = reinterpret_cast(&buffer[((y * width) + x0) * bytesPerPixel]); @@ -2336,12 +2383,14 @@ void Image::Timestamp(const char *label, SystemTimePoint when, const Vector2 &co void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqsubpixelorder) { Debug(9, "Colourise: Req colours: %u Req subpixel order: %u Current colours: %u Current subpixel order: %u",p_reqcolours,p_reqsubpixelorder,colours,subpixelorder); - if ( colours != ZM_COLOUR_GRAY8) { + if ( imagePixFormat != AV_PIX_FMT_GRAY8) { Warning("Target image is already colourised, colours: %u",colours); return; } - if ( p_reqcolours == ZM_COLOUR_RGB32 ) { + AVPixelFormat p_req_pixfmt = zm_pixformat_from_colours(p_reqcolours, p_reqsubpixelorder); + + if ( zm_is_rgb32(p_req_pixfmt) ) { /* RGB32 */ Rgb* new_buffer = (Rgb*)AllocBuffer(pixels*sizeof(Rgb)); @@ -2350,7 +2399,7 @@ void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqs Rgb subpixel; Rgb newpixel; - if ( p_reqsubpixelorder == ZM_SUBPIX_ORDER_ABGR || p_reqsubpixelorder == ZM_SUBPIX_ORDER_ARGB ) { + if ( p_req_pixfmt == AV_PIX_FMT_ABGR || p_req_pixfmt == AV_PIX_FMT_ARGB ) { /* ARGB\ABGR subpixel order. alpha byte is first (mem+0), so we need to shift the pixel left in the end */ for ( unsigned int i=0; i < pixels; i++ ) { newpixel = subpixel = psrc[i]; @@ -2371,7 +2420,7 @@ void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqs /* Directly assign the new buffer and make sure it will be freed when not needed anymore */ AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, (uint8_t*)new_buffer, pixels*4, ZM_BUFTYPE_ZM); - } else if ( p_reqcolours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(p_req_pixfmt) ) { /* RGB24 */ uint8_t *new_buffer = AllocBuffer(pixels*3); @@ -2392,10 +2441,10 @@ void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqs /* RGB32 compatible: complete */ void Image::DeColourise() { - const unsigned int src_colours = colours; + const AVPixelFormat src_pixfmt = imagePixFormat; const unsigned int src_subpixelorder = subpixelorder; - if ( src_colours == ZM_COLOUR_RGB32 && config.cpu_extensions && sse_version >= 35 ) { + if ( zm_is_rgb32(src_pixfmt) && config.cpu_extensions && sse_version >= 35 ) { /* Use SSSE3 functions */ switch (src_subpixelorder) { case ZM_SUBPIX_ORDER_BGRA: @@ -2414,7 +2463,7 @@ void Image::DeColourise() { } } else { /* Use standard functions */ - if ( src_colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(src_pixfmt) ) { if ( pixels % 16 ) { switch (src_subpixelorder) { case ZM_SUBPIX_ORDER_BGRA: @@ -2476,12 +2525,13 @@ void Image::DeColourise() { colours = ZM_COLOUR_GRAY8; subpixelorder = ZM_SUBPIX_ORDER_NONE; + imagePixFormat = AV_PIX_FMT_GRAY8; size = width * height; } /* RGB32 compatible: complete */ void Image::Fill( Rgb colour, const Box *limits ) { - if ( !(colours == ZM_COLOUR_GRAY8 || colours == ZM_COLOUR_RGB24 || colours == ZM_COLOUR_RGB32 ) ) { + if ( !(zm_bytes_per_pixel(imagePixFormat) == 1 || zm_is_rgb24(imagePixFormat) || zm_is_rgb32(imagePixFormat)) ) { Panic("Attempt to fill image with unexpected colours %d", colours); } @@ -2492,14 +2542,14 @@ void Image::Fill( Rgb colour, const Box *limits ) { unsigned int lo_y = limits ? limits->Lo().y_ : 0; unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { unsigned char *p = &buffer[(y*width)+lo_x]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { *p = colour; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { unsigned char *p = &buffer[colours*((y*width)+lo_x)]; for ( unsigned int x = lo_x; x <= hi_x; x++, p += 3) { @@ -2508,7 +2558,7 @@ void Image::Fill( Rgb colour, const Box *limits ) { BLUE_PTR_RGBA(p) = BLUE_VAL_RGBA(colour); } } - } else if ( colours == ZM_COLOUR_RGB32 ) { /* RGB32 */ + } else if ( zm_is_rgb32(imagePixFormat) ) { /* RGB32 */ for ( unsigned int y = lo_y; y <= (unsigned int)hi_y; y++ ) { Rgb *p = (Rgb*)&buffer[((y*width)+lo_x)<<2]; @@ -2526,7 +2576,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { if ( density <= 1 ) return Fill(colour,limits); - if ( !(colours == ZM_COLOUR_GRAY8 || colours == ZM_COLOUR_RGB24 || colours == ZM_COLOUR_RGB32 ) ) { + if ( !(zm_bytes_per_pixel(imagePixFormat) == 1 || zm_is_rgb24(imagePixFormat) || zm_is_rgb32(imagePixFormat)) ) { Panic("Attempt to fill image with unexpected colours %d", colours); } @@ -2537,7 +2587,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { unsigned int lo_y = limits ? limits->Lo().y_ : 0; unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { unsigned char *p = &buffer[(y*width)+lo_x]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { @@ -2545,7 +2595,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { *p = colour; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { unsigned char *p = &buffer[colours*((y*width)+lo_x)]; for ( unsigned int x = lo_x; x <= hi_x; x++, p += 3) { @@ -2556,7 +2606,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { } } } - } else if ( colours == ZM_COLOUR_RGB32 ) { /* RGB32 */ + } else if ( zm_is_rgb32(imagePixFormat) ) { /* RGB32 */ for ( unsigned int y = lo_y; y <= hi_y; y++ ) { Rgb* p = (Rgb*)&buffer[((y*width)+lo_x)<<2]; @@ -2571,7 +2621,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { /* RGB32 compatible: complete */ void Image::Outline( Rgb colour, const Polygon &polygon ) { - if ( !(colours == ZM_COLOUR_GRAY8 || colours == ZM_COLOUR_RGB24 || colours == ZM_COLOUR_RGB32 ) ) { + if ( !(zm_bytes_per_pixel(imagePixFormat) == 1 || zm_is_rgb24(imagePixFormat) || zm_is_rgb32(imagePixFormat)) ) { Panic("Attempt to outline image with unexpected colours %d", colours); } @@ -2603,18 +2653,18 @@ void Image::Outline( Rgb colour, const Polygon &polygon ) { double x; int y, yinc = (y1(it->min_x); int32 hi_x = static_cast((it + 1)->min_x); - if (colours == ZM_COLOUR_GRAY8) { + if (zm_bytes_per_pixel(imagePixFormat) == 1) { uint8 *p = &buffer[(scan_line * width) + lo_x]; for (int32 x = lo_x; x <= hi_x; x++, p++) { @@ -2734,7 +2784,7 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { *p = colour; } } - } else if (colours == ZM_COLOUR_RGB24) { + } else if (zm_is_rgb24(imagePixFormat)) { constexpr uint8 bytesPerPixel = 3; uint8 *ptr = &buffer[((scan_line * width) + lo_x) * bytesPerPixel]; @@ -2745,7 +2795,7 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { BLUE_PTR_RGBA(ptr) = BLUE_VAL_RGBA(colour); } } - } else if (colours == ZM_COLOUR_RGB32) { + } else if (zm_is_rgb32(imagePixFormat)) { constexpr uint8 bytesPerPixel = 4; Rgb *ptr = reinterpret_cast(&buffer[((scan_line * width) + lo_x) * bytesPerPixel]); @@ -2780,7 +2830,7 @@ void Image::Rotate(int angle) { unsigned int line_bytes = new_width*colours; unsigned char *s_ptr = buffer; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int i = new_width; i > 0; i-- ) { unsigned char *d_ptr = rotate_buffer+(i-1); for ( unsigned int j = new_height; j > 0; j-- ) { @@ -2788,7 +2838,7 @@ void Image::Rotate(int angle) { d_ptr += line_bytes; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { Rgb* s_rptr = (Rgb*)s_ptr; for ( unsigned int i = new_width; i; i-- ) { Rgb* d_rptr = (Rgb*)(rotate_buffer+((i-1)<<2)); @@ -2814,12 +2864,12 @@ void Image::Rotate(int angle) { unsigned char *s_ptr = buffer+size; unsigned char *d_ptr = rotate_buffer; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { while( s_ptr > buffer ) { s_ptr--; *d_ptr++ = *s_ptr; } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { Rgb* s_rptr = (Rgb*)s_ptr; Rgb* d_rptr = (Rgb*)d_ptr; while( s_rptr > (Rgb*)buffer ) { @@ -2843,7 +2893,7 @@ void Image::Rotate(int angle) { unsigned int line_bytes = new_width*colours; unsigned char *s_ptr = buffer+size; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int i = new_width; i > 0; i-- ) { unsigned char *d_ptr = rotate_buffer+(i-1); for ( unsigned int j = new_height; j > 0; j-- ) { @@ -2852,7 +2902,7 @@ void Image::Rotate(int angle) { d_ptr += line_bytes; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { Rgb* s_rptr = (Rgb*)s_ptr; for ( unsigned int i = new_width; i > 0; i-- ) { Rgb* d_rptr = (Rgb*)(rotate_buffer+((i-1)<<2)); @@ -2892,7 +2942,7 @@ void Image::Flip( bool leftright ) { unsigned char *d_ptr = flip_buffer; unsigned char *max_d_ptr = flip_buffer + size; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { while( d_ptr < max_d_ptr ) { for ( unsigned int j = 0; j < width; j++ ) { s_ptr--; @@ -2900,7 +2950,7 @@ void Image::Flip( bool leftright ) { } s_ptr += line_bytes2; } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { Rgb* s_rptr = (Rgb*)s_ptr; Rgb* d_rptr = (Rgb*)flip_buffer; Rgb* max_d_rptr = (Rgb*)max_d_ptr; @@ -3048,7 +3098,7 @@ void Image::Deinterlace_Discard() { /* Simple deinterlacing. Copy the even lines into the odd lines */ // ICON: These can be drastically improved. But who cares? - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { const uint8_t *psrc; uint8_t *pdest; for (unsigned int y = 0; y < (unsigned int)height; y += 2) { @@ -3058,7 +3108,7 @@ void Image::Deinterlace_Discard() { *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { const uint8_t *psrc; uint8_t *pdest; for (unsigned int y = 0; y < (unsigned int)height; y += 2) { @@ -3070,7 +3120,7 @@ void Image::Deinterlace_Discard() { *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { const Rgb *psrc; Rgb *pdest; for (unsigned int y = 0; y < (unsigned int)height; y += 2) { @@ -3091,7 +3141,7 @@ void Image::Deinterlace_Linear() { const uint8_t *pbelow, *pabove; uint8_t *pcurrent; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { pabove = buffer + ((y-1) * width); pbelow = buffer + ((y+1) * width); @@ -3106,7 +3156,7 @@ void Image::Deinterlace_Linear() { for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { pabove = buffer + (((y-1) * width) * 3); pbelow = buffer + (((y+1) * width) * 3); @@ -3125,7 +3175,7 @@ void Image::Deinterlace_Linear() { *pcurrent++ = *pabove++; *pcurrent++ = *pabove++; } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { pabove = buffer + (((y-1) * width) << 2); pbelow = buffer + (((y+1) * width) << 2); @@ -3157,7 +3207,7 @@ void Image::Deinterlace_Blend() { uint8_t *pabove, *pcurrent; - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { pabove = buffer + ((y-1) * width); pcurrent = buffer + (y * width); @@ -3166,7 +3216,7 @@ void Image::Deinterlace_Blend() { *pcurrent++ = *pabove++; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { pabove = buffer + (((y-1) * width) * 3); pcurrent = buffer + ((y * width) * 3); @@ -3179,7 +3229,7 @@ void Image::Deinterlace_Blend() { *pcurrent++ = *pabove++; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { pabove = buffer + (((y-1) * width) << 2); pcurrent = buffer + ((y * width) << 2); @@ -3214,7 +3264,7 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { Error("Deinterlace called with invalid blend ratio"); } - if ( colours == ZM_COLOUR_GRAY8 ) { + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { pabove = buffer + ((y-1) * width); pcurrent = buffer + (y * width); @@ -3225,7 +3275,7 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { *pabove++ = subpix2; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { pabove = buffer + (((y-1) * width) * 3); pcurrent = buffer + ((y * width) * 3); @@ -3244,7 +3294,7 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { *pabove++ = subpix2; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { pabove = buffer + (((y-1) * width) << 2); pcurrent = buffer + ((y * width) << 2); @@ -3279,39 +3329,28 @@ void Image::Deinterlace_4Field(const Image* next_image, unsigned int threshold) Panic( "Attempt to deinterlace different sized images, expected %dx%dx%d %d, got %dx%dx%d %d", width, height, colours, subpixelorder, next_image->width, next_image->height, next_image->colours, next_image->subpixelorder); } - switch(colours) { - case ZM_COLOUR_RGB24: { - if(subpixelorder == ZM_SUBPIX_ORDER_BGR) { - /* BGR subpixel order */ - std_deinterlace_4field_bgr(buffer, next_image->buffer, threshold, width, height); - } else { - /* Assume RGB subpixel order */ - std_deinterlace_4field_rgb(buffer, next_image->buffer, threshold, width, height); - } - break; - } - case ZM_COLOUR_RGB32: { - if(subpixelorder == ZM_SUBPIX_ORDER_ARGB) { - /* ARGB subpixel order */ - (*fptr_deinterlace_4field_argb)(buffer, next_image->buffer, threshold, width, height); - } else if(subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - (*fptr_deinterlace_4field_abgr)(buffer, next_image->buffer, threshold, width, height); - } else if(subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - (*fptr_deinterlace_4field_bgra)(buffer, next_image->buffer, threshold, width, height); - } else { - /* Assume RGBA subpixel order */ - (*fptr_deinterlace_4field_rgba)(buffer, next_image->buffer, threshold, width, height); - } - break; - } - case ZM_COLOUR_GRAY8: + if (imagePixFormat == AV_PIX_FMT_BGR24) { + /* BGR subpixel order */ + std_deinterlace_4field_bgr(buffer, next_image->buffer, threshold, width, height); + } else if (zm_is_rgb24(imagePixFormat)) { + /* Assume RGB subpixel order */ + std_deinterlace_4field_rgb(buffer, next_image->buffer, threshold, width, height); + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { + /* ARGB subpixel order */ + (*fptr_deinterlace_4field_argb)(buffer, next_image->buffer, threshold, width, height); + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { + /* ABGR subpixel order */ + (*fptr_deinterlace_4field_abgr)(buffer, next_image->buffer, threshold, width, height); + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { + /* BGRA subpixel order */ + (*fptr_deinterlace_4field_bgra)(buffer, next_image->buffer, threshold, width, height); + } else if (zm_is_rgb32(imagePixFormat)) { + /* Assume RGBA subpixel order */ + (*fptr_deinterlace_4field_rgba)(buffer, next_image->buffer, threshold, width, height); + } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { (*fptr_deinterlace_4field_gray8)(buffer, next_image->buffer, threshold, width, height); - break; - default: - Panic("Deinterlace_4Field called with unexpected colours: %d",colours); - break; + } else { + Panic("Deinterlace_4Field called with unexpected colours: %d", colours); } } @@ -5464,54 +5503,17 @@ __attribute__((noinline)) void std_deinterlace_4field_abgr(uint8_t* col1, uint8_ } AVPixelFormat Image::AVPixFormat() const { - if ( colours == ZM_COLOUR_RGB32 ) { - return AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR) { - return AV_PIX_FMT_BGR24; - } else { - return AV_PIX_FMT_RGB24; - } - } else if ( colours == ZM_COLOUR_GRAY8 ) { - return AV_PIX_FMT_GRAY8; - } else { - Error("Unknown colours (%d)",colours); - return AV_PIX_FMT_RGBA; - } + return zm_pixformat_from_colours(colours, subpixelorder); } AVPixelFormat Image::AVPixFormat(AVPixelFormat new_pixelformat) { - switch (new_pixelformat) { - case AV_PIX_FMT_YUVJ420P: - colours = ZM_COLOUR_YUVJ420P; - subpixelorder = ZM_SUBPIX_ORDER_YUVJ420P; - break; - case AV_PIX_FMT_YUV420P: - colours = ZM_COLOUR_YUV420P; - subpixelorder = ZM_SUBPIX_ORDER_YUV420P; - break; - case AV_PIX_FMT_RGBA: - colours = ZM_COLOUR_RGB32; - subpixelorder = ZM_SUBPIX_ORDER_RGBA; - break; - case AV_PIX_FMT_BGR24: - colours = ZM_COLOUR_RGB24; - subpixelorder = ZM_SUBPIX_ORDER_BGR; - break; - case AV_PIX_FMT_RGB24: - colours = ZM_COLOUR_RGB24; - subpixelorder = ZM_SUBPIX_ORDER_RGB; - break; - case AV_PIX_FMT_GRAY8: - colours = ZM_COLOUR_GRAY8; - subpixelorder = ZM_SUBPIX_ORDER_NONE; - break; - default: - Error("Unknown pixelformat %d %s", new_pixelformat, av_get_pix_fmt_name(new_pixelformat)); + if (!zm_colours_from_pixformat(new_pixelformat, colours, subpixelorder)) { + Error("Unknown pixelformat %d %s", new_pixelformat, av_get_pix_fmt_name(new_pixelformat)); } Debug(4, "Old size: %d, old pixelformat %d", size, imagePixFormat); + imagePixFormat = new_pixelformat; size = av_image_get_buffer_size(new_pixelformat, width, height, 32); Debug(4, "New size: %d new pixelformat %d", size, new_pixelformat); linesize = FFALIGN(av_image_get_linesize(new_pixelformat, width, 0), 32); - return imagePixFormat = new_pixelformat; + return imagePixFormat; } diff --git a/src/zm_image.h b/src/zm_image.h index 2ffc671922c..985d7f7ad38 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -24,6 +24,7 @@ #include "zm_jpeg.h" #include "zm_logger.h" #include "zm_mem_utils.h" +#include "zm_pixformat.h" #include "zm_rgb.h" #include "zm_time.h" #include "zm_vector2.h" @@ -176,8 +177,9 @@ class Image { inline unsigned int Size() const { return size; } std::string Filename() const { return filename_; } - AVPixelFormat AVPixFormat() const; - AVPixelFormat AVPixFormat(AVPixelFormat); + AVPixelFormat PixFormat() const { return imagePixFormat; } + AVPixelFormat AVPixFormat() const; // DEPRECATED: use PixFormat() + AVPixelFormat AVPixFormat(AVPixelFormat); // Sets imagePixFormat and updates colours/subpixelorder inline uint8_t* Buffer() { return buffer; } inline const uint8_t* Buffer() const { return buffer; } diff --git a/src/zm_libvlc_camera.cpp b/src/zm_libvlc_camera.cpp index 49fe07aacdd..f66651a87ee 100644 --- a/src/zm_libvlc_camera.cpp +++ b/src/zm_libvlc_camera.cpp @@ -145,16 +145,19 @@ LibvlcCamera::LibvlcCamera( mOptArgV = nullptr; /* Has to be located inside the constructor so other components such as zma will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_BGRA; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); mTargetChroma = "RV32"; mBpp = 4; - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_BGR; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); mTargetChroma = "RV24"; mBpp = 3; - } else if ( colours == ZM_COLOUR_GRAY8 ) { + } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); mTargetChroma = "GREY"; mBpp = 1; } else { diff --git a/src/zm_libvnc_camera.cpp b/src/zm_libvnc_camera.cpp index 6059788d315..2ad337d3aed 100644 --- a/src/zm_libvnc_camera.cpp +++ b/src/zm_libvnc_camera.cpp @@ -117,15 +117,18 @@ VncCamera::VncCamera( mPort(port), mUser(user), mPass(pass) { - if (colours == ZM_COLOUR_RGB32) { + if (zm_is_rgb32(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; mImgPixFmt = AV_PIX_FMT_RGBA; - } else if (colours == ZM_COLOUR_RGB24) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if (zm_is_rgb24(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGB; mImgPixFmt = AV_PIX_FMT_RGB24; - } else if (colours == ZM_COLOUR_GRAY8) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if (pixelFormat == AV_PIX_FMT_GRAY8) { subpixelorder = ZM_SUBPIX_ORDER_NONE; mImgPixFmt = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected colours: %d", colours); } diff --git a/src/zm_local_camera.cpp b/src/zm_local_camera.cpp index e17bd0b5b55..8b2b0d7ffab 100644 --- a/src/zm_local_camera.cpp +++ b/src/zm_local_camera.cpp @@ -323,25 +323,32 @@ LocalCamera::LocalCamera( /* Try to find a match for the selected palette and target colourspace */ /* RGB32 palette and 32bit target colourspace */ - if (palette == V4L2_PIX_FMT_RGB32 && colours == ZM_COLOUR_RGB32) { + if (palette == V4L2_PIX_FMT_RGB32 && zm_is_rgb32(pixelFormat)) { conversion_type = 0; subpixelorder = ZM_SUBPIX_ORDER_ARGB; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* BGR32 palette and 32bit target colourspace */ - } else if (palette == V4L2_PIX_FMT_BGR32 && colours == ZM_COLOUR_RGB32) { + } else if (palette == V4L2_PIX_FMT_BGR32 && zm_is_rgb32(pixelFormat)) { conversion_type = 0; subpixelorder = ZM_SUBPIX_ORDER_BGRA; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* RGB24 palette and 24bit target colourspace */ - } else if (palette == V4L2_PIX_FMT_RGB24 && colours == ZM_COLOUR_RGB24) { + } else if (palette == V4L2_PIX_FMT_RGB24 && zm_is_rgb24(pixelFormat)) { conversion_type = 0; - conversion_type = 0; - subpixelorder = ZM_SUBPIX_ORDER_BGR; + // V4L2_PIX_FMT_RGB24 is byte-order R,G,B in memory (maps to AV_PIX_FMT_RGB24 + // in getFfPixFormatFromV4lPalette above), so the subpixel order must be RGB. + // Setting BGR here was a long-standing bug that swapped red and blue on + // RGB24-capture cameras. + subpixelorder = ZM_SUBPIX_ORDER_RGB; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* Grayscale palette and grayscale target colourspace */ - } else if (palette == V4L2_PIX_FMT_GREY && colours == ZM_COLOUR_GRAY8) { + } else if (palette == V4L2_PIX_FMT_GREY && pixelFormat == AV_PIX_FMT_GRAY8) { conversion_type = 0; subpixelorder = ZM_SUBPIX_ORDER_NONE; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* Unable to find a solution for the selected palette and target colourspace. Conversion required. Notify the user of performance penalty */ } else { if (capture) { @@ -353,15 +360,18 @@ LocalCamera::LocalCamera( /* Try using swscale for the conversion */ conversion_type = 1; Debug(2, "Using swscale for image conversion"); - if (colours == ZM_COLOUR_RGB32) { + if (zm_is_rgb32(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; imagePixFormat = AV_PIX_FMT_RGBA; - } else if (colours == ZM_COLOUR_RGB24) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if (zm_is_rgb24(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGB; imagePixFormat = AV_PIX_FMT_RGB24; - } else if (colours == ZM_COLOUR_GRAY8) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if (pixelFormat == AV_PIX_FMT_GRAY8) { subpixelorder = ZM_SUBPIX_ORDER_NONE; imagePixFormat = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected colours: %u",colours); } @@ -376,7 +386,7 @@ LocalCamera::LocalCamera( } } /* Our YUYV->Grayscale conversion is a lot faster than swscale's */ - if (colours == ZM_COLOUR_GRAY8 && palette == V4L2_PIX_FMT_YUYV) { + if (pixelFormat == AV_PIX_FMT_GRAY8 && palette == V4L2_PIX_FMT_YUYV) { conversion_type = 2; } @@ -388,13 +398,15 @@ LocalCamera::LocalCamera( if (conversion_type == 2) { Debug(2,"Using ZM for image conversion"); - if ( palette == V4L2_PIX_FMT_RGB32 && colours == ZM_COLOUR_GRAY8 ) { + if ( palette == V4L2_PIX_FMT_RGB32 && pixelFormat == AV_PIX_FMT_GRAY8 ) { conversion_fptr = &std_convert_argb_gray8; subpixelorder = ZM_SUBPIX_ORDER_NONE; - } else if (palette == V4L2_PIX_FMT_BGR32 && colours == ZM_COLOUR_GRAY8) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_BGR32 && pixelFormat == AV_PIX_FMT_GRAY8) { conversion_fptr = &std_convert_bgra_gray8; subpixelorder = ZM_SUBPIX_ORDER_NONE; - } else if (palette == V4L2_PIX_FMT_YUYV && colours == ZM_COLOUR_GRAY8) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_YUYV && pixelFormat == AV_PIX_FMT_GRAY8) { /* Fast YUYV->Grayscale conversion by extracting the Y channel */ if (config.cpu_extensions && sse_version >= 35) { conversion_fptr = &ssse3_convert_yuyv_gray8; @@ -404,24 +416,31 @@ LocalCamera::LocalCamera( Debug(2,"Using standard YUYV->grayscale fast conversion"); } subpixelorder = ZM_SUBPIX_ORDER_NONE; - } else if (palette == V4L2_PIX_FMT_YUYV && colours == ZM_COLOUR_RGB24) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_YUYV && zm_is_rgb24(pixelFormat)) { conversion_fptr = &zm_convert_yuyv_rgb; subpixelorder = ZM_SUBPIX_ORDER_RGB; - } else if (palette == V4L2_PIX_FMT_YUYV && colours == ZM_COLOUR_RGB32) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_YUYV && zm_is_rgb32(pixelFormat)) { conversion_fptr = &zm_convert_yuyv_rgba; subpixelorder = ZM_SUBPIX_ORDER_RGBA; - } else if (palette == V4L2_PIX_FMT_RGB555 && colours == ZM_COLOUR_RGB24) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB555 && zm_is_rgb24(pixelFormat)) { conversion_fptr = &zm_convert_rgb555_rgb; subpixelorder = ZM_SUBPIX_ORDER_RGB; - } else if (palette == V4L2_PIX_FMT_RGB555 && colours == ZM_COLOUR_RGB32) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB555 && zm_is_rgb32(pixelFormat)) { conversion_fptr = &zm_convert_rgb555_rgba; subpixelorder = ZM_SUBPIX_ORDER_RGBA; - } else if (palette == V4L2_PIX_FMT_RGB565 && colours == ZM_COLOUR_RGB24) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB565 && zm_is_rgb24(pixelFormat)) { conversion_fptr = &zm_convert_rgb565_rgb; subpixelorder = ZM_SUBPIX_ORDER_RGB; - } else if (palette == V4L2_PIX_FMT_RGB565 && colours == ZM_COLOUR_RGB32) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB565 && zm_is_rgb32(pixelFormat)) { conversion_fptr = &zm_convert_rgb565_rgba; subpixelorder = ZM_SUBPIX_ORDER_RGBA; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); } else { Fatal("Unable to find a suitable format conversion for the selected palette and target colorspace."); } @@ -878,11 +897,12 @@ uint32_t LocalCamera::AutoSelectFormat(int p_colours) { int nIndexUsed = -1; unsigned int n_preferedformats = 0; const uint32_t* preferedformats; - if ( p_colours == ZM_COLOUR_RGB32 ) { + AVPixelFormat p_pixfmt = zm_db_colours_to_pixformat(p_colours); + if ( zm_is_rgb32(p_pixfmt) ) { /* 32bit */ preferedformats = prefered_rgb32_formats; n_preferedformats = sizeof(prefered_rgb32_formats) / sizeof(uint32_t); - } else if ( p_colours == ZM_COLOUR_GRAY8 ) { + } else if ( p_pixfmt == AV_PIX_FMT_GRAY8 ) { /* Grayscale */ preferedformats = prefered_gray8_formats; n_preferedformats = sizeof(prefered_gray8_formats) / sizeof(uint32_t); diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 4027127e59e..b7c33642d75 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -522,6 +522,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { camera_height = atoi(dbrow[col]); col++; colours = atoi(dbrow[col]); + // colours from DB is legacy {1,3,4}. Derive the actual pixel format. col++; palette = atoi(dbrow[col]); col++; @@ -1683,7 +1684,7 @@ void Monitor::DumpZoneImage(const char *zone_string) { } } - if ( zone_image->Colours() == ZM_COLOUR_GRAY8 ) { + if (zone_image->PixFormat() == AV_PIX_FMT_GRAY8) { zone_image->Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); } @@ -1746,7 +1747,7 @@ bool Monitor::CheckSignal(const Image *image) { const uint8_t *buffer = image->Buffer(); int pixels = image->Pixels(); int width = image->Width(); - int colours = image->Colours(); + AVPixelFormat pix_fmt = image->PixFormat(); int index = 0; for (int i = 0; i < signal_check_points; i++) { @@ -1765,24 +1766,24 @@ bool Monitor::CheckSignal(const Image *image) { } } - if (colours == ZM_COLOUR_GRAY8) { + if (pix_fmt == AV_PIX_FMT_GRAY8 || zm_is_yuv420(pix_fmt)) { if (*(buffer+index) != grayscale_val) return true; - } else if (colours == ZM_COLOUR_RGB24) { - const uint8_t *ptr = buffer+(index*colours); + } else if (zm_is_rgb24(pix_fmt)) { + const uint8_t *ptr = buffer + (index * static_cast(zm_bytes_per_pixel(pix_fmt))); - if (usedsubpixorder == ZM_SUBPIX_ORDER_BGR) { + if (pix_fmt == AV_PIX_FMT_BGR24) { if ((RED_PTR_BGRA(ptr) != red_val) || (GREEN_PTR_BGRA(ptr) != green_val) || (BLUE_PTR_BGRA(ptr) != blue_val)) return true; } else { - /* Assume RGB */ + /* Assume RGB24 */ if ((RED_PTR_RGBA(ptr) != red_val) || (GREEN_PTR_RGBA(ptr) != green_val) || (BLUE_PTR_RGBA(ptr) != blue_val)) return true; } - } else if (colours == ZM_COLOUR_RGB32) { - if (usedsubpixorder == ZM_SUBPIX_ORDER_ARGB || usedsubpixorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (zm_is_rgb32(pix_fmt)) { + if (pix_fmt == AV_PIX_FMT_ARGB || pix_fmt == AV_PIX_FMT_ABGR) { if (ARGB_ABGR_ZEROALPHA(*(((const Rgb*)buffer)+index)) != ARGB_ABGR_ZEROALPHA(colour_val)) return true; } else { @@ -3069,7 +3070,34 @@ bool Monitor::Decode() { } if (!packet->image) { - packet->image = new Image(camera_width, camera_height, camera->Colours(), camera->SubpixelOrder()); + // Use a pipeline-friendly pixel format. Prefer the decoded frame's native + // format when Image can represent it (currently YUV 4:2:0 / 4:2:2 planar + // including the JPEG full-range variants, GRAY8, RGB24, and RGB32) — both + // 4:2:0 and 4:2:2 paths now have full coverage in zm_pixformat / + // zm_colours_from_pixformat. Anything outside that set falls back to a + // YUV420P conversion via swscale. + unsigned int native_colours, native_subpixelorder; + AVPixelFormat native_fmt = static_cast(packet->in_frame->format); + const char *native_fmt_name = av_get_pix_fmt_name(native_fmt); + if (!native_fmt_name) native_fmt_name = "unknown"; + + bool can_passthrough = (native_fmt == AV_PIX_FMT_YUV420P + || native_fmt == AV_PIX_FMT_YUVJ420P + || native_fmt == AV_PIX_FMT_YUV422P + || native_fmt == AV_PIX_FMT_YUVJ422P + || native_fmt == AV_PIX_FMT_GRAY8 + || zm_is_rgb24(native_fmt) + || zm_is_rgb32(native_fmt)); + + if (can_passthrough && zm_colours_from_pixformat(native_fmt, native_colours, native_subpixelorder)) { + Debug(1, "Using native frame format %s", native_fmt_name); + } else { + Debug(1, "Converting %s to yuv420p for pipeline", native_fmt_name); + native_colours = ZM_COLOUR_GRAY8; + native_subpixelorder = ZM_SUBPIX_ORDER_YUV420P; + } + + packet->image = new Image(camera_width, camera_height, native_colours, native_subpixelorder); bool have_converter = convert_context || setupConvertContext(packet->in_frame.get(), packet->image); if (have_converter) { diff --git a/src/zm_mpeg.cpp b/src/zm_mpeg.cpp index d1c10047f40..bf28032265d 100644 --- a/src/zm_mpeg.cpp +++ b/src/zm_mpeg.cpp @@ -20,6 +20,7 @@ #include "zm_mpeg.h" #include "zm_logger.h" +#include "zm_pixformat.h" #include "zm_rgb.h" #include "zm_time.h" @@ -58,38 +59,9 @@ int VideoStream::SetupCodec( int bitrate, double frame_rate ) { - /* ffmpeg format matching */ - switch (colours) { - case ZM_COLOUR_RGB24: - if (subpixelorder == ZM_SUBPIX_ORDER_BGR) { - /* BGR subpixel order */ - pf = AV_PIX_FMT_BGR24; - } else { - /* Assume RGB subpixel order */ - pf = AV_PIX_FMT_RGB24; - } - break; - case ZM_COLOUR_RGB32: - if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) { - /* ARGB subpixel order */ - pf = AV_PIX_FMT_ARGB; - } else if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - pf = AV_PIX_FMT_ABGR; - } else if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - pf = AV_PIX_FMT_BGRA; - } else { - /* Assume RGBA subpixel order */ - pf = AV_PIX_FMT_RGBA; - } - break; - case ZM_COLOUR_GRAY8: - pf = AV_PIX_FMT_GRAY8; - break; - default: - Panic("Unexpected colours: %d",colours); - break; + pf = zm_pixformat_from_colours(colours, subpixelorder); + if (pf == AV_PIX_FMT_NONE) { + Panic("Unexpected colours: %d", colours); } if (strcmp("rtp", of->name) == 0) { diff --git a/src/zm_pixformat.h b/src/zm_pixformat.h new file mode 100644 index 00000000000..88c6f49fb2a --- /dev/null +++ b/src/zm_pixformat.h @@ -0,0 +1,154 @@ +// +// ZoneMinder Pixel Format Helpers +// Copyright (C) 2026 ZoneMinder +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// + +#ifndef ZM_PIXFORMAT_H +#define ZM_PIXFORMAT_H + +#include "zm_ffmpeg.h" +#include "zm_rgb.h" + +// +// Central pixel format conversion helpers. +// +// These replace the legacy ZM_COLOUR_* / ZM_SUBPIX_ORDER_* integer pair with +// AVPixelFormat as the single source of truth. The ZM_COLOUR_* constants +// (1=GRAY8, 3=RGB24, 4=RGB32) are retained only as bytes-per-pixel values for +// backwards compatibility with pixel-stride arithmetic. +// + +// Forward mapping: (ZM colours, ZM subpixelorder) -> AVPixelFormat +inline AVPixelFormat zm_pixformat_from_colours(unsigned int colours, unsigned int subpixelorder) { + if (colours == ZM_COLOUR_GRAY8) { + if (subpixelorder == ZM_SUBPIX_ORDER_YUV420P) return AV_PIX_FMT_YUV420P; + if (subpixelorder == ZM_SUBPIX_ORDER_YUVJ420P) return AV_PIX_FMT_YUVJ420P; + if (subpixelorder == ZM_SUBPIX_ORDER_YUV422P) return AV_PIX_FMT_YUV422P; + if (subpixelorder == ZM_SUBPIX_ORDER_YUVJ422P) return AV_PIX_FMT_YUVJ422P; + return AV_PIX_FMT_GRAY8; + } + if (colours == ZM_COLOUR_RGB24) { + if (subpixelorder == ZM_SUBPIX_ORDER_BGR) return AV_PIX_FMT_BGR24; + return AV_PIX_FMT_RGB24; + } + if (colours == ZM_COLOUR_RGB32) { + if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) return AV_PIX_FMT_ARGB; + if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) return AV_PIX_FMT_ABGR; + if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) return AV_PIX_FMT_BGRA; + return AV_PIX_FMT_RGBA; + } + return AV_PIX_FMT_NONE; +} + +// Inverse mapping: AVPixelFormat -> (ZM colours, ZM subpixelorder) +// Returns true if the format is recognised, false otherwise (out params untouched). +inline bool zm_colours_from_pixformat(AVPixelFormat fmt, + unsigned int &colours, + unsigned int &subpixelorder) { + switch (fmt) { + case AV_PIX_FMT_GRAY8: + colours = ZM_COLOUR_GRAY8; + subpixelorder = ZM_SUBPIX_ORDER_NONE; + return true; + case AV_PIX_FMT_YUV420P: + colours = ZM_COLOUR_GRAY8; + subpixelorder = ZM_SUBPIX_ORDER_YUV420P; + return true; + case AV_PIX_FMT_YUVJ420P: + colours = ZM_COLOUR_GRAY8; + subpixelorder = ZM_SUBPIX_ORDER_YUVJ420P; + return true; + case AV_PIX_FMT_YUV422P: + colours = ZM_COLOUR_GRAY8; // Y-plane is 1 byte/pixel + subpixelorder = ZM_SUBPIX_ORDER_YUV422P; + return true; + case AV_PIX_FMT_YUVJ422P: + colours = ZM_COLOUR_GRAY8; // Y-plane is 1 byte/pixel + subpixelorder = ZM_SUBPIX_ORDER_YUVJ422P; + return true; + case AV_PIX_FMT_RGB24: + colours = ZM_COLOUR_RGB24; + subpixelorder = ZM_SUBPIX_ORDER_RGB; + return true; + case AV_PIX_FMT_BGR24: + colours = ZM_COLOUR_RGB24; + subpixelorder = ZM_SUBPIX_ORDER_BGR; + return true; + case AV_PIX_FMT_RGBA: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_RGBA; + return true; + case AV_PIX_FMT_BGRA: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_BGRA; + return true; + case AV_PIX_FMT_ARGB: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_ARGB; + return true; + case AV_PIX_FMT_ABGR: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_ABGR; + return true; + default: + return false; + } +} + +// Bytes-per-pixel in the primary buffer. For planar YUV formats this is the +// Y-plane stride (1), not the overall average. +inline unsigned int zm_bytes_per_pixel(AVPixelFormat fmt) { + switch (fmt) { + case AV_PIX_FMT_GRAY8: + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVJ422P: + return 1; + case AV_PIX_FMT_RGB24: + case AV_PIX_FMT_BGR24: + return 3; + case AV_PIX_FMT_RGBA: + case AV_PIX_FMT_BGRA: + case AV_PIX_FMT_ARGB: + case AV_PIX_FMT_ABGR: + return 4; + default: + return 0; + } +} + +// Map the persisted DB Monitors.Colours value to an AVPixelFormat. +// Valid DB values are {1, 3, 4}. +inline AVPixelFormat zm_db_colours_to_pixformat(int db_colours) { + switch (db_colours) { + case ZM_COLOUR_GRAY8: return AV_PIX_FMT_GRAY8; + case ZM_COLOUR_RGB24: return AV_PIX_FMT_RGB24; + case ZM_COLOUR_RGB32: return AV_PIX_FMT_RGBA; + default: return AV_PIX_FMT_NONE; + } +} + +// Format-family predicates. Prefer these over ZM_COLOUR_* comparisons when +// dispatching on pixel format. +inline bool zm_is_rgb32(AVPixelFormat fmt) { + return fmt == AV_PIX_FMT_RGBA + || fmt == AV_PIX_FMT_BGRA + || fmt == AV_PIX_FMT_ARGB + || fmt == AV_PIX_FMT_ABGR; +} + +inline bool zm_is_rgb24(AVPixelFormat fmt) { + return fmt == AV_PIX_FMT_RGB24 || fmt == AV_PIX_FMT_BGR24; +} + +inline bool zm_is_yuv420(AVPixelFormat fmt) { + return fmt == AV_PIX_FMT_YUV420P || fmt == AV_PIX_FMT_YUVJ420P; +} + +#endif // ZM_PIXFORMAT_H diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index 501d9aa49cb..ec355e06636 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -69,15 +69,18 @@ RemoteCameraRtsp::RemoteCameraRtsp( } /* Has to be located inside the constructor so other components such as zma will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; imagePixFormat = AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGB; imagePixFormat = AV_PIX_FMT_RGB24; - } else if ( colours == ZM_COLOUR_GRAY8 ) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; imagePixFormat = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected colours: %d", colours); } diff --git a/src/zm_rgb.h b/src/zm_rgb.h index 66b1c169c78..eb40e76b88c 100644 --- a/src/zm_rgb.h +++ b/src/zm_rgb.h @@ -100,15 +100,18 @@ constexpr Rgb kRGBTransparent = 0x01000000; // #define RGB_FASTLUM_SINGLE_ITU601(v) ((RED(v)+RED(v)+RED(v)+BLUE(v)+GREEN(v)+GREEN(v)+GREEN(v)+GREEN(v))>>3) // #define RGB_FASTLUM_VALUES_ITU601(ra,ga,ba) (((ra)+(ra)+(ra)+(ba)+(ga)+(ga)+(ga)+(ga))>>3) -/* ZM colours */ +// DEPRECATED: ZM_COLOUR_* and ZM_SUBPIX_ORDER_* are being replaced by +// AVPixelFormat throughout the codebase. Use the helpers in zm_pixformat.h for +// format identification. These values are retained only for byte-per-pixel +// stride arithmetic and backwards compatibility with the DB Monitors.Colours +// column which stores {1, 3, 4}. #define ZM_COLOUR_RGB32 4 #define ZM_COLOUR_RGB24 3 #define ZM_COLOUR_GRAY8 1 -#define ZM_COLOUR_YUV420P 1 -#define ZM_COLOUR_YUVJ420P 1 +#define ZM_COLOUR_YUV420P 1 // DEPRECATED: collides with GRAY8; use AVPixelFormat +#define ZM_COLOUR_YUVJ420P 1 // DEPRECATED: collides with GRAY8; use AVPixelFormat -/* Subpixel ordering */ -/* Based on byte order naming. For example, for ARGB (on both little endian or big endian) byte+0 should be alpha, byte+1 should be red, and so on. */ +// DEPRECATED: Subpixel ordering constants. Use AVPixelFormat directly. #define ZM_SUBPIX_ORDER_NONE 2 #define ZM_SUBPIX_ORDER_RGB 6 #define ZM_SUBPIX_ORDER_BGR 5 @@ -118,9 +121,11 @@ constexpr Rgb kRGBTransparent = 0x01000000; #define ZM_SUBPIX_ORDER_ARGB 10 #define ZM_SUBPIX_ORDER_YUV420P 11 #define ZM_SUBPIX_ORDER_YUVJ420P 12 +#define ZM_SUBPIX_ORDER_YUV422P 13 +#define ZM_SUBPIX_ORDER_YUVJ422P 14 -/* A macro to use default subpixel order for a specified colour. */ -/* for grayscale it will use NONE, for 3 colours it will use R,G,B, for 4 colours it will use R,G,B,A */ +// DEPRECATED: Use zm_pixformat_from_colours() in zm_pixformat.h instead. +// Only valid for DB-persisted colours values {1, 3, 4}. #define ZM_SUBPIX_ORDER_DEFAULT_FOR_COLOUR(c) ((c)<<1) /* Convert RGB colour value into BGR\ARGB\ABGR */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 508a6ac380b..0360a5a8707 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,7 @@ set(TEST_SOURCES zm_crypt.cpp zm_font.cpp zm_onvif_renewal.cpp + zm_pixformat.cpp zm_poly.cpp zm_utils.cpp zm_vector2.cpp diff --git a/tests/zm_pixformat.cpp b/tests/zm_pixformat.cpp new file mode 100644 index 00000000000..aa7fad03467 --- /dev/null +++ b/tests/zm_pixformat.cpp @@ -0,0 +1,201 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "zm_catch2.h" + +#include "zm_pixformat.h" + +TEST_CASE("zm_pixformat_from_colours: GRAY8 family", "[pixformat]") { + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE) == AV_PIX_FMT_GRAY8); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P) == AV_PIX_FMT_YUV420P); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUVJ420P) == AV_PIX_FMT_YUVJ420P); +} + +TEST_CASE("zm_pixformat_from_colours: RGB24 family", "[pixformat]") { + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB) == AV_PIX_FMT_RGB24); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_BGR) == AV_PIX_FMT_BGR24); +} + +TEST_CASE("zm_pixformat_from_colours: RGB32 family", "[pixformat]") { + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA) == AV_PIX_FMT_RGBA); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_BGRA) == AV_PIX_FMT_BGRA); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_ARGB) == AV_PIX_FMT_ARGB); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_ABGR) == AV_PIX_FMT_ABGR); +} + +TEST_CASE("zm_pixformat_from_colours: unknown returns NONE", "[pixformat]") { + REQUIRE(zm_pixformat_from_colours(0, 0) == AV_PIX_FMT_NONE); + REQUIRE(zm_pixformat_from_colours(2, 0) == AV_PIX_FMT_NONE); + REQUIRE(zm_pixformat_from_colours(5, 0) == AV_PIX_FMT_NONE); +} + +TEST_CASE("zm_colours_from_pixformat: maps each supported format", "[pixformat]") { + unsigned int c = 0, s = 0; + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_GRAY8, c, s)); + REQUIRE(c == ZM_COLOUR_GRAY8); + REQUIRE(s == ZM_SUBPIX_ORDER_NONE); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUV420P, c, s)); + REQUIRE(c == ZM_COLOUR_GRAY8); // collision: same value as ZM_COLOUR_YUV420P + REQUIRE(s == ZM_SUBPIX_ORDER_YUV420P); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUVJ420P, c, s)); + REQUIRE(c == ZM_COLOUR_GRAY8); // collision + REQUIRE(s == ZM_SUBPIX_ORDER_YUVJ420P); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUV422P, c, s)); + REQUIRE(c == ZM_COLOUR_GRAY8); // collision: 4:2:2 also aliases to GRAY8 + REQUIRE(s == ZM_SUBPIX_ORDER_YUV422P); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUVJ422P, c, s)); + REQUIRE(c == ZM_COLOUR_GRAY8); // collision + REQUIRE(s == ZM_SUBPIX_ORDER_YUVJ422P); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_RGB24, c, s)); + REQUIRE(c == ZM_COLOUR_RGB24); + REQUIRE(s == ZM_SUBPIX_ORDER_RGB); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_BGR24, c, s)); + REQUIRE(c == ZM_COLOUR_RGB24); + REQUIRE(s == ZM_SUBPIX_ORDER_BGR); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_RGBA, c, s)); + REQUIRE(c == ZM_COLOUR_RGB32); + REQUIRE(s == ZM_SUBPIX_ORDER_RGBA); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_BGRA, c, s)); + REQUIRE(c == ZM_COLOUR_RGB32); + REQUIRE(s == ZM_SUBPIX_ORDER_BGRA); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_ARGB, c, s)); + REQUIRE(c == ZM_COLOUR_RGB32); + REQUIRE(s == ZM_SUBPIX_ORDER_ARGB); + + REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_ABGR, c, s)); + REQUIRE(c == ZM_COLOUR_RGB32); + REQUIRE(s == ZM_SUBPIX_ORDER_ABGR); +} + +TEST_CASE("zm_colours_from_pixformat: unknown returns false and leaves out params untouched", "[pixformat]") { + unsigned int c = 42, s = 99; + REQUIRE_FALSE(zm_colours_from_pixformat(AV_PIX_FMT_YUV444P, c, s)); + REQUIRE(c == 42); + REQUIRE(s == 99); +} + +TEST_CASE("round-trip: from_colours(colours_from_pixformat(fmt)) == fmt", "[pixformat]") { + const AVPixelFormat formats[] = { + AV_PIX_FMT_GRAY8, + AV_PIX_FMT_YUV420P, + AV_PIX_FMT_YUVJ420P, + AV_PIX_FMT_YUV422P, + AV_PIX_FMT_YUVJ422P, + AV_PIX_FMT_RGB24, + AV_PIX_FMT_BGR24, + AV_PIX_FMT_RGBA, + AV_PIX_FMT_BGRA, + AV_PIX_FMT_ARGB, + AV_PIX_FMT_ABGR, + }; + for (AVPixelFormat fmt : formats) { + unsigned int c = 0, s = 0; + REQUIRE(zm_colours_from_pixformat(fmt, c, s)); + REQUIRE(zm_pixformat_from_colours(c, s) == fmt); + } +} + +TEST_CASE("zm_bytes_per_pixel: primary-buffer stride", "[pixformat]") { + // GRAY8 and YUV planar Y-plane all have 1-byte stride in the primary buffer + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_GRAY8) == 1); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_YUV420P) == 1); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_YUVJ420P) == 1); + + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_RGB24) == 3); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_BGR24) == 3); + + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_RGBA) == 4); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_BGRA) == 4); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_ARGB) == 4); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_ABGR) == 4); + + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_NONE) == 0); + REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_YUV444P) == 0); +} + +TEST_CASE("zm_db_colours_to_pixformat: maps DB values 1/3/4", "[pixformat]") { + REQUIRE(zm_db_colours_to_pixformat(ZM_COLOUR_GRAY8) == AV_PIX_FMT_GRAY8); + REQUIRE(zm_db_colours_to_pixformat(ZM_COLOUR_RGB24) == AV_PIX_FMT_RGB24); + REQUIRE(zm_db_colours_to_pixformat(ZM_COLOUR_RGB32) == AV_PIX_FMT_RGBA); + REQUIRE(zm_db_colours_to_pixformat(0) == AV_PIX_FMT_NONE); + REQUIRE(zm_db_colours_to_pixformat(2) == AV_PIX_FMT_NONE); +} + +TEST_CASE("zm_is_rgb32: matches 4-byte packed RGB formats only", "[pixformat]") { + REQUIRE(zm_is_rgb32(AV_PIX_FMT_RGBA)); + REQUIRE(zm_is_rgb32(AV_PIX_FMT_BGRA)); + REQUIRE(zm_is_rgb32(AV_PIX_FMT_ARGB)); + REQUIRE(zm_is_rgb32(AV_PIX_FMT_ABGR)); + + REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_RGB24)); + REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_BGR24)); + REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_GRAY8)); + REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_YUV420P)); + REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_YUVJ420P)); + REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_NONE)); +} + +TEST_CASE("zm_is_rgb24: matches 3-byte packed RGB formats only", "[pixformat]") { + REQUIRE(zm_is_rgb24(AV_PIX_FMT_RGB24)); + REQUIRE(zm_is_rgb24(AV_PIX_FMT_BGR24)); + + REQUIRE_FALSE(zm_is_rgb24(AV_PIX_FMT_RGBA)); + REQUIRE_FALSE(zm_is_rgb24(AV_PIX_FMT_GRAY8)); + REQUIRE_FALSE(zm_is_rgb24(AV_PIX_FMT_YUV420P)); +} + +TEST_CASE("zm_is_yuv420: matches planar YUV 4:2:0 formats only", "[pixformat]") { + REQUIRE(zm_is_yuv420(AV_PIX_FMT_YUV420P)); + REQUIRE(zm_is_yuv420(AV_PIX_FMT_YUVJ420P)); + + REQUIRE_FALSE(zm_is_yuv420(AV_PIX_FMT_GRAY8)); + REQUIRE_FALSE(zm_is_yuv420(AV_PIX_FMT_YUV444P)); + REQUIRE_FALSE(zm_is_yuv420(AV_PIX_FMT_RGBA)); +} + +// Regression test for the ZM_COLOUR_GRAY8 == ZM_COLOUR_YUV420P collision. +// The collision is real in zm_rgb.h (both defined to 1) but the AVPixelFormat +// path must disambiguate correctly via subpixelorder, and user-selectable DB +// colours must not be mistaken for the internal YUV420P format. +TEST_CASE("regression: GRAY8/YUV420P collision does not confuse the pipeline", "[pixformat]") { + // The collision itself + REQUIRE(ZM_COLOUR_GRAY8 == ZM_COLOUR_YUV420P); + REQUIRE(ZM_COLOUR_GRAY8 == ZM_COLOUR_YUVJ420P); + + // But the format helpers disambiguate by subpixelorder + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE) == AV_PIX_FMT_GRAY8); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P) == AV_PIX_FMT_YUV420P); + REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUVJ420P) == AV_PIX_FMT_YUVJ420P); + + // DB value 1 (user-selected "8BitGrey") unambiguously maps to GRAY8, not YUV420P + REQUIRE(zm_db_colours_to_pixformat(1) == AV_PIX_FMT_GRAY8); + REQUIRE(zm_db_colours_to_pixformat(1) != AV_PIX_FMT_YUV420P); + + // DB value 4 (user-selected "32BitColour") maps to RGBA, not GRAY8 + REQUIRE(zm_db_colours_to_pixformat(4) == AV_PIX_FMT_RGBA); + REQUIRE(zm_db_colours_to_pixformat(4) != AV_PIX_FMT_GRAY8); +} diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 3f9db81ded4..83e865ce2f7 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -655,6 +655,7 @@ 'StorageScheme' => 'Scheme', 'StreamReplayBuffer' => 'Stream Replay Image Buffer', 'TargetColorspace' => 'Target colorspace', + 'DeprecatedColoursSetting' => 'Deprecated - will be auto-detected in a future release', 'TimeDelta' => 'Time Delta', 'TimelineTip1' => 'Pass your mouse over the graph to view a snapshot image and event details.', // Added 2013.08.15. 'TimelineTip2' => 'Click on the coloured sections of the graph, or the image, to view the event.', // Added 2013.08.15. diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 85061666511..3a3dde26aeb 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -907,6 +907,7 @@ class="nav-link Colours()) ?> + ()
  • From 45a467107b9d1e4dd99bf65687c92cc0c324cea5 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 3 May 2026 10:00:53 -0400 Subject: [PATCH 02/35] fix: planar YUV correctness + alignment in Image methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several Image methods were silently producing wrong output for planar YUV formats and for any format-width pair where the natural linesize isn't a multiple of 32. Each surfaced as a different visual artefact once the SHM started carrying YUV420P/RGB32/etc. through to the JPEG encoders without an upfront convert-to-RGB32 step: - Image::Assign(const Image &): sized new_size from image.height * image.linesize, which counts only the Y plane for planar layouts. Use image.size, which is populated from av_image_get_buffer_size and includes chroma. Without this an Assign'd YUV420P image left Cb/Cr at zero — solid green output via YCbCr-to-RGB later. - Image::AssignDirect(width,height,colours,...): computed new_buffer_size as W*H*p_colours and linesize as W*p_colours, both wrong for planar (1.5x undersize) and for non-32-aligned RGB widths (the actual buffer stride after sws_scale + av_image_fill_arrays with align=32 is FFALIGN(W*bpp, 32), not W*bpp). Use av_image_get_buffer_size and FFALIGN(av_image_get_linesize(...), 32) so the recorded size/linesize match the real buffer layout. Mismatched linesize produced the diagonal-shift artefact in RGB32 streams at scaled widths like 1094. - Image::WriteBuffer: same FFALIGN linesize fix; it was using the unaligned natural linesize too. - Image::Scale(new_width, new_height): scale_buffer was sized (new_W+1)*(new_H+1)*colours which undercounts planar formats. SWScale::Convert correctly checks the buffer against av_image_get_buffer_size and returns an error, but the caller ignored the return value and AssignDirect'd the empty/uninit buffer anyway. Size with av_image_get_buffer_size, check the Convert return, free and bail on failure. - Image::Scale(factor): the hand-rolled pixel-doubling/decimation loop treated the buffer as packed `colours`-bytes-per-pixel — scaled only the Y plane and dropped chroma. Drop the loop and delegate to Scale(new_width, new_height), which now uses sws_scale for all formats. - Image::EncodeJpeg + Image::WriteJpeg: format dispatch and scanline writer had no planar-YUV support. WriteJpeg's existing branch unpacked the buffer as packed YUYV 4:2:2 with offset scanline*W*2, which read W*H/2 bytes past the end of any YUV420P buffer — a latent crash that became reachable once image_buffer could carry YUV420P data, segfaulting from the Event thread on every event-frame write. Add a planar branch that uses av_image_fill_arrays for plane pointers and per-plane linesizes and feeds JCS_YCbCr scanlines built from the Y/U/V planes — works for both 4:2:0 (YUV420P/YUVJ420P) and 4:2:2 (YUV422P/YUVJ422P). - Image::Overlay: the warning fired on a benign GRAY8-on-YUV420P case (both report colours=1 due to the GRAY8/YUV420P alias collision in zm_rgb.h, but their imagePixFormat differs). Reframe the check so it only warns when imagePixFormat actually matches but the ZM (colours, subpixelorder) metadata diverges — i.e. a real format-tracking bug. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 277 +++++++++++++++++++++++++---------------------- 1 file changed, 149 insertions(+), 128 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index da047c6bd01..8a7e8fd185a 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -740,7 +740,13 @@ uint8_t* Image::WriteBuffer( p_pixfmt, p_width, p_height); return nullptr; } - int av_linesize = av_image_get_linesize(p_pixfmt, p_width, 0); + // FFALIGN to 32 to match what av_image_get_buffer_size with align=32 + // assumes for the buffer layout, and what sws_scale writes when it + // produces this format/width pair. Using the unaligned natural linesize + // here would mismatch the buffer's actual row stride and cause + // diagonal-shift artefacts in any consumer that walks the buffer at + // Image::linesize stride (e.g. EncodeJpeg's non-planar path). + int av_linesize = FFALIGN(av_image_get_linesize(p_pixfmt, p_width, 0), 32); if (av_linesize < 0) { Error("WriteBuffer: av_image_get_linesize failed for fmt=%d width=%u", p_pixfmt, p_width); @@ -832,16 +838,32 @@ void Image::AssignDirect( return; } - if (zm_pixformat_from_colours(p_colours, p_subpixelorder) == AV_PIX_FMT_NONE) { + AVPixelFormat new_pix_fmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + if (new_pix_fmt == AV_PIX_FMT_NONE) { Error("Attempt to directly assign buffer with unexpected colours per pixel: %d", p_colours); return; } - size_t new_buffer_size = static_cast(p_width) * p_height * p_colours; + // p_width * p_height * p_colours undercounts planar formats (YUV420P needs + // ~1.5x; with the GRAY8 alias colours=1 here would otherwise treat it as + // GRAY8 size W*H). Use av_image_* so the size and linesize match what the + // pixel format actually requires. + int av_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 32); + // FFALIGN the linesize to 32 to match the buffer's actual row stride + // (av_image_get_buffer_size with align=32 assumes this layout, and + // sws_scale writes in this layout). Storing the unaligned natural + // linesize would cause downstream consumers that walk the buffer at + // Image::linesize stride to drift sideways every row. + int av_linesize = FFALIGN(av_image_get_linesize(new_pix_fmt, p_width, 0), 32); + if (av_size < 0 || av_linesize < 0) { + Error("AssignDirect: av_image sizing failed for %s %ux%u", av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + return; + } + size_t new_buffer_size = static_cast(av_size); if ( buffer_size < new_buffer_size ) { - Error("Attempt to directly assign buffer from an undersized buffer of size: %zu, needed %dx%d*%d colours = %zu", - buffer_size, p_width, p_height, p_colours, new_buffer_size); + Error("Attempt to directly assign buffer from an undersized buffer of size: %zu, needed %dx%d %s = %zu", + buffer_size, p_width, p_height, av_get_pix_fmt_name(new_pix_fmt), new_buffer_size); return; } @@ -852,7 +874,7 @@ void Image::AssignDirect( } else { /* Copy into the held buffer */ if ( new_buffer != buffer ) { - (*fptr_imgbufcpy)(buffer, new_buffer, size); + (*fptr_imgbufcpy)(buffer, new_buffer, new_buffer_size); } /* Free the new buffer */ DumpBuffer(new_buffer, p_buffertype); @@ -868,9 +890,9 @@ void Image::AssignDirect( width = p_width; height = p_height; colours = p_colours; - linesize = width * colours; + linesize = static_cast(av_linesize); subpixelorder = p_subpixelorder; - imagePixFormat = zm_pixformat_from_colours(colours, subpixelorder); + imagePixFormat = new_pix_fmt; pixels = width * height; size = new_buffer_size; update_function_pointers(); @@ -934,7 +956,12 @@ void Image::Assign( } void Image::Assign(const Image &image) { - unsigned int new_size = image.height * image.linesize; + // image.size is the AVPixelFormat-aware buffer size (av_image_get_buffer_size, + // including chroma planes for planar YUV); image.height * image.linesize is + // only the Y plane for planar formats and would silently leave U/V planes + // uninitialised in the destination — producing a Cb=Cr=0 "solid green" image + // when the dest is later interpreted as YCbCr. + unsigned int new_size = image.size; if ( image.buffer == nullptr ) { Error("Attempt to assign image with an empty buffer"); @@ -953,7 +980,7 @@ void Image::Assign(const Image &image) { if ( holdbuffer && buffer ) { if ( new_size > allocation ) { - Error("Held buffer is undersized for assigned buffer"); + Error("Held buffer is undersized for assigned buffer (need %u, have %lu)", new_size, allocation); return; } } else { @@ -976,6 +1003,10 @@ void Image::Assign(const Image &image) { if ( image.buffer != buffer ) { if (image.linesize > linesize) { + // This branch is only reached when dimensions and colours/subpixelorder + // match but linesize disagrees, which is an oddly-shaped Image. Copy the + // Y/primary plane line by line; planar chroma planes are not handled + // here. The common path is the flat copy below. Debug(1, "Must copy line by line due to different line size %d != %d", image.linesize, linesize); uint8_t *src_ptr = image.buffer; uint8_t *dst_ptr = buffer; @@ -985,7 +1016,7 @@ void Image::Assign(const Image &image) { dst_ptr += linesize; } } else { - Debug(4, "Doing full copy line size %d != %d", image.linesize, linesize); + Debug(4, "Doing full copy line size %d == %d, %u bytes", image.linesize, linesize, size); (*fptr_imgbufcpy)(buffer, image.buffer, size); } } @@ -1376,6 +1407,10 @@ bool Image::WriteJpeg(const std::string &filename, fclose(outfile); return false; #endif + } else if (imagePixFormat == AV_PIX_FMT_YUV420P || imagePixFormat == AV_PIX_FMT_YUVJ420P + || imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P) { + cinfo->input_components = 3; + cinfo->in_color_space = JCS_YCbCr; } else { /* Assume RGB24/BGR24 */ cinfo->input_components = 3; @@ -1390,17 +1425,8 @@ bool Image::WriteJpeg(const std::string &filename, fclose(outfile); return false; #endif - } else if (zm_is_yuv420(imagePixFormat)) { - cinfo->in_color_space = JCS_YCbCr; } else { /* Assume RGB */ - /* - #ifdef JCS_EXTENSIONS - cinfo->out_color_space = JCS_EXT_RGB; - #else - cinfo->out_color_space = JCS_RGB; - #endif - */ cinfo->in_color_space = JCS_RGB; } } // end format dispatch @@ -1446,21 +1472,42 @@ bool Image::WriteJpeg(const std::string &filename, jpeg_write_marker(cinfo, EXIF_CODE, (const JOCTET *) exiftimes, sizeof(exiftimes)); } - if (zm_is_yuv420(imagePixFormat)) { - std::vector tmprowbuf(width * 3); - JSAMPROW row_pointer = &tmprowbuf[0]; /* pointer to a single row */ + if (imagePixFormat == AV_PIX_FMT_YUV420P || imagePixFormat == AV_PIX_FMT_YUVJ420P + || imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P) { + // Planar YUV: interleave Y/U/V plane rows into per-scanline YCbCr triples. + // The previous code here unpacked the buffer as if it were packed YUYV + // 4:2:2, which read way past the W*H*1.5 YUV420P buffer end on every + // frame — a latent crash that became reachable when image_buffer was + // pinned to YUV420P. Use av_image_fill_arrays for plane offsets so this + // works for both 4:2:0 and 4:2:2 layouts. + uint8_t *plane[4] = {nullptr, nullptr, nullptr, nullptr}; + int plane_linesizes[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(plane, plane_linesizes, buffer, imagePixFormat, width, height, 32) < 0) { + Error("WriteJpeg: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + jpeg_abort_compress(cinfo); + fl.l_type = F_UNLCK; + fcntl(raw_fd, F_SETLK, &fl); + fclose(outfile); + return false; + } + const bool is_422 = (imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P); + const unsigned chroma_v_shift = is_422 ? 0u : 1u; + + std::vector tmprow(static_cast(width) * 3); + JSAMPROW row_ptr = tmprow.data(); + while (cinfo->next_scanline < cinfo->image_height) { - unsigned i, j; - unsigned offset = cinfo->next_scanline * cinfo->image_width * 2; //offset to the correct row - for (i = 0, j = 0; i < cinfo->image_width * 2; i += 4, j += 6) { //input strides by 4 bytes, output strides by 6 (2 pixels) - tmprowbuf[j + 0] = buffer[offset + i + 0]; // Y (unique to this pixel) - tmprowbuf[j + 1] = buffer[offset + i + 1]; // U (shared between pixels) - tmprowbuf[j + 2] = buffer[offset + i + 3]; // V (shared between pixels) - tmprowbuf[j + 3] = buffer[offset + i + 2]; // Y (unique to this pixel) - tmprowbuf[j + 4] = buffer[offset + i + 1]; // U (shared between pixels) - tmprowbuf[j + 5] = buffer[offset + i + 3]; // V (shared between pixels) + const unsigned y = cinfo->next_scanline; + const unsigned cy = y >> chroma_v_shift; + const uint8_t *yrow = plane[0] + static_cast(y) * plane_linesizes[0]; + const uint8_t *urow = plane[1] + static_cast(cy) * plane_linesizes[1]; + const uint8_t *vrow = plane[2] + static_cast(cy) * plane_linesizes[2]; + for (unsigned x = 0; x < width; ++x) { + tmprow[x * 3 + 0] = yrow[x]; + tmprow[x * 3 + 1] = urow[x >> 1]; + tmprow[x * 3 + 2] = vrow[x >> 1]; } - jpeg_write_scanlines(cinfo, &row_pointer, 1); + jpeg_write_scanlines(cinfo, &row_ptr, 1); } } else { JSAMPROW row_pointer = buffer; /* pointer to a single row */ @@ -1620,6 +1667,10 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov cinfo->image_width = width; /* image width and height, in pixels */ cinfo->image_height = height; + const bool is_yuv_planar = + (imagePixFormat == AV_PIX_FMT_YUV420P || imagePixFormat == AV_PIX_FMT_YUVJ420P + || imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P); + if (imagePixFormat == AV_PIX_FMT_GRAY8) { cinfo->input_components = 1; cinfo->in_color_space = JCS_GRAYSCALE; @@ -1644,6 +1695,9 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov jpeg_abort_compress(cinfo); return false; #endif + } else if (is_yuv_planar) { + cinfo->input_components = 3; + cinfo->in_color_space = JCS_YCbCr; } else { /* Assume RGB24/BGR24 */ cinfo->input_components = 3; @@ -1657,13 +1711,6 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov #endif } else { /* Assume RGB */ - /* - #ifdef JCS_EXTENSIONS - cinfo->out_color_space = JCS_EXT_RGB; - #else - cinfo->out_color_space = JCS_RGB; - #endif - */ cinfo->in_color_space = JCS_RGB; } } // end format dispatch @@ -1674,10 +1721,44 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov jpeg_start_compress(cinfo, TRUE); - JSAMPROW row_pointer = buffer; - while ( cinfo->next_scanline < cinfo->image_height ) { - jpeg_write_scanlines(cinfo, &row_pointer, 1); - row_pointer += linesize; + if (is_yuv_planar) { + // Planar YUV: interleave Y/U/V plane rows into per-scanline YCbCr triples + // for libjpeg's JCS_YCbCr input. av_image_fill_arrays computes plane + // pointers and per-plane linesizes from the AVPixelFormat, so this works + // for both 4:2:0 (YUV420P/YUVJ420P) and 4:2:2 (YUV422P/YUVJ422P) without + // hand-coded plane offsets. + uint8_t *plane[4] = {nullptr, nullptr, nullptr, nullptr}; + int plane_linesizes[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(plane, plane_linesizes, buffer, imagePixFormat, width, height, 32) < 0) { + Error("EncodeJpeg: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + jpeg_abort_compress(cinfo); + return false; + } + const bool is_422 = (imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P); + const unsigned chroma_v_shift = is_422 ? 0u : 1u; // 4:2:0 halves vertical too + + std::vector tmprow(static_cast(width) * 3); + JSAMPROW row_ptr = tmprow.data(); + + while (cinfo->next_scanline < cinfo->image_height) { + const unsigned y = cinfo->next_scanline; + const unsigned cy = y >> chroma_v_shift; + const uint8_t *yrow = plane[0] + static_cast(y) * plane_linesizes[0]; + const uint8_t *urow = plane[1] + static_cast(cy) * plane_linesizes[1]; + const uint8_t *vrow = plane[2] + static_cast(cy) * plane_linesizes[2]; + for (unsigned x = 0; x < width; ++x) { + tmprow[x * 3 + 0] = yrow[x]; + tmprow[x * 3 + 1] = urow[x >> 1]; // chroma is 2x horizontal subsampled in 4:2:0/4:2:2 + tmprow[x * 3 + 2] = vrow[x >> 1]; + } + jpeg_write_scanlines(cinfo, &row_ptr, 1); + } + } else { + JSAMPROW row_pointer = buffer; + while ( cinfo->next_scanline < cinfo->image_height ) { + jpeg_write_scanlines(cinfo, &row_pointer, 1); + row_pointer += linesize; + } } jpeg_finish_compress(cinfo); @@ -2990,21 +3071,29 @@ void Image::Flip( bool leftright ) { void Image::Scale(const unsigned int new_width, const unsigned int new_height) { if (width == new_width and height == new_height) return; - // Why larger than we need? - size_t scale_buffer_size = static_cast(new_width+1) * (new_height+1) * colours; + AVPixelFormat format = AVPixFormat(); + // (new_width+1)*(new_height+1)*colours undercounts planar formats: for + // YUV420P, colours=1 due to the GRAY8 alias, but the buffer needs + // ~1.5x for chroma. Use av_image_get_buffer_size so this works for any + // AVPixelFormat. SWScale::Convert checks the buffer size against this + // and errors out if undersized. + int new_size = av_image_get_buffer_size(format, new_width, new_height, 32); + if (new_size < 0) { + Error("Scale: av_image_get_buffer_size failed for %s %ux%u", av_get_pix_fmt_name(format), new_width, new_height); + return; + } + size_t scale_buffer_size = static_cast(new_size); uint8_t* scale_buffer = AllocBuffer(scale_buffer_size); - AVPixelFormat format = AVPixFormat(); SWScale swscale; swscale.init(); - swscale.Convert( buffer, allocation, - scale_buffer, - scale_buffer_size, - format, format, - width, - height, - new_width, - new_height); + if (swscale.Convert(buffer, allocation, scale_buffer, scale_buffer_size, + format, format, width, height, new_width, new_height) < 0) { + Error("Scale: sws_scale conversion failed (%ux%u %s -> %ux%u)", + width, height, av_get_pix_fmt_name(format), new_width, new_height); + DumpBuffer(scale_buffer, ZM_BUFTYPE_ZM); + return; + } AssignDirect(new_width, new_height, colours, subpixelorder, scale_buffer, scale_buffer_size, ZM_BUFTYPE_ZM); } @@ -3017,81 +3106,13 @@ void Image::Scale(const unsigned int factor) { return; } + // Delegate to the (new_width, new_height) variant, which uses sws_scale. + // The previous hand-rolled pixel-doubling/decimation loops here did not + // understand planar YUV layouts (only the Y plane was scaled, the U/V + // planes were dropped), so a YUV420P image came out garbled. unsigned int new_width = (width*factor)/ZM_SCALE_BASE; unsigned int new_height = (height*factor)/ZM_SCALE_BASE; - - // Why larger than we need? - size_t scale_buffer_size = static_cast(new_width+1) * (new_height+1) * colours; - - uint8_t* scale_buffer = AllocBuffer(scale_buffer_size); - - if ( factor > ZM_SCALE_BASE ) { - unsigned char *pd = scale_buffer; - unsigned int wc = width*colours; - unsigned int nwc = new_width*colours; - unsigned int h_count = ZM_SCALE_BASE/2; - unsigned int last_h_index = 0; - unsigned int last_w_index = 0; - for ( unsigned int y = 0; y < height; y++ ) { - unsigned char *ps = &buffer[y*wc]; - unsigned int w_count = ZM_SCALE_BASE/2; - last_w_index = 0; - for ( unsigned int x = 0; x < width; x++ ) { - w_count += factor; - unsigned int w_index = w_count/ZM_SCALE_BASE; - for (unsigned int f = last_w_index; f < w_index; f++ ) { - for ( unsigned int c = 0; c < colours; c++ ) { - *pd++ = *(ps+c); - } - } - ps += colours; - last_w_index = w_index; - } - h_count += factor; - unsigned int h_index = h_count/ZM_SCALE_BASE; - for ( unsigned int f = last_h_index+1; f < h_index; f++ ) { - memcpy(pd, pd-nwc, nwc); - pd += nwc; - } - last_h_index = h_index; - } // end foreach line - new_width = last_w_index; - new_height = last_h_index; - } else { - unsigned char *pd = scale_buffer; - unsigned int wc = width*colours; - unsigned int h_count = factor/2; - unsigned int last_h_index = 0; - unsigned int last_w_index = 0; - for ( unsigned int y = 0; y < height; y++ ) { - h_count += factor; - unsigned int h_index = h_count/ZM_SCALE_BASE; - if ( h_index > last_h_index ) { - unsigned int w_count = factor/2; - unsigned int w_index; - last_w_index = 0; - - unsigned char *ps = &buffer[y*wc]; - for ( unsigned int x = 0; x < width; x++ ) { - w_count += factor; - w_index = w_count/ZM_SCALE_BASE; - - if ( w_index > last_w_index ) { - for ( unsigned int c = 0; c < colours; c++ ) { - *pd++ = *ps++; - } - } else { - ps += colours; - } - last_w_index = w_index; - } - } - last_h_index = h_index; - } - new_width = last_w_index; - new_height = last_h_index; - } // end foreach line - AssignDirect(new_width, new_height, colours, subpixelorder, scale_buffer, scale_buffer_size, ZM_BUFTYPE_ZM); + Scale(new_width, new_height); } void Image::Deinterlace_Discard() { From c8d8cc384a66dcd8c5df415ad214da1e1ae1ed60 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 3 May 2026 10:01:30 -0400 Subject: [PATCH 03/35] feat: SHM cross-process format consistency via per-slot AVPixelFormat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deprecated Monitors.Colours column was driving the on-disk SHM image_buffer slot format end-to-end: zmc allocated each slot in camera->Colours()/SubpixelOrder() shape, zms attached and built its own per-process Image objects assuming the same shape. Anything that wrote a different format into a slot — i.e. anything that wasn't already RGB32 on a Colours=4 monitor — left zmc's local Image with the new format but zms reading the bytes as RGB32. The visible artefacts varied with the format pair (4 small images per row for YUV420P-into-RGB32, 3 tiles for the planar-as-RGB24 fallthrough, fully garbled colours for image_pixelformats overlapping alarm_image, etc.) but the underlying contract was just broken — the SHM had no representation of the per-slot format. Make the SHM transport any format Image supports without an upfront convert step: - Place image_pixelformats[] correctly. mem_size already reserved image_buffer_count*sizeof(AVPixelFormat) at the end of the SHM region, but the pointer was set to shared_images + image_buffer_count*image_size — overlapping the alarm_image region. zmc's writes scribbled into alarm_image; zms read alarm pixel data back as enum values. Move it to +2*image_buffer_count*image_size, after both image regions. - Add Monitor::WriteShmFrame(index, capture_image): no conversion, just Assign and record image_pixelformats[index] = capture_image->AVPixFormat(). Used by both the normal Decode path and the signal-loss path so they share the same cross-process format-consistency contract. - Add Monitor::ReadShmFrame(index): the read-side counterpart. Calls image_buffer[index]->AVPixFormat(image_pixelformats[index]) before returning so other-process Image objects interpret the SHM bytes correctly per-frame. zm_monitorstream's image_buffer[index] accesses go through this accessor. - image_buffer[i] / alarm_image: initial format is now just a placeholder (YUV420P), with allocation sized from camera->ImageSize() as the upper bound. The actual format carried in each slot is the one zmc wrote, recorded in image_pixelformats and adopted on read. This means the typical capture pipelines run with zero sws_scale calls in the SHM hot path: - H.264/H.265 sw decode -> YUV420P -> identity copy via av_image_copy in Image::Assign(AVFrame*). - LocalCamera/MJPEG -> RGB32 (DecodeJpeg target) -> SHM holds RGB32; zms reads RGB32 directly. - H.265 hwaccel -> NV12 -> falls through to YUV420P (NV12 isn't in zm_pixformat) — one convert at decode time, then passthrough through the rest of the pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_monitor.cpp | 84 ++++++++++++++++++++++++++++++++-------- src/zm_monitor.h | 10 +++++ src/zm_monitorstream.cpp | 10 ++--- 3 files changed, 82 insertions(+), 22 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index b7c33642d75..b852c4cd253 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1120,19 +1120,33 @@ bool Monitor::connect() { image_buffer.resize(image_buffer_count); for (int32_t i = 0; i < image_buffer_count; i++) { - image_buffer[i] = new Image(width, height, camera->Colours(), camera->SubpixelOrder(), &(shared_images[i*image_size])); + // The initial format is a placeholder. The actual SHM bytes can be in + // any AVPixelFormat zm_pixformat supports — zmc records the per-slot + // format via image_pixelformats[index] in WriteShmFrame, and ReadShmFrame + // applies it to image_buffer[i] before each consumer access. We pass + // image_size as the allocation upper bound so any format up to that + // byte count fits the held SHM slot. + image_buffer[i] = new Image(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P, + &(shared_images[i*image_size]), image_size, 0); image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ } - alarm_image.AssignDirect(width, height, camera->Colours(), camera->SubpixelOrder(), - &(shared_images[image_buffer_count*image_size]), - image_size, - ZM_BUFTYPE_DONTFREE - ); + // alarm_image follows the same per-slot format convention. Initial format + // is a placeholder; consumers should AVPixFormat()-sync from + // image_pixelformats[alarm_index] when reading via GetAlarmImage(). + alarm_image.AssignDirect(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P, + &(shared_images[image_buffer_count*image_size]), image_size, ZM_BUFTYPE_DONTFREE); alarm_image.HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ if (alarm_image.Buffer() + image_size > mem_ptr + mem_size) { Warning("We will exceed memsize by %td bytes!", (alarm_image.Buffer() + image_size) - (mem_ptr + mem_size)); } - image_pixelformats = (AVPixelFormat *)(shared_images + (image_buffer_count*image_size)); + // Layout in SHM is: image_buffer_count*image_size for image_buffer slots, + // then image_buffer_count*image_size for alarm_image slots, THEN the + // image_pixelformats[] array (mem_size at line ~1002 reserves the space). + // Placing it at +1*image_buffer_count*image_size collides with alarm_image's + // buffer region — zmc's writes to image_pixelformats[index] corrupt the + // alarm image, and zms reads alarm-image bytes back as AVPixelFormat enum + // values, producing per-frame "different format every slot" garble. + image_pixelformats = (AVPixelFormat *)(shared_images + (2 * image_buffer_count * image_size)); if (purpose == CAPTURE) { memset(mem_ptr, 0, mem_size); @@ -1226,6 +1240,11 @@ bool Monitor::connect() { Error("Shared data not initialised by capture daemon for monitor %s", name.c_str()); return false; } + // No zms-side per-slot pixformat adoption needed: image_buffer was + // constructed above with the same hardcoded YUV420P that zmc uses, so + // both processes interpret the SHM bytes the same way without consulting + // the image_pixelformats[] array (which still records the actual format + // zmc wrote, kept for diagnostic visibility). // We set these here because otherwise the first fps calc is meaningless last_fps_time = std::chrono::system_clock::now(); @@ -2698,7 +2717,7 @@ int Monitor::Capture() { shared_data->signal = false; shared_data->last_write_index = index; shared_data->last_write_time = shared_timestamps[index].tv_sec; - image_buffer[index]->Assign(*capture_image); + WriteShmFrame(index, capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); delete capture_image; shared_data->image_count++; @@ -2836,6 +2855,33 @@ bool Monitor::setupConvertContext(const AVFrame *input_frame, const Image *image return (convert_context != nullptr); } +void Monitor::WriteShmFrame(unsigned int index, Image *capture_image) { + // No conversion at the SHM-write side. zmc records the format the bytes + // were actually written in via image_pixelformats[index]; consumers in + // other processes (zms etc.) sync image_buffer[index]->AVPixFormat() from + // that array before reading via ReadShmFrame, so the SHM transports any + // format Image can represent without a sws_scale step. This is the + // central no-conversion promise of the AVPixelFormat migration. + image_buffer[index]->Assign(*capture_image); + image_pixelformats[index] = capture_image->AVPixFormat(); +} + +Image *Monitor::ReadShmFrame(unsigned int index) { + // Adopt the per-slot format zmc recorded into image_pixelformats[]. zms + // (and zma, etc.) construct image_buffer[i] at attach time with whatever + // initial format was convenient; the actual bytes the capture daemon + // wrote into the slot can be in any AVPixelFormat that zm_pixformat + // supports, varying per-slot and across reconnections. AVPixFormat() + // updates imagePixFormat, colours, subpixelorder, size and linesize + // together so the Image object consistently interprets the SHM bytes. + // No-op when the slot's format already matches. + AVPixelFormat fmt = image_pixelformats[index]; + if (fmt != AV_PIX_FMT_NONE && image_buffer[index]->AVPixFormat() != fmt) { + image_buffer[index]->AVPixFormat(fmt); + } + return image_buffer[index]; +} + void Monitor::applyOrientation(Image *image) { if (orientation == ROTATE_0) return; @@ -3070,12 +3116,17 @@ bool Monitor::Decode() { } if (!packet->image) { - // Use a pipeline-friendly pixel format. Prefer the decoded frame's native - // format when Image can represent it (currently YUV 4:2:0 / 4:2:2 planar - // including the JPEG full-range variants, GRAY8, RGB24, and RGB32) — both - // 4:2:0 and 4:2:2 paths now have full coverage in zm_pixformat / - // zm_colours_from_pixformat. Anything outside that set falls back to a - // YUV420P conversion via swscale. + // Pick the most pipeline-friendly format Image can represent. If the + // decoder's native format is one Image supports, capture into that + // directly so sws_scale becomes a no-op identity copy via av_image_copy + // (Image::Assign(AVFrame*) takes that fast path on src_fmt == format). + // Otherwise fall back to YUV420P, which is universally supported and + // the smallest planar option. + // + // Cross-process consistency with zms is maintained per-slot via + // image_pixelformats[index] (written by WriteShmFrame, read on the + // zms side before each frame is consumed) — not by pinning the SHM + // to a single format here. unsigned int native_colours, native_subpixelorder; AVPixelFormat native_fmt = static_cast(packet->in_frame->format); const char *native_fmt_name = av_get_pix_fmt_name(native_fmt); @@ -3158,11 +3209,10 @@ bool Monitor::Decode() { TimestampImage(capture_image, packet->timestamp); } - // Write to shared image buffer + // Write to shared image buffer. unsigned int index = (shared_data->last_write_index + 1) % image_buffer_count; decoding_image_count++; - image_buffer[index]->Assign(*capture_image); - image_pixelformats[index] = capture_image->AVPixFormat(); + WriteShmFrame(index, capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); shared_data->signal = signal_check_points ? CheckSignal(capture_image) : true; shared_data->last_write_index = index; diff --git a/src/zm_monitor.h b/src/zm_monitor.h index d4daf6db23f..e1b0b8306de 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -985,6 +985,16 @@ class Monitor : public std::enable_shared_from_this { bool CheckSignal( const Image *image ); bool Analyse(); bool setupConvertContext(const AVFrame *input_frame, const Image *image); + // Write capture_image into image_buffer[index] without conversion and + // record its AVPixelFormat in image_pixelformats[index] so reading + // processes can adopt that format via ReadShmFrame. + void WriteShmFrame(unsigned int index, Image *capture_image); + + // Read-side counterpart: ensures image_buffer[index]'s metadata matches + // the format zmc wrote via image_pixelformats[index] before returning + // it. Use this from zms / zma / event paths instead of touching + // image_buffer[index] directly. + Image *ReadShmFrame(unsigned int index); void applyOrientation(Image *image); bool applyDeinterlacing(std::shared_ptr &packet, Image *capture_image); bool Decode(); diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index 68532864cdd..f3c23a8199a 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -643,7 +643,7 @@ void MonitorStream::runStream() { if (!was_paused) { int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; Debug(1, "Saving paused image from index %d",index); - paused_image = new Image(*monitor->image_buffer[index]); + paused_image = new Image(*monitor->ReadShmFrame(index)); paused_timestamp = SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index])); } } else if (paused_image) { @@ -756,12 +756,12 @@ void MonitorStream::runStream() { send_image = monitor->GetAlarmImage(); if (!send_image) { Debug(1, "Falling back"); - send_image = monitor->image_buffer[index]; + send_image = monitor->ReadShmFrame(index); } } else { //AVPixelFormat pixformat = monitor->image_pixelformats[index]; //Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, av_get_pix_fmt_name(pixformat)); - send_image = monitor->image_buffer[index]; + send_image = monitor->ReadShmFrame(index); } if (!sendFrame(send_image, last_frame_timestamp)) { @@ -829,7 +829,7 @@ void MonitorStream::runStream() { temp_image_buffer[temp_index].timestamp = SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index])); - monitor->image_buffer[index]->WriteJpeg(temp_image_buffer[temp_index].file_name, config.jpeg_file_quality); + monitor->ReadShmFrame(index)->WriteJpeg(temp_image_buffer[temp_index].file_name, config.jpeg_file_quality); temp_write_index = MOD_ADD(temp_write_index, 1, temp_image_buffer_count); if (temp_write_index == temp_read_index) { // Go back to live viewing @@ -985,7 +985,7 @@ void MonitorStream::SingleImage(int scale) { int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; AVPixelFormat pixformat = monitor->image_pixelformats[index]; Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, av_get_pix_fmt_name(pixformat)); - Image *snap_image = monitor->image_buffer[index]; + Image *snap_image = monitor->ReadShmFrame(index); if (!config.timestamp_on_capture) { monitor->TimestampImage(snap_image, SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index]))); From 1fe29202f552aa5d412fe2b114c55d00577f0baa Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 22 May 2026 19:35:50 -0400 Subject: [PATCH 04/35] fix: address PR #4788 review comments on AVPixelFormat migration - Image::Assign: when source linesize > destination linesize, copy only the destination's row capacity per line; previously copied image.linesize bytes into rows of smaller linesize, overflowing the destination buffer on the last row. - Monitor::CheckSignal: dispatch the 1-byte-per-pixel branch via zm_bytes_per_pixel(pix_fmt) == 1 so YUV422P/J422P are sampled on the Y plane like GRAY8/YUV420P, instead of falling through and reporting "no signal". - Monitor::WriteShmFrame: record image_pixelformats[index] via capture_image->PixFormat() (canonical imagePixFormat) instead of AVPixFormat(), which re-derives from the deprecated (colours, subpixelorder) pair and could propagate stale metadata. - Monitor::connect: rewrite stale comment that claimed readers don't need per-slot pixformat adoption; readers MUST call ReadShmFrame() to adopt the actual format zmc wrote. - Camera::Camera: guard linesize/imagesize derivation against AV_PIX_FMT_NONE and negative returns from av_image_get_linesize / av_image_get_buffer_size, falling back to width * colours stride so unsigned wrap-around can't break SHM sizing. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_camera.cpp | 24 ++++++++++++++++++++++-- src/zm_image.cpp | 10 +++++++--- src/zm_monitor.cpp | 31 ++++++++++++++++++++----------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/zm_camera.cpp b/src/zm_camera.cpp index 7c3ee460b02..c5e55cee20a 100644 --- a/src/zm_camera.cpp +++ b/src/zm_camera.cpp @@ -64,9 +64,29 @@ Camera::Camera( mLastAudioDTS(AV_NOPTS_VALUE), bytes(0), mIsPrimed(false) { - linesize = FFALIGN(av_image_get_linesize(pixelFormat, width, 0), 32); + // Guard against an unknown (colours, subpixelorder) pair that produced + // pixelFormat == AV_PIX_FMT_NONE, or any case where av_image_get_* + // returns a negative size — assigning those to unsigned would wrap to a + // huge value and break SHM sizing and downstream allocations. pixels = width * height; - imagesize = av_image_get_buffer_size(pixelFormat, width, height, 32); + if (pixelFormat == AV_PIX_FMT_NONE) { + Error("Camera: unknown pixel format from colours=%u subpixelorder=%u; falling back to width*colours stride", + p_colours, p_subpixelorder); + linesize = width * colours; + imagesize = static_cast(height) * linesize; + } else { + int raw_linesize = av_image_get_linesize(pixelFormat, width, 0); + int raw_imagesize = av_image_get_buffer_size(pixelFormat, width, height, 32); + if (raw_linesize < 0 || raw_imagesize < 0) { + Error("Camera: av_image_get_* returned %d/%d for pixelFormat=%s; falling back", + raw_linesize, raw_imagesize, av_get_pix_fmt_name(pixelFormat)); + linesize = width * colours; + imagesize = static_cast(height) * linesize; + } else { + linesize = FFALIGN(raw_linesize, 32); + imagesize = static_cast(raw_imagesize); + } + } Debug(2, "New camera id: %d width: %d line size: %d height: %d colours: %d subpixelorder: %d capture: %d, size: %llu", monitor->Id(), width, linesize, height, colours, subpixelorder, capture, imagesize); diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 8a7e8fd185a..0511ff07575 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1003,15 +1003,19 @@ void Image::Assign(const Image &image) { if ( image.buffer != buffer ) { if (image.linesize > linesize) { + // Source has more per-row padding than the destination can hold. Copy + // only the destination's row capacity per line to avoid writing past + // the destination buffer on the last row. Source padding bytes beyond + // the common row width are discarded. // This branch is only reached when dimensions and colours/subpixelorder - // match but linesize disagrees, which is an oddly-shaped Image. Copy the - // Y/primary plane line by line; planar chroma planes are not handled + // match but linesize disagrees, which is an oddly-shaped Image. Only + // the Y/primary plane is copied; planar chroma planes are not handled // here. The common path is the flat copy below. Debug(1, "Must copy line by line due to different line size %d != %d", image.linesize, linesize); uint8_t *src_ptr = image.buffer; uint8_t *dst_ptr = buffer; for (unsigned int i=0; i< image.height; i++) { - (*fptr_imgbufcpy)(dst_ptr, src_ptr, image.linesize); + (*fptr_imgbufcpy)(dst_ptr, src_ptr, linesize); src_ptr += image.linesize; dst_ptr += linesize; } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index ed4d20515d2..7cf3b550b59 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1240,11 +1240,14 @@ bool Monitor::connect() { Error("Shared data not initialised by capture daemon for monitor %s", name.c_str()); return false; } - // No zms-side per-slot pixformat adoption needed: image_buffer was - // constructed above with the same hardcoded YUV420P that zmc uses, so - // both processes interpret the SHM bytes the same way without consulting - // the image_pixelformats[] array (which still records the actual format - // zmc wrote, kept for diagnostic visibility). + // image_buffer[] is constructed above with a hardcoded YUV420P placeholder + // format because we don't yet know which AVPixelFormat zmc has written into + // each SHM slot. Readers (zms, zma, etc.) MUST call ReadShmFrame() before + // interpreting a slot's bytes — it adopts the per-slot AVPixelFormat from + // image_pixelformats[] (written by zmc in WriteShmFrame) and updates the + // Image's imagePixFormat, colours, subpixelorder, size and linesize + // together. Direct image_buffer[i] access without ReadShmFrame() will + // mis-interpret any slot whose actual format differs from the placeholder. // We set these here because otherwise the first fps calc is meaningless last_fps_time = std::chrono::system_clock::now(); @@ -1787,7 +1790,9 @@ bool Monitor::CheckSignal(const Image *image) { } } - if (pix_fmt == AV_PIX_FMT_GRAY8 || zm_is_yuv420(pix_fmt)) { + if (zm_bytes_per_pixel(pix_fmt) == 1) { + // GRAY8 plus all planar YUV variants we transport (420P/J420P/422P/J422P). + // For planar formats the Y plane is the first byte at each pixel index. if (*(buffer+index) != grayscale_val) return true; @@ -2860,12 +2865,16 @@ bool Monitor::setupConvertContext(const AVFrame *input_frame, const Image *image void Monitor::WriteShmFrame(unsigned int index, Image *capture_image) { // No conversion at the SHM-write side. zmc records the format the bytes // were actually written in via image_pixelformats[index]; consumers in - // other processes (zms etc.) sync image_buffer[index]->AVPixFormat() from - // that array before reading via ReadShmFrame, so the SHM transports any - // format Image can represent without a sws_scale step. This is the - // central no-conversion promise of the AVPixelFormat migration. + // other processes (zms etc.) sync image_buffer[index] from that array + // before reading via ReadShmFrame, so the SHM transports any format + // Image can represent without a sws_scale step. This is the central + // no-conversion promise of the AVPixelFormat migration. + // + // Record imagePixFormat directly via PixFormat() — AVPixFormat() re-derives + // from the deprecated (colours, subpixelorder) pair and could propagate + // stale metadata. image_buffer[index]->Assign(*capture_image); - image_pixelformats[index] = capture_image->AVPixFormat(); + image_pixelformats[index] = capture_image->PixFormat(); } Image *Monitor::ReadShmFrame(unsigned int index) { From d6a146e1d7232301299e683d977807b91acc7ecf Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 23 May 2026 09:40:00 -0400 Subject: [PATCH 05/35] fix: address PR #4788 review comments on SHM publish ordering and overlay bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image::Overlay: in the 1-byte-per-pixel branch, bound the loop by std::min(height*linesize, image.height*image.linesize) instead of `buffer + size`. For planar YUV destinations `size` includes chroma planes, while GRAY8 sources have only Y bytes; the old loop read past image.buffer and clobbered the destination's chroma. Restricting to the smaller of the two Y-plane spans keeps the overlay in the luma plane and never over-reads the source. - Monitor::ReadShmFrame: image_pixelformats[] lives in SHM and is written by another process, so its value must be treated as untrusted. Validate via zm_colours_from_pixformat() before passing into AVPixFormat(fmt) — an unsupported enum value would make av_image_get_buffer_size return negative and wrap into the Image's unsigned size/linesize members. Compare against PixFormat() (canonical imagePixFormat) instead of the deprecated AVPixFormat() getter. - Monitor::Capture signal-loss path: reorder SHM publishing so the slot bytes and per-slot shared_timestamps[index] are written first, then last_write_time is set from packet->timestamp (the fresh value, not the stale tv_sec the slot held from its previous occupant), and last_write_index is assigned last as the commit step. Readers gate on last_write_index, so all per-slot state must be visible before that final assignment. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 10 +++++++++- src/zm_monitor.cpp | 25 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 0511ff07575..75726f7b004 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1857,7 +1857,15 @@ void Image::Overlay( const Image &image ) { /* Grayscale/YUV420 on top of grayscale/YUV420 - complete */ if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { - const uint8_t* const max_ptr = buffer+size; + // Overlay only the luma/primary plane. For planar YUV destinations `size` + // includes chroma planes which the GRAY8 source has no bytes for — + // walking the whole `size` would over-read image.buffer and clobber the + // destination's chroma. Bound by the smaller of the two Y-plane spans so + // a smaller source can't run past its own buffer either. + const size_t y_span = std::min( + static_cast(height) * linesize, + static_cast(image.height) * image.linesize); + const uint8_t* const max_ptr = buffer + y_span; const uint8_t* psrc = image.buffer; uint8_t* pdest = buffer; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 7cf3b550b59..00e828ab44e 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -2722,10 +2722,15 @@ int Monitor::Capture() { Image *capture_image = new Image(width, height, camera->Colours(), camera->SubpixelOrder()); capture_image->Fill(signalcolor); shared_data->signal = false; - shared_data->last_write_index = index; - shared_data->last_write_time = shared_timestamps[index].tv_sec; + // Publish in dependency order: slot bytes + per-slot timestamp first, + // then last_write_time (a fresh value, not the stale tv_sec the slot + // held from its previous occupant), then last_write_index as the + // commit step. Readers gate on last_write_index, so anything they need + // for that slot must be visible before this final assignment. WriteShmFrame(index, capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); + shared_data->last_write_time = std::chrono::system_clock::to_time_t(packet->timestamp); + shared_data->last_write_index = index; delete capture_image; shared_data->image_count++; // What about timestamping it? @@ -2886,9 +2891,23 @@ Image *Monitor::ReadShmFrame(unsigned int index) { // updates imagePixFormat, colours, subpixelorder, size and linesize // together so the Image object consistently interprets the SHM bytes. // No-op when the slot's format already matches. + // + // image_pixelformats[] lives in SHM and is written by another process — + // treat it as untrusted: an uninitialised slot, a torn write, or a + // mismatched zmc/zms build could leave an arbitrary enum value here. + // Reject anything not in our supported set; av_image_get_buffer_size + // returns negative for unrecognised formats and would wrap into Image's + // unsigned size/linesize members. AVPixelFormat fmt = image_pixelformats[index]; - if (fmt != AV_PIX_FMT_NONE && image_buffer[index]->AVPixFormat() != fmt) { + unsigned int ignored_colours, ignored_subpix; + if (fmt != AV_PIX_FMT_NONE + && zm_colours_from_pixformat(fmt, ignored_colours, ignored_subpix) + && image_buffer[index]->PixFormat() != fmt) { image_buffer[index]->AVPixFormat(fmt); + } else if (fmt != AV_PIX_FMT_NONE + && !zm_colours_from_pixformat(fmt, ignored_colours, ignored_subpix)) { + Warning("ReadShmFrame: ignoring unsupported pixelformat %d in slot %u; keeping current %s", + fmt, index, av_get_pix_fmt_name(image_buffer[index]->PixFormat())); } return image_buffer[index]; } From 158d9433d36718eb9f8256bfd3ee06877132b3c3 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 23 May 2026 17:10:22 -0400 Subject: [PATCH 06/35] fix: address PR #4788 review comments on planar sizing and SHM bounds - Image::Assign(raw buffer overload): derive new_size from av_image_get_buffer_size(pix_fmt, w, h, 32) instead of width*height*colours, which undercounts planar formats (YUV420P/J420P/ YUV422P/J422P) and would silently truncate U/V planes. Update linesize in the same branch so LineSize() consumers (JPEG encode, overlay) stay in sync with the new format. Reject AV_PIX_FMT_NONE and negative av_image_get_buffer_size returns up-front. - Image::AVPixFormat(AVPixelFormat) setter: validate the requested format via zm_colours_from_pixformat() and check both av_image_* return values before mutating any state. On failure, leave imagePixFormat/size/linesize untouched so callers can recover instead of inheriting wrapped-unsigned junk. - Image::Assign(const Image&) stride-mismatch branch: refuse planar formats (YUV420P/J420P/YUV422P/J422P) explicitly. The per-row copy only touches the Y plane; doing it silently on planar input leaves U/V uninitialised in the destination and produces solid-green output downstream. Caller must reconvert or reallocate. - Monitor::connect: size SHM slots to an upper bound across every supported AVPixelFormat (RGBA at w*h, align=32) instead of the monitor's configured camera->ImageSize(). Prevents "Held buffer is undersized" failures when the no-conversion pipeline transports a format larger than the legacy DB Colours selection (e.g. YUV422P or RGBA into a GRAY8-configured monitor). Store the chosen capacity on the Monitor as shm_slot_size. - Monitor::ReadShmFrame: validate image_pixelformats[index] both as a recognised enum AND that its av_image_get_buffer_size fits shm_slot_size before calling AVPixFormat(fmt). Treats a corrupted or torn SHM value as ignorable instead of letting it overrun the held slot. Single zm_colours_from_pixformat() call (no longer duplicated). - web/.../monitor.php: inline fallback for translate('DeprecatedColoursSetting') so non-en_gb locales render an English string instead of the literal key name. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 96 ++++++++++++++++++++++------- src/zm_monitor.cpp | 55 +++++++++++++---- src/zm_monitor.h | 1 + web/skins/classic/views/monitor.php | 11 +++- 4 files changed, 126 insertions(+), 37 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 75726f7b004..32a0976c9a9 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -911,22 +911,33 @@ void Image::Assign( return; } - unsigned int new_size = p_width * p_height * p_colours; - if ( buffer_size < new_size ) { - Error("Attempt to assign buffer from an undersized buffer of size: %zu", buffer_size); - return; - } - if ( !p_height || !p_width ) { Error("Attempt to assign buffer with invalid width or height: %d %d", p_width, p_height); return; } - if (zm_pixformat_from_colours(p_colours, p_subpixelorder) == AV_PIX_FMT_NONE) { + AVPixelFormat new_pix_fmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + if (new_pix_fmt == AV_PIX_FMT_NONE) { Error("Attempt to assign buffer with unexpected colours per pixel: %d", p_colours); return; } + // Derive the required byte size from the AVPixelFormat. The legacy + // width*height*colours formula undercounts planar formats like YUV420P + // (colours==1 but chroma planes exist), leading to partial copies and + // corrupted output. + int raw_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 32); + if (raw_size < 0) { + Error("av_image_get_buffer_size failed (%d) for %s %ux%u", + raw_size, av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + return; + } + unsigned int new_size = static_cast(raw_size); + if ( buffer_size < new_size ) { + Error("Attempt to assign buffer from an undersized buffer of size: %zu (need %u)", buffer_size, new_size); + return; + } + if ( !buffer || p_width != width || p_height != height || p_colours != colours || p_subpixelorder != subpixelorder ) { if ( holdbuffer && buffer ) { @@ -946,8 +957,12 @@ void Image::Assign( pixels = width*height; colours = p_colours; subpixelorder = p_subpixelorder; - imagePixFormat = zm_pixformat_from_colours(colours, subpixelorder); + imagePixFormat = new_pix_fmt; size = new_size; + // Keep linesize in sync with the new format. av_image_get_linesize is + // signed; the format validation above guarantees a recognised format + // here so the call will not return a negative value. + linesize = FFALIGN(av_image_get_linesize(new_pix_fmt, p_width, 0), 32); } if ( new_buffer != buffer ) @@ -1003,14 +1018,30 @@ void Image::Assign(const Image &image) { if ( image.buffer != buffer ) { if (image.linesize > linesize) { - // Source has more per-row padding than the destination can hold. Copy - // only the destination's row capacity per line to avoid writing past - // the destination buffer on the last row. Source padding bytes beyond - // the common row width are discarded. - // This branch is only reached when dimensions and colours/subpixelorder - // match but linesize disagrees, which is an oddly-shaped Image. Only - // the Y/primary plane is copied; planar chroma planes are not handled - // here. The common path is the flat copy below. + // Source has more per-row padding than the destination can hold. This + // branch is only reached when dimensions and colours/subpixelorder + // match but linesize disagrees — an oddly-shaped Image. + // + // For planar formats (YUV420P/J420P/YUV422P/J422P) the per-row copy + // here only touches the Y plane; chroma planes would need plane-aware + // copying with separate strides via av_image_copy_plane. Doing a + // silent Y-only copy on a planar image leaves U/V uninitialised in + // the destination, producing solid-green output downstream. Refuse + // the assignment for planar formats with stride mismatch — the + // caller can fall back to a real sws_scale or re-allocate. + if (zm_is_yuv420(image.imagePixFormat) + || image.imagePixFormat == AV_PIX_FMT_YUV422P + || image.imagePixFormat == AV_PIX_FMT_YUVJ422P) { + Error("Image::Assign: planar format %s with stride mismatch (src %d > dst %d) " + "is not supported by the per-row copy; chroma planes would be lost. " + "Caller must convert or reallocate.", + av_get_pix_fmt_name(image.imagePixFormat), image.linesize, linesize); + return; + } + // Packed formats (GRAY8, RGB24/BGR24, RGB32 family): copy only the + // destination row capacity per line to avoid writing past the + // destination on the last row. Source padding bytes beyond the + // common row width are discarded. Debug(1, "Must copy line by line due to different line size %d != %d", image.linesize, linesize); uint8_t *src_ptr = image.buffer; uint8_t *dst_ptr = buffer; @@ -5540,13 +5571,32 @@ AVPixelFormat Image::AVPixFormat() const { } AVPixelFormat Image::AVPixFormat(AVPixelFormat new_pixelformat) { - if (!zm_colours_from_pixformat(new_pixelformat, colours, subpixelorder)) { - Error("Unknown pixelformat %d %s", new_pixelformat, av_get_pix_fmt_name(new_pixelformat)); - } - Debug(4, "Old size: %d, old pixelformat %d", size, imagePixFormat); + // Reject unrecognised formats up-front: av_image_get_buffer_size / + // av_image_get_linesize return negative for them, and assigning that into + // unsigned size/linesize would wrap to a huge value and corrupt later + // bounds checks. On failure, leave the Image's format/size/linesize + // untouched so callers can recover or skip the slot. + unsigned int probe_colours, probe_subpix; + if (!zm_colours_from_pixformat(new_pixelformat, probe_colours, probe_subpix)) { + Error("Image::AVPixFormat: refusing to set unknown pixelformat %d %s; keeping current %s", + new_pixelformat, av_get_pix_fmt_name(new_pixelformat), + av_get_pix_fmt_name(imagePixFormat)); + return imagePixFormat; + } + int new_size = av_image_get_buffer_size(new_pixelformat, width, height, 32); + int new_linesize = av_image_get_linesize(new_pixelformat, width, 0); + if (new_size < 0 || new_linesize < 0) { + Error("Image::AVPixFormat: av_image_get_* returned %d/%d for %s; keeping current %s", + new_size, new_linesize, av_get_pix_fmt_name(new_pixelformat), + av_get_pix_fmt_name(imagePixFormat)); + return imagePixFormat; + } + Debug(4, "Old size: %u, old pixelformat %s", size, av_get_pix_fmt_name(imagePixFormat)); + colours = probe_colours; + subpixelorder = probe_subpix; imagePixFormat = new_pixelformat; - size = av_image_get_buffer_size(new_pixelformat, width, height, 32); - Debug(4, "New size: %d new pixelformat %d", size, new_pixelformat); - linesize = FFALIGN(av_image_get_linesize(new_pixelformat, width, 0), 32); + size = static_cast(new_size); + linesize = FFALIGN(new_linesize, 32); + Debug(4, "New size: %u new pixelformat %s", size, av_get_pix_fmt_name(new_pixelformat)); return imagePixFormat; } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 00e828ab44e..4ba254ed303 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -299,6 +299,8 @@ Monitor::Monitor() : video_store_data(nullptr), shared_timestamps(nullptr), shared_images(nullptr), + image_pixelformats(nullptr), + shm_slot_size(0), video_stream_id(-1), audio_stream_id(-1), video_fifo(nullptr), @@ -991,7 +993,22 @@ bool Monitor::connect() { Warning("Already connected. Please call disconnect first."); } if (!camera) LoadCamera(); + // SHM slot size must be an upper bound across every AVPixelFormat the + // no-conversion pipeline can transport, not just the monitor's configured + // colours. If the camera is configured as GRAY8 (1 byte/pixel) but the + // decoder hands us YUV422P (2 bytes/pixel) or RGBA (4 bytes/pixel), the + // capture-side Assign would fail with "Held buffer is undersized". Size + // the slot for the largest supported format (RGBA at 4 bytes/pixel with + // 32-byte alignment) and fall back to camera->ImageSize() if that probe + // fails, so we never shrink the slot. size_t image_size = camera->ImageSize(); + int upper_bound = av_image_get_buffer_size(AV_PIX_FMT_RGBA, width, height, 32); + if (upper_bound > 0 && static_cast(upper_bound) > image_size) { + Debug(1, "SHM slot sized for upper bound %d (was camera->ImageSize()=%zu)", + upper_bound, image_size); + image_size = static_cast(upper_bound); + } + shm_slot_size = image_size; mem_size = sizeof(SharedData) + sizeof(TriggerData) + (zone_count * sizeof(int)) // Per zone scores @@ -1123,9 +1140,9 @@ bool Monitor::connect() { // The initial format is a placeholder. The actual SHM bytes can be in // any AVPixelFormat zm_pixformat supports — zmc records the per-slot // format via image_pixelformats[index] in WriteShmFrame, and ReadShmFrame - // applies it to image_buffer[i] before each consumer access. We pass - // image_size as the allocation upper bound so any format up to that - // byte count fits the held SHM slot. + // applies it to image_buffer[i] before each consumer access. image_size + // is sized above to the RGBA upper bound so any supported format fits + // the held SHM slot regardless of the monitor's configured colours. image_buffer[i] = new Image(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P, &(shared_images[i*image_size]), image_size, 0); image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ @@ -2895,20 +2912,32 @@ Image *Monitor::ReadShmFrame(unsigned int index) { // image_pixelformats[] lives in SHM and is written by another process — // treat it as untrusted: an uninitialised slot, a torn write, or a // mismatched zmc/zms build could leave an arbitrary enum value here. - // Reject anything not in our supported set; av_image_get_buffer_size - // returns negative for unrecognised formats and would wrap into Image's - // unsigned size/linesize members. + // Two checks before applying: + // 1. The format must be in our supported set; av_image_get_buffer_size + // returns negative for unrecognised formats and would wrap into + // Image's unsigned size/linesize. + // 2. The format's required buffer size must fit shm_slot_size; even a + // supported format (e.g. RGBA at a larger width than expected) could + // otherwise let subsequent reads/writes run past the held slot. AVPixelFormat fmt = image_pixelformats[index]; - unsigned int ignored_colours, ignored_subpix; - if (fmt != AV_PIX_FMT_NONE - && zm_colours_from_pixformat(fmt, ignored_colours, ignored_subpix) - && image_buffer[index]->PixFormat() != fmt) { - image_buffer[index]->AVPixFormat(fmt); - } else if (fmt != AV_PIX_FMT_NONE - && !zm_colours_from_pixformat(fmt, ignored_colours, ignored_subpix)) { + if (fmt == AV_PIX_FMT_NONE || image_buffer[index]->PixFormat() == fmt) { + return image_buffer[index]; + } + unsigned int probe_colours, probe_subpix; + if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) { Warning("ReadShmFrame: ignoring unsupported pixelformat %d in slot %u; keeping current %s", fmt, index, av_get_pix_fmt_name(image_buffer[index]->PixFormat())); + return image_buffer[index]; + } + int required = av_image_get_buffer_size(fmt, width, height, 32); + if (required < 0 || static_cast(required) > shm_slot_size) { + Warning("ReadShmFrame: format %s requires %d bytes but slot %u capacity is %zu; " + "keeping current %s to avoid overrun", + av_get_pix_fmt_name(fmt), required, index, shm_slot_size, + av_get_pix_fmt_name(image_buffer[index]->PixFormat())); + return image_buffer[index]; } + image_buffer[index]->AVPixFormat(fmt); return image_buffer[index]; } diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 3f6d9b66649..f0f73342630 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -659,6 +659,7 @@ class Monitor : public std::enable_shared_from_this { unsigned char *shared_images; std::vector image_buffer; AVPixelFormat *image_pixelformats; + size_t shm_slot_size; // per-slot byte capacity, sized to RGBA upper bound int video_stream_id; // will be filled in PrimeCapture int audio_stream_id; // will be filled in PrimeCapture diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index d2f61cf380a..20eb38b9184 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -907,7 +907,16 @@ class="nav-link Colours()) ?> - () + + ()
  • From 766b3c72fb7229df80d808e04fa9ebdd65e2cd56 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 24 May 2026 08:02:16 -0400 Subject: [PATCH 07/35] fix: cross-process AVPixelFormat sync for alarm_image (PR #4788) alarm_image's bytes live in SHM but its Image metadata (imagePixFormat/linesize/size) was per-process. Readers (zms) constructed alarm_image with a hardcoded YUV420P placeholder at attach time and there was no path for the writer (zmc/zma) to publish the actual format it wrote. The earlier comment pointed at image_pixelformats[alarm_index], but image_pixelformats[] only tracks the ring-buffer slots and no alarm_index exists. The result: when the writer assigned an RGB24/RGBA frame, reader streaming/encoding interpreted the bytes as YUV420P and produced garbage. Fix: - Allocate one extra AVPixelFormat slot at the end of SHM (alarm_image_pixelformat) and point a new Monitor member at it. - Add Monitor::WriteAlarmImage(const Image&) helper that mirrors WriteShmFrame's contract for the alarm slot: copy bytes, then publish the canonical PixFormat(). Replace the two direct alarm_image.Assign() call sites in Analyse() with WriteAlarmImage(). - Make Monitor::GetAlarmImage() sync alarm_image's format from *alarm_image_pixelformat before returning, with the same defensive validation ReadShmFrame uses (recognised enum + required size fits shm_slot_size, otherwise keep current format with a warning). - Initialise both image_pixelformats[] and alarm_image_pixelformat to AV_PIX_FMT_NONE in the CAPTURE-side memset path, since memset's 0 collides with AV_PIX_FMT_YUV420P rather than the NONE sentinel readers expect for "format not yet published". refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_monitor.cpp | 68 ++++++++++++++++++++++++++++++++++++++-------- src/zm_monitor.h | 4 +++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 4ba254ed303..a690103110c 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -300,6 +300,7 @@ Monitor::Monitor() : shared_timestamps(nullptr), shared_images(nullptr), image_pixelformats(nullptr), + alarm_image_pixelformat(nullptr), shm_slot_size(0), video_stream_id(-1), audio_stream_id(-1), @@ -1016,7 +1017,8 @@ bool Monitor::connect() { + (image_buffer_count*sizeof(struct timeval)) + (image_buffer_count*image_size) + (image_buffer_count*image_size) // alarm_images - + (image_buffer_count*sizeof(AVPixelFormat)) // + + (image_buffer_count*sizeof(AVPixelFormat)) // per-slot capture pix fmt + + sizeof(AVPixelFormat) // alarm_image pix fmt (cross-process sync) + 64; /* Padding used to permit aligning the images buffer to 64 byte boundary */ Debug(1, @@ -1147,9 +1149,9 @@ bool Monitor::connect() { &(shared_images[i*image_size]), image_size, 0); image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ } - // alarm_image follows the same per-slot format convention. Initial format - // is a placeholder; consumers should AVPixFormat()-sync from - // image_pixelformats[alarm_index] when reading via GetAlarmImage(). + // alarm_image follows the per-slot format convention. Initial format is a + // placeholder; consumers should sync via GetAlarmImage() which reads the + // cross-process *alarm_image_pixelformat. alarm_image.AssignDirect(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P, &(shared_images[image_buffer_count*image_size]), image_size, ZM_BUFTYPE_DONTFREE); alarm_image.HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ @@ -1158,12 +1160,14 @@ bool Monitor::connect() { } // Layout in SHM is: image_buffer_count*image_size for image_buffer slots, // then image_buffer_count*image_size for alarm_image slots, THEN the - // image_pixelformats[] array (mem_size at line ~1002 reserves the space). - // Placing it at +1*image_buffer_count*image_size collides with alarm_image's - // buffer region — zmc's writes to image_pixelformats[index] corrupt the - // alarm image, and zms reads alarm-image bytes back as AVPixelFormat enum - // values, producing per-frame "different format every slot" garble. + // image_pixelformats[image_buffer_count] array, THEN the single + // alarm_image_pixelformat slot. Placing image_pixelformats at + // +1*image_buffer_count*image_size would collide with the alarm_image + // buffer region — zmc's writes to image_pixelformats[index] would corrupt + // the alarm image, and zms would read alarm-image bytes back as + // AVPixelFormat enum values, producing per-frame garble. image_pixelformats = (AVPixelFormat *)(shared_images + (2 * image_buffer_count * image_size)); + alarm_image_pixelformat = image_pixelformats + image_buffer_count; if (purpose == CAPTURE) { memset(mem_ptr, 0, mem_size); @@ -1190,6 +1194,13 @@ bool Monitor::connect() { shared_data->alarm_y = -1; shared_data->format = camera->SubpixelOrder(); shared_data->imagesize = camera->ImageSize(); + // memset zeroed image_pixelformats/alarm_image_pixelformat above; 0 is + // AV_PIX_FMT_YUV420P, not the AV_PIX_FMT_NONE sentinel readers expect + // for "format not yet published". Initialise explicitly. + for (int32_t i = 0; i < image_buffer_count; i++) { + image_pixelformats[i] = AV_PIX_FMT_NONE; + } + *alarm_image_pixelformat = AV_PIX_FMT_NONE; shared_data->alarm_cause[0] = 0; shared_data->video_fifo_path[0] = 0; shared_data->audio_fifo_path[0] = 0; @@ -1387,9 +1398,44 @@ void Monitor::AddPrivacyBitmask() { } Image *Monitor::GetAlarmImage() { + // alarm_image's bytes live in SHM and are written by the capture/analysis + // process; alarm_image_pixelformat carries the format that process used. + // Without this sync, a reader process (zms) would interpret the bytes + // with whatever placeholder format alarm_image was constructed with — + // producing garbled output whenever the writer used RGB24/RGBA/etc. + if (alarm_image_pixelformat != nullptr) { + AVPixelFormat fmt = *alarm_image_pixelformat; + if (fmt != AV_PIX_FMT_NONE && alarm_image.PixFormat() != fmt) { + unsigned int probe_colours, probe_subpix; + if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) { + Warning("GetAlarmImage: ignoring unsupported pixelformat %d; keeping current %s", + fmt, av_get_pix_fmt_name(alarm_image.PixFormat())); + } else { + int required = av_image_get_buffer_size(fmt, width, height, 32); + if (required < 0 || static_cast(required) > shm_slot_size) { + Warning("GetAlarmImage: format %s requires %d bytes but slot capacity is %zu; " + "keeping current %s", + av_get_pix_fmt_name(fmt), required, shm_slot_size, + av_get_pix_fmt_name(alarm_image.PixFormat())); + } else { + alarm_image.AVPixFormat(fmt); + } + } + } + } return &alarm_image; } +void Monitor::WriteAlarmImage(const Image &src) { + // Mirror WriteShmFrame's contract for alarm_image: copy bytes then + // publish the canonical AVPixelFormat so reader processes can interpret + // the SHM correctly via GetAlarmImage(). + alarm_image.Assign(src); + if (alarm_image_pixelformat != nullptr) { + *alarm_image_pixelformat = src.PixFormat(); + } +} + int Monitor::GetImage(int32_t index, int scale) { if (index < 0 || index > image_buffer_count) { Debug(1, "Invalid index %d passed. image_buffer_count = %d", index, image_buffer_count); @@ -2189,7 +2235,7 @@ bool Monitor::Analyse() { } else { Debug(1, "No image to ref yet"); } - alarm_image.Assign(*(packet->image)); + WriteAlarmImage(*(packet->image)); } else { // didn't assign, do motion detection maybe and blending definitely if (!(analysis_image_count % (motion_frame_skip+1))) { @@ -2249,7 +2295,7 @@ bool Monitor::Analyse() { if (hasAnalysisViewers()) { // These extra copies are expensive, so only do it if we have viewers. - alarm_image.Assign(*(packet->analysis_image ? packet->analysis_image : packet->image)); + WriteAlarmImage(*(packet->analysis_image ? packet->analysis_image : packet->image)); } if (analysis_image == ANALYSISIMAGE_YCHANNEL) { diff --git a/src/zm_monitor.h b/src/zm_monitor.h index f0f73342630..ef8a2d5ca8f 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -659,6 +659,7 @@ class Monitor : public std::enable_shared_from_this { unsigned char *shared_images; std::vector image_buffer; AVPixelFormat *image_pixelformats; + AVPixelFormat *alarm_image_pixelformat; // cross-process format for alarm_image size_t shm_slot_size; // per-slot byte capacity, sized to RGBA upper bound int video_stream_id; // will be filled in PrimeCapture @@ -933,6 +934,9 @@ class Monitor : public std::enable_shared_from_this { const std::string &getONVIF_Options() const { return onvif_options; }; Image *GetAlarmImage(); + // Writer-side helper: copies src into alarm_image and publishes its + // AVPixelFormat so reader processes can correctly interpret the SHM bytes. + void WriteAlarmImage(const Image &src); int GetImage(int32_t index=-1, int scale=100); std::shared_ptr getSnapshot( int index=-1 ) const; SystemTimePoint GetTimestamp(int index = -1) const; From 77edeca57cd2550822f9d78b37a7471d79a294f4 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 24 May 2026 21:17:56 -0400 Subject: [PATCH 08/35] fix: use linesize for row stride in pixel ops (PR #4788) The FFALIGN(linesize, 32) introduced by the AVPixelFormat migration adds per-row padding for non-32-aligned widths. Functions that walked pixel buffers using width*bytes_per_pixel as the row stride wrote into padding, shifted rows sideways, or under-allocated output buffers for those widths. Convert each affected pixel op to use the Image's linesize as the per-row stride. Alignment is kept for SIMD/perf; only the consumer addressing is fixed. - Image::MaskPrivacy: reset the row pointer to `buffer + y*linesize` each iteration instead of monotonically incrementing across rows. - Image::Annotate (GRAY8/RGB24/RGB32 branches): use linesize for the per-row advance and char-block reset. The RGB32 branch additionally uses `linesize / sizeof(Rgb)` as the Rgb-typed stride. - Image::Fill and Image::Fill(colour, density, ...): compute each row's base address as `buffer + y*linesize + lo_x*colours` instead of `colours*(y*width + lo_x)`. - Image::Colourise: allocate the output buffer with av_image_get_buffer_size(target_fmt, w, h, 32) and copy row by row using src_linesize / dst_linesize. The previous tightly-packed allocation undersized the buffer (so AssignDirect would reject it) and smeared pixels across row boundaries for non-aligned widths. - Image::Deinterlace_Discard (all three colour branches): index source and destination rows by y*linesize instead of (y*width)*bytes. - Monitor::CheckSignal: convert the random linear index to (x, y) and address as y*LineSize() + x*bytes_per_pixel, so samples don't read per-row padding or land on the wrong row. - Monitor::Capture signal-loss path: rewrote the publish-order comment so it matches the actual code (WriteShmFrame, then timestamp, then last_write_time, then last_write_index as the commit step). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 130 +++++++++++++++++++++++++++++---------------- src/zm_monitor.cpp | 27 ++++++---- 2 files changed, 102 insertions(+), 55 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 32a0976c9a9..a8cb3f82020 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -2329,10 +2329,14 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour const uint8_t pixel_bw_col = pixel_colour & 0xff; const Rgb pixel_rgb_col = rgb_convert(pixel_colour,subpixelorder); - unsigned char *ptr = &buffer[0]; unsigned int i = 0; for ( unsigned int y = 0; y < height; y++ ) { + // Re-base the row pointer each iteration: with FFALIGN'd linesize there + // may be per-row padding between width*colours and linesize, and a + // monotonically-incremented ptr would walk into that padding (and + // misaddress subsequent rows). + unsigned char *ptr = buffer + y * linesize; if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int x = 0; x < width; x++, ptr++ ) { if ( p_bitmask[i] ) @@ -2399,8 +2403,11 @@ void Image::Annotate( for (const std::string &line : lines) { uint32 x = x0; + // Use linesize (which may include FFALIGN'd padding) for row stride + // rather than width*bytesPerPixel, so non-32-aligned widths don't + // skew annotation placement into per-row padding. if (zm_bytes_per_pixel(imagePixFormat) == 1) { - uint8 *ptr = &buffer[(y * width) + x0]; + uint8 *ptr = &buffer[y * linesize + x0]; for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { if (bg_colour != kRGBTransparent) { @@ -2412,9 +2419,9 @@ void Image::Annotate( *(ptr + column_idx) = fg_colour & 0xff; cp_row = cp_row & (cp_row - 1); } - ptr += width; + ptr += linesize; } - ptr -= (width * char_height); + ptr -= (linesize * char_height); ptr += char_width; x += char_width; if (x >= width) { @@ -2423,7 +2430,7 @@ void Image::Annotate( } } else if (zm_is_rgb24(imagePixFormat)) { constexpr uint8 bytesPerPixel = 3; - uint8 *ptr = &buffer[((y * width) + x0) * bytesPerPixel]; + uint8 *ptr = &buffer[y * linesize + x0 * bytesPerPixel]; for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { @@ -2444,9 +2451,9 @@ void Image::Annotate( BLUE_PTR_RGBA(colour_ptr) = BLUE_VAL_RGBA(fg_colour); cp_row = cp_row & (cp_row - 1); } - ptr += width * bytesPerPixel; + ptr += linesize; } - ptr -= (width * char_height * bytesPerPixel); + ptr -= (linesize * char_height); ptr += char_width * bytesPerPixel; x += char_width; if (x >= width) { @@ -2455,7 +2462,9 @@ void Image::Annotate( } } else if (zm_is_rgb32(imagePixFormat)) { constexpr uint8 bytesPerPixel = 4; - Rgb *ptr = reinterpret_cast(&buffer[((y * width) + x0) * bytesPerPixel]); + // Rgb is 4 bytes (rgb32); convert linesize bytes -> Rgb stride. + const unsigned int rgb_stride = linesize / sizeof(Rgb); + Rgb *ptr = reinterpret_cast(&buffer[y * linesize + x0 * bytesPerPixel]); for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { @@ -2468,9 +2477,9 @@ void Image::Annotate( *(ptr + column_idx) = fg_rgb_col; cp_row = cp_row & (cp_row - 1); } - ptr += width; + ptr += rgb_stride; } - ptr -= (width * char_height); + ptr -= (rgb_stride * char_height); ptr += char_width; x += char_width; if (x >= width) { @@ -2514,49 +2523,71 @@ void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqs AVPixelFormat p_req_pixfmt = zm_pixformat_from_colours(p_reqcolours, p_reqsubpixelorder); + // AssignDirect validates buffer_size against av_image_get_buffer_size(..., + // align=32) and uses an FFALIGN'd linesize. Allocate accordingly and copy + // row by row using the source (GRAY8) and destination (RGB) strides, so + // we don't undersize the output buffer for non-32-aligned widths and + // don't smear pixels across row boundaries. + int new_size_signed = av_image_get_buffer_size(p_req_pixfmt, width, height, 32); + int dst_linesize_signed = av_image_get_linesize(p_req_pixfmt, width, 0); + if (new_size_signed < 0 || dst_linesize_signed < 0) { + Error("Colourise: av_image sizing failed for %s %ux%u", + av_get_pix_fmt_name(p_req_pixfmt), width, height); + return; + } + const size_t new_size = static_cast(new_size_signed); + const unsigned int dst_linesize = FFALIGN(dst_linesize_signed, 32); + const unsigned int src_linesize = linesize; + if ( zm_is_rgb32(p_req_pixfmt) ) { /* RGB32 */ - Rgb* new_buffer = (Rgb*)AllocBuffer(pixels*sizeof(Rgb)); - - const uint8_t *psrc = buffer; - Rgb* pdest = new_buffer; - Rgb subpixel; - Rgb newpixel; + Rgb* new_buffer = (Rgb*)AllocBuffer(new_size); if ( p_req_pixfmt == AV_PIX_FMT_ABGR || p_req_pixfmt == AV_PIX_FMT_ARGB ) { /* ARGB\ABGR subpixel order. alpha byte is first (mem+0), so we need to shift the pixel left in the end */ - for ( unsigned int i=0; i < pixels; i++ ) { - newpixel = subpixel = psrc[i]; - newpixel = (newpixel<<8) | subpixel; - newpixel = (newpixel<<8) | subpixel; - pdest[i] = (newpixel<<8); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = buffer + y * src_linesize; + Rgb *pdest = (Rgb *)((uint8_t *)new_buffer + y * dst_linesize); + for (unsigned int x = 0; x < width; x++) { + Rgb subpixel = psrc[x]; + Rgb newpixel = subpixel; + newpixel = (newpixel << 8) | subpixel; + newpixel = (newpixel << 8) | subpixel; + pdest[x] = (newpixel << 8); + } } } else { /* RGBA\BGRA subpixel order, alpha byte is last (mem+3) */ - for ( unsigned int i=0; i < pixels; i++ ) { - newpixel = subpixel = psrc[i]; - newpixel = (newpixel<<8) | subpixel; - newpixel = (newpixel<<8) | subpixel; - pdest[i] = newpixel; + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = buffer + y * src_linesize; + Rgb *pdest = (Rgb *)((uint8_t *)new_buffer + y * dst_linesize); + for (unsigned int x = 0; x < width; x++) { + Rgb subpixel = psrc[x]; + Rgb newpixel = subpixel; + newpixel = (newpixel << 8) | subpixel; + newpixel = (newpixel << 8) | subpixel; + pdest[x] = newpixel; + } } } /* Directly assign the new buffer and make sure it will be freed when not needed anymore */ - AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, (uint8_t*)new_buffer, pixels*4, ZM_BUFTYPE_ZM); + AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, (uint8_t*)new_buffer, new_size, ZM_BUFTYPE_ZM); } else if ( zm_is_rgb24(p_req_pixfmt) ) { /* RGB24 */ - uint8_t *new_buffer = AllocBuffer(pixels*3); + uint8_t *new_buffer = AllocBuffer(new_size); - uint8_t *pdest = new_buffer; - const uint8_t *psrc = buffer; - - for ( unsigned int i=0; i < (unsigned int)pixels; i++, pdest += 3 ) { - RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = psrc[i]; + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = buffer + y * src_linesize; + uint8_t *pdest = new_buffer + y * dst_linesize; + for (unsigned int x = 0; x < width; x++, pdest += 3) { + RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = psrc[x]; + } } /* Directly assign the new buffer and make sure it will be freed when not needed anymore */ - AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, new_buffer, pixels*3, ZM_BUFTYPE_ZM); + AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, new_buffer, new_size, ZM_BUFTYPE_ZM); } else { Error("Colourise called with unexpected colours: %d", colours); return; @@ -2666,16 +2697,18 @@ void Image::Fill( Rgb colour, const Box *limits ) { unsigned int lo_y = limits ? limits->Lo().y_ : 0; unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; + // Use linesize as the row stride so non-32-aligned widths don't write + // into padding bytes or shift subsequent rows. if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[(y*width)+lo_x]; + unsigned char *p = &buffer[y * linesize + lo_x]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { *p = colour; } } } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[colours*((y*width)+lo_x)]; + unsigned char *p = &buffer[y * linesize + lo_x * colours]; for ( unsigned int x = lo_x; x <= hi_x; x++, p += 3) { RED_PTR_RGBA(p) = RED_VAL_RGBA(colour); GREEN_PTR_RGBA(p) = GREEN_VAL_RGBA(colour); @@ -2684,7 +2717,7 @@ void Image::Fill( Rgb colour, const Box *limits ) { } } else if ( zm_is_rgb32(imagePixFormat) ) { /* RGB32 */ for ( unsigned int y = lo_y; y <= (unsigned int)hi_y; y++ ) { - Rgb *p = (Rgb*)&buffer[((y*width)+lo_x)<<2]; + Rgb *p = (Rgb*)&buffer[y * linesize + (lo_x << 2)]; for ( unsigned int x = lo_x; x <= (unsigned int)hi_x; x++, p++) { /* Fast, copies the entire pixel in a single pass */ @@ -2711,9 +2744,11 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { unsigned int lo_y = limits ? limits->Lo().y_ : 0; unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; + // Use linesize as the row stride so non-32-aligned widths don't write + // into padding bytes or shift subsequent rows. if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[(y*width)+lo_x]; + unsigned char *p = &buffer[y * linesize + lo_x]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { if ( ( x == lo_x || x == hi_x || y == lo_y || y == hi_y ) || (!(x%density) && !(y%density) ) ) *p = colour; @@ -2721,7 +2756,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { } } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[colours*((y*width)+lo_x)]; + unsigned char *p = &buffer[y * linesize + lo_x * colours]; for ( unsigned int x = lo_x; x <= hi_x; x++, p += 3) { if ( ( x == lo_x || x == hi_x || y == lo_y || y == hi_y ) || (!(x%density) && !(y%density) ) ) { RED_PTR_RGBA(p) = RED_VAL_RGBA(colour); @@ -2732,7 +2767,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { } } else if ( zm_is_rgb32(imagePixFormat) ) { /* RGB32 */ for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - Rgb* p = (Rgb*)&buffer[((y*width)+lo_x)<<2]; + Rgb* p = (Rgb*)&buffer[y * linesize + (lo_x << 2)]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { if ( ( x == lo_x || x == hi_x || y == lo_y || y == hi_y ) || (!(x%density) && !(y%density) ) ) @@ -3162,12 +3197,15 @@ void Image::Deinterlace_Discard() { /* Simple deinterlacing. Copy the even lines into the odd lines */ // ICON: These can be drastically improved. But who cares? + // Use linesize as the per-row byte stride so non-32-aligned widths copy + // from/to the correct row offsets. Still copies `width` pixels per row + // (the padding bytes are untouched). if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { const uint8_t *psrc; uint8_t *pdest; for (unsigned int y = 0; y < (unsigned int)height; y += 2) { - psrc = buffer + (y * width); - pdest = buffer + ((y+1) * width); + psrc = buffer + y * linesize; + pdest = buffer + (y + 1) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pdest++ = *psrc++; } @@ -3176,8 +3214,8 @@ void Image::Deinterlace_Discard() { const uint8_t *psrc; uint8_t *pdest; for (unsigned int y = 0; y < (unsigned int)height; y += 2) { - psrc = buffer + ((y * width) * 3); - pdest = buffer + (((y+1) * width) * 3); + psrc = buffer + y * linesize; + pdest = buffer + (y + 1) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pdest++ = *psrc++; *pdest++ = *psrc++; @@ -3188,8 +3226,8 @@ void Image::Deinterlace_Discard() { const Rgb *psrc; Rgb *pdest; for (unsigned int y = 0; y < (unsigned int)height; y += 2) { - psrc = (Rgb*)(buffer + ((y * width) << 2)); - pdest = (Rgb*)(buffer + (((y+1) * width) << 2)); + psrc = (Rgb*)(buffer + y * linesize); + pdest = (Rgb*)(buffer + (y + 1) * linesize); for (unsigned int x = 0; x < (unsigned int)width; x++) { *pdest++ = *psrc++; } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index a690103110c..959f352f6cf 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1834,6 +1834,7 @@ bool Monitor::CheckSignal(const Image *image) { const uint8_t *buffer = image->Buffer(); int pixels = image->Pixels(); int width = image->Width(); + int linesize = image->LineSize(); AVPixelFormat pix_fmt = image->PixFormat(); int index = 0; @@ -1853,14 +1854,20 @@ bool Monitor::CheckSignal(const Image *image) { } } + // `index` is a pixel index in [0, width*height). Convert to (x, y) and + // use linesize for the row stride — buffer+index would read into per-row + // padding (or the wrong row) whenever linesize > width*bytes_per_pixel. + const int y = index / width; + const int x = index % width; + if (zm_bytes_per_pixel(pix_fmt) == 1) { // GRAY8 plus all planar YUV variants we transport (420P/J420P/422P/J422P). // For planar formats the Y plane is the first byte at each pixel index. - if (*(buffer+index) != grayscale_val) + if (*(buffer + y * linesize + x) != grayscale_val) return true; } else if (zm_is_rgb24(pix_fmt)) { - const uint8_t *ptr = buffer + (index * static_cast(zm_bytes_per_pixel(pix_fmt))); + const uint8_t *ptr = buffer + y * linesize + x * 3; if (pix_fmt == AV_PIX_FMT_BGR24) { if ((RED_PTR_BGRA(ptr) != red_val) || (GREEN_PTR_BGRA(ptr) != green_val) || (BLUE_PTR_BGRA(ptr) != blue_val)) @@ -1872,12 +1879,13 @@ bool Monitor::CheckSignal(const Image *image) { } } else if (zm_is_rgb32(pix_fmt)) { + const Rgb *ptr = (const Rgb *)(buffer + y * linesize + x * 4); if (pix_fmt == AV_PIX_FMT_ARGB || pix_fmt == AV_PIX_FMT_ABGR) { - if (ARGB_ABGR_ZEROALPHA(*(((const Rgb*)buffer)+index)) != ARGB_ABGR_ZEROALPHA(colour_val)) + if (ARGB_ABGR_ZEROALPHA(*ptr) != ARGB_ABGR_ZEROALPHA(colour_val)) return true; } else { /* Assume RGBA or BGRA */ - if (RGBA_BGRA_ZEROALPHA(*(((const Rgb*)buffer)+index)) != RGBA_BGRA_ZEROALPHA(colour_val)) + if (RGBA_BGRA_ZEROALPHA(*ptr) != RGBA_BGRA_ZEROALPHA(colour_val)) return true; } } @@ -2785,11 +2793,12 @@ int Monitor::Capture() { Image *capture_image = new Image(width, height, camera->Colours(), camera->SubpixelOrder()); capture_image->Fill(signalcolor); shared_data->signal = false; - // Publish in dependency order: slot bytes + per-slot timestamp first, - // then last_write_time (a fresh value, not the stale tv_sec the slot - // held from its previous occupant), then last_write_index as the - // commit step. Readers gate on last_write_index, so anything they need - // for that slot must be visible before this final assignment. + // Publish in dependency order: slot bytes, then per-slot timestamp, + // then last_write_time (fresh from packet->timestamp, not the stale + // tv_sec the slot held from its previous occupant), then + // last_write_index as the commit step. Readers gate on + // last_write_index, so every piece of per-slot state they consume + // must be visible before this final assignment. WriteShmFrame(index, capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); shared_data->last_write_time = std::chrono::system_clock::to_time_t(packet->timestamp); From 6a1c4e7b6e96be4829695e37bb3c44c53dce4092 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 24 May 2026 21:51:25 -0400 Subject: [PATCH 09/35] fix: fully invalidate Image on AssignDirect(AVFrame) format failure (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When av_image_get_buffer_size or zm_colours_from_pixformat rejects the AVFrame's format, AssignDirect(AVFrame) previously cleared size/linesize/colours but left width/height assigned and buffer pointing into frame->data[0]. A caller that ignored the error log could still walk width*height pixels through that pointer — and once the AVFrame is freed the buffer dangles. Clear width/height/pixels/buffer too so the Image is unambiguously empty after a failed assign. Reorder validation to run before any state mutation so a successful assign also leaves every field consistent in a single step. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index a8cb3f82020..f289fb104d9 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -783,34 +783,45 @@ uint8_t* Image::WriteBuffer( } void Image::AssignDirect(const AVFrame *frame) { - width = frame->width; - height = frame->height; - buffer = frame->data[0]; - linesize = frame->linesize[0]; - imagePixFormat = static_cast(frame->format); - + AVPixelFormat frame_fmt = static_cast(frame->format); + unsigned int probe_colours, probe_subpix; // av_image_get_buffer_size returns int (negative on error). Assigning a // negative value into the unsigned size/allocation members would wrap to - // huge and break later bounds-dependent code, so check first. - int av_size = av_image_get_buffer_size(imagePixFormat, frame->width, frame->height, 32); + // huge and break later bounds-dependent code, so check first. Validate + // the format against our supported set before touching any state. + int av_size = av_image_get_buffer_size(frame_fmt, frame->width, frame->height, 32); bool fmt_ok = (av_size >= 0) - && zm_colours_from_pixformat(imagePixFormat, colours, subpixelorder); + && zm_colours_from_pixformat(frame_fmt, probe_colours, probe_subpix); if (!fmt_ok) { - Error("AssignDirect: unsupported pixel format %d on frame %dx%d (av_size=%d)", + Error("AssignDirect: unsupported pixel format %d on frame %dx%d (av_size=%d); " + "fully invalidating Image", frame->format, frame->width, frame->height, av_size); - // Leave the Image in an explicit invalid state — don't keep stale - // size/linesize/colours that don't match imagePixFormat. + // Fully invalidate the Image so a caller that ignores the error can't + // accidentally walk width*height pixels through a buffer pointer that + // will dangle once the AVFrame is freed. + width = 0; + height = 0; + pixels = 0; + buffer = nullptr; imagePixFormat = AV_PIX_FMT_NONE; colours = 0; subpixelorder = 0; size = 0; allocation = 0; linesize = 0; - pixels = 0; - } else { - allocation = size = static_cast(av_size); - pixels = width * height; + holdbuffer = true; + buffertype = ZM_BUFTYPE_DONTFREE; + return; } + width = frame->width; + height = frame->height; + buffer = frame->data[0]; + linesize = frame->linesize[0]; + imagePixFormat = frame_fmt; + colours = probe_colours; + subpixelorder = probe_subpix; + allocation = size = static_cast(av_size); + pixels = width * height; holdbuffer = true; buffertype = ZM_BUFTYPE_DONTFREE; } From 667b00b30a2e1d00a75a86e065f9de617d78e971 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 24 May 2026 23:03:18 -0400 Subject: [PATCH 10/35] fix: address remaining width-vs-linesize and planar chroma issues (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuation of the FFALIGN(linesize, 32) consumer fixes. Alignment is kept for SIMD performance; only the addressing in consumers is fixed. - Image::Outline: both branches (dx>=dy and dx --- src/zm_image.cpp | 127 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index f289fb104d9..e0f5b84ff85 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -2375,6 +2375,53 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour return; } } // end foreach y + + // Planar YUV formats: the loop above masked only the Y plane, leaving the + // chroma (U/V) planes intact. Source colour bleeds through the mask + // because chroma still carries the original hue — a privacy leak. Set + // chroma samples covering any masked Y pixel to the neutral value (128). + // For YUV420P chroma is subsampled 2:2; for YUV422P it is 2:1 horizontal. + // Use the conservative rule: if any covered Y bit is set in the bitmask, + // neutralise the corresponding chroma sample. + const bool planar_420 = zm_is_yuv420(imagePixFormat); + const bool planar_422 = (imagePixFormat == AV_PIX_FMT_YUV422P + || imagePixFormat == AV_PIX_FMT_YUVJ422P); + if (planar_420 || planar_422) { + uint8_t *plane_ptrs[4] = {nullptr, nullptr, nullptr, nullptr}; + int plane_linesizes[4] = {0, 0, 0, 0}; + int fill_size = av_image_fill_arrays(plane_ptrs, plane_linesizes, buffer, + imagePixFormat, width, height, 32); + if (fill_size < 0 || !plane_ptrs[1] || !plane_ptrs[2]) { + Warning("MaskPrivacy: av_image_fill_arrays failed for %s; chroma not masked", + av_get_pix_fmt_name(imagePixFormat)); + return; + } + const unsigned int c_height = planar_420 ? height / 2 : height; + const unsigned int c_width = width / 2; + const int u_stride = plane_linesizes[1]; + const int v_stride = plane_linesizes[2]; + uint8_t *u_plane = plane_ptrs[1]; + uint8_t *v_plane = plane_ptrs[2]; + + for (unsigned int cy = 0; cy < c_height; cy++) { + for (unsigned int cx = 0; cx < c_width; cx++) { + const unsigned int y_x0 = cx * 2; + const unsigned int y_x1 = y_x0 + 1; + const unsigned int y_top = planar_420 ? cy * 2 : cy; + const unsigned int y_bot = planar_420 ? y_top + 1 : y_top; + bool mask_hit = p_bitmask[y_top * width + y_x0] + || (y_x1 < width && p_bitmask[y_top * width + y_x1]); + if (planar_420 && !mask_hit && y_bot < height) { + mask_hit = p_bitmask[y_bot * width + y_x0] + || (y_x1 < width && p_bitmask[y_bot * width + y_x1]); + } + if (mask_hit) { + u_plane[cy * u_stride + cx] = 128; + v_plane[cy * v_stride + cx] = 128; + } + } + } + } } /* RGB32 compatible: complete */ @@ -2825,18 +2872,18 @@ void Image::Outline( Rgb colour, const Polygon &polygon ) { grad *= yinc; if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( x = x1, y = y1; y != y2; y += yinc, x += grad ) { - buffer[(y*width)+int(round(x))] = colour; + buffer[y * linesize + int(round(x))] = colour; } } else if ( zm_is_rgb24(imagePixFormat) ) { for ( x = x1, y = y1; y != y2; y += yinc, x += grad ) { - unsigned char *p = &buffer[colours*((y*width)+int(round(x)))]; + unsigned char *p = &buffer[y * linesize + int(round(x)) * colours]; RED_PTR_RGBA(p) = RED_VAL_RGBA(colour); GREEN_PTR_RGBA(p) = GREEN_VAL_RGBA(colour); BLUE_PTR_RGBA(p) = BLUE_VAL_RGBA(colour); } } else if ( zm_is_rgb32(imagePixFormat) ) { for ( x = x1, y = y1; y != y2; y += yinc, x += grad ) { - *(Rgb*)(buffer+(((y*width)+int(round(x)))<<2)) = colour; + *(Rgb*)(buffer + y * linesize + (int(round(x)) << 2)) = colour; } } } else { @@ -2854,18 +2901,18 @@ void Image::Outline( Rgb colour, const Polygon &polygon ) { //Debug( 9, "x1:%d, x2:%d, y1:%d, y2:%d, gr:%.2lf", x1, x2, y1, y2, grad ); for ( y = y1, x = x1; x != x2; x += xinc, y += grad ) { //Debug( 9, "x:%d, y:%.2f", x, y ); - buffer[(int(round(y))*width)+x] = colour; + buffer[int(round(y)) * linesize + x] = colour; } } else if ( zm_is_rgb24(imagePixFormat) ) { for ( y = y1, x = x1; x != x2; x += xinc, y += grad ) { - unsigned char *p = &buffer[colours*((int(round(y))*width)+x)]; + unsigned char *p = &buffer[int(round(y)) * linesize + x * colours]; RED_PTR_RGBA(p) = RED_VAL_RGBA(colour); GREEN_PTR_RGBA(p) = GREEN_VAL_RGBA(colour); BLUE_PTR_RGBA(p) = BLUE_VAL_RGBA(colour); } } else if ( zm_is_rgb32(imagePixFormat) ) { for ( y = y1, x = x1; x != x2; x += xinc, y += grad ) { - *(Rgb*)(buffer+(((int(round(y))*width)+x)<<2)) = colour; + *(Rgb*)(buffer + int(round(y)) * linesize + (x << 2)) = colour; } } } @@ -2947,7 +2994,7 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { int32 lo_x = static_cast(it->min_x); int32 hi_x = static_cast((it + 1)->min_x); if (zm_bytes_per_pixel(imagePixFormat) == 1) { - uint8 *p = &buffer[(scan_line * width) + lo_x]; + uint8 *p = &buffer[scan_line * linesize + lo_x]; for (int32 x = lo_x; x <= hi_x; x++, p++) { if (!(x % density)) { @@ -2956,7 +3003,7 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { } } else if (zm_is_rgb24(imagePixFormat)) { constexpr uint8 bytesPerPixel = 3; - uint8 *ptr = &buffer[((scan_line * width) + lo_x) * bytesPerPixel]; + uint8 *ptr = &buffer[scan_line * linesize + lo_x * bytesPerPixel]; for (int32 x = lo_x; x <= hi_x; x++, ptr += bytesPerPixel) { if (!(x % density)) { @@ -2967,7 +3014,7 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { } } else if (zm_is_rgb32(imagePixFormat)) { constexpr uint8 bytesPerPixel = 4; - Rgb *ptr = reinterpret_cast(&buffer[((scan_line * width) + lo_x) * bytesPerPixel]); + Rgb *ptr = reinterpret_cast(&buffer[scan_line * linesize + lo_x * bytesPerPixel]); for (int32 x = lo_x; x <= hi_x; x++, ptr++) { if (!(x % density)) { @@ -3254,26 +3301,29 @@ void Image::Deinterlace_Linear() { const uint8_t *pbelow, *pabove; uint8_t *pcurrent; + // Use linesize as the per-row byte stride so non-32-aligned widths read + // and write the correct rows. The inner loops still process `width` + // pixels per row; per-row padding bytes are not touched. if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { - pabove = buffer + ((y-1) * width); - pbelow = buffer + ((y+1) * width); - pcurrent = buffer + (y * width); + pabove = buffer + (y - 1) * linesize; + pbelow = buffer + (y + 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = (*pabove++ + *pbelow++) >> 1; } } /* Special case for the last line */ - pcurrent = buffer + ((height-1) * width); - pabove = buffer + ((height-2) * width); + pcurrent = buffer + (height - 1) * linesize; + pabove = buffer + (height - 2) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; } } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { - pabove = buffer + (((y-1) * width) * 3); - pbelow = buffer + (((y+1) * width) * 3); - pcurrent = buffer + ((y * width) * 3); + pabove = buffer + (y - 1) * linesize; + pbelow = buffer + (y + 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = (*pabove++ + *pbelow++) >> 1; *pcurrent++ = (*pabove++ + *pbelow++) >> 1; @@ -3281,8 +3331,8 @@ void Image::Deinterlace_Linear() { } } /* Special case for the last line */ - pcurrent = buffer + (((height-1) * width) * 3); - pabove = buffer + (((height-2) * width) * 3); + pcurrent = buffer + (height - 1) * linesize; + pabove = buffer + (height - 2) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; *pcurrent++ = *pabove++; @@ -3290,9 +3340,9 @@ void Image::Deinterlace_Linear() { } } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { - pabove = buffer + (((y-1) * width) << 2); - pbelow = buffer + (((y+1) * width) << 2); - pcurrent = buffer + ((y * width) << 2); + pabove = buffer + (y - 1) * linesize; + pbelow = buffer + (y + 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = (*pabove++ + *pbelow++) >> 1; *pcurrent++ = (*pabove++ + *pbelow++) >> 1; @@ -3301,8 +3351,8 @@ void Image::Deinterlace_Linear() { } } /* Special case for the last line */ - pcurrent = buffer + (((height-1) * width) << 2); - pabove = buffer + (((height-2) * width) << 2); + pcurrent = buffer + (height - 1) * linesize; + pabove = buffer + (height - 2) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; *pcurrent++ = *pabove++; @@ -3320,10 +3370,13 @@ void Image::Deinterlace_Blend() { uint8_t *pabove, *pcurrent; + // Use linesize as the per-row byte stride so non-32-aligned widths read + // and write the correct rows. The inner loops still process `width` + // pixels per row; per-row padding bytes are not touched. if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + ((y-1) * width); - pcurrent = buffer + (y * width); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pabove = (*pabove + *pcurrent) >> 1; *pcurrent++ = *pabove++; @@ -3331,8 +3384,8 @@ void Image::Deinterlace_Blend() { } } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) * 3); - pcurrent = buffer + ((y * width) * 3); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pabove = (*pabove + *pcurrent) >> 1; *pcurrent++ = *pabove++; @@ -3344,8 +3397,8 @@ void Image::Deinterlace_Blend() { } } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) << 2); - pcurrent = buffer + ((y * width) << 2); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pabove = (*pabove + *pcurrent) >> 1; *pcurrent++ = *pabove++; @@ -3377,10 +3430,12 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { Error("Deinterlace called with invalid blend ratio"); } + // Use linesize as the per-row byte stride so non-32-aligned widths read + // and write the correct rows. Inner loops process `width` pixels. if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + ((y-1) * width); - pcurrent = buffer + (y * width); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { subpix1 = ((*pabove - *pcurrent)>>divider) + *pcurrent; subpix2 = ((*pcurrent - *pabove)>>divider) + *pabove; @@ -3390,8 +3445,8 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { } } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) * 3); - pcurrent = buffer + ((y * width) * 3); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { subpix1 = ((*pabove - *pcurrent)>>divider) + *pcurrent; subpix2 = ((*pcurrent - *pabove)>>divider) + *pabove; @@ -3409,8 +3464,8 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { } } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) << 2); - pcurrent = buffer + ((y * width) << 2); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { subpix1 = ((*pabove - *pcurrent)>>divider) + *pcurrent; subpix2 = ((*pcurrent - *pabove)>>divider) + *pabove; From e6ed0e027dd36ac260919238d69ac6aca6c9b9b4 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 26 May 2026 19:11:22 -0400 Subject: [PATCH 11/35] fix: linesize/planar correctness for Overlay, Rotate, Flip, AssignDirect(AVFrame) (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image::Overlay(image, lo_x, lo_y): rebase source and destination pointers per row using each image's own linesize. The previous sequential psrc++ drifted across rows whenever linesize > width*colours (i.e. non-32-aligned widths). - Image::Rotate / Image::Flip: rewrite as plane-aware via av_image_fill_arrays. Each plane is rotated/flipped independently using its own dimensions and stride, so planar YUV (YUV420P/J420P and YUV422P/J422P for 180/hflip/vflip; YUV420P/J420P additionally for 90/270) no longer loses chroma. YUV422P 90/270 is explicitly refused — the chroma subsampling would transpose to vertical-only, which isn't the same AVPixelFormat. Packed RGB24/RGB32/GRAY8 go through the same helper with bpp set per format. Inner helpers use linesize for stride, so non-32-aligned widths no longer drift into per-row padding. - Image::AssignDirect(AVFrame*): derive the wrapped size from the AVFrame's own linesize[] via av_image_fill_pointers rather than av_image_get_buffer_size(..., 32) — decoder-allocated frames may have a different alignment, so the 32-aligned size could overstate the actual buffer. Also sanity-check that data[1]/data[2]/data[3] are contiguous from data[0] (a non-contiguous AVFrame can't be wrapped with a single Image::buffer pointer). Both failure paths fully invalidate the Image (the existing helper). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 405 +++++++++++++++++++++++++++-------------------- 1 file changed, 237 insertions(+), 168 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index e0f5b84ff85..1c578f9f1fd 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -783,22 +783,7 @@ uint8_t* Image::WriteBuffer( } void Image::AssignDirect(const AVFrame *frame) { - AVPixelFormat frame_fmt = static_cast(frame->format); - unsigned int probe_colours, probe_subpix; - // av_image_get_buffer_size returns int (negative on error). Assigning a - // negative value into the unsigned size/allocation members would wrap to - // huge and break later bounds-dependent code, so check first. Validate - // the format against our supported set before touching any state. - int av_size = av_image_get_buffer_size(frame_fmt, frame->width, frame->height, 32); - bool fmt_ok = (av_size >= 0) - && zm_colours_from_pixformat(frame_fmt, probe_colours, probe_subpix); - if (!fmt_ok) { - Error("AssignDirect: unsupported pixel format %d on frame %dx%d (av_size=%d); " - "fully invalidating Image", - frame->format, frame->width, frame->height, av_size); - // Fully invalidate the Image so a caller that ignores the error can't - // accidentally walk width*height pixels through a buffer pointer that - // will dangle once the AVFrame is freed. + auto invalidate = [&]() { width = 0; height = 0; pixels = 0; @@ -811,8 +796,57 @@ void Image::AssignDirect(const AVFrame *frame) { linesize = 0; holdbuffer = true; buffertype = ZM_BUFTYPE_DONTFREE; + }; + + AVPixelFormat frame_fmt = static_cast(frame->format); + unsigned int probe_colours, probe_subpix; + // Validate the format against our supported set before touching any state. + if (!zm_colours_from_pixformat(frame_fmt, probe_colours, probe_subpix)) { + Error("AssignDirect: unsupported pixel format %d on frame %dx%d; " + "fully invalidating Image", frame->format, frame->width, frame->height); + invalidate(); + return; + } + if (!frame->data[0]) { + Error("AssignDirect: AVFrame has null data[0]; fully invalidating Image"); + invalidate(); + return; + } + + // av_image_get_buffer_size(..., align=32) assumes 32-byte stride + // alignment, but the AVFrame may have been allocated with a different + // alignment by the decoder. Derive the size from the *actual* frame + // strides instead — av_image_fill_pointers computes expected per-plane + // offsets given a tightly-packed buffer using the frame's linesize[]; the + // last (plane+1) offset is the total contiguous span. We also sanity- + // check that the frame's plane pointers really are contiguous starting + // at data[0] (a non-contiguous AVFrame can't be wrapped with a single + // Image::buffer pointer). + uint8_t *expected_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int frame_strides[4] = { + frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3] + }; + int total_size = av_image_fill_pointers(expected_planes, frame_fmt, frame->height, + frame->data[0], frame_strides); + if (total_size <= 0) { + Error("AssignDirect: av_image_fill_pointers returned %d for %s %dx%d; invalidating", + total_size, av_get_pix_fmt_name(frame_fmt), frame->width, frame->height); + invalidate(); return; } + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame_fmt); + const int plane_count = desc ? av_pix_fmt_count_planes(frame_fmt) : 1; + for (int p = 1; p < plane_count && p < 4; p++) { + if (frame->data[p] && frame->data[p] != expected_planes[p]) { + Error("AssignDirect: AVFrame plane %d not contiguous with data[0] " + "(expected %p, got %p) for %s %dx%d; invalidating", + p, expected_planes[p], frame->data[p], + av_get_pix_fmt_name(frame_fmt), frame->width, frame->height); + invalidate(); + return; + } + } + width = frame->width; height = frame->height; buffer = frame->data[0]; @@ -820,7 +854,7 @@ void Image::AssignDirect(const AVFrame *frame) { imagePixFormat = frame_fmt; colours = probe_colours; subpixelorder = probe_subpix; - allocation = size = static_cast(av_size); + allocation = size = static_cast(total_size); pixels = width * height; holdbuffer = true; buffertype = ZM_BUFTYPE_DONTFREE; @@ -2078,18 +2112,21 @@ void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned unsigned int hi_x = (lo_x+image.width)-1; unsigned int hi_y = (lo_y+image.height-1); + // Both buffers may carry per-row padding (linesize > width*colours after + // FFALIGN). Rebase source and destination per row using each image's + // own linesize so the sequential psrc++ advance can't drift across rows. if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { - const uint8_t *psrc = image.buffer; for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - uint8_t *pdest = &buffer[(y*width)+lo_x]; + const uint8_t *psrc = image.buffer + (y - lo_y) * image.linesize; + uint8_t *pdest = buffer + y * linesize + lo_x; for ( unsigned int x = lo_x; x <= hi_x; x++ ) { *pdest++ = *psrc++; } } } else if ( zm_is_rgb24(imagePixFormat) ) { - const uint8_t *psrc = image.buffer; for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - uint8_t *pdest = &buffer[colours*((y*width)+lo_x)]; + const uint8_t *psrc = image.buffer + (y - lo_y) * image.linesize; + uint8_t *pdest = buffer + y * linesize + lo_x * colours; for ( unsigned int x = lo_x; x <= hi_x; x++ ) { *pdest++ = *psrc++; *pdest++ = *psrc++; @@ -2097,9 +2134,9 @@ void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned } } } else if ( zm_is_rgb32(imagePixFormat) ) { - const Rgb *psrc = (Rgb*)(image.buffer); for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - Rgb *pdest = (Rgb*)&buffer[((y*width)+lo_x)<<2]; + const Rgb *psrc = (const Rgb*)(image.buffer + (y - lo_y) * image.linesize); + Rgb *pdest = (Rgb*)(buffer + y * linesize + (lo_x << 2)); for ( unsigned int x = lo_x; x <= hi_x; x++ ) { *pdest++ = *psrc++; } @@ -3029,176 +3066,208 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { } // end while } +namespace { +// Rotate `src_w` × `src_h` plane (bpp bytes per sample, src_linesize stride) +// into dst (dst_linesize stride) according to angle (90/180/270). +// For 90/270: dst dims are src_h × src_w. For 180: dst dims are src_w × src_h. +void rotate_plane(const uint8_t *src, int src_linesize, + unsigned int src_w, unsigned int src_h, + uint8_t *dst, int dst_linesize, + unsigned int bpp, int angle) { + if (angle == 90) { + // (sx, sy) -> (dx, dy) = (src_h-1-sy, sx). Equivalently, for each + // destination row dy=sx, write the column src_w-1-dy from the source. + for (unsigned int sy = 0; sy < src_h; sy++) { + const uint8_t *src_row = src + sy * src_linesize; + const unsigned int dx = src_h - 1 - sy; + for (unsigned int sx = 0; sx < src_w; sx++) { + const unsigned int dy = sx; + uint8_t *dst_px = dst + dy * dst_linesize + dx * bpp; + const uint8_t *src_px = src_row + sx * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; + } + } + } else if (angle == 180) { + // (sx, sy) -> (dx, dy) = (src_w-1-sx, src_h-1-sy). + for (unsigned int sy = 0; sy < src_h; sy++) { + const uint8_t *src_row = src + sy * src_linesize; + uint8_t *dst_row = dst + (src_h - 1 - sy) * dst_linesize; + for (unsigned int sx = 0; sx < src_w; sx++) { + const unsigned int dx = src_w - 1 - sx; + uint8_t *dst_px = dst_row + dx * bpp; + const uint8_t *src_px = src_row + sx * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; + } + } + } else if (angle == 270) { + // (sx, sy) -> (dx, dy) = (sy, src_w-1-sx). + for (unsigned int sy = 0; sy < src_h; sy++) { + const uint8_t *src_row = src + sy * src_linesize; + const unsigned int dx = sy; + for (unsigned int sx = 0; sx < src_w; sx++) { + const unsigned int dy = src_w - 1 - sx; + uint8_t *dst_px = dst + dy * dst_linesize + dx * bpp; + const uint8_t *src_px = src_row + sx * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; + } + } + } +} +} // namespace + void Image::Rotate(int angle) { angle %= 360; if ( !angle || angle%90 ) { return; } - unsigned int new_height = height; - unsigned int new_width = width; - uint8_t* rotate_buffer = AllocBuffer(size); - switch ( angle ) { - case 90 : { - new_height = width; - new_width = height; + // For 90°/270° the chroma subsampling pattern itself transposes. YUV420P + // has symmetric (2:2) subsampling and survives — the rotated chroma is + // still W/2 × H/2 of the rotated image. YUV422P has horizontal-only + // subsampling; after a 90°/270° rotation it would need vertical-only + // subsampling, which is not the same AVPixelFormat. Refuse rather than + // silently producing a non-conformant buffer. + const bool is_422 = (imagePixFormat == AV_PIX_FMT_YUV422P + || imagePixFormat == AV_PIX_FMT_YUVJ422P); + if ((angle == 90 || angle == 270) && is_422) { + Error("Rotate %d° not supported for %s (asymmetric chroma subsampling); " + "convert to RGB/YUV420P first", angle, av_get_pix_fmt_name(imagePixFormat)); + return; + } - unsigned int line_bytes = new_width*colours; - unsigned char *s_ptr = buffer; + const unsigned int new_width = (angle == 180) ? width : height; + const unsigned int new_height = (angle == 180) ? height : width; - if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { - for ( unsigned int i = new_width; i > 0; i-- ) { - unsigned char *d_ptr = rotate_buffer+(i-1); - for ( unsigned int j = new_height; j > 0; j-- ) { - *d_ptr = *s_ptr++; - d_ptr += line_bytes; - } - } - } else if ( zm_is_rgb32(imagePixFormat) ) { - Rgb* s_rptr = (Rgb*)s_ptr; - for ( unsigned int i = new_width; i; i-- ) { - Rgb* d_rptr = (Rgb*)(rotate_buffer+((i-1)<<2)); - for ( unsigned int j = new_height; j; j-- ) { - *d_rptr = *s_rptr++; - d_rptr += new_width; - } - } - } else { /* Assume RGB24 */ - for ( unsigned int i = new_width; i; i-- ) { - unsigned char *d_ptr = rotate_buffer+((i-1)*3); - for ( unsigned int j = new_height; j; j-- ) { - *d_ptr = *s_ptr++; - *(d_ptr+1) = *s_ptr++; - *(d_ptr+2) = *s_ptr++; - d_ptr += line_bytes; - } - } - } - break; + int new_size_signed = av_image_get_buffer_size(imagePixFormat, new_width, new_height, 32); + if (new_size_signed < 0) { + Error("Rotate: av_image_get_buffer_size failed for %s %ux%u", + av_get_pix_fmt_name(imagePixFormat), new_width, new_height); + return; + } + const size_t new_size = static_cast(new_size_signed); + uint8_t* rotate_buffer = AllocBuffer(new_size); + + uint8_t *src_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int src_strides[4] = {0, 0, 0, 0}; + uint8_t *dst_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int dst_strides[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(src_planes, src_strides, buffer, imagePixFormat, width, height, 32) < 0 + || av_image_fill_arrays(dst_planes, dst_strides, rotate_buffer, imagePixFormat, new_width, new_height, 32) < 0) { + Error("Rotate: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(rotate_buffer, ZM_BUFTYPE_ZM); + return; } - case 180 : { - unsigned char *s_ptr = buffer+size; - unsigned char *d_ptr = rotate_buffer; - if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { - while( s_ptr > buffer ) { - s_ptr--; - *d_ptr++ = *s_ptr; - } - } else if ( zm_is_rgb32(imagePixFormat) ) { - Rgb* s_rptr = (Rgb*)s_ptr; - Rgb* d_rptr = (Rgb*)d_ptr; - while( s_rptr > (Rgb*)buffer ) { - s_rptr--; - *d_rptr++ = *s_rptr; - } - } else { /* Assume RGB24 */ - while( s_ptr > buffer ) { - s_ptr -= 3; - *d_ptr++ = *s_ptr; - *d_ptr++ = *(s_ptr+1); - *d_ptr++ = *(s_ptr+2); - } + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(imagePixFormat); + if (!desc) { + Error("Rotate: av_pix_fmt_desc_get failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(rotate_buffer, ZM_BUFTYPE_ZM); + return; + } + const bool planar = (desc->flags & AV_PIX_FMT_FLAG_PLANAR) != 0; + + if (planar) { + // Each plane has 1 byte per sample. Plane 0 is luma at full resolution; + // planes 1/2 are chroma subsampled by desc->log2_chroma_w/h. + const unsigned int cw = width >> desc->log2_chroma_w; + const unsigned int ch = height >> desc->log2_chroma_h; + const unsigned int new_cw = new_width >> desc->log2_chroma_w; + const unsigned int new_ch = new_height >> desc->log2_chroma_h; + rotate_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], 1, angle); + rotate_plane(src_planes[1], src_strides[1], cw, ch, + dst_planes[1], dst_strides[1], 1, angle); + rotate_plane(src_planes[2], src_strides[2], cw, ch, + dst_planes[2], dst_strides[2], 1, angle); + (void)new_cw; (void)new_ch; // dst dims implied by new_w/new_h and subsampling + } else { + // Packed format: single plane with bpp bytes per pixel. + const unsigned int bpp = zm_bytes_per_pixel(imagePixFormat); + if (bpp == 0) { + Error("Rotate: bytes_per_pixel unknown for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(rotate_buffer, ZM_BUFTYPE_ZM); + return; } - break; + rotate_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], bpp, angle); } - case 270 : { - new_height = width; - new_width = height; - unsigned int line_bytes = new_width*colours; - unsigned char *s_ptr = buffer+size; + AssignDirect(new_width, new_height, colours, subpixelorder, rotate_buffer, new_size, ZM_BUFTYPE_ZM); +} // void Image::Rotate(int angle) - if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { - for ( unsigned int i = new_width; i > 0; i-- ) { - unsigned char *d_ptr = rotate_buffer+(i-1); - for ( unsigned int j = new_height; j > 0; j-- ) { - s_ptr--; - *d_ptr = *s_ptr; - d_ptr += line_bytes; - } - } - } else if ( zm_is_rgb32(imagePixFormat) ) { - Rgb* s_rptr = (Rgb*)s_ptr; - for ( unsigned int i = new_width; i > 0; i-- ) { - Rgb* d_rptr = (Rgb*)(rotate_buffer+((i-1)<<2)); - for ( unsigned int j = new_height; j > 0; j-- ) { - s_rptr--; - *d_rptr = *s_rptr; - d_rptr += new_width; - } - } - } else { /* Assume RGB24 */ - for ( unsigned int i = new_width; i > 0; i-- ) { - unsigned char *d_ptr = rotate_buffer+((i-1)*3); - for ( unsigned int j = new_height; j > 0; j-- ) { - *(d_ptr+2) = *(--s_ptr); - *(d_ptr+1) = *(--s_ptr); - *d_ptr = *(--s_ptr); - d_ptr += line_bytes; - } +namespace { +// Flip a single plane (bpp bytes per sample) into dst. leftright=true is +// horizontal flip; leftright=false is vertical flip. src/dst dimensions +// stay the same; only the order of pixels/rows changes. +void flip_plane(const uint8_t *src, int src_linesize, + unsigned int plane_w, unsigned int plane_h, + uint8_t *dst, int dst_linesize, + unsigned int bpp, bool leftright) { + if (leftright) { + for (unsigned int y = 0; y < plane_h; y++) { + const uint8_t *src_row = src + y * src_linesize; + uint8_t *dst_row = dst + y * dst_linesize; + for (unsigned int x = 0; x < plane_w; x++) { + const uint8_t *src_px = src_row + x * bpp; + uint8_t *dst_px = dst_row + (plane_w - 1 - x) * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; } } - break; - } + } else { + // Vertical flip: row y maps to row (plane_h - 1 - y); copy bpp*plane_w + // bytes per row (the data portion; padding bytes are left untouched). + const size_t row_data_bytes = static_cast(plane_w) * bpp; + for (unsigned int y = 0; y < plane_h; y++) { + memcpy(dst + (plane_h - 1 - y) * dst_linesize, + src + y * src_linesize, + row_data_bytes); + } } +} +} // namespace - AssignDirect(new_width, new_height, colours, subpixelorder, rotate_buffer, size, ZM_BUFTYPE_ZM); -} // void Image::Rotate(int angle) - -/* RGB32 compatible: complete */ +/* RGB32 compatible: complete; planar YUV (YUV420P/J420P/YUV422P/J422P) flipped per-plane */ void Image::Flip( bool leftright ) { uint8_t* flip_buffer = AllocBuffer(size); - unsigned int line_bytes = width*colours; - unsigned int line_bytes2 = 2*line_bytes; - if ( leftright ) { - // Horizontal flip, left to right - unsigned char *s_ptr = buffer+line_bytes; - unsigned char *d_ptr = flip_buffer; - unsigned char *max_d_ptr = flip_buffer + size; + uint8_t *src_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int src_strides[4] = {0, 0, 0, 0}; + uint8_t *dst_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int dst_strides[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(src_planes, src_strides, buffer, imagePixFormat, width, height, 32) < 0 + || av_image_fill_arrays(dst_planes, dst_strides, flip_buffer, imagePixFormat, width, height, 32) < 0) { + Error("Flip: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(flip_buffer, ZM_BUFTYPE_ZM); + return; + } - if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { - while( d_ptr < max_d_ptr ) { - for ( unsigned int j = 0; j < width; j++ ) { - s_ptr--; - *d_ptr++ = *s_ptr; - } - s_ptr += line_bytes2; - } - } else if ( zm_is_rgb32(imagePixFormat) ) { - Rgb* s_rptr = (Rgb*)s_ptr; - Rgb* d_rptr = (Rgb*)flip_buffer; - Rgb* max_d_rptr = (Rgb*)max_d_ptr; - while( d_rptr < max_d_rptr ) { - for ( unsigned int j = 0; j < width; j++ ) { - s_rptr--; - *d_rptr++ = *s_rptr; - } - s_rptr += width * 2; - } - } else { /* Assume RGB24 */ - while( d_ptr < max_d_ptr ) { - for ( unsigned int j = 0; j < width; j++ ) { - s_ptr -= 3; - *d_ptr++ = *s_ptr; - *d_ptr++ = *(s_ptr+1); - *d_ptr++ = *(s_ptr+2); - } - s_ptr += line_bytes2; - } - } + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(imagePixFormat); + if (!desc) { + Error("Flip: av_pix_fmt_desc_get failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(flip_buffer, ZM_BUFTYPE_ZM); + return; + } + const bool planar = (desc->flags & AV_PIX_FMT_FLAG_PLANAR) != 0; + + if (planar) { + const unsigned int cw = width >> desc->log2_chroma_w; + const unsigned int ch = height >> desc->log2_chroma_h; + flip_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], 1, leftright); + flip_plane(src_planes[1], src_strides[1], cw, ch, + dst_planes[1], dst_strides[1], 1, leftright); + flip_plane(src_planes[2], src_strides[2], cw, ch, + dst_planes[2], dst_strides[2], 1, leftright); } else { - // Vertical flip, top to bottom - unsigned char *s_ptr = buffer+(height*line_bytes); - unsigned char *d_ptr = flip_buffer; - - while ( s_ptr > buffer ) { - s_ptr -= line_bytes; - memcpy(d_ptr, s_ptr, line_bytes); - d_ptr += line_bytes; + const unsigned int bpp = zm_bytes_per_pixel(imagePixFormat); + if (bpp == 0) { + Error("Flip: bytes_per_pixel unknown for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(flip_buffer, ZM_BUFTYPE_ZM); + return; } + flip_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], bpp, leftright); } AssignDirect(width, height, colours, subpixelorder, flip_buffer, size, ZM_BUFTYPE_ZM); From e5446d3af5c7516032d7c95715a9961bd8a157cc Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 26 May 2026 23:03:33 -0400 Subject: [PATCH 12/35] fix: report AVPixelFormat in Panic messages for actionable diagnosis (PR #4788) Six format-dispatch fallbacks were still printing only the legacy `colours` value, but the dispatch in each spot is now keyed off pixelFormat/imagePixFormat. With the GRAY8/YUV420P alias collision (both colours=1) the legacy value frequently doesn't identify the true format, so a "Unexpected colours: 1" log on a misroute is useless. Updated each Panic to include the AVPixelFormat enum value and its human-readable name (via av_get_pix_fmt_name), plus the legacy (colours, subpixelorder) for context: - RemoteCameraRtsp constructor - FfmpegCamera constructor - VncCamera constructor - LocalCamera conversion-selection branch - VideoStream::SetupCodec mpeg helper - Image::Delta unknown-format fallback refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_ffmpeg_camera.cpp | 3 ++- src/zm_image.cpp | 3 ++- src/zm_libvnc_camera.cpp | 3 ++- src/zm_local_camera.cpp | 3 ++- src/zm_mpeg.cpp | 3 ++- src/zm_remote_camera_rtsp.cpp | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index 4ec504b32a8..bf0d18d614d 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -107,7 +107,8 @@ FfmpegCamera::FfmpegCamera( imagePixFormat = AV_PIX_FMT_GRAY8; pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %d", colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", + pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } packet = av_packet_ptr{av_packet_alloc()}; diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 1c578f9f1fd..ece9909146f 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -2325,7 +2325,8 @@ bool Image::Delta(const Image &image, Image* targetimage) const { } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { (*delta8_gray8)(buffer, image.buffer, pdiff, pixels); } else { - Panic("Delta called with unexpected colours: %d", colours); + Panic("Delta called with unexpected pixel format %d (%s); legacy colours=%d", + imagePixFormat, av_get_pix_fmt_name(imagePixFormat), colours); } #ifdef ZM_IMAGE_PROFILING diff --git a/src/zm_libvnc_camera.cpp b/src/zm_libvnc_camera.cpp index 2ad337d3aed..ae8aab482a6 100644 --- a/src/zm_libvnc_camera.cpp +++ b/src/zm_libvnc_camera.cpp @@ -130,7 +130,8 @@ mPass(pass) { mImgPixFmt = AV_PIX_FMT_GRAY8; pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %d", colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", + pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } if (capture) { diff --git a/src/zm_local_camera.cpp b/src/zm_local_camera.cpp index 8b2b0d7ffab..02ec39b5316 100644 --- a/src/zm_local_camera.cpp +++ b/src/zm_local_camera.cpp @@ -373,7 +373,8 @@ LocalCamera::LocalCamera( imagePixFormat = AV_PIX_FMT_GRAY8; pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %u",colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%u subpixelorder=%u", + pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } if (capture) { if (!sws_isSupportedInput(capturePixFormat)) { diff --git a/src/zm_mpeg.cpp b/src/zm_mpeg.cpp index bf28032265d..4ff8b819727 100644 --- a/src/zm_mpeg.cpp +++ b/src/zm_mpeg.cpp @@ -61,7 +61,8 @@ int VideoStream::SetupCodec( ) { pf = zm_pixformat_from_colours(colours, subpixelorder); if (pf == AV_PIX_FMT_NONE) { - Panic("Unexpected colours: %d", colours); + Panic("Unsupported (colours, subpixelorder) pair: colours=%d subpixelorder=%d", + colours, subpixelorder); } if (strcmp("rtp", of->name) == 0) { diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index ec355e06636..18c4fbfa567 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -82,7 +82,8 @@ RemoteCameraRtsp::RemoteCameraRtsp( imagePixFormat = AV_PIX_FMT_GRAY8; pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %d", colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", + pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } } // end RemoteCameraRtsp::RemoteCameraRtsp(...) From 3592fbb2b461a00161dd1b1e40d7f40ea1db8ab7 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 27 May 2026 06:30:23 -0400 Subject: [PATCH 13/35] fix: dest linesize in HighlightEdges, ceiling chroma dims in MaskPrivacy (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image::HighlightEdges: the RGB24 and RGB32 output branches addressed the destination buffer as `high_buff + ((y*linesize + lo_x) * 3|4)`, where `linesize` is the *source* (GRAY8) stride. For non-32-aligned widths the source GRAY8 stride and destination RGB24/RGB32 stride differ in their padding, so multiplying the source stride by 3 or 4 doesn't yield the destination's row offset and writes spill past high_buff on the last row. Read the destination's linesize from high_image and use that for phigh. The neighbour-pixel lookups also used `p ± width`; switched to `p ± src_linesize` so we follow the actual source row stride. All three branches (GRAY8/RGB24/RGB32) now pick neighbours via src_linesize. - Image::MaskPrivacy: chroma plane dimensions were `width/2` and `height/2` (truncating). For odd width/height the planar layout in AVFrame uses ceil(W/2) / ceil(H/2), so the last column or row of chroma was never neutralised, leaving a strip with the source's original hue along the right/bottom edge — defeats the privacy mask. Use (W+1)/2 and (H+1)/2; existing in-loop guards on the Y indices already clamp the odd-edge lookup to the bitmap bounds. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 55 +++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index ece9909146f..a000d2fb61c 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1129,20 +1129,23 @@ Image *Image::HighlightEdges( unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; + // Source (this) is GRAY8 with src_linesize per row. The destination Image + // has its own (possibly different) row stride; use that for high_buff + // addressing so non-32-aligned widths don't overflow the destination on + // the last row, and use src_linesize when looking up neighbour pixels + // (p ± linesize) so we follow the actual row stride rather than `width`. + const unsigned int src_linesize = linesize; + const unsigned int dst_linesize = high_image->LineSize(); + if ( p_pixfmt == AV_PIX_FMT_GRAY8 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - const uint8_t* p = buffer + (y * linesize) + lo_x; - uint8_t* phigh = high_buff + (y * linesize) + lo_x; + const uint8_t* p = buffer + (y * src_linesize) + lo_x; + uint8_t* phigh = high_buff + (y * dst_linesize) + lo_x; for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh++ ) { bool edge = false; if ( *p ) { - edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) || (y > 0 && !*(p-width)) || (y < (height-1) && !*(p+width)); -#if 0 - if ( !edge && x > 0 && !*(p-1) ) edge = true; - if ( !edge && x < (width-1) && !*(p+1) ) edge = true; - if ( !edge && y > 0 && !*(p-width) ) edge = true; - if ( !edge && y < (height-1) && !*(p+width) ) edge = true; -#endif + edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) + || (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize)); } if ( edge ) { *phigh = colour; @@ -1151,18 +1154,13 @@ Image *Image::HighlightEdges( } } else if ( zm_is_rgb24(p_pixfmt) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - const uint8_t* p = buffer + (y * linesize) + lo_x; - uint8_t* phigh = high_buff + (((y * linesize) + lo_x) * 3); + const uint8_t* p = buffer + (y * src_linesize) + lo_x; + uint8_t* phigh = high_buff + (y * dst_linesize) + lo_x * 3; for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh += 3 ) { bool edge = false; if ( *p ) { - edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) || (y > 0 && !*(p-width)) || (y < (height-1) && !*(p+width)); -#if 0 - if ( !edge && x > 0 && !*(p-1) ) edge = true; - if ( !edge && x < (width-1) && !*(p+1) ) edge = true; - if ( !edge && y > 0 && !*(p-width) ) edge = true; - if ( !edge && y < (height-1) && !*(p+width) ) edge = true; -#endif + edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) + || (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize)); } if ( edge ) { RED_PTR_RGBA(phigh) = RED_VAL_RGBA(colour); @@ -1173,18 +1171,13 @@ Image *Image::HighlightEdges( } } else if ( zm_is_rgb32(p_pixfmt) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - const uint8_t* p = buffer + (y * linesize) + lo_x; - Rgb* phigh = (Rgb*)(high_buff + (((y * linesize) + lo_x) * 4)); + const uint8_t* p = buffer + (y * src_linesize) + lo_x; + Rgb* phigh = (Rgb*)(high_buff + (y * dst_linesize) + (lo_x << 2)); for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh++ ) { bool edge = false; if ( *p ) { - edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) || (y > 0 && !*(p-width)) || (y < (height-1) && !*(p+width)); -#if 0 - if ( !edge && x > 0 && !*(p-1) ) edge = true; - if ( !edge && x < (width-1) && !*(p+1) ) edge = true; - if ( !edge && y > 0 && !*(p-width) ) edge = true; - if ( !edge && y < (height-1) && !*(p+width) ) edge = true; -#endif + edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) + || (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize)); } if ( edge ) { *phigh = colour; @@ -2434,8 +2427,12 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour av_get_pix_fmt_name(imagePixFormat)); return; } - const unsigned int c_height = planar_420 ? height / 2 : height; - const unsigned int c_width = width / 2; + // Chroma plane dimensions use ceiling division so an odd width/height + // doesn't drop the last chroma column/row — that would leave a strip + // along the right/bottom edge with original colour bleeding through + // the privacy mask. + const unsigned int c_height = planar_420 ? (height + 1) / 2 : height; + const unsigned int c_width = (width + 1) / 2; const int u_stride = plane_linesizes[1]; const int v_stride = plane_linesizes[2]; uint8_t *u_plane = plane_ptrs[1]; From 070704d92c236075b6021ac41cc21286d39673a1 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 27 May 2026 08:15:39 -0400 Subject: [PATCH 14/35] fix: ceiling chroma dims in Rotate/Flip; clearer Assign error (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image::Rotate / Image::Flip: chroma plane dimensions were computed as `width >> log2_chroma_w` / `height >> log2_chroma_h`, which floors. For odd luma widths/heights the last chroma column/row was skipped, leaving U/V samples unrotated/unflipped on odd-sized frames. Use AV_CEIL_RSHIFT to match FFmpeg's plane dimension convention. - Image::Assign(const Image&): the AV_PIX_FMT_NONE guard's error message said "unexpected colours per pixel" and printed image.colours, which is misleading — planar formats legitimately have colours==1. Report the real cause (missing AVPixelFormat metadata) plus the legacy (colours, subpixelorder) for context. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index a000d2fb61c..dcd04bc6b00 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1029,7 +1029,12 @@ void Image::Assign(const Image &image) { } if (image.imagePixFormat == AV_PIX_FMT_NONE) { - Error("Attempt to assign image with unexpected colours per pixel: %d", image.colours); + // Real cause is unset/unknown AVPixelFormat metadata, not the + // (colours, subpixelorder) values themselves — those can legitimately + // be 1/something for planar YUV. Report the underlying state. + Error("Attempt to assign image with missing AVPixelFormat metadata " + "(imagePixFormat=AV_PIX_FMT_NONE, legacy colours=%u subpixelorder=%u)", + image.colours, image.subpixelorder); return; } @@ -3167,18 +3172,17 @@ void Image::Rotate(int angle) { if (planar) { // Each plane has 1 byte per sample. Plane 0 is luma at full resolution; - // planes 1/2 are chroma subsampled by desc->log2_chroma_w/h. - const unsigned int cw = width >> desc->log2_chroma_w; - const unsigned int ch = height >> desc->log2_chroma_h; - const unsigned int new_cw = new_width >> desc->log2_chroma_w; - const unsigned int new_ch = new_height >> desc->log2_chroma_h; + // planes 1/2 are chroma subsampled by desc->log2_chroma_w/h. Use ceiling + // (AV_CEIL_RSHIFT) — flooring would drop the last chroma column/row for + // odd luma dimensions and leave part of U/V unrotated. + const unsigned int cw = AV_CEIL_RSHIFT(width, desc->log2_chroma_w); + const unsigned int ch = AV_CEIL_RSHIFT(height, desc->log2_chroma_h); rotate_plane(src_planes[0], src_strides[0], width, height, dst_planes[0], dst_strides[0], 1, angle); rotate_plane(src_planes[1], src_strides[1], cw, ch, dst_planes[1], dst_strides[1], 1, angle); rotate_plane(src_planes[2], src_strides[2], cw, ch, dst_planes[2], dst_strides[2], 1, angle); - (void)new_cw; (void)new_ch; // dst dims implied by new_w/new_h and subsampling } else { // Packed format: single plane with bpp bytes per pixel. const unsigned int bpp = zm_bytes_per_pixel(imagePixFormat); @@ -3249,8 +3253,10 @@ void Image::Flip( bool leftright ) { const bool planar = (desc->flags & AV_PIX_FMT_FLAG_PLANAR) != 0; if (planar) { - const unsigned int cw = width >> desc->log2_chroma_w; - const unsigned int ch = height >> desc->log2_chroma_h; + // Ceiling division so odd luma dimensions don't drop the last chroma + // column/row. + const unsigned int cw = AV_CEIL_RSHIFT(width, desc->log2_chroma_w); + const unsigned int ch = AV_CEIL_RSHIFT(height, desc->log2_chroma_h); flip_plane(src_planes[0], src_strides[0], width, height, dst_planes[0], dst_strides[0], 1, leftright); flip_plane(src_planes[1], src_strides[1], cw, ch, From 133bb53c776003b576783bc503efd321bb744dd9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 27 May 2026 10:42:38 -0400 Subject: [PATCH 15/35] fix: Camera::linesize/imagesize back to tightly-packed (PR #4788) Camera::linesize/imagesize was changed by the migration to FFALIGN(.,32) and av_image_get_buffer_size(...,32) to keep SIMD-friendly strides. But Camera describes the *device-side* buffer that capture paths copy from (V4L2 mmap buffers, raw RTP frames, etc.). Those external buffers are tightly packed at the driver/source stride, not 32-byte aligned, so the inflated imagesize causes: - LocalCamera::PrimeCapture's `pSize != imagesize` check (av_image_get_buffer_size(..., align=1) vs Camera::imagesize) to Fatal for any width that isn't a multiple of 32. - Other raw-socket capture paths to pass an oversized imagesize as the source buffer size, risking out-of-bounds reads of driver-allocated buffers. Revert Camera::linesize/imagesize to align=1 (tightly packed). Image internal buffers and SHM slots independently apply 32-byte alignment where they need it (Monitor::connect already takes max(camera->ImageSize(), av_image_get_buffer_size(RGBA, w, h, 32)) for the SHM slot, so the SIMD-aligned slot capacity is preserved). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_camera.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/zm_camera.cpp b/src/zm_camera.cpp index c5e55cee20a..9f3ed7b7c93 100644 --- a/src/zm_camera.cpp +++ b/src/zm_camera.cpp @@ -64,6 +64,14 @@ Camera::Camera( mLastAudioDTS(AV_NOPTS_VALUE), bytes(0), mIsPrimed(false) { + // Camera::linesize/imagesize describe the *device-side* buffer that + // capture paths copy from (V4L2 mmap buffers, raw RTP frames, etc). + // Those buffers are tightly packed at the driver/source stride, not + // 32-byte aligned, so use align=1 here. ZM's internal Image buffers + // and SHM slots independently apply their own 32-byte alignment for + // SIMD performance — that decoupling is the source of the size-mismatch + // Fatal in LocalCamera::PrimeCapture when widths aren't 32-aligned. + // // Guard against an unknown (colours, subpixelorder) pair that produced // pixelFormat == AV_PIX_FMT_NONE, or any case where av_image_get_* // returns a negative size — assigning those to unsigned would wrap to a @@ -76,14 +84,14 @@ Camera::Camera( imagesize = static_cast(height) * linesize; } else { int raw_linesize = av_image_get_linesize(pixelFormat, width, 0); - int raw_imagesize = av_image_get_buffer_size(pixelFormat, width, height, 32); + int raw_imagesize = av_image_get_buffer_size(pixelFormat, width, height, 1); if (raw_linesize < 0 || raw_imagesize < 0) { Error("Camera: av_image_get_* returned %d/%d for pixelFormat=%s; falling back", raw_linesize, raw_imagesize, av_get_pix_fmt_name(pixelFormat)); linesize = width * colours; imagesize = static_cast(height) * linesize; } else { - linesize = FFALIGN(raw_linesize, 32); + linesize = static_cast(raw_linesize); imagesize = static_cast(raw_imagesize); } } From a5bf39d41326cdb1dc02645f605d38e9b33a0a24 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 28 May 2026 08:15:36 -0400 Subject: [PATCH 16/35] fix: keep DeColourise/AssignDirect(AVFrame) state self-consistent (PR #4788) - Image::DeColourise: updating only imagePixFormat=GRAY8 left size still at the previous RGB sizing and linesize at the previous RGB stride. Downstream ops that allocate via size (Flip/Rotate) under-allocate, and row-stride loops that use linesize address the wrong rows. Now recomputes size and linesize from the new GRAY8 layout via av_image_get_buffer_size/av_image_get_linesize and calls update_function_pointers(). - Image::AssignDirect(AVFrame*): never called update_function_pointers() after the format change, so an Image that previously held a different format kept stale fptr_delta/fptr_blend/fptr_convert bindings and subsequent ops took the wrong optimized path. Added the call at the end of the success path. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index dcd04bc6b00..c82acd8965a 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -858,6 +858,11 @@ void Image::AssignDirect(const AVFrame *frame) { pixels = width * height; holdbuffer = true; buffertype = ZM_BUFTYPE_DONTFREE; + // Re-bind format-specific function pointers (delta/blend/convert + // dispatch) to the new format. Without this, an Image that previously + // held a different format keeps its old fptr_* and subsequent ops take + // the wrong optimized path. + update_function_pointers(); } @@ -2779,7 +2784,14 @@ void Image::DeColourise() { colours = ZM_COLOUR_GRAY8; subpixelorder = ZM_SUBPIX_ORDER_NONE; imagePixFormat = AV_PIX_FMT_GRAY8; - size = width * height; + // Keep size and linesize consistent with the new format; downstream ops + // (Flip/Rotate allocate via size; row-stride loops use linesize) need both + // to match the GRAY8 32-byte-aligned layout, not the previous RGB sizing. + int new_size = av_image_get_buffer_size(AV_PIX_FMT_GRAY8, width, height, 32); + int new_linesize = av_image_get_linesize(AV_PIX_FMT_GRAY8, width, 0); + if (new_size > 0) size = static_cast(new_size); + if (new_linesize > 0) linesize = FFALIGN(new_linesize, 32); + update_function_pointers(); } /* RGB32 compatible: complete */ From 284697061434b81ffd429b3d6968b5738bdee139 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 28 May 2026 12:38:18 -0400 Subject: [PATCH 17/35] fix: PopulateFrame error handling + canonical pix fmt in Scale (PR #4788) - Image::Assign(AVFrame*) fast path: PopulateFrame can fail (av_buffer_create / av_image_fill_arrays errors); calling av_image_copy on an unpopulated temp_frame is undefined. Check the return and bail out cleanly on failure. - Image::Scale: use the canonical imagePixFormat directly instead of the deprecated AVPixFormat() getter (which re-derives via the legacy (colours, subpixelorder) pair and would hit the GRAY8/YUV420P alias collision if those drift). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index c82acd8965a..56d548269c7 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -378,7 +378,12 @@ bool Image::Assign(const AVFrame *frame) { Error("Unable to allocate destination frame"); return false; } - PopulateFrame(temp_frame.get()); + // PopulateFrame can fail (av_buffer_create / av_image_fill_arrays errors); + // calling av_image_copy on an unpopulated temp_frame is undefined. + if (PopulateFrame(temp_frame.get()) < 0) { + Error("PopulateFrame failed; skipping av_image_copy fast path"); + return false; + } temp_frame->pts = frame->pts; // u_buffer/v_buffer are not members of Image on master; av_image_copy // reads the planes directly from temp_frame->data, so we don't need to @@ -3292,7 +3297,10 @@ void Image::Flip( bool leftright ) { void Image::Scale(const unsigned int new_width, const unsigned int new_height) { if (width == new_width and height == new_height) return; - AVPixelFormat format = AVPixFormat(); + // imagePixFormat is the canonical source of truth; the deprecated + // AVPixFormat() getter re-derives via (colours, subpixelorder) and would + // hit the GRAY8/YUV420P alias collision if those legacy fields ever drift. + const AVPixelFormat format = imagePixFormat; // (new_width+1)*(new_height+1)*colours undercounts planar formats: for // YUV420P, colours=1 due to the GRAY8 alias, but the buffer needs // ~1.5x for chroma. Use av_image_get_buffer_size so this works for any From 36effc8bb597a6a673ec87dd0329f489b4455af2 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 28 May 2026 23:35:32 -0400 Subject: [PATCH 18/35] fix: stride-aware Delta and Deinterlace_4Field (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The delta8_* and *_deinterlace_4field_* SIMD helpers process pixel runs or 2D blocks without any concept of per-row stride. With the FFALIGN(linesize, 32) layout, an Image's buffer may have per-row padding (linesize > width*bpp), and feeding the helpers the raw buffer pointer made them treat padding bytes as image data — wrong motion deltas and visibly broken deinterlacing on non-32-aligned widths. - Image::Delta: drive each delta8_* helper one row at a time, passing `width` pixels per call with `buffer + y*src_linesize`, `image.buffer + y*img_linesize`, and `pdiff + y*dst_linesize`. No copies; just a stride-aware caller-side loop. - Image::Deinterlace_4Field: the 4-field helpers internally use the passed `width` as their row stride, so a per-row loop wouldn't preserve their inter-row processing. Pack both inputs into tightly-laid-out (linesize == width*bpp) temp buffers, run the helper, then copy only the data bytes back into the padded source buffer (padding stays intact). Fast path skips the copy when linesize already equals width*bpp on both images (the 32-aligned-width case). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 96 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 56d548269c7..8a635e24dfc 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -2312,26 +2312,46 @@ bool Image::Delta(const Image &image, Image* targetimage) const { TimePoint start = std::chrono::steady_clock::now(); #endif + // The delta8_* SIMD helpers process N contiguous bytes / pixels without + // any concept of row stride. With FFALIGN'd linesize, an image's buffer + // may have per-row padding (linesize > width*bpp), so passing the full + // pixel count would read padding bytes as image data and produce wrong + // motion deltas. Drive the helpers row by row instead — width pixels per + // row of `this`, of `image`, and of the GRAY8 target, each at their own + // linesize stride. + const unsigned int src_linesize = linesize; + const unsigned int img_linesize = image.linesize; + const unsigned int dst_linesize = targetimage->LineSize(); + + auto delta_row = [&](void (*fn)(const uint8_t *, const uint8_t *, uint8_t *, unsigned long)) { + for (unsigned int y = 0; y < height; y++) { + fn(buffer + y * src_linesize, + image.buffer + y * img_linesize, + pdiff + y * dst_linesize, + width); + } + }; + if (imagePixFormat == AV_PIX_FMT_BGR24) { /* BGR subpixel order */ - (*delta8_bgr)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_bgr); } else if (zm_is_rgb24(imagePixFormat)) { /* Assume RGB subpixel order */ - (*delta8_rgb)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_rgb); } else if (imagePixFormat == AV_PIX_FMT_ARGB) { /* ARGB subpixel order */ - (*delta8_argb)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_argb); } else if (imagePixFormat == AV_PIX_FMT_ABGR) { /* ABGR subpixel order */ - (*delta8_abgr)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_abgr); } else if (imagePixFormat == AV_PIX_FMT_BGRA) { /* BGRA subpixel order */ - (*delta8_bgra)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_bgra); } else if (zm_is_rgb32(imagePixFormat)) { /* Assume RGBA subpixel order */ - (*delta8_rgba)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_rgba); } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { - (*delta8_gray8)(buffer, image.buffer, pdiff, pixels); + delta_row(*delta8_gray8); } else { Panic("Delta called with unexpected pixel format %d (%s); legacy colours=%d", imagePixFormat, av_get_pix_fmt_name(imagePixFormat), colours); @@ -3590,30 +3610,76 @@ void Image::Deinterlace_4Field(const Image* next_image, unsigned int threshold) Panic( "Attempt to deinterlace different sized images, expected %dx%dx%d %d, got %dx%dx%d %d", width, height, colours, subpixelorder, next_image->width, next_image->height, next_image->colours, next_image->subpixelorder); } + // The *_deinterlace_4field_* SIMD helpers use `width` as the row stride + // internally and have no concept of per-row padding. With FFALIGN'd + // linesize the source row stride may exceed width*bpp, so feeding them + // the held buffers directly would treat padding bytes as image data and + // corrupt the output for non-32-aligned widths. Pack both inputs into + // tightly-laid-out temp buffers (linesize == width*bpp), run the helper, + // then copy only the data bytes back into our linesize-padded buffer + // (leaving padding untouched). The fast path — no copies — is used + // whenever linesize already equals width*bpp on both images, which is + // the common case for 32-aligned widths. + const unsigned int bpp = (zm_bytes_per_pixel(imagePixFormat) == 1) + ? 1 + : zm_bytes_per_pixel(imagePixFormat); + const size_t row_data_bytes = static_cast(width) * bpp; + const bool needs_pack = (linesize != row_data_bytes) || (next_image->linesize != row_data_bytes); + + uint8_t *col1 = buffer; + uint8_t *col2 = next_image->buffer; + uint8_t *tmp1 = nullptr; + uint8_t *tmp2 = nullptr; + if (needs_pack) { + const size_t packed_size = row_data_bytes * height; + tmp1 = AllocBuffer(packed_size); + tmp2 = AllocBuffer(packed_size); + for (unsigned int y = 0; y < height; y++) { + memcpy(tmp1 + y * row_data_bytes, buffer + y * linesize, row_data_bytes); + memcpy(tmp2 + y * row_data_bytes, next_image->buffer + y * next_image->linesize, row_data_bytes); + } + col1 = tmp1; + col2 = tmp2; + } + if (imagePixFormat == AV_PIX_FMT_BGR24) { /* BGR subpixel order */ - std_deinterlace_4field_bgr(buffer, next_image->buffer, threshold, width, height); + std_deinterlace_4field_bgr(col1, col2, threshold, width, height); } else if (zm_is_rgb24(imagePixFormat)) { /* Assume RGB subpixel order */ - std_deinterlace_4field_rgb(buffer, next_image->buffer, threshold, width, height); + std_deinterlace_4field_rgb(col1, col2, threshold, width, height); } else if (imagePixFormat == AV_PIX_FMT_ARGB) { /* ARGB subpixel order */ - (*fptr_deinterlace_4field_argb)(buffer, next_image->buffer, threshold, width, height); + (*fptr_deinterlace_4field_argb)(col1, col2, threshold, width, height); } else if (imagePixFormat == AV_PIX_FMT_ABGR) { /* ABGR subpixel order */ - (*fptr_deinterlace_4field_abgr)(buffer, next_image->buffer, threshold, width, height); + (*fptr_deinterlace_4field_abgr)(col1, col2, threshold, width, height); } else if (imagePixFormat == AV_PIX_FMT_BGRA) { /* BGRA subpixel order */ - (*fptr_deinterlace_4field_bgra)(buffer, next_image->buffer, threshold, width, height); + (*fptr_deinterlace_4field_bgra)(col1, col2, threshold, width, height); } else if (zm_is_rgb32(imagePixFormat)) { /* Assume RGBA subpixel order */ - (*fptr_deinterlace_4field_rgba)(buffer, next_image->buffer, threshold, width, height); + (*fptr_deinterlace_4field_rgba)(col1, col2, threshold, width, height); } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { - (*fptr_deinterlace_4field_gray8)(buffer, next_image->buffer, threshold, width, height); + (*fptr_deinterlace_4field_gray8)(col1, col2, threshold, width, height); } else { - Panic("Deinterlace_4Field called with unexpected colours: %d", colours); + if (needs_pack) { + DumpBuffer(tmp1, ZM_BUFTYPE_ZM); + DumpBuffer(tmp2, ZM_BUFTYPE_ZM); + } + Panic("Deinterlace_4Field called with unexpected pixel format %d (%s)", + imagePixFormat, av_get_pix_fmt_name(imagePixFormat)); } + if (needs_pack) { + // Copy the processed Y/primary plane back into our padded buffer, + // touching only the data bytes per row so existing padding is intact. + for (unsigned int y = 0; y < height; y++) { + memcpy(buffer + y * linesize, tmp1 + y * row_data_bytes, row_data_bytes); + } + DumpBuffer(tmp1, ZM_BUFTYPE_ZM); + DumpBuffer(tmp2, ZM_BUFTYPE_ZM); + } } From f794e260ccf5e7f21cca1f4526e50edccf4ba86a Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 30 May 2026 13:54:31 -0400 Subject: [PATCH 19/35] fix: per-row Overlay; align image_pixelformats SHM pointer (PR #4788) - Image::Overlay (no-offset variant): rewrite all eight format branches to walk row-by-row using each image's own linesize. The previous linear walk with `buffer + size` as the end pointer drifted across rows whenever this->linesize != image.linesize (which can happen for images constructed via held-buffer ctors at a different stride) and also walked into the destination's chroma planes for planar YUV destinations whose `size` covers chroma. Inner loops bound by `width` per row in every branch, so per-row padding is left untouched. - Monitor::connect: align the image_pixelformats SHM base address up to alignof(AVPixelFormat) before casting. shared_images + 2*image_buffer_count*image_size can be misaligned when image_size isn't a multiple of alignof(AVPixelFormat) (e.g. GRAY8 with odd width sourced from camera->ImageSize() before the SHM upper-bound applies). An unaligned AVPixelFormat* is undefined behaviour on strict-alignment ISAs and slow even on x86. The +64-byte padding reserved in mem_size already covers the small shift. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 183 ++++++++++++++++++--------------------------- src/zm_monitor.cpp | 13 +++- 2 files changed, 86 insertions(+), 110 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 8a635e24dfc..2212f8252a4 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1939,102 +1939,86 @@ void Image::Overlay( const Image &image ) { colours, subpixelorder, image.colours, image.subpixelorder); } + // Drive every branch row-by-row using each image's own linesize. Walking + // linearly with `buffer + size` as the end was unsafe in two ways: + // (a) source and destination linesizes can differ (e.g. one came in via + // AssignDirect with a held buffer at a non-FFALIGN stride), so a shared + // index drifted across rows; (b) the destination's `size` includes chroma + // planes for planar YUV destinations, so iterating to `buffer + size` + // walked past the source's Y plane and clobbered the destination's + // chroma. After Colourise() the destination's linesize is updated to the + // new format's stride, so we re-read it inside each branch. + /* Grayscale/YUV420 on top of grayscale/YUV420 - complete */ if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { - // Overlay only the luma/primary plane. For planar YUV destinations `size` - // includes chroma planes which the GRAY8 source has no bytes for — - // walking the whole `size` would over-read image.buffer and clobber the - // destination's chroma. Bound by the smaller of the two Y-plane spans so - // a smaller source can't run past its own buffer either. - const size_t y_span = std::min( - static_cast(height) * linesize, - static_cast(image.height) * image.linesize); - const uint8_t* const max_ptr = buffer + y_span; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( *psrc ) { - *pdest = *psrc; + // Overlay only the luma/primary plane. Width is shared (panic above). + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc++, pdest++) { + if (*psrc) *pdest = *psrc; } - pdest++; - psrc++; } /* RGB24 on top of grayscale/YUV420 - convert to same format first - complete */ } else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_is_rgb24(image.imagePixFormat) ) { Colourise(image.colours, image.subpixelorder); - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc) ) { - RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); - GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); - BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc += 3, pdest += 3) { + if (RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc)) { + RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); + GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); + BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + } } - pdest += 3; - psrc += 3; } /* RGB32 on top of grayscale/YUV420 - convert to same format first - complete */ } else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_is_rgb32(image.imagePixFormat) ) { Colourise(image.colours, image.subpixelorder); - const Rgb* const max_ptr = (Rgb*)(buffer+size); - const Rgb* prsrc = (Rgb*)image.buffer; - Rgb* prdest = (Rgb*)buffer; - - if ( imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA ) { - /* RGB\BGR\RGBA\BGRA subpixel order - Alpha byte is last */ - while ( prdest < max_ptr) { - if ( RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc) ) { - *prdest = *prsrc; - } - prdest++; - prsrc++; - } - } else { - /* ABGR\ARGB subpixel order - Alpha byte is first */ - while ( prdest < max_ptr) { - if ( RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc) ) { - *prdest = *prsrc; + const bool alpha_last = (imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA); + for (unsigned int y = 0; y < height; y++) { + const Rgb *prsrc = (const Rgb *)(image.buffer + y * image.linesize); + Rgb *prdest = (Rgb *)(buffer + y * linesize); + for (unsigned int x = 0; x < width; x++, prsrc++, prdest++) { + if (alpha_last) { + if (RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc)) + *prdest = *prsrc; + } else { + if (RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc)) + *prdest = *prsrc; } - prdest++; - prsrc++; } } /* Grayscale/YUV420 on top of RGB24 - complete */ } else if ( zm_is_rgb24(imagePixFormat) && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( *psrc ) { - RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = *psrc; + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc++, pdest += 3) { + if (*psrc) { + RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = *psrc; + } } - pdest += 3; - psrc++; } /* RGB24 on top of RGB24 - not complete. need to take care of different subpixel orders */ } else if ( zm_is_rgb24(imagePixFormat) && zm_is_rgb24(image.imagePixFormat) ) { - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc) ) { - RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); - GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); - BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc += 3, pdest += 3) { + if (RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc)) { + RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); + GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); + BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + } } - pdest += 3; - psrc += 3; } /* RGB32 on top of RGB24 - TO BE DONE */ @@ -2043,28 +2027,18 @@ void Image::Overlay( const Image &image ) { /* Grayscale/YUV420 on top of RGB32 - complete */ } else if ( zm_is_rgb32(imagePixFormat) && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { - const Rgb* const max_ptr = (Rgb*)(buffer+size); - Rgb* prdest = (Rgb*)buffer; - const uint8_t* psrc = image.buffer; - - if ( imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA ) { - /* RGBA\BGRA subpixel order - Alpha byte is last */ - while ( prdest < max_ptr ) { - if ( *psrc ) { - RED_PTR_RGBA(prdest) = *psrc; - //RED_PTR_RGBA(prdest) = GREEN_PTR_RGBA(prdest) = BLUE_PTR_RGBA(prdest) = *psrc; - } - prdest++; - psrc++; - } - } else { - /* ABGR\ARGB subpixel order - Alpha byte is first */ - while ( prdest < max_ptr ) { - if ( *psrc ) { - RED_PTR_ABGR(prdest) = GREEN_PTR_ABGR(prdest) = BLUE_PTR_ABGR(prdest) = *psrc; + const bool alpha_last = (imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + Rgb *prdest = (Rgb *)(buffer + y * linesize); + for (unsigned int x = 0; x < width; x++, psrc++, prdest++) { + if (*psrc) { + if (alpha_last) { + RED_PTR_RGBA(prdest) = *psrc; + } else { + RED_PTR_ABGR(prdest) = GREEN_PTR_ABGR(prdest) = BLUE_PTR_ABGR(prdest) = *psrc; + } } - prdest++; - psrc++; } } @@ -2074,27 +2048,18 @@ void Image::Overlay( const Image &image ) { /* RGB32 on top of RGB32 - not complete. need to take care of different subpixel orders */ } else if ( zm_is_rgb32(imagePixFormat) && zm_is_rgb32(image.imagePixFormat) ) { - const Rgb* const max_ptr = (Rgb*)(buffer+size); - Rgb* prdest = (Rgb*)buffer; - const Rgb* prsrc = (Rgb*)image.buffer; - - if ( image.imagePixFormat == AV_PIX_FMT_RGBA || image.imagePixFormat == AV_PIX_FMT_BGRA ) { - /* RGB\BGR\RGBA\BGRA subpixel order - Alpha byte is last */ - while ( prdest < max_ptr ) { - if ( RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc) ) { - *prdest = *prsrc; - } - prdest++; - prsrc++; - } - } else { - /* ABGR\ARGB subpixel order - Alpha byte is first */ - while ( prdest < max_ptr ) { - if ( RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc) ) { - *prdest = *prsrc; + const bool alpha_last = (image.imagePixFormat == AV_PIX_FMT_RGBA || image.imagePixFormat == AV_PIX_FMT_BGRA); + for (unsigned int y = 0; y < height; y++) { + const Rgb *prsrc = (const Rgb *)(image.buffer + y * image.linesize); + Rgb *prdest = (Rgb *)(buffer + y * linesize); + for (unsigned int x = 0; x < width; x++, prsrc++, prdest++) { + if (alpha_last) { + if (RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc)) + *prdest = *prsrc; + } else { + if (RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc)) + *prdest = *prsrc; } - prdest++; - prsrc++; } } } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 959f352f6cf..d8280d6e971 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1166,7 +1166,18 @@ bool Monitor::connect() { // buffer region — zmc's writes to image_pixelformats[index] would corrupt // the alarm image, and zms would read alarm-image bytes back as // AVPixelFormat enum values, producing per-frame garble. - image_pixelformats = (AVPixelFormat *)(shared_images + (2 * image_buffer_count * image_size)); + // + // image_size may not be a multiple of alignof(AVPixelFormat) (for + // example GRAY8 with odd width when image_size comes from + // camera->ImageSize()). Casting an unaligned address to AVPixelFormat* + // is undefined behaviour on strict-alignment ISAs (and slow even on x86), + // so round the offset up. The +64 bytes reserved in mem_size for the + // 64-byte alignment of shared_images covers this small adjustment. + uintptr_t pixfmt_addr = reinterpret_cast( + shared_images + (2 * image_buffer_count * image_size)); + const uintptr_t pixfmt_align = alignof(AVPixelFormat); + pixfmt_addr = (pixfmt_addr + pixfmt_align - 1) & ~(pixfmt_align - 1); + image_pixelformats = reinterpret_cast(pixfmt_addr); alarm_image_pixelformat = image_pixelformats + image_buffer_count; if (purpose == CAPTURE) { From fb7b1d1c77e15e36db03b2ff09d58f4191fd9dfe Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 31 May 2026 08:27:12 -0400 Subject: [PATCH 20/35] fix: SHM padding covers both alignment adjustments (PR #4788) mem_size only reserved 64 bytes of slack for the 64-byte alignment of shared_images, but the code subsequently rounds image_pixelformats up to alignof(AVPixelFormat) too. In the worst case shared_images shifts by 63 bytes and image_pixelformats shifts by alignof(AVPixelFormat)-1 more, which could push image_pixelformats / alarm_image_pixelformat past the end of the mapped region. Reserve 63 + (alignof(AVPixelFormat) - 1) bytes so both adjustments fit. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_monitor.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index d8280d6e971..1adfe28fc6c 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1019,7 +1019,14 @@ bool Monitor::connect() { + (image_buffer_count*image_size) // alarm_images + (image_buffer_count*sizeof(AVPixelFormat)) // per-slot capture pix fmt + sizeof(AVPixelFormat) // alarm_image pix fmt (cross-process sync) - + 64; /* Padding used to permit aligning the images buffer to 64 byte boundary */ + // Padding covers two independent alignment adjustments: + // * up to 63 bytes to push shared_images to a 64-byte boundary + // * up to alignof(AVPixelFormat)-1 bytes to push + // image_pixelformats to its required alignment after the + // image_size-stride run of bytes. + // Reserve the worst case so neither adjustment can run past the + // mapped region. + + 63 + (alignof(AVPixelFormat) - 1); Debug(1, "SharedData=%zu " From 6765962737d6c9ecafc82dc2be945b7068ae5b0c Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 31 May 2026 09:22:09 -0400 Subject: [PATCH 21/35] fix: nullptr-safe pixfmt name; route GetImage/getSnapshot through ReadShmFrame (PR #4788) - Added zm_get_pix_fmt_name() in zm_pixformat.h: av_get_pix_fmt_name() returns nullptr for unknown formats (including AV_PIX_FMT_NONE), and passing nullptr to %s is undefined and can crash. Wraps with an "unknown" fallback. Replaced the five raw uses flagged by review (RemoteCameraRtsp/FfmpegCamera/VncCamera/LocalCamera constructors and MonitorStream::sendFrame's Debug log) with the wrapper. Each can hit AV_PIX_FMT_NONE before the first frame or via an unexpected (colours, subpixelorder) pair. - Monitor::GetImage and Monitor::getSnapshot now route the slot read through ReadShmFrame so the per-slot AVPixelFormat zmc recorded is adopted on image_buffer[index] before its bytes are interpreted. Without this, the JPEG encode in GetImage and the ZMPacket returned by getSnapshot would interpret SHM bytes using the placeholder format set at attach time and produce garbled output when the slot's actual format differs (the contract previously relied on callers calling ReadShmFrame themselves). getSnapshot/GetTimestamp drop const since ReadShmFrame mutates image_buffer[index]; callers in this codebase already use a non-const Monitor pointer. - Monitor::connect: updated the misleading "+64 bytes reserved" comment to match the actual reservation of 63 + (alignof(AVPixelFormat) - 1). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_ffmpeg_camera.cpp | 2 +- src/zm_libvnc_camera.cpp | 2 +- src/zm_local_camera.cpp | 2 +- src/zm_monitor.cpp | 26 +++++++++++++++++++------- src/zm_monitor.h | 4 ++-- src/zm_monitorstream.cpp | 2 +- src/zm_pixformat.h | 8 ++++++++ src/zm_remote_camera_rtsp.cpp | 2 +- 8 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index bf0d18d614d..6f543bb3e4e 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -108,7 +108,7 @@ FfmpegCamera::FfmpegCamera( pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", - pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } packet = av_packet_ptr{av_packet_alloc()}; diff --git a/src/zm_libvnc_camera.cpp b/src/zm_libvnc_camera.cpp index ae8aab482a6..876ebd6db7a 100644 --- a/src/zm_libvnc_camera.cpp +++ b/src/zm_libvnc_camera.cpp @@ -131,7 +131,7 @@ mPass(pass) { pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", - pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } if (capture) { diff --git a/src/zm_local_camera.cpp b/src/zm_local_camera.cpp index 02ec39b5316..69011d6f94f 100644 --- a/src/zm_local_camera.cpp +++ b/src/zm_local_camera.cpp @@ -374,7 +374,7 @@ LocalCamera::LocalCamera( pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected pixel format %d (%s); legacy colours=%u subpixelorder=%u", - pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } if (capture) { if (!sws_isSupportedInput(capturePixFormat)) { diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 1adfe28fc6c..42ffcdc0a43 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1178,8 +1178,10 @@ bool Monitor::connect() { // example GRAY8 with odd width when image_size comes from // camera->ImageSize()). Casting an unaligned address to AVPixelFormat* // is undefined behaviour on strict-alignment ISAs (and slow even on x86), - // so round the offset up. The +64 bytes reserved in mem_size for the - // 64-byte alignment of shared_images covers this small adjustment. + // so round the offset up. mem_size reserves + // 63 + (alignof(AVPixelFormat) - 1) bytes of slack so the combined + // 64-byte alignment of shared_images and this pixformat alignment both + // fit inside the mapped region. uintptr_t pixfmt_addr = reinterpret_cast( shared_images + (2 * image_buffer_count * image_size)); const uintptr_t pixfmt_align = alignof(AVPixelFormat); @@ -1468,11 +1470,17 @@ int Monitor::GetImage(int32_t index, int scale) { return 0; } + // Route the slot read through ReadShmFrame so the per-slot AVPixelFormat + // recorded by the capture process is adopted on image_buffer[index] + // before its bytes are interpreted. Otherwise the JPEG would be encoded + // using the placeholder format set at attach time and produce garbled + // output whenever the slot's actual format differs. + Image *src = ReadShmFrame(index); std::string filename = stringtf("Monitor%u.jpg", id); // If we are going to be modifying the snapshot before writing, then we need to copy it if ((scale != ZM_SCALE_BASE) || (!config.timestamp_on_capture)) { Image image; - image.Assign(*image_buffer[index]); + image.Assign(*src); if (scale != ZM_SCALE_BASE) { image.Scale(scale); @@ -1483,11 +1491,11 @@ int Monitor::GetImage(int32_t index, int scale) { } return image.WriteJpeg(filename); } else { - return image_buffer[index]->WriteJpeg(filename); + return src->WriteJpeg(filename); } } -std::shared_ptr Monitor::getSnapshot(int index) const { +std::shared_ptr Monitor::getSnapshot(int index) { if ((index < 0) || (index >= image_buffer_count)) { index = shared_data->last_write_index; } @@ -1496,7 +1504,11 @@ std::shared_ptr Monitor::getSnapshot(int index) const { return nullptr; } if (index != image_buffer_count) { - std::shared_ptr packet = std::make_shared (image_buffer[index], + // ReadShmFrame syncs image_buffer[index] to the per-slot format that + // zmc wrote, so the ZMPacket consumer can read its bytes in the + // correct format instead of the placeholder set at attach time. + Image *src = ReadShmFrame(index); + std::shared_ptr packet = std::make_shared (src, SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); return packet; } else { @@ -1505,7 +1517,7 @@ std::shared_ptr Monitor::getSnapshot(int index) const { return nullptr; } -SystemTimePoint Monitor::GetTimestamp(int index) const { +SystemTimePoint Monitor::GetTimestamp(int index) { std::shared_ptr packet = getSnapshot(index); if (packet) return packet->timestamp; diff --git a/src/zm_monitor.h b/src/zm_monitor.h index ef8a2d5ca8f..31048228fca 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -938,8 +938,8 @@ class Monitor : public std::enable_shared_from_this { // AVPixelFormat so reader processes can correctly interpret the SHM bytes. void WriteAlarmImage(const Image &src); int GetImage(int32_t index=-1, int scale=100); - std::shared_ptr getSnapshot( int index=-1 ) const; - SystemTimePoint GetTimestamp(int index = -1) const; + std::shared_ptr getSnapshot( int index=-1 ); + SystemTimePoint GetTimestamp(int index = -1); void UpdateAdaptiveSkip(); useconds_t GetAnalysisRate(); Microseconds GetAnalysisUpdateDelay() const { return analysis_update_delay; } diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index 05a8eb919d9..0af045d1056 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -1003,7 +1003,7 @@ void MonitorStream::SingleImage(int scale) { int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; AVPixelFormat pixformat = monitor->image_pixelformats[index]; - Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, av_get_pix_fmt_name(pixformat)); + Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, zm_get_pix_fmt_name(pixformat)); Image *snap_image = monitor->ReadShmFrame(index); if (!config.timestamp_on_capture) { monitor->TimestampImage(snap_image, diff --git a/src/zm_pixformat.h b/src/zm_pixformat.h index 88c6f49fb2a..a16fa9ae154 100644 --- a/src/zm_pixformat.h +++ b/src/zm_pixformat.h @@ -151,4 +151,12 @@ inline bool zm_is_yuv420(AVPixelFormat fmt) { return fmt == AV_PIX_FMT_YUV420P || fmt == AV_PIX_FMT_YUVJ420P; } +// av_get_pix_fmt_name returns nullptr for unknown formats (including +// AV_PIX_FMT_NONE). Passing nullptr to a %s format specifier is undefined, +// so always go through this wrapper when logging. +inline const char *zm_get_pix_fmt_name(AVPixelFormat fmt) { + const char *name = av_get_pix_fmt_name(fmt); + return name ? name : "unknown"; +} + #endif // ZM_PIXFORMAT_H diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index 18c4fbfa567..41c4f769bcd 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -83,7 +83,7 @@ RemoteCameraRtsp::RemoteCameraRtsp( pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", - pixelFormat, av_get_pix_fmt_name(pixelFormat), colours, subpixelorder); + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } } // end RemoteCameraRtsp::RemoteCameraRtsp(...) From ec8d48aa5183939eafb70a69edb8761ad41cce60 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 31 May 2026 09:31:34 -0400 Subject: [PATCH 22/35] fix: switch remaining av_get_pix_fmt_name calls in zm_ffmpeg_camera.cpp to zm_ wrapper Two Debug log sites in the hwaccel selection loop still called av_get_pix_fmt_name directly. AV_PIX_FMT_NONE is a real possibility for config->pix_fmt during the loop (the search may hit the sentinel) and for hw_pix_fmt before find_fmt_by_hw_type returns a match. Route both through zm_get_pix_fmt_name so a nullptr return can't be passed to %s. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_ffmpeg_camera.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index 6f543bb3e4e..7e60c9d96a8 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -529,7 +529,7 @@ int FfmpegCamera::OpenFfmpeg() { mVideoCodec->name, av_hwdevice_get_type_name(type), av_hwdevice_get_type_name(config->device_type), - av_get_pix_fmt_name(config->pix_fmt) + zm_get_pix_fmt_name(config->pix_fmt) ); } } // end foreach hwconfig @@ -538,7 +538,7 @@ int FfmpegCamera::OpenFfmpeg() { #endif if (hw_pix_fmt != AV_PIX_FMT_NONE) { Debug(1, "Selected hw_pix_fmt %d %s", - hw_pix_fmt, av_get_pix_fmt_name(hw_pix_fmt)); + hw_pix_fmt, zm_get_pix_fmt_name(hw_pix_fmt)); mVideoCodecContext->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL; //if (!lavc_param->check_hw_profile) From 90c6bc0b429d659ac7403e3c1f361ce47c7df31e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 1 Jun 2026 08:24:42 -0400 Subject: [PATCH 23/35] fix: guard SHM pixformat publish on assign failure (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Monitor::WriteShmFrame: image_pixelformats[index] was being recorded even when image_buffer[index]->Assign(*capture_image) silently left the slot untouched (Assign returns void; held-buffer undersize and unknown src format both early-return without mutating the dest). Readers would then adopt the newly-published format while the slot still held the previous frame's bytes, producing garble. Check the dest's post-Assign PixFormat against the source's and only publish on match; warn and keep the previously-published format otherwise so readers continue interpreting the slot bytes correctly. - Monitor::WriteAlarmImage: same fix for alarm_image — verify Assign adopted src.PixFormat() before publishing *alarm_image_pixelformat. - Image::AVPixFormat(AVPixelFormat) setter: switched the four format-name logs to the nullptr-safe zm_get_pix_fmt_name wrapper. The first Error branch triggers exactly when new_pixelformat is unrecognised, and av_get_pix_fmt_name returns nullptr for those. - monitor.php: removed the manual translate fallback for DeprecatedColoursSetting. web/includes/lang.php always merges en_gb.php as a fallback when the active language differs, so translate() never returns the bare key once en_gb.php defines it. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 12 +++++------ src/zm_monitor.cpp | 32 +++++++++++++++++++++++++++-- web/skins/classic/views/monitor.php | 11 +--------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 2212f8252a4..fb1a9e3cf53 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -5807,24 +5807,24 @@ AVPixelFormat Image::AVPixFormat(AVPixelFormat new_pixelformat) { unsigned int probe_colours, probe_subpix; if (!zm_colours_from_pixformat(new_pixelformat, probe_colours, probe_subpix)) { Error("Image::AVPixFormat: refusing to set unknown pixelformat %d %s; keeping current %s", - new_pixelformat, av_get_pix_fmt_name(new_pixelformat), - av_get_pix_fmt_name(imagePixFormat)); + new_pixelformat, zm_get_pix_fmt_name(new_pixelformat), + zm_get_pix_fmt_name(imagePixFormat)); return imagePixFormat; } int new_size = av_image_get_buffer_size(new_pixelformat, width, height, 32); int new_linesize = av_image_get_linesize(new_pixelformat, width, 0); if (new_size < 0 || new_linesize < 0) { Error("Image::AVPixFormat: av_image_get_* returned %d/%d for %s; keeping current %s", - new_size, new_linesize, av_get_pix_fmt_name(new_pixelformat), - av_get_pix_fmt_name(imagePixFormat)); + new_size, new_linesize, zm_get_pix_fmt_name(new_pixelformat), + zm_get_pix_fmt_name(imagePixFormat)); return imagePixFormat; } - Debug(4, "Old size: %u, old pixelformat %s", size, av_get_pix_fmt_name(imagePixFormat)); + Debug(4, "Old size: %u, old pixelformat %s", size, zm_get_pix_fmt_name(imagePixFormat)); colours = probe_colours; subpixelorder = probe_subpix; imagePixFormat = new_pixelformat; size = static_cast(new_size); linesize = FFALIGN(new_linesize, 32); - Debug(4, "New size: %u new pixelformat %s", size, av_get_pix_fmt_name(new_pixelformat)); + Debug(4, "New size: %u new pixelformat %s", size, zm_get_pix_fmt_name(new_pixelformat)); return imagePixFormat; } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 42ffcdc0a43..5ce76aae281 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1450,9 +1450,23 @@ void Monitor::WriteAlarmImage(const Image &src) { // Mirror WriteShmFrame's contract for alarm_image: copy bytes then // publish the canonical AVPixelFormat so reader processes can interpret // the SHM correctly via GetAlarmImage(). + // + // Only publish *alarm_image_pixelformat if Assign actually adopted the + // source format. Image::Assign silently leaves the destination untouched + // on failure (held-buffer undersize, unknown src format), so publishing + // a new format whose bytes never landed would make readers misinterpret + // the previous alarm-image contents. + const AVPixelFormat src_fmt = src.PixFormat(); alarm_image.Assign(src); if (alarm_image_pixelformat != nullptr) { - *alarm_image_pixelformat = src.PixFormat(); + if (alarm_image.PixFormat() == src_fmt) { + *alarm_image_pixelformat = src_fmt; + } else { + Warning("WriteAlarmImage: assign failed (dst fmt %s != src fmt %s); " + "keeping previously published pixelformat", + zm_get_pix_fmt_name(alarm_image.PixFormat()), + zm_get_pix_fmt_name(src_fmt)); + } } } @@ -2980,8 +2994,22 @@ void Monitor::WriteShmFrame(unsigned int index, Image *capture_image) { // Record imagePixFormat directly via PixFormat() — AVPixFormat() re-derives // from the deprecated (colours, subpixelorder) pair and could propagate // stale metadata. + // + // Only publish image_pixelformats[index] if Assign actually adopted the + // source format. Image::Assign returns void and silently leaves the + // destination untouched on failure (held-buffer undersize, unknown src + // format, etc.); publishing a new format whose bytes never landed in the + // slot would make readers misinterpret the previous slot contents. + const AVPixelFormat src_fmt = capture_image->PixFormat(); image_buffer[index]->Assign(*capture_image); - image_pixelformats[index] = capture_image->PixFormat(); + if (image_buffer[index]->PixFormat() == src_fmt) { + image_pixelformats[index] = src_fmt; + } else { + Warning("WriteShmFrame: slot %u assign failed (dst fmt %s != src fmt %s); " + "keeping previously published pixelformat", + index, zm_get_pix_fmt_name(image_buffer[index]->PixFormat()), + zm_get_pix_fmt_name(src_fmt)); + } } Image *Monitor::ReadShmFrame(unsigned int index) { diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 20eb38b9184..d2f61cf380a 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -907,16 +907,7 @@ class="nav-link Colours()) ?> - - () + ()
  • From bfe9c4ad4a61459ab3d824e8b36a48832ec050c9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 2 Jun 2026 21:39:16 -0400 Subject: [PATCH 24/35] fix: free previously-owned buffer in AssignDirect(AVFrame); nullptr-safe logs (PR #4788) - Image::AssignDirect(const AVFrame*): both the invalidate() failure path and the success path overwrote buffer/buffertype without freeing any previously-owned buffer. If the Image currently owned a ZM_BUFTYPE_ZM/MALLOC/NEW allocation, that memory was leaked on every AssignDirect(frame) call. Added DumpImgBuffer() at the top of invalidate() and immediately before the success-path buffer reassignment; DumpBuffer is a no-op for DONTFREE buffers, so this is safe whether the Image previously owned its memory or wrapped a caller's buffer. - Switched five remaining av_get_pix_fmt_name() calls flagged by review to zm_get_pix_fmt_name(): * Monitor::GetAlarmImage warnings (zm_monitor.cpp:1432, 1438-1439) * Monitor::ReadShmFrame warnings (zm_monitor.cpp:3042, 3049-3050) * Camera ctor fallback Error (zm_camera.cpp:90) All can be reached with AV_PIX_FMT_NONE or unrecognised formats where av_get_pix_fmt_name returns nullptr, which is undefined when fed to %s. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_camera.cpp | 2 +- src/zm_image.cpp | 12 ++++++++++++ src/zm_monitor.cpp | 12 ++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/zm_camera.cpp b/src/zm_camera.cpp index 9f3ed7b7c93..3f3c8c62a33 100644 --- a/src/zm_camera.cpp +++ b/src/zm_camera.cpp @@ -87,7 +87,7 @@ Camera::Camera( int raw_imagesize = av_image_get_buffer_size(pixelFormat, width, height, 1); if (raw_linesize < 0 || raw_imagesize < 0) { Error("Camera: av_image_get_* returned %d/%d for pixelFormat=%s; falling back", - raw_linesize, raw_imagesize, av_get_pix_fmt_name(pixelFormat)); + raw_linesize, raw_imagesize, zm_get_pix_fmt_name(pixelFormat)); linesize = width * colours; imagesize = static_cast(height) * linesize; } else { diff --git a/src/zm_image.cpp b/src/zm_image.cpp index fb1a9e3cf53..7a4f63cd747 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -789,6 +789,12 @@ uint8_t* Image::WriteBuffer( void Image::AssignDirect(const AVFrame *frame) { auto invalidate = [&]() { + // Release any previously-owned buffer before overwriting buffer/buffertype; + // a held (DONTFREE) buffer is left in place by DumpImgBuffer, so this is + // safe whether the Image currently owns its memory or wraps somebody + // else's. Without this step a ZM_BUFTYPE_ZM allocation would be leaked + // every time AssignDirect(frame) hits the invalidate path. + DumpImgBuffer(); width = 0; height = 0; pixels = 0; @@ -852,6 +858,12 @@ void Image::AssignDirect(const AVFrame *frame) { } } + // Release any previously-owned buffer before we replace it with + // frame->data[0]. DumpImgBuffer is a no-op for DONTFREE buffers, so this + // is safe whether the Image currently owns its memory or wraps somebody + // else's. Without this step a ZM_BUFTYPE_ZM allocation would be leaked + // every time AssignDirect(frame) succeeds. + DumpImgBuffer(); width = frame->width; height = frame->height; buffer = frame->data[0]; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 5ce76aae281..e658f749e32 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1429,14 +1429,14 @@ Image *Monitor::GetAlarmImage() { unsigned int probe_colours, probe_subpix; if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) { Warning("GetAlarmImage: ignoring unsupported pixelformat %d; keeping current %s", - fmt, av_get_pix_fmt_name(alarm_image.PixFormat())); + fmt, zm_get_pix_fmt_name(alarm_image.PixFormat())); } else { int required = av_image_get_buffer_size(fmt, width, height, 32); if (required < 0 || static_cast(required) > shm_slot_size) { Warning("GetAlarmImage: format %s requires %d bytes but slot capacity is %zu; " "keeping current %s", - av_get_pix_fmt_name(fmt), required, shm_slot_size, - av_get_pix_fmt_name(alarm_image.PixFormat())); + zm_get_pix_fmt_name(fmt), required, shm_slot_size, + zm_get_pix_fmt_name(alarm_image.PixFormat())); } else { alarm_image.AVPixFormat(fmt); } @@ -3039,15 +3039,15 @@ Image *Monitor::ReadShmFrame(unsigned int index) { unsigned int probe_colours, probe_subpix; if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) { Warning("ReadShmFrame: ignoring unsupported pixelformat %d in slot %u; keeping current %s", - fmt, index, av_get_pix_fmt_name(image_buffer[index]->PixFormat())); + fmt, index, zm_get_pix_fmt_name(image_buffer[index]->PixFormat())); return image_buffer[index]; } int required = av_image_get_buffer_size(fmt, width, height, 32); if (required < 0 || static_cast(required) > shm_slot_size) { Warning("ReadShmFrame: format %s requires %d bytes but slot %u capacity is %zu; " "keeping current %s to avoid overrun", - av_get_pix_fmt_name(fmt), required, index, shm_slot_size, - av_get_pix_fmt_name(image_buffer[index]->PixFormat())); + zm_get_pix_fmt_name(fmt), required, index, shm_slot_size, + zm_get_pix_fmt_name(image_buffer[index]->PixFormat())); return image_buffer[index]; } image_buffer[index]->AVPixFormat(fmt); From bc8be20e682c18fb43254b79ba777fcf0744c9ca Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 2 Jun 2026 22:16:52 -0400 Subject: [PATCH 25/35] fix: use canonical imagePixFormat in Image::Assign(AVFrame*) (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The destination format for the same-format fast path was derived via the deprecated AVPixFormat() getter, which re-derives from the legacy (colours, subpixelorder) pair. If those fields ever drift out of sync with imagePixFormat — or hit the GRAY8/YUV420P alias collision — the fast path would compare against the wrong target and either fall through to sws_scale unnecessarily or, worse, av_image_copy the wrong layout. Use imagePixFormat directly and drop the manual av_get_pix_fmt_name nullptr dance in the Debug log in favour of the zm_get_pix_fmt_name wrapper. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 7a4f63cd747..58d8fa5909c 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -361,8 +361,12 @@ bool Image::Assign(const AVFrame *frame) { } zm_dump_video_frame(frame, "source frame in Image::Assign"); - AVPixelFormat format = (AVPixelFormat)AVPixFormat(); - AVPixelFormat src_fmt = static_cast(frame->format); + // imagePixFormat is the canonical destination format; the deprecated + // AVPixFormat() getter re-derives via (colours, subpixelorder) and would + // pick the wrong swscale target if those legacy fields drift out of sync + // (e.g. the GRAY8/YUV420P alias collision). + const AVPixelFormat format = imagePixFormat; + const AVPixelFormat src_fmt = static_cast(frame->format); // If source and destination format + dimensions match, do a direct plane // copy instead of running through sws_scale. This avoids the overhead of @@ -370,9 +374,8 @@ bool Image::Assign(const AVFrame *frame) { if (src_fmt == format && frame->width == static_cast(width) && frame->height == static_cast(height)) { - const char *fmt_name = av_get_pix_fmt_name(format); Debug(4, "Same format %s %dx%d, using av_image_copy", - fmt_name ? fmt_name : "unknown", width, height); + zm_get_pix_fmt_name(format), width, height); av_frame_ptr temp_frame{av_frame_alloc()}; if (!temp_frame) { Error("Unable to allocate destination frame"); From 9bae26b9900e5cabf28084ea6a9ee8973f0c739e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 3 Jun 2026 07:45:55 -0400 Subject: [PATCH 26/35] fix: handle "no frames yet" sentinel before bounds check (PR #4788) GetImage() and getSnapshot() both ran the bounds check `(size_t)index >= image_buffer.size()` before testing the sentinel `index == image_buffer_count`. When no frames have been written yet, shared_data->last_write_index equals image_buffer_count, the initial fallback assigns that to index, and image_buffer.size() also equals image_buffer_count (the vector is sized to image_buffer_count). So the bounds check fired first, logging the misleading "Image Buffer has not been allocated" and returning -1/nullptr instead of reaching the intended "no images in buffer" branch (return 0 / nullptr with a proper message). Reorder both functions to handle the sentinel after confirming image_buffer is non-empty, then do the bounds check, then the actual slot read. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_monitor.cpp | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index e658f749e32..2154722486a 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1475,14 +1475,23 @@ int Monitor::GetImage(int32_t index, int scale) { Debug(1, "Invalid index %d passed. image_buffer_count = %d", index, image_buffer_count); index = shared_data->last_write_index; } - if (!image_buffer.size() or static_cast(index) >= image_buffer.size()) { + if (!image_buffer.size()) { Error("Image Buffer has not been allocated"); return -1; } - if ( index == image_buffer_count ) { + // Check the sentinel BEFORE the bounds check: when last_write_index is + // still image_buffer_count (no frames written yet), index equals the + // sentinel which also equals image_buffer.size(), so a plain bounds + // check would mask the intended "no images" branch and log a misleading + // "buffer not allocated" error. + if (index == image_buffer_count) { Error("Unable to generate image, no images in buffer"); return 0; } + if (static_cast(index) >= image_buffer.size()) { + Error("Image Buffer has not been allocated"); + return -1; + } // Route the slot read through ReadShmFrame so the per-slot AVPixelFormat // recorded by the capture process is adopted on image_buffer[index] @@ -1513,22 +1522,28 @@ std::shared_ptr Monitor::getSnapshot(int index) { if ((index < 0) || (index >= image_buffer_count)) { index = shared_data->last_write_index; } - if (!image_buffer.size() or static_cast(index) >= image_buffer.size()) { + if (!image_buffer.size()) { Error("Image Buffer has not been allocated"); return nullptr; } - if (index != image_buffer_count) { - // ReadShmFrame syncs image_buffer[index] to the per-slot format that - // zmc wrote, so the ZMPacket consumer can read its bytes in the - // correct format instead of the placeholder set at attach time. - Image *src = ReadShmFrame(index); - std::shared_ptr packet = std::make_shared (src, - SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); - return packet; - } else { + // Sentinel before bounds: index == image_buffer_count means "no frames + // written yet" (last_write_index is still the sentinel). Without this + // ordering the bounds check below would fire first and log a misleading + // "buffer not allocated" error. + if (index == image_buffer_count) { Error("Unable to generate image, no images in buffer"); + return nullptr; } - return nullptr; + if (static_cast(index) >= image_buffer.size()) { + Error("Image Buffer has not been allocated"); + return nullptr; + } + // ReadShmFrame syncs image_buffer[index] to the per-slot format that + // zmc wrote, so the ZMPacket consumer can read its bytes in the + // correct format instead of the placeholder set at attach time. + Image *src = ReadShmFrame(index); + return std::make_shared(src, + SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); } SystemTimePoint Monitor::GetTimestamp(int index) { From bc88781a08fc67936f7dd75299893454f0076f32 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 3 Jun 2026 10:30:49 -0400 Subject: [PATCH 27/35] fix: Image::Assign(uint8_t*) handle packed source -> aligned dest (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The raw-buffer Assign overload previously validated buffer_size against av_image_get_buffer_size(..., align=32) and then memcpy'd `size` bytes flat. But callers feed it packed source buffers — Camera::ImageSize() is now derived with align=1 to describe device buffers (V4L2 mmap, raw RTP, etc.) — so for non-32-aligned widths: * the size check spuriously rejected valid packed source buffers, and * if relaxed, the flat memcpy would have read past the source. Validate the source size against the packed (align=1) layout and the destination size/allocation against the aligned (align=32) layout, then copy plane-by-plane via av_image_copy. av_image_fill_arrays gives us both layouts' per-plane pointers and strides, and av_image_copy walks each plane with its own src/dst linesize so per-row padding never over-reads the source. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 49 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 58d8fa5909c..a9227b20e82 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -992,21 +992,25 @@ void Image::Assign( return; } - // Derive the required byte size from the AVPixelFormat. The legacy - // width*height*colours formula undercounts planar formats like YUV420P - // (colours==1 but chroma planes exist), leading to partial copies and - // corrupted output. - int raw_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 32); - if (raw_size < 0) { - Error("av_image_get_buffer_size failed (%d) for %s %ux%u", - raw_size, av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + // Source and destination may use different alignment. Callers pass + // device-side buffers sized at align=1 (e.g. Camera::ImageSize() is now + // packed) while ZM's internal Image keeps a 32-byte-aligned layout for + // SIMD. Validate the source size against the packed layout and the + // destination/allocation against the aligned layout, then copy + // plane-by-plane so per-row padding doesn't read past the source. + int packed_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 1); + int aligned_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 32); + if (packed_size < 0 || aligned_size < 0) { + Error("av_image_get_buffer_size failed (%d/%d) for %s %ux%u", + packed_size, aligned_size, av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); return; } - unsigned int new_size = static_cast(raw_size); - if ( buffer_size < new_size ) { - Error("Attempt to assign buffer from an undersized buffer of size: %zu (need %u)", buffer_size, new_size); + if (buffer_size < static_cast(packed_size)) { + Error("Attempt to assign buffer from an undersized buffer of size: %zu (need %d packed)", + buffer_size, packed_size); return; } + const unsigned int new_size = static_cast(aligned_size); if ( !buffer || p_width != width || p_height != height || p_colours != colours || p_subpixelorder != subpixelorder ) { @@ -1035,8 +1039,27 @@ void Image::Assign( linesize = FFALIGN(av_image_get_linesize(new_pix_fmt, p_width, 0), 32); } - if ( new_buffer != buffer ) - (*fptr_imgbufcpy)(buffer, new_buffer, size); + if (new_buffer != buffer) { + // Plane-aware copy: src is laid out packed (align=1), dst is laid out + // aligned (align=32). av_image_copy walks each plane using its own + // src/dst linesize so per-row padding doesn't read past the source. + uint8_t *src_data[4] = {nullptr, nullptr, nullptr, nullptr}; + int src_strides[4] = {0, 0, 0, 0}; + uint8_t *dst_data[4] = {nullptr, nullptr, nullptr, nullptr}; + int dst_strides[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(src_data, src_strides, + const_cast(new_buffer), + new_pix_fmt, p_width, p_height, 1) < 0 + || av_image_fill_arrays(dst_data, dst_strides, buffer, + new_pix_fmt, p_width, p_height, 32) < 0) { + Error("Assign: av_image_fill_arrays failed for %s %ux%u", + av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + return; + } + av_image_copy(dst_data, dst_strides, + const_cast(src_data), src_strides, + new_pix_fmt, p_width, p_height); + } update_function_pointers(); } From de3638ec48704b8500207f540e561dfb63147d00 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 3 Jun 2026 20:13:05 -0400 Subject: [PATCH 28/35] fix: Image::Assign(Image&) handle smaller-source linesize; explicit pixdesc.h (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image::Assign(const Image&): when the source linesize was *smaller* than the destination's (e.g. src came from a held-buffer ctor with a packed device stride while dst is FFALIGN'd to 32 from av_image_get_buffer_size), the function fell through to the flat memcpy of `size` bytes — over-reading the source by (linesize - image.linesize) * height bytes. Undefined behaviour; could crash or copy unrelated memory. Generalised the per-row branch to handle both directions: trigger whenever linesize != image.linesize and copy min(src, dst) bytes per row, so neither buffer is read or written past its capacity. Planar formats still refuse explicitly since per-row would lose chroma; the error message now reads "vs" instead of ">" to match the broader condition. - Added an explicit `#include ` in zm_image.cpp. The recent Rotate/Flip rewrites call av_pix_fmt_desc_get / av_pix_fmt_count_planes / use AVPixFmtDescriptor, which previously only compiled thanks to transitive includes from zm_ffmpeg.h → libavutil/imgutils.h. Make the dependency explicit. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index a9227b20e82..10057585769 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -23,6 +23,9 @@ #include "zm_poly.h" #include "zm_swscale.h" #include "zm_utils.h" + +#include + #include #include #include @@ -1115,36 +1118,39 @@ void Image::Assign(const Image &image) { } if ( image.buffer != buffer ) { - if (image.linesize > linesize) { - // Source has more per-row padding than the destination can hold. This - // branch is only reached when dimensions and colours/subpixelorder - // match but linesize disagrees — an oddly-shaped Image. + if (image.linesize != linesize) { + // Dimensions/colours/subpixelorder all match but linesizes disagree. + // This happens when src and dst use different alignment conventions + // (e.g. dst is FFALIGN'd to 32 from av_image_get_buffer_size while + // src came from a held-buffer ctor with a packed driver stride). // - // For planar formats (YUV420P/J420P/YUV422P/J422P) the per-row copy - // here only touches the Y plane; chroma planes would need plane-aware - // copying with separate strides via av_image_copy_plane. Doing a - // silent Y-only copy on a planar image leaves U/V uninitialised in - // the destination, producing solid-green output downstream. Refuse - // the assignment for planar formats with stride mismatch — the - // caller can fall back to a real sws_scale or re-allocate. + // For planar formats (YUV420P/J420P/YUV422P/J422P) a per-row copy of + // just the Y plane would leave U/V uninitialised in the destination, + // producing solid-green output downstream. Plane-aware copying via + // av_image_copy_plane would be needed and the caller may as well + // reconvert; refuse explicitly. if (zm_is_yuv420(image.imagePixFormat) || image.imagePixFormat == AV_PIX_FMT_YUV422P || image.imagePixFormat == AV_PIX_FMT_YUVJ422P) { - Error("Image::Assign: planar format %s with stride mismatch (src %d > dst %d) " + Error("Image::Assign: planar format %s with stride mismatch (src %d vs dst %d) " "is not supported by the per-row copy; chroma planes would be lost. " "Caller must convert or reallocate.", av_get_pix_fmt_name(image.imagePixFormat), image.linesize, linesize); return; } - // Packed formats (GRAY8, RGB24/BGR24, RGB32 family): copy only the - // destination row capacity per line to avoid writing past the - // destination on the last row. Source padding bytes beyond the - // common row width are discarded. - Debug(1, "Must copy line by line due to different line size %d != %d", image.linesize, linesize); + // Packed formats: copy min(src, dst) bytes per row so we never write + // past the destination row capacity AND never read past the source + // row capacity. Source padding (when src > dst) is discarded; dest + // padding (when src < dst) is left untouched. Both directions are + // safe — the previous flat-memcpy path silently over-read the + // source when image.linesize < linesize. + const unsigned int copy_bytes = std::min(image.linesize, linesize); + Debug(1, "Must copy line by line due to different line size %d != %d (copy %u)", + image.linesize, linesize, copy_bytes); uint8_t *src_ptr = image.buffer; uint8_t *dst_ptr = buffer; for (unsigned int i=0; i< image.height; i++) { - (*fptr_imgbufcpy)(dst_ptr, src_ptr, linesize); + (*fptr_imgbufcpy)(dst_ptr, src_ptr, copy_bytes); src_ptr += image.linesize; dst_ptr += linesize; } From 9e19c6689cd25a086b6ee8ea67c85b6c1eff510f Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 3 Jun 2026 21:23:37 -0400 Subject: [PATCH 29/35] fix: Image(AVFrame*) ctor delegates to default ctor before AssignDirect (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Image(const AVFrame*) constructor only initialised blend_buffer_ and blend_buffer_size_ before calling AssignDirect(frame). AssignDirect now calls DumpImgBuffer() on both the failure (invalidate) and success paths to release any previously-owned buffer — but in the freshly-constructed Image, buffer/buffertype/allocation are uninitialised, so DumpImgBuffer would read garbage and potentially free an invalid pointer. Use constructor delegation so Image() runs first (zero-initialises every member, sets buffertype = DONTFREE), making the subsequent DumpImgBuffer call a no-op on a known-empty Image. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 10057585769..2dedff9a1ec 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -312,9 +312,12 @@ Image::Image(const AVFrame *frame, int p_width, int p_height) : this->Assign(frame); } -Image::Image(const AVFrame *frame) { - blend_buffer_ = nullptr; - blend_buffer_size_ = 0; +Image::Image(const AVFrame *frame) : Image() { + // Delegate to the default ctor first so buffer/buffertype/allocation + // are fully initialised before AssignDirect runs. AssignDirect's failure + // and success paths both call DumpImgBuffer() to release any previously + // owned buffer; without this delegation those calls would read + // uninitialised members and potentially free a garbage pointer. AssignDirect(frame); } From 6ecf51061a2fb6b8e8c930ef307b584f088486b0 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 3 Jun 2026 22:12:26 -0400 Subject: [PATCH 30/35] fix: more actionable error messages in GetImage/getSnapshot/WriteBuffer (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Monitor::GetImage and Monitor::getSnapshot bounds-check branches were logging "Image Buffer has not been allocated", but by the time control reaches them the empty-vector check has already passed — the buffer IS allocated, the index is just out of range. Replaced with explicit "index N out of range (image_buffer.size() = M)" so debugging starts from the right place. - Image::WriteBuffer unsupported-format error only printed `colours`, but the mapping in zm_pixformat_from_colours depends on both `colours` and `subpixelorder`, and planar formats legitimately have colours=1. Print both fields so a bad (colours, subpixelorder) pair is immediately identifiable. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 4 +++- src/zm_monitor.cpp | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 2dedff9a1ec..c1ea79babbe 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -733,7 +733,9 @@ uint8_t* Image::WriteBuffer( AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); if (p_pixfmt == AV_PIX_FMT_NONE) { - Error("WriteBuffer called with unexpected colours: %d", p_colours); + Error("WriteBuffer called with unsupported (colours=%u, subpixelorder=%u) pair; " + "no AVPixelFormat mapping", + p_colours, p_subpixelorder); return nullptr; } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 2154722486a..813b93d28f5 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1489,7 +1489,8 @@ int Monitor::GetImage(int32_t index, int scale) { return 0; } if (static_cast(index) >= image_buffer.size()) { - Error("Image Buffer has not been allocated"); + Error("GetImage: index %d out of range (image_buffer.size() = %zu)", + index, image_buffer.size()); return -1; } @@ -1535,7 +1536,8 @@ std::shared_ptr Monitor::getSnapshot(int index) { return nullptr; } if (static_cast(index) >= image_buffer.size()) { - Error("Image Buffer has not been allocated"); + Error("getSnapshot: index %d out of range (image_buffer.size() = %zu)", + index, image_buffer.size()); return nullptr; } // ReadShmFrame syncs image_buffer[index] to the per-slot format that From 12d864e77236a900cdc3970f1e3bc0963ceccf8b Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 4 Jun 2026 20:10:39 -0400 Subject: [PATCH 31/35] fix: Deinterlace_Discard OOB write on odd heights (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The y += 2 loop ran while `y < height`, but copied even row y into odd row y+1. For odd image heights, the final iteration had y == height-1 and wrote to row height — past the end of the image buffer in all three colour branches (GRAY8/YUV-Y plane, RGB24, RGB32). Stop one short of the last row (y < height - 1) so y+1 stays in bounds; the orphan last row in odd-height images is left untouched (consistent with the (even, odd) pairing this discard pass is doing). Also added a guard for height < 2 to keep the unsigned `height - 1` calculation from wrapping. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index c1ea79babbe..5639451c757 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -3384,11 +3384,15 @@ void Image::Deinterlace_Discard() { // Use linesize as the per-row byte stride so non-32-aligned widths copy // from/to the correct row offsets. Still copies `width` pixels per row - // (the padding bytes are untouched). + // (the padding bytes are untouched). Stop one short of the last row + // when height is odd — y == height-1 would write to row height which + // is past the buffer. + if (height < 2) return; + const unsigned int last_src = static_cast(height) - 1; if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { const uint8_t *psrc; uint8_t *pdest; - for (unsigned int y = 0; y < (unsigned int)height; y += 2) { + for (unsigned int y = 0; y < last_src; y += 2) { psrc = buffer + y * linesize; pdest = buffer + (y + 1) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { @@ -3398,7 +3402,7 @@ void Image::Deinterlace_Discard() { } else if ( zm_is_rgb24(imagePixFormat) ) { const uint8_t *psrc; uint8_t *pdest; - for (unsigned int y = 0; y < (unsigned int)height; y += 2) { + for (unsigned int y = 0; y < last_src; y += 2) { psrc = buffer + y * linesize; pdest = buffer + (y + 1) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { @@ -3410,7 +3414,7 @@ void Image::Deinterlace_Discard() { } else if ( zm_is_rgb32(imagePixFormat) ) { const Rgb *psrc; Rgb *pdest; - for (unsigned int y = 0; y < (unsigned int)height; y += 2) { + for (unsigned int y = 0; y < last_src; y += 2) { psrc = (Rgb*)(buffer + y * linesize); pdest = (Rgb*)(buffer + (y + 1) * linesize); for (unsigned int x = 0; x < (unsigned int)width; x++) { From 965936eaf89f054587041807071af5150e9015be Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 4 Jun 2026 21:26:55 -0400 Subject: [PATCH 32/35] fix: pass delta8_* fn ptrs directly; zm_get_pix_fmt_name in Delta panic (PR #4788) - Image::Delta: pass delta8_bgr/rgb/argb/abgr/bgra/rgba/gray8 directly to delta_row() instead of dereferencing with *. They're already function pointers; the deref is redundant and would be undefined behaviour if any pointer were null (Initialise() sets them, but the explicit * obscures the intent and adds nothing). - Switched the Delta fallback Panic from av_get_pix_fmt_name() to the nullptr-safe zm_get_pix_fmt_name() wrapper. The Panic fires exactly when imagePixFormat is unrecognised, where the raw function would return nullptr. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 5639451c757..b22ce3acb65 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -2348,27 +2348,27 @@ bool Image::Delta(const Image &image, Image* targetimage) const { if (imagePixFormat == AV_PIX_FMT_BGR24) { /* BGR subpixel order */ - delta_row(*delta8_bgr); + delta_row(delta8_bgr); } else if (zm_is_rgb24(imagePixFormat)) { /* Assume RGB subpixel order */ - delta_row(*delta8_rgb); + delta_row(delta8_rgb); } else if (imagePixFormat == AV_PIX_FMT_ARGB) { /* ARGB subpixel order */ - delta_row(*delta8_argb); + delta_row(delta8_argb); } else if (imagePixFormat == AV_PIX_FMT_ABGR) { /* ABGR subpixel order */ - delta_row(*delta8_abgr); + delta_row(delta8_abgr); } else if (imagePixFormat == AV_PIX_FMT_BGRA) { /* BGRA subpixel order */ - delta_row(*delta8_bgra); + delta_row(delta8_bgra); } else if (zm_is_rgb32(imagePixFormat)) { /* Assume RGBA subpixel order */ - delta_row(*delta8_rgba); + delta_row(delta8_rgba); } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { - delta_row(*delta8_gray8); + delta_row(delta8_gray8); } else { Panic("Delta called with unexpected pixel format %d (%s); legacy colours=%d", - imagePixFormat, av_get_pix_fmt_name(imagePixFormat), colours); + imagePixFormat, zm_get_pix_fmt_name(imagePixFormat), colours); } #ifdef ZM_IMAGE_PROFILING From 2b55ed45d12f8aca5cdfe298d0730dcd4f425fbc Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 6 Jun 2026 10:01:13 -0400 Subject: [PATCH 33/35] fix: nullptr-safe pix fmt name in Colourise/Deinterlace_4Field error paths (PR #4788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image::Colourise sizing-failure Error: av_image_get_buffer_size / av_image_get_linesize return negative for AV_PIX_FMT_NONE and unrecognised formats, so this branch fires exactly when av_get_pix_fmt_name(p_req_pixfmt) can return nullptr. Switched to zm_get_pix_fmt_name. - Image::Deinterlace_4Field fallback Panic: same issue — fires when imagePixFormat is outside our dispatch set, where av_get_pix_fmt_name may return nullptr. Switched to zm_get_pix_fmt_name. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_image.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index b22ce3acb65..095fc2ae931 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -2674,7 +2674,7 @@ void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqs int dst_linesize_signed = av_image_get_linesize(p_req_pixfmt, width, 0); if (new_size_signed < 0 || dst_linesize_signed < 0) { Error("Colourise: av_image sizing failed for %s %ux%u", - av_get_pix_fmt_name(p_req_pixfmt), width, height); + zm_get_pix_fmt_name(p_req_pixfmt), width, height); return; } const size_t new_size = static_cast(new_size_signed); @@ -3686,7 +3686,7 @@ void Image::Deinterlace_4Field(const Image* next_image, unsigned int threshold) DumpBuffer(tmp2, ZM_BUFTYPE_ZM); } Panic("Deinterlace_4Field called with unexpected pixel format %d (%s)", - imagePixFormat, av_get_pix_fmt_name(imagePixFormat)); + imagePixFormat, zm_get_pix_fmt_name(imagePixFormat)); } if (needs_pack) { From b486c0cd6b52efa6626114eafccc7dc7d013a9d6 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 6 Jun 2026 11:49:30 -0400 Subject: [PATCH 34/35] fix: include in zm_pixformat.h (PR #4788) zm_get_pix_fmt_name() calls av_get_pix_fmt_name() which is declared in . The header previously got away with relying on transitive includes via zm_ffmpeg.h, but tests/zm_pixformat.cpp (and any other consumer that includes only zm_pixformat.h) would fail to compile. Add the explicit include so the header is self-contained. refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_pixformat.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zm_pixformat.h b/src/zm_pixformat.h index a16fa9ae154..c53068f59cc 100644 --- a/src/zm_pixformat.h +++ b/src/zm_pixformat.h @@ -14,6 +14,8 @@ #include "zm_ffmpeg.h" #include "zm_rgb.h" +#include + // // Central pixel format conversion helpers. // From 9687faac67cc8f268675b86c8dfcfbf19933fe8b Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 6 Jun 2026 12:29:35 -0400 Subject: [PATCH 35/35] refactor: remove dead FfmpegCamera::imagePixFormat field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field was assigned in three branches of the constructor (always to the same value as Camera::pixelFormat) and never read anywhere. Unlike LocalCamera, FfmpegCamera doesn't run a sws_scale step at the Camera layer — libavcodec hands frames to the pipeline directly — so there's no separate capture-vs-image format distinction to track. Drop the redundant assignments and the member declaration. The canonical camera-side format is now exclusively Camera::pixelFormat (via Camera::PixelFormat()). refs #4788 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_ffmpeg_camera.cpp | 3 --- src/zm_ffmpeg_camera.h | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index 7e60c9d96a8..728b419dafd 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -96,15 +96,12 @@ FfmpegCamera::FfmpegCamera( * will receive correct colours and subpixel order */ if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; - imagePixFormat = AV_PIX_FMT_RGBA; pixelFormat = AV_PIX_FMT_RGBA; } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGB; - imagePixFormat = AV_PIX_FMT_RGB24; pixelFormat = AV_PIX_FMT_RGB24; } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; - imagePixFormat = AV_PIX_FMT_GRAY8; pixelFormat = AV_PIX_FMT_GRAY8; } else { Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", diff --git a/src/zm_ffmpeg_camera.h b/src/zm_ffmpeg_camera.h index e45f5516eb6..611cbc38676 100644 --- a/src/zm_ffmpeg_camera.h +++ b/src/zm_ffmpeg_camera.h @@ -54,8 +54,6 @@ class FfmpegCamera : public Camera { int frameCount; - _AVPIXELFORMAT imagePixFormat; - bool use_hwaccel; //will default to on if hwaccel specified, will get turned off if there is a failure #if HAVE_LIBAVUTIL_HWCONTEXT_H AVBufferRef *hw_device_ctx = nullptr;