/**
 * Looking Glass
 * Copyright © 2017-2025 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "d12.h"

#include "interface/capture.h"

#include "common/array.h"
#include "common/debug.h"
#include "common/windebug.h"
#include "common/option.h"
#include "common/rects.h"
#include "common/vector.h"
#include "common/display.h"
#include "com_ref.h"

#include "backend.h"
#include "effects.h"
#include "command_group.h"

#include <dxgi.h>
#include <dxgi1_3.h>
#include <dxgi1_6.h>
#include <d3dcommon.h>

// definitions
struct D12Interface
{
  HMODULE d3d12;

  IDXGIFactory2      ** factory;
  ID3D12Device3      ** device;

  DISPLAYCONFIG_PATH_INFO displayPathInfo;

  ID3D12CommandQueue ** copyQueue;
  ID3D12CommandQueue ** computeQueue;
  D12CommandGroup       copyCommand;
  D12CommandGroup       computeCommand;

  void        * ivshmemBase;
  ID3D12Heap ** ivshmemHeap;
  bool          indirectCopy;

  CaptureGetPointerBuffer  getPointerBufferFn;
  CapturePostPointerBuffer postPointerBufferFn;

  D12Backend * backend;
  Vector       effects;
  bool         effectsActive;

  // capture format tracking
  D12FrameFormat captureFormat;
  unsigned       formatVer;
  unsigned       pitch;
  unsigned       bpp;

  // output format tracking
  D12FrameFormat dstFormat;

  // prior frame dirty rects
  RECT      dirtyRects[D12_MAX_DIRTY_RECTS];
  unsigned  nbDirtyRects;

  // options
  bool debug;
  bool trackDamage;

  unsigned frameBufferCount;
  // must be last
  struct
  {
    // the size of the frame buffer
    unsigned          size;
    // the frame buffer it itself
    FrameBuffer    *  frameBuffer;
    // the resource backed by the framebuffer
    ID3D12Resource ** resource;
    // the mapped resource if indirectCopy is in use
    void           *  map;
  }
  frameBuffers[0];
};

// gloabls

struct DX12 DX12 = {0};
ComScope * d12_comScope = NULL;

// defines

// locals

static struct D12Interface * this = NULL;

// forwards

static bool d12_enumerateDevices(
  IDXGIFactory2 ** factory,
  IDXGIAdapter1 ** adapter,
  IDXGIOutput   ** output);

static bool d12_heapTest(ID3D12Device3 * device, ID3D12Heap * heap);

static ID3D12Resource * d12_frameBufferToResource(
  unsigned      frameBufferIndex,
  FrameBuffer * frameBuffer,
  unsigned size,
  void ** map);

// implementation

static const char * d12_getName(void)
{
  return "D12";
}

static void d12_initOptions(void)
{
  struct Option options[] =
  {
    {
      .module         = "d12",
      .name           = "adapter",
      .description    = "The name of the adapter to capture",
      .type           = OPTION_TYPE_STRING,
      .value.x_string = NULL
    },
    {
      .module         = "d12",
      .name           = "output",
      .description    = "The name of the adapter's output to capture",
      .type           = OPTION_TYPE_STRING,
      .value.x_string = NULL
    },
    {
      .module         = "d12",
      .name           = "trackDamage",
      .description    = "Perform damage-aware copies (saves bandwidth)",
      .type           = OPTION_TYPE_BOOL,
      .value.x_bool   = true
    },
    {
      .module         = "d12",
      .name           = "debug",
      .description    = "Enable DirectX12 debugging and validation (SLOW!)",
      .type           = OPTION_TYPE_BOOL,
      .value.x_bool   = false
    },
    {
      .module         = "d12",
      .name           = "indirectCopy",
      .description    = "Force the less optimal indirect copy method",
      .type           = OPTION_TYPE_BOOL,
      .value.x_bool   = false
    },
    {0}
  };

  option_register(options);

  for(const D12Effect ** effect = D12Effects; *effect; ++effect)
    d12_effectInitOptions(*effect);
}

static bool d12_create(
  CaptureGetPointerBuffer  getPointerBufferFn,
  CapturePostPointerBuffer postPointerBufferFn,
  unsigned                 frameBuffers)
{
  this = calloc(1, offsetof(struct D12Interface, frameBuffers) +
    sizeof(this->frameBuffers[0]) * frameBuffers);
  if (!this)
  {
    DEBUG_ERROR("failed to allocate D12Interface struct");
    return false;
  }

  this->debug        = option_get_bool("d12", "debug"       );
  this->trackDamage  = option_get_bool("d12", "trackDamage" );
  this->indirectCopy = option_get_bool("d12", "indirectCopy");

  DEBUG_INFO(
    "debug:%d trackDamage:%d indirectCopy:%d",
    this->debug, this->trackDamage, this->indirectCopy);

  this->d3d12 = LoadLibrary("d3d12.dll");
  if (!this->d3d12)
  {
    DEBUG_ERROR("failed to load d3d12.dll");
    free(this);
    return false;
  }

  DX12.D3D12CreateDevice = (typeof(DX12.D3D12CreateDevice))
    GetProcAddress(this->d3d12, "D3D12CreateDevice");

  DX12.D3D12GetDebugInterface = (typeof(DX12.D3D12GetDebugInterface))
    GetProcAddress(this->d3d12, "D3D12GetDebugInterface");

  DX12.D3D12SerializeVersionedRootSignature =
    (typeof(DX12.D3D12SerializeVersionedRootSignature))
      GetProcAddress(this->d3d12, "D3D12SerializeVersionedRootSignature");

  if (!DX12.D3D12CreateDevice      ||
      !DX12.D3D12GetDebugInterface ||
      !DX12.D3D12SerializeVersionedRootSignature)
  {
    DEBUG_ERROR("Failed to get required exports from d3d12.dll");
    CloseHandle(this->d3d12);
    free(this);
    return false;
  }

  this->getPointerBufferFn  = getPointerBufferFn;
  this->postPointerBufferFn = postPointerBufferFn;

  if (!d12_backendCreate(&D12Backend_DD, &this->backend, frameBuffers))
  {
    DEBUG_ERROR("backend \"%s\" failed to create", this->backend->codeName);
    CloseHandle(this->d3d12);
    free(this);
    return false;
  }

  this->frameBufferCount = frameBuffers;

  return true;
}

static bool d12_init(void * ivshmemBase, unsigned * alignSize)
{
  bool reInit = false;
  bool result = false;
  comRef_initGlobalScope(100, d12_comScope);
  comRef_scopePush(10);

  // create a DXGI factory
  comRef_defineLocal(IDXGIFactory2, factory);
  DEBUG_TRACE("CreateDXGIFactory2");
  HRESULT hr = CreateDXGIFactory2(
    this->debug ? DXGI_CREATE_FACTORY_DEBUG : 0,
    &IID_IDXGIFactory2,
    (void **)factory);

  if (FAILED(hr))
  {
    DEBUG_WINERROR("Failed to create the DXGI factory", hr);
    goto exit;
  }

  // find the adapter and output we want to use
  comRef_defineLocal(IDXGIAdapter1, adapter);
  comRef_defineLocal(IDXGIOutput  , output );
  if (!d12_enumerateDevices(factory, adapter, output))
    goto exit;

  if (this->debug)
  {
    comRef_defineLocal(ID3D12Debug1, debug);
    hr = DX12.D3D12GetDebugInterface(&IID_ID3D12Debug1, (void **)debug);
    if (FAILED(hr))
    {
      DEBUG_WINERROR("D3D12GetDebugInterface", hr);
      goto exit;
    }

    ID3D12Debug1_EnableDebugLayer(*debug);
    ID3D12Debug1_SetEnableGPUBasedValidation(*debug, TRUE);
    ID3D12Debug1_SetEnableSynchronizedCommandQueueValidation(*debug, TRUE);
  }

  // get the display path info
  comRef_defineLocal(IDXGIOutput6, output6);
  hr = IDXGIOutput_QueryInterface(*output, &IID_IDXGIOutput6, (void **)output6);
  if (FAILED(hr))
  {
    DEBUG_WINERROR("Failed to obtain the IDXGIOutput6 interface", hr);
    goto exit;
  }

  DXGI_OUTPUT_DESC1 desc1;
  IDXGIOutput6_GetDesc1(*output6, &desc1);
  if (!display_getPathInfo(desc1.Monitor, &this->displayPathInfo))
  {
    DEBUG_ERROR("Failed to get the display path info");
    goto exit;
  }

  // create the D3D12 device
  comRef_defineLocal(ID3D12Device3, device);
  DEBUG_TRACE("D3D12CreateDevice");
  hr = DX12.D3D12CreateDevice(
    (IUnknown *)*adapter,
    D3D_FEATURE_LEVEL_12_0,
    &IID_ID3D12Device3,
    (void **)device);

  if (FAILED(hr))
  {
    DEBUG_WINERROR("Failed to create the DirectX12 device", hr);
    goto exit;
  }

  D3D12_COMMAND_QUEUE_DESC copyQueueDesc =
  {
    .Type     = D3D12_COMMAND_LIST_TYPE_COPY,
    .Priority = D3D12_COMMAND_QUEUE_PRIORITY_HIGH,
    .Flags    = D3D12_COMMAND_QUEUE_FLAG_NONE
  };

  comRef_defineLocal(ID3D12CommandQueue, copyQueue);
  DEBUG_TRACE("D3D12Device3_CreateCommandQueue");
  hr = ID3D12Device3_CreateCommandQueue(
    *device, &copyQueueDesc, &IID_ID3D12CommandQueue, (void **)copyQueue);
  if (FAILED(hr))
  {
    DEBUG_WINERROR("Failed to create ID3D12CommandQueue (copy)", hr);
    goto exit;
  }
  ID3D12CommandQueue_SetName(*copyQueue, L"Copy");

  // create the compute queue
  D3D12_COMMAND_QUEUE_DESC computeQueueDesc =
  {
    .Type     = D3D12_COMMAND_LIST_TYPE_COMPUTE,
    .Priority = D3D12_COMMAND_QUEUE_PRIORITY_HIGH,
    .Flags    = D3D12_COMMAND_QUEUE_FLAG_NONE
  };

  comRef_defineLocal(ID3D12CommandQueue, computeQueue);
  DEBUG_TRACE("D3D12Device3_CreateCommandQueue");
  hr = ID3D12Device3_CreateCommandQueue(
    *device, &computeQueueDesc, &IID_ID3D12CommandQueue, (void **)computeQueue);
  if (FAILED(hr))
  {
    DEBUG_WINERROR("Failed to create the ID3D12CommandQueue (compute)", hr);
    goto exit;
  }
  ID3D12CommandQueue_SetName(*computeQueue, L"Compute");

  DEBUG_TRACE("d12_commandGroupCreate Copy");
  if (!d12_commandGroupCreate(
    *device, D3D12_COMMAND_LIST_TYPE_COPY, &this->copyCommand, L"Copy"))
    goto exit;

  DEBUG_TRACE("d12_commandGroupCreate Compute");
  if (!d12_commandGroupCreate(
    *device, D3D12_COMMAND_LIST_TYPE_COMPUTE, &this->computeCommand, L"Compute"))
    goto exit;

  comRef_defineLocal(ID3D12Heap, ivshmemHeap);
  if (!this->indirectCopy)
  {
    // Create the IVSHMEM heap
    this->ivshmemBase = ivshmemBase;
    DEBUG_TRACE("ID3D12Device3_OpenExistingHeapFromAddress");
    hr = ID3D12Device3_OpenExistingHeapFromAddress(
      *device, ivshmemBase, &IID_ID3D12Heap, (void **)ivshmemHeap);
    if (FAILED(hr))
    {
      DEBUG_WINERROR("Failed to open the framebuffer as a D3D12Heap", hr);
      goto exit;
    }

    // Adjust the alignSize based on the required heap alignment
    D3D12_HEAP_DESC heapDesc = ID3D12Heap_GetDesc(*ivshmemHeap);
    *alignSize = heapDesc.Alignment;

    /* Ensure we can create resources in the ivshmem heap
     * NOTE: It is safe to do this as the application has not yet setup the KVMFR
     * headers, so we can just attempt to create a resource at the start of the
     * heap without corrupting anything */
    DEBUG_TRACE("d12_heapTest");
    if (!d12_heapTest(*device, *ivshmemHeap))
    {
      this->indirectCopy = true;
      DEBUG_WARN("Unable to create resources in the IVSHMEM heap, "
        "falling back to indirect copy");

      // we need to retry from scratch this failure acts like the GPU crashed
      reInit = true;
      goto exit;
    }
  }

  // initialize the backend
  DEBUG_TRACE("d12_backendInit");
  if (!d12_backendInit(this->backend, this->debug, *device, *adapter, *output,
    this->trackDamage))
    goto exit;

  // create the vector of effects
  vector_create(&this->effects, sizeof(D12Effect *), 0);

  // create all the effects
  for(const D12Effect ** effect = D12Effects; *effect; ++effect)
  {
    D12Effect * instance;
    switch(d12_effectCreate(*effect, &instance, *device, &this->displayPathInfo))
    {
      case D12_EFFECT_STATUS_OK:
        DEBUG_INFO("D12 Created Effect: %s", (*effect)->name);
        vector_push(&this->effects, &instance);
        break;

      case D12_EFFECT_STATUS_BYPASS:
        continue;

      case D12_EFFECT_STATUS_ERROR:
        DEBUG_ERROR("Failed to create effect: %s", (*effect)->name);
        goto exit;
    }
  }

  comRef_toGlobal(this->factory     , factory      );
  comRef_toGlobal(this->device      , device       );
  comRef_toGlobal(this->copyQueue   , copyQueue    );
  comRef_toGlobal(this->computeQueue, computeQueue );

  if (!this->indirectCopy)
    comRef_toGlobal(this->ivshmemHeap, ivshmemHeap);

  DEBUG_TRACE("Init success");
  result = true;

