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

#pragma once

#include <set>
#include "api/replay/replay_enums.h"
#include "api/replay/structured_data.h"
#include "common/formatting.h"
#include "common/result.h"
#include "streamio.h"

// function to deallocate anything from a serialise. Default impl
// does no deallocation of anything.
template <class T>
void Deserialise(const T &el)
{
}

#define DECLARE_DESERIALISE_TYPE(type) \
  template <>                          \
  void Deserialise(const type &el);

// this is a bit of a hack, but necessary. We don't want to have all of our DoSerialise functions
// defined in global cross-project headers and compiled everywhere, we just want to define them in
// .cpp files.
// However because they are template specializations, so they need to be explicitly instantiated in
// one file to link elsewhere.
#define INSTANTIATE_SERIALISE_TYPE(type)                                    \
  template void DoSerialise(Serialiser<SerialiserMode::Writing> &, type &); \
  template void DoSerialise(Serialiser<SerialiserMode::Reading> &, type &);

typedef rdcstr (*ChunkLookup)(uint32_t chunkType);

enum class SerialiserFlags
{
  NoFlags = 0x0,
  AllocateMemory = 0x1,
};

BITMASK_OPERATORS(SerialiserFlags);

// This class is used to read and write arbitrary structured data from a stream. The primary
// mechanism is in template overloads of DoSerialise functions for each struct that can be
// serialised, down to primitive types (ints, floats, strings, etc).
//
// A serialised stream is defined in terms of 'chunks', each with an ID and some metadata attached
// like timestamp, duration, callstack, etc. Each chunk can then have objects serialised into it
// which automatically recurse and serialise all their members. When reading from/writing to a
// stream then the binary form is purely data - no structure encoded. When reading, the data can be
// optionally pulled out into a structured form that can then be manipulated or exported.
//
// Since a stream can be an in-memory buffer, a file, or a network socket this class is used to
// serialised complex data anywhere that we need structured I/O.

enum class SerialiserMode
{
  Writing,
  Reading,
};

struct CompressedFileIO;

template <SerialiserMode sertype>
class Serialiser
{
public:
  static constexpr bool IsReading() { return sertype != SerialiserMode::Writing; }
  static constexpr bool IsWriting() { return sertype == SerialiserMode::Writing; }
  bool IsStructurising() const { return m_Structuriser; }
  bool ExportStructure() const
  {
    // in debug builds, allow structured export during write for debugging. In release, only allow
    // it on read to compile out the extra code on the writing path
    return
#if ENABLED(RDOC_RELEASE)
        sertype == SerialiserMode::Reading &&
#endif
        m_ExportStructured && m_InternalElement == 0;
  }

  enum ChunkFlags
  {
    ChunkIndexMask = 0x0000ffff,
    ChunkCallstack = 0x00010000,
    ChunkThreadID = 0x00020000,
    ChunkDuration = 0x00040000,
    ChunkTimestamp = 0x00080000,
    Chunk64BitSize = 0x00100000,
  };

  //////////////////////////////////////////
  // Init and error handling
  ~Serialiser();

  // no copies
  Serialiser(const Serialiser &other) = delete;

  bool IsErrored() { return IsReading() ? m_Read->IsErrored() : m_Write->IsErrored(); }
  RDResult GetError() { return IsReading() ? m_Read->GetError() : m_Write->GetError(); }
  void SetError(RDResult result)
  {
    IsReading() ? m_Read->SetError(result) : m_Write->SetError(result);
  }
  StreamWriter *GetWriter() { return m_Write; }
  StreamReader *GetReader() { return m_Read; }
  uint32_t GetChunkMetadataRecording() { return m_ChunkFlags; }
  void SetChunkMetadataRecording(uint32_t flags);
  void SetChunkTimestampBasis(uint64_t base, double freq)
  {
    m_TimerBase = base;
    m_TimerFrequency = freq;
  }

  // debug-only option to dump out (roughly) the data going through the serialiser as it happens
  void EnableDumping(FileIO::LogFileHandle *debugLog) { m_DebugDumpLog = debugLog; }
  SDChunkMetaData &ChunkMetadata() { return m_ChunkMetadata; }
  //////////////////////////////////////////
  // Utility functions

  static uint64_t GetChunkAlignment() { return ChunkAlignment; }
  void *GetUserData() { return m_pUserData; }
  void SetUserData(void *userData) { m_pUserData = userData; }
  void SetStringDatabase(std::set<rdcstr> *db) { m_ExtStringDB = db; }
  // jumps to the byte after the current chunk, can be called any time after BeginChunk
  void SkipCurrentChunk();

  //////////////////////////////////////////
  // Version checking

  void SetVersion(uint64_t version) { m_Version = version; }
  // assume that we always write the latest version, so on writing the version check always passes.
  bool VersionAtLeast(uint64_t req) const { return IsWriting() || m_Version >= req; }
  bool VersionLess(uint64_t req) const { return IsReading() && m_Version < req; }
  // enable 'streaming mode' for ephemeral transfers like temporary I/O over sockets, where there's
  // no need for the chunk length - avoids needing to seek internally in a stream that might not
  // support seeking to fixup lengths, while also not requiring conservative length estimates
  // up-front
  void SetStreamingMode(bool stream) { m_DataStreaming = stream; }
  SDFile &GetStructuredFile() { return *m_StructuredFile; }
  void WriteStructuredFile(const SDFile &file, RENDERDOC_ProgressCallback progress);
  void SetActionChunk() { m_ActionChunk = true; }
  // the struct argument allows nested structs to pass a bit of data so a child struct can have
  // context from a parent struct if needed to serialise properly. Rarely used, primarily to be able
  // to flag if some context-sensitive members might be invalid
  void SetStructArg(uint64_t arg) { m_StructArg = arg; }
  uint64_t GetStructArg() { return m_StructArg; }

  // in rare cases it is necessary to pass side-band data even further than the small context
  // allowed by SetStructArg, from serialisation users all the way down to child structs. We allow
  // adding side-band data indexed by 'GUID' here for those rare cases.
  template <typename T>
  T GetSidebandData(uint64_t guid)
  {
    RDCCOMPILE_ASSERT(sizeof(T) <= sizeof(uint64_t), "Side-band data is at most 64-bit");

    T ret;
    for(rdcpair<uint64_t, uint64_t> &kv : m_SidebandKV)
    {
      if(kv.first == guid)
      {
        memcpy(&ret, &kv.second, sizeof(T));
        return ret;
      }
    }

    return ret;
  }
  template <typename T>
  void SetSidebandData(uint64_t guid, const T &t)
  {
    RDCCOMPILE_ASSERT(sizeof(T) <= sizeof(uint64_t), "Side-band data is at most 64-bit");
    uint64_t data = 0;
    memcpy(&data, &t, sizeof(T));
    m_SidebandKV.push_back({guid, data});
  }

  //////////////////////////////////////////
  // Public serialisation interface

  void ConfigureStructuredExport(ChunkLookup lookup, bool includeBuffers, uint64_t timeBase,
                                 double timeFreq)
  {
    m_ChunkLookup = lookup;
    m_ExportBuffers = includeBuffers;
    m_ExportStructured = (lookup != NULL);
    m_TimerBase = timeBase;
    m_TimerFrequency = timeFreq;
  }

  uint32_t BeginChunk(uint32_t chunkID, uint64_t byteLength);
  void EndChunk();

  rdcstr GetCurChunkName()
  {
    if(m_ChunkLookup)
      return m_ChunkLookup(m_ChunkMetadata.chunkID);

    return StringFormat::Fmt("<No Chunk Lookup: %u>", m_ChunkMetadata.chunkID);
  }
  ChunkLookup GetChunkLookup() { return m_ChunkLookup; }
  /////////////////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////////////////
  // Templated serialisation functions

  // Users of the serialisation call Serialise() on the objects they want
  // to serialise

  // for structure type members or external use, main entry point

  template <typename T>
  void SerialiseStringify(const T el)
  {
    if(ExportStructure())
    {
      m_StructureStack.back()->data.str = ToStr(el);
      m_StructureStack.back()->type.flags |= SDTypeFlags::HasCustomString;
    }
  }

  template <class T>
  void ClearObj(T &el)
  {
    if(IsReading())
      m_Read->Clear(&el, sizeof(T));
  }

