// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Metal/Metal.h>

#include "base/apple/scoped_nsobject.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_display.h"
#include "ui/gl/gl_implementation.h"

// From ANGLE's egl/eglext_angle.h.
#ifndef EGL_ANGLE_metal_shared_event_sync
#define EGL_ANGLE_metal_hared_event_sync 1
#define EGL_SYNC_METAL_SHARED_EVENT_ANGLE 0x34D8
#define EGL_SYNC_METAL_SHARED_EVENT_OBJECT_ANGLE 0x34D9
#define EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_LO_ANGLE 0x34DA
#define EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_HI_ANGLE 0x34DB
#define EGL_SYNC_METAL_SHARED_EVENT_SIGNALED_ANGLE 0x34DC
#endif

namespace gl {

struct GLDisplayEGL::ObjCStorage {
  base::apple::scoped_nsprotocol<id<MTLSharedEvent>> metal_shared_event;
  uint64_t metal_signaled_value = 0;
};

// Because on Apple platforms there is a member variable of a type (ObjCStorage)
// that is defined in this file, the constructor/destructor also have to be
// here. If making changes to this copy, be sure to adjust the other copy in
// gl_display.cc.
GLDisplayEGL::GLDisplayEGL(uint64_t system_device_id, DisplayKey display_key)
    : GLDisplay(system_device_id, display_key, EGL) {
  ext = std::make_unique<DisplayExtensionsEGL>();
}

GLDisplayEGL::~GLDisplayEGL() = default;

bool GLDisplayEGL::CreateMetalSharedEvent(id<MTLSharedEvent>* shared_event_out,
                                          uint64_t* signal_value_out) {
  CHECK(ext->b_EGL_ANGLE_metal_shared_event_sync);
  if (!objc_storage_->metal_shared_event) {
    std::vector<EGLAttrib> attribs;
    attribs.push_back(EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_LO_ANGLE);
    attribs.push_back(0);
    attribs.push_back(EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_HI_ANGLE);
    attribs.push_back(0);
    attribs.push_back(EGL_NONE);
    EGLSync sync =
        eglCreateSync(display_, EGL_SYNC_METAL_SHARED_EVENT_ANGLE, &attribs[0]);
    if (!sync)
      return false;

    // eglCopyMetalSharedEventANGLE returns an MTLSharedEvent object that has a
    // retain count of 1.
    objc_storage_->metal_shared_event.reset(static_cast<id<MTLSharedEvent>>(
        eglCopyMetalSharedEventANGLE(display_, sync)));

    // The sync object is already enqueued for signaling in ANGLE's command
    // stream. Since the MTLSharedEvent is already retained, it's safe to delete
    // the sync object immediately.
    eglDestroySync(display_, sync);
  }

  // Create another sync object, perhaps redundantly the first time,
  // but with our specified signal value.
  ++objc_storage_->metal_signaled_value;
  id<MTLSharedEvent> shared_event = objc_storage_->metal_shared_event.get();
  std::vector<EGLAttrib> attribs;
  attribs.push_back(EGL_SYNC_METAL_SHARED_EVENT_OBJECT_ANGLE);
  attribs.push_back(
      static_cast<EGLAttrib>(reinterpret_cast<uintptr_t>(shared_event)));
  attribs.push_back(EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_LO_ANGLE);
  attribs.push_back(objc_storage_->metal_signaled_value & 0xFFFFFFFF);
  attribs.push_back(EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_HI_ANGLE);
  attribs.push_back((objc_storage_->metal_signaled_value >> 32) & 0xFFFFFFFF);
  attribs.push_back(EGL_NONE);

  EGLSync sync =
      eglCreateSync(display_, EGL_SYNC_METAL_SHARED_EVENT_ANGLE, &attribs[0]);
  if (!sync)
    return false;

  // The sync object is already enqueued for signaling in ANGLE's command
  // stream. Since the MTLSharedEvent is already retained, it's safe to delete
  // the sync object immediately.
  eglDestroySync(display_, sync);

  *shared_event_out = objc_storage_->metal_shared_event.get();
  *signal_value_out = objc_storage_->metal_signaled_value;
  return true;
}

void GLDisplayEGL::WaitForMetalSharedEvent(id<MTLSharedEvent> shared_event,
                                           uint64_t signal_value) {
  CHECK(objc_storage_);
  if (objc_storage_->metal_shared_event.get() == shared_event) {
    // If the event is owned by this display, skip the wait.
    // Currently ANGLE/Metal is only single threaded. There is no need to issue
    // a GPU wait for an event that was signaled by the same display. Because
    // the works before and after the signal belong to the same metal queue
    // hence they are already synchronized with each other implicitly.
    CHECK_GE(objc_storage_->metal_signaled_value, signal_value);
    return;
  }

  CHECK(ext->b_EGL_ANGLE_metal_shared_event_sync);
  EGLAttrib attribs[] = {
      // Pass the Metal shared event as an EGLAttrib.
      EGL_SYNC_METAL_SHARED_EVENT_OBJECT_ANGLE,
      static_cast<EGLAttrib>(reinterpret_cast<uintptr_t>(shared_event)),
      // EGL_SYNC_METAL_SHARED_EVENT_SIGNALED_ANGLE is important as it requests
      // ANGLE to create an EGL sync object from the Metal shared event, but NOT
      // signal it to the specified value. The shared event is imported with
      // that signal value. The next call to eglWaitSync enqueue's a GPU wait to
      // wait for that value to be signaled by another command buffer.
      EGL_SYNC_CONDITION,
      EGL_SYNC_METAL_SHARED_EVENT_SIGNALED_ANGLE,
      // Encode the signaled value in two EGLAttribs.
      EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_LO_ANGLE,
      EGLAttrib(signal_value & 0xFFFFFFFF),
      EGL_SYNC_METAL_SHARED_EVENT_SIGNAL_VALUE_HI_ANGLE,
      EGLAttrib((signal_value >> 32) & 0xFFFFFFFF),
      EGL_NONE,
  };

  EGLSync sync =
      eglCreateSync(display_, EGL_SYNC_METAL_SHARED_EVENT_ANGLE, attribs);
  EGLBoolean res = eglWaitSync(display_, sync, 0);
  DCHECK(res == EGL_TRUE);
  // The wait on the sync object has been enqueued already, so it's safe to
  // destroy it now.
  eglDestroySync(display_, sync);
}

id<MTLDevice> GLDisplayEGL::GetMetalDevice() const {
  if (GetANGLEImplementation() != ANGLEImplementation::kMetal) {
    return nil;
  }
  EGLAttrib angle_device_attrib = 0;
  if (!eglQueryDisplayAttribEXT(display_, EGL_DEVICE_EXT,
                                &angle_device_attrib)) {
    return nil;
  }
  EGLDeviceEXT angle_device =
      reinterpret_cast<EGLDeviceEXT>(angle_device_attrib);
  EGLAttrib metal_device_attrib = 0;
  if (!eglQueryDeviceAttribEXT(angle_device, EGL_METAL_DEVICE_ANGLE,
                               &metal_device_attrib)) {
    return nil;
  }
  return (__bridge id<MTLDevice>)(void*)metal_device_attrib;
}

size_t GLDisplayEGL::GetMetalDeviceAllocatedMemory() const {
  id<MTLDevice> device = GetMetalDevice();
  return device ? device.currentAllocatedSize : 0;
}

void GLDisplayEGL::InitMetalSharedEventStorage() {
  objc_storage_ = std::make_unique<ObjCStorage>();
}

void GLDisplayEGL::CleanupMetalSharedEventStorage() {
  objc_storage_.reset();
}

}  // namespace gl