exit:
  comRef_scopePop();
  if (!result)
  {
    DEBUG_TRACE("Init failed");
    D12Effect * effect;
    vector_forEach(effect, &this->effects)
      d12_effectFree(&effect);
    vector_destroy(&this->effects);
    comRef_freeScope(&d12_comScope);

    if (reInit)
    {
      DEBUG_INFO("Attempting to reinitialize with indirectCopy=1");
      return d12_init(ivshmemBase, alignSize);
    }
  }

  return result;
}

static void d12_stop(void)
{
}

static bool d12_deinit(void)
{
  bool result = true;

  if (this->indirectCopy)
  {
    for(int i = 0; i < this->frameBufferCount; ++i)
      if (this->frameBuffers[i].map)
        ID3D12Resource_Unmap(*this->frameBuffers[i].resource, 0, NULL);
  }

  D12Effect * effect;
  vector_forEach(effect, &this->effects)
    d12_effectFree(&effect);
  vector_destroy(&this->effects);

  DEBUG_TRACE("Backend deinit");
  if (!d12_backendDeinit(this->backend))
    result = false;

  DEBUG_TRACE("commandGroupFree");
  d12_commandGroupFree(&this->copyCommand   );
  d12_commandGroupFree(&this->computeCommand);

  DEBUG_TRACE("comRef_freeScope");
  IDXGIFactory2 * factory = *this->factory;
  IDXGIFactory2_AddRef(factory);
  comRef_freeScope(&d12_comScope);
  if (IDXGIFactory2_Release(factory) != 0)
    DEBUG_WARN("MEMORY LEAK");

  // zero the framebuffers
  memset(this->frameBuffers, 0,
    sizeof(*this->frameBuffers) * this->frameBufferCount);

  /* zero the formats so we properly reinit otherwise we wont detect the format
  change and setup the effect chain */
  memset(&this->captureFormat, 0, sizeof(this->captureFormat));
  memset(&this->dstFormat    , 0, sizeof(this->dstFormat    ));

  /* dirty rect history is no longer valid */
  this->nbDirtyRects = 0;

  return result;
}