  // serialise an object (either loose, or a structure member).
  template <class T>
  Serialiser &Serialise(const rdcliteral &name, T &el,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &current = *m_StructureStack.back();

      SDObject &obj = *current.AddAndOwnChild(new SDObject(name, TypeName<T>()));
      m_StructureStack.push_back(&obj);

      obj.type.byteSize = sizeof(T);
      if(std::is_union<T>::value)
        obj.type.flags |= SDTypeFlags::Union;
    }

    SerialiseDispatch<Serialiser, T>::Do(*this, el);

    if(ExportStructure())
      m_StructureStack.pop_back();

    return *this;
  }

  // special function for serialising buffers
  Serialiser &Serialise(const rdcliteral &name, byte *&el, uint64_t byteSize,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    // silently handle NULL buffers
    if(IsWriting() && el == NULL)
      byteSize = 0;

    {
      m_InternalElement++;
      DoSerialise(*this, byteSize);
      m_InternalElement--;
    }

    if(IsReading())
    {
      VerifyArraySize(byteSize);
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &current = *m_StructureStack.back();

      SDObject &obj = *current.AddAndOwnChild(new SDObject(name, "Byte Buffer"_lit));
      m_StructureStack.push_back(&obj);

      obj.type.basetype = SDBasic::Buffer;
      obj.type.byteSize = byteSize;
    }

    byte *tempAlloc = NULL;

    {
      if(IsWriting())
      {
        // ensure byte alignment
        m_Write->AlignTo<ChunkAlignment>();

        if(el)
          m_Write->Write(el, byteSize);
        else
          RDCASSERT(byteSize == 0);
      }
      else if(IsReading())
      {
        // ensure byte alignment
        m_Read->AlignTo<ChunkAlignment>();

// Coverity is unable to tie this allocation together with the automatic scoped deallocation in the
// ScopedDeseralise* classes. We can verify with e.g. valgrind that there are no leaks, so to keep
// the analysis non-spammy we just don't allocate for coverity builds
#if !defined(__COVERITY__)
        if(!m_Structuriser && (flags & SerialiserFlags::AllocateMemory))
        {
          if(byteSize > 0)
            el = AllocAlignedBuffer(byteSize);
          else
            el = NULL;
        }

        // if we're exporting the buffers, make sure to always alloc space to read the data, so we
        // can save it out, even if the external code has no use for it and has asked for no
        // allocation.
        if(el == NULL && ExportStructure() && m_ExportBuffers)
        {
          if(byteSize > 0)
            el = tempAlloc = AllocAlignedBuffer(byteSize);
          else
            el = NULL;
        }
#endif

        m_Read->Read(el, byteSize);
      }
    }

    if(ExportStructure())
    {
      if(m_ExportBuffers)
      {
        SDObject &obj = *m_StructureStack.back();

        obj.data.basic.u = m_StructuredFile->buffers.size();

        bytebuf *alloc = new bytebuf;
        alloc->resize((size_t)byteSize);
        if(el)
          memcpy(alloc->data(), el, (size_t)byteSize);

        m_StructuredFile->buffers.push_back(alloc);
      }

      m_StructureStack.pop_back();
    }

#if !defined(__COVERITY__)
    if(tempAlloc)
    {
      FreeAlignedBuffer(tempAlloc);
      el = NULL;
    }
#endif

    return *this;
  }

  Serialiser &Serialise(const rdcliteral &name, bytebuf &el,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    uint64_t count = (uint64_t)el.size();

    {
      m_InternalElement++;
      DoSerialise(*this, count);
      m_InternalElement--;
    }

    if(IsReading())
    {
      VerifyArraySize(count);
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &current = *m_StructureStack.back();

      SDObject &obj = *current.AddAndOwnChild(new SDObject(name, "Byte Buffer"_lit));
      m_StructureStack.push_back(&obj);

      obj.type.basetype = SDBasic::Buffer;
      obj.type.byteSize = count;
    }

    {
      if(IsWriting())
      {
        // ensure byte alignment
        m_Write->AlignTo<ChunkAlignment>();
        m_Write->Write(el.data(), count);
      }
      else if(IsReading())
      {
        // ensure byte alignment
        m_Read->AlignTo<ChunkAlignment>();

        el.resize((size_t)count);

        m_Read->Read(el.data(), count);
      }
    }

    if(ExportStructure())
    {
      if(m_ExportBuffers)
      {
        SDObject &obj = *m_StructureStack.back();

        obj.data.basic.u = m_StructuredFile->buffers.size();

        bytebuf *alloc = new bytebuf;
        alloc->assign(el);

        m_StructuredFile->buffers.push_back(alloc);
      }

      m_StructureStack.pop_back();
    }

    return *this;
  }

  Serialiser &Serialise(const rdcliteral &name, void *&el, uint64_t byteSize,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return Serialise(name, (byte *&)el, byteSize, flags);
  }

  Serialiser &Serialise(const rdcliteral &name, const void *&el, uint64_t byteSize,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return Serialise(name, (byte *&)el, byteSize, flags);
  }

#if ENABLED(RDOC_SIZET_SEP_TYPE)
  Serialiser &Serialise(const rdcliteral &name, void *&el, size_t &byteSize,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return Serialise(name, (byte *&)el, byteSize, flags);
  }

  Serialiser &Serialise(const rdcliteral &name, const void *&el, size_t &byteSize,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return Serialise(name, (byte *&)el, byteSize, flags);
  }
#endif

#if ENABLED(RDOC_WIN32)
  // annoyingly, windows SIZE_T is unsigned long on win32 which is a different type to
  // uint32_t/uint64_t. So we add a special overload here for its sake.
  Serialiser &Serialise(const rdcliteral &name, const void *&el, unsigned long &byteSize,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    uint64_t bs = byteSize;
    Serialiser &ret = Serialise(name, (byte *&)el, bs, flags);
    byteSize = (unsigned long)bs;
    return ret;
  }
