// Copyright 2020 The Chromium Authors. All rights reserved. // 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 #include #include #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" namespace { // Source of the shader to perform tonemapping. Note that the functions // ToLinearSRGB, 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. const char* tonemapping_shader_source = "#include \n" "#include \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 ToLinearSRGB(float v) {\n" " float abs_v = abs(v);\n" " float sgn_v = sign(v);\n" " if (abs_v < 0.0404482362771082f)\n" " return v/12.92f;\n" " else\n" " return sgn_v*pow((abs_v+0.055f)/1.055f, 2.4f);\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 t [[texture(0)]],\n" " constant float3x3& m [[buffer(0)]],\n" " constant uint32_t& f [[buffer(1)]]) {\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 = ToLinearSRGB(color.x);\n" " color.y = ToLinearSRGB(color.y);\n" " color.z = ToLinearSRGB(color.z);\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) { switch (color_space.GetTransferID()) { case gfx::ColorSpace::TransferID::SRGB_HDR: return 1; 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. MTLPixelFormat IOSurfaceGetMTLPixelFormat(IOSurfaceRef buffer) { uint32_t format = IOSurfaceGetPixelFormat(buffer); switch (format) { case kCVPixelFormatType_64RGBAHalf: return MTLPixelFormatRGBA16Float; case kCVPixelFormatType_ARGB2101010LEPacked: return MTLPixelFormatBGR10A2Unorm; case kCVPixelFormatType_32BGRA: return MTLPixelFormatBGRA8Unorm; case kCVPixelFormatType_32RGBA: return MTLPixelFormatRGBA8Unorm; default: break; } return MTLPixelFormatInvalid; } base::scoped_nsprotocol> CreateRenderPipelineState( id device) { base::scoped_nsprotocol> render_pipeline_state; base::scoped_nsprotocol> library; { NSError* error = nil; base::scoped_nsobject source([[NSString alloc] initWithCString:tonemapping_shader_source encoding:NSASCIIStringEncoding]); base::scoped_nsobject options( [[MTLCompileOptions alloc] init]); library.reset([device newLibraryWithSource:source options:options error:&error]); if (error) { NSLog(@"Failed to compile shader: %@", error); return render_pipeline_state; } } { base::scoped_nsprotocol> vertex_function( [library newFunctionWithName:@"vertexShader"]); base::scoped_nsprotocol> fragment_function( [library newFunctionWithName:@"fragmentShader"]); NSError* error = nil; base::scoped_nsobject 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 #if !defined(MAC_OS_X_VERSION_10_15) API_AVAILABLE(macos(10.15)) @interface CAMetalLayer (Forward) @property(readonly) id preferredDevice; @end #endif API_AVAILABLE(macos(10.15)) @interface HDRCopierLayer : CAMetalLayer { base::scoped_nsprotocol> _render_pipeline_state; gfx::ColorSpace _color_space; } - (id)init; - (void)setHDRContents:(IOSurfaceRef)buffer withColorSpace:(gfx::ColorSpace)color_space; @end @implementation HDRCopierLayer - (id)init { if (self = [super init]) { base::scoped_nsprotocol> device(metal::CreateDefaultDevice()); [self setWantsExtendedDynamicRangeContent:YES]; [self setDevice:device]; [self setOpaque:NO]; [self setPresentsWithTransaction:YES]; [self setPixelFormat:MTLPixelFormatRGBA16Float]; [self setColorspace:CGColorSpaceCreateWithName( kCGColorSpaceExtendedLinearSRGB)]; } return self; } - (void)setHDRContents:(IOSurfaceRef)buffer withColorSpace:(gfx::ColorSpace)color_space { // Retrieve information about the IOSurface. size_t width = IOSurfaceGetWidth(buffer); size_t height = IOSurfaceGetHeight(buffer); MTLPixelFormat mtl_format = IOSurfaceGetMTLPixelFormat(buffer); if (mtl_format == MTLPixelFormatInvalid) { DLOG(ERROR) << "Unsupported IOSurface format."; return; } // Set metadata for tone mapping. if (_color_space != color_space) { CAEDRMetadata* edr_metadata = nil; switch (color_space.GetTransferID()) { case gfx::ColorSpace::TransferID::PQ: edr_metadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0 maxLuminance:10000 opticalOutputScale:100]; break; case gfx::ColorSpace::TransferID::HLG: edr_metadata = [CAEDRMetadata HLGMetadata]; break; default: [self setEDRMetadata:nil]; break; } [self setEDRMetadata:edr_metadata]; _color_space = color_space; } // Migrate to the MTLDevice on which the CAMetalLayer is being composited, if // known. if ([self respondsToSelector:@selector(preferredDevice)]) { id preferred_device = [self preferredDevice]; if (preferred_device) [self setDevice:preferred_device]; } id device = [self device]; // When the device changes, rebuild the RenderPipelineState. if (device != [_render_pipeline_state device]) _render_pipeline_state = CreateRenderPipelineState(device); if (!_render_pipeline_state) 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> buffer_texture; { base::scoped_nsobject tex_desc( [MTLTextureDescriptor new]); [tex_desc setTextureType:MTLTextureType2D]; [tex_desc setUsage:MTLTextureUsageShaderRead]; [tex_desc setPixelFormat:mtl_format]; [tex_desc setWidth:width]; [tex_desc setHeight:height]; [tex_desc setDepth:1]; [tex_desc setMipmapLevelCount:1]; [tex_desc setArrayLength:1]; [tex_desc setSampleCount:1]; [tex_desc setStorageMode:MTLStorageModeManaged]; buffer_texture.reset([device newTextureWithDescriptor:tex_desc iosurface:buffer plane:0]); } // Create a texture to wrap the drawable. id drawable = [self nextDrawable]; id drawable_texture = [drawable texture]; // Copy from the IOSurface to the drawable. base::scoped_nsprotocol> command_queue( [device newCommandQueue]); id command_buffer = [command_queue commandBuffer]; id encoder = nil; { MTLRenderPassDescriptor* desc = [MTLRenderPassDescriptor renderPassDescriptor]; desc.colorAttachments[0].texture = drawable_texture; desc.colorAttachments[0].loadAction = MTLLoadActionClear; desc.colorAttachments[0].storeAction = MTLStoreActionStore; desc.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 0.0); encoder = [command_buffer 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:_render_pipeline_state]; [encoder setFragmentTexture:buffer_texture 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 transfer_function_index = GetTransferFunctionIndex(color_space); DCHECK(transfer_function_index); // 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; color_space.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 setFragmentBytes:&transfer_function_index length:sizeof(transfer_function_index) atIndex:1]; [encoder setVertexBytes:positions length:sizeof(positions) atIndex:0]; [encoder setFragmentBytes:&matrix length:sizeof(matrix) atIndex:0]; [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6]; } [encoder endEncoding]; [command_buffer commit]; [command_buffer 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, const gfx::ColorSpace& color_space) { if (@available(macos 10.15, *)) { if (auto* hdr_copier_layer = base::mac::ObjCCast(layer)) { [hdr_copier_layer setHDRContents:buffer withColorSpace:color_space]; return; } } NOTREACHED(); } bool ShouldUseHDRCopier(IOSurfaceRef buffer, const gfx::ColorSpace& color_space) { if (@available(macos 10.15, *)) { return GetTransferFunctionIndex(color_space) && IOSurfaceGetMTLPixelFormat(buffer) != MTLPixelFormatInvalid; } return false; } } // namespace metal