// 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/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/strings/sys_string_conversions.h"
#include "build/build_config.h"
#include "components/metal_util/device.h"
#include "third_party/skia/include/core/SkM44.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 {

// If true, then use the HDRCopierLayer for all HLG video content.
BASE_FEATURE(kMacHlgUseHdrCopier, base::FEATURE_DISABLED_BY_DEFAULT);

// 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(crbug.com/40138176): 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::float4x4;\n"
     "using metal::float4;\n"
     "using metal::sampler;\n"
     "using metal::texture2d;\n"
     "using metal::abs;\n"
     "using metal::clamp;\n"
     "using metal::dot;\n"
     "using metal::exp;\n"
     "using metal::exp2;\n"
     "using metal::log;\n"
     "using metal::log2;\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"
     "  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 * v / 3.f;\n"
     "  return (exp((v - c) / a) + b) / 12.f;\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(\n"
     "        RasterizerData in [[stage_in]],\n"
     "        texture2d<float> plane0 [[texture(0)]],\n"
     "        texture2d<float> plane1 [[texture(1)]],\n"
     "        constant float4x4& yuvToRgb [[buffer(0)]],\n"
     "        constant float3x3& primaryMatrix [[buffer(1)]],\n"
     "        constant uint32_t& numPlanes [[buffer(2)]],\n"
     "        constant uint32_t& trfnId [[buffer(3)]],\n"
     "        constant float* gabcdef [[buffer(4)]],\n"
     "        constant float& screenHdrHeadroom [[buffer(5)]]) {\n"
     "    constexpr sampler s(metal::mag_filter::nearest,\n"
     "                        metal::min_filter::nearest);\n"
     "    float4 color = plane0.sample(s, in.texcoord);\n"
     "    if (numPlanes >= 2) {\n"
     "        color.yz = plane1.sample(s, in.texcoord).xy;\n"
     "        color.w = 1.0;\n"
     "    }\n"
     "    if (color.w != 0.0) {\n"
     "      color.xyz /= color.w;\n"
     "    }\n"
     "    color = yuvToRgb * color;\n"
     "    switch (trfnId) {\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 = primaryMatrix * color.xyz;\n"
     "    switch (trfnId) {\n"
     "      case 2:\n"
     "         // Scale to be relative to 203 nits.\n"
     "         color.xyz *= 10000.f / 203.f;\n"
     "         break;\n"
     "      case 3: {\n"
     "         // Compute the gain for the ITU-R BT.2100 Reference OOTF.\n"
     "         const float3 Y = float3(0.2627f, 0.6780f, 0.0593f);\n"
     "         float gain = max(pow(dot(Y, color.xyz), 0.2), 1.0);\n"
     "         // Add the gain to scale to be relative to 203 nits.\n"
     "         gain *= 1000.f / 203.f;\n"
     "         // Compute the gain weight;\n"
     "         const float contentHdrHeadroom = 1000.f/203.f;\n"
     "         const float weight = clamp(\n"
     "             log2(screenHdrHeadroom) / log2(contentHdrHeadroom),\n"
     "             0.f, 1.f);\n"
     "         // Apply the gain.\n"
     "         color.xyz *= exp2(weight * log2(gain));\n"
     "         break;\n"
     "      }\n"
     "      default:\n"
     "         break;\n"
     "    }\n"
     "    color.xyz *= color.w;\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. Return true in