#endif

  // serialise a fixed array like foo[4];
  // never needs to allocate, just needs to be iterated
  template <class T, size_t N>
  Serialiser &Serialise(const rdcliteral &name, T (&el)[N],
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    // for consistency with other arrays, even though this is redundant, we serialise out and in the
    // size
    uint64_t count = N;
    {
      m_InternalElement++;
      DoSerialise(*this, count);
      m_InternalElement--;

      if(count != N)
        RDCWARN("Fixed-size array length %zu serialised with different size %llu", N, count);
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &parent = *m_StructureStack.back();

      SDObject &arr = *parent.AddAndOwnChild(new SDObject(name, TypeName<T>()));
      m_StructureStack.push_back(&arr);

      arr.type.basetype = SDBasic::Array;
      arr.type.byteSize = N;
      arr.type.flags |= SDTypeFlags::FixedArray;

      arr.ReserveChildren(N);

      for(size_t i = 0; i < N; i++)
      {
        SDObject &obj = *arr.AddAndOwnChild(new SDObject("$el"_lit, TypeName<T>()));
        m_StructureStack.push_back(&obj);

        // default to struct. This will be overwritten if appropriate
        obj.type.basetype = SDBasic::Struct;
        obj.type.byteSize = sizeof(T);
        if(std::is_union<T>::value)
          obj.type.flags |= SDTypeFlags::Union;

        // Check against the serialised count here - on read if we don't have the right size this
        // means we won't read past the provided data.
        if(i < count)
        {
          SerialiseDispatch<Serialiser, T>::Do(*this, el[i]);
        }
        else
        {
          // we should have data for these elements, but we don't. Just default initialise
          el[i] = T();
        }

        m_StructureStack.pop_back();
      }

      // if we have more data than the fixed sized array allows, we must simply discard the excess
      if(count > N)
      {
        // prevent any trashing of structured data by these
        m_InternalElement++;
        T dummy;
        SerialiseDispatch<Serialiser, T>::Do(*this, dummy);
        m_InternalElement--;
      }

      m_StructureStack.pop_back();
    }
    else
    {
      for(size_t i = 0; i < N && i < count; i++)
        SerialiseDispatch<Serialiser, T>::Do(*this, el[i]);

      for(size_t i = N; i < count; i++)
      {
        T dummy = T();
        SerialiseDispatch<Serialiser, T>::Do(*this, dummy);
      }
    }

    return *this;
  }

  // specialisation for fixed character arrays, serialising as strings
  template <size_t N>
  Serialiser &Serialise(const rdcliteral &name, char (&el)[N],
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    rdcstr str;
    if(IsWriting())
      str = el;
    Serialise(name, str, flags);
    if(str.length() >= N)
    {
      RDCWARN("Serialising string too large for fixed-size array '%s', will be truncated",
              name.c_str());
      memcpy(el, str.c_str(), N - 1);
      el[N - 1] = 0;
    }
    else
    {
      // copy the string & trailing NULL into el.
      memcpy(el, str.c_str(), str.length() + 1);
    }

    return *this;
  }

  // special function for serialising dynamically sized arrays
  template <class T>
  Serialiser &Serialise(const rdcliteral &name, T *&el, uint64_t arrayCount,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    // silently handle NULL arrays
    if(IsWriting() && el == NULL)
      arrayCount = 0;

    {
      m_InternalElement++;
      DoSerialise(*this, arrayCount);
      m_InternalElement--;
    }

    if(IsReading())
    {
      VerifyArraySize(arrayCount);
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &parent = *m_StructureStack.back();

      SDObject &arr = *parent.AddAndOwnChild(new SDObject(name, TypeName<T>()));
      m_StructureStack.push_back(&arr);

      arr.type.basetype = SDBasic::Array;
      arr.type.byteSize = arrayCount;

      arr.ReserveChildren((size_t)arrayCount);

// Coverity is unable to tie this allocation together with the automatic scoped deallocation in the
// ScopedDeseralise* classes. We can verify with e.g. valgrind that there are no leaks, so to keep
// the analysis non-spammy we just don't allocate for coverity builds
#if !defined(__COVERITY__)
      if(IsReading() && !m_Structuriser && (flags & SerialiserFlags::AllocateMemory))
      {
        if(arrayCount > 0)
          el = new T[(size_t)arrayCount];
        else
          el = NULL;
      }
#endif

      if(m_LazyThreshold > 0 && arrayCount > m_LazyThreshold)
      {
        PushInternal();

        for(uint64_t i = 0; el && i < arrayCount; i++)
          SerialiseDispatch<Serialiser, T>::Do(*this, el[i]);

        PopInternal();

        arr.SetLazyArray(arrayCount, el, MakeLazySerialiser<T>());
      }
      else
      {
        for(uint64_t i = 0; el && i < arrayCount; i++)
        {
          SDObject &obj = *arr.AddAndOwnChild(new SDObject("$el"_lit, TypeName<T>()));
          m_StructureStack.push_back(&obj);

          // default to struct. This will be overwritten if appropriate
          obj.type.basetype = SDBasic::Struct;
          obj.type.byteSize = sizeof(T);
          if(std::is_union<T>::value)
            obj.type.flags |= SDTypeFlags::Union;

          SerialiseDispatch<Serialiser, T>::Do(*this, el[i]);

          m_StructureStack.pop_back();
        }
      }

      m_StructureStack.pop_back();
    }
    else
    {
// Coverity is unable to tie this allocation together with the automatic scoped deallocation in the
// ScopedDeseralise* classes. We can verify with e.g. valgrind that there are no leaks, so to keep
// the analysis non-spammy we just don't allocate for coverity builds
#if !defined(__COVERITY__)
      if(IsReading() && !m_Structuriser && (flags & SerialiserFlags::AllocateMemory))
      {
        if(arrayCount > 0)
          el = new T[(size_t)arrayCount];
        else
          el = NULL;
      }
#endif

      for(size_t i = 0; el && i < arrayCount; i++)
        SerialiseDispatch<Serialiser, T>::Do(*this, el[i]);
    }

    return *this;
  }

  // specialisations for container types
  template <class U>
  Serialiser &Serialise(const rdcliteral &name, rdcarray<U> &el,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    uint64_t size = (uint64_t)el.size();

    {
      m_InternalElement++;
      DoSerialise(*this, size);
      m_InternalElement--;
    }

    if(IsReading())
    {
      VerifyArraySize(size);
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &parent = *m_StructureStack.back();

      SDObject &arr = *parent.AddAndOwnChild(new SDObject(name, TypeName<U>()));
      m_StructureStack.push_back(&arr);

      arr.type.basetype = SDBasic::Array;
      arr.type.byteSize = size;

      arr.ReserveChildren((size_t)size);

      if(IsReading())
        el.resize((size_t)size);

      if(m_LazyThreshold > 0 && size > m_LazyThreshold)
      {
        PushInternal();

        for(size_t i = 0; i < (size_t)size; i++)
          SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);

        PopInternal();

        arr.SetLazyArray(size, el.data(), MakeLazySerialiser<U>());
      }
      else
      {
        for(size_t i = 0; i < (size_t)size; i++)
        {
          SDObject &obj = *arr.AddAndOwnChild(new SDObject("$el"_lit, TypeName<U>()));
          m_StructureStack.push_back(&obj);

          // default to struct. This will be overwritten if appropriate
          obj.type.basetype = SDBasic::Struct;
          obj.type.byteSize = sizeof(U);

          SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);

          m_StructureStack.pop_back();
        }
      }

      m_StructureStack.pop_back();
    }
    else
    {
      if(IsReading())
        el.resize((size_t)size);

      for(size_t i = 0; i < (size_t)size; i++)
        SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);
    }

    return *this;
  }

  template <class U, size_t N>
  Serialiser &Serialise(const rdcliteral &name, rdcfixedarray<U, N> &el,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    // for consistency with other arrays, even though this is redundant, we serialise out and in the
    // size
    uint64_t count = N;
    {
      m_InternalElement++;
      DoSerialise(*this, count);
      m_InternalElement--;

      if(count != N)
        RDCWARN("Fixed-size array length %zu serialised with different size %llu", N, count);
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &parent = *m_StructureStack.back();

      SDObject &arr = *parent.AddAndOwnChild(new SDObject(name, TypeName<U>()));
      m_StructureStack.push_back(&arr);

      arr.type.basetype = SDBasic::Array;
      arr.type.byteSize = N;
      arr.type.flags |= SDTypeFlags::FixedArray;

      arr.ReserveChildren(N);

      for(size_t i = 0; i < N; i++)
      {
        SDObject &obj = *arr.AddAndOwnChild(new SDObject("$el"_lit, TypeName<U>()));
        m_StructureStack.push_back(&obj);

        // default to struct. This will be overwritten if appropriate
        obj.type.basetype = SDBasic::Struct;
        obj.type.byteSize = sizeof(U);
        if(std::is_union<U>::value)
          obj.type.flags |= SDTypeFlags::Union;

        // Check against the serialised count here - on read if we don't have the right size this
        // means we won't read past the provided data.
        if(i < count)
        {
          SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);
        }
        else
        {
          // we should have data for these elements, but we don't. Just default initialise
          el[i] = U();
        }

        m_StructureStack.pop_back();
      }

      // if we have more data than the fixed sized array allows, we must simply discard the excess
      if(count > N)
      {
        // prevent any trashing of structured data by these
        m_InternalElement++;
        U dummy;
        SerialiseDispatch<Serialiser, U>::Do(*this, dummy);
        m_InternalElement--;
      }

      m_StructureStack.pop_back();
    }
    else
    {
      for(size_t i = 0; i < N && i < count; i++)
        SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);

      for(size_t i = N; i < count; i++)
      {
        U dummy = U();
        SerialiseDispatch<Serialiser, U>::Do(*this, dummy);
      }
    }

    return *this;
  }

  template <class U, class V>
  Serialiser &Serialise(const rdcliteral &name, rdcpair<U, V> &el,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &parent = *m_StructureStack.back();

      SDObject &arr = *parent.AddAndOwnChild(new SDObject(name, "pair"_lit));
      m_StructureStack.push_back(&arr);

      arr.type.basetype = SDBasic::Struct;
      arr.type.byteSize = 2;

      arr.ReserveChildren(2);

      {
        SDObject &obj = *arr.AddAndOwnChild(new SDObject("first"_lit, TypeName<U>()));
        m_StructureStack.push_back(&obj);

        // default to struct. This will be overwritten if appropriate
        obj.type.basetype = SDBasic::Struct;
        obj.type.byteSize = sizeof(U);

        SerialiseDispatch<Serialiser, U>::Do(*this, el.first);

        m_StructureStack.pop_back();
      }

      {
        SDObject &obj = *arr.AddAndOwnChild(new SDObject("second"_lit, TypeName<V>()));
        m_StructureStack.push_back(&obj);

        // default to struct. This will be overwritten if appropriate
        obj.type.basetype = SDBasic::Struct;
        obj.type.byteSize = sizeof(V);

        SerialiseDispatch<Serialiser, V>::Do(*this, el.second);

        m_StructureStack.pop_back();
      }

      m_StructureStack.pop_back();
    }
    else
    {
      SerialiseDispatch<Serialiser, U>::Do(*this, el.first);
      SerialiseDispatch<Serialiser, V>::Do(*this, el.second);
    }

    return *this;
  }

  // for const types, to cast away the const!
  // caller is responsible for ensuring that on write, it's safe to write into
  // this pointer on the other end
  template <class T>
  Serialiser &Serialise(const rdcliteral &name, const T &el,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return Serialise(name, (T &)el, flags);
  }

  // dynamic array variant
  template <class T>
  Serialiser &Serialise(const rdcliteral &name, const T *&el, uint64_t arrayCount,
                        SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return Serialise(name, (T *&)el, arrayCount, flags);
  }

  template <class T>
  Serialiser &SerialiseNullable(const rdcliteral &name, T *&el,
                                SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    bool present = (el != NULL);

    {
      m_InternalElement++;
      DoSerialise(*this, present);
      m_InternalElement--;
    }

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

// Coverity is unable to tie this allocation together with the automatic scoped deallocation in the
// ScopedDeseralise* classes. We can verify with e.g. valgrind that there are no leaks, so to keep
// the analysis non-spammy we just don't allocate for coverity builds
#if !defined(__COVERITY__)
      if(IsReading())
      {
        if(present)
          el = new T;
        else
          el = NULL;
      }
#endif

      if(el)
      {
        Serialise(name, *el, flags);

        SDObject &parent = *m_StructureStack.back();

        SDObject &nullable = *parent.GetChild(parent.NumChildren() - 1);

        nullable.type.flags |= SDTypeFlags::Nullable;
        if(std::is_union<T>::value)
          nullable.type.flags |= SDTypeFlags::Union;
      }
      else
      {
        SDObject &parent = *m_StructureStack.back();

        SDObject &nullable = *parent.AddAndOwnChild(new SDObject(name, TypeName<T>()));

        nullable.type.basetype = SDBasic::Null;
        nullable.type.byteSize = 0;
        nullable.type.flags |= SDTypeFlags::Nullable;
        if(std::is_union<T>::value)
          nullable.type.flags |= SDTypeFlags::Union;
      }
    }
    else
    {
// Coverity is unable to tie this allocation together with the automatic scoped deallocation in the
// ScopedDeseralise* classes. We can verify with e.g. valgrind that there are no leaks, so to keep
// the analysis non-spammy we just don't allocate for coverity builds
#if !defined(__COVERITY__)
      if(IsReading())
      {
        if(present)
          el = new T;
        else
          el = NULL;
      }
#endif

      if(el)
        Serialise(name, *el, flags);
    }

    return *this;
  }

  template <class T>
  Serialiser &SerialiseNullable(const rdcliteral &name, const T *&el,
                                SerialiserFlags flags = SerialiserFlags::NoFlags)
  {
    return SerialiseNullable(name, (T *&)el, flags);
  }

  Serialiser &SerialiseStream(const rdcstr &name, StreamReader &stream,
                              RENDERDOC_ProgressCallback progress = RENDERDOC_ProgressCallback())
  {
    // we don't make this a compile-time assert so this code can be compiled in a
    // templated-serialisation function (but we still assert as this will not be valid to execute
    RDCASSERTMSG("Can't read into a StreamReader", IsWriting());

    uint64_t totalSize = stream.GetSize();

    {
      m_InternalElement++;
      DoSerialise(*this, totalSize);
      m_InternalElement--;
    }

    // ensure byte alignment
    m_Write->AlignTo<ChunkAlignment>();

    StreamTransfer(m_Write, &stream, progress);

    return *this;
  }

  Serialiser &SerialiseStream(const rdcstr &name, StreamWriter &stream,
                              RENDERDOC_ProgressCallback progress)
  {
    RDCCOMPILE_ASSERT(IsReading(), "Can't write from a StreamWriter");

    uint64_t totalSize = 0;

    {
      m_InternalElement++;
      DoSerialise(*this, totalSize);
      m_InternalElement--;
    }

    size_t byteSize = (size_t)totalSize;

    byte *structBuf = NULL;

    if(ExportStructure())
    {
      if(m_StructureStack.empty())
      {
        RDCERR("Serialising object outside of chunk context! Start Chunk before any Serialise!");
        return *this;
      }

      SDObject &current = *m_StructureStack.back();

      SDObject &obj = *current.AddAndOwnChild(new SDObject(name, "Byte Buffer"_lit));
      m_StructureStack.push_back(&obj);

      obj.type.basetype = SDBasic::Buffer;
      obj.type.byteSize = totalSize;

      if(m_ExportBuffers)
      {
        obj.data.basic.u = m_StructuredFile->buffers.size();

        m_StructuredFile->buffers.push_back(new bytebuf);
        m_StructuredFile->buffers.back()->resize((size_t)totalSize);

        // this will be filled as we read below
        structBuf = m_StructuredFile->buffers.back()->data();
      }

      m_StructureStack.pop_back();
    }

    // ensure byte alignment
    m_Read->AlignTo<ChunkAlignment>();

    if(totalSize > 0)
    {
      // copy 1MB at a time
      const uint64_t StreamIOChunkSize = 1024 * 1024;

      // copy 1MB at a time
      const uint64_t bufSize = RDCMIN(StreamIOChunkSize, totalSize);
      uint64_t numBufs = totalSize / bufSize;
      // last remaining partial buffer
      if(totalSize % (uint64_t)bufSize > 0)
        numBufs++;

      byte *buf = new byte[byteSize];

      if(progress)
        progress(0.0001f);

      for(uint64_t i = 0; i < numBufs; i++)
      {
        uint64_t payloadLength = RDCMIN(bufSize, totalSize);

        m_Read->Read(buf, payloadLength);
        stream.Write(buf, payloadLength);

        if(structBuf)
        {
          memcpy(structBuf, buf, (size_t)payloadLength);
          structBuf += payloadLength;
        }

        totalSize -= payloadLength;
        if(progress)
          progress(float(i + 1) / float(numBufs));
      }

      delete[] buf;
    }
    else
    {
      if(progress)
        progress(1.0f);
    }

    return *this;
  }

  // these functions can be chained onto the end of a Serialise() call or macro to
  // set additional properties or change things
  Serialiser &Hidden()
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      current.type.flags |= SDTypeFlags::HiddenChildren;

      if(current.NumChildren() > 0)
        current.GetChild(current.NumChildren() - 1)->type.flags |= SDTypeFlags::Hidden;
    }

    return *this;
  }

  Serialiser &TypedAs(const rdcstr &name)
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      if(current.NumChildren() > 0)
      {
        SDObject *last = current.GetChild(current.NumChildren() - 1);
        last->type.name = name;

        if(last->type.basetype == SDBasic::Array)
        {
          for(SDObject *obj : *last)
            obj->type.name = name;
        }
      }
    }

    return *this;
  }

  Serialiser &Important()
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      current.type.flags |= SDTypeFlags::ImportantChildren;

      if(current.NumChildren() > 0)
      {
        SDObject *last = current.GetChild(current.NumChildren() - 1);
        last->type.flags |= SDTypeFlags::Important;
      }
    }

    return *this;
  }

  Serialiser &Unimportant()
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      // similar to Important() above but we *don't* set the important flag, we just mark the
      // parent has
      current.type.flags |= SDTypeFlags::ImportantChildren;
    }

    return *this;
  }

  Serialiser &Named(const rdcstr &name)
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      if(current.NumChildren() > 0)
        current.GetChild(current.NumChildren() - 1)->name = name;
    }

    return *this;
  }

  Serialiser &OffsetOrSize()
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      if(current.NumChildren() > 0)
        current.GetChild(current.NumChildren() - 1)->type.flags |= SDTypeFlags::OffsetOrSize;
    }

    return *this;
  }

  Serialiser &GPUAddress()
  {
    if(ExportStructure() && !m_StructureStack.empty())
    {
      SDObject &current = *m_StructureStack.back();

      if(current.NumChildren() > 0)
      {
        SDType &type = current.GetChild(current.NumChildren() - 1)->type;
        RDCASSERT(type.basetype == SDBasic::UnsignedInteger);
        RDCASSERT(type.byteSize == 8);
        type.basetype = SDBasic::GPUAddress;
      }
    }

    return *this;
  }

  // these functions should be used very carefully, they completely disable structured export for
  // anything serialised while internal is set.
  void PushInternal() { m_InternalElement++; }
  void PopInternal() { m_InternalElement--; }
  // this sets the current threshold for making structured data lazy for arrays above this size.
  // If set to 0, structured data is never set as lazy
  void SetLazyThreshold(uint32_t arraySize) { m_LazyThreshold = arraySize; }
  /////////////////////////////////////////////////////////////////////////////

  // for basic/leaf types. Read/written just as byte soup, MUST be plain old data
  template <class T>
  void SerialiseValue(SDBasic type, size_t byteSize, T &el)
  {
    if(IsWriting())
    {
      m_Write->Write(el);
    }
    else if(IsReading())
    {
      m_Read->Read(el);
    }

    if(!ExportStructure())
      return;

    SDObject &current = *m_StructureStack.back();

    current.type.basetype = type;
    current.type.byteSize = byteSize;

    switch(type)
    {
      case SDBasic::Chunk:
      case SDBasic::Struct:
      case SDBasic::Array:
      case SDBasic::Buffer:
      case SDBasic::Null: RDCFATAL("Cannot call SerialiseValue for type %d!", type); break;
      case SDBasic::String: RDCFATAL("eString should be specialised!"); break;
      case SDBasic::Enum:
      case SDBasic::Resource:
      case SDBasic::GPUAddress:
      case SDBasic::UnsignedInteger:
        if(byteSize == 1)
          current.data.basic.u = (uint64_t)(uint8_t)el;
        else if(byteSize == 2)
          current.data.basic.u = (uint64_t)(uint16_t)el;
        else if(byteSize == 4)
          current.data.basic.u = (uint64_t)(uint32_t)el;
        else if(byteSize == 8)
          current.data.basic.u = (uint64_t)el;
        else
          RDCFATAL("Unsupported unsigned integer byte width: %u", byteSize);
        break;
      case SDBasic::SignedInteger:
        if(byteSize == 1)
          current.data.basic.i = (int64_t)(int8_t)el;
        else if(byteSize == 2)
          current.data.basic.i = (int64_t)(int16_t)el;
        else if(byteSize == 4)
          current.data.basic.i = (int64_t)(int32_t)el;
        else if(byteSize == 8)
          current.data.basic.i = (int64_t)el;
        else
          RDCFATAL("Unsupported signed integer byte width: %u", byteSize);
        break;
      case SDBasic::Float:
        if(byteSize == 4)
          current.data.basic.d = (double)(float)el;
        else if(byteSize == 8)
          current.data.basic.d = (double)el;
        else
          RDCFATAL("Unsupported floating point byte width: %u", byteSize);
        break;
      case SDBasic::Boolean:
        // co-erce to boolean
        current.data.basic.b = !!(el);
        break;
      case SDBasic::Character: current.data.basic.c = (char)(el); break;
    }
  }

  void SerialiseValue(SDBasic type, size_t byteSize, rdcstr &el)
  {
    uint32_t len = 0;

    if(IsReading())
    {
      m_Read->Read(len);
      VerifyArraySize(len);
      el.resize((size_t)len);
      if(len > 0)
        m_Read->Read(&el[0], len);
    }
    else
    {
      len = (uint32_t)el.size();
      m_Write->Write(len);
      m_Write->Write(el.c_str(), len);
    }

    if(ExportStructure())
    {
      SDObject &current = *m_StructureStack.back();

      current.type.basetype = type;
      current.type.byteSize = len;
      current.data.str = el;
    }
  }

  void SerialiseValue(SDBasic type, size_t byteSize, rdcinflexiblestr &el)
  {
    if(IsReading())
    {
      rdcstr str;
      SerialiseValue(type, byteSize, str);
      el = str;
    }
    else
    {
      rdcstr str;
      str = el;
      SerialiseValue(type, byteSize, str);
    }
  }

  void SerialiseValue(SDBasic type, size_t byteSize, char *&el)
  {
    int32_t len = 0;

    if(IsReading())
    {
      m_Read->Read(len);
      if(len == -1)
      {
        el = NULL;
      }
      else
      {
        rdcstr str;
        str.resize(len);
        if(len > 0)
          m_Read->Read(&str[0], len);
        el = (char *)StringDB(str);
      }
    }
    else
    {
      len = el ? (int32_t)strlen(el) : -1;
      m_Write->Write(len);
      if(len > 0)
        m_Write->Write(el, len);
    }

    if(ExportStructure())
    {
      SDObject &current = *m_StructureStack.back();

      current.type.basetype = type;
      current.type.byteSize = RDCMAX(len, 0);
      current.data.str = el ? el : "";
      if(len == -1)
        current.type.flags |= SDTypeFlags::NullString;
    }
  }

  void SerialiseValue(SDBasic type, size_t byteSize, const char *&el)
  {
    SerialiseValue(type, byteSize, (char *&)el);
  }

  template <typename U>
  void SerialiseArrayValue(SDBasic type, rdcarray<U> &el)
  {
    uint64_t size = (uint64_t)el.size();
    {
      m_InternalElement++;
      DoSerialise(*this, size);
      m_InternalElement--;
    }

    if(IsReading())
    {
      VerifyArraySize(size);
    }

    if(ExportStructure())
    {
      SDObject &current = *m_StructureStack.back();

      current.type.basetype = SDBasic::Array;
      current.type.byteSize = size;

      current.ReserveChildren((size_t)size);

      if(IsReading())
        el.resize((size_t)size);

      if(m_LazyThreshold > 0 && size > m_LazyThreshold)
      {
        PushInternal();

        for(size_t i = 0; i < (size_t)size; i++)
          SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);

        PopInternal();

        current.SetLazyArray(size, el.data(), MakeLazySerialiser<U>());
      }
      else
      {
        for(size_t i = 0; i < (size_t)size; i++)
        {
          SDObject &obj = *current.AddAndOwnChild(new SDObject("$el"_lit, TypeName<U>()));
          m_StructureStack.push_back(&obj);

          // default to struct. This will be overwritten if appropriate
          obj.type.basetype = SDBasic::Struct;
          obj.type.byteSize = sizeof(U);

          SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);

          m_StructureStack.pop_back();
        }
      }
    }
    else
    {
      if(IsReading())
        el.resize((size_t)size);

      for(size_t i = 0; i < (size_t)size; i++)
        SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);
    }
  }

  // constructors only available by the derived classes for each serialiser type
