去年 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 的电视原型。
2015 年
HDR10/HLG 标准发布,Davince Resolve(调色软件)在其 12.2 版本支持了 HLG。
2017 年
苹果旗下首个基于 OLED 技术的 iPhone X 发布,其超视网膜高清屏幕静态对比度达到了 1000000:1,亮度达到了 625 nit,iPhone X 的发布为消费级市场 HDR 技术的发展带了个好头。
2019 年
苹果发布了支持系统级 HDR 的 macOS 15 系统,以及新款 Mac Pro + Pro Display XDR 显示器,在 HDR 制作侧,将门槛进一步降低。此后视频从业者无需购买昂贵的 HDR 监视器或 OLED 电视,即可在桌面上进行 HDR 调色了。
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 屏幕。
2022 年
大量 MiniLED 产品发布,将 MiniLED 显示器的价位直接拉到了 2000 RMB 基准线,同时也出现了一批 OLED 显示器。
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 表示,显然这么多级肯定不够表示,会高概率造成色阶断裂。
那么这个时候应该怎么做才会提升画质?有两种做法:
-
**使用更高比特的编码:**比如直接使用 14 Bit 进行编码,此时每一个通道可支持:0 ~ 16383,那么 0 ~ 10000 nit 分别用 0 ~ 10000 的色阶来表示就好了,剩下的 10001 ~ 16383 浪费掉就好了。
-
**使用非线性曲线编码:**由于人眼对于暗部细节更敏感,因此在暗部提高采样频率,在亮部降低采样频率,这样只使用 10 Bit 进行编码就够了。
显然,带宽和空间的开销是巨大的,很明显厂商也不傻,大家经过权衡,选择了最经济的方案二:非线性曲线编码,在省钱的同时实现了 HDR 视频的基础所需。
基本 HDR
如何进行非线性曲线编码,目前基本上可以分为两种:PQ 和 HLG 曲线。
首先是 PQ 曲线,最先由杜比研发,并由 SMPTE 在 2014 年将其标准化。
PQ (SMPTE ST 2084)
EOTF**(电光转换函数)如下**
PQ 是绝对亮度曲线,最高可表示 10000 nit (cd/m^2) 的亮度,显示器的绝对亮度和信号范围一一对应。因此,将 PQ HDR渲染在普通SDR屏幕时,如果不做任何处理,且屏幕无法支持的视频里的最高亮度范围,则会出现高光溢出(简单粗暴的叫法:过曝),因此如果想让一个 HDR PQ 视频正常在普通显示器显示,保证其兼容性,则需要进行亮度映射,将视频的高亮度信号,压低到一个屏幕可以支撑的低亮度范围,即所谓的:HDR Tone Mapping。
HLG (ARIB STD-B67)
如果想正常在 SDR 显示器显示 PQ 内容,必须要 Tone Mapping,对老设备不友好,英国 BBC 广播公司与日本放送协会 NHK 共同开发 HLG 标准,并于 2015 年将其标准化,相比 PQ,即使不执行 Tone Mapping,其也可以显出“凑合能看”的画面。
为什么说是凑合能看?首先看一下 HLG 和 SDR BT.709 的 Gamma 曲线,**在低亮度范围,HLG 和BT.709 的亮度是重合的,即:如果一个 HLG 视频没有画面没有极亮区域,暗部区域曲线和普通 SDR 视频的亮度完全一致。如果存在极亮区域,该区域也不会过曝,整体表现出高兼容性。**但是需要注意的是,由于 HLG 默认使用远大于传统 BT.709 的 BT.2020 色彩空间,如果不进行 Color Transform,在普通显示器上仍然会出现饱和度低的问题,但相比 PQ 的完全不能看(亮度过亮,暗部过暗)还是好太多了。
可以看到,与 PQ 曲线相比,在 100nit 以下的部分 HLG 曲线基本与 SDR 曲线(BT.709)重合。
总结:PQ是绝对亮度曲线,完全不向下兼容,软件必须做 Tone Mapping,将超过屏幕显示范围的亮度压缩到屏幕显示范围内,HLG是相对亮度曲线,相对的向下兼容,在不做 Color Transform 的情况存在偏色问题,但不存在亮度映射问题。
静态元数据 HDR
由上述内容可知,PQ 曲线代表绝对亮度曲线,实际解码时,所有的 YCbCr 值都会进行归一化,变成一个 0 ~ 1 的数字,如果需要对他进行 Tone Mapping,在不包含静态元数据的情况,由于无法知道PQ的最高亮度是多少,只能将其假定为 10000nit,这会导致视频高光部分会压的比较暗。
2015 年 CTA 提出了 HDR10 标准,即带有静态元数据的 PQ。
MDCV (SMPTE ST 2086)
MDCV 表示渲染内容的显示器能力要求,比如:颜色、白点、最小最大亮度。我个人觉得这个参数目前没啥用,至少Chrome 里其实不怎么用 MDCV。
CLLI (CTA 861.3)
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)
HDR10+ (SMPTE ST2094-40)
HDR Vivid (CUVA 005-2020)
HDR 从 0 到 1
简单介绍了一些 HDR 的基本概念后,下面开始介绍 Chrome 是如何实现 HDR 渲染的,考虑到目前 Linux 尚未提供系统级的 HDR 支持,因此这篇本文重点关注 Windows 和 macOS 下的实现。另外需要注意的是,目前 Chrome 的所有 HDR 渲染均不包含动态元数据。
Windows HDR 实现
Windows 平台虽然做了 HDR 支持,但是个人愚见,应该是目前所有支持 HDR 的系统里做的最差的,用户如果想看到 HDR 效果,首先购买支持 HDR 的显示器,然后在系统设置 – 屏幕 – HDR 内手动开启开关,开启后屏幕亮度固定,用户只能通过调整 “SDR 参考亮度” 完成 UI White Level 的调整,对于普通用户非常不友好。
另外,在 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。
在代码里,可以调用下述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”获取。
Viz + SkiaRender Tone Mapping(实时逐帧映射)
在 Windows,Chrome 需要自己去做色彩转换,并完成 HDR Tone Mapping。
Tone Mapping 的整体操作步骤如下:
-
调整 YCbCr 三个通道的 Range,通常来说,YUV 420 10bit 视频需要将 Limited Range 转为 Full Range。
-
应用反变换 Matrix,YCbCr 转 RGB。
-
将 PQ 曲线转换为线性曲线。
-
RGB 转为 XYZ。
-
XYZ 转 Rec2020。
-
在 Rec2020 空间进行 Tone mapping (这一步,主要是利用我们之前提取到的显示器亮度 + UI 白色亮度,进行高光压缩)。
-
Rec2020 转 XYZ(5的逆向步骤)。
-
转回目标显示器的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 源码。
使用 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。
获取 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,如下:
设置屏幕输出类型
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 渲染还是一个不算太成熟的领域,不过,未来可期,敬请期待。