// `is_unorm` if the format, when sampled, can produce values outside of [0, 1].
bool IOSurfaceGetMTLPixelFormat(IOSurfaceRef buffer,
                                uint32_t& num_planes,
                                MTLPixelFormat format[2],
                                bool& is_unorm) {
  num_planes = 1;
  format[0] = MTLPixelFormatInvalid;
  UNSAFE_TODO(format[1]) = MTLPixelFormatInvalid;
  is_unorm = true;
  switch (IOSurfaceGetPixelFormat(buffer)) {
    case kCVPixelFormatType_64RGBAHalf:
      is_unorm = false;
      format[0] = MTLPixelFormatRGBA16Float;
      return true;
    case kCVPixelFormatType_ARGB2101010LEPacked:
      format[0] = MTLPixelFormatBGR10A2Unorm;
      return true;
    case kCVPixelFormatType_32BGRA:
      format[0] = MTLPixelFormatBGRA8Unorm;
      return true;
    case kCVPixelFormatType_32RGBA:
      format[0] = MTLPixelFormatRGBA8Unorm;
      return true;
    case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
    case kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange:
    case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
    case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
      num_planes = 2;
      format[0] = MTLPixelFormatR8Unorm;
      UNSAFE_TODO(format[1]) = MTLPixelFormatRG8Unorm;
      return true;
    case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
    case kCVPixelFormatType_Lossless_420YpCbCr10PackedBiPlanarVideoRange:
    case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
    case kCVPixelFormatType_Lossless_422YpCbCr10PackedBiPlanarVideoRange:
    case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
      num_planes = 2;
      format[0] = MTLPixelFormatR16Unorm;
      UNSAFE_TODO(format[1]) = MTLPixelFormatRG16Unorm;
      return true;
    default:
      break;
  }
  return false;
}

id<MTLRenderPipelineState> CreateRenderPipelineState(id<MTLDevice> device) {
  NSError* error = nil;
  id<MTLLibrary> library =
      [device newLibraryWithSource:tonemapping_shader_source
                           options:[[MTLCompileOptions alloc] init]
                             error:&error];
  if (error) {
    NSLog(@"Failed to compile shader: %@", error);
    return nil;
  }

  MTLRenderPipelineDescriptor* desc =
      [[MTLRenderPipelineDescriptor alloc] init];
  desc.vertexFunction = [library newFunctionWithName:@"vertexShader"];
  desc.fragmentFunction = [library newFunctionWithName:@"fragmentShader"];
  desc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA16Float;
  id<MTLRenderPipelineState> render_pipeline_state =
      [device newRenderPipelineStateWithDescriptor:desc error:&error];
  if (error) {
    NSLog(@"Failed to create render pipeline state: %@", error);
    return nil;
  }

  return render_pipeline_state;
}

}  // namespace

@interface HDRCopierLayer : CAMetalLayer
- (id)init;
- (void)setHDRContents:(IOSurfaceRef)buffer
                device:(id<MTLDevice>)device
     screenHdrHeadroom:(float)screenHdrHeadroom
            colorSpace:(gfx::ColorSpace)colorSpace
              metadata:(gfx::HDRMetadata)hdrMetadata;
@end

@implementation HDRCopierLayer {
  id<MTLRenderPipelineState> __strong _renderPipelineState;
  IOSurfaceRef _buffer;
  id<MTLDevice> _device;
  float _screenHdrHeadroom;
  gfx::ColorSpace _colorSpace;
  gfx::HDRMetadata _hdrMetadata;
}
- (id)init {
  if ((self = [super init])) {
    id<MTLDevice> device = metal::GetDefaultDevice();
#if !BUILDFLAG(IS_IOS_TVOS)
    self.wantsExtendedDynamicRangeContent = YES;
#endif
    self.device = device;
    self.opaque = NO;
    self.presentsWithTransaction = YES;
    self.pixelFormat = MTLPixelFormatRGBA16Float;
    base::apple::ScopedCFTypeRef<CGColorSpaceRef> colorSpace(
        CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearITUR_2020));
    self.colorspace = colorSpace.get();
  }
  return self;
}

