浅谈 Chrome HDR 视频渲染流程

去年 Chrome 107 发布后,B 站解除了 Windows 和 macOS 等平台 Chrome 播放 HEVC HDR 的限制,随后腾讯视频也跟进支持了臻彩视界(HEVC + HDR10),抖音 PC 尽管还未支持,不过听小伙伴说目前手机端也在灰度中了。目前在国内使用浏览器看 HDR 视频基本上已经是一个很容易的事情了。那么 HDR 是如何在 Chrome 进行渲染的呢?这篇分享从个人的视角,简述我的理解。

HDR 的历史简述

HDR = 亮的更亮,暗的更暗,这个概念本身很早之前就有了,但是在早期因为技术上的限制,其概念一直仅局限于发烧爱好者圈子里一个比较小的范畴内,研究方向主要是如何将高动态范围的原始视频,压缩到普通显示器能支持显示亮度范围内,即:HDR Tone Mapping(特指 HDR -> SDR 转换)。

2014 年

Dolby 在 CES 上发布 杜比视界 技术,并展示了支持 HDR 的电视原型。

image

2015 年

HDR10/HLG 标准发布,Davince Resolve(调色软件)在其 12.2 版本支持了 HLG。

image

2017 年

苹果旗下首个基于 OLED 技术的 iPhone X 发布,其超视网膜高清屏幕静态对比度达到了 1000000:1,亮度达到了 625 nit,iPhone X 的发布为消费级市场 HDR 技术的发展带了个好头。

image

2019 年

苹果发布了支持系统级 HDR 的 macOS 15 系统,以及新款 Mac Pro + Pro Display XDR 显示器,在 HDR 制作侧,将门槛进一步降低。此后视频从业者无需购买昂贵的 HDR 监视器或 OLED 电视,即可在桌面上进行 HDR 调色了。

image

2020 年

macOS 11.0 Big Sur 发布,这个版本发布后,Mac 上正式支持了 VP9 以及 HLG 和 HDR10,也是使用 Mac Chrome 流畅播放 4K VP9 Profile2 HDR10 视频的最低版本要求。

2021 年

苹果发布了基于 M1 Pro 的 14寸, 16寸 Macbook Pro,在笔记本电脑上首次配置了 Liquid 视网膜 XDR 显示屏,基于 MiniLED 技术,最高可达到 1600 nit 峰值亮度,以及 1000000: 1 的对比度,截止2023年写稿的今天,该屏幕是目前市面上性能最好的小尺寸 HDR 屏幕。

image

2022 年

大量 MiniLED 产品发布,将 MiniLED 显示器的价位直接拉到了 2000 RMB 基准线,同时也出现了一批 OLED 显示器。

image

10月底,Chrome 107 开始支持 HEVC。

2023 年

Chrome 112 发布,之前所有 Pending 的 HDR Bug 均修复完成,HDR 相关渲染和显示效果趋于稳定。

简而言之,从 2014 年到 2023年,HDR 技术在过去不到 10 年的时间内,得到较大的发展,已经可以确认成为事实上的音视频行业的下个阶段的发展方向,这个领域在软件和硬件上,还有大量的坑没有填。

HDR 的基本类型

HDR(High Dynamic Range),翻译成中文叫做:高动态范围,顾名思义,和之前的SDR(Standard Dynamic Range)的主要区别在于,HDR 支持更广的亮度范围。

一个 SDR 视频,通常是 Y, Cb, Cr(或者 R, G, B)每个通道占用 8 Bit 进行编码,即:0 ~ 255 的亮度级数,用来表示 0 ~ 80 nit,还记得翻盖手机的时代经常有厂商吹自己的屏幕支持 1600 万色,这个 1600 万就是这么来的:256 * 256 * 256 = 16777216。(注:这里的 80 nit 是参考亮度,一个相对概念,意味着,其实你自己手动改屏幕亮度,想调高多少都可以 !!! 调的越高,视频整体越亮,不存在某一部分特别亮,某一部分特别暗的情况,即所谓的低动态范围)。

一个 HDR 视频,因为需要支持到最高 10000 nit 的亮度,我们当然也可以继续用 0 ~ 255 的亮度级数来表示,但是显然,0 – 10000 一共是 10000 级,如果只用 0 ~ 255 表示,显然这么多级肯定不够表示,会高概率造成色阶断裂

那么这个时候应该怎么做才会提升画质?有两种做法:

  1. **使用更高比特的编码:**比如直接使用 14 Bit 进行编码,此时每一个通道可支持:0 ~ 16383,那么 0 ~ 10000 nit 分别用 0 ~ 10000 的色阶来表示就好了,剩下的 10001 ~ 16383 浪费掉就好了。

  2. **使用非线性曲线编码:**由于人眼对于暗部细节更敏感,因此在暗部提高采样频率,在亮部降低采样频率,这样只使用 10 Bit 进行编码就够了。

显然,带宽和空间的开销是巨大的,很明显厂商也不傻,大家经过权衡,选择了最经济的方案二:非线性曲线编码,在省钱的同时实现了 HDR 视频的基础所需。

基本 HDR

如何进行非线性曲线编码,目前基本上可以分为两种:PQ 和 HLG 曲线。

首先是 PQ 曲线,最先由杜比研发,并由 SMPTE 在 2014 年将其标准化。

PQ (SMPTE ST 2084)

EOTF**(电光转换函数)如下**

image

PQ 是绝对亮度曲线,最高可表示 10000 nit (cd/m^2) 的亮度,显示器的绝对亮度和信号范围一一对应。因此,将 PQ HDR渲染在普通SDR屏幕时,如果不做任何处理,且屏幕无法支持的视频里的最高亮度范围,则会出现高光溢出(简单粗暴的叫法:过曝),因此如果想让一个 HDR PQ 视频正常在普通显示器显示,保证其兼容性,则需要进行亮度映射,将视频的高亮度信号,压低到一个屏幕可以支撑的低亮度范围,即所谓的:HDR Tone Mapping

image

HLG (ARIB STD-B67)

如果想正常在 SDR 显示器显示 PQ 内容,必须要 Tone Mapping,对老设备不友好,英国 BBC 广播公司与日本放送协会 NHK 共同开发 HLG 标准,并于 2015 年将其标准化,相比 PQ,即使不执行 Tone Mapping,其也可以显出“凑合能看”的画面。

image

为什么说是凑合能看?首先看一下 HLG 和 SDR BT.709 的 Gamma 曲线,**在低亮度范围,HLG 和BT.709 的亮度是重合的,即:如果一个 HLG 视频没有画面没有极亮区域,暗部区域曲线和普通 SDR 视频的亮度完全一致。如果存在极亮区域,该区域也不会过曝,整体表现出高兼容性。**但是需要注意的是,由于 HLG 默认使用远大于传统 BT.709 的 BT.2020 色彩空间,如果不进行 Color Transform,在普通显示器上仍然会出现饱和度低的问题,但相比 PQ 的完全不能看(亮度过亮,暗部过暗)还是好太多了。

image

可以看到,与 PQ 曲线相比,在 100nit 以下的部分 HLG 曲线基本与 SDR 曲线(BT.709)重合。

image

总结:PQ是绝对亮度曲线,完全不向下兼容,软件必须做 Tone Mapping,将超过屏幕显示范围的亮度压缩到屏幕显示范围内,HLG是相对亮度曲线,相对的向下兼容,在不做 Color Transform 的情况存在偏色问题,但不存在亮度映射问题。

