/* Copyright (C) 2000-2003 Constantin Kaplinsky.  All Rights Reserved.
 * Copyright (C) 2004-2005 Cendio AB. All rights reserved.
 * Copyright (C) 2011 D. R. Commander.  All Rights Reserved.
 * Copyright 2014 Pierre Ossman for Cendio AB
 * Copyright (C) 2020-2026 m-privacy GmbH.  All Rights Reserved.
 * 
 * This 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 software 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 software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

#include <rfb/MPDecompressor.h>
#include <rdr/Exception.h>
#include <rfb/Rect.h>
#include <rfb/PixelFormat.h>
#include <rfb/LogWriter.h>

#include <stdio.h>
#include <libyuv/convert_argb.h>

#include <stdio.h>
extern "C" {
#include <jpeglib.h>
#include <libavcodec/avcodec.h>
}
#include <jerror.h>
#include <setjmp.h>
#include <zstd.h>

using namespace rfb;

static rfb::LogWriter vlog("MPDecompressor");

unsigned MPDecompressor::compressionCount[MP_COMPRESSION_MAX+1];

struct MP_JPEG_ERROR_MGR {
  struct jpeg_error_mgr pub;
  jmp_buf jmpBuffer;
  char lastError[JMSG_LENGTH_MAX];
};
static void
JpegErrorExit(j_common_ptr jpegDinfo)
{
  MP_JPEG_ERROR_MGR *jpegErr = (MP_JPEG_ERROR_MGR *)jpegDinfo->err;

  (*jpegDinfo->err->output_message)(jpegDinfo);
  longjmp(jpegErr->jmpBuffer, 1);
}
static void
JpegOutputMessage(j_common_ptr jpegDinfo)
{
  MP_JPEG_ERROR_MGR *jpegErr = (MP_JPEG_ERROR_MGR *)jpegDinfo->err;

  (*jpegDinfo->err->format_message)(jpegDinfo, jpegErr->lastError);
}
struct MP_JPEG_SRC_MGR {
  struct jpeg_source_mgr pub;
  MPDecompressor *instance;
};
static void
JpegNoOp(j_decompress_ptr jpegDinfo)
{
}
static boolean
JpegFillInputBuffer(j_decompress_ptr jpegDinfo)
{
  ERREXIT(jpegDinfo, JERR_BUFFER_SIZE);
  return TRUE;
}
static void
JpegSkipInputData(j_decompress_ptr jpegDinfo, long num_bytes)
{
  MP_JPEG_SRC_MGR *jpegSrc = (MP_JPEG_SRC_MGR *)jpegDinfo->src;

  if (unlikely(num_bytes < 0 || (size_t)num_bytes > jpegSrc->pub.bytes_in_buffer)) {
    ERREXIT(jpegDinfo, JERR_BUFFER_SIZE);
  } else {
    jpegSrc->pub.next_input_byte += (size_t) num_bytes;
    jpegSrc->pub.bytes_in_buffer -= (size_t) num_bytes;
  }
}

MPDecompressor::MPDecompressor(size_t threadNum_) : zstdDctx(NULL)
{
	threadNum = threadNum_;
	vlog.debug("initializing for Decoder thread %lu", threadNum);

	dataBuffer = NULL;
	dataBufferLen = 0;
	tmpDataBuffer = NULL;
	tmpDataBufferLen = 0;

#pragma GCC ivdep
	for (int i = MP_COMPRESSION_MIN; i <= MP_COMPRESSION_MAX; i++) {
		compressionCountLocal[i] = 0;
		totalBytes[i] = 0;
		totalCBytes[i] = 0;
		minCompressed[i] = 100000;
		maxCompressed[i] = 0;
	}
}

MPDecompressor::~MPDecompressor()
{
	if (zstdDctx)
		ZSTD_freeDCtx(zstdDctx);
	if (dataBuffer) {
		free(dataBuffer);
		vlog.debug("thread %lu: dataBufferLen %u", threadNum, dataBufferLen);
	}
	if (tmpDataBuffer) {
		free(tmpDataBuffer);
		vlog.debug("thread %lu: tmpDataBufferLen %u", threadNum, tmpDataBufferLen);
	}

	if (vlog.getLevel() >= 100) {
		unsigned compressionCountSum = 0;
		rdr::U64 totalBytesSum = 0;
		rdr::U64 totalCBytesSum = 0;
		int minCompressedTotal = 100000;
		int maxCompressedTotal = 0;
		for (int i = MP_COMPRESSION_MIN; i <= MP_COMPRESSION_MAX; i++) {
			if (compressionCountLocal[i] > 0 && totalBytes[i] > 0) {
				compressionCountSum += compressionCountLocal[i];
				vlog.debug("thread %lu: compression %u %s: %u calls decompressed %llu Bytes from %llu Bytes with ratio %llu%%, average rect size %llu uncompressed and %llu compressed, min/max compressed size %u/%u", threadNum, i, mpCompressionName(i), compressionCountLocal[i], totalBytes[i], totalCBytes[i], 100 * totalCBytes[i] / totalBytes[i], totalBytes[i]/compressionCountLocal[i], totalCBytes[i]/compressionCountLocal[i], minCompressed[i], maxCompressed[i]);
				totalBytesSum += totalBytes[i];
				totalCBytesSum += totalCBytes[i];
				if (minCompressed[i] < minCompressedTotal)
					minCompressedTotal = minCompressed[i];
				if (maxCompressed[i] > maxCompressedTotal)
					maxCompressedTotal = maxCompressed[i];
			}
		}
		if (compressionCountSum > 0 && totalBytesSum > 0 && totalCBytesSum > 0)
			vlog.debug("thread %lu: %u calls decompressed %llu Bytes from %llu Bytes with ratio %llu%%, average rect size %llu uncompressed and %llu compressed, min/max compressed size %u/%u", threadNum, compressionCountSum, totalBytesSum, totalCBytesSum, 100 * totalCBytesSum / totalBytesSum, totalBytesSum/compressionCountSum, totalCBytesSum/compressionCountSum, minCompressedTotal, maxCompressedTotal);
	}
}

rdr::U8 * MPDecompressor::getDataBuffer(const int newsize, const char * compName)
{
	if (unlikely(dataBufferLen < newsize)) {
		vlog.verbose2("getDataBuffer: resize dataBuffer for %s from %u to %u", compName, dataBufferLen, newsize);
		rdr::U8 * tmp = (rdr::U8*) realloc(dataBuffer, newsize);
		if (unlikely(!tmp)) {
			vlog.error("dataBuffer realloc(%u) for %s failed", newsize, compName);
			return NULL;
		}
		dataBuffer = tmp;
		dataBufferLen = newsize;
	}
	return dataBuffer;
}

rdr::U8 * MPDecompressor::getTmpDataBuffer(const int newsize, const char * compName)
{
	if (unlikely(tmpDataBufferLen < newsize)) {
		vlog.verbose2("getTmpDataBuffer: resize tmpDataBuffer for %s from %u to %u", compName, tmpDataBufferLen, newsize);
		rdr::U8 * tmp = (rdr::U8*) realloc(tmpDataBuffer, newsize);
		if (unlikely(!tmp)) {
			vlog.error("tmpDataBuffer realloc(%u) for %s failed", newsize, compName);
			return NULL;
		}
		tmpDataBuffer = tmp;
		tmpDataBufferLen = newsize;
	}
	return tmpDataBuffer;
}

void MPDecompressor::decompress(const rdr::U8 *mpBuf, int mpBufLen,
	rdr::U8 *buf, int stride, const Rect& r, const PixelFormat& pf)
{
	const rdr::U8 *srcBuf = mpBuf;
	int srcBufLen = mpBufLen;

	int w = r.width();
	int h = r.height();

	const bool is888 = pf.is888();
	rdr::U8 *transBuf = buf;
	const int targetBufPixelsize = 4;
	int mpPixelsize;
	int mpStride;
	rdr::U8 * data;
	rdr::U8 mpCompression;
	int mpWidth;
	int mpHeight;

	if (stride == 0)
		stride = w;

	/* read header */
	mpCompression = *srcBuf;
	srcBuf++;
	srcBufLen--;
	/* flags: highest 4 Bits for mpLevel */
	const int mpLevel = *srcBuf >> 4;
	srcBuf++;
	srcBufLen--;

	compressionCount[mpCompression]++;
	compressionCountLocal[mpCompression]++;
	totalBytes[mpCompression] += w * h * targetBufPixelsize;
	totalCBytes[mpCompression] += srcBufLen;
	if (unlikely(srcBufLen < minCompressed[mpCompression]))
		minCompressed[mpCompression] = srcBufLen;
	if (unlikely(srcBufLen > maxCompressed[mpCompression]))
		maxCompressed[mpCompression] = srcBufLen;

	/* solid? */
	if (mpCompression == MP_COMPRESSION_SOLID) {
		if (likely(is888)) {
			const int stridesize = stride * targetBufPixelsize;
			rdr::U8 * line = buf;

			vlog.verbose2("SOLID direct: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, raw size: %u, compressed: %u, colour RGBA: %#02X%02X%02X%02X", r.tl.x, r.tl.y, w, h, targetBufPixelsize, stride, h * w * targetBufPixelsize, srcBufLen, *srcBuf, *(srcBuf+1), *(srcBuf+2), *(srcBuf+3));
#pragma GCC ivdep
			for (int y = 0; y < h ; y++) {
#pragma GCC ivdep
				for (int x = 0; x < w ; x++) {
					memcpy(line + x * targetBufPixelsize, srcBuf, targetBufPixelsize);
				}
				line += stridesize;
			}
		} else {
			static const PixelFormat pfRGBX(32, 24, false, true, 255, 255, 255, 0, 8, 16);

			vlog.verbose2("SOLID buffered: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, raw size: %u, compressed: %u, colour RGBA: %#02X%02X%02X%02X", r.tl.x, r.tl.y, w, h, targetBufPixelsize, stride, h * w * targetBufPixelsize, srcBufLen, *srcBuf, *(srcBuf+1), *(srcBuf+2), *(srcBuf+3));
			data = getDataBuffer(w * h * targetBufPixelsize, "SOLID");
			if (unlikely(!data))
				return;
#pragma GCC ivdep
			for (int i = 0; i < w * h ; i++)
				memcpy(data + i * targetBufPixelsize, srcBuf, targetBufPixelsize);
			pf.bufferFromBuffer(buf, pfRGBX, data, w, h, stride, w);
		}
		return;
	}

	/* process body */
	if (mpCompression == MP_COMPRESSION_RAW) {
		data = (rdr::U8 *) srcBuf;
		mpWidth = w;
		mpHeight = h;
		mpPixelsize = 4;
		mpStride = mpWidth * mpPixelsize;
	} else if (mpCompression == MP_COMPRESSION_JPEG) {
		struct jpeg_decompress_struct *jpegDinfo;
		struct MP_JPEG_ERROR_MGR *jpegErr;
		struct MP_JPEG_SRC_MGR *jpegSrc;
		JSAMPROW *jpegRowPointer = NULL;
#ifdef JCS_EXTENSIONS
		bool direct = false;
#endif

		mpWidth = w;
		mpHeight = h;
#ifdef JCS_EXTENSIONS
		if (likely(is888))
			direct = true;
		mpPixelsize = 4;
#else
		mpPixelsize = 3;
#endif
		mpStride = mpWidth * mpPixelsize;
//		vlog.verbose2("JPEG: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpWidth: %u, mpHeight: %u, mpStride: %u, raw size: %u, compressed: %u", r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpWidth, mpHeight, mpStride, h * w * mpPixelsize, srcBufLen);

		jpegDinfo = new jpeg_decompress_struct;
		jpegErr = new struct MP_JPEG_ERROR_MGR;
		jpegDinfo->err = jpeg_std_error(&jpegErr->pub);
		snprintf(jpegErr->lastError, JMSG_LENGTH_MAX, "No error");
		jpegErr->pub.error_exit = JpegErrorExit;
		jpegErr->pub.output_message = JpegOutputMessage;
		if(unlikely(setjmp(jpegErr->jmpBuffer))) {
			// this will execute if libjpeg has an error
			throw rdr::Exception("%s", jpegErr->lastError);
		}
		jpeg_create_decompress(jpegDinfo);
		jpegSrc = new struct MP_JPEG_SRC_MGR;
		jpegSrc->pub.init_source = JpegNoOp;
		jpegSrc->pub.fill_input_buffer = JpegFillInputBuffer;
		jpegSrc->pub.skip_input_data = JpegSkipInputData;
		jpegSrc->pub.resync_to_restart = jpeg_resync_to_restart;
		jpegSrc->pub.term_source = JpegNoOp;
		jpegSrc->instance = this;
		jpegDinfo->src = (struct jpeg_source_mgr *)jpegSrc;
		if(unlikely(setjmp(jpegErr->jmpBuffer))) {
			// this will execute if libjpeg has an error
			jpeg_abort_decompress(jpegDinfo);
			if (jpegRowPointer) delete[] jpegRowPointer;
			throw rdr::Exception("%s", jpegErr->lastError);
		}
		jpegSrc->pub.next_input_byte = (const JOCTET *) srcBuf;
		jpegSrc->pub.bytes_in_buffer = srcBufLen;
		jpeg_read_header(jpegDinfo, TRUE);
#ifdef JCS_EXTENSIONS
		jpegDinfo->out_color_space = JCS_EXT_RGBX;
#else
		jpegDinfo->out_color_space = JCS_RGB;
#endif
		jpegRowPointer = new JSAMPROW[mpHeight];
#ifdef JCS_EXTENSIONS
		if (likely(direct)) {
//			vlog.verbose2("direct: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
			mpStride = stride * targetBufPixelsize;
			data = buf;
		} else {
#endif
			data = getDataBuffer(mpHeight * mpStride, "JPEG");
			if (unlikely(!data)) {
				delete[] jpegRowPointer;
				return;
			}
#ifdef JCS_EXTENSIONS
		}
#endif
#pragma GCC ivdep
		for (int y = 0; y < mpHeight; y++)
			jpegRowPointer[y] = (JSAMPROW) data + y * mpStride;
		jpeg_start_decompress(jpegDinfo);
		if (unlikely(   jpegDinfo->output_width != (unsigned) mpWidth
			     || jpegDinfo->output_height != (unsigned) mpHeight
			     || jpegDinfo->output_components != mpPixelsize
			    )
		   ) {
			jpeg_abort_decompress(jpegDinfo);
			if (jpegRowPointer) delete[] jpegRowPointer;
			vlog.debug("rect size mismatch: expected %ux%ux%u, got MP image with %ux%ux%u", mpWidth, mpHeight, mpPixelsize, jpegDinfo->output_width, jpegDinfo->output_height, jpegDinfo->output_components);
			return;
		}
		while (jpegDinfo->output_scanline < jpegDinfo->output_height) {
			jpeg_read_scanlines(jpegDinfo, &jpegRowPointer[jpegDinfo->output_scanline],
					jpegDinfo->output_height - jpegDinfo->output_scanline);
		}
		jpeg_finish_decompress(jpegDinfo);
		delete[] jpegRowPointer;
		if(unlikely(setjmp(jpegErr->jmpBuffer))) {
			// this will execute if libjpeg has an error
			return;
		}
		jpeg_destroy_decompress(jpegDinfo);
		delete jpegErr;
		delete jpegSrc;
		delete jpegDinfo;
#ifdef JCS_EXTENSIONS
		if (likely(direct))
			return;
#endif
	} else if (mpCompression == MP_COMPRESSION_H264_I444 || mpCompression == MP_COMPRESSION_H264_I420 || mpCompression == MP_COMPRESSION_H264) {
		const AVCodec *av_codec;
		AVCodecContext *av_context = NULL;
		AVFrame *av_frame;
		AVPacket *av_pkt;
		int res;
		bool got_frame = false;

//		vlog.verbose("H264_I4XX: got buffer of size %u", srcBufLen);
		av_codec = avcodec_find_decoder(AV_CODEC_ID_H264);
		if (unlikely(!av_codec)) {
			vlog.error("H264_I4XX: avcodec_find_decoder() failed");
			return;
		}
		av_pkt = av_packet_alloc();
		if (unlikely(!av_pkt)) {
			vlog.error("H264_I4XX: av_packet_alloc() failed");
			return;
		}
		av_context = avcodec_alloc_context3(av_codec);
		if (unlikely(!av_context)) {
			vlog.error("H264_I4XX: avcodec_alloc_context3() failed");
			av_packet_free(&av_pkt);
			return;
		}
		res = avcodec_open2(av_context, av_codec, NULL);
		if (unlikely(res < 0)) {
			vlog.error("H264_I4XX: avcodec_open2() failed");
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			return;
		}
		av_frame = av_frame_alloc();
		if (unlikely(!av_frame)) {
			vlog.error("H264_I4XX: av_frame_alloc() failed");
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			return;
		}

		av_pkt->data = (uint8_t*) srcBuf;
		av_pkt->size = srcBufLen;
		res = avcodec_send_packet(av_context, av_pkt);
		if (unlikely(res < 0)) {
			vlog.error("H264_I4XX: avcodec_send_packet(av_pkt) failed with error %i", res);
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			av_frame_free(&av_frame);
			return;
		}
		res = avcodec_send_packet(av_context, NULL);
		if (unlikely(res < 0)) {
			vlog.error("H264_I4XX: avcodec_send_packet(NULL) failed with error %i", res);
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			av_frame_free(&av_frame);
			return;
		}
		while (res >= 0) {
			res = avcodec_receive_frame(av_context, av_frame);
			if (unlikely(res == AVERROR(EAGAIN) || res == AVERROR_EOF)) {
				vlog.verbose("H264_I4XX: avcodec_receive_frame returned AVERROR(EAGAIN) or AVERROR_EOF");
				break;
			}
			if (unlikely(res < 0)) {
				vlog.error("H264_I4XX: avcodec_receive_frame() failed with error %i", res);
				av_packet_free(&av_pkt);
				avcodec_free_context(&av_context);
				av_frame_free(&av_frame);
				return;
			}
			if (unlikely(!av_frame->width || !av_frame->height)) {
				vlog.verbose("H264_I4XX: got delayed frame with zero width or height");
				continue;
			}
//			vlog.verbose("H264_I4XX: got delayed frame with size %ux%u", av_frame->width, av_frame->height);
			got_frame = true;
			break;
		}
		if (unlikely(!got_frame)) {
			vlog.error("H264_I4XX: no frame received!");
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			av_frame_free(&av_frame);
			return;
		}
		mpWidth = av_frame->width;
		mpHeight = av_frame->height;
		/* sanity checks */
		if (unlikely(   (mpWidth != w && mpWidth != w + 1)
			     || (mpHeight != h && mpHeight != h + 1)
			    )
		   ) {
			vlog.debug("H264_I4XX: rect size mismatch: expected %u-%u x %u-%u, got MP image with %ux%u", w, w + 1, h, h + 1, mpWidth, mpHeight);
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			av_frame_free(&av_frame);
			return;
		}
		mpPixelsize = 4;
		mpStride = mpWidth * mpPixelsize;
		if (likely(is888)) {
			/* direct */
			vlog.verbose("direct: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u, AVPixelFormat %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, mpBufLen, av_frame->format);
			if (av_frame->format == AV_PIX_FMT_YUV420P) {
				libyuv::I420ToABGR(av_frame->data[0], av_frame->linesize[0], av_frame->data[1], av_frame->linesize[1], av_frame->data[2], av_frame->linesize[2], buf, stride * targetBufPixelsize, w, h);
			} else if (av_frame->format == AV_PIX_FMT_YUV444P) {
				libyuv::I444ToABGR(av_frame->data[0], av_frame->linesize[0], av_frame->data[1], av_frame->linesize[1], av_frame->data[2], av_frame->linesize[2], buf, stride * targetBufPixelsize, w, h);
			} else {
				vlog.error("H264_I4XX: unsupported AVPixelFormat %u!", av_frame->format);
			}
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			av_frame_free(&av_frame);
			return;
		}
		data = getDataBuffer(mpHeight * mpStride, "H264");
		if (unlikely(!data)) {
			av_packet_free(&av_pkt);
			avcodec_free_context(&av_context);
			av_frame_free(&av_frame);
			return;
		}
		vlog.verbose("compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u, AVPixelFormat %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, mpBufLen, av_frame->format);
		if (av_frame->format == AV_PIX_FMT_YUV420P) {
			libyuv::I420ToABGR(av_frame->data[0], av_frame->linesize[0], av_frame->data[1], av_frame->linesize[1], av_frame->data[2], av_frame->linesize[2], data, mpStride, w, h);
		} else if (av_frame->format == AV_PIX_FMT_YUV444P) {
			libyuv::I444ToABGR(av_frame->data[0], av_frame->linesize[0], av_frame->data[1], av_frame->linesize[1], av_frame->data[2], av_frame->linesize[2], data, mpStride, w, h);
		} else {
			vlog.error("H264_I4XX: unsupported AVPixelFormat %u!", av_frame->format);
		}
		av_packet_free(&av_pkt);
		avcodec_free_context(&av_context);
		av_frame_free(&av_frame);
	} else if (mpCompression == MP_COMPRESSION_RAWI420) {
		int expectedDataSize;
		const rdr::U8 * srcBufU;
		const rdr::U8 * srcBufV;

		mpWidth = w;
		mpHeight = h;
		mpPixelsize = 4;
		mpStride = mpWidth * mpPixelsize;
		const int ustride = ((mpWidth + 1) / 2);
		const int vstride = ustride;
		/* U and V each need 1/4 of pixels plus extra column for
		 * uneven mpWidth and extra line for uneven mpHeight
		 */
		expectedDataSize = mpWidth * mpHeight * 3 / 2 + 2 * ((mpWidth & 1) * mpHeight + (mpHeight & 1) * mpWidth);
//		vlog.verbose2("RAWI420: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpWidth: %u, mpHeight: %u, mpStride: %u, raw size: %u, compressed: %u", r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpWidth, mpHeight, mpStride, h * w * mpPixelsize, srcBufLen);
		if (unlikely(srcBufLen != expectedDataSize)) {
			vlog.error("RAWI420: buffer size mismatch: expected %u, got %u", expectedDataSize, srcBufLen);
			return;
		}
		srcBufU = srcBuf + mpWidth * mpHeight;
		srcBufV = srcBufU + mpWidth * mpHeight / 4 + (mpWidth & 1) * mpHeight + (mpHeight & 1) * mpWidth;
		if (likely(is888)) {
			/* direct */
			vlog.verbose2("direct: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
			libyuv::I420ToABGR(srcBuf, mpWidth, srcBufU, ustride, srcBufV, vstride, buf, stride * targetBufPixelsize, mpWidth, mpHeight);
			return;
		}
		data = getDataBuffer(mpHeight * mpStride, "RAWI420");
		if (unlikely(!data))
			return;
		libyuv::I420ToABGR(srcBuf, mpWidth, srcBufU, ustride, srcBufV, vstride, data, mpStride, mpWidth, mpHeight);
	} else if (mpCompression == MP_COMPRESSION_RAWI444) {
		int expectedDataSize;
		const rdr::U8 * srcBufU;
		const rdr::U8 * srcBufV;

		mpWidth = w;
		mpHeight = h;
		mpPixelsize = 4;
		mpStride = mpWidth * mpPixelsize;
		expectedDataSize = 3 * mpHeight * mpWidth;
		vlog.verbose2("RAWI444: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpWidth: %u, mpHeight: %u, mpStride: %u, raw size: %u, compressed: %u", r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpWidth, mpHeight, mpStride, h * w * mpPixelsize, srcBufLen);
		if (unlikely(srcBufLen != expectedDataSize)) {
			vlog.error("RAWI444: buffer size mismatch: expected %u, got %u", expectedDataSize, srcBufLen);
			return;
		}
		srcBufU = srcBuf + mpWidth * mpHeight;
		srcBufV = srcBufU + mpWidth * mpHeight;
		if (likely(is888)) {
			/* direct */
			vlog.verbose2("direct: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
			libyuv::I444ToABGR(srcBuf, mpWidth, srcBufU, mpWidth, srcBufV, mpWidth, buf, stride * targetBufPixelsize, mpWidth, mpHeight);
			return;
		}
		data = getDataBuffer(mpHeight * mpStride, "RAWI444");
		if (unlikely(!data))
			return;
		libyuv::I444ToABGR(srcBuf, mpWidth, srcBufU, mpWidth, srcBufV, mpWidth, data, mpStride, mpWidth, mpHeight);
	} else if (mpCompression == MP_COMPRESSION_ZSTD) {
		int dataSize;

		mpWidth = w;
		mpHeight = h;
		mpPixelsize = 4;
		mpStride = mpWidth * mpPixelsize;
//		vlog.verbose2("ZSTD: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpWidth: %u, mpHeight: %u, mpStride: %u, raw size: %u, compressed: %u", r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpWidth, mpHeight, mpStride, h * w * mpPixelsize, srcBufLen);
		dataSize = ZSTD_getFrameContentSize(srcBuf, srcBufLen);
		if (unlikely(dataSize != mpStride * mpHeight)) {
			vlog.error("ZSTD: data size mismatch: expected %u, got %i", mpStride * mpHeight, dataSize);
			return;
		}
		if (unlikely(!zstdDctx)) {
			zstdDctx = ZSTD_createDCtx();
			if (unlikely(!zstdDctx)) {
				vlog.error("ZSTD_createDCtx() failed");
				return;
			}
		}
		data = getDataBuffer(dataSize, "ZSTD");
		if (unlikely(!data))
			return;
		dataSize = ZSTD_decompressDCtx(zstdDctx, data, dataSize, srcBuf, srcBufLen);
		if (unlikely(dataSize < 0)) {
			vlog.error("ZSTD: ZSTD_decompressDCtx() failed with error %i", dataSize);
			return;
		}
//		vlog.verbose("ZSTD: compressed: %u, decompressed: %u, ratio: %u%%", srcBufLen, dataSize, 100 * srcBufLen / dataSize);
	} else if (mpCompression == MP_COMPRESSION_I420ZSTD || mpCompression == MP_COMPRESSION_I444ZSTD) {
		int expectedUncompSize;
		rdr::U8 * uncompBuf;
		int uncompBufSize;
		const rdr::U8 * uncompBufU;
		const rdr::U8 * uncompBufV;

		mpWidth = w;
		mpHeight = h;
		mpPixelsize = 4;
		mpStride = mpWidth * mpPixelsize;
//		vlog.verbose2("I420+ZSTD: x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpWidth: %u, mpHeight: %u, mpStride: %u, raw size: %u, compressed: %u", r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpWidth, mpHeight, mpStride, h * w * mpPixelsize, srcBufLen);
		if (mpCompression == MP_COMPRESSION_I420ZSTD)
			expectedUncompSize = mpWidth * mpHeight * 3 / 2 + 2 * ((mpWidth & 1) * mpHeight + (mpHeight & 1) * mpWidth);
		else /* I444ZSTD */
			expectedUncompSize = 3 * mpWidth * mpHeight;
		uncompBufSize = ZSTD_getFrameContentSize(srcBuf, srcBufLen);
		if (unlikely(uncompBufSize != expectedUncompSize)) {
			vlog.error("I420ZSTD/I444ZSTD: uncompressed data size mismatch: expected %u, got %i", expectedUncompSize, uncompBufSize);
			return;
		}
		if (unlikely(!zstdDctx)) {
			zstdDctx = ZSTD_createDCtx();
			if (unlikely(!zstdDctx)) {
				vlog.error("ZSTD_createDCtx() failed");
				return;
			}
		}
		uncompBuf = getTmpDataBuffer(uncompBufSize, "I420ZSTD/I444ZSTD");
		if (unlikely(!uncompBuf)) {
			vlog.error("I420ZSTD/I444ZSTD: failed to alloc memory for uncompressed data");
			return;
		}
		uncompBufSize = ZSTD_decompressDCtx(zstdDctx, uncompBuf, uncompBufSize, srcBuf, srcBufLen);
		if (unlikely(uncompBufSize < 0)) {
			vlog.error("I420ZSTD/I444ZSTD: ZSTD_decompressDCtx() failed with error %i", uncompBufSize);
			return;
		}
		uncompBufU = uncompBuf + mpWidth * mpHeight;
		if (mpCompression == MP_COMPRESSION_I420ZSTD)
			uncompBufV = uncompBufU + mpWidth * mpHeight / 4 + (mpWidth & 1) * mpHeight + (mpHeight & 1) * mpWidth;
		else /* I444ZSTD */
			uncompBufV = uncompBufU + mpWidth * mpHeight;
		if (likely(is888)) {
			/* direct */
			vlog.verbose2("direct: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
			if (mpCompression == MP_COMPRESSION_I420ZSTD)
				libyuv::I420ToABGR(uncompBuf, mpWidth, uncompBufU, (mpWidth + 1) / 2, uncompBufV, (mpWidth + 1) / 2, buf, stride * targetBufPixelsize, mpWidth, mpHeight);
			else /* I444ZSTD */
				libyuv::I444ToABGR(uncompBuf, mpWidth, uncompBufU, mpWidth, uncompBufV, mpWidth, buf, stride * targetBufPixelsize, mpWidth, mpHeight);
			return;
		}
		data = getDataBuffer(mpStride * mpHeight, "I420ZSTD/I444ZSTD");
		if (unlikely(!data)) {
			return;
		}
		if (mpCompression == MP_COMPRESSION_I420ZSTD)
			libyuv::I420ToABGR(uncompBuf, mpWidth, uncompBufU, (mpWidth + 1) / 2, uncompBufV, (mpWidth + 1) / 2, data, mpStride, mpWidth, mpHeight);
		else
			libyuv::I444ToABGR(uncompBuf, mpWidth, uncompBufU, mpWidth, uncompBufV, mpWidth, data, mpStride, mpWidth, mpHeight);
	} else {
		vlog.error("invalid MP compression: %u, mpLevel: %u,w %u, h %u", mpCompression, mpLevel, w, h);
		return;
	}
	if (unlikely(!is888)) {
		vlog.verbose("not is888, use transBuf: w: %u, h: %u, stride: %u, mpWidth: %u, mpStride: %u", w, h, stride, mpWidth, mpStride);
		transBuf = (rdr::U8 *) malloc(w * h * targetBufPixelsize);
		if (unlikely(!transBuf)) {
			vlog.error("failed to alloc transBuf");
			return;
		}
	}
	if(stride != w || mpStride != w * targetBufPixelsize) {
		const int srcoffset = mpStride;
		const int targetoffset = stride * targetBufPixelsize;
		int y;

		if (mpPixelsize == targetBufPixelsize) {
			const int linesize = (w <= mpWidth ? w * targetBufPixelsize : mpWidth * targetBufPixelsize);

			vlog.verbose2("line: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
#pragma GCC ivdep
			for (y = 0; y < h ; y++)
				memcpy(transBuf + y * targetoffset, data + y * srcoffset, linesize);
		} else {
			const rdr::U8 * srcline = data;
			rdr::U8 * targetline = transBuf;
			int x;

			vlog.verbose2("pixel: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
#pragma GCC ivdep
			for (y = 0; y < h ; y++) {
#pragma GCC ivdep
				for (x = 0; x < w ; x++) {
					memcpy(targetline + x * targetBufPixelsize, srcline + x * mpPixelsize, mpPixelsize);
					targetline[x * targetBufPixelsize + mpPixelsize] = 0;
				}
				targetline += targetoffset;
				srcline += srcoffset;
			}
		}
	} else {
		vlog.verbose2("copy: compression: %u (%s), mpLevel: %u, x: %u, y: %u, w: %u, h: %u, mpPixelsize: %u, stride: %u, mpStride: %u, raw size: %u, compressed: %u", mpCompression, mpCompressionName(mpCompression), mpLevel, r.tl.x, r.tl.y, w, h, mpPixelsize, stride, mpStride, h * w * mpPixelsize, srcBufLen);
		if (transBuf != data)
			memcpy(transBuf, data, w * h * targetBufPixelsize);
	}

	if (unlikely(!is888)) {
		if (mpPixelsize == targetBufPixelsize) {
			static const PixelFormat pfRGBX(32, 24, false, true, 255, 255, 255, 0, 8, 16);

			pf.bufferFromBuffer(buf, pfRGBX, transBuf, w, h, stride, w);
		} else {
			pf.bufferFromRGB(buf, transBuf, w, stride, h);
		}
		free(transBuf);
	}
}