- (void)setHDRContents:(IOSurfaceRef)buffer
                device:(id<MTLDevice>)device
     screenHdrHeadroom:(float)screenHdrHeadroom
            colorSpace:(gfx::ColorSpace)colorSpace
              metadata:(gfx::HDRMetadata)hdrMetadata {
  // HLG does not use the OS-provided tone mapping, so it will need to be
  // re-drawn whenever the HDR headroom changes.
  // https://crbug.com/343249142
  bool useCustomToneMapping =
      colorSpace.GetTransferID() == gfx::ColorSpace::TransferID::HLG;

  // Early-out if nothing changed.
  if (buffer == _buffer && device == _device &&
      screenHdrHeadroom == _screenHdrHeadroom && colorSpace == _colorSpace &&
      hdrMetadata == _hdrMetadata) {
    // If custom tone mapping is not performed, then content does not need to
    // re-render when HDR headroom changes.
    if (!useCustomToneMapping) {
      _screenHdrHeadroom = screenHdrHeadroom;
      return;
    }
  }
  _screenHdrHeadroom = screenHdrHeadroom;
  _buffer = buffer;
  _device = device;

  // Retrieve information about the IOSurface.
  size_t width = IOSurfaceGetWidth(buffer);
  size_t height = IOSurfaceGetHeight(buffer);
  uint32_t numPlanes = 1;
  MTLPixelFormat mtlFormat[2] = {MTLPixelFormatInvalid, MTLPixelFormatInvalid};
  bool isUnorm = false;
  if (!IOSurfaceGetMTLPixelFormat(buffer, numPlanes, mtlFormat, isUnorm)) {
    DLOG(ERROR) << "Unsupported IOSurface format.";
    return;
  }

  // Set metadata for tone mapping.
#if !BUILDFLAG(IS_IOS_TVOS)
  if (_colorSpace != colorSpace || _hdrMetadata != hdrMetadata) {
    CAEDRMetadata* edrMetadata = nil;
    if (colorSpace.GetTransferID() == gfx::ColorSpace::TransferID::PQ) {
      base::apple::ScopedCFTypeRef<CFDataRef> display_info =
          gfx::GenerateMasteringDisplayColorVolume(hdrMetadata);
      base::apple::ScopedCFTypeRef<CFDataRef> content_info =
          gfx::GenerateContentLightLevelInfo(hdrMetadata);
      edrMetadata = [CAEDRMetadata
          HDR10MetadataWithDisplayInfo:base::apple::CFToNSPtrCast(
                                           display_info.get())
                           contentInfo:base::apple::CFToNSPtrCast(
                                           content_info.get())
                    opticalOutputScale:203];
    }
    self.EDRMetadata = edrMetadata;
    _colorSpace = colorSpace;
    _hdrMetadata = hdrMetadata;
  }
#endif

  // Migrate to the MTLDevice on which the CAMetalLayer is being composited, if
  // known.
  if (device) {
    self.device = device;
  } else {
    id<MTLDevice> preferredDevice = self.preferredDevice;
    if (preferredDevice) {
      self.device = 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.drawableSize = CGSizeMake(width, height);

  // Create a texture to wrap the IOSurface.
  id<MTLTexture> bufferTexture[2] = {nil, nil};
  for (uint32_t i = 0; i < numPlanes; ++i) {
    MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init];
    texDesc.textureType = MTLTextureType2D;
    texDesc.usage = MTLTextureUsageShaderRead;
    texDesc.pixelFormat = UNSAFE_TODO(mtlFormat[i]);
    texDesc.width = IOSurfaceGetWidthOfPlane(buffer, i);
    texDesc.height = IOSurfaceGetHeightOfPlane(buffer, i);
    texDesc.depth = 1;
    texDesc.mipmapLevelCount = 1;
    texDesc.arrayLength = 1;
    texDesc.sampleCount = 1;
#if BUILDFLAG(IS_MAC)
    texDesc.storageMode = MTLStorageModeManaged;
#endif
    UNSAFE_TODO(bufferTexture[i]) = [device newTextureWithDescriptor:texDesc
                                                           iosurface:buffer
                                                               plane:i];
  }

  // Create a texture to wrap the drawable.
  id<CAMetalDrawable> drawable = [self nextDrawable];
  id<MTLTexture> drawableTexture = drawable.texture;

  // Copy from the IOSurface to the drawable.
  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[0] atIndex:0];
    [encoder setFragmentTexture:bufferTexture[1] atIndex:1];
  }

  {
    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
    simd::float4x4 yuvToRgb;
    {
      SkM44 skYuvToRgb;
      if (!colorSpace.GetTransferMatrix(10).invert(&skYuvToRgb)) {
        return;
      }
      SkM44 m = skYuvToRgb * colorSpace.GetRangeAdjustMatrix(10);
      yuvToRgb = simd::float4x4(
          simd::make_float4(m.rc(0, 0), m.rc(1, 0), m.rc(2, 0), m.rc(3, 0)),
          simd::make_float4(m.rc(0, 1), m.rc(1, 1), m.rc(2, 1), m.rc(3, 1)),
          simd::make_float4(m.rc(0, 2), m.rc(1, 2), m.rc(2, 2), m.rc(3, 2)),
          simd::make_float4(m.rc(0, 3), m.rc(1, 3), m.rc(2, 3), m.rc(3, 3)));
    }

    // Compute the primary transform matrix from |color_space| to Rec2020.
    simd::float3x3 primaryMatrix;
    {
      skcms_Matrix3x3 src_to_xyz;
      skcms_Matrix3x3 rec2020_to_xyz;
      skcms_Matrix3x3 xyz_to_rec2020;
      SkNamedPrimaries::kRec2020.toXYZD50(&rec2020_to_xyz);
      colorSpace.GetPrimaryMatrix(&src_to_xyz);
      skcms_Matrix3x3_invert(&rec2020_to_xyz, &xyz_to_rec2020);
      skcms_Matrix3x3 m = skcms_Matrix3x3_concat(&xyz_to_rec2020, &src_to_xyz);
      primaryMatrix = 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:&yuvToRgb length:sizeof(yuvToRgb) atIndex:0];
    [encoder setFragmentBytes:&primaryMatrix
                       length:sizeof(primaryMatrix)
                      atIndex:1];
    [encoder setFragmentBytes:&numPlanes length:sizeof(numPlanes) atIndex:2];
    [encoder setFragmentBytes:&transferFunctionIndex
                       length:sizeof(transferFunctionIndex)
                      atIndex:3];
    [encoder setFragmentBytes:&fn length:sizeof(fn) atIndex:4];
    [encoder setFragmentBytes:&screenHdrHeadroom
                       length:sizeof(screenHdrHeadroom)
                      atIndex:5];
    [encoder drawPrimitives:MTLPrimitiveTypeTriangle
                vertexStart:0
                vertexCount:6];
  }

  [encoder endEncoding];
  [commandBuffer commit];
  [commandBuffer waitUntilScheduled];
  [drawable present];
}
@end