static void d12_free(void)
{
  DEBUG_TRACE("d12_backendFree");
  d12_backendFree(&this->backend);
  FreeLibrary(this->d3d12);
  free(this);
  this = NULL;
}

static CaptureResult d12_capture(
  unsigned frameBufferIndex, FrameBuffer * frameBuffer)
{
  DEBUG_TRACE("d12_backendCapture");
  return d12_backendCapture(this->backend, frameBufferIndex);
}

static CaptureResult d12_waitFrame(unsigned frameBufferIndex,
  CaptureFrame * frame, const size_t maxFrameSize)
{
  CaptureResult result = CAPTURE_RESULT_ERROR;
  comRef_scopePush(1);

  D12FrameDesc desc;

  comRef_defineLocal(ID3D12Resource, src);
  DEBUG_TRACE("d12_backendFetch");
  *src = d12_backendFetch(this->backend, frameBufferIndex, &desc);
  if (!*src)
  {
    DEBUG_ERROR("D12 backend failed to produce an expected frame: %u",
      frameBufferIndex);
    goto exit;
  }

  D12FrameFormat srcFormat =
  {
    .desc       = ID3D12Resource_GetDesc(*src),
    .colorSpace = desc.colorSpace,
    .width      = srcFormat.desc.Width,
    .height     = srcFormat.desc.Height
  };

  switch(srcFormat.desc.Format)
  {
    case DXGI_FORMAT_B8G8R8A8_UNORM:
      srcFormat.format = CAPTURE_FMT_BGRA;
      break;

    case DXGI_FORMAT_R8G8B8A8_UNORM:
      srcFormat.format = CAPTURE_FMT_RGBA;
      break;

    case DXGI_FORMAT_R10G10B10A2_UNORM:
      srcFormat.format = CAPTURE_FMT_RGBA10;
      break;

    case DXGI_FORMAT_R16G16B16A16_FLOAT:
      srcFormat.format = CAPTURE_FMT_RGBA16F;
      break;

    default:
      DEBUG_ERROR("Unsupported source format");
      goto exit;
  }

  // if the input format changed, reconfigure the effects
  if (srcFormat.desc.Width  == 0 ||
      srcFormat.desc.Width  != this->captureFormat.desc.Width  ||
      srcFormat.desc.Height != this->captureFormat.desc.Height ||
      srcFormat.desc.Format != this->captureFormat.desc.Format ||
      srcFormat.colorSpace  != this->captureFormat.colorSpace)
  {
    DEBUG_TRACE("Capture format changed");

    D12FrameFormat dstFormat = this->dstFormat;
    this->captureFormat      = srcFormat;
    this->effectsActive      = false;

    D12Effect * effect;
    D12FrameFormat curFormat = srcFormat;
    vector_forEach(effect, &this->effects)
    {
      dstFormat = curFormat;
      switch(d12_effectSetFormat(effect, *this->device, &curFormat, &dstFormat))
      {
        case D12_EFFECT_STATUS_OK:
          this->effectsActive = true;
          curFormat           = dstFormat;
          effect->enabled     = true;
          DEBUG_INFO("D12 Effect Active: %s", effect->name);
          break;

        case D12_EFFECT_STATUS_ERROR:
          DEBUG_ERROR("Failed to set the effect input format");
          goto exit;

        case D12_EFFECT_STATUS_BYPASS:
          effect->enabled = false;
          break;
      }
    }

    // if the output format changed
    if (dstFormat.desc.Width  != this->dstFormat.desc.Width  ||
        dstFormat.desc.Height != this->dstFormat.desc.Height ||
        dstFormat.desc.Format != this->dstFormat.desc.Format ||
        dstFormat.colorSpace  != this->dstFormat.colorSpace  ||
        dstFormat.width       != this->dstFormat.width       ||
        dstFormat.height      != this->dstFormat.height      ||
        dstFormat.format      != this->dstFormat.format)
    {
      DEBUG_TRACE("Output format changed");
      ++this->formatVer;
      this->dstFormat = dstFormat;
    }
  }

  D3D12_PLACED_SUBRESOURCE_FOOTPRINT layout;
  ID3D12Device3_GetCopyableFootprints(*this->device,
    &this->dstFormat.desc,
    0       , // FirstSubresource
    1       , // NumSubresources
    0       , // BaseOffset,
    &layout , // pLayouts
    NULL    , // pNumRows,
    NULL    , // pRowSizeInBytes,
    NULL);  // pTotalBytes
  this->pitch = layout.Footprint.RowPitch;

  const unsigned maxRows = maxFrameSize / layout.Footprint.RowPitch;
  this->bpp = this->dstFormat.format == CAPTURE_FMT_RGBA16F ? 8 : 4;

  frame->formatVer        = this->formatVer;
  frame->screenWidth      = srcFormat.width;
  frame->screenHeight     = srcFormat.height;
  frame->dataWidth        = this->dstFormat.desc.Width;
  frame->dataHeight       = min(maxRows, this->dstFormat.desc.Height);
  frame->frameWidth       = this->dstFormat.width;
  frame->frameHeight      = this->dstFormat.height;
  frame->truncated        = maxRows < this->dstFormat.desc.Height;
  frame->pitch            = this->pitch;
  frame->stride           = this->pitch / this->bpp;
  frame->format           = this->dstFormat.format;
  frame->hdr              = this->dstFormat.colorSpace ==
    DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
  frame->hdrPQ            = false;
  frame->rotation         = desc.rotation;

  D12Effect * effect;
  vector_forEach(effect, &this->effects)
    if (effect->enabled)
      d12_effectAdjustDamage(effect, desc.dirtyRects, &desc.nbDirtyRects);

  {
    // create a clean list of rects
    FrameDamageRect allRects[desc.nbDirtyRects];
    unsigned count = 0;
    for(const RECT * rect = desc.dirtyRects;
      rect < desc.dirtyRects + desc.nbDirtyRects; ++rect)
      allRects[count++] = (FrameDamageRect){
        .x      = rect->left,
        .y      = rect->top,
        .width  = (rect->right  - rect->left),
        .height = (rect->bottom - rect->top)
      };

    count = rectsMergeOverlapping(allRects, count);

    // if there are too many rects
    if (unlikely(count > ARRAY_LENGTH(frame->damageRects)))
      frame->damageRectsCount = 0;
    else
    {
      // send the list of dirty rects for this frame
      frame->damageRectsCount = count;
      memcpy(frame->damageRects, allRects, sizeof(*allRects) * count);
    }
  }

  result = CAPTURE_RESULT_OK;

exit:
  comRef_scopePop();
  return result;
}

