/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2026 Baldur Karlsson
 * Copyright (c) 2014 Crytek
 *
 * 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.
 ******************************************************************************/

#pragma once

#include <algorithm>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include "api/replay/rdcflatmap.h"
#include "api/replay/resourceid.h"
#include "common/threading.h"
#include "core/core.h"
#include "os/os_specific.h"
#include "serialise/serialiser.h"

// In what way (read, write, etc) was a resource referenced in a frame -
// used to determine if initial contents are needed and to what degree.
// These values are used both as states (representing the cumulative previous
// accesses to the resource), and state transitions (access by a single
// command, modifying the state). This state machine is illustrated below,
// with states represented in caps, and transitions in lower case.
//
//         +------------------ NONE -----------------------------+
//         |                    |                                |
//        read            partialWrite                     completeWrite
//         |                    |                                |
//         V                    V                                V
//       READ             PARTIAL_WRITE --completeWrite--> COMPLETE_WRITE
//         |                    |
//         |                  read
//       write                  |
//         |                    V
//         |            WRITE_BEFORE_READ
//         V                    |
//  READ_BEFORE_WRITE <--write--+
//
// Note:
//  * All resources begin implicitly in the None state.
//  * The transitions labeled "write" correspond to either PartialWrite or
//    CompleteWrite (e.g. in the READ state, either a PartialWrite or a
//    CompleteWrite moves to the READ_BEFORE_WRITE state).
//  * The state transitions for ReadBeforeWrite are simply the composition of
//    the transition for read, followed by the transition for write (e.g.
//    ReadBeforeWrite moves from NONE state to READBEFOREWRITE state);
//    similarly, the state transitions for WriteBeforeRead are the composition
//    of the transition for write, followed by the transition for read.
//  * All other transitions (excluding ReadBeforeWrite and WriteBeforeRead)
//    that are not explicitly shown leave the state unchanged (e.g. a read in
//    the COMPLETE_WRITE state remains in the COMPLETE_WRITE state).
enum FrameRefType
{
  // Initial state, no reads or writes
  eFrameRef_None = 0,

  // Write to some unknown subset of resource.
  // As a state, this represents that unlike clear, some part of the
  // initial contents might still be visible to later reads.
  eFrameRef_PartialWrite = 1,

  // Write to the entire resource.
  // As a state, this represents that no later reads will even be able to see
  // the initial contents, and therefore, the initial contents need not be
  // restored for replay.
  eFrameRef_CompleteWrite = 2,

  // Read from the resource;
  // As a state, this represents a read that could have seen the resource's
  // initial contents, but the value seen by the read has not been overwritten;
  // therefore, the initial contents needs to be restored before the first time
  // we replay, but doesn't need to be reset between subsequent replays.
  eFrameRef_Read = 3,

  // Read followed by a write;
  // As a state, this represents a read that could have seen the resource
  // initial contents, followed by a write that could have modified that
  // initial contents; therefore, the initial contents will need to be reset
  // before each time we replay the frame.
  eFrameRef_ReadBeforeWrite = 4,

  // Partial write followed by read;
  // For the purpose of correct replay, this is equivalent to `Read`. However,
  // if this resource is inspected by the user before the write, the future
  // read could, incorrectly, be observed. This is because read-only resources
  // are not reset, so the write from the previous replay may still be present.
  eFrameRef_WriteBeforeRead = 5,

  // Similar to a CompleteWrite, but this is effectively an atomic "completely written, used, and
  // discarded". This encodes more information than CompleteWrite because it tells us that the
  // resource contents won't ever be used. For most purposes we treat this the same as
  // CompleteWrite, but below we use it to identify skippable resources.
  eFrameRef_CompleteWriteAndDiscard = 6,

  // No reference info is available;
  // This should only appear durring replay, and any (sub)resource with `Unknown`
  // reference type should be conservatively reset before each replay.
  eFrameRef_Unknown = 1000000000,
};

bool IncludesRead(FrameRefType refType);

bool IncludesWrite(FrameRefType refType);

const FrameRefType eFrameRef_Minimum = eFrameRef_None;
const FrameRefType eFrameRef_Maximum = eFrameRef_CompleteWriteAndDiscard;

// Threshold value for resource "age", i.e. how long it wasn't
// referred with the any write reference.
const double PERSISTENT_RESOURCE_AGE = 3000;
// how long since the first time it was skipped. If the skip state hasn't been invalidated in this
// long it can be skipped
const double SKIP_RESOURCE_AGE = 3000;

DECLARE_REFLECTION_ENUM(FrameRefType);

typedef FrameRefType (*FrameRefCompFunc)(FrameRefType, FrameRefType);

// Compose frame refs that occur in a known order.
// This can be thought of as a state (`first`) and a transition from that state
// (`second`), returning the new state (see the state diagram for
// `FrameRefType` above)
FrameRefType ComposeFrameRefs(FrameRefType first, FrameRefType second);

// Compose frame refs when the order is unknown.
// This is conservative, in that, if there is both a Read and a Write/Clear, it
// assumes the Read occurs before the Write/Clear, forcing that resource to be
// reset for replay.
FrameRefType ComposeFrameRefsUnordered(FrameRefType first, FrameRefType second);

// Compose frame refs for disjoint subresources.
// This is used to compute the overall frame ref for images/memory from the
// frame refs of their subresources.
FrameRefType ComposeFrameRefsDisjoint(FrameRefType x, FrameRefType y);

// Returns whichever of `first` or `second` is valid.
FrameRefType ComposeFrameRefsFirstKnown(FrameRefType first, FrameRefType second);

// Dummy frame ref composition that always keeps the old ref.
FrameRefType KeepOldFrameRef(FrameRefType first, FrameRefType second);

bool IsDirtyFrameRef(FrameRefType refType);
bool IsCompleteWriteFrameRef(FrameRefType refType);

// Captures the possible initialization/reset requirements for resources.
// These requirements are entirely determined by the resource's FrameRefType,
// but this type improves the readability of the code that checks
// init/reset requirements.
enum InitReqType
{
  // No initialization required.
  eInitReq_None,

  // Initialize the resource by clearing.
  eInitReq_Clear,

  // Initialize the resource by copying initial data.
  eInitReq_Copy,
};

enum InitPolicy
{
  // Completely disable optimizations--copy initial data into every resource
  // before every replay.
  eInitPolicy_NoOpt,

  // CopyAll--conservative policy which ensures each subresource begins each
  // replay with the correct initial data.
  //
  // Initialization policy:
  //   Copy initial data into each subresource
  //
  // Reset policy:
  //   Copy initial data into each subresource which is written
  eInitPolicy_CopyAll,

  // ClearUnread--avoid copying initial data which is never read by the replay
  // commands. A user inspecting a resource before it is written may observe
  // cleared data, rather than the actual initial data.
  //
  // Initialization policy:
  //   Copy initial data into each subresource that is read.
  //   Clear each subresource that is not read.
  //
  // Reset policy:
  //   Copy initial data into each subresource where the initial data is read
  //   and then overwritten.
  //   Clear each subresource which is written, but whose initial data is not read.
  eInitPolicy_ClearUnread,

  // Fastest--Initialize/reset as little as possible for correct replay.
  // A user inspecting a resource before it is written may observe the data
  // from a future write (from the previous replay).
  //
  // Initialization policy:
  //   Copy initial data into each subresource that is read.
  //   Clear each subresource that is not read.
  //
  // Reset policy:
  //   Copy initial data into each subresource where the initial data is read
  //   and then overwritten.
  eInitPolicy_Fastest,
};