protected:
  Serialiser(StreamWriter *writer, Ownership own);
  Serialiser(StreamReader *reader, Ownership own, SDObject *rootStructuredObj);

  template <SerialiserMode othertype>
  friend class Serialiser;

  void SetStructuriser(bool s) { m_Structuriser = s; }
private:
  static const uint64_t ChunkAlignment = 64;
  template <class SerialiserMode, typename T, bool isEnum = std::is_enum<T>::value>
  struct SerialiseDispatch
  {
    static void Do(SerialiserMode &ser, T &el) { DoSerialise(ser, el); }
  };

  template <class SerialiserMode, typename T>
  struct SerialiseDispatch<SerialiserMode, T, true>
  {
    static void Do(SerialiserMode &ser, T &el)
    {
      typedef typename std::underlying_type<T>::type etype;
      constexpr bool is_valid_type =
          std::is_same<etype, uint64_t>::value || std::is_same<etype, uint32_t>::value ||
          std::is_same<etype, uint16_t>::value || std::is_same<etype, uint8_t>::value ||
          std::is_same<etype, int>::value;
      RDCCOMPILE_ASSERT(is_valid_type, "enum isn't expected type");
      ser.SerialiseValue(SDBasic::Enum, sizeof(T), (etype &)(el));
      ser.SerialiseStringify(el);
    }
  };

  template <typename intSize>
  void VerifyArraySize(intSize &count)
  {
    uint64_t size = m_Read->GetSize();

// for streaming, just take 4GB as a 'semi reasonable' upper limit for array sizes
// use 1GB on 32-bit to avoid overflows
#if ENABLED(RDOC_X64)
    if(m_DataStreaming)
      size = 0xFFFFFFFFU;
#else
    if(m_DataStreaming)
      size = 0x3FFFFFFFU;
#endif

    if(count > size)
    {
      RDResult result;
      SET_ERROR_RESULT(
          result, ResultCode::FileCorrupted,
          "Reading invalid array or byte buffer - %llu larger than total stream size %llu.", count,
          size);

      // if we owned the previous stream, delete it
      if(m_Ownership == Ownership::Stream)
        delete m_Read;

      // replace our stream with an invalid one so all subsequent reads fail
      m_Read = new StreamReader(StreamReader::InvalidStream, result);
      m_Ownership = Ownership::Stream;

      // set the count to 0
      count = 0;
    }
  }

  template <typename T>
  LazyGenerator MakeLazySerialiser()
  {
    ChunkLookup lookup = m_ChunkLookup;
    void *userData = m_pUserData;
    bool buffers = m_ExportBuffers;
    uint64_t ver = m_Version;
    std::set<rdcstr> *stringDB = m_ExtStringDB;
    return [lookup, userData, buffers, ver, stringDB](const void *ptr) {
      T &input = *(T *)ptr;
      static StreamReader dummy(StreamReader::DummyStream);

      SDObject *ret = new SDObject("$el"_lit, TypeName<T>());

      ret->type.byteSize = sizeof(T);
      if(std::is_union<T>::value)
        ret->type.flags |= SDTypeFlags::Union;

      Serialiser<SerialiserMode::Reading> ser(&dummy, Ownership::Nothing, ret);

      ser.SetVersion(ver);
      ser.ConfigureStructuredExport(lookup, buffers, 0, 1.0);
      ser.SetStreamingMode(true);
      ser.SetStructuriser(true);
      ser.SetUserData(userData);
      ser.SetStringDatabase(stringDB);

      SerialiseDispatch<Serialiser<SerialiserMode::Reading>, T>::Do(ser, input);

      return ret;
    };
  }

  void *m_pUserData = NULL;
  uint64_t m_Version = 0;

  uint64_t m_StructArg = 0;
  rdcarray<rdcpair<uint64_t, uint64_t>> m_SidebandKV;

  StreamWriter *m_Write = NULL;
  StreamReader *m_Read = NULL;

  Ownership m_Ownership;

  // See SetStreamingMode
  bool m_DataStreaming = false;
  bool m_ActionChunk = false;
  bool m_Structuriser = false;

  uint64_t m_LastChunkOffset = 0;
  uint64_t m_ChunkFixup = 0;

  bool m_ExportStructured = false;
  bool m_ExportBuffers = false;
  int m_InternalElement = 0;
  uint32_t m_LazyThreshold = 0;
  SDFile m_StructData;
  SDFile *m_StructuredFile = &m_StructData;
  rdcarray<SDObject *> m_StructureStack;

  uint32_t m_ChunkFlags = 0;
  SDChunkMetaData m_ChunkMetadata;
  double m_TimerFrequency = 1.0;
  uint64_t m_TimerBase = 0;

  // a database of strings read from the file, useful when serialised structures
  // expect a char* to return and point to static memory
  std::set<rdcstr> m_StringDB;

  // external storage - so the string storage can persist after the lifetime of the serialiser
  std::set<rdcstr> *m_ExtStringDB = NULL;

  const char *StringDB(const rdcstr &s)
  {
    if(m_ExtStringDB)
    {
      auto it = m_ExtStringDB->insert(s);
      return it.first->c_str();
    }

    auto it = m_StringDB.insert(s);
    return it.first->c_str();
  }

  ChunkLookup m_ChunkLookup = NULL;
  FileIO::LogFileHandle *m_DebugDumpLog = NULL;
};