static CaptureResult d12_getFrame(unsigned frameBufferIndex,
  FrameBuffer * frameBuffer, const size_t maxFrameSize)
{
  CaptureResult result = CAPTURE_RESULT_ERROR;
  comRef_scopePush(3);

  D12FrameDesc desc;

  comRef_defineLocal(ID3D12Resource, src);
  DEBUG_TRACE("d12_backendFetch");
  *src = d12_backendFetch(this->backend, frameBufferIndex, &desc);
  unsigned rectCount = 0;
  FrameDamageRect allRects[this->nbDirtyRects + desc.nbDirtyRects];

  if (!*src)
  {
    DEBUG_ERROR("D12 backend failed to produce an expected frame: %u",
      frameBufferIndex);
    goto exit;
  }

  void * map;
  comRef_defineLocal(ID3D12Resource, dst)
  DEBUG_TRACE("d12_frameBufferToResource");
  *dst = d12_frameBufferToResource(frameBufferIndex, frameBuffer, maxFrameSize,
    &map);
  if (!*dst)
    goto exit;

  // place a fence into the queue
  DEBUG_TRACE("d12_backendSync");
  result = d12_backendSync(this->backend,
    this->effectsActive ? *this->computeQueue : *this->copyQueue);

  if (result != CAPTURE_RESULT_OK)
    goto exit;

  ID3D12Resource * next = *src;
  D12Effect * effect;
  vector_forEach(effect, &this->effects)
  {
    if (!effect->enabled)
      continue;

    DEBUG_TRACE("d12_effectRun: %s", effect->name);
    next = d12_effectRun(effect,
      *this->device,
      *this->computeCommand.gfxList,
      next,
      desc.dirtyRects,
      &desc.nbDirtyRects);
  }

  // copy into the framebuffer resource
  D3D12_TEXTURE_COPY_LOCATION srcLoc =
  {
    .pResource        = next,
    .Type             = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX,
    .SubresourceIndex = 0
  };

  D3D12_TEXTURE_COPY_LOCATION dstLoc =
  {
    .pResource       = *dst,
    .Type            = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT,
    .PlacedFootprint =
    {
      .Offset = 0,
      .Footprint =
      {
        .Format   = this->dstFormat.desc.Format,
        .Width    = this->dstFormat.desc.Width,
        .Height   = this->dstFormat.desc.Height,
        .Depth    = 1,
        .RowPitch = this->pitch
      }
    }
  };

  // if full frame damage
  if (desc.nbDirtyRects == 0)
  {
    DEBUG_TRACE("Full frame damage");
    this->nbDirtyRects = 0;
    ID3D12GraphicsCommandList_CopyTextureRegion(
      *this->copyCommand.gfxList, &dstLoc, 0, 0, 0, &srcLoc, NULL);
  }
  else
  {
    /* if the prior frame was a full update */
    if (this->nbDirtyRects == 0)
    {
      DEBUG_TRACE("Full frame update");

      /* the prior frame was fully damaged, we must update everything */
      ID3D12GraphicsCommandList_CopyTextureRegion(
        *this->copyCommand.gfxList, &dstLoc, 0, 0, 0, &srcLoc, NULL);
    }
    else
    {
      DEBUG_TRACE("Damage aware update");

      /* we must update the rects that were dirty in the prior frame also,
       * otherwise the frame in memory will not be consistent when areas need to
       * be redrawn by the client, such as under the cursor */
      for(const RECT * rect = this->dirtyRects;
        rect < this->dirtyRects + this->nbDirtyRects; ++rect)
        allRects[rectCount++] = (FrameDamageRect){
          .x      = rect->left,
          .y      = rect->top,
          .width  = rect->right  - rect->left,
          .height = rect->bottom - rect->top
        };

      /* add the new dirtyRects to the array */
      for(const RECT * rect = desc.dirtyRects;
        rect < desc.dirtyRects + desc.nbDirtyRects; ++rect)
        allRects[rectCount++] = (FrameDamageRect){
          .x      = rect->left,
          .y      = rect->top,
          .width  = rect->right  - rect->left,
          .height = rect->bottom - rect->top
        };

      /* resolve the rects */
      rectCount = rectsMergeOverlapping(allRects, rectCount);

      /* copy all the rects */
      for(FrameDamageRect * rect = allRects; rect < allRects + rectCount; ++rect)
      {
        D3D12_BOX box =
        {
          .left   = rect->x,
          .top    = rect->y,
          .front  = 0,
          .back   = 1,
          .right  = rect->x + rect->width,
          .bottom = rect->y + rect->height
        };

        ID3D12GraphicsCommandList_CopyTextureRegion(
          *this->copyCommand.gfxList, &dstLoc,
          box.left, box.top, 0, &srcLoc, &box);
      }
    }

    /* store the dirty rects for the next frame */
    memcpy(this->dirtyRects, desc.dirtyRects,
      desc.nbDirtyRects * sizeof(*this->dirtyRects));
    this->nbDirtyRects = desc.nbDirtyRects;
  }

  // execute the compute commands
  if (this->effectsActive)
  {
    DEBUG_TRACE("Execute compute commands");
    d12_commandGroupExecute(*this->computeQueue, &this->computeCommand);

    // insert a fence to wait for the compute commands to finish
    DEBUG_TRACE("Fence wait");
    ID3D12CommandQueue_Wait(*this->copyQueue,
      *this->computeCommand.fence, this->computeCommand.fenceValue);
  }

  // execute the copy commands
  DEBUG_TRACE("Execute copy commands");
  d12_commandGroupExecute(*this->copyQueue, &this->copyCommand);

  // wait for the copy to complete
  DEBUG_TRACE("Fence wait");
  d12_commandGroupWait(&this->copyCommand);

  if (this->indirectCopy)
  {
    if (rectCount == 0)
    {
      framebuffer_write(frameBuffer, map,
        this->pitch * this->dstFormat.desc.Height);
    }
    else
    {
      /* copy all the rects */
      rectsBufferToFramebuffer(allRects, rectCount, this->bpp, frameBuffer,
        this->pitch, this->dstFormat.desc.Height, map, this->pitch);

      // signal the frame is complete
      framebuffer_set_write_ptr(frameBuffer,
        this->dstFormat.desc.Height * this->pitch);
    }
  }
  else
  {
    // signal the frame is complete
    framebuffer_set_write_ptr(frameBuffer,
      this->dstFormat.desc.Height * this->pitch);
  }

  // reset the command queues
  if (this->effectsActive)
  {
    DEBUG_TRACE("Reset compute command group");
    if (!d12_commandGroupReset(&this->computeCommand))
      goto exit;
  }

  DEBUG_TRACE("Reset copy command group");
  if (!d12_commandGroupReset(&this->copyCommand))
    goto exit;

  result = CAPTURE_RESULT_OK;

exit:
  comRef_scopePop();
  return result;
}