// Return the initialization/reset requirements for a FrameRefType
inline InitReqType InitReq(FrameRefType refType, InitPolicy policy, bool initialized)
{
  if(eFrameRef_Minimum > refType || refType > eFrameRef_Maximum)
    return eInitReq_Copy;
#define COPY_ONCE (initialized ? eInitReq_None : eInitReq_Copy)
#define CLEAR_ONCE (initialized ? eInitReq_None : eInitReq_Clear)
  switch(policy)
  {
    case eInitPolicy_NoOpt: return eInitReq_Copy;
    case eInitPolicy_CopyAll:
      switch(refType)
      {
        case eFrameRef_None: return COPY_ONCE;
        case eFrameRef_Read: return COPY_ONCE;
        default: return eInitReq_Copy;
      }
    case eInitPolicy_ClearUnread:
      switch(refType)
      {
        case eFrameRef_None: return CLEAR_ONCE;
        case eFrameRef_Read: return COPY_ONCE;
        case eFrameRef_ReadBeforeWrite: return eInitReq_Copy;
        case eFrameRef_WriteBeforeRead: return eInitReq_Copy;
        default: return eInitReq_Clear;
      }
    case eInitPolicy_Fastest:
      switch(refType)
      {
        case eFrameRef_None: return CLEAR_ONCE;
        case eFrameRef_Read: return COPY_ONCE;
        case eFrameRef_ReadBeforeWrite: return eInitReq_Copy;
        case eFrameRef_WriteBeforeRead: return COPY_ONCE;
        default: return CLEAR_ONCE;
      }
    default: RDCERR("Unknown initialization policy (%d).", policy); return eInitReq_Copy;
  }
#undef COPY_ONCE
#undef CLEAR_ONCE
}

// handle marking a resource referenced for read or write and storing RAW access etc.
template <typename Compose>
bool MarkReferenced(std::unordered_map<ResourceId, FrameRefType> &refs, ResourceId id,
                    FrameRefType refType, Compose comp)
{
  auto refit = refs.find(id);
  if(refit == refs.end())
  {
    refs[id] = refType;
    return true;
  }
  else
  {
    refit->second = comp(refit->second, refType);
  }
  return false;
}

inline bool MarkReferenced(std::unordered_map<ResourceId, FrameRefType> &refs, ResourceId id,
                           FrameRefType refType)
{
  return MarkReferenced(refs, id, refType, ComposeFrameRefs);
}

// verbose prints with IDs of each dirty resource and whether it was prepared,
// and whether it was serialised.
#define VERBOSE_DIRTY_RESOURCES OPTION_OFF

namespace ResourceIDGen
{
ResourceId GetNewUniqueID();
bool IsReplayOnlyID(ResourceId id);
void SetReplayResourceIDs();
};

struct ResourceRecord;

class ResourceRecordHandler
{
public:
  virtual void MarkDirtyResource(ResourceId id) = 0;
  virtual void RemoveResourceRecord(ResourceId id) = 0;
  virtual void MarkResourceFrameReferenced(ResourceId id, FrameRefType refType) = 0;
  virtual void DestroyResourceRecord(ResourceRecord *record) = 0;
};

// This is a generic resource record, that APIs can inherit from and use.
// A resource is an API object that gets tracked on its own, has dependencies on other resources
// and has its own stream of chunks.
//
// This is used to track the necessary resources for a frame, and include only those required
// for the captured frame in its log. It also handles anything resource-specific such as
// shadow CPU copies of data.
struct ResourceRecord
{
  ResourceRecord(ResourceId id, bool lock)
      : RefCount(1),
        ResID(id),
        UpdateCount(0),
        DataInSerialiser(false),
        DataPtr(NULL),
        DataOffset(0),
        Length(0),
        DataWritten(false),
        InternalResource(false)
  {
    m_ChunkLock = NULL;

    if(lock)
      m_ChunkLock = new Threading::CriticalSection();
  }

  void DisableChunkLocking() { SAFE_DELETE(m_ChunkLock); }
  ~ResourceRecord() { SAFE_DELETE(m_ChunkLock); }
  void AddParent(ResourceRecord *r)
  {
    if(r == this)
      return;

    if(Parents.indexOf(r) < 0)
    {
      r->AddRef();
      Parents.push_back(r);
    }
  }
  bool HasParent(ResourceRecord *r) const { return Parents.indexOf(r) >= 0; }
  void MarkParentsDirty(ResourceRecordHandler *mgr)
  {
    for(auto it = Parents.begin(); it != Parents.end(); ++it)
      mgr->MarkDirtyResource((*it)->GetResourceID());
  }

  void MarkParentsReferenced(ResourceRecordHandler *mgr, FrameRefType refType)
  {
    for(auto it = Parents.begin(); it != Parents.end(); ++it)
      mgr->MarkResourceFrameReferenced((*it)->GetResourceID(), refType);
  }

  void FreeParents(ResourceRecordHandler *mgr)
  {
    for(auto it = Parents.begin(); it != Parents.end(); ++it)
      (*it)->Delete(mgr);

    Parents.clear();
  }

  void MarkDataUnwritten() { DataWritten = false; }
  void Insert(std::map<int64_t, Chunk *> &recordlist)
  {
    bool dataWritten = DataWritten;

    DataWritten = true;

    for(auto it = Parents.begin(); it != Parents.end(); ++it)
    {
      if(!(*it)->DataWritten)
      {
        (*it)->Insert(recordlist);
      }
    }

    if(!dataWritten)
    {
      for(auto it = m_Chunks.begin(); it != m_Chunks.end(); ++it)
        recordlist[it->id] = it->chunk;
    }
  }

  void AddRef() { Atomic::Inc32(&RefCount); }
  int GetRefCount() const { return RefCount; }
  void Delete(ResourceRecordHandler *mgr);

  ResourceId GetResourceID() const { return ResID; }
  void AddChunk(Chunk *chunk, int64_t ID = 0)
  {
    if(ID == 0)
      ID = GetID();
    LockChunks();
    m_Chunks.push_back(StoredChunk(ID, chunk));
    UnlockChunks();
  }

  void LockChunks()
  {
    if(m_ChunkLock)
      m_ChunkLock->Lock();
  }
  void UnlockChunks()
  {
    if(m_ChunkLock)
      m_ChunkLock->Unlock();
  }

  bool HasChunks() const { return !m_Chunks.empty(); }
  size_t NumChunks() const { return m_Chunks.size(); }
  void SwapChunks(ResourceRecord *other)
  {
    LockChunks();
    other->LockChunks();
    m_Chunks.swap(other->m_Chunks);
    m_FrameRefs.swap(other->m_FrameRefs);
    other->UnlockChunks();
    UnlockChunks();
  }

  void AppendFrom(ResourceRecord *other)
  {
    LockChunks();
    other->LockChunks();

    for(auto it = other->m_Chunks.begin(); it != other->m_Chunks.end(); ++it)
      AddChunk(it->chunk->Duplicate());

    for(auto it = other->Parents.begin(); it != other->Parents.end(); ++it)
      AddParent(*it);

    other->UnlockChunks();
    UnlockChunks();
  }

  void DeleteChunks()
  {
    LockChunks();
    for(auto it = m_Chunks.begin(); it != m_Chunks.end(); ++it)
      it->chunk->Delete(it->fromAllocator != 0);
    m_Chunks.clear();
    UnlockChunks();
  }

