/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "GpuProcessD3D11TextureMap.h"

#include "libyuv.h"
#include "mozilla/layers/CompositorThread.h"
#include "mozilla/layers/D3D11ZeroCopyTextureImage.h"
#include "mozilla/layers/HelpersD3D11.h"
#include "mozilla/layers/TextureHostWrapperD3D11.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/SharedThreadPool.h"
#include "mozilla/webrender/RenderThread.h"

namespace mozilla {

namespace layers {

StaticAutoPtr<GpuProcessD3D11TextureMap> GpuProcessD3D11TextureMap::sInstance;

/* static */
void GpuProcessD3D11TextureMap::Init() {
  MOZ_ASSERT(XRE_IsGPUProcess());
  sInstance = new GpuProcessD3D11TextureMap();
}

/* static */
void GpuProcessD3D11TextureMap::Shutdown() {
  MOZ_ASSERT(XRE_IsGPUProcess());
  sInstance = nullptr;
}

/* static */
GpuProcessTextureId GpuProcessD3D11TextureMap::GetNextTextureId() {
  MOZ_ASSERT(XRE_IsGPUProcess());
  return GpuProcessTextureId::GetNext();
}

GpuProcessD3D11TextureMap::GpuProcessD3D11TextureMap()
    : mMonitor("GpuProcessD3D11TextureMap::mMonitor") {}

GpuProcessD3D11TextureMap::~GpuProcessD3D11TextureMap() {}

void GpuProcessD3D11TextureMap::Register(
    GpuProcessTextureId aTextureId, ID3D11Texture2D* aTexture,
    uint32_t aArrayIndex, const gfx::IntSize& aSize,
    ZeroCopyUsageInfo* aUsageInfo,
    RefPtr<gfx::FileHandleWrapper> aSharedHandle) {
  MonitorAutoLock lock(mMonitor);
  Register(lock, aTextureId, aTexture, aArrayIndex, aSize, aUsageInfo,
           aSharedHandle);
}
void GpuProcessD3D11TextureMap::Register(
    const MonitorAutoLock& aProofOfLock, GpuProcessTextureId aTextureId,
    ID3D11Texture2D* aTexture, uint32_t aArrayIndex, const gfx::IntSize& aSize,
    ZeroCopyUsageInfo* aUsageInfo,
    RefPtr<gfx::FileHandleWrapper> aSharedHandle) {
  MOZ_RELEASE_ASSERT(aTexture);

  auto it = mD3D11TexturesById.find(aTextureId);
  if (it != mD3D11TexturesById.end()) {
    MOZ_ASSERT_UNREACHABLE("unexpected to be called");
    return;
  }
  mD3D11TexturesById.emplace(
      aTextureId,
      TextureHolder(aTexture, aArrayIndex, aSize, aUsageInfo, aSharedHandle));
}

void GpuProcessD3D11TextureMap::Unregister(GpuProcessTextureId aTextureId) {
  MonitorAutoLock lock(mMonitor);

  auto it = mD3D11TexturesById.find(aTextureId);
  if (it == mD3D11TexturesById.end()) {
    return;
  }
  mD3D11TexturesById.erase(it);
}

RefPtr<ID3D11Texture2D> GpuProcessD3D11TextureMap::GetTexture(
    GpuProcessTextureId aTextureId) {
  MonitorAutoLock lock(mMonitor);

  auto it = mD3D11TexturesById.find(aTextureId);
  if (it == mD3D11TexturesById.end()) {
    return nullptr;
  }

  return it->second.mTexture;
}

Maybe<HANDLE> GpuProcessD3D11TextureMap::GetSharedHandle(
    GpuProcessTextureId aTextureId) {
  TextureHolder holder;
  {
    MonitorAutoLock lock(mMonitor);

    auto it = mD3D11TexturesById.find(aTextureId);
    if (it == mD3D11TexturesById.end()) {
      return Nothing();
    }

    if (it->second.mSharedHandle) {
      return Some(it->second.mSharedHandle->GetHandle());
    }

    if (it->second.mCopiedTextureSharedHandle) {
      return Some(it->second.mCopiedTextureSharedHandle->GetHandle());
    }

    holder = it->second;
  }

  RefPtr<ID3D11Device> device;
  holder.mTexture->GetDevice(getter_AddRefs(device));
  if (!device) {
    return Nothing();
  }

  RefPtr<ID3D11DeviceContext> context;
  device->GetImmediateContext(getter_AddRefs(context));
  if (!context) {
    return Nothing();
  }

  D3D11_TEXTURE2D_DESC existingDesc;
  holder.mTexture->GetDesc(&existingDesc);

  CD3D11_TEXTURE2D_DESC newDesc(
      existingDesc.Format, holder.mSize.width, holder.mSize.height, 1, 1,
      D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
  newDesc.MiscFlags =
      D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED;

  RefPtr<ID3D11Texture2D> copiedTexture;
  HRESULT hr =
      device->CreateTexture2D(&newDesc, nullptr, getter_AddRefs(copiedTexture));
  if (FAILED(hr)) {
    return Nothing();
  }

  D3D11_TEXTURE2D_DESC inDesc;
  holder.mTexture->GetDesc(&inDesc);

  D3D11_TEXTURE2D_DESC outDesc;
  copiedTexture->GetDesc(&outDesc);

  UINT height = std::min(inDesc.Height, outDesc.Height);
  UINT width = std::min(inDesc.Width, outDesc.Width);
  D3D11_BOX srcBox = {0, 0, 0, width, height, 1};

  context->CopySubresourceRegion(copiedTexture, 0, 0, 0, 0, holder.mTexture,
                                 holder.mArrayIndex, &srcBox);

  RefPtr<IDXGIResource1> resource;
  copiedTexture->QueryInterface((IDXGIResource1**)getter_AddRefs(resource));
  if (!resource) {
    return Nothing();
  }

  HANDLE sharedHandle;
  hr = resource->CreateSharedHandle(
      nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, nullptr,
      &sharedHandle);
  if (FAILED(hr)) {
    return Nothing();
  }

  RefPtr handle =
      MakeRefPtr<gfx::FileHandleWrapper>(UniqueFileHandle(sharedHandle));

  RefPtr<ID3D11Query> query;
  CD3D11_QUERY_DESC desc(D3D11_QUERY_EVENT);
  hr = device->CreateQuery(&desc, getter_AddRefs(query));
  if (FAILED(hr) || !query) {
    gfxWarning() << "Could not create D3D11_QUERY_EVENT: " << gfx::hexa(hr);
    return Nothing();
  }

  context->End(query);

  BOOL result;
  bool ret = WaitForFrameGPUQuery(device, context, query, &result);
  if (!ret) {
    gfxCriticalNoteOnce << "WaitForFrameGPUQuery() failed";
  }

  {
    MonitorAutoLock lock(mMonitor);

    auto it = mD3D11TexturesById.find(aTextureId);
    if (it == mD3D11TexturesById.end()) {
      MOZ_ASSERT_UNREACHABLE("unexpected to be called");
      return Nothing();
    }

    // Disable no video copy for future decoded video frames. Since
    // Get SharedHandle of copied Texture() is slow.
    if (it->second.mZeroCopyUsageInfo) {
      it->second.mZeroCopyUsageInfo->DisableZeroCopyNV12Texture();
    }

    it->second.mCopiedTexture = copiedTexture;
    it->second.mCopiedTextureSharedHandle = handle;
  }

  return Some(handle->GetHandle());
}

void GpuProcessD3D11TextureMap::DisableZeroCopyNV12Texture(
    GpuProcessTextureId aTextureId) {
  MonitorAutoLock lock(mMonitor);

  auto it = mD3D11TexturesById.find(aTextureId);
  if (it == mD3D11TexturesById.end()) {
    MOZ_ASSERT_UNREACHABLE("unexpected to be called");
    return;
  }

  if (!it->second.mZeroCopyUsageInfo) {
    return;
  }

  // Disable no video copy for future decoded video frames. Since
  // Get SharedHandle of copied Texture() is slow.
  it->second.mZeroCopyUsageInfo->DisableZeroCopyNV12Texture();
}

size_t GpuProcessD3D11TextureMap::GetWaitingTextureCount() const {
  MonitorAutoLock lock(mMonitor);
  return mWaitingTextures.size();
}

bool GpuProcessD3D11TextureMap::WaitTextureReady(
    const GpuProcessTextureId aTextureId) {
  MOZ_ASSERT(wr::RenderThread::IsInRenderThread());

  MonitorAutoLock lock(mMonitor);
  auto it = mWaitingTextures.find(aTextureId);
  if (it == mWaitingTextures.end()) {
    return true;
  }

  auto start = TimeStamp::Now();
  const TimeDuration timeout = TimeDuration::FromMilliseconds(1000);
  while (1) {
    CVStatus status = mMonitor.Wait(timeout);
    if (status == CVStatus::Timeout) {
      MOZ_ASSERT_UNREACHABLE("unexpected to be called");
      gfxCriticalNoteOnce << "GpuProcessTextureI wait time out id:"
                          << uint64_t(aTextureId);
      return false;
    }

    auto it = mWaitingTextures.find(aTextureId);
    if (it == mWaitingTextures.end()) {
      break;
    }
  }

  auto end = TimeStamp::Now();
  const auto durationMs = static_cast<uint32_t>((end - start).ToMilliseconds());
  nsPrintfCString marker("TextureReadyWait %ums ", durationMs);
  PROFILER_MARKER_TEXT("PresentWait", GRAPHICS, {}, marker);

  return true;
}

void GpuProcessD3D11TextureMap::PostUpdateTextureDataTask(
    const GpuProcessTextureId aTextureId, TextureHost* aTextureHost,
    TextureHost* aWrappedTextureHost,
    TextureWrapperD3D11Allocator* aAllocator) {
  {
    MonitorAutoLock lock(mMonitor);

    auto it = mWaitingTextures.find(aTextureId);
    if (it != mWaitingTextures.end()) {
      MOZ_ASSERT_UNREACHABLE("unexpected to be called");
      return;
    }

    auto updatingTexture = MakeUnique<UpdatingTextureHolder>(
        aTextureId, aTextureHost, aWrappedTextureHost, aAllocator);

    mWaitingTextures.emplace(aTextureId);
    mWaitingTextureQueue.push_back(std::move(updatingTexture));
  }

  RefPtr<Runnable> runnable = NS_NewRunnableFunction(
      "GpuProcessD3D11TextureMap::PostUpdateTextureDataTaskRunnable", []() {
        GpuProcessD3D11TextureMap::Get()->HandleInTextureUpdateThread();
      });
  nsCOMPtr<nsIEventTarget> thread = aAllocator->mThread;
  thread->Dispatch(runnable.forget());
}

void GpuProcessD3D11TextureMap::HandleInTextureUpdateThread() {
  UniquePtr<UpdatingTextureHolder> textureHolder;
  {
    MonitorAutoLock lock(mMonitor);

    if (mWaitingTextureQueue.empty()) {
      return;
    }
    textureHolder = std::move(mWaitingTextureQueue.front());
    mWaitingTextureQueue.pop_front();
  }

  RefPtr<ID3D11Texture2D> texture = UpdateTextureData(textureHolder.get());

  {
    MonitorAutoLock lock(mMonitor);
    if (texture) {
      auto size = textureHolder->mWrappedTextureHost->GetSize();
      Register(lock, textureHolder->mTextureId, texture, /* aArrayIndex */ 0,
               size, /* aUsageInfo */ nullptr, /* aSharedHandle */ nullptr);
    }
    mWaitingTextures.erase(textureHolder->mTextureId);
    MOZ_ASSERT(mWaitingTextures.size() == mWaitingTextureQueue.size());
    mMonitor.Notify();
  }

  // Release UpdatingTextureHolder in CompositorThread
  RefPtr<Runnable> runnable = NS_NewRunnableFunction(
      "GpuProcessD3D11TextureMap::HandleInTextureUpdateThread::Runnable",
      [textureHolder = std::move(textureHolder)]() mutable {
        textureHolder = nullptr;
      });
  CompositorThread()->Dispatch(runnable.forget());
}

RefPtr<ID3D11Texture2D> GpuProcessD3D11TextureMap::UpdateTextureData(
    UpdatingTextureHolder* aHolder) {
  MOZ_ASSERT(aHolder);
  MOZ_ASSERT(aHolder->mAllocator->mThread->IsOnCurrentThread());

  auto* bufferTexture = aHolder->mWrappedTextureHost->AsBufferTextureHost();
  MOZ_ASSERT(bufferTexture);
  if (!bufferTexture) {
    MOZ_ASSERT_UNREACHABLE("unexpected to be called");
    return nullptr;
  }

  auto size = bufferTexture->GetSize();
  auto colorDepth = bufferTexture->GetColorDepth();
  gfx::SurfaceFormat surfaceFormat = colorDepth == gfx::ColorDepth::COLOR_10
                                         ? gfx::SurfaceFormat::P010
                                         : gfx::SurfaceFormat::NV12;

  RefPtr<ID3D11Texture2D> texture2D =
      aHolder->mAllocator->CreateOrRecycle(surfaceFormat, size);
  if (!texture2D) {
    return nullptr;
  }

  RefPtr<ID3D11Texture2D> stagingTexture =
      aHolder->mAllocator->GetStagingTexture();
  if (!stagingTexture) {
    return nullptr;
  }

  RefPtr<ID3D11DeviceContext> context;
  RefPtr<ID3D11Device> device = aHolder->mAllocator->GetDevice();
  device->GetImmediateContext(getter_AddRefs(context));
  if (!context) {
    return nullptr;
  }

  RefPtr<ID3D10Multithread> mt;
  HRESULT hr = device->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
  if (FAILED(hr) || !mt) {
    gfxCriticalError() << "Multithread safety interface not supported. " << hr;
    return nullptr;
  }

  if (!mt->GetMultithreadProtected()) {
    gfxCriticalError() << "Device used not marked as multithread-safe.";
    return nullptr;
  }

  D3D11MTAutoEnter mtAutoEnter(mt.forget());

  AutoLockD3D11Texture lockSt(stagingTexture);

  D3D11_MAP mapType = D3D11_MAP_WRITE;
  D3D11_MAPPED_SUBRESOURCE mappedResource;

  hr = context->Map(stagingTexture, 0, mapType, 0, &mappedResource);
  if (FAILED(hr)) {
    gfxCriticalNoteOnce << "Mapping D3D11 staging texture failed: "
                        << gfx::hexa(hr);
    return nullptr;
  }

  const size_t destStride = mappedResource.RowPitch;
  int32_t yStride = bufferTexture->GetYStride();
  int32_t cbCrStride = bufferTexture->GetCbCrStride();

  if (colorDepth == gfx::ColorDepth::COLOR_10) {
    const uint16_t* yChannel = bufferTexture->GetYChannel16();
    const uint16_t* cbChannel = bufferTexture->GetCbChannel16();
    const uint16_t* crChannel = bufferTexture->GetCrChannel16();
    uint16_t* yDestPlaneStart =
        reinterpret_cast<uint16_t*>(mappedResource.pData);
    uint16_t* uvDestPlaneStart = reinterpret_cast<uint16_t*>(
        reinterpret_cast<uint8_t*>(mappedResource.pData) +
        destStride * size.height);

    libyuv::I010ToP010(yChannel, yStride / 2, cbChannel, cbCrStride / 2,
                       crChannel, cbCrStride / 2, yDestPlaneStart,
                       destStride / 2, uvDestPlaneStart, destStride / 2,
                       size.width, size.height);
  } else {
    const uint8_t* yChannel = bufferTexture->GetYChannel();
    const uint8_t* cbChannel = bufferTexture->GetCbChannel();
    const uint8_t* crChannel = bufferTexture->GetCrChannel();
    uint8_t* yDestPlaneStart = reinterpret_cast<uint8_t*>(mappedResource.pData);
    uint8_t* uvDestPlaneStart =
        reinterpret_cast<uint8_t*>(mappedResource.pData) +
        destStride * size.height;

    libyuv::I420ToNV12(yChannel, yStride, cbChannel, cbCrStride, crChannel,
                       cbCrStride, yDestPlaneStart, destStride,
                       uvDestPlaneStart, destStride, size.width, size.height);
  }

  context->Unmap(stagingTexture, 0);

  context->CopyResource(texture2D, stagingTexture);

  return texture2D;
}

GpuProcessD3D11TextureMap::TextureHolder::TextureHolder(
    ID3D11Texture2D* aTexture, uint32_t aArrayIndex, const gfx::IntSize& aSize,
    ZeroCopyUsageInfo* aUsageInfo, RefPtr<gfx::FileHandleWrapper> aSharedHandle)
    : mTexture(aTexture),
      mArrayIndex(aArrayIndex),
      mSize(aSize),
      mZeroCopyUsageInfo(aUsageInfo),
      mSharedHandle(aSharedHandle) {}

GpuProcessD3D11TextureMap::UpdatingTextureHolder::UpdatingTextureHolder(
    const GpuProcessTextureId aTextureId, TextureHost* aTextureHost,
    TextureHost* aWrappedTextureHost, TextureWrapperD3D11Allocator* aAllocator)
    : mTextureId(aTextureId),
      mTextureHost(aTextureHost),
      mWrappedTextureHost(aWrappedTextureHost),
      mAllocator(aAllocator) {
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
}

GpuProcessD3D11TextureMap::UpdatingTextureHolder::~UpdatingTextureHolder() {
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
}

}  // namespace layers
}  // namespace mozilla
