/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2021 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "d3d11_test.h"

typedef int(WINAPI *PFN_BEGIN_EVENT)(DWORD, WCHAR *);
typedef int(WINAPI *PFN_END_EVENT)();

RD_TEST(D3D11_Refcount_Check, D3D11GraphicsTest)
{
  static constexpr const char *Description =
      "Ensures that the device etc doesn't delete itself when there are still outstanding "
      "references, and also that it *does* delete itself when any cycle is detected.";

  D3D11GraphicsTest reftest;

  void Prepare(int argc, char **argv)
  {
    reftest.headless = true;

    reftest.Prepare(argc, argv);

    D3D11GraphicsTest::Prepare(argc, argv);
  }

#define CHECK_REFCOUNT(obj, expected)                                                    \
  {                                                                                      \
    ULONG count = GetRefcount(obj);                                                      \
    if(count != expected)                                                                \
    {                                                                                    \
      if(!failed)                                                                        \
        DEBUG_BREAK();                                                                   \
      failed = true;                                                                     \
      TEST_WARN(#obj " has wrong reference count. Got %u expected %u", count, expected); \
    }                                                                                    \
  }

  bool IsICDLoaded()
  {
    if(rdoc || reftest.rdoc)
    {
      // renderdoc keeps driver DLLs around to avoid race condition bugs, so we can't check on those
      // DLLs being unloaded.
      // Instead we hack by calling D3D9's Begin/EndEvent. If there's no D3D11 device alive it
      // always returns 0, otherwise it returns the nesting level minus 1. Since we don't have a
      // device/context to force it to drain the annotation queue we take advantage of the nesting
      // level starting at 0, so calling an unbalanced end() will return -1.

      static HMODULE d3d9 = LoadLibraryA("d3d9.dll");
      static PFN_BEGIN_EVENT begin = (PFN_BEGIN_EVENT)GetProcAddress(d3d9, "D3DPERF_BeginEvent");
      static PFN_END_EVENT end = (PFN_END_EVENT)GetProcAddress(d3d9, "D3DPERF_EndEvent");

      // don't care about these being unbalanced
      return end() != 0;
    }

    // a bit of a hack but I don't know of a better way to test if the device was really destroyed
    return GetModuleHandleA("nvwgf2um.dll") != NULL || GetModuleHandleA("nvwgf2umx.dll") != NULL ||
           GetModuleHandleA("atidxx32.dll") != NULL || GetModuleHandleA("atidxx64.dll") != NULL ||
           GetModuleHandleA("igd10iumd32.dll") != NULL ||
           GetModuleHandleA("igd10iumd64.dll") != NULL;
  }

  bool HasMessages(ID3D11InfoQueue * infoQueue, const std::vector<std::string> &haystacks)
  {
    std::string concat;
    UINT64 num = infoQueue->GetNumStoredMessages();
    for(UINT64 i = 0; i < num; i++)
    {
      SIZE_T len = 0;
      infoQueue->GetMessage(i, NULL, &len);

      char *msgbuf = new char[len];
      D3D11_MESSAGE *message = (D3D11_MESSAGE *)msgbuf;

      infoQueue->GetMessage(i, message, &len);

      if(message->Severity == D3D11_MESSAGE_SEVERITY_INFO)
        concat += "INFO: ";
      concat += message->pDescription;
      concat += "\n";

      delete[] msgbuf;
    }
    infoQueue->ClearStoredMessages();
    bool ret = true;
    for(const std::string &haystack : haystacks)
      ret &= (concat.find(haystack) != std::string::npos);
    return ret;
  }

  bool HasMessage(ID3D11InfoQueue * infoQueue, const std::string &haystack)
  {
    return HasMessages(infoQueue, {haystack});
  }

  int main()
  {
    // force a debug device
    reftest.debugDevice = true;

    if(!reftest.Init())
      return 4;

    {
      D3D_FEATURE_LEVEL features[] = {D3D_FEATURE_LEVEL_11_0};
      ULONG ret = 0;
      UINT dummy[] = {16, 16, 16, 16, 16};

      bool failed = false;

      static const GUID unwrappedID3D11InfoQueue__uuid = {
          0x3fc4e618, 0x3f70, 0x452a, {0x8b, 0x8f, 0xa7, 0x3a, 0xcc, 0xb5, 0x8e, 0x3d}};

      // for the first device enable INFO for creation/destruction
      ID3D11InfoQueue *infoQueue = NULL;

      // try first with renderdoc's GUID to get the unwrapped queue for testing against
      reftest.dev.QueryInterface(unwrappedID3D11InfoQueue__uuid, &infoQueue);

      if(infoQueue == NULL)
        reftest.dev.QueryInterface(__uuidof(ID3D11InfoQueue), &infoQueue);

      infoQueue->ClearStorageFilter();
      infoQueue->ClearRetrievalFilter();
      infoQueue->ClearStoredMessages();

      ID3D11Debug *dbg = NULL;
      reftest.dev.QueryInterface(__uuidof(ID3D11Debug), &dbg);

      // remove our references to everything but vb which we take locally
      reftest.defaultLayout = NULL;
      reftest.swap = NULL;
      reftest.bbTex = NULL;
      reftest.bbRTV = NULL;
      reftest.dev1 = NULL;
      reftest.dev2 = NULL;
      reftest.dev3 = NULL;
      reftest.dev4 = NULL;
      reftest.dev5 = NULL;
      reftest.ctx = NULL;
      reftest.ctx1 = NULL;
      reftest.ctx2 = NULL;
      reftest.ctx3 = NULL;
      reftest.ctx4 = NULL;
      reftest.annot = NULL;
      reftest.swapBlitVS = NULL;
      reftest.swapBlitPS = NULL;

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      // reference counting behaviour is NOT CONTRACTUAL but some applications check for it anyway.
      // this is particularly annoying when they're checking for implementation details, like
      // whether a resource hits 0 refcount even if it's still bound somewhere, etc.
      // The below refcounting behaviour was accurate for the D3D11 runtime at time of writing, and
      // we check it against renderdoc which is based on emulating that behaviour enough to fit this
      // test.

      // grab the device into a local pointer so we can AddRef / Release manually
      ID3D11Device *localdev = reftest.dev;
      localdev->AddRef();
      reftest.dev = NULL;

      ID3D11DeviceContext *localctx = NULL;
      localdev->GetImmediateContext(&localctx);

      ////////////////////////////////////////////////////////////
      // Create a VB and test basic 'child resource' <-> device refcounting

      ID3D11BufferPtr buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);

      ID3D11Buffer *localvb = buf;
      localvb->AddRef();
      buf = NULL;

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      // the device should have 5 references - localdev, localctx, localvb, dbg, and infoQueue
      CHECK_REFCOUNT(localdev, 5);

      // the VB has one reference
      CHECK_REFCOUNT(localvb, 1);

      // add 3 refs to the vertex buffer
      localvb->AddRef();
      localvb->AddRef();
      localvb->AddRef();

      // the device should still only have 5 references, localvb only holds one on the device
      CHECK_REFCOUNT(localdev, 5);

      // but the VB has 4 references
      CHECK_REFCOUNT(localvb, 4);

      localvb->Release();
      localvb->Release();
      localvb->Release();

      CHECK_REFCOUNT(localdev, 5);
      CHECK_REFCOUNT(localvb, 1);

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      ////////////////////////////////////////////////////////////
      // in spite of being cached, state objects should not refcount strangely (apart from
      // duplicates sharing a pointer)

      {
        D3D11_RASTERIZER_DESC rsdesc = {};

        // ensure this isn't the default rasterizer state
        rsdesc.CullMode = D3D11_CULL_BACK;
        rsdesc.FillMode = D3D11_FILL_WIREFRAME;
        rsdesc.DepthBias = 55;

        ID3D11RasterizerState *rs1 = NULL, *rs2 = NULL, *rs3 = NULL;
        localdev->CreateRasterizerState(&rsdesc, &rs1);

        CHECK_REFCOUNT(localdev, 6);
        CHECK_REFCOUNT(rs1, 1);

        // change the state, get a new object
        rsdesc.DepthBias = 99;
        localdev->CreateRasterizerState(&rsdesc, &rs2);

        CHECK_REFCOUNT(localdev, 7);
        CHECK_REFCOUNT(rs1, 1);
        CHECK_REFCOUNT(rs2, 1);

        // keep the same state, get the same object
        localdev->CreateRasterizerState(&rsdesc, &rs3);

        CHECK_REFCOUNT(localdev, 7);
        CHECK_REFCOUNT(rs1, 1);
        CHECK_REFCOUNT(rs2, 2);
        CHECK_REFCOUNT(rs3, 2);

        if(rs2 != rs3)
        {
          failed = true;
          TEST_ERROR("Expected to get the same state object back");
        }

        rs1->Release();
        rs2->Release();
        rs3->Release();
      }
      CHECK_REFCOUNT(localdev, 5);

      ////////////////////////////////////////////////////////////
      // create a texture and check view <-> resource <-> device refcounting

      ID3D11Texture2DPtr tex =
          D3D11TextureCreator(localdev, DXGI_FORMAT_BC1_UNORM, 128, 128, 1).SRV();

      ID3D11Texture2D *localtex = tex;
      localtex->AddRef();
      tex = NULL;

      // device has a new reference
      CHECK_REFCOUNT(localdev, 6);
      CHECK_REFCOUNT(localtex, 1);

      ID3D11ShaderResourceViewPtr srv = D3D11ViewCreator(localdev, ViewType::SRV, localtex);

      ID3D11ShaderResourceView *localsrv = srv;
      localsrv->AddRef();
      srv = NULL;

      // the device has a new ref from the texture, AND from the SRV
      CHECK_REFCOUNT(localdev, 7);
      // the texture doesn't get a ref from the view
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      // release the texture. It is kept alive by the SRV, but the device refcount goes down too
      localtex->Release();
      CHECK_REFCOUNT(localdev, 6);
      CHECK_REFCOUNT(localtex, 0);
      CHECK_REFCOUNT(localsrv, 1);

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      {
        ID3D11Texture2D *texretrieve = NULL;
        ID3D11Resource *resretrieve = NULL;
        localsrv->GetResource(&resretrieve);
        resretrieve->QueryInterface(__uuidof(ID3D11Texture2D), (void **)&texretrieve);
        ID3D11Texture2D *texcast = (ID3D11Texture2D *)resretrieve;

        if(texretrieve != localtex)
        {
          failed = true;
          TEST_ERROR("Expected texture to come back identically");
        }
        if(texcast != localtex)
        {
          failed = true;
          TEST_ERROR("Expected texture to come back identically");
        }

        texretrieve->Release();
        resretrieve->Release();
      }

      localtex->AddRef();

      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      localsrv->AddRef();
      localsrv->AddRef();
      localsrv->AddRef();

      // external SRV references only apply to the SRV, not the texture or device. Same as any other
      // ID3D11DeviceChild
      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 4);

      localsrv->Release();
      localsrv->Release();
      localsrv->Release();

      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      ////////////////////////////////////////////////////////////
      // check refcounting on a deferred context

      ID3D11DeviceContext *localdefctx;
      localdev->CreateDeferredContext(0, &localdefctx);

      // device gets another reference
      CHECK_REFCOUNT(localdev, 8);
      CHECK_REFCOUNT(localdefctx, 1);

      localdefctx->ClearState();

      // bind the VB. Doesn't change any public refcounts
      localdefctx->IASetVertexBuffers(0, 1, &localvb, dummy, dummy);

      CHECK_REFCOUNT(localdev, 8);
      CHECK_REFCOUNT(localdefctx, 1);
      CHECK_REFCOUNT(localvb, 1);

      // VB is now held alive by the defctx
      localvb->Release();
      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localdefctx, 1);
      CHECK_REFCOUNT(localvb, 0);

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      {
        ID3D11Buffer *vbretrieve = NULL;
        localdefctx->IAGetVertexBuffers(0, 1, &vbretrieve, NULL, NULL);

        if(vbretrieve != localvb)
        {
          failed = true;
          TEST_ERROR("Expected buffer to come back identically");
        }

        vbretrieve->Release();
      }

      localvb->AddRef();

      CHECK_REFCOUNT(localdev, 8);
      CHECK_REFCOUNT(localdefctx, 1);
      CHECK_REFCOUNT(localvb, 1);

      localdefctx->Draw(0, 0);

      ID3D11CommandList *locallist = NULL;
      localdefctx->FinishCommandList(FALSE, &locallist);

      infoQueue->ClearStoredMessages();

      // extra refcount for the list, but otherwise unchanged
      CHECK_REFCOUNT(localdev, 9);
      CHECK_REFCOUNT(localdefctx, 1);
      CHECK_REFCOUNT(locallist, 1);
      CHECK_REFCOUNT(localvb, 1);

      ret = localdefctx->Release();

      // this should release it
      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localdefctx still has outstanding references");
      }

      if(!HasMessage(infoQueue, "INFO: Destroy ID3D11Context"))
      {
        failed = true;
        TEST_ERROR("Expected localdefctx to be really destroyed");
      }

      CHECK_REFCOUNT(localdev, 8);
      CHECK_REFCOUNT(locallist, 1);
      CHECK_REFCOUNT(localvb, 1);

      // the VB is held alive by the list now, though we can't retrieve it anymore
      // we skip this test because although the runtime is smart enough to keep refs
      // on the necessary objects, we aren't and we hope no-one actually takes advantage of this.
      if(0)
      {
        localvb->Release();
        CHECK_REFCOUNT(localvb, 0);

        dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

        localvb->AddRef();
        CHECK_REFCOUNT(localvb, 1);

        if(HasMessage(infoQueue, "INFO: Destroy"))
        {
          failed = true;
          TEST_ERROR("localvb should not have been destroyed");
        }
      }

      CHECK_REFCOUNT(localdev, 8);
      CHECK_REFCOUNT(locallist, 1);
      CHECK_REFCOUNT(localvb, 1);

      ret = locallist->Release();
      localctx->Flush();

      // this should release it
      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("locallist still has outstanding references");
      }

      if(!HasMessage(infoQueue, "INFO: Destroy ID3D11CommandList"))
      {
        failed = true;
        TEST_ERROR("Expected locallist to be really destroyed");
      }

      CHECK_REFCOUNT(localdev, 7);

      ////////////////////////////////////////////////////////////
      // check that resources which are bound but don't have an external ref stay alive

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      infoQueue->ClearStoredMessages();

      // another new device refcount
      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localctx, 1);
      CHECK_REFCOUNT(localvb, 1);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      // binding doesn't change public refcounts
      localctx->ClearState();
      localctx->IASetVertexBuffers(0, 1, &localvb, dummy, dummy);
      localctx->PSSetShaderResources(0, 1, &localsrv);

      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localctx, 1);
      CHECK_REFCOUNT(localvb, 1);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      // but it means we can release things and they stay alive
      localvb->Release();
      localtex->Release();
      localsrv->Release();
      localctx->Flush();

      CHECK_REFCOUNT(localvb, 0);
      CHECK_REFCOUNT(localtex, 0);
      CHECK_REFCOUNT(localsrv, 0);

      if(HasMessage(infoQueue, "INFO: Destroy"))
      {
        failed = true;
        TEST_ERROR("Expected nothing to be destroyed");
      }

      localvb->AddRef();
      localtex->AddRef();
      localsrv->AddRef();

      CHECK_REFCOUNT(localdev, 7);
      CHECK_REFCOUNT(localctx, 1);
      CHECK_REFCOUNT(localvb, 1);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      localctx->ClearState();
      localctx->Flush();

      ////////////////////////////////////////////////////////////

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      ret = localvb->Release();
      localctx->Flush();

      // this should release it
      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localvb still has outstanding references");
      }

      if(!HasMessage(infoQueue, "INFO: Destroy ID3D11Buffer"))
      {
        failed = true;
        TEST_ERROR("Expected localvb to be really destroyed");
      }

      CHECK_REFCOUNT(localdev, 6);

      ret = localsrv->Release();
      localctx->Flush();

      // this should release it
      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localsrv still has outstanding references");
      }

      if(!HasMessage(infoQueue, "INFO: Destroy ID3D11ShaderResourceView"))
      {
        failed = true;
        TEST_ERROR("Expected localsrv to be really destroyed");
      }

      CHECK_REFCOUNT(localdev, 5);

      ret = localtex->Release();
      localctx->Flush();

      // this should release it
      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localtex still has outstanding references");
      }

      if(!HasMessage(infoQueue, "INFO: Destroy ID3D11Texture2D"))
      {
        failed = true;
        TEST_ERROR("Expected localtex to be really destroyed");
      }

      // the device should have 4 references - localdev, localctx, dbg and infoQueue
      CHECK_REFCOUNT(localdev, 4);

      dbg->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

      // ID3DUserDefinedAnnotation shares the context's refcount
      {
        CHECK_REFCOUNT(localctx, 1);

        ID3DUserDefinedAnnotationPtr annottest = localctx;

        if(annottest)
        {
          CHECK_REFCOUNT(localctx, 2);
          ID3DUserDefinedAnnotation *localannot = annottest;
          CHECK_REFCOUNT(localctx, 2);
          CHECK_REFCOUNT(localannot, 2);
        }
      }
      CHECK_REFCOUNT(localctx, 1);

      CHECK_REFCOUNT(localdev, 4);

      localctx->Release();
      localctx = NULL;
      dbg->Release();
      dbg = NULL;
      infoQueue->Release();
      infoQueue = NULL;

      // the device should only have this reference - localdev
      CHECK_REFCOUNT(localdev, 1);

      bool before = IsICDLoaded();

      ret = localdev->Release();

      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localdev still has outstanding references");
      }

      bool after = IsICDLoaded();

      if(!before)
      {
        TEST_WARN("Couldn't detect ICD at all - unclear if device really was destroyed");
      }
      else if(before && after)
      {
        failed = true;
        TEST_ERROR("Device leaked - ICD dll stayed present");
      }

      ///////////////////////////////////////////////////////////////////////////
      // test a device staying alive based on an unbound child resource
      reftest.CreateDevice({}, NULL, features, D3D11_CREATE_DEVICE_DEBUG);

      localdev = reftest.dev;
      localdev->AddRef();
      reftest.dev = NULL;
      reftest.ctx = NULL;
      CHECK_REFCOUNT(localdev, 1);

      buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);
      localvb = buf;
      localvb->AddRef();
      buf = NULL;

      CHECK_REFCOUNT(localdev, 2);
      CHECK_REFCOUNT(localvb, 1);

      localdev->Release();

      CHECK_REFCOUNT(localdev, 1);
      CHECK_REFCOUNT(localvb, 1);

      // release the device with the VB
      before = IsICDLoaded();

      ret = localvb->Release();

      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localvb still has outstanding references");
      }

      after = IsICDLoaded();

      if(!before)
      {
        TEST_WARN("Couldn't detect ICD at all - unclear if device really was destroyed");
      }
      else if(before && after)
      {
        failed = true;
        TEST_ERROR("Device leaked - ICD dll stayed present");
      }

      ///////////////////////////////////////////////////////////////////////////
      // test a device staying alive based on a *bound* child resource on the immediate context
      reftest.CreateDevice({}, NULL, features, D3D11_CREATE_DEVICE_DEBUG);

      localdev = reftest.dev;
      localdev->AddRef();
      reftest.dev = NULL;
      reftest.ctx = NULL;
      CHECK_REFCOUNT(localdev, 1);

      buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);
      localvb = buf;
      localvb->AddRef();
      buf = NULL;

      CHECK_REFCOUNT(localdev, 2);
      CHECK_REFCOUNT(localvb, 1);

      localdev->Release();

      CHECK_REFCOUNT(localdev, 1);
      CHECK_REFCOUNT(localvb, 1);

      localdev->GetImmediateContext(&localctx);
      localctx->IASetVertexBuffers(0, 1, &localvb, dummy, dummy);
      localctx->Flush();

      localctx->Release();
      localctx = NULL;

      before = IsICDLoaded();

      ret = localvb->Release();

      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localvb still has outstanding references");
      }

      after = IsICDLoaded();

      if(!before)
      {
        TEST_WARN("Couldn't detect ICD at all - unclear if device really was destroyed");
      }
      else if(before && after)
      {
        failed = true;
        TEST_ERROR("Device leaked - ICD dll stayed present");
      }

      ///////////////////////////////////////////////////////////////////////////
      // test a resource or view being destroyed when unbound
      reftest.CreateDevice({}, NULL, features, D3D11_CREATE_DEVICE_DEBUG);

      localdev = reftest.dev;
      localdev->AddRef();
      reftest.dev = NULL;
      reftest.ctx = NULL;
      CHECK_REFCOUNT(localdev, 1);

      buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);
      localvb = buf;
      localvb->AddRef();
      buf = NULL;

      tex = D3D11TextureCreator(localdev, DXGI_FORMAT_BC1_UNORM, 128, 128, 1).SRV();
      localtex = tex;
      localtex->AddRef();
      tex = NULL;

      srv = D3D11ViewCreator(localdev, ViewType::SRV, localtex);
      localsrv = srv;
      localsrv->AddRef();
      srv = NULL;

      CHECK_REFCOUNT(localdev, 4);
      CHECK_REFCOUNT(localvb, 1);
      CHECK_REFCOUNT(localtex, 1);
      CHECK_REFCOUNT(localsrv, 1);

      localdev->GetImmediateContext(&localctx);

      // try first with renderdoc's GUID to get the unwrapped queue for testing against
      localdev->QueryInterface(unwrappedID3D11InfoQueue__uuid, (void **)&infoQueue);

      if(infoQueue == NULL)
        localdev->QueryInterface(__uuidof(ID3D11InfoQueue), (void **)&infoQueue);

      infoQueue->ClearStorageFilter();
      infoQueue->ClearRetrievalFilter();
      infoQueue->ClearStoredMessages();

      CHECK_REFCOUNT(localdev, 6);
      CHECK_REFCOUNT(localvb, 1);

      localctx->IASetVertexBuffers(0, 1, &localvb, dummy, dummy);
      localctx->PSSetShaderResources(0, 1, &localsrv);
      localctx->Flush();

      localvb->Release();
      localtex->Release();
      localsrv->Release();
      localctx->Flush();

      CHECK_REFCOUNT(localdev, 3);
      CHECK_REFCOUNT(localvb, 0);
      CHECK_REFCOUNT(localtex, 0);
      CHECK_REFCOUNT(localsrv, 0);

      if(HasMessage(infoQueue, "INFO: Destroy"))
      {
        failed = true;
        TEST_ERROR("Expected nothing to be destroyed");
      }

      localctx->ClearState();
      localctx->Flush();

      CHECK_REFCOUNT(localdev, 3);

      if(!HasMessages(infoQueue, {"INFO: Destroy ID3D11Buffer", "INFO: Destroy ID3D11Texture2D",
                                  "INFO: Destroy ID3D11ShaderResourceView"}))
      {
        failed = true;
        TEST_ERROR("Expected buffer, texture and SRV to be destroyed on unbind");
      }

      localctx->Release();
      localctx = NULL;
      infoQueue->Release();
      infoQueue = NULL;

      CHECK_REFCOUNT(localdev, 1);

      before = IsICDLoaded();

      ret = localdev->Release();

      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localdev still has outstanding references");
      }

      after = IsICDLoaded();

      if(!before)
      {
        TEST_WARN("Couldn't detect ICD at all - unclear if device really was destroyed");
      }
      else if(before && after)
      {
        failed = true;
        TEST_ERROR("Device leaked - ICD dll stayed present");
      }

      ///////////////////////////////////////////////////////////////////////////
      // test that resources which temporarily bounce off 0 refcounts in a naive bind/unbind don't
      // get destroyed.
      reftest.CreateDevice({}, NULL, features, D3D11_CREATE_DEVICE_DEBUG);

      localdev = reftest.dev;
      localdev->AddRef();
      reftest.dev = NULL;
      reftest.ctx = NULL;
      CHECK_REFCOUNT(localdev, 1);

      buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);
      localvb = buf;
      localvb->AddRef();
      buf = NULL;

      buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);
      ID3D11Buffer *localvb2 = buf;
      localvb2->AddRef();
      buf = NULL;

      buf = D3D11BufferCreator(localdev).Vertex().Data(DefaultTri);
      ID3D11Buffer *localvb3 = buf;
      localvb3->AddRef();
      buf = NULL;

      CHECK_REFCOUNT(localdev, 4);
      CHECK_REFCOUNT(localvb, 1);
      CHECK_REFCOUNT(localvb2, 1);
      CHECK_REFCOUNT(localvb3, 1);

      localdev->GetImmediateContext(&localctx);

      // try first with renderdoc's GUID to get the unwrapped queue for testing against
      localdev->QueryInterface(unwrappedID3D11InfoQueue__uuid, (void **)&infoQueue);

      if(infoQueue == NULL)
        localdev->QueryInterface(__uuidof(ID3D11InfoQueue), (void **)&infoQueue);

      infoQueue->ClearStorageFilter();
      infoQueue->ClearRetrievalFilter();
      infoQueue->ClearStoredMessages();

      CHECK_REFCOUNT(localdev, 6);
      CHECK_REFCOUNT(localvb, 1);
      CHECK_REFCOUNT(localvb2, 1);
      CHECK_REFCOUNT(localvb3, 1);

      ID3D11Buffer *firstBuffers[] = {localvb, localvb2, localvb3};
      localctx->IASetVertexBuffers(0, 3, firstBuffers, dummy, dummy);
      localctx->Flush();

      localvb->Release();
      localvb2->Release();
      localvb3->Release();
      localctx->Flush();

      CHECK_REFCOUNT(localdev, 3);
      CHECK_REFCOUNT(localvb, 0);
      CHECK_REFCOUNT(localvb2, 0);
      CHECK_REFCOUNT(localvb3, 0);

      if(HasMessage(infoQueue, "INFO: Destroy"))
      {
        failed = true;
        TEST_ERROR("Expected nothing to be destroyed");
      }

      // in a naive approach, when replacing localvb with localvb3 in slot 0, localvb is truly not
      // referenced anywhere at all. This test ensures that we don't immediately destroy localvb
      // when it's unbound from slot 0 because it's soon to be bound to slot 1. Note the same then
      // happens with localvb2 which is temporarily reference-less when it's unbound from slot 1

      ID3D11Buffer *secondBuffers[] = {localvb3, localvb, localvb2};
      localctx->IASetVertexBuffers(0, 3, secondBuffers, dummy, dummy);
      localctx->Flush();

      CHECK_REFCOUNT(localdev, 3);
      CHECK_REFCOUNT(localvb, 0);
      CHECK_REFCOUNT(localvb2, 0);
      CHECK_REFCOUNT(localvb3, 0);

      if(HasMessage(infoQueue, "INFO: Destroy"))
      {
        failed = true;
        TEST_ERROR("Expected nothing to be destroyed");
      }

      // clearing the state should still unbind and destroy the buffers
      localctx->ClearState();
      localctx->Flush();

      CHECK_REFCOUNT(localdev, 3);

      if(!HasMessage(infoQueue, "INFO: Destroy ID3D11Buffer"))
      {
        failed = true;
        TEST_ERROR("Expected buffer, texture and SRV to be destroyed on unbind");
      }

      localctx->Release();
      localctx = NULL;
      infoQueue->Release();
      infoQueue = NULL;

      CHECK_REFCOUNT(localdev, 1);

      before = IsICDLoaded();

      ret = localdev->Release();

      if(ret != 0)
      {
        failed = true;
        TEST_ERROR("localdev still has outstanding references");
      }

      after = IsICDLoaded();

      if(!before)
      {
        TEST_WARN("Couldn't detect ICD at all - unclear if device really was destroyed");
      }
      else if(before && after)
      {
        failed = true;
        TEST_ERROR("Device leaked - ICD dll stayed present");
      }

      ///////////////////////////////////////////////////////////////////////////
      if(failed)
      {
        TEST_ERROR("Encountered refcounting errors, aborting test");
        return 5;
      }
    }

    // initialise, create window, create device, etc
    if(!Init())
      return 3;

    ID3D11InfoQueuePtr infoQueue = dev;
    if(infoQueue)
    {
      infoQueue->ClearStorageFilter();
      infoQueue->ClearRetrievalFilter();
    }

    // run a normal test that we can capture from, so the checker can see that we got this far
    // without failing
    ID3DBlobPtr vsblob = Compile(D3DDefaultVertex, "main", "vs_4_0");
    ID3DBlobPtr psblob = Compile(D3DDefaultPixel, "main", "ps_4_0");

    CreateDefaultInputLayout(vsblob);

    ID3D11VertexShaderPtr vs = CreateVS(vsblob);
    ID3D11PixelShaderPtr ps = CreatePS(psblob);

    ID3D11BufferPtr vb = MakeBuffer().Vertex().Data(DefaultTri);

    // destroy backbuffer RTV
    bbRTV = NULL;
    ctx->Flush();

    // save the backbuffer texture
    ID3D11Texture2D *localbbtex = bbTex;
    // release the backbuffer texture
    bbTex = NULL;
    ctx->Flush();

    if(GetRefcount(localbbtex) != 0)
      TEST_FATAL("backbuffer texture isn't 0 refcount!");

    // get it back again
    CHECK_HR(swap->GetBuffer(0, __uuidof(ID3D11Texture2D), (void **)&bbTex));

    if(bbTex != localbbtex)
      TEST_FATAL("Expected backbuffer texture to be identical after obtaining it again");

    // recreate the RTV
    CHECK_HR(dev->CreateRenderTargetView(bbTex, NULL, &bbRTV));

    while(Running())
    {
      ClearRenderTargetView(bbRTV, {0.2f, 0.2f, 0.2f, 1.0f});

      IASetVertexBuffer(vb, sizeof(DefaultA2V), 0);
      ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
      ctx->IASetInputLayout(defaultLayout);

      ctx->VSSetShader(vs, NULL, 0);
      ctx->PSSetShader(ps, NULL, 0);

      RSSetViewport({0.0f, 0.0f, (float)screenWidth, (float)screenHeight, 0.0f, 1.0f});

      ctx->OMSetRenderTargets(1, &bbRTV.GetInterfacePtr(), NULL);

      setMarker("Color Draw");
      ctx->Draw(3, 0);

      Present();
    }

    return 0;
  }
};

REGISTER_TEST();