#ifndef SERIALISER_IMPL
class WriteSerialiser : public Serialiser<SerialiserMode::Writing>
{
public:
  WriteSerialiser(StreamWriter *writer, Ownership own) : Serialiser(writer, own) {}
  void WriteChunk(uint32_t chunkID, uint64_t byteLength = 0) { BeginChunk(chunkID, byteLength); }
};

class ReadSerialiser : public Serialiser<SerialiserMode::Reading>
{
public:
  ReadSerialiser(StreamReader *reader, Ownership own) : Serialiser(reader, own, NULL) {}
  template <typename ChunkType>
  ChunkType ReadChunk()
  {
    // parameters are ignored when reading
    return (ChunkType)BeginChunk(0, 0);
  }
};

class StructuredSerialiser : public Serialiser<SerialiserMode::Reading>
{
public:
  StructuredSerialiser(SDObject *obj, ChunkLookup lookup)
      : Serialiser(new StreamReader(StreamReader::DummyStream), Ownership::Stream, obj)
  {
    ConfigureStructuredExport(lookup, false, 0, 1.0);
    SetStreamingMode(true);
    SetStructuriser(true);
  }
};
#endif

#define BASIC_TYPE_SERIALISE(typeName, member, type, byteSize) \
  DECLARE_STRINGISE_TYPE(typeName)                             \
  DECLARE_STRINGISE_TYPE(rdcarray<typeName>)                   \
  template <class SerialiserType>                              \
  void DoSerialise(SerialiserType &ser, typeName &el)          \
  {                                                            \
    ser.SerialiseValue(type, byteSize, member);                \
  }