静态元数据 HDR

由上述内容可知,PQ 曲线代表绝对亮度曲线,实际解码时,所有的 YCbCr 值都会进行归一化,变成一个 0 ~ 1 的数字,如果需要对他进行 Tone Mapping,在不包含静态元数据的情况,由于无法知道PQ的最高亮度是多少,只能将其假定为 10000nit,这会导致视频高光部分会压的比较暗。

2015 年 CTA 提出了 HDR10 标准,即带有静态元数据的 PQ。

MDCV (SMPTE ST 2086)

image

MDCV 表示渲染内容的显示器能力要求,比如:颜色、白点、最小最大亮度。我个人觉得这个参数目前没啥用,至少Chrome 里其实不怎么用 MDCV。

CLLI (CTA 861.3)

image

CLLI 只有两个参数,其中 max_content_light_level表示整个视频帧最大像素亮度(单位:nit),Chrome会基于该参数进行 Tone Mapping,max_pic_average_light_level 表示整个视频帧的最大平均亮度(单位:nit)。

动态元数据 HDR

由于静态元数据执行 Tone Mapping 的细粒度比较粗糙,整个视频均基于相同的最大像素亮度进行映射,因此后续的厂商推出了一些动态元数据标准,其中杜比视界是付费标准,不开源,另外两种动态元数据HDR开源,免费,三者均包含 MDCV + CLLI 静态元数据,以保留其兼容性(杜比视界 Profile 5 除外)。

需要说明的是,目前互联网很多人对 HDR 动态元数据存在误区,个人的理解是:HDR 动态元数据的主要场景是确保老 SDR 或者较差 HDR 屏幕能基于摄影师的意图微调色彩。如果屏幕本身能达到 1600nit 以上,比视频本身(假设他是1000nit的调色最高亮度)所表达的最高亮度要更高,此时 HDR 动态元数据实际上没什么作用,即:HDR 动态元数据的主要场景是为了兼容较差设备。

杜比视界 (SMPTE ST2094-10)

image

HDR10+ (SMPTE ST2094-40)

image

HDR Vivid (CUVA 005-2020)

image

HDR 从 0 到 1

简单介绍了一些 HDR 的基本概念后,下面开始介绍 Chrome 是如何实现 HDR 渲染的,考虑到目前 Linux 尚未提供系统级的 HDR 支持,因此这篇本文重点关注 Windows 和 macOS 下的实现。另外需要注意的是,目前 Chrome 的所有 HDR 渲染均不包含动态元数据。

Windows HDR 实现

Windows 平台虽然做了 HDR 支持,但是个人愚见,应该是目前所有支持 HDR 的系统里做的最差的,用户如果想看到 HDR 效果,首先购买支持 HDR 的显示器,然后在系统设置 – 屏幕 – HDR 内手动开启开关,开启后屏幕亮度固定,用户只能通过调整 “SDR 参考亮度” 完成 UI White Level 的调整,对于普通用户非常不友好。

image

另外,在 Windows 平台实现 HDR Render,应用必须要自己负责 Tone Mapping ,见:Use DirectX with Advanced Color on high/standard dynamic range displays

Demux

从 Container 提取 HDR Metadata(静态元数据)

如果是 HDR10 视频,首先需要尝试从容器提取 HDR 静态元数据。

// media/ffmpeg/ffmpeg_common.cc#L552