  Chunk *GetLastChunk() const
  {
    RDCASSERT(HasChunks());
    return m_Chunks.back().chunk;
  }

  int64_t GetLastChunkID() const
  {
    RDCASSERT(HasChunks());
    return m_Chunks.back().id;
  }

  void PopChunk() { m_Chunks.pop_back(); }
  byte *GetDataPtr() { return DataPtr + DataOffset; }
  bool HasDataPtr() { return DataPtr != NULL; }
  void SetDataOffset(uint64_t offs) { DataOffset = offs; }
  void SetDataPtr(byte *ptr) { DataPtr = ptr; }
  template <typename Compose>
  bool MarkResourceFrameReferenced(ResourceId id, FrameRefType refType, Compose comp);
  inline bool MarkResourceFrameReferenced(ResourceId id, FrameRefType refType)
  {
    return MarkResourceFrameReferenced(id, refType, ComposeFrameRefs);
  }
  void AddResourceReferences(ResourceRecordHandler *mgr);
  void AddReferencedIDs(std::unordered_set<ResourceId> &ids)
  {
    for(auto it = m_FrameRefs.begin(); it != m_FrameRefs.end(); ++it)
      ids.insert(it->first);
  }

  uint64_t Length;

  int UpdateCount;
  bool DataInSerialiser;

  // anything internal that shouldn't be automatically pulled in by 'Ref All Resources' or have
  // initial contents stored. This could either be a type of object that would break if its chunks
  // were inserted into the initialisation phase (like an D3D11 DeviceContext which contains
  // commands) or just debug objects created during capture as helpers which shouldn't be included.
  //
  // The implication is that they are handled specially for being inserted into the capture, or
  // aren't inserted at all. Note that if a resource is frame-referenced, it will be included
  // regardless but still without initial contents, capture drivers should be careful.
  bool InternalResource;
  bool DataWritten;

protected:
  int32_t RefCount;

  byte *DataPtr;
  uint64_t DataOffset;

  ResourceId ResID;

  rdcarray<ResourceRecord *> Parents;

  int64_t GetID()
  {
    static int64_t globalIDCounter = 10;

    return Atomic::Inc64(&globalIDCounter);
  }

  struct StoredChunk
  {
    StoredChunk(int64_t i, Chunk *c)
    {
      id = i;
      // we store this here because by the time it comes to delete the chunks the allocator may have
      // already been reset and the contents trashed.
      fromAllocator = c->IsFromAllocator() ? 1 : 0;
      chunk = c;
    }
    int64_t id : 63;
    int64_t fromAllocator : 1;
    Chunk *chunk;
  };

  rdcarray<StoredChunk> m_Chunks;
  Threading::CriticalSection *m_ChunkLock;

  std::unordered_map<ResourceId, FrameRefType> m_FrameRefs;
};

template <typename Compose>
bool ResourceRecord::MarkResourceFrameReferenced(ResourceId id, FrameRefType refType, Compose comp)
{
  if(id == ResourceId())
    return false;
  return MarkReferenced(m_FrameRefs, id, refType, comp);
}

// the resource manager is a utility class that's not required but is likely wanted by any API
// implementation.
// It keeps track of resource records, which resources are alive and allows you to query for them by
// ID. It tracks which resources are marked as dirty (needing their initial contents fetched before capture).
//
// For APIs that wrap their resources it provides tracking for that.
template <typename Configuration>
class ResourceManager : public ResourceRecordHandler
{
public:
  typedef typename Configuration::WrappedResourceType WrappedResourceType;
  typedef typename Configuration::RealResourceType RealResourceType;
  typedef typename Configuration::RecordType RecordType;
  typedef typename Configuration::InitialContentData InitialContentData;

  ResourceManager(CaptureState &state);
  virtual ~ResourceManager();

  void Shutdown();

  ///////////////////////////////////////////
  // Capture-side methods

  // while capturing, resource records containing chunk streams and metadata for resources
  RecordType *GetResourceRecord(ResourceId id);
  bool HasResourceRecord(ResourceId id);
  RecordType *AddResourceRecord(ResourceId id);
  inline void RemoveResourceRecord(ResourceId id);
  void DestroyResourceRecord(ResourceRecord *record);

  // while capturing or replaying, resources and their IDs
  void AddResource(ResourceId id, WrappedResourceType res);
  bool HasResource(ResourceId id);
  WrappedResourceType GetResource(ResourceId id, bool optional = false);
  void ReleaseResource(ResourceId id);

  // insert the chunks for the resources referenced in the frame
  void InsertReferencedChunks(WriteSerialiser &ser);

  // mark resource records as unwritten, ready to be written to a new logfile.
  void MarkUnwrittenResources();

  // clear the list of frame-referenced resources - e.g. if you're about to recapture a frame
  void ClearReferencedResources();

  // indicates this resource could have been modified by the GPU,
  // so it's now suspect and the data we have on it might well be out of date
  // and to be correct its contents should be serialised out at the start
  // of the frame.
  inline void MarkDirtyResource(ResourceId res);

  // returns if the resource has been marked as dirty
  bool IsResourceDirty(ResourceId res);

  // call callbacks to prepare initial contents for dirty resources
  void PrepareInitialContents();

  InitialContentData GetInitialContents(ResourceId id);
  void SetInitialContents(ResourceId id, InitialContentData contents);
  void SetInitialChunk(ResourceId id, Chunk *chunk);
  void SetInitialFileStore(ResourceId id, const rdcstr &filename, uint64_t start, uint64_t end);

  // generate chunks for initial contents and insert.
  void InsertInitialContentsChunks(WriteSerialiser &ser);

  // for initial contents that don't need a chunk - apply them here. This allows any patching to
  // creation-time chunks to happen before they're written to disk.
  void ApplyInitialContentsNonChunks(WriteSerialiser &ser);

  // Serialise out which resources need initial contents, along with whether their
  // initial contents are in the serialised stream (e.g. RTs might still want to be
  // cleared on frame init).
  void Serialise_InitialContentsNeeded(WriteSerialiser &ser);

  // mark resource referenced somewhere in the main frame-affecting calls.
  // That means this resource should be included in the final serialise out
  template <typename Compose>
  void MarkResourceFrameReferenced(ResourceId id, FrameRefType refType, Compose comp);

  inline void MarkResourceFrameReferenced(ResourceId id, FrameRefType refType);
  void MarkBackgroundFrameReferenced(const rdcflatmap<ResourceId, FrameRefType> &refs);
  void CleanBackgroundFrameReferences();

  ///////////////////////////////////////////
  // Replay-side methods

  // when asked for a given id, return the resource for a replacement id
  void ReplaceResource(ResourceId from, ResourceId to);
  bool HasReplacement(ResourceId from);
  void RemoveReplacement(ResourceId id);

  // get the canonical ID for an ID that may be the id a replacement. i.e. if ID 123 is replaced
  // with 456, then calling this function with either ID 123 or ID 456 will return ID 123.
  ResourceId GetUnreplacedID(ResourceId id);

  // Serialise in which resources need initial contents and set them up.
  void CreateInitialContents(ReadSerialiser &ser);

  // Free any initial contents that are prepared (for after capture is complete)
  void FreeInitialContents();

  // Apply the initial contents for the resources that need them, used at the start of a frame
  void ApplyInitialContents();

