diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs index 6e7e762..3dca24c 100644 --- a/Util/FFmpeg/FFmpegNativeEncoder.cs +++ b/Util/FFmpeg/FFmpegNativeEncoder.cs @@ -201,6 +201,12 @@ namespace ADOFAIRenderer.Util.FFmpeg /// private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P; + /// + /// Color range of the encoder input and output (codec context color_range of the selected encoder). + /// Full range (JPEG) for every encoder except libvvenc, limited range (MPEG) for libvvenc. + /// + private AVColorRange _colorRange = AVColorRange.AVCOL_RANGE_JPEG; + /// Pixel format of the finally selected and running encoder. public AVPixelFormat SelectedPixelFormat => _dstPixFmt; @@ -399,7 +405,19 @@ namespace ADOFAIRenderer.Util.FFmpeg // ── Basic video parameters configuration ── _pCodecContext->width = _settings.Width; _pCodecContext->height = _settings.Height; - _pCodecContext->color_range = AVColorRange.AVCOL_RANGE_JPEG; + + // ── Output color tags ── + // Matrix: BT.709, the RGB→YUV matrix of both conversion paths + // (GPU: RGB2YUV_BT709_Full in ColorConversion.compute, CPU: ConfigureSwsColorspace). + // Players use this tag to convert the YUV frames back to RGB. + // Range: full (0-255) for every encoder except libvvenc, limited (16-235) for libvvenc. + // The FFmpeg libvvenc wrapper passes the matrix/primaries/transfer tags to VVenC but not the range, + // so a VVC stream is read as limited range; its input is converted to limited range to match. + // Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libavcodec/libvvenc.c (vvenc_set_color_format) + _pCodecContext->color_range = encoder == VideoEncoder.LibVvenc + ? AVColorRange.AVCOL_RANGE_MPEG + : AVColorRange.AVCOL_RANGE_JPEG; + _pCodecContext->colorspace = AVColorSpace.AVCOL_SPC_BT709; // thread_count = 0: FFmpeg automatically allocates threads according to the system CPU core count. // For libvvenc, the FFmpeg wrapper passes thread_count as the VVenC internal thread count, // so setting 0 (auto) is required for parallel encoding utilizing all cores. @@ -756,6 +774,7 @@ namespace ADOFAIRenderer.Util.FFmpeg var accelStatus = isHardware ? "Hardware Acceleration Enabled" : "Software CPU Usage"; Melon.Logger.Msg($"Video encoder successfully initialized: {encoder.Name} ({accelStatus})"); _dstPixFmt = selectedPixFmt; + _colorRange = _pCodecContext->color_range; openedSuccessfully = true; break; } @@ -855,6 +874,8 @@ namespace ADOFAIRenderer.Util.FFmpeg if (_pSwsContext == null) throw new Exception("Failed to initialize software scaler."); + ConfigureSwsColorspace(); + _isInitialized = true; _loggedConversionMode = false; } @@ -1240,7 +1261,51 @@ namespace ADOFAIRenderer.Util.FFmpeg } /// - /// Sets the format, size and color range of the destination frame (_pDstFrame) and allocates a new pixel buffer for it. + /// Sets the RGB→YUV conversion of the CPU conversion path (_pSwsContext) to the BT.709 matrix + /// and the color range of the encoder (_colorRange), so that it produces the same colors as the GPU path + /// (RGB2YUV_BT709_Full in ColorConversion.compute) and as the tags of the output stream. + /// A new libswscale context uses the BT.601 matrix (SWS_CS_DEFAULT = SWS_CS_ITU601) and the limited range. + /// + /// For RGB input, "table" holds the coefficients of the output YUV space and dstRange its range (1 = full, 0 = limited). + /// The source (RGB) values, brightness, contrast and saturation keep the values of the context. + /// + /// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html (sws_setColorspaceDetails, sws_getCoefficients) + /// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libswscale/swscale.h (SWS_CS_ITU709, SWS_CS_DEFAULT) + /// + private void ConfigureSwsColorspace() + { + int* currentInvTable; + int* currentTable; + int srcRange, dstRange, brightness, contrast, saturation; + + var ret = ffmpeg.sws_getColorspaceDetails(_pSwsContext, ¤tInvTable, &srcRange, ¤tTable, &dstRange, + &brightness, &contrast, &saturation); + if (ret < 0) + { + Melon.Logger.Warning($"[Encoder] Cannot read the colorspace details of the software scaler: {GetFFmpegErrorString(ret)} (Code: {ret}). The CPU conversion path keeps the libswscale defaults."); + return; + } + + var coefficients = ffmpeg.sws_getCoefficients(ffmpeg.SWS_CS_ITU709); + var invTable = new int_array4(); + var table = new int_array4(); + for (uint i = 0; i < 4; i++) + { + invTable[i] = currentInvTable[i]; + table[i] = coefficients[i]; + } + + var fullRange = _colorRange == AVColorRange.AVCOL_RANGE_JPEG; + ret = ffmpeg.sws_setColorspaceDetails(_pSwsContext, invTable, srcRange, table, fullRange ? 1 : 0, + brightness, contrast, saturation); + if (ret < 0) + { + Melon.Logger.Warning($"[Encoder] Cannot set the colorspace details of the software scaler: {GetFFmpegErrorString(ret)} (Code: {ret}). The CPU conversion path keeps the libswscale defaults."); + } + } + + /// + /// Sets the format, size, color range and matrix of the destination frame (_pDstFrame) and allocates a new pixel buffer for it. /// av_frame_get_buffer allocates memory matching format, width and height and sets the data[]/linesize[] pointers. /// Second argument 0 means default alignment. /// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html (av_frame_get_buffer) @@ -1250,7 +1315,8 @@ namespace ADOFAIRenderer.Util.FFmpeg _pDstFrame->format = (int)_dstPixFmt; _pDstFrame->width = _settings.Width; _pDstFrame->height = _settings.Height; - _pDstFrame->color_range = AVColorRange.AVCOL_RANGE_JPEG; + _pDstFrame->color_range = _colorRange; + _pDstFrame->colorspace = AVColorSpace.AVCOL_SPC_BT709; var ret = ffmpeg.av_frame_get_buffer(_pDstFrame, 0); if (ret < 0)