blob: 8891d99e4d39e32107f3c037ce6ccd01cc18a25f [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/metal_util/hdr_copier_layer.h"
#include <CoreGraphics/CoreGraphics.h>
#include <CoreVideo/CoreVideo.h>
#include <Metal/Metal.h>
#include <MetalKit/MetalKit.h>
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/notreached.h"
#include "base/strings/sys_string_conversions.h"
#include "components/metal_util/device.h"
#include "third_party/skia/modules/skcms/skcms.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/hdr_metadata.h"
#include "ui/gfx/hdr_metadata_mac.h"
namespace {
// Source of the shader to perform tonemapping. Note that the functions
// ToLinearSRGBIsh, ToLinearPQ, and ToLinearHLG are copy-pasted from the GLSL
// shader source in gfx::ColorTransform.
// TODO(https://crbug.com/1101041): Add non-identity tonemapping to the shader.
NSString* tonemapping_shader_source =
@"#include <metal_stdlib>\n"
"#include <simd/simd.h>\n"
"using metal::float2;\n"
"using metal::float3;\n"
"using metal::float3x3;\n"
"using metal::float4;\n"
"using metal::sampler;\n"
"using metal::texture2d;\n"
"using metal::abs;\n"
"using metal::exp;\n"
"using metal::max;\n"
"using metal::pow;\n"
"using metal::sign;\n"
"\n"
"typedef struct {\n"
" float4 clipSpacePosition [[position]];\n"
" float2 texcoord;\n"
"} RasterizerData;\n"
"\n"
"float ToLinearSRGBIsh(float v, constant float* gabcdef) {\n"
" float g = gabcdef[0];\n"
" float a = gabcdef[1];\n"
" float b = gabcdef[2];\n"
" float c = gabcdef[3];\n"
" float d = gabcdef[4];\n"
" float e = gabcdef[5];\n"
" float f = gabcdef[6];\n"
" float abs_v = abs(v);\n"
" float sgn_v = sign(v);\n"
" if (abs_v < d)\n"
" return sgn_v*(c*abs_v + f);\n"
" else\n"
" return sgn_v*(pow(a*abs_v+b, g) + e);\n"
"}\n"
"\n"
"float ToLinearPQ(float v) {\n"
" v = max(0.0f, v);\n"
" constexpr float m1 = (2610.0 / 4096.0) / 4.0;\n"
" constexpr float m2 = (2523.0 / 4096.0) * 128.0;\n"
" constexpr float c1 = 3424.0 / 4096.0;\n"
" constexpr float c2 = (2413.0 / 4096.0) * 32.0;\n"
" constexpr float c3 = (2392.0 / 4096.0) * 32.0;\n"
" float p = pow(v, 1.f / m2);\n"
" v = pow(max(p - c1, 0.f) / (c2 - c3 * p), 1.f / m1);\n"
" float sdr_white_level = 100.f;\n"
" v *= 10000.f / sdr_white_level;\n"
" return v;\n"
"}\n"
"\n"
"float ToLinearHLG(float v) {\n"
" constexpr float a = 0.17883277;\n"
" constexpr float b = 0.28466892;\n"
" constexpr float c = 0.55991073;\n"
" v = max(0.f, v);\n"
" if (v <= 0.5f)\n"
" return (v * 2.f) * (v * 2.f);\n"
" return exp((v - c) / a) + b;\n"
"}\n"
"\n"
"vertex RasterizerData vertexShader(\n"
" uint vertexID [[vertex_id]],\n"
" constant float2 *positions[[buffer(0)]]) {\n"
" RasterizerData out;\n"
" out.clipSpacePosition = vector_float4(0.f, 0.f, 0.f, 1.f);\n"
" out.clipSpacePosition.x = 2.f * positions[vertexID].x - 1.f;\n"
" out.clipSpacePosition.y = -2.f * positions[vertexID].y + 1.f;\n"
" out.texcoord = positions[vertexID];\n"
" return out;\n"
"}\n"
"\n"
"float3 ToneMap(float3 v) {\n"
" return v;\n"
"}\n"
"\n"
"fragment float4 fragmentShader(RasterizerData in [[stage_in]],\n"
" texture2d<float> t [[texture(0)]],\n"
" constant float3x3& m [[buffer(0)]],\n"
" constant uint32_t& f [[buffer(1)]],\n"
" constant float* gabcdef [[buffer(2)]]) {\n"
" constexpr sampler s(metal::mag_filter::nearest,\n"
" metal::min_filter::nearest);\n"
" float4 color = t.sample(s, in.texcoord);\n"
" switch (f) {\n"
" case 1:\n"
" color.x = ToLinearSRGBIsh(color.x, gabcdef);\n"
" color.y = ToLinearSRGBIsh(color.y, gabcdef);\n"
" color.z = ToLinearSRGBIsh(color.z, gabcdef);\n"
" break;\n"
" case 2:\n"
" color.x = ToLinearPQ(color.x);\n"
" color.y = ToLinearPQ(color.y);\n"
" color.z = ToLinearPQ(color.z);\n"
" break;\n"
" case 3:\n"
" color.x = ToLinearHLG(color.x);\n"
" color.y = ToLinearHLG(color.y);\n"
" color.z = ToLinearHLG(color.z);\n"
" break;\n"
" default:\n"
" break;\n"
" }\n"
" color.xyz = ToneMap(m * color.xyz);\n"
" return color;\n"
"}\n";
// Return the integer to use to specify a transfer function to the shader
// defined in the above source. Return 0 if the transfer function is
// unsupported.
uint32_t GetTransferFunctionIndex(const gfx::ColorSpace& color_space) {
skcms_TransferFunction fn;
if (color_space.GetTransferFunction(&fn))
return 1;
switch (color_space.GetTransferID()) {
case gfx::ColorSpace::TransferID::PQ:
return 2;
case gfx::ColorSpace::TransferID::HLG:
return 3;
default:
return 0;
}
}
// Convert from an IOSurface's pixel format to a MTLPixelFormat. Crash on any
// unsupported formats. Return true in `is_unorm` if the format, when sampled,
// can produce values outside of [0, 1].
MTLPixelFormat IOSurfaceGetMTLPixelFormat(IOSurfaceRef buffer,
bool* is_unorm = nullptr) {
uint32_t format = IOSurfaceGetPixelFormat(buffer);
if (is_unorm)
*is_unorm = true;
switch (format) {
case kCVPixelFormatType_64RGBAHalf:
if (is_unorm)
*is_unorm = false;
return MTLPixelFormatRGBA16Float;
case kCVPixelFormatType_ARGB2101010LEPacked:
return MTLPixelFormatBGR10A2Unorm;
case kCVPixelFormatType_32BGRA:
return MTLPixelFormatBGRA8Unorm;
case kCVPixelFormatType_32RGBA:
return MTLPixelFormatRGBA8Unorm;
default:
break;
}
return MTLPixelFormatInvalid;
}
base::scoped_nsprotocol<id<MTLRenderPipelineState>> CreateRenderPipelineState(
id<MTLDevice> device) {
base::scoped_nsprotocol<id<MTLRenderPipelineState>> render_pipeline_state;
base::scoped_nsprotocol<id<MTLLibrary>> library;
{
NSError* error = nil;
base::scoped_nsobject<MTLCompileOptions> options(
[[MTLCompileOptions alloc] init]);
library.reset([device newLibraryWithSource:tonemapping_shader_source
options:options
error:&error]);
if (error) {
NSLog(@"Failed to compile shader: %@", error);
return render_pipeline_state;
}
}
{
base::scoped_nsprotocol<id<MTLFunction>> vertex_function(
[library newFunctionWithName:@"vertexShader"]);
base::scoped_nsprotocol<id<MTLFunction>> fragment_function(
[library newFunctionWithName:@"fragmentShader"]);
NSError* error = nil;
base::scoped_nsobject<MTLRenderPipelineDescriptor> desc(
[[MTLRenderPipelineDescriptor alloc] init]);
[desc setVertexFunction:vertex_function];
[desc setFragmentFunction:fragment_function];
[[desc colorAttachments][0] setPixelFormat:MTLPixelFormatRGBA16Float];
render_pipeline_state.reset(
[device newRenderPipelineStateWithDescriptor:desc error:&error]);
if (error) {
NSLog(@"Failed to create render pipeline state: %@", error);
return render_pipeline_state;
}
}
return render_pipeline_state;
}
} // namespace
API_AVAILABLE(macos(10.15))
@interface HDRCopierLayer : CAMetalLayer {
base::scoped_nsprotocol<id<MTLRenderPipelineState>> _renderPipelineState;
gfx::ColorSpace _colorSpace;
absl::optional<gfx::HDRMetadata> _hdrMetadata;
}
- (id)init;
- (void)setHDRContents:(IOSurfaceRef)buffer
withDevice:(id<MTLDevice>)device
withColorSpace:(gfx::ColorSpace)colorSpace
withMetadata:(absl::optional<gfx::HDRMetadata>)hdrMetadata;
@end
@implementation HDRCopierLayer
- (id)init {
if (self = [super init]) {
base::scoped_nsprotocol<id<MTLDevice>> device(metal::CreateDefaultDevice());
if (@available(macOS 10.11, iOS 16.0, *)) {
[self setWantsExtendedDynamicRangeContent:YES];
}
[self setDevice:device];
[self setOpaque:NO];
[self setPresentsWithTransaction:YES];
[self setPixelFormat:MTLPixelFormatRGBA16Float];
base::ScopedCFTypeRef<CGColorSpaceRef> colorSpace(
CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearSRGB));
[self setColorspace:colorSpace];
}
return self;
}
- (void)setHDRContents:(IOSurfaceRef)buffer
withDevice:(id<MTLDevice>)device
withColorSpace:(gfx::ColorSpace)colorSpace
withMetadata:(absl::optional<gfx::HDRMetadata>)hdrMetadata {
// Retrieve information about the IOSurface.
size_t width = IOSurfaceGetWidth(buffer);
size_t height = IOSurfaceGetHeight(buffer);
MTLPixelFormat mtlFormat = IOSurfaceGetMTLPixelFormat(buffer);
if (mtlFormat == MTLPixelFormatInvalid) {
DLOG(ERROR) << "Unsupported IOSurface format.";
return;
}
if (@available(macOS 10.15, iOS 16.0, *)) {
// Set metadata for tone mapping.
if (_colorSpace != colorSpace || _hdrMetadata != hdrMetadata) {
CAEDRMetadata* edrMetadata = nil;
switch (colorSpace.GetTransferID()) {
case gfx::ColorSpace::TransferID::PQ: {
base::ScopedCFTypeRef<CFDataRef> displayInfo;
base::ScopedCFTypeRef<CFDataRef> contentInfo;
displayInfo = gfx::GenerateMasteringDisplayColorVolume(hdrMetadata);
contentInfo = gfx::GenerateContentLightLevelInfo(hdrMetadata);
edrMetadata = [CAEDRMetadata
HDR10MetadataWithDisplayInfo:base::mac::CFToNSCast(displayInfo)
contentInfo:base::mac::CFToNSCast(contentInfo)
opticalOutputScale:100];
break;
}
case gfx::ColorSpace::TransferID::HLG:
edrMetadata = [CAEDRMetadata HLGMetadata];
break;
default:
[self setEDRMetadata:nil];
break;
}
[self setEDRMetadata:edrMetadata];
_colorSpace = colorSpace;
_hdrMetadata = hdrMetadata;
}
}
// Migrate to the MTLDevice on which the CAMetalLayer is being composited, if
// known.
if (device) {
[self setDevice:device];
} else {
if (@available(macOS 10.15, *)) {
id<MTLDevice> preferredDevice = [self preferredDevice];
if (preferredDevice) {
[self setDevice:preferredDevice];
}
}
device = [self device];
}
// When the device changes, rebuild the RenderPipelineState.
if (device != [_renderPipelineState device]) {
_renderPipelineState = CreateRenderPipelineState(device);
}
if (!_renderPipelineState) {
return;
}
// Update the layer's properties to match the IOSurface.
[self setDrawableSize:CGSizeMake(width, height)];
// Create a texture to wrap the IOSurface.
base::scoped_nsprotocol<id<MTLTexture>> bufferTexture;
{
base::scoped_nsobject<MTLTextureDescriptor> texDesc(
[MTLTextureDescriptor new]);
[texDesc setTextureType:MTLTextureType2D];
[texDesc setUsage:MTLTextureUsageShaderRead];
[texDesc setPixelFormat:mtlFormat];
[texDesc setWidth:width];
[texDesc setHeight:height];
[texDesc setDepth:1];
[texDesc setMipmapLevelCount:1];
[texDesc setArrayLength:1];
[texDesc setSampleCount:1];
#if BUILDFLAG(IS_MAC)
[texDesc setStorageMode:MTLStorageModeManaged];
#endif
bufferTexture.reset([device newTextureWithDescriptor:texDesc
iosurface:buffer
plane:0]);
}
// Create a texture to wrap the drawable.
id<CAMetalDrawable> drawable = [self nextDrawable];
id<MTLTexture> drawableTexture = [drawable texture];
// Copy from the IOSurface to the drawable.
base::scoped_nsprotocol<id<MTLCommandQueue>> commandQueue(
[device newCommandQueue]);
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
id<MTLRenderCommandEncoder> encoder = nil;
{
MTLRenderPassDescriptor* desc =
[MTLRenderPassDescriptor renderPassDescriptor];
desc.colorAttachments[0].texture = drawableTexture;
desc.colorAttachments[0].loadAction = MTLLoadActionClear;
desc.colorAttachments[0].storeAction = MTLStoreActionStore;
desc.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 0.0);
encoder = [commandBuffer renderCommandEncoderWithDescriptor:desc];
MTLViewport viewport;
viewport.originX = 0;
viewport.originY = 0;
viewport.width = width;
viewport.height = height;
viewport.znear = -1.0;
viewport.zfar = 1.0;
[encoder setViewport:viewport];
[encoder setRenderPipelineState:_renderPipelineState];
[encoder setFragmentTexture:bufferTexture atIndex:0];
}
{
simd::float2 positions[6] = {
simd::make_float2(0, 0), simd::make_float2(0, 1),
simd::make_float2(1, 1), simd::make_float2(1, 1),
simd::make_float2(1, 0), simd::make_float2(0, 0),
};
// The value of |transfer_function| corresponds to the value as used in
// the above shader source.
uint32_t transferFunctionIndex = GetTransferFunctionIndex(colorSpace);
DCHECK(transferFunctionIndex);
skcms_TransferFunction fn;
colorSpace.GetTransferFunction(&fn);
// Matrix is the primary transform matrix from |color_space| to sRGB.
simd::float3x3 matrix;
{
skcms_Matrix3x3 src_to_xyz;
skcms_Matrix3x3 srgb_to_xyz;
skcms_Matrix3x3 xyz_to_srgb;
colorSpace.GetPrimaryMatrix(&src_to_xyz);
gfx::ColorSpace::CreateSRGB().GetPrimaryMatrix(&srgb_to_xyz);
skcms_Matrix3x3_invert(&srgb_to_xyz, &xyz_to_srgb);
skcms_Matrix3x3 m = skcms_Matrix3x3_concat(&xyz_to_srgb, &src_to_xyz);
matrix = simd::float3x3(
simd::make_float3(m.vals[0][0], m.vals[1][0], m.vals[2][0]),
simd::make_float3(m.vals[0][1], m.vals[1][1], m.vals[2][1]),
simd::make_float3(m.vals[0][2], m.vals[1][2], m.vals[2][2]));
}
[encoder setVertexBytes:positions length:sizeof(positions) atIndex:0];
[encoder setFragmentBytes:&matrix length:sizeof(matrix) atIndex:0];
[encoder setFragmentBytes:&transferFunctionIndex
length:sizeof(transferFunctionIndex)
atIndex:1];
[encoder setFragmentBytes:&fn length:sizeof(fn) atIndex:2];
[encoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:6];
}
[encoder endEncoding];
[commandBuffer commit];
[commandBuffer waitUntilScheduled];
[drawable present];
}
@end
namespace metal {
CALayer* CreateHDRCopierLayer() {
// If this is hit by non-10.15 paths (e.g, for testing), then return an
// ordinary CALayer. Calling setContents on that CALayer will work fine
// (HDR content will be clipped, but that would have happened anyway).
if (@available(macos 10.15, *))
return [[HDRCopierLayer alloc] init];
NOTREACHED();
return nil;
}
void UpdateHDRCopierLayer(
CALayer* layer,
IOSurfaceRef buffer,
id<MTLDevice> device,
const gfx::ColorSpace& color_space,
const absl::optional<gfx::HDRMetadata>& hdr_metadata) {
if (@available(macos 10.15, *)) {
if (auto* hdr_copier_layer = base::mac::ObjCCast<HDRCopierLayer>(layer)) {
[hdr_copier_layer setHDRContents:buffer
withDevice:device
withColorSpace:color_space
withMetadata:hdr_metadata];
return;
}
}
NOTREACHED();
}
bool ShouldUseHDRCopier(IOSurfaceRef buffer,
gfx::HDRMode hdr_mode,
const gfx::ColorSpace& color_space) {
if (@available(macos 10.15, *)) {
// Only some transfer functions are supported.
if (!GetTransferFunctionIndex(color_space))
return false;
// Only some pixel formats are supported.
bool is_unorm = false;
if (IOSurfaceGetMTLPixelFormat(buffer, &is_unorm) == MTLPixelFormatInvalid)
return false;
if (color_space.IsToneMappedByDefault())
return true;
if (hdr_mode == gfx::HDRMode::kDefault) {
if (color_space.GetTransferID() ==
gfx::ColorSpace::TransferID::SRGB_HDR) {
// Rasterized tiles and the primary plane specify a color space of
// SRGB_HDR with gfx::HDRMode::kDefault.
return !is_unorm;
}
return false;
}
return true;
}
return false;
}
} // namespace metal