  // Resource wrapping, allows for querying and adding/removing of wrapper layers around resources
  bool AddWrapper(WrappedResourceType wrap, RealResourceType real);
  bool HasWrapper(RealResourceType real);
  WrappedResourceType GetWrapper(RealResourceType real);
  void RemoveWrapper(WrappedResourceType wrapped, RealResourceType real);
  void OverrideWrapper(RealResourceType real);

  void ResetLastWriteTimes();
  void ResetCaptureStartTime();

  bool HasPersistentAge(ResourceId id);
  bool HasSkippableAge(ResourceId id);
  bool IsResourcePostponed(ResourceId id);
  bool IsResourceSkipped(ResourceId id);
  bool ShouldPostpone(ResourceId id);
  bool ShouldSkip(ResourceId id);

  virtual bool IsResourceTrackedForPersistency(const WrappedResourceType &res) { return false; }
protected:
  friend InitialContentData;
  // 'interface' to implement by derived classes
  virtual ResourceId GetID(WrappedResourceType res) = 0;

  virtual bool ResourceTypeRelease(WrappedResourceType res) = 0;

  virtual bool Need_InitialStateChunk(ResourceId id, const InitialContentData &initial)
  {
    return true;
  }
  virtual void Begin_PrepareInitialBatch() {}
  virtual bool Prepare_InitialState(WrappedResourceType res) = 0;
  virtual void End_PrepareInitialBatch() {}
  virtual uint64_t GetSize_InitialState(ResourceId id, const InitialContentData &initial) = 0;
  virtual bool Serialise_InitialState(WriteSerialiser &ser, ResourceId id, RecordType *record,
                                      const InitialContentData *initialData) = 0;
  virtual void Create_InitialState(ResourceId id, WrappedResourceType res, bool hasData) = 0;
  virtual void Apply_InitialState(WrappedResourceType res, InitialContentData &initial) = 0;
  virtual rdcarray<ResourceId> InitialContentResources();

  void UpdateLastWriteTime(ResourceId id, FrameRefType refType);

  void Prepare_InitialStateIfPostponed(ResourceId id, bool midframe);
  void SkipOrPostponeOrPrepare_InitialState(ResourceId id, FrameRefType refType);

  // very coarse lock, protects EVERYTHING. This could certainly be improved and it may be a
  // bottleneck for performance. Given that the main use cases are write-rarely read-often the lock
  // should be optimised for that as we only want to make sure we're not modifying the objects
  // together, by far the most common operation is looking up data.
  Threading::CriticalSection m_Lock;

  // we only need to lock during capturing, on replay we have single threaded access.
  bool m_Capturing;

  // easy optimisation win - don't use maps everywhere. It's convenient but not optimal, and
  // profiling will likely prove that some or all of these could be a problem

  // used during capture - map from real resource to its wrapper (other way can be done just with an
  // Unwrap)
  std::map<RealResourceType, WrappedResourceType> m_WrapperMap;

  // used during capture - holds resources referenced in current frame (and how they're referenced)
  std::unordered_map<ResourceId, FrameRefType> m_FrameReferencedResources;

  // used during capture - holds resources marked as dirty, needing initial contents
  std::set<ResourceId> m_DirtyResources;

  struct InitialContentStorage
  {
    Chunk *chunk = NULL;
    InitialContentData data;
    rdcstr filename;
    uint64_t fileStart = 0, fileEnd = 0;

    void Free(ResourceManager *mgr)
    {
      if(chunk)
      {
        chunk->Delete();
        chunk = NULL;
      }

      data.Free(mgr);
    }
  };

  // used during capture or replay - holds initial contents
  std::unordered_map<ResourceId, InitialContentStorage> m_InitialContents;

  // used during capture or replay - map of resources currently alive from their IDs, used in
  // capture and replay.
  std::unordered_map<ResourceId, WrappedResourceType> m_ResourceMap;

  // used during capture - holds resource records by id.
  std::unordered_map<ResourceId, RecordType *> m_ResourceRecords;
  Threading::RWLock m_ResourceRecordLock;

  // used during replay - holds current resource replacements
  // replaced -> replacement
  std::unordered_map<ResourceId, ResourceId> m_Replacements;
  // replacement -> replaced (for looking up canonical IDs)
  std::unordered_map<ResourceId, ResourceId> m_Replaced;

  // During initial resources preparation, persistent resources are
  // postponed until serializing to RDC file.
  std::unordered_set<ResourceId> m_PostponedResourceIDs;
  // During initial resources preparation, resources that are completely written
  // over are skipped
  std::unordered_set<ResourceId> m_SkippedResourceIDs;

  struct ResourceRefTimes
  {
    ResourceId id;

    // On marking resource write-referenced in frame, its last write time is reset. The time is used
    // to determine persistent resources, and is checked against the `PERSISTENT_RESOURCE_AGE`.
    double writeTime;

    // firstSkipTime is referring to the first time we saw a resource get marked as skippable.
    // Any write will reset this time to 0.0 (not skippable), so if the firstSkipTime is longer than
    // `SKIP_RESOURCE_AGE` ago then we know the resource has only ever been skipped (or not written)
    // for that long. If the time is 0.0 then it's been written in a more visible way recently or
    // has never been marked skippable.
    double firstSkipTime;

    bool operator<(const ResourceId &o) const { return id < o; }
  };

  // all resources that are written in some way end up in this list. We then check the last time
  // they were written, and the last time they were ever partially used (not completely overwritten
  // in one atomic chunk).
  rdcarray<ResourceRefTimes> m_ResourceRefTimes;

  // Timestamp at the beginning of the frame capture. Used to determine which
  // resources to refresh for their last write or partial use time (see `ResourceRefTimes`).
  double m_captureStartTime;

  PerformanceTimer m_ResourcesUpdateTimer;

  // The capture state is propagated by a specific driver.
  CaptureState &m_State;
};

template <typename Configuration>
ResourceManager<Configuration>::ResourceManager(CaptureState &state) : m_State(state)
{
  m_Capturing = IsCaptureMode(state);
  RenderDoc::Inst().RegisterMemoryRegion(this, sizeof(ResourceManager));
}

template <typename Configuration>
void ResourceManager<Configuration>::Shutdown()
{
  FreeInitialContents();

  if(!m_Capturing)
  {
    while(!m_ResourceMap.empty())
    {
      auto it = m_ResourceMap.begin();
      ResourceId id = it->first;
      ResourceTypeRelease(it->second);

      auto removeit = m_ResourceMap.find(id);
      if(removeit != m_ResourceMap.end())
        m_ResourceMap.erase(removeit);
    }
  }

  RDCASSERT(m_ResourceRecords.empty());
}

template <typename Configuration>
ResourceManager<Configuration>::~ResourceManager()
{
  RDCASSERT(m_ResourceMap.empty());
  RDCASSERT(m_InitialContents.empty());
  RDCASSERT(m_ResourceRecords.empty());

  RenderDoc::Inst().UnregisterMemoryRegion(this);
}

template <typename Configuration>
void ResourceManager<Configuration>::MarkBackgroundFrameReferenced(
    const rdcflatmap<ResourceId, FrameRefType> &refs)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(IsBackgroundCapturing(m_State))
  {
    if(refs.size() <= m_ResourceRefTimes.size())
    {
      for(auto it = refs.begin(); it != refs.end(); ++it)
        UpdateLastWriteTime(it->first, it->second);
    }
    else
    {
      for(const ResourceRefTimes &res : m_ResourceRefTimes)
      {
        auto it = refs.find(res.id);

        if(it != refs.end())
          UpdateLastWriteTime(it->first, it->second);
      }
    }
  }
}