#define BASIC_TYPE_SERIALISE_STRINGIFY(typeName, member, type, byteSize) \
  template <class SerialiserType>                                        \
  void DoSerialise(SerialiserType &ser, typeName &el)                    \
  {                                                                      \
    ser.SerialiseValue(type, byteSize, member);                          \
    ser.SerialiseStringify(el);                                          \
  }

BASIC_TYPE_SERIALISE(int64_t, el, SDBasic::SignedInteger, 8);
BASIC_TYPE_SERIALISE(uint64_t, el, SDBasic::UnsignedInteger, 8);
BASIC_TYPE_SERIALISE(int32_t, el, SDBasic::SignedInteger, 4);
BASIC_TYPE_SERIALISE(uint32_t, el, SDBasic::UnsignedInteger, 4);
BASIC_TYPE_SERIALISE(int16_t, el, SDBasic::SignedInteger, 2);
BASIC_TYPE_SERIALISE(uint16_t, el, SDBasic::UnsignedInteger, 2);
BASIC_TYPE_SERIALISE(int8_t, el, SDBasic::SignedInteger, 1);
BASIC_TYPE_SERIALISE(uint8_t, el, SDBasic::UnsignedInteger, 1);

BASIC_TYPE_SERIALISE(double, el, SDBasic::Float, 8);
BASIC_TYPE_SERIALISE(float, el, SDBasic::Float, 4);

BASIC_TYPE_SERIALISE(bool, el, SDBasic::Boolean, 1);

BASIC_TYPE_SERIALISE(char, el, SDBasic::Character, 1);

template <>
inline rdcliteral TypeName<char *>()
{
  return "string"_lit;
}
template <class SerialiserType>
void DoSerialise(SerialiserType &ser, char *&el)
{
  ser.SerialiseValue(SDBasic::String, 0, el);
}

template <>
inline rdcliteral TypeName<const char *>()
{
  return "string"_lit;
}
template <class SerialiserType>
void DoSerialise(SerialiserType &ser, const char *&el)
{
  ser.SerialiseValue(SDBasic::String, 0, el);
}

template <>
inline rdcliteral TypeName<rdcstr>()
{
  return "string"_lit;
}
template <class SerialiserType>
void DoSerialise(SerialiserType &ser, rdcstr &el)
{
  ser.SerialiseValue(SDBasic::String, 0, el);
}

template <>
inline rdcliteral TypeName<rdcinflexiblestr>()
{
  return "string"_lit;
}
template <class SerialiserType>
void DoSerialise(SerialiserType &ser, rdcinflexiblestr &el)
{
  ser.SerialiseValue(SDBasic::String, 0, el);
}

template <class SerialiserType, typename U>
void DoSerialise(SerialiserType &ser, rdcarray<U> &el)
{
  ser.SerialiseArrayValue(SDBasic::Array, el);
}

DECLARE_STRINGISE_TYPE(SDObject *);

class ScopedChunk;

struct ChunkPage
{
  // compare just with the ID, so that old pages w hich have been reset in the pool don't get reset
  // again if an allocator subsequently tries to free them
  bool operator==(const ChunkPage &o) { return ID == o.ID; }
  size_t ID;

  // we allocate at two granularities, chunks are 16 bytes, buffers are multiples of 64-bytes
  // to keep things simple we allocate the chunk memory as 16/64 = a quarter the size of the
  // buffer memory. This will waste a bit of memory because we expect buffers to be on average
  // larger than 64 bytes.