static bool d12_enumerateDevices(
  IDXGIFactory2 ** factory,
  IDXGIAdapter1 ** adapter,
  IDXGIOutput   ** output)
{
  DXGI_ADAPTER_DESC1 adapterDesc;
  DXGI_OUTPUT_DESC   outputDesc;

  const char * _optAdapter = option_get_string("d12", "adapter");
  const char * _optOutput  = option_get_string("d12", "output" );

  wchar_t * optAdapter = NULL;
  wchar_t * optOutput  = NULL;
  if (_optAdapter)
  {
    optAdapter = malloc((strlen(_optAdapter) + 1) * 2);
    mbstowcs(optAdapter, _optAdapter, strlen(_optAdapter));
  }

  if (_optOutput)
  {
    optOutput = malloc((strlen(_optOutput) + 1) * 2);
    mbstowcs(optOutput, _optOutput, strlen(_optOutput));
  }

  for(
    int i = 0;
    IDXGIFactory2_EnumAdapters1(*factory, i, adapter)
      != DXGI_ERROR_NOT_FOUND;
    ++i, comRef_release(adapter))
  {
    HRESULT hr = IDXGIAdapter1_GetDesc1(*adapter, &adapterDesc);
    if (FAILED(hr))
    {
      DEBUG_WINERROR("Failed to get the device description", hr);
      comRef_release(adapter);
      return false;
    }

    // check for devices without D3D support
    static const UINT blacklist[][2] =
    {
      //VID  , PID
      {0x1414, 0x008c}, // Microsoft Basic Render Driver
      {0x1b36, 0x000d}, // QXL
      {0x1234, 0x1111}  // QEMU Standard VGA
    };

    bool skip = false;
    for(int n = 0; n < ARRAY_LENGTH(blacklist); ++n)
    {
      if (adapterDesc.VendorId == blacklist[n][0] &&
          adapterDesc.DeviceId == blacklist[n][1])
      {
        DEBUG_INFO("Not using unsupported adapter: %ls",
          adapterDesc.Description);
        skip = true;
        break;
      }
    }
    if (skip)
      continue;

    if (optAdapter)
    {
      if (wcsstr(adapterDesc.Description, optAdapter) == NULL)
      {
        DEBUG_INFO("Not using adapter: %ls", adapterDesc.Description);
        continue;
      }
      DEBUG_INFO("Adapter matched, trying: %ls", adapterDesc.Description);
    }

    for(
      int n = 0;
      IDXGIAdapter1_EnumOutputs(*adapter, n, output) != DXGI_ERROR_NOT_FOUND;
      ++n, comRef_release(output))
    {
      IDXGIOutput_GetDesc(*output, &outputDesc);

      if (optOutput)
      {
        if (wcsstr(outputDesc.DeviceName, optOutput) == NULL)
        {
          DEBUG_INFO("Not using adapter output: %ls", outputDesc.DeviceName);
          continue;
        }

        DEBUG_INFO("Adapter output matched, trying: %ls", outputDesc.DeviceName);
      }

      if (outputDesc.AttachedToDesktop)
        break;
    }

    if (*output)
      break;
  }

  free(optAdapter);
  free(optOutput );

  if (!*output)
  {
    DEBUG_ERROR("Failed to locate a valid output device");
    return false;
  }

  DEBUG_INFO("Device Name       : %ls"    , outputDesc.DeviceName);
  DEBUG_INFO("Device Description: %ls"    , adapterDesc.Description);
  DEBUG_INFO("Device Vendor ID  : 0x%x"   , adapterDesc.VendorId);
  DEBUG_INFO("Device Device ID  : 0x%x"   , adapterDesc.DeviceId);
  DEBUG_INFO("Device Video Mem  : %u MiB" ,
    (unsigned)(adapterDesc.DedicatedVideoMemory  / 1048576));
  DEBUG_INFO("Device Sys Mem    : %u MiB" ,
    (unsigned)(adapterDesc.DedicatedSystemMemory / 1048576));
  DEBUG_INFO("Shared Sys Mem    : %u MiB" ,
    (unsigned)(adapterDesc.SharedSystemMemory    / 1048576));

  return true;
}