template <typename Configuration>
void ResourceManager<Configuration>::CleanBackgroundFrameReferences()
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(IsBackgroundCapturing(m_State))
  {
    double now = m_ResourcesUpdateTimer.GetMilliseconds();

    // retire any old entries, if they were written once they shouldn't be tracked forever. This
    // means the list only keeps track of recently written resources, not all resources that have
    // ever been written.
    //
    // walk the array. If we find an item we want to remove we increment src otherwise we copy src
    // to dst (if they are different). Thus next time dst will point at the item we want to skip, at
    // each stage copying src to dst (if they are different). If we find an item we want to remove
    // we increment src and continue (thus it will get copied ove
    size_t dst = 0, src = 0;
    for(dst = 0, src = 0; src < m_ResourceRefTimes.size();)
    {
      ResourceRefTimes &check = m_ResourceRefTimes[src];

      // if this isn't skippable, and the write time was a long time ago then we can delete it.
      // Resources not in the list are treated as if they were written an infinite time ago and so
      // are postponable.
      if(now - check.writeTime > PERSISTENT_RESOURCE_AGE && check.firstSkipTime == 0.0)
      {
        // skip src, check the next one
        src++;
        continue;
      }

      // we want to keep src. If dst == src we can just continue on to the next iteration, if not
      // then we need to copy src into dst (where dst is the leftovers from previously remove
      // entries)
      if(dst != src)
        m_ResourceRefTimes[dst] = m_ResourceRefTimes[src];

      dst++;
      src++;
    }

    m_ResourceRefTimes.resize(dst);
  }
}

template <typename Configuration>
template <typename Compose>
void ResourceManager<Configuration>::MarkResourceFrameReferenced(ResourceId id,
                                                                 FrameRefType refType, Compose comp)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(id == ResourceId())
    return;

  if(IsActiveCapturing(m_State))
  {
    SkipOrPostponeOrPrepare_InitialState(id, refType);

    if(IsDirtyFrameRef(refType))
      Prepare_InitialStateIfPostponed(id, true);
  }

  UpdateLastWriteTime(id, refType);

  if(IsBackgroundCapturing(m_State))
    return;

  bool newRef = MarkReferenced(m_FrameReferencedResources, id, refType, comp);

  if(newRef)
  {
    RecordType *record = GetResourceRecord(id);

    if(record)
      record->AddRef();
  }
}

template <typename Configuration>
void ResourceManager<Configuration>::MarkResourceFrameReferenced(ResourceId id, FrameRefType refType)
{
  return MarkResourceFrameReferenced(id, refType, ComposeFrameRefs);
}

template <typename Configuration>
void ResourceManager<Configuration>::MarkDirtyResource(ResourceId res)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(res == ResourceId())
    return;

  m_DirtyResources.insert(res);
}

template <typename Configuration>
bool ResourceManager<Configuration>::IsResourceDirty(ResourceId res)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(res == ResourceId())
    return false;

  return m_DirtyResources.find(res) != m_DirtyResources.end();
}

template <typename Configuration>
void ResourceManager<Configuration>::SetInitialContents(ResourceId id, InitialContentData contents)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  RDCASSERT(id != ResourceId());

  auto it = m_InitialContents.find(id);

  if(it != m_InitialContents.end())
    it->second.Free(this);

  m_InitialContents[id].data = contents;
}

template <typename Configuration>
void ResourceManager<Configuration>::SetInitialChunk(ResourceId id, Chunk *chunk)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  RDCASSERT(id != ResourceId());
  RDCASSERT(chunk->GetChunkType<SystemChunk>() == SystemChunk::InitialContents);

  InitialContentStorage &data = m_InitialContents[id];

  if(data.chunk)
    data.chunk->Delete();

  data.chunk = chunk;
}

template <typename Configuration>
void ResourceManager<Configuration>::SetInitialFileStore(ResourceId id, const rdcstr &filename,
                                                         uint64_t start, uint64_t end)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  RDCASSERT(id != ResourceId());
  RDCASSERT(!filename.empty());
  RDCASSERT(end > start);

  InitialContentStorage &data = m_InitialContents[id];

  data.filename = filename;
  data.fileStart = start;
  data.fileEnd = end;
}

template <typename Configuration>
typename Configuration::InitialContentData ResourceManager<Configuration>::GetInitialContents(
    ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(id == ResourceId())
    return InitialContentData();

  if(m_InitialContents.find(id) != m_InitialContents.end())
    return m_InitialContents[id].data;

  return InitialContentData();
}

// use a namespace so this doesn't pollute the global namesapce
namespace ResourceManagerInternal
{
struct WrittenRecord
{
  ResourceId id;
  bool written;
};
};

DECLARE_REFLECTION_STRUCT(ResourceManagerInternal::WrittenRecord);

template <class SerialiserType>
void DoSerialise(SerialiserType &ser, ResourceManagerInternal::WrittenRecord &el)
{
  SERIALISE_MEMBER(id);
  SERIALISE_MEMBER(written);
}

template <typename Configuration>
void ResourceManager<Configuration>::Serialise_InitialContentsNeeded(WriteSerialiser &ser)
{
  using namespace ResourceManagerInternal;

  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  // which resources need initial contents? either those which we didn't have proper initialisation
  // for (because they were dirty one way or another) or resources that are written mid-frame and so
  // need to be reset.
  rdcarray<WrittenRecord> NeededInitials;

  // reasonable estimate, and these records are small
  NeededInitials.reserve(m_FrameReferencedResources.size() + m_InitialContents.size());

  // all resources that were recorded as being modified should be included in the list of those
  // needing initial contents
  for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
  {
    RecordType *record = GetResourceRecord(it->first);
    if(IsDirtyFrameRef(it->second))
    {
      WrittenRecord wr = {it->first, record ? record->DataInSerialiser : true};

      NeededInitials.push_back(wr);
    }
  }

  // any resources that had initial contents generated should also be included, even if they're only
  // referenced read-only, as anything not in this list will have its initial contents freed on
  // replay (see CreateInitialContents). However we only need to keep resources that are referenced
  // (unless we have ref all resources on)
  for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
  {
    bool include = RenderDoc::Inst().GetCaptureOptions().refAllResources;

    ResourceId id = it->first;
    if(m_FrameReferencedResources.find(id) != m_FrameReferencedResources.end())
      include = true;

    if(include)
    {
      WrittenRecord wr = {id, true};

      NeededInitials.push_back(wr);
    }
  }

  uint64_t chunkSize = uint64_t(NeededInitials.size() * sizeof(WrittenRecord) + 16);

  SCOPED_SERIALISE_CHUNK(SystemChunk::InitialContentsList, chunkSize);
  SERIALISE_ELEMENT(NeededInitials);
}

template <typename Configuration>
void ResourceManager<Configuration>::FreeInitialContents()
{
  while(!m_InitialContents.empty())
  {
    auto it = m_InitialContents.begin();
    it->second.Free(this);
    if(!m_InitialContents.empty())
      m_InitialContents.erase(m_InitialContents.begin());
  }
  m_PostponedResourceIDs.clear();
  m_SkippedResourceIDs.clear();
}

template <typename Configuration>
void ResourceManager<Configuration>::Prepare_InitialStateIfPostponed(ResourceId id, bool midframe)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(!IsResourcePostponed(id))
    return;

  if(midframe)
  {
    RDCLOG("Preparing resource %s after it has been postponed.", ToStr(id).c_str());
    Begin_PrepareInitialBatch();
  }

  WrappedResourceType res = GetResource(id);
  Prepare_InitialState(res);

  if(midframe)
    End_PrepareInitialBatch();

  m_PostponedResourceIDs.erase(id);
}