  // base of the buffer
  byte *bufferBase;
  // head of the buffer
  byte *bufferHead;

  byte *chunkBase;
  byte *chunkHead;
};

// this is the first level, it allocates whole pages and returns them to allocators for finer
// grained allocation, and those allocators can return whole pages back. This is necessary because
// when fine-grained resetting is allowed we need to associate whole pages with objects and if those
// objects are allocating interleaved we need to immediately associate pages with them.
class ChunkPagePool
{
public:
  ChunkPagePool(size_t PageSize) : BufferPageSize(PageSize), ChunkPageSize(PageSize / 32) {}
  ChunkPagePool(const ChunkPagePool &) = delete;
  ChunkPagePool(ChunkPagePool &&) = delete;
  ChunkPagePool &operator=(const ChunkPagePool &) = delete;
  ~ChunkPagePool();

  // Allocate a page
  ChunkPage AllocPage();

  // really free any unused pages
  void Trim();

  // reset all pages to free
  void Reset();

  // reset a page set, other pages will remain in use
  void ResetPageSet(const rdcarray<ChunkPage> &pages);

  size_t GetBufferPageSize() { return BufferPageSize; }
  size_t GetChunkPageSize() { return ChunkPageSize; }
private:
  size_t BufferPageSize;
  size_t ChunkPageSize;

  size_t m_ID = 1;

  // a page is in precisely ONE of these arrays at any time.
  // Reset() will move all allocated pages back to free pages and reclaim all that memory
  // ResetPageSet() will move any referenced pages from allocatedPages back to freePages
  rdcarray<ChunkPage> freePages;
  rdcarray<ChunkPage> allocatedPages;
};

// this is the second level, it should only be used by one object (or a group of objects that are
// always reset together). It pulls pages from the pool and allocates from them, and can then
// release those pages back again with a reset operation.
class ChunkAllocator
{
public:
  ChunkAllocator(ChunkPagePool &pool) : m_Pool(pool) {}
  ChunkAllocator(const ChunkAllocator &) = delete;
  ChunkAllocator(ChunkAllocator &&) = delete;
  ChunkAllocator &operator=(const ChunkAllocator &) = delete;
  ~ChunkAllocator();

  // swap with another chunk allocator - must be from the same pool
  void swap(ChunkAllocator &alloc);

  byte *AllocAlignedBuffer(uint64_t size);
  byte *AllocChunk();

  void Reset();

private:
  ChunkPagePool &m_Pool;

  // as we're recording each new page we start gets added here. The last page is the one we're
  // currently allocating from.
  rdcarray<ChunkPage> pages;

  // given a page and the known page size, how much is left
  inline size_t GetRemainingBufferBytes(const ChunkPage &p)
  {
    return m_Pool.GetBufferPageSize() - (p.bufferHead - p.bufferBase);
  }
  inline size_t GetRemainingChunkBytes(const ChunkPage &p)
  {
    return m_Pool.GetChunkPageSize() - (p.chunkHead - p.chunkBase);
  }
  inline size_t GetRemainingBytes(bool chunkAlloc, const ChunkPage &p)
  {
    return chunkAlloc ? GetRemainingChunkBytes(p) : GetRemainingBufferBytes(p);
  }

  byte *AllocateFromPages(bool chunkAlloc, size_t size);
};

// holds the memory, length and type for a given chunk, so that it can be
// passed around and moved between owners before being serialised out
class Chunk
{
  Chunk(bool fromAllocator) : m_FromAllocator(fromAllocator) {}
  ~Chunk()
  {
    FreeAlignedBuffer(m_Data);

#if ENABLED(RDOC_DEVEL)
    Atomic::Dec64(&m_LiveChunks);
    Atomic::ExchAdd64(&m_TotalMem, -int64_t(m_Length));
#endif
  }

public:
  void Delete() { Delete(m_FromAllocator); }
  bool IsFromAllocator() { return m_FromAllocator; }
  void Delete(bool fromAllocator)
  {
    if(!fromAllocator)
      delete this;
  }

  template <typename ChunkType>
  ChunkType GetChunkType()
  {
    return (ChunkType)m_ChunkType;
  }
#if ENABLED(RDOC_DEVEL)
  static uint64_t NumLiveChunks() { return m_LiveChunks; }
  static uint64_t TotalMem() { return m_TotalMem; }
#else
  static uint64_t NumLiveChunks() { return 0; }
  static uint64_t TotalMem() { return 0; }
#endif

  // grab current contents of the serialiser into a new chunk
  static Chunk *Create(Serialiser<SerialiserMode::Writing> &ser, uint16_t chunkType,
                       ChunkAllocator *allocator = NULL, bool stealDataFromWriter = false);

  byte *GetData() const { return m_Data; }
  Chunk *Duplicate()
  {
    Chunk *ret = new Chunk();
    ret->m_Length = m_Length;
    ret->m_ChunkType = m_ChunkType;

    ret->m_Data = AllocAlignedBuffer(m_Length);
    ret->m_FromAllocator = false;

    memcpy(ret->m_Data, m_Data, (size_t)m_Length);

#if ENABLED(RDOC_DEVEL)
    Atomic::Inc64(&m_LiveChunks);
    Atomic::ExchAdd64(&m_TotalMem, int64_t(m_Length));
#endif

    return ret;
  }

  void Write(Serialiser<SerialiserMode::Writing> &ser)
  {
    ser.GetWriter()->Write((const void *)m_Data, (size_t)m_Length);
  }

private:
  Chunk() = default;
  Chunk(const Chunk &) = delete;
  Chunk &operator=(const Chunk &) = delete;

  uint16_t m_ChunkType;

  bool m_FromAllocator = false;

  uint32_t m_Length;
  byte *m_Data;

#if ENABLED(RDOC_DEVEL)
  static int64_t m_LiveChunks, m_TotalMem;
#endif
};

#ifndef SERIALISER_IMPL
class ScopedChunk
{
public:
  template <typename ChunkType>
  ScopedChunk(WriteSerialiser &s, ChunkType i, uint64_t byteLength = 0)
      : m_Idx(uint16_t(i)), m_Ser(s), m_Ended(false)
  {
    m_Ser.WriteChunk(m_Idx, byteLength);
  }
  ~ScopedChunk()
  {
    if(!m_Ended)
      End();
  }

  Chunk *Steal()
  {
    End();
    return Chunk::Create(m_Ser, m_Idx, NULL, true);
  }

  Chunk *Get(ChunkAllocator *allocator = NULL)
  {
    End();
    return Chunk::Create(m_Ser, m_Idx, allocator, false);
  }

private:
  WriteSerialiser &m_Ser;
  uint16_t m_Idx;
  bool m_Ended;

  void End()
  {
    RDCASSERT(!m_Ended);

    m_Ser.EndChunk();

    m_Ended = true;
  }
};
#endif

template <class SerialiserType, class T>
struct ScopedDeserialise
{
  ScopedDeserialise(const SerialiserType &ser, const T &el) : m_Ser(ser), m_El(el) {}
  ~ScopedDeserialise()
  {
    if(m_Ser.IsReading())
      Deserialise(m_El);
  }
  const SerialiserType &m_Ser;
  const T &m_El;
};

template <class SerialiserType, class ptrT>
struct ScopedDeserialiseNullable
{
  typedef typename std::remove_pointer<ptrT>::type T;

  ScopedDeserialiseNullable(const SerialiserType &ser, T **el) : m_Ser(ser), m_El(el) {}
  ~ScopedDeserialiseNullable()
  {
    if(m_Ser.IsReading() && *m_El != NULL)
    {
      Deserialise(**m_El);
      delete *m_El;
    }
  }
  const SerialiserType &m_Ser;
  T **m_El;
};

template <class SerialiserType, class ptrT>
struct ScopedDeserialiseArray
{
  typedef typename std::remove_pointer<ptrT>::type T;

  ScopedDeserialiseArray(const SerialiserType &ser, T **el, uint64_t c)
      : m_Ser(ser), m_El(el), count(c)
  {
  }
  ~ScopedDeserialiseArray()
  {
    if(m_Ser.IsReading() && *m_El)
    {
      for(uint64_t i = 0; i < count; i++)
        Deserialise((*m_El)[i]);
      delete[] * m_El;
    }
  }
  const SerialiserType &m_Ser;
  T **m_El;
  uint64_t count;
};

