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

#include "ui/gl/direct_composition_child_surface_win.h"

#include <d3d11_1.h>
#include <dcomptypes.h>

#include "base/debug/alias.h"
#include "base/debug/dump_without_crashing.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/process/process.h"
#include "base/synchronization/waitable_event.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/traced_value.h"
#include "ui/gfx/color_space_win.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gl/direct_composition_support.h"
#include "ui/gl/egl_util.h"
#include "ui/gl/gl_angle_util_win.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_features.h"
#include "ui/gl/gl_surface_egl.h"
#include "ui/gl/gl_switches.h"
#include "ui/gl/gl_utils.h"

#ifndef EGL_ANGLE_d3d_texture_client_buffer
#define EGL_ANGLE_d3d_texture_client_buffer 1
#define EGL_D3D_TEXTURE_ANGLE 0x33A3
#define EGL_TEXTURE_OFFSET_X_ANGLE 0x3490
#define EGL_TEXTURE_OFFSET_Y_ANGLE 0x3491
#endif /* EGL_ANGLE_d3d_texture_client_buffer */

namespace gl {

namespace {
// Only one DirectComposition surface can be rendered into at a time. Track
// here which IDCompositionSurface is being rendered into. If another context
// is made current, then this surface will be suspended.
IDCompositionSurface* g_current_surface = nullptr;

const char* kDirectCompositionChildSurfaceLabel =
    "DirectCompositionChildSurface";

bool IsVerifyDrawOffsetEnabled() {
  return base::FeatureList::IsEnabled(
      features::kDirectCompositionVerifyDrawOffset);
}

}  // namespace

DirectCompositionChildSurfaceWin::DirectCompositionChildSurfaceWin(
    GLDisplayEGL* display,
    bool use_angle_texture_offset)
    : GLSurfaceEGL(display),
      use_angle_texture_offset_(use_angle_texture_offset) {}

DirectCompositionChildSurfaceWin::~DirectCompositionChildSurfaceWin() {
  Destroy();
}

bool DirectCompositionChildSurfaceWin::Initialize(GLSurfaceFormat format) {
  d3d11_device_ = QueryD3D11DeviceObjectFromANGLE();
  dcomp_device_ = GetDirectCompositionDevice();
  if (!dcomp_device_)
    return false;

  EGLint pbuffer_attribs[] = {
      EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE,
  };

  default_surface_ = eglCreatePbufferSurface(display_->GetDisplay(),
                                             GetConfig(), pbuffer_attribs);
  if (!default_surface_) {
    DLOG(ERROR) << "eglCreatePbufferSurface failed with error "
                << ui::GetLastEGLErrorString();
    return false;
  }

  return true;
}

bool DirectCompositionChildSurfaceWin::ReleaseDrawTexture(bool will_discard) {
  EGLSurface egl_surface = real_surface_;
  real_surface_ = nullptr;

  // We make current with the same surface (could be the parent), but its
  // handle has changed to |default_surface_|.
  gl::GLContext* context = gl::GLContext::GetCurrent();
  DCHECK(context);
  gl::GLSurface* surface = gl::GLSurface::GetCurrent();
  DCHECK(surface);
  bool result = context->MakeCurrent(surface);
  // If MakeCurrent fails (probably lost device), we'll want to return failure,
  // but we still want to reset the rest of the state for consistency.
  DLOG_IF(ERROR, !result) << "Failed to make current in ReleaseDrawTexture";

  if (egl_surface)
    eglDestroySurface(display_->GetDisplay(), egl_surface);

  if (dcomp_surface_.Get() == g_current_surface)
    g_current_surface = nullptr;

  HRESULT hr, device_removed_reason;
  if (draw_texture_) {
    CopyOffscreenTextureToDrawTexture();
    draw_texture_.Reset();
    if (dcomp_surface_) {
      TRACE_EVENT0("gpu", "DirectCompositionChildSurfaceWin::EndDraw");
      hr = dcomp_surface_->EndDraw();
      if (FAILED(hr)) {
        DLOG(ERROR) << "EndDraw failed with error " << std::hex << hr;
        return false;
      }
      dcomp_surface_serial_++;
    } else if (!will_discard) {
      const bool use_swap_chain_tearing =
          DirectCompositionSwapChainTearingEnabled();
      const bool force_present_interval_0 = base::FeatureList::IsEnabled(
          features::kDXGISwapChainPresentInterval0);
      UINT interval = first_swap_ || !vsync_enabled_ ||
                              use_swap_chain_tearing || force_present_interval_0
                          ? 0
                          : 1;
      UINT flags = use_swap_chain_tearing ? DXGI_PRESENT_ALLOW_TEARING : 0;

      TRACE_EVENT2("gpu", "DirectCompositionChildSurfaceWin::PresentSwapChain",
                   "has_alpha", has_alpha_, "dirty_rect",
                   swap_rect_.ToString());
      DXGI_PRESENT_PARAMETERS params = {};
      RECT dirty_rect = swap_rect_.ToRECT();
      params.DirtyRectsCount = 1;
      params.pDirtyRects = &dirty_rect;
      hr = swap_chain_->Present1(interval, flags, &params);

      // Ignore DXGI_STATUS_OCCLUDED since that's not an error but only
      // indicates that the window is occluded and we can stop rendering.
      if (FAILED(hr) && hr != DXGI_STATUS_OCCLUDED) {
        DLOG(ERROR) << "Present1 failed with error " << std::hex << hr;
        return false;
      }

      if (first_swap_) {
        // Wait for the GPU to finish executing its commands before
        // committing the DirectComposition tree, or else the swapchain
        // may flicker black when it's first presented.
        first_swap_ = false;
        Microsoft::WRL::ComPtr<IDXGIDevice2> dxgi_device2;
        d3d11_device_.As(&dxgi_device2);
        DCHECK(dxgi_device2);
        base::WaitableEvent event(
            base::WaitableEvent::ResetPolicy::AUTOMATIC,
            base::WaitableEvent::InitialState::NOT_SIGNALED);
        hr = dxgi_device2->EnqueueSetEvent(event.handle());
        if (SUCCEEDED(hr)) {
          event.Wait();
        } else {
          device_removed_reason = d3d11_device_->GetDeviceRemovedReason();
          base::debug::Alias(&hr);
          base::debug::Alias(&device_removed_reason);
          base::debug::DumpWithoutCrashing();
        }
      }
    }
  }
  return result;
}

void DirectCompositionChildSurfaceWin::Destroy() {
  if (default_surface_) {
    if (!eglDestroySurface(display_->GetDisplay(), default_surface_)) {
      DLOG(ERROR) << "eglDestroySurface failed with error "
                  << ui::GetLastEGLErrorString();
    }
    default_surface_ = nullptr;
  }
  if (real_surface_) {
    if (!eglDestroySurface(display_->GetDisplay(), real_surface_)) {
      DLOG(ERROR) << "eglDestroySurface failed with error "
                  << ui::GetLastEGLErrorString();
    }
    real_surface_ = nullptr;
  }
  if (dcomp_surface_ && (dcomp_surface_.Get() == g_current_surface)) {
    CopyOffscreenTextureToDrawTexture();
    HRESULT hr = dcomp_surface_->EndDraw();
    if (FAILED(hr))
      DLOG(ERROR) << "EndDraw failed with error " << std::hex << hr;
    g_current_surface = nullptr;
  }
  draw_texture_.Reset();
  offscreen_texture_.Reset();
  dcomp_surface_.Reset();
}

gfx::Size DirectCompositionChildSurfaceWin::GetSize() {
  return size_;
}

bool DirectCompositionChildSurfaceWin::IsOffscreen() {
  return false;
}

void* DirectCompositionChildSurfaceWin::GetHandle() {
  return real_surface_ ? real_surface_ : default_surface_;
}

gfx::SwapResult DirectCompositionChildSurfaceWin::SwapBuffers(
    PresentationCallback callback,
    gfx::FrameData data) {
  NOTREACHED();
  return gfx::SwapResult::SWAP_FAILED;
}

bool DirectCompositionChildSurfaceWin::EndDraw(gfx::Rect* swap_rect) {
  TRACE_EVENT1("gpu", "DirectCompositionChildSurfaceWin::EndDraw", "size",
               size_.ToString());

  bool success = ReleaseDrawTexture(false /* will_discard */);

  *swap_rect = swap_rect_;

  // Reset swap_rect_ since SetDrawRectangle may not be called when the root
  // damage rect is empty.
  swap_rect_ = gfx::Rect();

  return success;
}

gfx::SurfaceOrigin DirectCompositionChildSurfaceWin::GetOrigin() const {
  return gfx::SurfaceOrigin::kTopLeft;
}

bool DirectCompositionChildSurfaceWin::SupportsPostSubBuffer() {
  return true;
}

bool DirectCompositionChildSurfaceWin::OnMakeCurrent(GLContext* context) {
  if (g_current_surface != dcomp_surface_.Get()) {
    if (g_current_surface) {
      HRESULT hr = g_current_surface->SuspendDraw();
      if (FAILED(hr)) {
        DLOG(ERROR) << "SuspendDraw failed with error " << std::hex << hr;
        return false;
      }
      g_current_surface = nullptr;
    }
    // We're in the middle of |dcomp_surface_| draw only if |draw_texture_| is
    // not null.
    if (dcomp_surface_ && draw_texture_) {
      HRESULT hr = dcomp_surface_->ResumeDraw();
      if (FAILED(hr)) {
        DLOG(ERROR) << "ResumeDraw failed with error " << std::hex << hr;
        return false;
      }
      g_current_surface = dcomp_surface_.Get();
    }
  }
  return true;
}

bool DirectCompositionChildSurfaceWin::SupportsDCLayers() const {
  return true;
}

bool DirectCompositionChildSurfaceWin::SetDrawRectangle(
    const gfx::Rect& rectangle) {
  if (!gfx::Rect(size_).Contains(rectangle)) {
    DLOG(ERROR) << "Draw rectangle must be contained within size of surface";
    return false;
  }

  if (draw_texture_) {
    DLOG(ERROR) << "SetDrawRectangle must be called only once per swap buffers";
    return false;
  }
  DCHECK(!real_surface_);
  DCHECK(!g_current_surface);

  if (gfx::Rect(size_) != rectangle && !swap_chain_ && !dcomp_surface_) {
    DLOG(ERROR) << "First draw to surface must draw to everything";
    return false;
  }

  gl::GLContext* context = gl::GLContext::GetCurrent();
  if (!context) {
    DLOG(ERROR) << "gl::GLContext::GetCurrent() returned nullptr";
    return false;
  }
  gl::GLSurface* surface = gl::GLSurface::GetCurrent();
  if (!surface) {
    DLOG(ERROR) << "gl::GLSurface::GetCurrent() returned nullptr";
    return false;
  }

  DXGI_FORMAT dxgi_format = gfx::ColorSpaceWin::GetDXGIFormat(color_space_);

  // IDCompositionDevice2::CreateSurface does not support rgb10. In cases where
  // dc overlays are to be used for rgb10, use swap chains instead.
  if (!dcomp_surface_ && enable_dc_layers_ &&
      dxgi_format != DXGI_FORMAT::DXGI_FORMAT_R10G10B10A2_UNORM) {
    TRACE_EVENT2("gpu", "DirectCompositionChildSurfaceWin::CreateSurface",
                 "width", size_.width(), "height", size_.height());
    swap_chain_.Reset();
    // Always treat as premultiplied, because an underlay could cause it to
    // become transparent.
    HRESULT hr = dcomp_device_->CreateSurface(
        size_.width(), size_.height(), dxgi_format,
        DXGI_ALPHA_MODE_PREMULTIPLIED, &dcomp_surface_);
    base::UmaHistogramSparse("GPU.DirectComposition.DcompDeviceCreateSurface",
                             hr);
    if (FAILED(hr)) {
      DLOG(ERROR) << "CreateSurface failed with error " << std::hex << hr;
      // Disable direct composition because CreateSurface might fail again next
      // time.
      SetDirectCompositionSwapChainFailed();
      return false;
    }

    // Use swap chains for rgb10 because dcomp surfaces cannot be created.
  } else if (!swap_chain_ &&
             (!enable_dc_layers_ ||
              dxgi_format == DXGI_FORMAT::DXGI_FORMAT_R10G10B10A2_UNORM)) {
    TRACE_EVENT2("gpu", "DirectCompositionChildSurfaceWin::CreateSwapChain",
                 "width", size_.width(), "height", size_.height());
    offscreen_texture_.Reset();
    dcomp_surface_.Reset();

    Microsoft::WRL::ComPtr<IDXGIDevice> dxgi_device;
    d3d11_device_.As(&dxgi_device);
    DCHECK(dxgi_device);
    Microsoft::WRL::ComPtr<IDXGIAdapter> dxgi_adapter;
    dxgi_device->GetAdapter(&dxgi_adapter);
    DCHECK(dxgi_adapter);
    Microsoft::WRL::ComPtr<IDXGIFactory2> dxgi_factory;
    dxgi_adapter->GetParent(IID_PPV_ARGS(&dxgi_factory));
    DCHECK(dxgi_factory);

    DXGI_SWAP_CHAIN_DESC1 desc = {};
    desc.Width = size_.width();
    desc.Height = size_.height();
    desc.Format = dxgi_format;
    desc.Stereo = FALSE;
    desc.SampleDesc.Count = 1;
    desc.BufferCount = gl::DirectCompositionRootSurfaceBufferCount();
    desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    desc.Scaling = DXGI_SCALING_STRETCH;
    desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
    desc.AlphaMode =
        has_alpha_ ? DXGI_ALPHA_MODE_PREMULTIPLIED : DXGI_ALPHA_MODE_IGNORE;
    desc.Flags = 0;
    if (DirectCompositionSwapChainTearingEnabled()) {
      desc.Flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
    }
    if (DXGIWaitableSwapChainEnabled()) {
      desc.Flags |= DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
    }

    HRESULT hr = dxgi_factory->CreateSwapChainForComposition(
        d3d11_device_.Get(), &desc, nullptr, &swap_chain_);
    first_swap_ = true;
    base::UmaHistogramSparse(
        "GPU.DirectComposition.CreateSwapChainForComposition", hr);

    // If CreateSwapChainForComposition fails, we cannot draw to the
    // browser window. Return false after disabling Direct Composition support
    // and let the Renderer handle it. Either the GPU command buffer or the GPU
    // process will be restarted.
    if (FAILED(hr)) {
      DLOG(ERROR) << "CreateSwapChainForComposition failed with error "
                  << std::hex << hr;
      // Disable direct composition because SwapChain creation might fail again
      // next time.
      SetDirectCompositionSwapChainFailed();
      return false;
    }

    gl::LabelSwapChainAndBuffers(swap_chain_.Get(),
                                 kDirectCompositionChildSurfaceLabel);

    Microsoft::WRL::ComPtr<IDXGISwapChain3> swap_chain;
    if (SUCCEEDED(swap_chain_.As(&swap_chain))) {
      hr = swap_chain->SetColorSpace1(
          gfx::ColorSpaceWin::GetDXGIColorSpace(color_space_));
      DCHECK(SUCCEEDED(hr))
          << "SetColorSpace1 failed with error " << std::hex << hr;
      if (DXGIWaitableSwapChainEnabled()) {
        hr = swap_chain->SetMaximumFrameLatency(
            GetDXGIWaitableSwapChainMaxQueuedFrames());
        DCHECK(SUCCEEDED(hr)) << "SetMaximumFrameLatency failed with error "
                              << logging::SystemErrorCodeToString(hr);
      }
    }
  }

  swap_rect_ = rectangle;
  draw_offset_ = gfx::Vector2d();
  const bool verify_draw_offset = dcomp_surface_ && IsVerifyDrawOffsetEnabled();

  if (dcomp_surface_) {
    TRACE_EVENT0("gpu", "DirectCompositionChildSurfaceWin::BeginDraw");
    const RECT rect = rectangle.ToRECT();
    dcomp_update_offset_ = {};
    HRESULT hr = dcomp_surface_->BeginDraw(&rect, IID_PPV_ARGS(&draw_texture_),
                                           &dcomp_update_offset_);
    if (FAILED(hr)) {
      DLOG(ERROR) << "BeginDraw failed with error " << std::hex << hr;
      return false;
    }
    if (verify_draw_offset) {
      draw_offset_ = {features::kVerifyDrawOffsetX.Get(),
                      features::kVerifyDrawOffsetY.Get()};
    } else {
      draw_offset_ = gfx::Point(dcomp_update_offset_) - rectangle.origin();
    }
  } else {
    TRACE_EVENT0("gpu", "DirectCompositionChildSurfaceWin::GetBuffer");
    swap_chain_->GetBuffer(0, IID_PPV_ARGS(&draw_texture_));
  }
  DCHECK(draw_texture_);

  g_current_surface = dcomp_surface_.Get();

  std::vector<EGLint> pbuffer_attribs;
  pbuffer_attribs.push_back(EGL_WIDTH);
  pbuffer_attribs.push_back(size_.width());
  pbuffer_attribs.push_back(EGL_HEIGHT);
  pbuffer_attribs.push_back(size_.height());
  if (use_angle_texture_offset_) {
    pbuffer_attribs.push_back(EGL_TEXTURE_OFFSET_X_ANGLE);
    pbuffer_attribs.push_back(draw_offset_.x());
    pbuffer_attribs.push_back(EGL_TEXTURE_OFFSET_Y_ANGLE);
    pbuffer_attribs.push_back(draw_offset_.y());
  }
  pbuffer_attribs.push_back(EGL_NONE);

  EGLClientBuffer buffer =
      reinterpret_cast<EGLClientBuffer>(draw_texture_.Get());

  if (verify_draw_offset) {
    buffer = reinterpret_cast<EGLClientBuffer>(GetOffscreenTexture().Get());
  }

  real_surface_ = eglCreatePbufferFromClientBuffer(
      display_->GetDisplay(), EGL_D3D_TEXTURE_ANGLE, buffer, GetConfig(),
      pbuffer_attribs.data());
  if (!real_surface_) {
    DLOG(ERROR) << "eglCreatePbufferFromClientBuffer failed with error "
                << ui::GetLastEGLErrorString();
    return false;
  }

  // We make current with the same surface (could be the parent), but its
  // handle has changed to |real_surface_|.
  if (!context->MakeCurrent(surface)) {
    DLOG(ERROR) << "Failed to make current in SetDrawRectangle";
    return false;
  }

  return true;
}

void DirectCompositionChildSurfaceWin::SetDCompSurfaceForTesting(
    Microsoft::WRL::ComPtr<IDCompositionSurface> surface) {
  offscreen_texture_.Reset();
  dcomp_surface_ = std::move(surface);
}

gfx::Vector2d DirectCompositionChildSurfaceWin::GetDrawOffset() const {
  return use_angle_texture_offset_ ? gfx::Vector2d() : draw_offset_;
}

void DirectCompositionChildSurfaceWin::SetVSyncEnabled(bool enabled) {
  vsync_enabled_ = enabled;
}

bool DirectCompositionChildSurfaceWin::Resize(
    const gfx::Size& size,
    float scale_factor,
    const gfx::ColorSpace& color_space,
    bool has_alpha) {
  if (size_ == size && has_alpha_ == has_alpha && color_space_ == color_space)
    return true;

  // This will release indirect references to swap chain (|real_surface_|) by
  // binding |default_surface_| as the default framebuffer.
  if (!ReleaseDrawTexture(true /* will_discard */))
    return false;

  bool resize_only = has_alpha_ == has_alpha && color_space_ == color_space;

  size_ = size;
  color_space_ = color_space;
  has_alpha_ = has_alpha;

  // ResizeBuffers can't change alpha blending mode.
  if (swap_chain_ && resize_only) {
    UINT buffer_count = gl::DirectCompositionRootSurfaceBufferCount();
    DXGI_FORMAT format = gfx::ColorSpaceWin::GetDXGIFormat(color_space_);
    UINT flags = 0;
    if (DirectCompositionSwapChainTearingEnabled()) {
      flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
    }
    if (DXGIWaitableSwapChainEnabled()) {
      flags |= DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
    }
    HRESULT hr = swap_chain_->ResizeBuffers(buffer_count, size.width(),
                                            size.height(), format, flags);
    UMA_HISTOGRAM_BOOLEAN("GPU.DirectComposition.SwapChainResizeResult",
                          SUCCEEDED(hr));
    if (SUCCEEDED(hr)) {
      // Resizing swap chain buffers causes the internal textures to be released
      // and re-created as new textures. We need to label the new textures.
      gl::LabelSwapChainBuffers(swap_chain_.Get(),
                                kDirectCompositionChildSurfaceLabel);
      return true;
    }

    DLOG(ERROR) << "ResizeBuffers failed with error 0x" << std::hex << hr;
  }
  // Next SetDrawRectangle call will recreate the swap chain or surface.
  swap_chain_.Reset();
  offscreen_texture_.Reset();
  dcomp_surface_.Reset();
  return true;
}

bool DirectCompositionChildSurfaceWin::SetEnableDCLayers(bool enable) {
  if (enable_dc_layers_ == enable)
    return true;
  enable_dc_layers_ = enable;
  if (!ReleaseDrawTexture(true /* will_discard */))
    return false;
  // Next SetDrawRectangle call will recreate the swap chain or surface.
  swap_chain_.Reset();
  offscreen_texture_.Reset();
  dcomp_surface_.Reset();
  return true;
}

Microsoft::WRL::ComPtr<ID3D11Texture2D>
DirectCompositionChildSurfaceWin::GetOffscreenTexture() {
  if (!dcomp_surface_) {
    return offscreen_texture_ = nullptr;
  }
  if (offscreen_texture_) {
    return offscreen_texture_;
  }

  D3D11_TEXTURE2D_DESC desc = {};
  desc.Width = size_.width() + features::kVerifyDrawOffsetX.Get();
  desc.Height = size_.height() + features::kVerifyDrawOffsetY.Get();
  desc.MipLevels = 1;
  desc.ArraySize = 1;
  desc.Format = gfx::ColorSpaceWin::GetDXGIFormat(color_space_);
  desc.SampleDesc.Count = 1;
  desc.BindFlags = D3D11_BIND_RENDER_TARGET;
  d3d11_device_->CreateTexture2D(&desc, nullptr, &offscreen_texture_);
  return offscreen_texture_;
}

void DirectCompositionChildSurfaceWin::CopyOffscreenTextureToDrawTexture() {
  if (!offscreen_texture_ || !draw_texture_ || !dcomp_surface_) {
    return;
  }

  D3D11_BOX box = {};
  box.left = swap_rect_.origin().x() + features::kVerifyDrawOffsetX.Get();
  box.top = swap_rect_.origin().y() + features::kVerifyDrawOffsetY.Get();
  box.right = box.left + swap_rect_.width();
  box.bottom = box.top + swap_rect_.height();
  box.front = 0;
  box.back = 1;

  Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
  d3d11_device_->GetImmediateContext(&context);
  context->CopySubresourceRegion(draw_texture_.Get(), 0, dcomp_update_offset_.x,
                                 dcomp_update_offset_.y, 0,
                                 offscreen_texture_.Get(), 0, &box);
}

}  // namespace gl