template <typename Configuration>
void ResourceManager<Configuration>::SkipOrPostponeOrPrepare_InitialState(ResourceId id,
                                                                          FrameRefType refType)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(!IsResourceSkipped(id))
    return;

  // the first time we encounter a skipped resource, we can choose to
  // skip this resource forever, convert it to be postponed, or prepare
  // it immediately. We can't retrieve its initial state once it has
  // been written over.
  m_SkippedResourceIDs.erase(id);

  // skip this forever if the first encounter is a complete write
  if(IsCompleteWriteFrameRef(refType))
  {
    RDCDEBUG("Resource %s skipped forever on refType of %s", ToStr(id).c_str(),
             ToStr(refType).c_str());
    return;
  }

  // If this resource is only being read, we might as well try to
  // postpone it to conserve memory consumption.
  if(!IsDirtyFrameRef(refType) && IsResourceTrackedForPersistency(GetResource(id)))
  {
    m_PostponedResourceIDs.insert(id);
    RDCDEBUG("Resource %s converted from skipped to postponed on refType of %s", ToStr(id).c_str(),
             ToStr(refType).c_str());
    SetInitialContents(id, InitialContentData());
  }
  else
  {
    WrappedResourceType res = GetResource(id);
    RDCDEBUG("Preparing resource %s after it has been skipped on refType of %s", ToStr(id).c_str(),
             ToStr(refType).c_str());
    Begin_PrepareInitialBatch();
    Prepare_InitialState(res);
    End_PrepareInitialBatch();
  }
}

template <typename Configuration>
inline void ResourceManager<Configuration>::ResetCaptureStartTime()
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
  // This time is used to analyze which resources to refresh
  // for their last write time.
  m_captureStartTime = m_ResourcesUpdateTimer.GetMilliseconds();
}

template <typename Configuration>
inline void ResourceManager<Configuration>::ResetLastWriteTimes()
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
  for(auto it = m_ResourceRefTimes.begin(); it != m_ResourceRefTimes.end(); ++it)
  {
    // Reset only those resources which were below the threshold on
    // capture start. Other resource are already above the threshold.
    if(m_captureStartTime - it->writeTime <= PERSISTENT_RESOURCE_AGE)
      it->writeTime = m_ResourcesUpdateTimer.GetMilliseconds();
  }
}

template <typename Configuration>
inline void ResourceManager<Configuration>::UpdateLastWriteTime(ResourceId id, FrameRefType refType)
{
  // parent must hold m_Lock for us

  // only care about write refs. A read ref would invalidate skippable state, however a skippable
  // resource is left in an undefined state where reads are not valid so we don't. We need to see
  // another write first before a read could be a problem, so we just pay attention for that write.
  if(!IsDirtyFrameRef(refType))
    return;

  ResourceRefTimes *it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);

  if(it == m_ResourceRefTimes.end() || it->id != id)
  {
    // if it's not pointing to the end, figure out where we need to insert it there
    size_t idx = it - m_ResourceRefTimes.begin();
    m_ResourceRefTimes.insert(idx, {id, 0.0, 0.0});
    it = m_ResourceRefTimes.begin() + idx;
  }

  double now = m_ResourcesUpdateTimer.GetMilliseconds();

  it->writeTime = now;

  if(refType == eFrameRef_CompleteWriteAndDiscard)
  {
    // don't continually update it. We want to know that this resource *was* completely written and
    // discarded, and hasn't been written in any other way since then.
    if(it->firstSkipTime == 0.0)
      it->firstSkipTime = now;
  }
  else
  {
    it->firstSkipTime = 0.0;
  }
}

template <typename Configuration>
inline bool ResourceManager<Configuration>::HasPersistentAge(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  ResourceRefTimes *it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);

  if(it == m_ResourceRefTimes.end() || it->id != id)
    return true;

  return m_ResourcesUpdateTimer.GetMilliseconds() - it->writeTime >= PERSISTENT_RESOURCE_AGE;
}

template <typename Configuration>
inline bool ResourceManager<Configuration>::HasSkippableAge(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  ResourceRefTimes *it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);

  // if it doesn't have a write time, it can't be skippable
  if(it == m_ResourceRefTimes.end() || it->id != id)
    return false;

  // if it's never been skipped or it was reset, it's also not skippable
  if(it->firstSkipTime == 0.0)
    return false;

  return m_ResourcesUpdateTimer.GetMilliseconds() - it->firstSkipTime >= SKIP_RESOURCE_AGE;
}

template <typename Configuration>
inline bool ResourceManager<Configuration>::IsResourcePostponed(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
  return m_PostponedResourceIDs.find(id) != m_PostponedResourceIDs.end();
}

template <typename Configuration>
inline bool ResourceManager<Configuration>::IsResourceSkipped(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
  return m_SkippedResourceIDs.find(id) != m_SkippedResourceIDs.end();
}

template <typename Configuration>
inline bool ResourceManager<Configuration>::ShouldPostpone(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  WrappedResourceType res = GetResource(id);

  if(!IsResourceTrackedForPersistency(res))
    return false;

  return HasPersistentAge(id);
}

template <typename Configuration>
inline bool ResourceManager<Configuration>::ShouldSkip(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  WrappedResourceType res = GetResource(id);

  if(!IsResourceTrackedForPersistency(res))
    return false;

  return HasSkippableAge(id);
}

template <typename Configuration>
void ResourceManager<Configuration>::CreateInitialContents(ReadSerialiser &ser)
{
  using namespace ResourceManagerInternal;

  std::unordered_set<ResourceId> ids;

  rdcarray<WrittenRecord> NeededInitials;
  SERIALISE_ELEMENT(NeededInitials);

  for(const WrittenRecord &wr : NeededInitials)
  {
    ResourceId id = wr.id;

    ids.insert(id);

    // if this resource exists and we don't have initial contents for it serialised, create some for
    // reset purposes.
    if(HasResource(id) && m_InitialContents.find(id) == m_InitialContents.end())
      Create_InitialState(id, GetResource(id), wr.written);
  }

  // any initial contents that we ended up with which we don't need can be freed now
  for(auto it = m_InitialContents.begin(); it != m_InitialContents.end();)
  {
    ResourceId id = it->first;

    if(ids.find(id) == ids.end())
    {
      it->second.Free(this);
      ++it;
      m_InitialContents.erase(id);
    }
    else
    {
      ++it;
    }
  }
}

template <typename Configuration>
void ResourceManager<Configuration>::ApplyInitialContents()
{
  RDCDEBUG("Applying initial contents");
  rdcarray<ResourceId> resources = InitialContentResources();
  for(auto it = resources.begin(); it != resources.end(); ++it)
  {
    ResourceId id = *it;
    InitialContentStorage &data = m_InitialContents[id];
    WrappedResourceType live = GetResource(id);
    Apply_InitialState(live, data.data);
  }
  RDCDEBUG("Applied %d", (uint32_t)resources.size());
}

template <typename Configuration>
rdcarray<ResourceId> ResourceManager<Configuration>::InitialContentResources()
{
  rdcarray<ResourceId> resources;
  for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
  {
    ResourceId id = it->first;

    if(IsCaptureMode(m_State) || HasResource(id))
    {
      resources.push_back(id);
    }
  }
  return resources;
}