#if BUILDFLAG(ENABLE_PLATFORM_HEVC)
case VideoCodec::kHEVC: {
  int hevc_profile = -1;
  if (codec_context->extradata && codec_context->extradata_size) {
    mp4::HEVCDecoderConfigurationRecord hevc_config;
    if (hevc_config.Parse(codec_context->extradata,
                          codec_context->extradata_size)) {
      hevc_profile = hevc_config.general_profile_idc;
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
      if (!color_space.IsSpecified()) {
        color_space = hevc_config.GetColorSpace();
      }
      // 首先尝试从容器的 extradata 获取 SEI,SEI 可能会包含 HDR 静态元数据
      hdr_metadata = hevc_config.GetHDRMetadata();
      alpha_mode = hevc_config.GetAlphaMode();
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
    }

  }

 ...

Decode

从 Bit Stream 提取 HDR Metadata(静态元数据)

以 HEVC 格式为例,由于比特流内的 SEI NALU 同样可以写入 HDR Static Metadata,因此在 Decoder 内还需要额外解析一遍 SEI,尝试提取 HDR Static Metadata,以防有些编码器不在容器里写入 Metadata。

对于 PQ10 视频,容器和比特流均不存在 HDR Static Metadata。

// media/gpu/h265_decoder.cc#L377














case H265NALU::PREFIX_SEI_NUT: {
  H265SEI sei;
  if (parser_.ParseSEI(&sei) != H265Parser::kOk)
    break;
  for (auto& sei_msg : sei.msgs) {
    switch (sei_msg.type) {
      case H265SEIMessage::kSEIContentLightLevelInfo:
        // HEVC HDR metadata may appears in the below places:
        // 1. Container.
        // 2. Bitstream.
        // 3. Both container and bitstream.
        // Thus we should also extract HDR metadata here in case we
        // miss the information.
        if (!hdr_metadata_.has_value()) {
          hdr_metadata_.emplace();
        }
        hdr_metadata_->cta_861_3 =
            sei_msg.content_light_level_info.ToGfx();
        break;
      case H265SEIMessage::kSEIMasteringDisplayInfo:
        if (!hdr_metadata_.has_value()) {
          hdr_metadata_.emplace();
        }
        hdr_metadata_->smpte_st_2086 =
            sei_msg.mastering_display_info.ToGfx();
        break;
      default:
        break;
    }

  }
  break;
}

解码,设定 DXGI_FORMAT 输出格式为 DXGI_FORMAT_P010

109 版本之前,Chrome 在 Windows SDR 模式下会使用 D3D11VideoProcessor 输出 B8G8R8A8 格式的视频,在早期由于 Skia 的问题,无法使用零拷贝,因此被迫输出了该格式,而该格式本质上存在一层转换,同时存在额外的拷贝,因此 Chrome 在 109 版本之前 HDR 性能(显示器处于 SDR 模式时)很差。

109 及之后版本,Chrome 切换到了输出 P010 格式,由于大部分 10bit 420 HDR 视频均为原生 P010,因此切换后 因为变成了零拷贝,Chrome 存在一个明显的(50%)GPU 显存下降。

// media/gpu/windows/d3d11_texture_selector.cc#L112














// 视频原始格式为下述6种(HEVC 在 Intel 11 Gen+ GPU 支持高达 12 Bit 444 的采样)
// 10 Bit 420
case DXGI_FORMAT_P010:
// 12 Bit 444
case DXGI_FORMAT_Y416:
// 12 Bit 422
case DXGI_FORMAT_Y216:
// 12 Bit 420
case DXGI_FORMAT_P016:
// 10 Bit 444
case DXGI_FORMAT_Y410:
// 10 Bit 422
case DXGI_FORMAT_Y210: {
  MEDIA_LOG(INFO, media_log) << "D3D11VideoDecoder producing "
                              << DxgiFormatToString(decoder_output_format);
  // 如果 Windows 系统处于非 HDR 默认,优先让显卡输出 P010 格式 (P010 时为 Zero Copy)
  if (hdr_output_mode == HDRMode::kSDROnly &&
      supports_fmt(DXGI_FORMAT_P010)) {
    output_dxgi_format = DXGI_FORMAT_P010;
    output_pixel_format = PIXEL_FORMAT_P016LE;
    output_color_space.reset();
    MEDIA_LOG(INFO, media_log) << "D3D11VideoDecoder: Selected P016LE";
    // 如果 Windows 系统处于非 HDR 默认,显卡不支持 P010格式,选择 B8G8R8A8
  } else if (hdr_output_mode == HDRMode::kSDROnly &&
              supports_fmt(DXGI_FORMAT_B8G8R8A8_UNORM)) {
    output_dxgi_format = DXGI_FORMAT_B8G8R8A8_UNORM;
    output_pixel_format = PIXEL_FORMAT_ARGB;
    output_color_space.reset();
    MEDIA_LOG(INFO, media_log) << "D3D11VideoDecoder: Selected ARGB";
    // 如果 Windows 系统处于 HDR 模式,同样优先让显卡输出 P010 格式 (仅 P010 时为 Zero Copy)
  } else if (!needs_texture_copy || supports_fmt(DXGI_FORMAT_P010)) {
    output_dxgi_format = DXGI_FORMAT_P010;
    output_pixel_format = PIXEL_FORMAT_P016LE;
    output_color_space.reset();
    MEDIA_LOG(INFO, media_log) << "D3D11VideoDecoder: Selected P016LE";
  }

Render

在 Windows 平台,Chrome 需要自己去做 Tone Mapping Render。

读取显示器的 EDID (获取显示器的最大亮度)

软件自己做 Tone Mapping,需要感知到外界硬件的信息,幸运的是我们通过调用 Windows API 可以拿到显示器 EDID信息,拿到显示器所能表示的最大亮度,因此解析显示器 EDID 为 HDR Renderer 的一个必要步骤。

// ui/display/util/edid_parser.cc#L677














case kHDRStaticMetadataCapabilityTag: {
    constexpr size_t kMaxNumHDRStaticMetadataEntries = 4;
    const std::bitset<kMaxNumHDRStaticMetadataEntries>
        supported_eotfs_bitfield(edid[data_offset + 2]);
    static_assert(
        kMaxNumHDRStaticMetadataEntries == std::size(kTransferIDMap),
        "kTransferIDMap should describe all possible transfer entries");
    for (size_t entry = 0; entry < kMaxNumHDRStaticMetadataEntries;
          ++entry) {
      if (supported_eotfs_bitfield[entry])
        supported_color_transfer_ids_.insert(kTransferIDMap[entry]);
    }
    hdr_static_metadata_ =
        absl::make_optional<gfx::HDRStaticMetadata>({});
    hdr_static_metadata_->supported_eotf_mask =
        base::checked_cast<uint8_t>(supported_eotfs_bitfield.to_ulong());

    // See CEA 861.3-2015, Sec.7.5.13, "HDR Static Metadata Data Block"
    // for details on the following calculations.
    const uint8_t length_of_data_block =
        edid[data_offset] & kHDRStaticMetadataDataBlockLengthMask;
    if (length_of_data_block <= 3)
      break;
    const uint8_t desired_content_max_luminance = edid[data_offset + 4];
    hdr_static_metadata_->max =
        50.0 * pow(2, desired_content_max_luminance / 32.0);


    if (length_of_data_block <= 4)
      break;
    const uint8_t desired_content_max_frame_average_luminance =
        edid[data_offset + 5];
    hdr_static_metadata_->max_avg =
        50.0 * pow(2, desired_content_max_frame_average_luminance / 32.0);


    if (length_of_data_block <= 5)
      break;
    const uint8_t desired_content_min_luminance = edid[data_offset + 6];
    hdr_static_metadata_->min =
        hdr_static_metadata_->max *
        pow(desired_content_min_luminance / 255.0, 2) / 100.0;
    break;
  }
  default:
    break;
}

最终可以拿到的显示器 EDID 信息如下,我们其实能通过这个知道,在 Windows HDR 模式下,最终需要转换成何种色彩空间,以及显示器的最大亮度,这些非常有用的信息。

// ui/gfx/hdr_static_metadata.h














namespace gfx {

// This structure is used to define the HDR static capabilities of a display.
// Reflects CEA 861.G-2018, Sec.7.5.13, "HDR Static Metadata Data Block"
// A value of 0.0 in any of the fields means that it's not indicated.
struct COLOR_SPACE_EXPORT HDRStaticMetadata {
  // See Table 43 Data Byte 1 - Electro-Optical Transfer Function
  enum class Eotf {
    // "If “Traditional Gamma - SDR Luminance Range” is indicated, then the
    // maximum encoded luminance is typically mastered to 100 cd/m2"
    kGammaSdrRange = 0,
    // "If “Traditional Gamma – HDR Luminance Range” is indicated, then the
    // maximum encoded luminance is understood to be the maximum luminance of
    // the Sink device."
    kGammaHdrRange = 1,
    kPq = 2,
    kHlg = 3,
  };

  // "Desired Content Max Luminance Data. This is the content’s absolute peak
  // luminance (in cd/m2) (likely only in a small area of the screen) that the
  // display prefers for optimal content rendering."
  double max;
  // "Desired Content Max Frame-average Luminance. This is the content’s max
  // frame-average luminance (in cd/m2) that the display prefers for optimal
  // content rendering."
  double max_avg;
  // "Desired Content Min Luminance. This is the minimum value of the content
  // (in cd/m2) that the display prefers for optimal content rendering."
  double min;
  // "Electro-Optical Transfer Functions supported by the Sink." See Table 85
  // Supported Electro-Optical Transfer Function.
  uint8_t supported_eotf_mask;


  ...
 }

读取 OS 的 SDR 白点亮度(SDR Content Brightness)

Windows 系统在开启 HDR 后无法调整显示器亮度,如果想调整 SDR UI 的亮度,只能通过调整下述的 “SDR Content Brightness” 来调整,其可选的范围为:80 ~ 480nit。

image

在代码里,可以调用下述API获取:

// ui/display/win/screen_win.cc#L142














float GetSDRWhiteLevel(const absl::optional<DISPLAYCONFIG_PATH_INFO>& path) {
  if (path) {
    DISPLAYCONFIG_SDR_WHITE_LEVEL white_level = {};
    white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
    white_level.header.size = sizeof(white_level);
    white_level.header.adapterId = path->targetInfo.adapterId;
    white_level.header.id = path->targetInfo.id;
    if (DisplayConfigGetDeviceInfo(&white_level.header) == ERROR_SUCCESS)
      return white_level.SDRWhiteLevel * 80.0 / 1000.0;  // From wingdi.h.
  }
  return 200.0f;
}

如果你想看它的实时值,可以打开chrome://gpu ,观察“SDR White Level in nits”获取。

image

Viz + SkiaRender Tone Mapping(实时逐帧映射)

在 Windows,Chrome 需要自己去做色彩转换,并完成 HDR Tone Mapping。

Tone Mapping 的整体操作步骤如下:

  1. 调整 YCbCr 三个通道的 Range,通常来说,YUV 420 10bit 视频需要将 Limited Range 转为 Full Range。

  2. 应用反变换 Matrix,YCbCr 转 RGB。

  3. 将 PQ 曲线转换为线性曲线。

  4. RGB 转为 XYZ。

  5. XYZ 转 Rec2020。

  6. 在 Rec2020 空间进行 Tone mapping (这一步,主要是利用我们之前提取到的显示器亮度 + UI 白色亮度,进行高光压缩)。

  7. Rec2020 转 XYZ(5的逆向步骤)。

  8. 转回目标显示器的RGB,可以理解为步骤一到步骤七的逆变换,通常来说是 sRGB + Gamma 2.2 曲线(SDR模式)或者 Rec2020 + PQ曲线(外置显示器+开启HDR)

gfx::ColorTransform 的整体流程如下,里面包含了上述 1 – 8 步骤:

// ui/gfx/color_transform.cc#L1108














// 这里 Src 表示源色彩空间,即:视频自身的色彩空间,dst 表示目标色彩空间,即:显示器的色彩空间
void ColorTransformInternal::AppendColorSpaceToColorSpaceTransform(
    const ColorSpace& src,
    const ColorSpace& dst,
    const Options& options) {
  const bool src_matrix_is_identity_or_ycgco =
      src.GetMatrixID() == ColorSpace::MatrixID::GBR ||
      src.GetMatrixID() == ColorSpace::MatrixID::YCOCG;
  auto src_range_adjust_matrix = std::make_unique<ColorTransformMatrix>(
      src.GetRangeAdjustMatrix(options.src_bit_depth));

  if (!src_matrix_is_identity_or_ycgco)
    // 步骤一,执行Range变换
    steps_.push_back(std::move(src_range_adjust_matrix));


  if (src.GetMatrixID() == ColorSpace::MatrixID::BT2020_CL) {
    steps_.push_back(std::make_unique<ColorTransformFromBT2020CL>());
  } else {
    // 步骤二,反变换 ColorSpace Matrix,YCbCr 转 RGB
    steps_.push_back(std::make_unique<ColorTransformMatrix>(
        Invert(src.GetTransferMatrix(options.src_bit_depth))));
  }



  if (src_matrix_is_identity_or_ycgco)
    steps_.push_back(std::move(src_range_adjust_matrix));

  if (!dst.IsValid())
    return;

  switch (src.GetTransferID()) {
    // 步骤三: 如果是 HLG 视频,应用 HLG 反向 OETF
    case ColorSpace::TransferID::HLG:
      steps_.push_back(std::make_unique<ColorTransformHLG_InvOETF>());
      break;
    // 步骤三: 如果是 PQ 视频,将 PQ曲线 转 线性曲线。
    case ColorSpace::TransferID::PQ:
      steps_.push_back(std::make_unique<ColorTransformPQToLinear>());
      break;
    case ColorSpace::TransferID::SCRGB_LINEAR_80_NITS:
      steps_.push_back(std::make_unique<ColorTransformNitsToSdrRelative>(
          80.f, /*use_default_sdr_white=*/false));
      break;
    case ColorSpace::TransferID::PIECEWISE_HDR: {
      skcms_TransferFunction fn;
      float p, q, r;
      ColorTransformPiecewiseHDR::GetParams(src, &fn, &p, &q, &r);
      steps_.push_back(
          std::make_unique<ColorTransformPiecewiseHDR>(fn, p, q, r));
      break;
    }
    default: {
      skcms_TransferFunction src_to_linear_fn;
      if (src.GetTransferFunction(&src_to_linear_fn)) {
        steps_.push_back(std::make_unique<ColorTransformSkTransferFn>(
            src_to_linear_fn, src.HasExtendedSkTransferFn()));
      } else {
        steps_.push_back(
            std::make_unique<ColorTransformToLinear>(src.GetTransferID()));
      }
    }
  }

  if (src.GetMatrixID() == ColorSpace::MatrixID::BT2020_CL) {
    steps_.push_back(std::make_unique<ColorTransformMatrix>(
        Invert(src.GetTransferMatrix(options.src_bit_depth))));
  }

  // 步骤四:RGB空间转XYZ空间
  steps_.push_back(
      std::make_unique<ColorTransformMatrix>(src.GetPrimaryMatrix()));

  // Perform tone mapping in a linear space
  const ColorSpace rec2020_linear(
      ColorSpace::PrimaryID::BT2020, ColorSpace::TransferID::LINEAR,
      ColorSpace::MatrixID::RGB, ColorSpace::RangeID::FULL);
  switch (src.GetTransferID()) {
    case ColorSpace::TransferID::HLG: {
       // 这一步暂时可以忽略,关闭状态。
      if (IsHlgPqUnifiedTonemapEnabled()) {
        steps_.push_back(std::make_unique<ColorTransformMatrix>(
            Invert(rec2020_linear.GetPrimaryMatrix())));

        steps_.push_back(std::make_unique<ColorTransformHLG_RefOOTF>());

        steps_.push_back(std::make_unique<ColorTransformNitsToSdrRelative>(
            kHLGRefMaxLumNits, IsHlgPqSdrRelative()));

        if (options.tone_map_pq_and_hlg_to_dst) {
          steps_.push_back(
              std::make_unique<ColorTransformToneMapInRec2020Linear>(src));
        }

        steps_.push_back(std::make_unique<ColorTransformMatrix>(
            rec2020_linear.GetPrimaryMatrix()));
      } else {
        if (options.tone_map_pq_and_hlg_to_dst) {
          // 应用 HLG OOTF
          steps_.push_back(std::make_unique<ColorTransformHLG_OOTF>());
        } else {
          steps_.push_back(std::make_unique<ColorTransformNitsToSdrRelative>(
              12.f * ColorSpace::kDefaultSDRWhiteLevel,
              /*use_default_sdr_white=*/false));
        }
      }
      break;
    }
    case ColorSpace::TransferID::PQ: {
      // 将 0 ~ 10000 转为 0 ~ 10000 / 203 
      steps_.push_back(std::make_unique<ColorTransformNitsToSdrRelative>(
          kPQRefMaxLumNits, IsHlgPqSdrRelative()));

      if (options.tone_map_pq_and_hlg_to_dst) {
        // 步骤五:XYZ 转 Rec2020。
        steps_.push_back(std::make_unique<ColorTransformMatrix>(
            Invert(rec2020_linear.GetPrimaryMatrix())));

        // 步骤六:基于显示器最高亮度 + UI白点亮度,进行 Tone Mapping
        steps_.push_back(
            std::make_unique<ColorTransformToneMapInRec2020Linear>(src));

        // 步骤七:Rec2020 转为 XYZ
        steps_.push_back(std::make_unique<ColorTransformMatrix>(
            rec2020_linear.GetPrimaryMatrix()));
      }
      break;
    }
    default:
      break;
  }

  // 后续步骤基本就是,步骤一到步骤七的逆变换
  steps_.push_back(
      std::make_unique<ColorTransformMatrix>(Invert(dst.GetPrimaryMatrix())));
  if (dst.GetMatrixID() == ColorSpace::MatrixID::BT2020_CL) {
    // BT2020 CL is a special case.
    steps_.push_back(std::make_unique<ColorTransformMatrix>(
        dst.GetTransferMatrix(options.dst_bit_depth)));
  }

  switch (dst.GetTransferID()) {
    case ColorSpace::TransferID::HLG:
      steps_.push_back(std::make_unique<ColorTransformSdrToNitsRelative>(
          gfx::ColorSpace::kDefaultSDRWhiteLevel));
      steps_.push_back(std::make_unique<ColorTransformHLG_OETF>());
      break;
    case ColorSpace::TransferID::PQ:
      steps_.push_back(
          std::make_unique<ColorTransformSdrToNitsRelative>(kPQRefMaxLumNits));
      steps_.push_back(std::make_unique<ColorTransformPQFromLinear>());
      break;
    case ColorSpace::TransferID::SCRGB_LINEAR_80_NITS:
      steps_.push_back(std::make_unique<ColorTransformSdrToNitsRelative>(80.f));
      break;
    case ColorSpace::TransferID::PIECEWISE_HDR: {
      skcms_TransferFunction fn;
      float p, q, r;
      ColorTransformPiecewiseHDR::GetParams(dst, &fn, &p, &q, &r);
      ColorTransformPiecewiseHDR::InvertParams(&fn, &p, &q, &r);
      steps_.push_back(
          std::make_unique<ColorTransformPiecewiseHDR>(fn, p, q, r));
      break;
    }
    default: {
      skcms_TransferFunction dst_from_linear_fn;
      if (dst.GetInverseTransferFunction(&dst_from_linear_fn)) {
        steps_.push_back(std::make_unique<ColorTransformSkTransferFn>(
            dst_from_linear_fn, dst.HasExtendedSkTransferFn()));
      } else {
        steps_.push_back(
            std::make_unique<ColorTransformFromLinear>(dst.GetTransferID()));
      }
      break;
    }
  }

  const bool dst_matrix_is_identity_or_ycgco =
      dst.GetMatrixID() == ColorSpace::MatrixID::GBR ||
      dst.GetMatrixID() == ColorSpace::MatrixID::YCOCG;
  auto dst_range_adjust_matrix = std::make_unique<ColorTransformMatrix>(
      Invert(dst.GetRangeAdjustMatrix(options.dst_bit_depth)));

  if (dst_matrix_is_identity_or_ycgco)
    steps_.push_back(std::move(dst_range_adjust_matrix));

  if (dst.GetMatrixID() == ColorSpace::MatrixID::BT2020_CL) {
    NOTREACHED();
  } else {
    steps_.push_back(std::make_unique<ColorTransformMatrix>(
        dst.GetTransferMatrix(options.dst_bit_depth)));
  }

  if (!dst_matrix_is_identity_or_ycgco)
    steps_.push_back(std::move(dst_range_adjust_matrix));
}

其中最核心的流程是:ColorTransformToneMapInRec2020Linear,通过它把超过显示器亮度范围的高光部分压暗。

class ColorTransformToneMapInRec2020Linear : public ColorTransformStep {
 public:
  explicit ColorTransformToneMapInRec2020Linear(const gfx::ColorSpace& src)
      : use_ref_max_luminance_(src.GetTransferID() ==
                               ColorSpace::TransferID::HLG) {}



  // ColorTransformStep implementation:
  void Transform(ColorTransform::TriStim* color,
                 size_t num,
                 const ColorTransform::RuntimeOptions& options) const override {
    float a = 0.f;
    float b = 0.f;
    ComputeToneMapConstants(options, a, b);

    for (size_t i = 0; i < num; i++) {
      float L = kLr * color[i].x() + kLg * color[i].y() + kLb * color[i].z();
      if (L > 0.f)
        color[i].Scale((1.f + a * L) / (1.f + b * L));
    }

  }

  void AppendSkShaderSource(std::stringstream* src) const override {
    *src << "{\n"
         << "  half4 luma_vec = half4(" << kLr << ", " << kLg << ", " << kLb
         << ", 0.0);\n"
         << "  half L = dot(color, luma_vec);\n"
         << "  if (L > 0.0) {\n"
         << "    color.rgb *= (1.0 + pq_tonemap_a * L) / \n"
         << "                 (1.0 + pq_tonemap_b * L);\n"
         << "  }\n"
         << "}\n";
  }
  void SetShaderUniforms(const ColorTransform::RuntimeOptions& options,
                         SkShaderUniforms* uniforms) const override {
    ComputeToneMapConstants(options, uniforms->pq_tonemap_a,
                            uniforms->pq_tonemap_b);
  }


 private:
  float ComputeSrcMaxLumRelative(
      const ColorTransform::RuntimeOptions& options) const {
    float src_max_lum_nits = kHLGRefMaxLumNits;
    if (!use_ref_max_luminance_) {
      const auto hdr_metadata =
          gfx::HDRMetadata::PopulateUnspecifiedWithDefaults(
              options.src_hdr_metadata);
      // 优先取 CLLI的 Max Content Light Level 
      src_max_lum_nits = (hdr_metadata.cta_861_3 &&
                          hdr_metadata.cta_861_3->max_content_light_level > 0)
                             ? hdr_metadata.cta_861_3->max_content_light_level
                             : hdr_metadata.smpte_st_2086->luminance_max;
    }
    if (IsHlgPqSdrRelative()) {
      return src_max_lum_nits / ColorSpace::kDefaultSDRWhiteLevel;
    }
    return src_max_lum_nits / options.sdr_max_luminance_nits;
  }
  // 这里基于:显示器最高亮度,画面最高亮度(来自静态元数据),以及 SDR UI 亮度
  // 计算出 a 和 b 两个参数。
  void ComputeToneMapConstants(
      const gfx::ColorTransform::RuntimeOptions& options,
      float& a,
      float& b) const {
    const float src_max_lum_relative = ComputeSrcMaxLumRelative(options);
    if (src_max_lum_relative > options.dst_max_luminance_relative) {
      a = options.dst_max_luminance_relative /
          (src_max_lum_relative * src_max_lum_relative);
      b = 1.f / options.dst_max_luminance_relative;
    } else {
      a = 0;
      b = 0;
    }
  }

  const bool use_ref_max_luminance_;
};

这里整体流程图如下,可以看到流程是非常复杂的,需要处理两种输出的情况。仅仅是静态元数据,就已经很复杂了,因此暂时不在本文讨论 Chrome 是如何做高光压缩这块的算法了,感兴趣的小伙伴建议自己去发掘下 gfx_colortransform.cc 源码。

image

使用 Skia 应用 Runtime Effect(实时滤镜)

前一步,如果你很细心,应该能看出来,我们基本上是将 HDR Tone Mapping 的整个流程,拆成了不同的 Step,

那么这些流程最终会转换为 SKRuntimeEffect, 最终给每一帧应用实时滤镜,代码如下:

// ui/gfx/color_transform.cc#L1325














sk_sp<SkRuntimeEffect> ColorTransformInternal::GetSkRuntimeEffect() const {
  std::stringstream src;
  InitStringStream(&src);



  src << "uniform half offset;\n"
      << "uniform half multiplier;\n"
      << "uniform half sdr_max_luminance_nits;\n"
      << "uniform half pq_tonemap_a;\n"
      << "uniform half pq_tonemap_b;\n"
      << "uniform half hlg_ootf_gamma_minus_one;\n"
      << "uniform half hlg_dst_max_luminance_relative;\n"
      << "\n"
      << "half4 main(half4 color) {\n"
      << "  // Un-premultiply alpha\n"
      << "  if (color.a > 0)\n"
      << "    color.rgb /= color.a;\n"
      << "\n"
      << "  color.rgb -= offset;\n"
      << "  color.rgb *= multiplier;\n";

  for (const auto& step : steps_)
    step->AppendSkShaderSource(&src);



  src << "  // premultiply alpha\n"
         "  color.rgb *= color.a;\n"
         "  return color;\n"
         "}\n";

  auto sksl_source = src.str();
  auto result = SkRuntimeEffect::MakeForColorFilter(
      SkString(sksl_source.c_str(), sksl_source.size()),
      /*options=*/{});
  DCHECK(result.effect) << '\n'
                        << result.errorText.c_str() << "\n\nShader Source:\n"
                        << sksl_source;
  return result.effect;
}

监听系统 SDR <-> HDR 的切换(确保切换前后显示效果)

同时需要监听系统的显示器信息变化,当变化时随时调整目标色彩空间。

// components/viz/service/display/dc_layer_overlay.cc#L515














DCLayerOverlayProcessor::DCLayerOverlayProcessor(
    int allowed_yuv_overlay_count,
    bool skip_initialization_for_testing)
    : has_overlay_support_(skip_initialization_for_testing),
      allowed_yuv_overlay_count_(allowed_yuv_overlay_count),
      no_undamaged_overlay_promotion_(base::FeatureList::IsEnabled(
          features::kNoUndamagedOverlayPromotion)) {
  if (!skip_initialization_for_testing) {
    UpdateHasHwOverlaySupport();
    // 初始化一下
    UpdateSystemHDRStatus();
    gl::DirectCompositionOverlayCapsMonitor::GetInstance()->AddObserver(this);
  }
  allow_promotion_hinting_ = media::SupportMediaFoundationClearPlayback();
}


void DCLayerOverlayProcessor::UpdateSystemHDRStatus() {
  bool hdr_enabled = false;
  // 通过 `gl::GetDirectCompositionHDRMonitorDXGIInfo`,我们可以知道哪个显示器的HDR处于开启状态
  // 很不幸,目前只要有一个显示器开启了 HDR,Chrome 整体就会按照 HDR 开启处理,
  // 其实应该按窗口所在的显示器处理。
  auto dxgi_info = gl::GetDirectCompositionHDRMonitorDXGIInfo();
  for (const auto& output_desc : dxgi_info->output_descs)
    hdr_enabled |= output_desc->hdr_enabled;
  system_hdr_enabled_ = hdr_enabled;
}


// 每次显示器配置变换后触发
void DCLayerOverlayProcessor::OnOverlayCapsChanged() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  UpdateHasHwOverlaySupport();
  UpdateSystemHDRStatus();
}

Chrome 的 HDR 提升之路

Chrome 使用 D3D11 API 进行解码 (D3D11VideoDecoder + D3D11VideoDevice),使用 Skia Render 进行渲染,在早先,Chrome SDR 模式下的 HDR 支持是比较差的,从 105 版本开始,我有幸参与了这块的一些建设,逐渐对其做了一些改善。

Chrome 在 105 版本前,仅在HDR 模式下使用 viz + gfx::ColorTransform + SkiaRender 进行 Tone Mapping,而在 SDR 显示模式时使用 Video Processor (D3D11VideoProcessor)进行,Skia 不负责 Tone Mapping,因为 Video Processor 是显卡驱动实现的,各家最终的输出效果参差不齐,HDR效果奇差,不同的显卡品牌下视频输出均不相同。

Chrome 在 105 ~ 108 版本之间,在 SDR 显示模式和 HDR 显示默认均使用 viz + gfx::ColorTransform 进行 Tone Mapping,但是错误的使用了 B8G8R8A8 格式(RGB)进行的 Tone Mapping,导致 PQ 和 HLG 渲染存在画面偏灰的问题。

Chrome 在 109 版本,将 SDR 模式下的 B8G8R8A8 切换到了 P010 (YCbCr),解决了 PQ 和 HLG 渲染画面偏灰的问题。

Chrome 在 110 版本,支持了 Decode SEI 提取 HDR Metadata,解决了 SDR/HDR 高光亮度不够的问题。

从 110 版本到现在最新的 117 版本,目前效果已经比较完美了。

总结

综上,按我目前的理解,在 Windows 上一个,要实现 HDR,过程还是非常复杂的,如果还要考虑色彩科学的正确性,复杂度又提高了一个层次,同时,非常蛋疼的点是,整个流程还需要考虑用户可能在视频播放的过程中随时开关系统 HDR 功能这个事,此时如何能保证视频的正确渲染,细节很多,很难想象有多少软件能把这里的每个细节都做好。

因此,像 Chrome 这样的软件,其实在 110 版本之前也是在摸索,也是存在颜色不正确,HDR 崩溃,等等各种问题,好在,Chrome 在 110 版本之后,HDR 效果真的可以说很棒了,我个人理解的是,目前 Chrome 做的比较好的地方,其实是可以支持 OS 的 SDR <-> HDR 无缝切换,切换前后确保颜色均正确。原因就是 Chrome 的 HDR 是全流程的 HDR Render + 显示器 SDR/HDR 监听 + 全程 P010 零拷贝,至少目前 Windows 电影与电视,VLC,还有 PotPlayer 都没做到这点。

再聊一下,为什么 Windows Chrome 现在不支持杜比视界,不支持HDR10+,首先是杜比视界,这个 Feature 在未来是有很可能通过 MediaFoundation For Clear 去支持的,但是其路径和本文讲述的 D3D11 + Skia Render + Viz 的路子不同,因为杜比视界是闭源的,因此如果想在 Windows 按照官方路子实现杜比视界解码,只有一条路,就是安装“HEVC视频扩展插件”+“杜比视界插件”。然后说到 HDR10+,其实有 Intel 的 Committer 已经在做了,但是估计是遇到了什么困难,目前进度暂时Pending了。

macOS HDR 实现

下面来说下 macOS,macOS 是现在市面上 HDR 支持最好的操作系统,没有之一,搭配 Mac 无敌的 1600nit MiniLED 显示屏,在 HDR 这块比 Windows 高了两个 Level,是地球上支持 HDR 最好的 OS。

Demux

从 Container 提取 HDR Metadata(静态元数据)

和 Windows 是相同的,没什么说的。

Decode

从 BitStream 提取 HDR Metadata(静态元数据)

和 Windows 差不多,macOS 也需要在容器解析 SEI,获取 HDR 元数据。

// media/gpu/mac/vt_video_decode_accelerator_mac.cc#L1473














hevc_parser_.SetStream(buffer->data(), buffer->data_size());
H265NALU nalu;
while (true) {
  ...
  case H265NALU::PREFIX_SEI_NUT: {
      nalus.push_back(nalu);
      data_size += kNALUHeaderLength + nalu.size;
      H265SEI sei; 
      result = hevc_parser_.ParseSEI(&sei);
      if (result != H265Parser::kOk)
        break;
      for (auto& sei_msg : sei.msgs) {
        switch (sei_msg.type) {
          ...
          // 解析 Mastering Display Info (
          case H265SEIMessage::kSEIMasteringDisplayInfo:
            if (!config_.hdr_metadata.has_value()) {
              config_.hdr_metadata.emplace();
            }
            config_.hdr_metadata->smpte_st_2086 =
                sei_msg.mastering_display_info.ToGfx();
            break;
          // 解析 Content Light Level Info
          case H265SEIMessage::kSEIContentLightLevelInfo:
            if (!config_.hdr_metadata.has_value()) {
              config_.hdr_metadata.emplace();
            }
            config_.hdr_metadata->cta_861_3 =
                sei_msg.content_light_level_info.ToGfx();
            break;
          default:
            break;
        }
      }

  }
}

解码,设定 CVPixelBufferRef 输出格式为 P010

和 Windows 有点类似,我们也需要告诉一下 VideoToolbox 我们需要输出的 Buffer 是啥格式,就完事了,在 MacOS 平台,10bit 及以上视频,默认设置为:kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange。

// media/gpu/mac/vt_video_decode_accelerator_mac.cc#L148














// Build an |image_config| dictionary for VideoToolbox initialization.
base::ScopedCFTypeRef<CFMutableDictionaryRef> BuildImageConfig(
    CMVideoDimensions coded_dimensions,
    bool is_hbd,
    bool has_alpha) {
  base::ScopedCFTypeRef<CFMutableDictionaryRef> image_config;



  // HDR 格式输出 P010,非 HDR 格式输出 NV12
  int32_t pixel_format = is_hbd
                             ? kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange
                             : kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
  // HEVC with Alpha 输出 NV12A
  if (has_alpha)
    pixel_format = kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar;


#define CFINT(i) CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &i)
  base::ScopedCFTypeRef<CFNumberRef> cf_pixel_format(CFINT(pixel_format));
  base::ScopedCFTypeRef<CFNumberRef> cf_width(CFINT(coded_dimensions.width));
  base::ScopedCFTypeRef<CFNumberRef> cf_height(CFINT(coded_dimensions.height));
#undef CFINT
  if (!cf_pixel_format.get() || !cf_width.get() || !cf_height.get())
    return image_config;



  image_config.reset(CFDictionaryCreateMutable(
      kCFAllocatorDefault,
      3,  // capacity
      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
  if (!image_config.get())
    return image_config;

  // 设定该属性,告知 VideoToolbox 输出格式为 P010
  CFDictionarySetValue(image_config, kCVPixelBufferPixelFormatTypeKey,
                       cf_pixel_format);
  CFDictionarySetValue(image_config, kCVPixelBufferWidthKey, cf_width);
  CFDictionarySetValue(image_config, kCVPixelBufferHeightKey, cf_height);

  return image_config;
}

Render

在 macOS 平台,我们只需要按照苹果的要求,调用相关API,提交 CVPixelBuffer,即可实现HDR,不需要自己去做 Tone mapping。

image

获取 HDR Headroom

什么是 HDR Headroom,其实就是 HDR 亮度 / SDR UI 亮度 相除得到的结果。

2021 款 Macbook Pro 支持最高支持 1600 nit,然后假定 SDR UI 亮度是 100 nit,则 HDR Headroom 为 16,假定 MacBook 的屏幕亮度,被调整到了 500 nit,此时 UI 亮度是 400 nit,则 HDR Headroom 为 4,以此类推。

利用下述API,我们可以获取当前的 HDR Headroom:

  // ui/display/mac/screen_mac.mm#L212
  
  CGDirectDisplayID display_id =
      [screen.deviceDescription[@"NSScreenNumber"] unsignedIntValue];

  Display display(display_id, gfx::Rect(NSRectToCGRect(frame)));
 
  bool enable_hdr = false;
  float hdr_max_lum_relative = 1.f;
  
  // macOS 10.15 之后支持系统级 HDR
  if (@available(macOS 10.15, *)) {
    // 利用这个API,可以判断设备是否支持 EDR,大于1 则认为支持。
    const float max_potential_edr_value =
        screen.maximumPotentialExtendedDynamicRangeColorComponentValue;
    // 利用这个API,可以拿到当前的 HDR Headroom。
    const float max_edr_value =
        screen.maximumExtendedDynamicRangeColorComponentValue;
    if (max_potential_edr_value > 1.f) {
      enable_hdr = true;
      hdr_max_lum_relative =
          std::max(kMinHDRCapableMaxLuminanceRelative, max_edr_value);
    }
  }

打开 chrome://gpu , 搜索 HDR relative maximum luminace,如下:

image

设置屏幕输出类型

macOS系统,整个 HDR 渲染流程都由 OS 来负责,应用只负责输出解码后的 Buffer,并告知 OS 需要渲染到屏幕的渲染类型,比如:extendedSRGB + RGBA_F16,之后就无需考虑 Tone Mapping Renderer 这样的事情了,macOS 自己会去做。

  // ui/display/mac/screen_mac.mm#L239














CGDirectDisplayID display_id =
      [screen.deviceDescription[@"NSScreenNumber"] unsignedIntValue];

Display display(display_id, gfx::Rect(NSRectToCGRect(frame)));




// 获取显示器ICC
{
  CGColorSpaceRef cg_color_space = screen.colorSpace.CGColorSpace;
  if (cg_color_space) {
    base::ScopedCFTypeRef<CFDataRef> cf_icc_profile(
        CGColorSpaceCopyICCData(cg_color_space));
    if (cf_icc_profile) {
      icc_profile = gfx::ICCProfile::FromData(
         CFDataGetBytePtr(cf_icc_profile), CFDataGetLength(cf_icc_profile));
    }
  }
}
// macOS 10.15 以下的系统不支持 EDR,设置 RGBA8888 作为输出类型
gfx::DisplayColorSpaces display_color_spaces(icc_profile.GetColorSpace(),
                                               gfx::BufferFormat::RGBA_8888);


if (enable_hdr) {
    // macOS 10.15 以上系统支持 EDR,设置 extendedSRGB + RGBA_F16 作为输出类型
    bool needs_alpha_values[] = {true, false};
    for (const auto& needs_alpha : needs_alpha_values) {
      display_color_spaces.SetOutputColorSpaceAndBufferFormat(
          gfx::ContentColorUsage::kHDR, needs_alpha,
          gfx::ColorSpace::CreateExtendedSRGB(), gfx::BufferFormat::RGBA_F16);
    }

    display_color_spaces.SetHDRMaxLuminanceRelative(hdr_max_lum_relative);
}
display.set_color_spaces(display_color_spaces);
display_color_spaces.SetSDRMaxLuminanceNits(
      gfx::ColorSpace::kDefaultSDRWhiteLevel);


if (enable_hdr) {
  // 三个通道一共 30 bit
  display.set_color_depth(Display::kHDR10BitsPerPixel);
  // 每个通道 10 bit
  display.set_depth_per_component(Display::kHDR10BitsPerComponent);
} else {
  // 三个通道一共 24 bit
  display.set_color_depth(Display::kDefaultBitsPerPixel);
  // 每个通道 8 bit
  display.set_depth_per_component(Display::kDefaultBitsPerComponent);
}

设置 IOSurface ColorSpace

*IOSurface *提供了一个适合跨进程边界共享的帧缓冲区对象。 我们需要将解码后的 CVPixelBufferRef 入队 IOSurface,并设置 IOSurface 的 ColorSpace。

// ui/gfx/mac/io_surface.cc#L=147














// Common method used by IOSurfaceSetColorSpace and IOSurfaceCanSetColorSpace.
bool IOSurfaceSetColorSpace(IOSurfaceRef io_surface,
                            const ColorSpace& color_space) {
  // Allow but ignore invalid color spaces.
  if (!color_space.IsValid())
    return true;



  // Prefer using named spaces.
  CFStringRef color_space_name = nullptr;
  if (color_space == ColorSpace::CreateSRGB() ||
      (base::FeatureList::IsEnabled(kIOSurfaceUseNamedSRGBForREC709) &&
       color_space == ColorSpace::CreateREC709())) {
    color_space_name = kCGColorSpaceSRGB;
  } else if (color_space == ColorSpace::CreateDisplayP3D65()) {
    color_space_name = kCGColorSpaceDisplayP3;
  } else if (color_space == ColorSpace::CreateExtendedSRGB()) {
    color_space_name = kCGColorSpaceExtendedSRGB;
  } else if (color_space == ColorSpace::CreateSRGBLinear()) {
    color_space_name = kCGColorSpaceExtendedLinearSRGB;
  }


  if (__builtin_available(macos 10.15, *)) {
    if (color_space == ColorSpace(ColorSpace::PrimaryID::BT2020,
                                  ColorSpace::TransferID::PQ,
                                  ColorSpace::MatrixID::BT2020_NCL,
                                  ColorSpace::RangeID::LIMITED)) {
      // 尽管 macOS 10.15 就支持 EDR 了,但是因为存在 Bug,实际上 Chrome 在 macOS 11.0
      // 及以后才支持 PQ 和 HLG.
      // 详情: crbug.com/1108627.
      if (__builtin_available(macos 11.0, *)) {
        color_space_name = kCGColorSpaceITUR_2100_PQ;
      } else {
        return true;
      }

    } else if (color_space == ColorSpace(ColorSpace::PrimaryID::BT2020,
                                         ColorSpace::TransferID::HLG,
                                         ColorSpace::MatrixID::BT2020_NCL,
                                         ColorSpace::RangeID::LIMITED)) {
      // 设置 HLG Color Space
      if (__builtin_available(macos 11.0, *)) {
        color_space_name = kCGColorSpaceITUR_2100_HLG;
      } else {
        return true;
      }
    }
  }
  if (color_space_name) {
    if (io_surface) {
      IOSurfaceSetValue(io_surface, CFSTR("IOSurfaceColorSpace"),
                        color_space_name);
    }
    return true;
  }

  gfx::ColorSpace as_rgb = color_space.GetAsRGB();
  gfx::ColorSpace as_full_range_rgb = color_space.GetAsFullRangeRGB();

  if (color_space != as_rgb && as_rgb == as_full_range_rgb)
    return false;

  ICCProfile icc_profile = ICCProfile::FromColorSpace(as_full_range_rgb);
  if (!icc_profile.IsValid())
    return false;

  std::vector<char> icc_profile_data = icc_profile.GetData();
  base::ScopedCFTypeRef<CFDataRef> cf_data_icc_profile(CFDataCreate(
      nullptr, reinterpret_cast<const UInt8*>(icc_profile_data.data()),
      icc_profile_data.size()));

  // 定义 IOSurface 存放内容的色彩空间
  IOSurfaceSetValue(io_surface, CFSTR("IOSurfaceColorSpace"),
                    cf_data_icc_profile);
  return true;
}

设置 CVPixelBufferRef HDR 属性

如果视频是 HDR10 或者 HLG 视频,需要为创建好的 CVPixelBuffer 设置属性,表明其为 PQ, HLG,以及 设置其 HDR Metadata。

// ui/accelerated_widget_mac/ca_renderer_layer_tree.mm#L155














bool AVSampleBufferDisplayLayerEnqueueIOSurface(
    AVSampleBufferDisplayLayer* av_layer,
    IOSurfaceRef io_surface,
    const gfx::ColorSpace& io_surface_color_space,
    absl::optional<gfx::HDRMetadata> hdr_metadata) {
  CVReturn cv_return = kCVReturnSuccess;



  // 新建一个 CVPixelBuffer
  base::ScopedCFTypeRef<CVPixelBufferRef> cv_pixel_buffer;
  cv_return = CVPixelBufferCreateWithIOSurface(
      nullptr, io_surface, nullptr, cv_pixel_buffer.InitializeInto());
  if (cv_return != kCVReturnSuccess) {
    LOG(ERROR) << "CVPixelBufferCreateWithIOSurface failed with " << cv_return;
    return false;
  }


  // 至少需要 11.0 以上
  if (__builtin_available(macos 11.0, *)) {
    if (io_surface_color_space ==
            gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT2020,
                            gfx::ColorSpace::TransferID::PQ,
                            gfx::ColorSpace::MatrixID::BT2020_NCL,
                            gfx::ColorSpace::RangeID::LIMITED) ||
        io_surface_color_space ==
            gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT2020,
                            gfx::ColorSpace::TransferID::HLG,
                            gfx::ColorSpace::MatrixID::BT2020_NCL,
                            gfx::ColorSpace::RangeID::LIMITED)) {
      // 设置 ColorSpace Primary
      CVBufferSetAttachment(cv_pixel_buffer, kCVImageBufferColorPrimariesKey,
                            kCVImageBufferColorPrimaries_ITU_R_2020,
                            kCVAttachmentMode_ShouldPropagate);
      // 设置 ColorSpace Matrix
      CVBufferSetAttachment(cv_pixel_buffer, kCVImageBufferYCbCrMatrixKey,
                            kCVImageBufferYCbCrMatrix_ITU_R_2020,
                            kCVAttachmentMode_ShouldPropagate);
      switch (io_surface_color_space.GetTransferID()) {
        case gfx::ColorSpace::TransferID::HLG:
          // 设置 ColorSpace Transfer 为 HLG曲线
          CVBufferSetAttachment(cv_pixel_buffer,
                                kCVImageBufferTransferFunctionKey,
                                kCVImageBufferTransferFunction_ITU_R_2100_HLG,
                                kCVAttachmentMode_ShouldPropagate);
          break;
        case gfx::ColorSpace::TransferID::PQ:
          // 设置 ColorSpace Transfer 为 HLG PQ
          CVBufferSetAttachment(cv_pixel_buffer,
                                kCVImageBufferTransferFunctionKey,
                                kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ,
                                kCVAttachmentMode_ShouldPropagate);
          // 设置 MDCV          
          CVBufferSetAttachment(
              cv_pixel_buffer, kCVImageBufferMasteringDisplayColorVolumeKey,
              gfx::GenerateMasteringDisplayColorVolume(hdr_metadata),
              kCVAttachmentMode_ShouldPropagate);
          // 设置 MDCV       
          CVBufferSetAttachment(
              cv_pixel_buffer, kCVImageBufferContentLightLevelInfoKey,
              gfx::GenerateContentLightLevelInfo(hdr_metadata),
              kCVAttachmentMode_ShouldPropagate);
          break;
        default:
          break;
      }
    }
  }


  // 将 CVPixelBuffer 传入 IOSurface,渲染并显示到屏幕
  return AVSampleBufferDisplayLayerEnqueueCVPixelBuffer(av_layer,
                                                        cv_pixel_buffer);
}

总结

经过上述代码分析,相比大家可以看到,在 macOS 实现 HDR 渲染,不存在 HDR Tone Mapping 这一流程,完全是将解码后的 CVPixelBufferRef 交给 macOS,让 macOS 自己去渲染,这样做的好处不言而喻,软件省心啊!

首先是 HDR 显示效果可以得到最大保障,macOS 自闭环了 HDR 渲染流程,将 HDR Tone Mapping 这一复杂流程收敛到了 OS 层面,最大程度避免了 Windows 不同软件解码效果不同的尴尬。同时,因为软件不需要考虑 HDR/SDR 切换等乱七八糟的事情,macOS 在遇到 HDR 内容后会自动提高亮度,这个过程的速度极快,SDR 亮度保持不变这个事情,几乎做到了人眼无法感知的程度,在 Windows 下,是想都不敢想的事情,你还要自己手动开关 HDR 选项。

其次是,性能明显更好,少了这一步,macOS 能闭环越多的流程,就能做到越好的性能,这点毋庸置疑。

全文总结

总结下来,HDR 渲染还是一个不算太成熟的领域,不过,未来可期,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYq4rjoJ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片