static bool d12_heapTest(ID3D12Device3 * device, ID3D12Heap * heap)
{
  bool result = false;
  comRef_scopePush(1);

  D3D12_RESOURCE_DESC desc =
  {
    .Dimension          = D3D12_RESOURCE_DIMENSION_BUFFER,
    .Alignment          = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,
    .Width              = 1048576,
    .Height             = 1,
    .DepthOrArraySize   = 1,
    .MipLevels          = 1,
    .Format             = DXGI_FORMAT_UNKNOWN,
    .SampleDesc.Count   = 1,
    .SampleDesc.Quality = 0,
    .Layout             = D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
    .Flags              = D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER
  };

  comRef_defineLocal(ID3D12Resource, resource);
  HRESULT hr = ID3D12Device3_CreatePlacedResource(
    device,
    heap,
    0,
    &desc,
    D3D12_RESOURCE_STATE_COPY_DEST,
    NULL,
    &IID_ID3D12Resource,
    (void **)resource);

  if (FAILED(hr))
  {
    DEBUG_WINERROR("Failed to create the FrameBuffer ID3D12Resource", hr);
    goto exit;
  }

  /* the above may succeed even if there was a fault, as such we also need to
   * check if the device was removed */
  hr = ID3D12Device3_GetDeviceRemovedReason(device);
  if (hr != S_OK)
  {
    DEBUG_WINERROR("Device Removed", hr);
    goto exit;
  }

  result = true;

exit:
  comRef_scopePop();
  return result;
}