template <typename Configuration>
void ResourceManager<Configuration>::MarkUnwrittenResources()
{
  SCOPED_READLOCK(m_ResourceRecordLock);

  for(auto it = m_ResourceRecords.begin(); it != m_ResourceRecords.end(); ++it)
    it->second->MarkDataUnwritten();
}

template <typename Configuration>
void ResourceManager<Configuration>::InsertReferencedChunks(WriteSerialiser &ser)
{
  std::map<int64_t, Chunk *> sortedChunks;

  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  RDCDEBUG("%u frame resource records", (uint32_t)m_FrameReferencedResources.size());

  if(RenderDoc::Inst().GetCaptureOptions().refAllResources)
  {
    SCOPED_READLOCK(m_ResourceRecordLock);

    float num = float(m_ResourceRecords.size());
    float idx = 0.0f;

    for(auto it = m_ResourceRecords.begin(); it != m_ResourceRecords.end(); ++it)
    {
      RenderDoc::Inst().SetProgress(CaptureProgress::AddReferencedResources, idx / num);
      idx += 1.0f;

      if(m_FrameReferencedResources.find(it->first) == m_FrameReferencedResources.end() &&
         it->second->InternalResource)
        continue;

      it->second->Insert(sortedChunks);
    }
  }
  else
  {
    float num = float(m_FrameReferencedResources.size());
    float idx = 0.0f;

    for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
    {
      RenderDoc::Inst().SetProgress(CaptureProgress::AddReferencedResources, idx / num);
      idx += 1.0f;

      RecordType *record = GetResourceRecord(it->first);
      if(record)
        record->Insert(sortedChunks);
    }
  }

  RDCDEBUG("%u frame resource chunks", (uint32_t)sortedChunks.size());

  for(auto it = sortedChunks.begin(); it != sortedChunks.end(); it++)
    it->second->Write(ser);

  RDCDEBUG("inserted to serialiser");
}

template <typename Configuration>
void ResourceManager<Configuration>::PrepareInitialContents()
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  RDCLOG("Preparing up to %u potentially dirty resources", (uint32_t)m_DirtyResources.size());
  uint32_t prepared = 0;
  uint32_t postponed = 0;
  uint32_t skipped = 0;

  float num = float(m_DirtyResources.size());
  float idx = 0.0f;

  Begin_PrepareInitialBatch();

  for(auto it = m_DirtyResources.begin(); it != m_DirtyResources.end(); ++it)
  {
    ResourceId id = *it;

    RenderDoc::Inst().SetProgress(CaptureProgress::PrepareInitialStates, idx / num);
    idx += 1.0f;

    // if somehow this resource has been deleted but is still dirty, we can't prepare it. Resources
    // deleted prior to beginning the frame capture cannot linger and be needed - we only need to
    // care about resources deleted after this point (mid-capture)
    if(!HasResource(id))
      continue;

    RecordType *record = GetResourceRecord(id);
    WrappedResourceType res = GetResource(id);

    // don't prepare internal resources, or those without a record
    if(record == NULL || record->InternalResource)
      continue;

    if(ShouldSkip(id))
    {
      m_SkippedResourceIDs.insert(id);
      skipped++;
      continue;
    }

    if(ShouldPostpone(id))
    {
      m_PostponedResourceIDs.insert(id);
      // Set empty contents here, it'll be prepared on serialization.
      SetInitialContents(id, InitialContentData());
      postponed++;
      continue;
    }

    prepared++;

#if ENABLED(VERBOSE_DIRTY_RESOURCES)
    RDCDEBUG("Prepare Resource %s", ToStr(id).c_str());
#endif

    Prepare_InitialState(res);
  }

  End_PrepareInitialBatch();

  RDCLOG("Prepared %u dirty resources, postponed %u, skipped %u", prepared, postponed, skipped);
}

template <typename Configuration>
void ResourceManager<Configuration>::InsertInitialContentsChunks(WriteSerialiser &ser)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  uint32_t dirty = 0;
  uint32_t skipped = 0;

  RDCLOG("Checking %u resources with initial contents against %u referenced resources",
         (uint32_t)m_InitialContents.size(), (uint32_t)m_FrameReferencedResources.size());

  float num = float(m_InitialContents.size());
  float idx = 0.0f;

  Begin_PrepareInitialBatch();

  for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
  {
    ResourceId id = it->first;

    if(m_FrameReferencedResources.find(id) == m_FrameReferencedResources.end() &&
       !RenderDoc::Inst().GetCaptureOptions().refAllResources)
    {
      continue;
    }

    RecordType *record = GetResourceRecord(id);

    if(record == NULL)
      continue;

    if(record->InternalResource)
      continue;

    // Load postponed resource if needed.
    Prepare_InitialStateIfPostponed(id, false);
  }

  End_PrepareInitialBatch();

  rdcarray<ResourceId> resources = InitialContentResources();
  for(auto it = resources.begin(); it != resources.end(); ++it)
  {
    ResourceId id = *it;

    RenderDoc::Inst().SetProgress(CaptureProgress::SerialiseInitialStates, idx / num);
    idx += 1.0f;

    if(m_FrameReferencedResources.find(id) == m_FrameReferencedResources.end() &&
       !RenderDoc::Inst().GetCaptureOptions().refAllResources)
    {
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
      RDCDEBUG("Dirty resource %s is GPU dirty but not referenced - skipping", ToStr(id).c_str());
#endif
      skipped++;
      continue;
    }

    RecordType *record = GetResourceRecord(id);

    if(record == NULL)
    {
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
      RDCDEBUG("Resource %s has no resource record - skipping", ToStr(id).c_str());
#endif
      continue;
    }

    if(record->InternalResource)
    {
#if ENABLED(VERBOSE_DIRTY_RESOURCES)
      RDCDEBUG("Resource %s is special - skipping", ToStr(id).c_str());
#endif
      continue;
    }

#if ENABLED(VERBOSE_DIRTY_RESOURCES)
    RDCDEBUG("Serialising dirty Resource %s", ToStr(id).c_str());
#endif

    dirty++;

    InitialContentStorage &data = m_InitialContents[id];

    if(!Need_InitialStateChunk(id, data.data))
    {
      // this was handled in ApplyInitialContentsNonChunks(), do nothing as there's no point copying
      // the data again (it's already been serialised).
      continue;
    }

    if(data.chunk)
    {
      data.chunk->Write(ser);
    }
    else if(!data.filename.empty())
    {
      FILE *f = FileIO::fopen(data.filename, FileIO::ReadBinary);
      FileIO::fseek64(f, data.fileStart, SEEK_SET);
      StreamReader reader(f, data.fileEnd - data.fileStart, Ownership::Stream);

      StreamTransfer(ser.GetWriter(), &reader, NULL);
    }
    else
    {
      uint64_t size = GetSize_InitialState(id, data.data);

      SCOPED_SERIALISE_CHUNK(SystemChunk::InitialContents, size);

      Serialise_InitialState(ser, id, record, &data.data);
    }

    // Reset back to empty contents, unloading the actual resource.
    SetInitialContents(id, InitialContentData());
  }

  RDCLOG("Serialised %u resources, skipped %u unreferenced", dirty, skipped);
}