namespace metal {

CALayer* MakeHDRCopierLayer() {
  return [[HDRCopierLayer alloc] init];
}

void UpdateHDRCopierLayer(CALayer* layer,
                          IOSurfaceRef buffer,
                          id<MTLDevice> device,
                          float screen_hdr_headroom,
                          const gfx::ColorSpace& color_space,
                          const gfx::HDRMetadata& hdr_metadata) {
  if (auto* hdr_copier_layer = base::apple::ObjCCast<HDRCopierLayer>(layer)) {
    [hdr_copier_layer setHDRContents:buffer
                              device:device
                   screenHdrHeadroom:screen_hdr_headroom
                          colorSpace:color_space
                            metadata:hdr_metadata];
    return;
  }
}

bool ShouldUseHDRCopier(IOSurfaceRef buffer,
                        const gfx::HDRMetadata& hdr_metadata,
                        const gfx::ColorSpace& color_space) {
  // Only some transfer functions are supported.
  if (!GetTransferFunctionIndex(color_space)) {
    return false;
  }

  // Only some pixel formats are supported.
  bool is_unorm = false;
  uint32_t num_planes = 0;
  MTLPixelFormat format[2] = {MTLPixelFormatInvalid, MTLPixelFormatInvalid};
  if (!IOSurfaceGetMTLPixelFormat(buffer, num_planes, format, is_unorm)) {
    return false;
  }

  // If this is a video frame (is multi-planar), then only override the default
  // behavior for HLG content.
  if (num_planes == 2) {
    if (color_space.GetTransferID() != gfx::ColorSpace::TransferID::HLG) {
      return false;
    }
    if (!base::FeatureList::IsEnabled(kMacHlgUseHdrCopier)) {
      return false;
    }
  }

  if (color_space.IsToneMappedByDefault()) {
    return true;
  }

  if (hdr_metadata.extended_range.has_value()) {
    return true;
  }

  // Rasterized tiles and the primary plane specify a color space of SRGB_HDR
  // LINEAR_HDR, or CUSTOM_HDR, with no extended range metadata.
  // TODO(crbug.com/40268540): Use extended range metadata instead of
  // the SDR_HDR color space to indicate this.
  if (color_space.IsHDR()) {
    return !is_unorm;
  }

  return false;
}

}  // namespace metal
