/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "RenderCompositorD3D11SWGL.h"

#include "mozilla/widget/CompositorWidget.h"
#include "mozilla/layers/Effects.h"
#include "mozilla/webrender/RenderD3D11TextureHost.h"
#include "RenderCompositorRecordedFrame.h"

namespace mozilla {
using namespace layers;

namespace wr {

extern LazyLogModule gRenderThreadLog;
#define LOG(...) MOZ_LOG(gRenderThreadLog, LogLevel::Debug, (__VA_ARGS__))

RenderCompositorD3D11SWGL::UploadMode
RenderCompositorD3D11SWGL::GetUploadMode() {
  int mode = StaticPrefs::gfx_webrender_software_d3d11_upload_mode();
  switch (mode) {
    case 1:
      return Upload_Immediate;
    case 2:
      return Upload_Staging;
    case 3:
      return Upload_StagingNoBlock;
    case 4:
      return Upload_StagingPooled;
    default:
      return Upload_Staging;
  }
}

UniquePtr<RenderCompositor> RenderCompositorD3D11SWGL::Create(
    const RefPtr<widget::CompositorWidget>& aWidget, nsACString& aError) {
  if (!aWidget->GetCompositorOptions().AllowSoftwareWebRenderD3D11() ||
      !gfx::gfxConfig::IsEnabled(gfx::Feature::D3D11_COMPOSITING)) {
    return nullptr;
  }

  void* ctx = wr_swgl_create_context();
  if (!ctx) {
    gfxCriticalNote << "Failed SWGL context creation for WebRender";
    return nullptr;
  }

  RefPtr<CompositorD3D11> compositor = MakeAndAddRef<CompositorD3D11>(aWidget);
  nsCString log;
  if (!compositor->Initialize(&log)) {
    gfxCriticalNote << "Failed to initialize CompositorD3D11 for SWGL: "
                    << log.get();
    return nullptr;
  }
  return MakeUnique<RenderCompositorD3D11SWGL>(compositor, aWidget, ctx);
}

RenderCompositorD3D11SWGL::RenderCompositorD3D11SWGL(
    CompositorD3D11* aCompositor,
    const RefPtr<widget::CompositorWidget>& aWidget, void* aContext)
    : RenderCompositorLayersSWGL(aCompositor, aWidget, aContext) {
  LOG("RenderCompositorD3D11SWGL::RenderCompositorD3D11SWGL()");

  mSyncObject = GetCompositorD3D11()->GetSyncObject();
}

RenderCompositorD3D11SWGL::~RenderCompositorD3D11SWGL() {
  LOG("RenderCompositorD3D11SWGL::~RenderCompositorD3D11SWGL()");
}

bool RenderCompositorD3D11SWGL::BeginFrame() {
  if (!RenderCompositorLayersSWGL::BeginFrame()) {
    return false;
  }

  mUploadMode = GetUploadMode();
  return true;
}

void RenderCompositorD3D11SWGL::HandleExternalImage(
    RenderTextureHost* aExternalImage, FrameSurface& aFrameSurface) {
  // We need to hold the texture source separately from the effect,
  // since the effect doesn't hold a strong reference.
  RefPtr<DataTextureSourceD3D11> layer;
  RefPtr<TexturedEffect> texturedEffect;
  gfx::IntSize size;
  if (auto* host = aExternalImage->AsRenderDXGITextureHost()) {
    if (!host->EnsureD3D11Texture2D(GetDevice())) {
      return;
    }

    layer = new DataTextureSourceD3D11(GetDevice(), host->GetFormat(),
                                       host->GetD3D11Texture2D());
    if (host->GetFormat() == SurfaceFormat::NV12 ||
        host->GetFormat() == SurfaceFormat::P010 ||
        host->GetFormat() == SurfaceFormat::P016) {
      const auto yuv = FromYUVRangedColorSpace(host->GetYUVColorSpace());
      texturedEffect =
          new EffectNV12(layer, yuv.space, yuv.range, host->GetColorDepth(),
                         aFrameSurface.mFilter);
    } else {
      MOZ_ASSERT(host->GetFormat() == SurfaceFormat::B8G8R8X8 ||
                 host->GetFormat() == SurfaceFormat::B8G8R8A8);
      texturedEffect = CreateTexturedEffect(host->GetFormat(), layer,
                                            aFrameSurface.mFilter, true);
    }
    size = host->GetSize(0);
    host->LockInternal();
  } else if (auto* host = aExternalImage->AsRenderDXGIYCbCrTextureHost()) {
    if (!host->EnsureD3D11Texture2D(GetDevice())) {
      return;
    }

    layer = new DataTextureSourceD3D11(GetDevice(), SurfaceFormat::A8,
                                       host->GetD3D11Texture2D(0));
    RefPtr<DataTextureSourceD3D11> u = new DataTextureSourceD3D11(
        GetDevice(), SurfaceFormat::A8, host->GetD3D11Texture2D(1));
    layer->SetNextSibling(u);
    RefPtr<DataTextureSourceD3D11> v = new DataTextureSourceD3D11(
        GetDevice(), SurfaceFormat::A8, host->GetD3D11Texture2D(2));
    u->SetNextSibling(v);

    const auto yuv = FromYUVRangedColorSpace(host->GetYUVColorSpace());
    texturedEffect =
        new EffectYCbCr(layer, yuv.space, yuv.range, host->GetColorDepth(),
                        aFrameSurface.mFilter);
    size = host->GetSize(0);
    host->LockInternal();
  }

  gfx::Rect drawRect(0, 0, size.width, size.height);

  EffectChain effect;
  effect.mPrimaryEffect = texturedEffect;
  mCompositor->DrawQuad(drawRect, aFrameSurface.mClipRect, effect, 1.0,
                        aFrameSurface.mTransform, drawRect);

  if (auto* host = aExternalImage->AsRenderDXGITextureHost()) {
    host->Unlock();
  } else if (auto* host = aExternalImage->AsRenderDXGIYCbCrTextureHost()) {
    host->Unlock();
  }
}

void RenderCompositorD3D11SWGL::Pause() {}

bool RenderCompositorD3D11SWGL::Resume() { return true; }

GLenum RenderCompositorD3D11SWGL::IsContextLost(bool aForce) {
  // CompositorD3D11 uses ID3D11Device for composite. The device status needs to
  // be checked.
  auto reason = GetDevice()->GetDeviceRemovedReason();
  switch (reason) {
    case S_OK:
      return LOCAL_GL_NO_ERROR;
    case DXGI_ERROR_DEVICE_REMOVED:
    case DXGI_ERROR_DRIVER_INTERNAL_ERROR:
      NS_WARNING("Device reset due to system / different device");
      return LOCAL_GL_INNOCENT_CONTEXT_RESET_ARB;
    case DXGI_ERROR_DEVICE_HUNG:
    case DXGI_ERROR_DEVICE_RESET:
    case DXGI_ERROR_INVALID_CALL:
      gfxCriticalError() << "Device reset due to WR device: "
                         << gfx::hexa(reason);
      return LOCAL_GL_GUILTY_CONTEXT_RESET_ARB;
    default:
      gfxCriticalError() << "Device reset with WR device unexpected reason: "
                         << gfx::hexa(reason);
      return LOCAL_GL_UNKNOWN_CONTEXT_RESET_ARB;
  }
}

UniquePtr<RenderCompositorLayersSWGL::Surface>
RenderCompositorD3D11SWGL::DoCreateSurface(wr::DeviceIntSize aTileSize,
                                           bool aIsOpaque) {
  return MakeUnique<SurfaceD3D11SWGL>(aTileSize, aIsOpaque);
}

SurfaceD3D11SWGL::SurfaceD3D11SWGL(wr::DeviceIntSize aTileSize, bool aIsOpaque)
    : Surface(aTileSize, aIsOpaque) {}

RenderCompositorD3D11SWGL::TileD3D11::TileD3D11(
    layers::DataTextureSourceD3D11* aTexture, ID3D11Texture2D* aStagingTexture,
    DataSourceSurface* aDataSourceSurface, Surface* aOwner,
    RenderCompositorD3D11SWGL* aRenderCompositor)
    : Tile(),
      mTexture(aTexture),
      mStagingTexture(aStagingTexture),
      mSurface(aDataSourceSurface),
      mOwner(aOwner->AsSurfaceD3D11SWGL()),
      mRenderCompositor(aRenderCompositor) {}

bool RenderCompositorD3D11SWGL::TileD3D11::Map(wr::DeviceIntRect aDirtyRect,
                                               wr::DeviceIntRect aValidRect,
                                               void** aData, int32_t* aStride) {
  const UploadMode uploadMode = mRenderCompositor->GetUploadMode();
  const gfx::IntSize tileSize = mOwner->TileSize();

  if (!IsValid()) {
    return false;
  }

  // Check if this tile's upload method matches what we're using for this frame,
  // and if not then reallocate to fix it. Do this before we copy the struct
  // into mCurrentTile.
  if (uploadMode == Upload_Immediate) {
    if (mStagingTexture) {
      MOZ_ASSERT(!mSurface);
      mStagingTexture = nullptr;
      mSurface = mRenderCompositor->CreateStagingSurface(tileSize);
    }
  } else {
    if (mSurface) {
      MOZ_ASSERT(!mStagingTexture);
      mSurface = nullptr;
      mStagingTexture = mRenderCompositor->CreateStagingTexture(tileSize);
    }
  }

  mRenderCompositor->mCurrentStagingTexture = mStagingTexture;
  mRenderCompositor->mCurrentStagingTextureIsTemp = false;

  if (uploadMode == Upload_Immediate) {
    DataSourceSurface::MappedSurface map;
    if (!mSurface->Map(DataSourceSurface::READ_WRITE, &map)) {
      return false;
    }

    *aData = map.mData + aValidRect.min.y * map.mStride + aValidRect.min.x * 4;
    *aStride = map.mStride;
    // Ensure our mapped data is accessible by writing to the beginning and end
    // of the dirty region. See bug 171519
    uint32_t* probeData = (uint32_t*)map.mData +
                          aDirtyRect.min.y * (map.mStride / 4) +
                          aDirtyRect.min.x;
    *probeData = 0;
    uint32_t* probeDataEnd = (uint32_t*)map.mData +
                             (aDirtyRect.max.y - 1) * (map.mStride / 4) +
                             (aDirtyRect.max.x - 1);
    *probeDataEnd = 0;

    mValidRect = gfx::Rect(aValidRect.min.x, aValidRect.min.y,
                           aValidRect.width(), aValidRect.height());
    return true;
  }

  if (!mRenderCompositor->mCurrentStagingTexture) {
    return false;
  }

  RefPtr<ID3D11DeviceContext> context;
  mRenderCompositor->GetDevice()->GetImmediateContext(getter_AddRefs(context));

  D3D11_MAPPED_SUBRESOURCE mappedSubresource;

  bool shouldBlock = uploadMode == Upload_Staging;

  HRESULT hr = context->Map(
      mRenderCompositor->mCurrentStagingTexture, 0, D3D11_MAP_READ_WRITE,
      shouldBlock ? 0 : D3D11_MAP_FLAG_DO_NOT_WAIT, &mappedSubresource);
  if (hr == DXGI_ERROR_WAS_STILL_DRAWING) {
    // mCurrentTile is a copy of the real tile data, so we can just replace the
    // staging one with a temporary for this draw. The staging texture for the
    // real tile remains untouched, so we'll go back to using that for future
    // frames and discard the new one. In the future we could improve this by
    // having a pool of shared staging textures for all the tiles.

    // Mark the tile as having a temporary staging texture.
    mRenderCompositor->mCurrentStagingTextureIsTemp = true;

    // Try grabbing a texture from the staging pool and see if we can use that.
    if (uploadMode == Upload_StagingPooled && mOwner->mStagingPool.Length()) {
      mRenderCompositor->mCurrentStagingTexture =
          mOwner->mStagingPool.ElementAt(0);
      mOwner->mStagingPool.RemoveElementAt(0);
      hr = context->Map(mRenderCompositor->mCurrentStagingTexture, 0,
                        D3D11_MAP_READ_WRITE, D3D11_MAP_FLAG_DO_NOT_WAIT,
                        &mappedSubresource);

      // If that failed, put it back into the pool (but at the end).
      if (hr == DXGI_ERROR_WAS_STILL_DRAWING) {
        mOwner->mStagingPool.AppendElement(
            mRenderCompositor->mCurrentStagingTexture);
      }
    }

    // No staging textures, or we tried one and it was busy. Allocate a brand
    // new one instead.
    if (hr == DXGI_ERROR_WAS_STILL_DRAWING) {
      mRenderCompositor->mCurrentStagingTexture =
          mRenderCompositor->CreateStagingTexture(tileSize);
      if (!mRenderCompositor->mCurrentStagingTexture) {
        return false;
      }
      hr = context->Map(mRenderCompositor->mCurrentStagingTexture, 0,
                        D3D11_MAP_READ_WRITE, 0, &mappedSubresource);
    }
  }
  if (!SUCCEEDED(hr)) {
    gfxCriticalError() << "Failed to map tile: " << gfx::hexa(hr);
    // This is only expected to fail if we hit a device reset.
    MOZ_RELEASE_ASSERT(
        mRenderCompositor->GetDevice()->GetDeviceRemovedReason() != S_OK);
    return false;
  }

  // aData is expected to contain a pointer to the first pixel within the valid
  // rect, so take the mapped resource's data (which covers the full tile size)
  // and offset it by the top/left of the valid rect.
  *aData = (uint8_t*)mappedSubresource.pData +
           aValidRect.min.y * mappedSubresource.RowPitch + aValidRect.min.x * 4;
  *aStride = mappedSubresource.RowPitch;

  // Ensure our mapped data is accessible by writing to the beginning and end
  // of the dirty region. See bug 171519
  uint32_t* probeData = (uint32_t*)mappedSubresource.pData +
                        aDirtyRect.min.y * (mappedSubresource.RowPitch / 4) +
                        aDirtyRect.min.x;
  *probeData = 0;
  uint32_t* probeDataEnd =
      (uint32_t*)mappedSubresource.pData +
      (aDirtyRect.max.y - 1) * (mappedSubresource.RowPitch / 4) +
      (aDirtyRect.max.x - 1);
  *probeDataEnd = 0;

  // Store the new valid rect, so that we can composite only those pixels
  mValidRect = gfx::Rect(aValidRect.min.x, aValidRect.min.y, aValidRect.width(),
                         aValidRect.height());

  return true;
}

void RenderCompositorD3D11SWGL::TileD3D11::Unmap(
    const gfx::IntRect& aDirtyRect) {
  const UploadMode uploadMode = mRenderCompositor->GetUploadMode();

  if (!IsValid()) {
    return;
  }

  if (mSurface) {
    MOZ_ASSERT(uploadMode == Upload_Immediate);
    mSurface->Unmap();
    nsIntRegion dirty(aDirtyRect);
    // This uses UpdateSubresource, which blocks, so is likely implemented as a
    // memcpy into driver owned memory, followed by an async upload. The staging
    // method should avoid this extra copy, and is likely to be faster usually.
    // We could possible do this call on a background thread so that sw-wr can
    // start drawing the next tile while the memcpy is in progress.
    mTexture->Update(mSurface, &dirty);
    return;
  }

  if (!mRenderCompositor->mCurrentStagingTexture) {
    return;
  }

  RefPtr<ID3D11DeviceContext> context;
  mRenderCompositor->GetDevice()->GetImmediateContext(getter_AddRefs(context));

  context->Unmap(mRenderCompositor->mCurrentStagingTexture, 0);

  D3D11_BOX box;
  box.front = 0;
  box.back = 1;
  box.left = aDirtyRect.X();
  box.top = aDirtyRect.Y();
  box.right = aDirtyRect.XMost();
  box.bottom = aDirtyRect.YMost();

  context->CopySubresourceRegion(
      mTexture->GetD3D11Texture(), 0, aDirtyRect.x, aDirtyRect.y, 0,
      mRenderCompositor->mCurrentStagingTexture, 0, &box);

  // If we allocated a temp staging texture for this tile, and we're running
  // in pooled mode, then consider adding it to the pool for later.
  if (mRenderCompositor->mCurrentStagingTextureIsTemp &&
      uploadMode == Upload_StagingPooled) {
    static const uint32_t kMaxPoolSize = 5;
    if (mOwner->mStagingPool.Length() < kMaxPoolSize) {
      mOwner->mStagingPool.AppendElement(
          mRenderCompositor->mCurrentStagingTexture);
    }
  }

  mRenderCompositor->mCurrentStagingTexture = nullptr;
  mRenderCompositor->mCurrentStagingTextureIsTemp = false;
}

bool RenderCompositorD3D11SWGL::TileD3D11::IsValid() { return !!mTexture; }

already_AddRefed<ID3D11Texture2D>
RenderCompositorD3D11SWGL::CreateStagingTexture(const gfx::IntSize aSize) {
  CD3D11_TEXTURE2D_DESC desc(DXGI_FORMAT_B8G8R8A8_UNORM, aSize.width,
                             aSize.height, 1, 1);

  desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ;
  desc.Usage = D3D11_USAGE_STAGING;
  desc.BindFlags = 0;

  RefPtr<ID3D11Texture2D> cpuTexture;
  DebugOnly<HRESULT> hr =
      GetDevice()->CreateTexture2D(&desc, nullptr, getter_AddRefs(cpuTexture));
  MOZ_ASSERT(SUCCEEDED(hr));
  if (!cpuTexture) {
    gfxCriticalNote << "Failed to create StagingTexture: " << aSize;
    RenderThread::Get()->HandleWebRenderError(WebRenderError::NEW_SURFACE);
  }
  return cpuTexture.forget();
}

already_AddRefed<DataSourceSurface>
RenderCompositorD3D11SWGL::CreateStagingSurface(const gfx::IntSize aSize) {
  return Factory::CreateDataSourceSurface(aSize, SurfaceFormat::B8G8R8A8);
}

UniquePtr<RenderCompositorLayersSWGL::Tile>
RenderCompositorD3D11SWGL::DoCreateTile(Surface* aSurface) {
  MOZ_RELEASE_ASSERT(aSurface);

  const auto tileSize = aSurface->TileSize();

  if (mUploadMode == Upload_Immediate) {
    RefPtr<DataTextureSourceD3D11> source =
        new DataTextureSourceD3D11(gfx::SurfaceFormat::B8G8R8A8, mCompositor,
                                   layers::TextureFlags::NO_FLAGS);
    RefPtr<DataSourceSurface> surf = CreateStagingSurface(tileSize);
    return MakeUnique<TileD3D11>(source, nullptr, surf, aSurface, this);
  }

  MOZ_ASSERT(mUploadMode == Upload_Staging ||
             mUploadMode == Upload_StagingNoBlock ||
             mUploadMode == Upload_StagingPooled);

  CD3D11_TEXTURE2D_DESC desc(DXGI_FORMAT_B8G8R8A8_UNORM, tileSize.width,
                             tileSize.height, 1, 1);

  RefPtr<ID3D11Texture2D> texture;
  DebugOnly<HRESULT> hr =
      GetDevice()->CreateTexture2D(&desc, nullptr, getter_AddRefs(texture));
  MOZ_ASSERT(SUCCEEDED(hr));
  if (!texture) {
    gfxCriticalNote << "Failed to allocate Texture2D: " << aSurface->TileSize();
    RenderThread::Get()->HandleWebRenderError(WebRenderError::NEW_SURFACE);
    return MakeUnique<TileD3D11>(nullptr, nullptr, nullptr, aSurface, this);
  }

  RefPtr<DataTextureSourceD3D11> source = new DataTextureSourceD3D11(
      GetDevice(), gfx::SurfaceFormat::B8G8R8A8, texture);

  RefPtr<ID3D11Texture2D> cpuTexture = CreateStagingTexture(tileSize);
  return MakeUnique<TileD3D11>(source, cpuTexture, nullptr, aSurface, this);
}

bool RenderCompositorD3D11SWGL::MaybeReadback(
    const gfx::IntSize& aReadbackSize, const wr::ImageFormat& aReadbackFormat,
    const Range<uint8_t>& aReadbackBuffer, bool* aNeedsYFlip) {
  MOZ_ASSERT(aReadbackFormat == wr::ImageFormat::BGRA8);

  auto stride =
      aReadbackSize.width * gfx::BytesPerPixel(SurfaceFormat::B8G8R8A8);
  RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateDrawTargetForData(
      gfx::BackendType::SKIA, &aReadbackBuffer[0], aReadbackSize, stride,
      SurfaceFormat::B8G8R8A8, false);
  if (!dt) {
    return false;
  }

  GetCompositorD3D11()->Readback(dt);
  return true;
}

}  // namespace wr
}  // namespace mozilla