template <typename Configuration>
void ResourceManager<Configuration>::ApplyInitialContentsNonChunks(WriteSerialiser &ser)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  for(auto it = m_InitialContents.begin(); it != m_InitialContents.end(); ++it)
  {
    ResourceId id = it->first;

    if(m_FrameReferencedResources.find(id) == m_FrameReferencedResources.end() &&
       !RenderDoc::Inst().GetCaptureOptions().refAllResources)
    {
      continue;
    }

    RecordType *record = GetResourceRecord(id);

    if(!record || record->InternalResource)
      continue;

    if(!Need_InitialStateChunk(id, it->second.data))
      Serialise_InitialState(ser, id, record, &it->second.data);
  }
}

template <typename Configuration>
void ResourceManager<Configuration>::ClearReferencedResources()
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
  {
    RecordType *record = GetResourceRecord(it->first);

    if(record)
    {
      if(IncludesWrite(it->second))
        MarkDirtyResource(it->first);
      record->Delete(this);
    }
  }

  m_FrameReferencedResources.clear();
}

template <typename Configuration>
void ResourceManager<Configuration>::ReplaceResource(ResourceId from, ResourceId to)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(HasResource(to))
  {
    m_Replacements[from] = to;
    m_Replaced[to] = from;
  }
}

template <typename Configuration>
bool ResourceManager<Configuration>::HasReplacement(ResourceId from)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  return m_Replacements.find(from) != m_Replacements.end();
}

template <typename Configuration>
void ResourceManager<Configuration>::RemoveReplacement(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  auto it = m_Replacements.find(id);

  if(it == m_Replacements.end())
    return;

  m_Replaced.erase(it->second);
  m_Replacements.erase(it);
}

template <typename Configuration>
typename Configuration::RecordType *ResourceManager<Configuration>::GetResourceRecord(ResourceId id)
{
  SCOPED_READLOCK(m_ResourceRecordLock);

  auto it = m_ResourceRecords.find(id);

  if(it == m_ResourceRecords.end())
    return NULL;

  return it->second;
}

template <typename Configuration>
bool ResourceManager<Configuration>::HasResourceRecord(ResourceId id)
{
  SCOPED_READLOCK(m_ResourceRecordLock);

  auto it = m_ResourceRecords.find(id);

  if(it == m_ResourceRecords.end())
    return false;

  return true;
}

template <typename Configuration>
typename Configuration::RecordType *ResourceManager<Configuration>::AddResourceRecord(ResourceId id)
{
  SCOPED_WRITELOCK(m_ResourceRecordLock);

  RDCASSERT(m_ResourceRecords.find(id) == m_ResourceRecords.end(), id);

  return (m_ResourceRecords[id] = new RecordType(id));
}

template <typename Configuration>
void ResourceManager<Configuration>::RemoveResourceRecord(ResourceId id)
{
  SCOPED_WRITELOCK(m_ResourceRecordLock);

  RDCASSERT(m_ResourceRecords.find(id) != m_ResourceRecords.end(), id);

  m_ResourceRecords.erase(id);
}

template <typename Configuration>
void ResourceManager<Configuration>::DestroyResourceRecord(ResourceRecord *record)
{
  delete(RecordType *)record;
}

template <typename Configuration>
bool ResourceManager<Configuration>::AddWrapper(WrappedResourceType wrap, RealResourceType real)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  bool ret = true;

  if(wrap == (WrappedResourceType)RecordType::NullResource ||
     real == (RealResourceType)RecordType::NullResource)
  {
    RDCERR("Invalid state creating resource wrapper - wrapped or real resource is NULL");
    ret = false;
  }

  if(m_WrapperMap[real] != (WrappedResourceType)RecordType::NullResource)
  {
    RDCERR("Overriding wrapper for resource");
    ret = false;
  }

  m_WrapperMap[real] = wrap;

  return ret;
}

template <typename Configuration>
void ResourceManager<Configuration>::RemoveWrapper(WrappedResourceType wrapped, RealResourceType real)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(real == (RealResourceType)RecordType::NullResource)
  {
    RDCERR("Invalid state removing resource wrapper - real resource is NULL");
    return;
  }

  auto it = m_WrapperMap.find(real);

  // silently ignore/drop removals of non-canonical wrappers. This handles the case where we have
  // multiple wrappers for the same object on replay, due to API deduplication between an internal
  // object and an application-created object. Backends are expected to deduplicate during capture
  // (See OverrideWrapper below)
  if(it != m_WrapperMap.end() && it->second == wrapped)
    m_WrapperMap.erase(it);
}

template <typename Configuration>
void ResourceManager<Configuration>::OverrideWrapper(RealResourceType real)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  // during replay we may find that we have extra duplicate wrappers because of internal resources,
  // even though we trid to deduplicate on capture. For this case we remove the old wrapper and
  // allow the new wrapper to become canonical.
  auto it = m_WrapperMap.find(real);

  if(it != m_WrapperMap.end())
    m_WrapperMap.erase(it);
}

template <typename Configuration>
bool ResourceManager<Configuration>::HasWrapper(RealResourceType real)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(real == (RealResourceType)RecordType::NullResource)
    return false;

  return (m_WrapperMap.find(real) != m_WrapperMap.end());
}

template <typename Configuration>
typename Configuration::WrappedResourceType ResourceManager<Configuration>::GetWrapper(
    RealResourceType real)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(real == (RealResourceType)RecordType::NullResource)
    return (WrappedResourceType)RecordType::NullResource;

  if(real != (RealResourceType)RecordType::NullResource && !HasWrapper(real))
  {
    RDCERR(
        "Invalid state removing resource wrapper - real resource isn't NULL and doesn't have "
        "wrapper");
  }

  return m_WrapperMap[real];
}

template <typename Configuration>
typename Configuration::WrappedResourceType ResourceManager<Configuration>::GetResource(ResourceId id,
                                                                                        bool optional)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(id == ResourceId())
    return (WrappedResourceType)RecordType::NullResource;

#if DISABLED(RDOC_RELEASE)
  if(!optional)
  {
    RDCASSERT(HasResource(id), id);
  }
#endif

  {
    auto it = m_Replacements.find(id);
    if(it != m_Replacements.end())
      id = it->second;
  }

  {
    auto it = m_ResourceMap.find(id);
    if(it != m_ResourceMap.end())
      return it->second;
  }

  return (WrappedResourceType)RecordType::NullResource;
}

template <typename Configuration>
void ResourceManager<Configuration>::AddResource(ResourceId id, WrappedResourceType res)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  RDCASSERT(m_ResourceMap.find(id) == m_ResourceMap.end());

  m_ResourceMap[id] = res;
}

template <typename Configuration>
bool ResourceManager<Configuration>::HasResource(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  if(id == ResourceId())
    return false;

  return m_Replacements.find(id) != m_Replacements.end() ||
         m_ResourceMap.find(id) != m_ResourceMap.end();
}

template <typename Configuration>
void ResourceManager<Configuration>::ReleaseResource(ResourceId id)
{
  SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);

  // We potentially need to prepare this resource on Active Capture,
  // if it was postponed, but is about to go away.
  if(IsActiveCapturing(m_State))
    Prepare_InitialStateIfPostponed(id, true);

  m_ResourceMap.erase(id);
  m_DirtyResources.erase(id);

  auto it = std::lower_bound(m_ResourceRefTimes.begin(), m_ResourceRefTimes.end(), id);
  if(it != m_ResourceRefTimes.end())
    m_ResourceRefTimes.erase(it - m_ResourceRefTimes.begin());
}

template <typename Configuration>
ResourceId ResourceManager<Configuration>::GetUnreplacedID(ResourceId id)
{
  if(id == ResourceId())
    return id;

  if(m_Replaced.find(id) != m_Replaced.end())
    return m_Replaced[id];

  return id;
}