static ID3D12Resource * d12_frameBufferToResource(unsigned frameBufferIndex,
  FrameBuffer * frameBuffer, unsigned size, void ** map)
{
  ID3D12Resource * result = NULL;
  comRef_scopePush(10);

  typeof(this->frameBuffers[0]) * fb = &this->frameBuffers[frameBufferIndex];

  // nothing to do if the resource is already setup and is big enough
  if (fb->resource && fb->frameBuffer == frameBuffer && fb->size >= size)
  {
    result = *fb->resource;
    *map   = fb->map;
    ID3D12Resource_AddRef(result);
    goto exit;
  }

  fb->size        = size;
  fb->frameBuffer = frameBuffer;

  comRef_defineLocal(ID3D12Resource, resource);
  HRESULT hr;
  if (this->indirectCopy)
  {
    if (fb->map)
    {
      ID3D12Resource_Unmap(*fb->resource, 0, NULL);
      fb->map = NULL;
    }

    D3D12_HEAP_PROPERTIES heapProps =
    {
      .Type                 = D3D12_HEAP_TYPE_READBACK,
      .CPUPageProperty      = D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
      .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN,
      .CreationNodeMask     = 1,
      .VisibleNodeMask      = 1
    };

    D3D12_RESOURCE_DESC desc =
    {
      .Dimension          = D3D12_RESOURCE_DIMENSION_BUFFER,
      .Alignment          = 0,
      .Width              = size,
      .Height             = 1,
      .DepthOrArraySize   = 1,
      .MipLevels          = 1,
      .Format             = DXGI_FORMAT_UNKNOWN,
      .SampleDesc.Count   = 1,
      .SampleDesc.Quality = 0,
      .Layout             = D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
      .Flags              = D3D12_RESOURCE_FLAG_NONE
    };

    hr = ID3D12Device3_CreateCommittedResource(
      *this->device,
      &heapProps,
      D3D12_HEAP_FLAG_NONE,
      &desc,
      D3D12_RESOURCE_STATE_COPY_DEST,
      NULL,
      &IID_ID3D12Resource,
      (void **)resource);


    if (FAILED(hr))
    {
      DEBUG_WINERROR("Failed to create the intermediate ID3D12Resource", hr);
      goto exit;
    }

    fb->map = NULL;
    D3D12_RANGE range = {0, 0};
    HRESULT hr = ID3D12Resource_Map(*resource, 0, &range, &fb->map);
    if (FAILED(hr) || !fb->map)
    {
      DEBUG_WINERROR("Failed to map readback resource", hr);
      goto exit;
    }

    *map = fb->map;
  }
  else
  {
    D3D12_RESOURCE_DESC desc =
    {
      .Dimension          = D3D12_RESOURCE_DIMENSION_BUFFER,
      .Alignment          = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,
      .Width              = size,
      .Height             = 1,
      .DepthOrArraySize   = 1,
      .MipLevels          = 1,
      .Format             = DXGI_FORMAT_UNKNOWN,
      .SampleDesc.Count   = 1,
      .SampleDesc.Quality = 0,
      .Layout             = D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
      .Flags              = D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER
    };

    hr = ID3D12Device3_CreatePlacedResource(
      *this->device,
      *this->ivshmemHeap,
      (uintptr_t)framebuffer_get_data(frameBuffer) -
        (uintptr_t)this->ivshmemBase,
      &desc,
      D3D12_RESOURCE_STATE_COPY_DEST,
      NULL,
      &IID_ID3D12Resource,
      (void **)resource);

    if (FAILED(hr))
    {
      DEBUG_WINERROR("Failed to create the FrameBuffer ID3D12Resource", hr);
      goto exit;
    }
  }

  // cache the resource
  comRef_toGlobal(fb->resource, resource);
  result = *fb->resource;
  ID3D12Resource_AddRef(result);

exit:
  comRef_scopePop();
  return result;
}

void d12_updatePointer(CapturePointer * pointer, void * shape, size_t shapeSize)
{
  if (pointer->shapeUpdate)
  {
    void * dst;
    UINT   dstSize;
    if (!this->getPointerBufferFn(&dst, &dstSize))
    {
      DEBUG_ERROR("Failed to obtain a buffer for the pointer shape");
      pointer->shapeUpdate = false;
    }

    size_t copySize = min(dstSize, shapeSize);
    memcpy(dst, shape, copySize);
  }

  this->postPointerBufferFn(pointer);
}

struct CaptureInterface Capture_D12 =
{
  .shortName       = "D12",
  .asyncCapture    = false,
  .getName         = d12_getName,
  .initOptions     = d12_initOptions,
  .create          = d12_create,
  .init            = d12_init,
  .stop            = d12_stop,
  .deinit          = d12_deinit,
  .free            = d12_free,
  .capture         = d12_capture,
  .waitFrame       = d12_waitFrame,
  .getFrame        = d12_getFrame
};