template <class SerialiserType>
struct ScopedDeserialiseArray<SerialiserType, void *>
{
  ScopedDeserialiseArray(const SerialiserType &ser, void **el, uint64_t) : m_Ser(ser), m_El(el) {}
  ~ScopedDeserialiseArray()
  {
    if(m_Ser.IsReading())
      FreeAlignedBuffer((byte *)*m_El);
  }
  const SerialiserType &m_Ser;
  void **m_El;
};

template <class SerialiserType>
struct ScopedDeserialiseArray<SerialiserType, const void *>
{
  ScopedDeserialiseArray(const SerialiserType &ser, const void **el, uint64_t)
      : m_Ser(ser), m_El(el)
  {
  }
  ~ScopedDeserialiseArray()
  {
    if(m_Ser.IsReading())
      FreeAlignedBuffer((byte *)*m_El);
  }
  const SerialiserType &m_Ser;
  const void **m_El;
};

template <class SerialiserType>
struct ScopedDeserialiseArray<SerialiserType, byte *>
{
  ScopedDeserialiseArray(const SerialiserType &ser, byte **el, uint64_t) : m_Ser(ser), m_El(el) {}
  ~ScopedDeserialiseArray()
  {
    if(m_Ser.IsReading())
      FreeAlignedBuffer(*m_El);
  }
  const SerialiserType &m_Ser;
  byte **m_El;
};

// can be overridden to change the name in the helper macros locally
#define GET_SERIALISER (ser)

#define SCOPED_SERIALISE_CHUNK(...) ScopedChunk scope(GET_SERIALISER, __VA_ARGS__);

// these helper macros are intended for use when serialising objects in chunks
#define SERIALISE_ELEMENT(obj)                                                               \
  ScopedDeserialise<decltype(GET_SERIALISER), decltype(obj)> CONCAT(deserialise_, __LINE__)( \
      GET_SERIALISER, obj);                                                                  \
  GET_SERIALISER.Serialise(STRING_LITERAL(#obj), obj)

// for _TYPED serialises, we need to first clear the object to 0 since the typed alias might not
// write it all. This is mostly only when co-oercing a BOOL type to bool, where only one byte gets
// written. We go via ClearObj() so that when StructuredSerialiser is in use and the reader is a
// dummy, it can skip the write.
#define SERIALISE_ELEMENT_TYPED(type, obj) \
  ser.ClearObj(obj);                       \
  union                                    \
  {                                        \
    type *t;                               \
    decltype(obj) *o;                      \
  } CONCAT(union, __LINE__);               \
  CONCAT(union, __LINE__).o = &obj;        \
  GET_SERIALISER.template Serialise<type>(STRING_LITERAL(#obj), *CONCAT(union, __LINE__).t)

#define SERIALISE_ELEMENT_ARRAY_TYPED(type, arrayObj, countObj)                                  \
  RDCCOMPILE_ASSERT(sizeof(*arrayObj) == sizeof(type),                                           \
                    "Array serialised co-erced type must be identically sized");                 \
  union                                                                                          \
  {                                                                                              \
    type **t;                                                                                    \
    decltype(arrayObj) *o;                                                                       \
  } CONCAT(union, __LINE__);                                                                     \
  CONCAT(union, __LINE__).o = &arrayObj;                                                         \
  GET_SERIALISER.template Serialise<type>(STRING_LITERAL(#arrayObj), *CONCAT(union, __LINE__).t, \
                                          countObj, SerialiserFlags::AllocateMemory)

#define SERIALISE_ELEMENT_ARRAY(obj, count)                                                       \
  uint64_t CONCAT(dummy_array_count, __LINE__) = 0;                                               \
  (void)CONCAT(dummy_array_count, __LINE__);                                                      \
  ScopedDeserialiseArray<decltype(GET_SERIALISER), decltype(obj)> CONCAT(deserialise_, __LINE__)( \
      GET_SERIALISER, &obj, count);                                                               \
  GET_SERIALISER.Serialise(STRING_LITERAL(#obj), obj, count, SerialiserFlags::AllocateMemory)

#define SERIALISE_ELEMENT_OPT(obj)                                           \
  ScopedDeserialiseNullable<decltype(GET_SERIALISER), decltype(obj)> CONCAT( \
      deserialise_, __LINE__)(GET_SERIALISER, &obj);                         \
  GET_SERIALISER.SerialiseNullable(STRING_LITERAL(#obj), obj)

#define SERIALISE_ELEMENT_LOCAL(obj, inValue)                                                 \
  typename std::remove_cv<typename std::remove_reference<decltype(inValue)>::type>::type obj; \
  ScopedDeserialise<decltype(GET_SERIALISER), decltype(obj)> CONCAT(deserialise_, __LINE__)(  \
      GET_SERIALISER, obj);                                                                   \
  if(GET_SERIALISER.IsWriting())                                                              \
    obj = (inValue);                                                                          \
  GET_SERIALISER.Serialise(STRING_LITERAL(#obj), obj)

// these macros are for use when implementing a DoSerialise function
#define SERIALISE_MEMBER(obj) ser.Serialise(STRING_LITERAL(#obj), el.obj)

#define SERIALISE_MEMBER_TYPED(type, obj) \
  ser.ClearObj(el.obj);                   \
  union                                   \
  {                                       \
    type *t;                              \
    decltype(el.obj) *o;                  \
  } CONCAT(union, __LINE__);              \
  CONCAT(union, __LINE__).o = &el.obj;    \
  ser.template Serialise<type>(STRING_LITERAL(#obj), *CONCAT(union, __LINE__).t)

#define SERIALISE_MEMBER_ARRAY(arrayObj, countObj) \
  ser.Serialise(STRING_LITERAL(#arrayObj), el.arrayObj, el.countObj, SerialiserFlags::AllocateMemory)

#define SERIALISE_MEMBER_ARRAY_TYPED(type, arrayObj, countObj)                                     \
  RDCCOMPILE_ASSERT(sizeof(*el.arrayObj) == sizeof(type),                                          \
                    "Array serialised co-erced type must be identically sized");                   \
  union                                                                                            \
  {                                                                                                \
    type **t;                                                                                      \
    decltype(el.arrayObj) *o;                                                                      \
  } CONCAT(union, __LINE__);                                                                       \
  CONCAT(union, __LINE__).o = &el.arrayObj;                                                        \
  ser.template Serialise<type>(STRING_LITERAL(#arrayObj), *CONCAT(union, __LINE__).t, el.countObj, \
                               SerialiserFlags::AllocateMemory)

// a member that is a pointer and could be NULL, so needs a hidden 'present'
// flag serialised out
#define SERIALISE_MEMBER_OPT(obj) ser.SerialiseNullable(STRING_LITERAL(#obj), el.obj)

// this is used when we want to serialise a member that should not be read from, if we know from
// context that it should always be serialised as NULL. It avoids the need to declare local dummy
// variables. It serialises elements as empty/NULL/etc from a local while writing, and sets to
// default on reading
#define SERIALISE_MEMBER_EMPTY(obj)             \
  {                                             \
    decltype(el.obj) dummy = {};                \
    ser.Serialise(STRING_LITERAL(#obj), dummy); \
    if(ser.IsReading())                         \
      el.obj = decltype(el.obj)();              \
  }
#define SERIALISE_MEMBER_OPT_EMPTY(obj)                 \
  {                                                     \
    decltype(el.obj) dummy = NULL;                      \
    ser.SerialiseNullable(STRING_LITERAL(#obj), dummy); \
    if(ser.IsReading())                                 \
      el.obj = NULL;                                    \
  }
#define SERIALISE_MEMBER_ARRAY_EMPTY(arrayObj)                                                    \
  {                                                                                               \
    decltype(el.arrayObj) dummy = NULL;                                                           \
    uint64_t dummycount = 0;                                                                      \
    ser.Serialise(STRING_LITERAL(#arrayObj), dummy, dummycount, SerialiserFlags::AllocateMemory); \
    if(ser.IsReading())                                                                           \
    {                                                                                             \
      el.arrayObj = NULL;                                                                         \
    }                                                                                             \
  }

// simple utility function for inside serialise functions, to check if the serialiser has hit an
// error and then bail, without trying to replay anything.
#define SERIALISE_CHECK_READ_ERRORS()                                       \
  if(ser.IsReading() && ser.IsErrored())                                    \
  {                                                                         \
    RDCERR("Serialisation failed in '%s'.", ser.GetCurChunkName().c_str()); \
    return false;                                                           \
  }
