diff --git a/renderdoc/CMakeLists.txt b/renderdoc/CMakeLists.txt
index 058cf0487..07f8ecc83 100644
--- a/renderdoc/CMakeLists.txt
+++ b/renderdoc/CMakeLists.txt
@@ -130,6 +130,8 @@ set(sources
serialise/zstdio.h
serialise/streamio.cpp
serialise/streamio.h
+ serialise/rdcfile.cpp
+ serialise/rdcfile.h
serialise/comp_io_tests.cpp
serialise/streamio_tests.cpp
strings/grisu2.cpp
diff --git a/renderdoc/core/core.cpp b/renderdoc/core/core.cpp
index 7c38e3b5f..13b6b60e3 100644
--- a/renderdoc/core/core.cpp
+++ b/renderdoc/core/core.cpp
@@ -58,19 +58,7 @@ std::string DoStringise(const ResourceId &el)
ReplayStatus IMG_CreateReplayDevice(const char *logfile, IReplayDriver **driver);
// not provided by tinyexr, just do by hand
-bool is_exr_file(FILE *f)
-{
- FileIO::fseek64(f, 0, SEEK_SET);
-
- const uint32_t openexr_magic = MAKE_FOURCC(0x76, 0x2f, 0x31, 0x01);
-
- uint32_t magic = 0;
- size_t bytesRead = FileIO::fread(&magic, 1, sizeof(magic), f);
-
- FileIO::fseek64(f, 0, SEEK_SET);
-
- return bytesRead == sizeof(magic) && magic == openexr_magic;
-}
+bool is_exr_file(FILE *f);
template <>
std::string DoStringise(const RDCDriver &el)
diff --git a/renderdoc/renderdoc.vcxproj b/renderdoc/renderdoc.vcxproj
index 1b42546b8..80a1ca7e3 100644
--- a/renderdoc/renderdoc.vcxproj
+++ b/renderdoc/renderdoc.vcxproj
@@ -191,6 +191,7 @@
+
@@ -398,6 +399,7 @@
+
diff --git a/renderdoc/renderdoc.vcxproj.filters b/renderdoc/renderdoc.vcxproj.filters
index 05b8dbd9d..4f5ffb9dc 100644
--- a/renderdoc/renderdoc.vcxproj.filters
+++ b/renderdoc/renderdoc.vcxproj.filters
@@ -100,6 +100,9 @@
{3cb4fd0e-7cd9-45a3-99d2-1158f27ec438}
+
+ {95eea12f-e6e5-4638-acd6-72e71dcd8853}
+
{3b8694d9-cb25-45e0-a1c2-8cc8cd03df3f}
@@ -354,6 +357,9 @@
Common\Serialise\Compressors
+
+ Common\Serialise\Container File
+
Common\Serialise\Stream I/O
@@ -617,6 +623,9 @@
Common\Serialise\Stream I/O
+
+ Common\Serialise\Container File
+
diff --git a/renderdoc/serialise/rdcfile.cpp b/renderdoc/serialise/rdcfile.cpp
new file mode 100644
index 000000000..316b9acfe
--- /dev/null
+++ b/renderdoc/serialise/rdcfile.cpp
@@ -0,0 +1,888 @@
+/******************************************************************************
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2017 Baldur Karlsson
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ ******************************************************************************/
+
+#include "rdcfile.h"
+#include "3rdparty/stb/stb_image.h"
+#include "api/replay/version.h"
+#include "common/dds_readwrite.h"
+#include "lz4io.h"
+#include "zstdio.h"
+
+const char *SectionTypeNames[] = {
+ // unknown
+ "",
+ // FrameCapture
+ "renderdoc/internal/framecapture",
+ // ResolveDatabase
+ "renderdoc/internal/resolvedb",
+ // FrameBookmarks
+ "renderdoc/ui/bookmarks",
+ // Notes
+ "renderdoc/ui/notes",
+};
+
+RDCCOMPILE_ASSERT(ARRAY_COUNT(SectionTypeNames) == (size_t)SectionType::Count,
+ "Missing section name");
+
+// not provided by tinyexr, just do by hand
+bool is_exr_file(FILE *f)
+{
+ FileIO::fseek64(f, 0, SEEK_SET);
+
+ const uint32_t openexr_magic = MAKE_FOURCC(0x76, 0x2f, 0x31, 0x01);
+
+ uint32_t magic = 0;
+ size_t bytesRead = FileIO::fread(&magic, 1, sizeof(magic), f);
+
+ FileIO::fseek64(f, 0, SEEK_SET);
+
+ return bytesRead == sizeof(magic) && magic == openexr_magic;
+}
+
+/*
+
+ -----------------------------
+ File format for version 0x100:
+
+ RDCHeader
+ {
+ uint64_t MAGIC_HEADER;
+
+ uint32_t version = 0x00000100;
+ uint32_t headerLength; // length of this header, from the start of the file. Allows adding new
+ // fields without breaking compatibilty
+ char progVersion[16]; // string "v0.34" or similar with 0s after the string
+
+ // thumbnail
+ uint16_t thumbWidth;
+ uint16_t thumbHeight; // thumbnail width and height. If 0x0, no thumbnail data
+ uint32_t thumbLength; // number of bytes in thumbnail array below
+ byte thumbData[ thumbLength ]; // JPG compressed thumbnail
+
+ // where was the capture created
+ uint64_t machineIdent;
+
+ uint32_t driverID; // the RDCDriver used for this log
+ uint8_t driverNameLength; // length in bytes of the driver name including null terminator
+ char driverName[ driverNameLength ]; // the driver name in ASCII. Useful if the current
+ // implementation doesn't recognise the driver ID above
+ }
+
+ 1 or more sections:
+
+ Section
+ {
+ char isASCII = '\0' or 'A'; // indicates the section is ASCII or binary. ASCII allows for easy
+ appending by hand/script
+ if(isASCII == 'A')
+ {
+ // ASCII sections are discouraged for tools, but useful for hand-editing by just
+ // appending a simple text file
+ char newline = '\n';
+ char length[]; // length of just section data below, as decimal string
+ char newline = '\n';
+ char sectionType[]; // section type, see SectionType enum, as decimal string.
+ char newline = '\n';
+ char sectionVersion[]; // section version, as decimal string. May be 0 when not necessary.
+ char newline = '\n';
+ char sectionName[]; // UTF-8 string name of section.
+ char newline = '\n';
+
+ // sectionName is an arbitrary string.
+ //
+ // No two sections may have the same section type or section name. Any file
+ // with duplicates is ill-formed and it's undefined how the file is interpreted.
+
+ byte sectiondata[ atoi(length) ]; // section data
+ }
+ else if(isASCII == '\0')
+ {
+ byte zero[3]; // pad out the above character with 0 bytes. Reserved for future use
+ uint32_t sectionType; // section type enum, see SectionType. Could be SectionType::Unknown
+ uint64_t sectionCompressedLength; // byte length of the actual section data on disk
+ uint64_t sectionUncompressedLength; // byte length of the section data after decompression.
+ // If the section isn't compressed this will be equal to
+ // sectionLength
+ uint64_t sectionVersion; // section version number.
+ // The meaning of this is section specific and may be 0 if a version
+ // isn't needed. Most commonly it's used for the frame capture section
+ // to store the version of the data within.
+ uint32_t sectionFlags; // section flags - e.g. is compressed or not.
+ uint32_t sectionNameLength; // byte length of the string below (minimum 1, for null terminator)
+ char sectionName[sectionNameLength]; // UTF-8 string name of section, optional.
+
+ byte sectiondata[length]; // actual contents of the section
+ }
+ };
+
+ // remainder of the file is tightly packed/unaligned section structures.
+ // The first section must always be the actual frame capture data in
+ // binary form, other sections can follow in any order
+ Section sections[];
+
+*/
+
+static const uint32_t MAGIC_HEADER = MAKE_FOURCC('R', 'D', 'O', 'C');
+
+namespace
+{
+struct FileHeader
+{
+ FileHeader()
+ {
+ magic = MAGIC_HEADER;
+ version = RDCFile::SERIALISE_VERSION;
+ headerLength = 0;
+ RDCEraseEl(progVersion);
+ char ver[] = MAJOR_MINOR_VERSION_STRING;
+ memcpy(progVersion, ver, RDCMIN(sizeof(progVersion), sizeof(ver)));
+ }
+
+ uint64_t magic;
+
+ uint32_t version;
+ uint32_t headerLength;
+
+ // string "v0.34" or similar with 0s after the string
+ char progVersion[16];
+};
+
+struct BinaryThumbnail
+{
+ // thumbnail width and height. If 0x0, no thumbnail data
+ uint16_t width;
+ uint16_t height;
+ // number of bytes in thumbnail array below
+ uint32_t length;
+ // JPG compressed thumbnail
+ byte data[1];
+};
+
+struct CaptureMetaData
+{
+ // where was the capture created
+ uint64_t machineIdent = 0;
+
+ // the RDCDriver used for this log
+ RDCDriver driverID = RDC_Unknown;
+ // length in bytes of the driver name
+ uint8_t driverNameLength = 1;
+ // the driver name in ASCII. Useful if the current implementation doesn't recognise the driver
+ // ID above
+ char driverName[1] = {0};
+};
+
+struct BinarySectionHeader
+{
+ // 0x0
+ byte isASCII;
+ // 0x0, 0x0, 0x0
+ byte zero[3];
+ // section type enum, see SectionType. Could be SectionType::Unknown
+ SectionType sectionType;
+ // byte length of the actual section data on disk
+ uint64_t sectionCompressedLength;
+ // byte length of the section data after decompression, could be equal to sectionLength if the
+ // section is not compressed
+ uint64_t sectionUncompressedLength;
+ // section version number, with a section specific meaning - could be 0 if not needed.
+ uint64_t sectionVersion;
+ // section flags - e.g. is compressed or not.
+ SectionFlags sectionFlags;
+ // byte length of the string below (could be 0)
+ uint32_t sectionNameLength;
+ // actually sectionNameLength, but at least 1 for null terminator
+ char name[1];
+
+ // char name[sectionNameLength];
+ // byte data[sectionLength];
+};
+};
+
+#define RETURNCORRUPT(...) \
+ { \
+ RDCERR(__VA_ARGS__); \
+ m_Error = ContainerError::Corrupt; \
+ return; \
+ }
+
+RDCFile::~RDCFile()
+{
+ if(m_File)
+ FileIO::fclose(m_File);
+
+ if(m_Thumb.pixels)
+ delete[] m_Thumb.pixels;
+}
+
+void RDCFile::Open(const char *path)
+{
+ RDCLOG("Opening RDCFile %s", path);
+
+ // ensure section header is compiled correctly
+ RDCCOMPILE_ASSERT(offsetof(BinarySectionHeader, name) == sizeof(uint32_t) * 10,
+ "BinarySectionHeader size has changed or contains padding");
+
+ m_File = FileIO::fopen(path, "rb");
+ m_Filename = path;
+
+ if(!m_File)
+ {
+ RDCERR("Can't open capture file '%s' for read - errno %d", path, errno);
+ m_Error = ContainerError::FileNotFound;
+ return;
+ }
+
+ // try to identify if this is an image
+ {
+ int x = 0, y = 0, comp = 0;
+ int ret = stbi_info_from_file(m_File, &x, &y, &comp);
+
+ FileIO::fseek64(m_File, 0, SEEK_SET);
+
+ if(is_dds_file(m_File))
+ ret = x = y = comp = 1;
+
+ if(is_exr_file(m_File))
+ ret = x = y = comp = 1;
+
+ FileIO::fseek64(m_File, 0, SEEK_SET);
+
+ if(ret == 1 && x > 0 && y > 0 && comp > 0)
+ {
+ m_Driver = RDC_Image;
+ m_DriverName = "Image";
+ m_MachineIdent = 0;
+ return;
+ }
+ }
+
+ FileIO::fseek64(m_File, 0, SEEK_END);
+ uint64_t fileSize = FileIO::ftell64(m_File);
+ FileIO::fseek64(m_File, 0, SEEK_SET);
+
+ StreamReader reader(m_File, fileSize, Ownership::Nothing);
+
+ Init(reader);
+}
+
+void RDCFile::Open(const std::vector &buffer)
+{
+ m_Buffer = buffer;
+ m_File = NULL;
+
+ StreamReader reader(m_Buffer);
+
+ Init(reader);
+}
+
+void RDCFile::Init(StreamReader &reader)
+{
+ RDCDEBUG("Opened capture file for read");
+
+ // read the first part of the file header
+ FileHeader header;
+ reader.Read(header);
+
+ if(reader.IsErrored())
+ {
+ RDCERR("I/O error reading magic number");
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ if(header.magic != MAGIC_HEADER)
+ {
+ RDCWARN("Invalid capture file. Expected magic %08x, got %08x.", MAGIC_HEADER,
+ (uint32_t)header.magic);
+
+ m_Error = ContainerError::Corrupt;
+ return;
+ }
+
+ m_SerVer = header.version;
+
+ if(m_SerVer != SERIALISE_VERSION)
+ {
+ RDCERR(
+ "Capture file from wrong version. This program (v%s) is logfile version %llu, file is "
+ "logfile version %llu capture on %s.",
+ SERIALISE_VERSION, header.version, MAJOR_MINOR_VERSION_STRING, header.progVersion);
+
+ m_Error = ContainerError::UnsupportedVersion;
+ return;
+ }
+
+ BinaryThumbnail thumb;
+ reader.Read(&thumb, offsetof(BinaryThumbnail, data));
+
+ if(reader.IsErrored())
+ {
+ RDCERR("I/O error reading thumbnail header");
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ // check the thumbnail size is sensible
+ if(thumb.length > 10 * 1024 * 1024)
+ {
+ RETURNCORRUPT("Thumbnail byte length invalid: %u", thumb.length);
+ }
+
+ byte *thumbData = new byte[thumb.length];
+ reader.Read(thumbData, thumb.length);
+
+ if(reader.IsErrored())
+ {
+ RDCERR("I/O error reading thumbnail data");
+ delete[] thumbData;
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ CaptureMetaData meta;
+ reader.Read(&meta, offsetof(CaptureMetaData, driverName));
+
+ if(reader.IsErrored())
+ {
+ RDCERR("I/O error reading capture metadata");
+ delete[] thumbData;
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ if(meta.driverNameLength == 0)
+ {
+ delete[] thumbData;
+ RETURNCORRUPT("Driver name length is invalid, must be at least 1 to contain NULL terminator");
+ }
+
+ char *driverName = new char[meta.driverNameLength];
+ reader.Read(driverName, meta.driverNameLength);
+
+ if(reader.IsErrored())
+ {
+ RDCERR("I/O error reading driver name");
+ delete[] thumbData;
+ delete[] driverName;
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ driverName[meta.driverNameLength - 1] = '\0';
+
+ m_Driver = meta.driverID;
+ m_DriverName = driverName;
+ m_MachineIdent = meta.machineIdent;
+ m_Thumb.width = thumb.width;
+ m_Thumb.height = thumb.height;
+ m_Thumb.len = thumb.length;
+
+ if(m_Thumb.len > 0 && m_Thumb.width > 0 && m_Thumb.height > 0)
+ {
+ m_Thumb.pixels = thumbData;
+ thumbData = NULL;
+ }
+
+ delete[] thumbData;
+ delete[] driverName;
+
+ if(reader.GetOffset() > header.headerLength)
+ {
+ RDCERR("I/O error seeking to end of header");
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ reader.SkipBytes(header.headerLength - (uint32_t)reader.GetOffset());
+
+ while(!reader.AtEnd())
+ {
+ BinarySectionHeader sectionHeader = {0};
+ byte *reading = (byte *)§ionHeader;
+
+ reader.Read(*reading);
+ reading++;
+
+ if(reader.IsErrored())
+ break;
+
+ if(sectionHeader.isASCII == 'A')
+ {
+ // ASCII section
+ char c = 0;
+ reader.Read(c);
+ if(reader.IsErrored())
+ RETURNCORRUPT("Invalid ASCII data section '%hhx'", c);
+
+ if(reader.AtEnd())
+ RETURNCORRUPT("Invalid truncated ASCII data section");
+
+ uint64_t length = 0;
+
+ c = '0';
+
+ while(!reader.IsErrored() && c != '\n')
+ {
+ reader.Read(c);
+
+ if(c == '\n' || reader.IsErrored())
+ break;
+
+ length *= 10;
+ length += int(c - '0');
+ }
+
+ if(reader.IsErrored() || reader.AtEnd())
+ RETURNCORRUPT("Invalid truncated ASCII data section");
+
+ uint32_t type = 0;
+
+ c = '0';
+
+ while(!reader.AtEnd() && c != '\n')
+ {
+ reader.Read(c);
+
+ if(c == '\n' || reader.IsErrored())
+ break;
+
+ type *= 10;
+ type += int(c - '0');
+ }
+
+ if(reader.IsErrored() || reader.AtEnd())
+ RETURNCORRUPT("Invalid truncated ASCII data section");
+
+ uint64_t version = 0;
+
+ c = '0';
+
+ while(!reader.AtEnd() && c != '\n')
+ {
+ reader.Read(c);
+
+ if(c == '\n' || reader.IsErrored())
+ break;
+
+ version *= 10;
+ version += int(c - '0');
+ }
+
+ if(reader.IsErrored() || reader.AtEnd())
+ RETURNCORRUPT("Invalid truncated ASCII data section");
+
+ std::string name;
+
+ c = 0;
+
+ while(!reader.AtEnd() && c != '\n')
+ {
+ reader.Read(c);
+
+ if(c == 0 || c == '\n' || reader.IsErrored())
+ break;
+
+ name.push_back(c);
+ }
+
+ if(reader.IsErrored() || reader.AtEnd())
+ RETURNCORRUPT("Invalid truncated ASCII data section");
+
+ SectionProperties props;
+ props.flags = SectionFlags::ASCIIStored;
+ props.type = (SectionType)type;
+ props.name = name;
+ props.version = version;
+ props.compressedSize = length;
+ props.uncompressedSize = length;
+
+ SectionLocation loc;
+ loc.offs = reader.GetOffset();
+ loc.diskLength = length;
+
+ reader.SkipBytes(loc.diskLength);
+
+ if(reader.IsErrored())
+ RETURNCORRUPT("Error seeking past ASCII section '%s' data", name.c_str());
+
+ m_Sections.push_back(props);
+ m_SectionLocations.push_back(loc);
+ }
+ else if(sectionHeader.isASCII == 0x0)
+ {
+ // -1 because we've already read the isASCII byte
+ reader.Read(reading, offsetof(BinarySectionHeader, name) - 1);
+
+ if(reader.IsErrored())
+ RETURNCORRUPT("Error reading binary section header");
+
+ SectionProperties props;
+ props.flags = sectionHeader.sectionFlags;
+ props.type = sectionHeader.sectionType;
+ props.compressedSize = sectionHeader.sectionCompressedLength;
+ props.uncompressedSize = sectionHeader.sectionUncompressedLength;
+ props.version = sectionHeader.sectionVersion;
+
+ if(sectionHeader.sectionNameLength == 0 || sectionHeader.sectionNameLength > 2 * 1024)
+ {
+ RETURNCORRUPT("Invalid section name length %u", sectionHeader.sectionNameLength);
+ }
+
+ props.name.resize(sectionHeader.sectionNameLength - 1);
+
+ reader.Read(&props.name[0], sectionHeader.sectionNameLength - 1);
+
+ if(reader.IsErrored())
+ RETURNCORRUPT("Error reading binary section header");
+
+ reader.SkipBytes(1);
+
+ if(reader.IsErrored())
+ RETURNCORRUPT("Error reading binary section header");
+
+ SectionLocation loc;
+ loc.offs = reader.GetOffset();
+ loc.diskLength = sectionHeader.sectionCompressedLength;
+
+ m_Sections.push_back(props);
+ m_SectionLocations.push_back(loc);
+
+ reader.SkipBytes(loc.diskLength);
+
+ if(reader.IsErrored())
+ RETURNCORRUPT("Error seeking past binary section '%s' data", props.name.c_str());
+ }
+ else
+ {
+ RETURNCORRUPT("Unrecognised section type '%hhx'", sectionHeader.isASCII);
+ }
+ }
+
+ if(SectionIndex(SectionType::FrameCapture) == -1)
+ {
+ RETURNCORRUPT("Capture file doesn't have a frame capture");
+ }
+}
+
+void RDCFile::SetData(RDCDriver driver, const char *driverName, uint64_t machineIdent,
+ const RDCThumb *thumb)
+{
+ m_Driver = driver;
+ m_DriverName = driverName;
+ m_MachineIdent = machineIdent;
+ if(thumb)
+ {
+ m_Thumb = *thumb;
+
+ byte *pixels = new byte[m_Thumb.len];
+ memcpy(pixels, thumb->pixels, m_Thumb.len);
+
+ m_Thumb.pixels = pixels;
+ }
+}
+
+void RDCFile::Create(const char *filename)
+{
+ m_File = FileIO::fopen(filename, "w+b");
+
+ RDCDEBUG("creating RDC file.");
+
+ if(!m_File)
+ {
+ RDCERR("Can't open capture file '%s' for write, errno %d", filename, errno);
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+
+ RDCDEBUG("Opened capture file for write");
+
+ FileHeader header; // automagically initialised with correct data apart from length
+
+ BinaryThumbnail thumbHeader = {0};
+
+ thumbHeader.width = m_Thumb.width;
+ thumbHeader.height = m_Thumb.height;
+ thumbHeader.length = m_Thumb.len;
+
+ CaptureMetaData meta;
+ meta.driverID = m_Driver;
+ meta.machineIdent = m_MachineIdent;
+ meta.driverNameLength = uint8_t(m_DriverName.size() + 1);
+
+ header.headerLength = sizeof(FileHeader) + offsetof(BinaryThumbnail, data) + thumbHeader.length +
+ offsetof(CaptureMetaData, driverName) + meta.driverNameLength;
+
+ StreamWriter writer(m_File, Ownership::Nothing);
+
+ writer.Write(header);
+ writer.Write(&thumbHeader, offsetof(BinaryThumbnail, data));
+
+ if(thumbHeader.length > 0)
+ writer.Write(m_Thumb.pixels, thumbHeader.length);
+
+ writer.Write(&meta, offsetof(CaptureMetaData, driverName));
+
+ writer.Write(m_DriverName.c_str(), meta.driverNameLength);
+
+ if(writer.IsErrored())
+ {
+ RDCERR("Error writing file header");
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+}
+
+int RDCFile::SectionIndex(SectionType type) const
+{
+ for(size_t i = 0; i < m_Sections.size(); i++)
+ if(m_Sections[i].type == type)
+ return int(i);
+
+ return -1;
+}
+
+int RDCFile::SectionIndex(const char *name) const
+{
+ for(size_t i = 0; i < m_Sections.size(); i++)
+ if(m_Sections[i].name == name)
+ return int(i);
+
+ return -1;
+}
+
+StreamReader *RDCFile::ReadSection(int index) const
+{
+ if(m_Error != ContainerError::NoError)
+ return new StreamReader(StreamReader::InvalidStream);
+
+ if(m_File == NULL)
+ {
+ if(index < (int)m_MemorySections.size())
+ return new StreamReader(m_MemorySections[index]);
+
+ RDCERR("Section %d is not available in memory.", index);
+ return new StreamReader(StreamReader::InvalidStream);
+ }
+
+ const SectionProperties &props = m_Sections[index];
+ SectionLocation offsetSize = m_SectionLocations[index];
+ FileIO::fseek64(m_File, offsetSize.offs, SEEK_SET);
+
+ StreamReader *fileReader = new StreamReader(m_File, offsetSize.diskLength, Ownership::Nothing);
+
+ StreamReader *compReader = NULL;
+
+ if(props.flags & SectionFlags::LZ4Compressed)
+ {
+ // the user will delete the compressed reader, and then it will delete the compressor and the
+ // file reader
+ compReader = new StreamReader(new LZ4Decompressor(fileReader, Ownership::Stream),
+ props.uncompressedSize, Ownership::Stream);
+ }
+ else if(props.flags & SectionFlags::ZstdCompressed)
+ {
+ compReader = new StreamReader(new ZSTDDecompressor(fileReader, Ownership::Stream),
+ props.uncompressedSize, Ownership::Stream);
+ }
+
+ // if we're compressing return that writer, otherwise return the file writer directly
+ return compReader ? compReader : fileReader;
+}
+
+StreamWriter *RDCFile::WriteSection(const SectionProperties &props)
+{
+ if(m_Error != ContainerError::NoError)
+ return new StreamWriter(StreamWriter::InvalidStream);
+
+ // only handle the case of writing a section that doesn't exist yet.
+ // For handling a section that does exist, it depends on the section type:
+ // - For frame capture, then we just write to a new file since we want it
+ // to be first. Once the writing is done, copy across any other sections
+ // after it.
+ // - For non-frame capture, we remove the existing section and move up any
+ // sections that were after it. Then just return a new writer that appends
+
+ if(props.type != SectionType::Unknown)
+ {
+ if(SectionIndex(props.type) >= 0)
+ {
+ RDCERR("Replacing sections is currently not supported.");
+ return new StreamWriter(StreamWriter::InvalidStream);
+ }
+ RDCASSERT((size_t)props.type < (size_t)SectionType::Count);
+ }
+
+ if(SectionIndex(props.name.c_str()) >= 0)
+ {
+ RDCERR("Replacing sections is currently not supported.");
+ return new StreamWriter(StreamWriter::InvalidStream);
+ }
+
+ if(m_File == NULL)
+ {
+ // if we have no file to write to, we just cache it in memory for future use (e.g. later writing
+ // to disk via the CaptureFile interface wih structured data for the frame capture section)
+ StreamWriter *w = new StreamWriter(64 * 1024);
+
+ w->AddCloseCallback([this, props, w]() {
+ m_MemorySections.push_back(std::vector(w->GetData(), w->GetData() + w->GetOffset()));
+
+ m_Sections.push_back(props);
+ m_Sections.back().compressedSize = m_Sections.back().uncompressedSize =
+ m_MemorySections.back().size();
+ });
+
+ return w;
+ }
+
+ if(m_Sections.empty() && props.type != SectionType::FrameCapture)
+ {
+ RDCERR("The first section written must be frame capture data.");
+ return new StreamWriter(StreamWriter::InvalidStream);
+ }
+
+ if(m_CurrentWritingProps.type != SectionType::Count)
+ {
+ RDCERR("Only one section can be written at once.");
+ return new StreamWriter(StreamWriter::InvalidStream);
+ }
+
+ std::string name = props.name;
+ SectionType type = props.type;
+
+ // normalise names for known sections
+ if(props.type != SectionType::Unknown)
+ name = SectionTypeNames[(size_t)type];
+
+ FileIO::fseek64(m_File, 0, SEEK_END);
+
+ uint64_t headerOffset = FileIO::ftell64(m_File);
+
+ size_t numWritten;
+
+ // write section header
+ BinarySectionHeader header = {// IsASCII
+ '\0',
+ // zero
+ {0, 0, 0},
+ // sectionType
+ type,
+ // sectionCompressedLength
+ 0,
+ // sectionUncompressedLength
+ 0,
+ // sectionVersion
+ props.version,
+ // sectionFlags
+ props.flags,
+ // sectionNameLength
+ uint32_t(name.length() + 1)};
+
+ // write the header then name
+ numWritten = FileIO::fwrite(&header, 1, offsetof(BinarySectionHeader, name), m_File);
+ numWritten += FileIO::fwrite(name.c_str(), 1, name.size() + 1, m_File);
+
+ if(numWritten != offsetof(BinarySectionHeader, name) + name.size() + 1)
+ {
+ RDCERR("Error seeking to end of file, errno %d", errno);
+ m_Error = ContainerError::FileIO;
+ return new StreamWriter(StreamWriter::InvalidStream);
+ }
+
+ // create a writer for writing to disk. It shouldn't close the file
+ StreamWriter *fileWriter = new StreamWriter(m_File, Ownership::Nothing);
+
+ StreamWriter *compWriter = NULL;
+
+ if(props.flags & SectionFlags::LZ4Compressed)
+ {
+ // the user will delete the compressed writer, and then it will delete the compressor and the
+ // file writer
+ compWriter =
+ new StreamWriter(new LZ4Compressor(fileWriter, Ownership::Stream), Ownership::Stream);
+ }
+ else if(props.flags & SectionFlags::ZstdCompressed)
+ {
+ compWriter =
+ new StreamWriter(new ZSTDCompressor(fileWriter, Ownership::Stream), Ownership::Stream);
+ }
+
+ m_CurrentWritingProps = props;
+ m_CurrentWritingProps.name = name;
+
+ // register a destroy callback to tidy up the section at the end
+ fileWriter->AddCloseCallback([this, type, name, headerOffset, fileWriter, compWriter]() {
+
+ // the offset of the file writer is how many bytes were written to disk - the compressed length.
+ uint64_t compressedLength = fileWriter->GetOffset();
+
+ // if there was no compression, this is also the uncompressed length.
+ uint64_t uncompressedLength = compressedLength;
+ if(compWriter)
+ uncompressedLength = compWriter->GetOffset();
+
+ RDCLOG("Finishing write to section %u (%s). Compressed from %llu bytes to %llu", type,
+ name.c_str(), uncompressedLength, compressedLength);
+
+ // finish up the properties and add to list of sections
+ m_CurrentWritingProps.compressedSize = compressedLength;
+ m_CurrentWritingProps.uncompressedSize = uncompressedLength;
+
+ m_Sections.push_back(m_CurrentWritingProps);
+
+ m_CurrentWritingProps = SectionProperties();
+
+ FileIO::fseek64(m_File, headerOffset + offsetof(BinarySectionHeader, sectionCompressedLength),
+ SEEK_SET);
+
+ size_t bytesWritten = FileIO::fwrite(&compressedLength, 1, sizeof(uint64_t), m_File);
+ bytesWritten += FileIO::fwrite(&uncompressedLength, 1, sizeof(uint64_t), m_File);
+
+ if(bytesWritten != 2 * sizeof(uint64_t))
+ {
+ RDCERR("Error applying fixup to section header, errno %d", errno);
+ m_Error = ContainerError::FileIO;
+ return;
+ }
+ });
+
+ // if we're compressing return that writer, otherwise return the file writer directly
+ return compWriter ? compWriter : fileWriter;
+}
+
+FILE *RDCFile::StealImageFileHandle(std::string &filename)
+{
+ if(m_Driver != RDC_Image)
+ {
+ RDCERR("Can't steal image file handle for non-image RDCFile");
+ return NULL;
+ }
+
+ filename = m_Filename;
+
+ FILE *ret = m_File;
+ m_File = NULL;
+ return ret;
+}
diff --git a/renderdoc/serialise/rdcfile.h b/renderdoc/serialise/rdcfile.h
new file mode 100644
index 000000000..957eabfea
--- /dev/null
+++ b/renderdoc/serialise/rdcfile.h
@@ -0,0 +1,146 @@
+/******************************************************************************
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2017 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 "core/core.h"
+#include "streamio.h"
+
+enum class ContainerError
+{
+ NoError = 0,
+ FileNotFound,
+ FileIO,
+ Corrupt,
+ UnsupportedVersion,
+};
+
+enum class SectionFlags : uint32_t
+{
+ NoFlags = 0x0,
+ ASCIIStored = 0x1,
+ LZ4Compressed = 0x2,
+ ZstdCompressed = 0x4,
+};
+
+BITMASK_OPERATORS(SectionFlags);
+
+enum class SectionType : uint32_t
+{
+ Unknown = 0,
+ First = Unknown,
+ FrameCapture, // renderdoc/internal/framecapture
+ ResolveDatabase, // renderdoc/internal/resolvedb
+ FrameBookmarks, // renderdoc/ui/bookmarks
+ Notes, // renderdoc/ui/notes
+ Count,
+};
+
+ITERABLE_OPERATORS(SectionType);
+
+extern const char *SectionTypeNames[];
+
+struct SectionProperties
+{
+ std::string name;
+ SectionType type = SectionType::Count;
+ SectionFlags flags = SectionFlags::NoFlags;
+ uint64_t version = 0;
+ uint64_t uncompressedSize = 0;
+ uint64_t compressedSize = 0;
+};
+
+struct RDCThumb
+{
+ const byte *pixels = NULL;
+ uint32_t len = 0;
+ uint16_t width = 0;
+ uint16_t height = 0;
+};
+
+class RDCFile
+{
+public:
+ // version number of overall file format or chunk organisation. If the contents/meaning/order of
+ // chunks have changed this does not need to be bumped, there are version numbers within each
+ // API that interprets the stream that can be bumped.
+ static const uint32_t SERIALISE_VERSION = 0x00000100;
+
+ ~RDCFile();
+
+ // opens an existing file for read and/or modification. Error if file doesn't exist
+ void Open(const char *filename);
+ void Open(const std::vector &buffer);
+
+ // Sets the parameters of an RDCFile in memory.
+ void SetData(RDCDriver driver, const char *driverName, uint64_t machineIdent,
+ const RDCThumb *thumb);
+
+ // creates a new file with current properties, file will be overwritten if it already exists
+ void Create(const char *filename);
+
+ ContainerError ErrorCode() const { return m_Error; }
+ RDCDriver GetDriver() const { return m_Driver; }
+ const std::string &GetDriverName() const { return m_DriverName; }
+ uint64_t GetMachineIdent() const { return m_MachineIdent; }
+ const RDCThumb &GetThumbnail() const { return m_Thumb; }
+ int SectionIndex(SectionType type) const;
+ int SectionIndex(const char *name) const;
+ int NumSections() const { return int(m_Sections.size()); }
+ const SectionProperties &GetSectionProperties(int index) const { return m_Sections[index]; }
+ StreamReader *ReadSection(int index) const;
+ StreamWriter *WriteSection(const SectionProperties &props);
+
+ // Only valid if GetDriver returns RDC_Image, passes over the underlying FILE * for use
+ // loading the image directly, since the RDC container isn't there to read from a section.
+ FILE *StealImageFileHandle(std::string &filename);
+
+private:
+ void Init(StreamReader &reader);
+
+ FILE *m_File = NULL;
+ std::string m_Filename;
+ std::vector m_Buffer;
+
+ SectionProperties m_CurrentWritingProps;
+
+ uint32_t m_SerVer = 0;
+
+ RDCDriver m_Driver = RDC_Unknown;
+ std::string m_DriverName;
+ uint64_t m_MachineIdent = 0;
+ RDCThumb m_Thumb;
+
+ ContainerError m_Error = ContainerError::NoError;
+
+ struct SectionLocation
+ {
+ uint64_t offs;
+ uint64_t diskLength;
+ };
+
+ std::vector m_Sections;
+ std::vector m_SectionLocations;
+ std::vector> m_MemorySections;
+};