Files
renderdoc/renderdoc/serialise/serialiser.h
T
baldurk d408f77241 Add configuration system for core renderdoc module
* This allows persistent config storage and registering tweak variables that
  works independent of the UI's configuration.
* Config vars can be debug only, which means they will be compiled out in stable
  version releases. This allows for debug-logging tweaks that are available in
  all builds (including nightly builds) for diagnostic purposes, but have zero
  overhead in stable releases.
* Variables have a loose hierarchy defined with _ or . to separate nodes.
2020-03-18 17:34:59 +00:00

1783 lines
54 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2020 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/structured_data.h"
#include "common/formatting.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 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;
}
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(); }
bool IsDummy() { return m_Dummy; }
StreamWriter *GetWriter() { return m_Write; }
StreamReader *GetReader() { return m_Read; }
uint32_t GetChunkMetadataRecording() { return m_ChunkFlags; }
void SetChunkMetadataRecording(uint32_t flags);
// 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 SetDrawChunk() { m_DrawChunk = 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; }
//////////////////////////////////////////
// Public serialisation interface
void ConfigureStructuredExport(ChunkLookup lookup, bool includeBuffers)
{
m_ChunkLookup = lookup;
m_ExportBuffers = includeBuffers;
m_ExportStructured = (lookup != NULL);
}
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();
current.data.basic.numChildren++;
current.data.children.push_back(new SDObject(name, TypeName<T>()));
m_StructureStack.push_back(current.data.children.back());
SDObject &obj = *m_StructureStack.back();
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 = true;
DoSerialise(*this, byteSize);
m_InternalElement = false;
}
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();
current.data.basic.numChildren++;
current.data.children.push_back(new SDObject(name, "Byte Buffer"_lit));
m_StructureStack.push_back(current.data.children.back());
SDObject &obj = *m_StructureStack.back();
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_Dummy && (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 = true;
DoSerialise(*this, count);
m_InternalElement = false;
}
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();
current.data.basic.numChildren++;
current.data.children.push_back(new SDObject(name, "Byte Buffer"_lit));
m_StructureStack.push_back(current.data.children.back());
SDObject &obj = *m_StructureStack.back();
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 = true;
DoSerialise(*this, count);
m_InternalElement = false;
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();
parent.data.basic.numChildren++;
parent.data.children.push_back(new SDObject(name, TypeName<T>()));
m_StructureStack.push_back(parent.data.children.back());
SDObject &arr = *m_StructureStack.back();
arr.type.basetype = SDBasic::Array;
arr.type.byteSize = N;
arr.type.flags |= SDTypeFlags::FixedArray;
arr.data.basic.numChildren = (uint64_t)N;
arr.data.children.resize(N);
for(size_t i = 0; i < N; i++)
{
arr.data.children[i] = new SDObject("$el"_lit, TypeName<T>());
m_StructureStack.push_back(arr.data.children[i]);
SDObject &obj = *m_StructureStack.back();
// 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
bool wasInternal = m_InternalElement;
m_InternalElement = true;
T dummy;
SerialiseDispatch<Serialiser, T>::Do(*this, dummy);
m_InternalElement = wasInternal;
}
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 = true;
DoSerialise(*this, arrayCount);
m_InternalElement = false;
}
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();
parent.data.basic.numChildren++;
parent.data.children.push_back(new SDObject(name, TypeName<T>()));
m_StructureStack.push_back(parent.data.children.back());
SDObject &arr = *m_StructureStack.back();
arr.type.basetype = SDBasic::Array;
arr.type.byteSize = arrayCount;
arr.data.basic.numChildren = arrayCount;
arr.data.children.resize((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_Dummy && (flags & SerialiserFlags::AllocateMemory))
{
if(arrayCount > 0)
el = new T[(size_t)arrayCount];
else
el = NULL;
}
#endif
for(uint64_t i = 0; el && i < arrayCount; i++)
{
arr.data.children[(size_t)i] = new SDObject("$el"_lit, TypeName<T>());
m_StructureStack.push_back(arr.data.children[(size_t)i]);
SDObject &obj = *m_StructureStack.back();
// 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_Dummy && (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 = true;
DoSerialise(*this, size);
m_InternalElement = false;
}
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();
parent.data.basic.numChildren++;
parent.data.children.push_back(new SDObject(name, TypeName<U>()));
m_StructureStack.push_back(parent.data.children.back());
SDObject &arr = *m_StructureStack.back();
arr.type.basetype = SDBasic::Array;
arr.type.byteSize = size;
arr.data.basic.numChildren = size;
arr.data.children.resize((size_t)size);
if(IsReading())
el.resize((int)size);
for(size_t i = 0; i < (size_t)size; i++)
{
arr.data.children[i] = new SDObject("$el"_lit, TypeName<U>());
m_StructureStack.push_back(arr.data.children[i]);
SDObject &obj = *m_StructureStack.back();
// 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((int)size);
for(size_t i = 0; i < (size_t)size; i++)
SerialiseDispatch<Serialiser, U>::Do(*this, el[i]);
}
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();
parent.data.basic.numChildren++;
parent.data.children.push_back(new SDObject(name, "pair"_lit));
m_StructureStack.push_back(parent.data.children.back());
SDObject &arr = *m_StructureStack.back();
arr.type.basetype = SDBasic::Struct;
arr.type.byteSize = 2;
arr.data.basic.numChildren = 2;
arr.data.children.resize(2);
{
arr.data.children[0] = new SDObject("first"_lit, TypeName<U>());
m_StructureStack.push_back(arr.data.children[0]);
SDObject &obj = *m_StructureStack.back();
// 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();
}
{
arr.data.children[1] = new SDObject("second"_lit, TypeName<V>());
m_StructureStack.push_back(arr.data.children[1]);
SDObject &obj = *m_StructureStack.back();
// 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 = true;
DoSerialise(*this, present);
m_InternalElement = false;
}
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.data.children.back();
nullable.type.flags |= SDTypeFlags::Nullable;
if(std::is_union<T>::value)
nullable.type.flags |= SDTypeFlags::Union;
}
else
{
SDObject &parent = *m_StructureStack.back();
parent.data.basic.numChildren++;
parent.data.children.push_back(new SDObject(name, TypeName<T>()));
SDObject &nullable = *parent.data.children.back();
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())
{
RDCCOMPILE_ASSERT(IsWriting(), "Can't read into a StreamReader");
uint64_t totalSize = stream.GetSize();
{
m_InternalElement = true;
DoSerialise(*this, totalSize);
m_InternalElement = false;
}
// 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 = true;
DoSerialise(*this, totalSize);
m_InternalElement = false;
}
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();
current.data.basic.numChildren++;
current.data.children.push_back(new SDObject(name.c_str(), "Byte Buffer"_lit));
m_StructureStack.push_back(current.data.children.back());
SDObject &obj = *m_StructureStack.back();
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();
if(!current.data.children.empty())
current.data.children.back()->type.flags |= SDTypeFlags::Hidden;
}
return *this;
}
Serialiser &TypedAs(const rdcstr &name)
{
if(ExportStructure() && !m_StructureStack.empty())
{
SDObject &current = *m_StructureStack.back();
if(!current.data.children.empty())
{
SDObject *last = current.data.children.back();
last->type.name = name;
if(last->type.basetype == SDBasic::Array)
{
for(SDObject *obj : last->data.children)
obj->type.name = name;
}
}
}
return *this;
}
Serialiser &Named(const rdcstr &name)
{
if(ExportStructure() && !m_StructureStack.empty())
{
SDObject &current = *m_StructureStack.back();
if(!current.data.children.empty())
current.data.children.back()->name = name;
}
return *this;
}
/////////////////////////////////////////////////////////////////////////////
// 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::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);
el.resize((int)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, 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);
}
// constructors only available by the derived classes for each serialiser type
protected:
Serialiser(StreamWriter *writer, Ownership own);
Serialiser(StreamReader *reader, Ownership own, SDObject *rootStructuredObj);
void SetDummy(bool dummy) { m_Dummy = dummy; }
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);
}
};
void VerifyArraySize(uint64_t &count)
{
uint64_t size = m_Read->GetSize();
// for streaming, just take 4GB as a 'semi reasonable' upper limit for array sizes
if(m_DataStreaming)
size = 0xFFFFFFFFU;
if(count > size)
{
RDCERR("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);
m_Ownership = Ownership::Stream;
// set the count to 0
count = 0;
}
}
void *m_pUserData = NULL;
uint64_t m_Version = 0;
uint64_t m_StructArg = 0;
StreamWriter *m_Write = NULL;
StreamReader *m_Read = NULL;
Ownership m_Ownership;
// See SetStreamingMode
bool m_DataStreaming = false;
bool m_DrawChunk = false;
bool m_Dummy = false;
uint64_t m_LastChunkOffset = 0;
uint64_t m_ChunkFixup = 0;
bool m_ExportStructured = false;
bool m_ExportBuffers = false;
bool m_InternalElement = false;
SDFile m_StructData;
SDFile *m_StructuredFile = &m_StructData;
rdcarray<SDObject *> m_StructureStack;
uint32_t m_ChunkFlags = 0;
SDChunkMetaData m_ChunkMetadata;
// 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);
SetStreamingMode(true);
SetDummy(true);
}
};
#endif
#define BASIC_TYPE_SERIALISE(typeName, member, type, byteSize) \
DECLARE_STRINGISE_TYPE(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);
}
DECLARE_STRINGISE_TYPE(SDObject *);
class ScopedChunk;
// 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
{
public:
~Chunk()
{
FreeAlignedBuffer(m_Data);
#if ENABLED(RDOC_DEVEL)
Atomic::Dec64(&m_LiveChunks);
Atomic::ExchAdd64(&m_TotalMem, -int64_t(m_Length));
#endif
}
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 this chunk
Chunk(Serialiser<SerialiserMode::Writing> &ser, uint32_t chunkType)
{
m_Length = (uint32_t)ser.GetWriter()->GetOffset();
RDCASSERT(ser.GetWriter()->GetOffset() < 0xffffffff);
m_ChunkType = chunkType;
m_Data = AllocAlignedBuffer(m_Length);
memcpy(m_Data, ser.GetWriter()->GetData(), (size_t)m_Length);
ser.GetWriter()->Rewind();
#if ENABLED(RDOC_DEVEL)
Atomic::Inc64(&m_LiveChunks);
Atomic::ExchAdd64(&m_TotalMem, int64_t(m_Length));
#endif
}
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);
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;
friend class ScopedChunk;
uint32_t m_ChunkType;
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(uint32_t(i)), m_Ser(s), m_Ended(false)
{
m_Ser.WriteChunk(m_Idx, byteLength);
}
~ScopedChunk()
{
if(!m_Ended)
End();
}
Chunk *Get()
{
End();
return new Chunk(m_Ser, m_Idx);
}
private:
WriteSerialiser &m_Ser;
uint32_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())
{
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(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; \
}