diff --git a/YUViewLib/src/handler/ItemMemoryHandler.h b/YUViewLib/src/handler/ItemMemoryHandler.h index b82ac9711..9b5c85913 100644 --- a/YUViewLib/src/handler/ItemMemoryHandler.h +++ b/YUViewLib/src/handler/ItemMemoryHandler.h @@ -33,6 +33,7 @@ #pragma once #include +#include namespace itemMemoryHandler { diff --git a/YUViewLib/src/video/yuv/PixelFormatYUV.cpp b/YUViewLib/src/video/yuv/PixelFormatYUV.cpp index e7e4b29fa..bd8c8e850 100644 --- a/YUViewLib/src/video/yuv/PixelFormatYUV.cpp +++ b/YUViewLib/src/video/yuv/PixelFormatYUV.cpp @@ -123,10 +123,7 @@ std::string formatSubsamplingWithColons(const Subsampling &subsampling) PixelFormatYUV::PixelFormatYUV(const std::string_view name) { if (auto predefinedFormat = PredefinedPixelFormatMapper.getValue(name)) - { - if (*predefinedFormat == PredefinedPixelFormat::V210) - this->predefinedPixelFormat = predefinedFormat; - } + this->predefinedPixelFormat = predefinedFormat; std::regex strExpr( "([YUVA]{3,6}(?:\\(IL\\))?) (4:[4210]{1}:[4210]{1}) ([0-9]{1,2})-bit[ ]?([BL]{1}E)?[ " @@ -307,7 +304,26 @@ bool PixelFormatYUV::isValid() const bool PixelFormatYUV::canConvertToRGB(Size imageSize, std::string *whyNot) const { if (this->predefinedPixelFormat.has_value()) + { + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV20) + { + if (imageSize.width % 2 != 0) + { + if (whyNot) + whyNot->append("The item width must be divisible by 2 for this format.\n"); + return false; + } + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15 && + imageSize.height % 2 != 0) + { + if (whyNot) + whyNot->append("The item height must be divisible by 2 for this format.\n"); + return false; + } + } return true; + } if (!this->isValid()) { if (whyNot) @@ -379,6 +395,33 @@ int64_t PixelFormatYUV::bytesPerFrame(const Size &frameSize) const auto roundedUpWidth = (((frameSize.width + 48 - 1) / 48) * 48); return frameSize.height * roundedUpWidth * 16 / 6; } + + // The semi planar NV15/NV20/NV30 formats: + // A Y plane followed by an interleaved Cb/Cr plane. + // 4 x 10 bit samples are packed into 5 bytes (little endian 40 bit word, + // the first sample in the lowest bits). The packing restarts at every line. + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV20 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV30) + { + const auto bytesPerLine = [](unsigned nrSamples) { + // 4 samples per 5 bytes, rounded up per line + return ((nrSamples + 3) / 4) * 5; + }; + const auto w = frameSize.width; + const auto h = frameSize.height; + + int64_t bytes = int64_t(h) * bytesPerLine(w); // The Y plane + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15) + // Chroma: w/2 U and w/2 V samples per line, h/2 lines + bytes += int64_t(h / 2) * bytesPerLine(w); + else if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV20) + // Chroma: w/2 U and w/2 V samples per line, h lines + bytes += int64_t(h) * bytesPerLine(w); + else // NV30: Chroma: w U and w V samples per line, h lines + bytes += int64_t(h) * bytesPerLine(2 * w); + return bytes; + } return -1; } @@ -502,6 +545,11 @@ unsigned PixelFormatYUV::getNrPlanes() const { if (*this->predefinedPixelFormat == PredefinedPixelFormat::V210) return 3; + // The semi planar NV 10bit formats have a Y plane and an interleaved UV plane + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV20 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV30) + return 2; return 0; } @@ -519,6 +567,12 @@ Subsampling PixelFormatYUV::getSubsampling() const { if (*this->predefinedPixelFormat == PredefinedPixelFormat::V210) return Subsampling::YUV_422; + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15) + return Subsampling::YUV_420; + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV20) + return Subsampling::YUV_422; + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV30) + return Subsampling::YUV_444; return Subsampling::UNKNOWN; } @@ -570,6 +624,10 @@ unsigned PixelFormatYUV::getBitsPerSample() const { if (*this->predefinedPixelFormat == PredefinedPixelFormat::V210) return 10; + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV20 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV30) + return 10; return 0; } @@ -622,6 +680,8 @@ Offset PixelFormatYUV::getChromaOffset() const { if (*this->predefinedPixelFormat == PredefinedPixelFormat::V210) return Offset({0, 0}); + else if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15) + return Offset({0, 1}); // 420 return Offset({0, 0}); } @@ -634,6 +694,11 @@ bool PixelFormatYUV::isBytePacking() const { if (*this->predefinedPixelFormat == PredefinedPixelFormat::V210) return true; + // The NV 10bit formats pack 4 x 10 bit samples into 5 bytes + if (*this->predefinedPixelFormat == PredefinedPixelFormat::NV15 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV20 || + *this->predefinedPixelFormat == PredefinedPixelFormat::NV30) + return true; return false; } diff --git a/YUViewLib/src/video/yuv/PixelFormatYUV.h b/YUViewLib/src/video/yuv/PixelFormatYUV.h index a49606089..12231bb56 100644 --- a/YUViewLib/src/video/yuv/PixelFormatYUV.h +++ b/YUViewLib/src/video/yuv/PixelFormatYUV.h @@ -113,11 +113,21 @@ enum class PredefinedPixelFormat { // https://developer.apple.com/library/archive/technotes/tn2162/_index.html#//apple_ref/doc/uid/DTS40013070-CH1-TNTAG8-V210__4_2_2_COMPRESSION_TYPE // Packed 422 format with 12 10 bit values in 16 bytes - V210 + V210, + // https://github.com/torvalds/linux/blob/master/include/uapi/drm/drm_fourcc.h + // 2 plane YCbCr + // index 0 = Y plane, [39:0] Y3:Y2:Y1:Y0 little endian + // index 1 = Cr:Cb plane, [39:0] Cr1:Cb1:Cr0:Cb0 little endian + NV15, // 420 + NV20, // 422 + NV30, // 444 }; -constexpr EnumMapper PredefinedPixelFormatMapper = { - std::make_pair(PredefinedPixelFormat::V210, "V210")}; +constexpr EnumMapper PredefinedPixelFormatMapper = { + std::make_pair(PredefinedPixelFormat::V210, "V210"), + std::make_pair(PredefinedPixelFormat::NV15, "NV15"), + std::make_pair(PredefinedPixelFormat::NV20, "NV20"), + std::make_pair(PredefinedPixelFormat::NV30, "NV30")}; enum class PackingOrder { diff --git a/YUViewLib/src/video/yuv/videoHandlerYUV.cpp b/YUViewLib/src/video/yuv/videoHandlerYUV.cpp index d99debd22..91a6f034c 100644 --- a/YUViewLib/src/video/yuv/videoHandlerYUV.cpp +++ b/YUViewLib/src/video/yuv/videoHandlerYUV.cpp @@ -383,6 +383,123 @@ std::pair convertV210PackedToPlanar(const QByteArray &sour return {true, newFormat}; } +// Get the 10 bit sample with the given index from a line of NV15/NV20/NV30 data. +// 4 x 10 bit samples are packed into 5 bytes: A 40 bit little endian word +// containing the samples like [39:0] Y3:Y2:Y1:Y0 (the first sample is in the lowest 10 bits). +// The packing restarts at the beginning of every line. The chroma plane is interleaved +// (Cr1:Cb1:Cr0:Cb0 per group, so in sample order: Cb0, Cr0, Cb1, Cr1). +inline unsigned short getNV10bitLineSample(const unsigned char *restrict line, unsigned sampleIdx) +{ + line += (sampleIdx / 4) * 5; + switch (sampleIdx % 4) + { + case 0: + return line[0] + ((line[1] & 0x03) << 8); + case 1: + return ((line[1] >> 2) & 0x3f) + ((line[2] & 0x0f) << 6); + case 2: + return (line[2] >> 4) + ((line[3] & 0x3f) << 4); + default: + return ((line[3] >> 6) & 0x03) + (line[4] << 2); + } +} + + +inline unsigned getNV10bitBytesPerLine(unsigned nrSamples) +{ + // 4 samples per 5 bytes, rounded up per line + return ((nrSamples + 3) / 4) * 5; +} + +// Convert the semi planar packed NV15 (420), NV20 (422) or NV30 (444) format to a planar format +// with 2 bytes per sample so that the normal planar conversion functions can be used. +// The NV formats consist of a Y plane followed by an interleaved Cb/Cr plane. 4 10 bit samples +// are packed into 5 bytes (per line, see getNV10bitLineSample). +std::pair convertNV10bitSemiPlanarPackedToPlanar(const QByteArray &sourceBuffer, + QByteArray &targetBuffer, + const Size curFrameSize, + const PredefinedPixelFormat nvFormat) +{ + Subsampling subsampling; + if (nvFormat == PredefinedPixelFormat::NV15) + subsampling = Subsampling::YUV_420; + else if (nvFormat == PredefinedPixelFormat::NV20) + subsampling = Subsampling::YUV_422; + else + subsampling = Subsampling::YUV_444; + + const auto w = curFrameSize.width; + const auto h = curFrameSize.height; + + // The output format is planar 10 bit with the same subsampling + auto newFormat = PixelFormatYUV(subsampling, 10, PlaneOrder::YUV); + const auto bytesPerOutFrame = newFormat.bytesPerFrame(curFrameSize); + if (targetBuffer.size() < bytesPerOutFrame) + targetBuffer.resize(bytesPerOutFrame); + + // The interleaved chroma plane contains one U and one V sample per chroma position. + const auto chromaWidth = (subsampling == Subsampling::YUV_444) ? w : w / 2; + const auto chromaHeight = (subsampling == Subsampling::YUV_420) ? h / 2 : h; + const auto strideY = getNV10bitBytesPerLine(w); + const auto strideC = getNV10bitBytesPerLine(chromaWidth * 2); + + const unsigned char *restrict srcY = (unsigned char *)sourceBuffer.data(); + const unsigned char *restrict srcC = srcY + strideY * h; + unsigned short *restrict dstY = (unsigned short *)targetBuffer.data(); + unsigned short *restrict dstU = dstY + w * h; + unsigned short *restrict dstV = dstU + chromaWidth * chromaHeight; + + for (unsigned y = 0; y < h; y++) + { + const unsigned char *restrict lineY = srcY + y * strideY; + unsigned short *restrict outY = dstY + y * w; + for (unsigned x = 0; x < w; x++) + outY[x] = getNV10bitLineSample(lineY, x); + } + + for (unsigned y = 0; y < chromaHeight; y++) + { + const unsigned char *restrict lineC = srcC + y * strideC; + unsigned short *restrict outU = dstU + y * chromaWidth; + unsigned short *restrict outV = dstV + y * chromaWidth; + for (unsigned x = 0; x < chromaWidth; x++) + { + outU[x] = getNV10bitLineSample(lineC, x * 2); + outV[x] = getNV10bitLineSample(lineC, x * 2 + 1); + } + } + + return {true, newFormat}; +} + +yuv_t getPixelValueNV10bit(const QByteArray &sourceBuffer, + const Size &curFrameSize, + const QPoint &pixelPos, + const PredefinedPixelFormat nvFormat) +{ + const auto w = curFrameSize.width; + const auto h = curFrameSize.height; + + const auto chromaWidth = (nvFormat == PredefinedPixelFormat::NV30) ? w : w / 2; + const auto subHor = (nvFormat == PredefinedPixelFormat::NV30) ? 1u : 2u; + const auto subVer = (nvFormat == PredefinedPixelFormat::NV15) ? 2u : 1u; + const auto strideY = getNV10bitBytesPerLine(w); + const auto strideC = getNV10bitBytesPerLine(chromaWidth * 2); + + const unsigned char *restrict srcY = (unsigned char *)sourceBuffer.data(); + const unsigned char *restrict srcC = srcY + strideY * h; + + yuv_t ret; + ret.Y = getNV10bitLineSample(srcY + pixelPos.y() * strideY, pixelPos.x()); + + const auto chromaLine = srcC + (pixelPos.y() / subVer) * strideC; + const unsigned chromaIndex = (unsigned(pixelPos.x()) / subHor) * 2; + ret.U = getNV10bitLineSample(chromaLine, chromaIndex); + ret.V = getNV10bitLineSample(chromaLine, chromaIndex + 1); + + return ret; +} + yuv_t getPixelValueV210(const QByteArray &sourceBuffer, const Size &curFrameSize, const QPoint &pixelPos) @@ -2346,6 +2463,11 @@ void convertYUVToImage(const QByteArray &sourceBuffer, if (*predefinedFormat == PredefinedPixelFormat::V210) std::tie(convOK, newPixelFormat) = convertV210PackedToPlanar(sourceBuffer, tmpPlanarYUVSource, curFrameSize); + else if (*predefinedFormat == PredefinedPixelFormat::NV15 || + *predefinedFormat == PredefinedPixelFormat::NV20 || + *predefinedFormat == PredefinedPixelFormat::NV30) + std::tie(convOK, newPixelFormat) = convertNV10bitSemiPlanarPackedToPlanar( + sourceBuffer, tmpPlanarYUVSource, curFrameSize, *predefinedFormat); else convOK = false; } @@ -2380,7 +2502,10 @@ std::vector videoHandlerYUV::formatPresetList = { PixelFormatYUV(Subsampling::YUV_420, 10, PlaneOrder::YUV), PixelFormatYUV(Subsampling::YUV_422, 8, PlaneOrder::YUV), PixelFormatYUV(Subsampling::YUV_444, 8, PlaneOrder::YUV), - PixelFormatYUV(PredefinedPixelFormat::V210)}; + PixelFormatYUV(PredefinedPixelFormat::V210), + PixelFormatYUV(PredefinedPixelFormat::NV15), + PixelFormatYUV(PredefinedPixelFormat::NV20), + PixelFormatYUV(PredefinedPixelFormat::NV30)}; videoHandlerYUV::videoHandlerYUV() : videoHandler() { @@ -3288,6 +3413,10 @@ yuv_t videoHandlerYUV::getPixelValue(const QPoint &pixelPos) const { if (predefinedFormat == PredefinedPixelFormat::V210) value = getPixelValueV210(currentFrameRawData, frameSize, pixelPos); + else if (*predefinedFormat == PredefinedPixelFormat::NV15 || + *predefinedFormat == PredefinedPixelFormat::NV20 || + *predefinedFormat == PredefinedPixelFormat::NV30) + value = getPixelValueNV10bit(currentFrameRawData, frameSize, pixelPos, *predefinedFormat); } else if (format.isPlanar()) {