Fix structured data conversion to preserve timestamp base

* The timestamp base is queryable from the capture file and settable too, and
  conversions preserve un-rebased timestamps. Only rebasing when loading a
  capture for replay.
This commit is contained in:
baldurk
2020-08-27 15:54:01 +01:00
parent 9583a26754
commit 680857cb0f
24 changed files with 162 additions and 52 deletions
+23 -1
View File
@@ -1455,6 +1455,22 @@ replay support.
)");
virtual const char *RecordedMachineIdent() = 0;
DOCUMENT(R"(Retrieves the timestamp basis that all timestamps in the capture are relative to. May
be 0 if all timestamps are already absolute.
:return: The timestamp base value
:rtype: ``int``
)");
virtual uint64_t TimestampBase() = 0;
DOCUMENT(R"(Retrieves frequency for timestamps and durations to be divided by to convert to
microseconds. May be 1.0 if all timestamps and durations are already in microseconds.
:return: The timestamp frequency
:rtype: ``float``
)");
virtual double TimestampFrequency() = 0;
DOCUMENT(R"(Sets the matadata for this capture handle.
This function may only be called if the handle is 'empty' - i.e. no file has been opened with
@@ -1475,9 +1491,15 @@ This function may only be called if the handle is 'empty' - i.e. no file has bee
:param int thumbHeight: The height of the thumbnail. Ignored if :paramref:`SetMetadata.thumbData` is
empty.
:param bytes thumbData: The raw data of the thumbnail. If empty, no thumbnail is set.
:param int timeBase: The base value for timestamps in the capture. Can be set to 0 to indicate that
timestamps are already capture relative.
:param float timeFreq: The frequency for timestamps and durations to be divided by to convert to
microseconds. Can be set to 1.0 to indicate that timestamps and durations are already in
microseconds.
)");
virtual void SetMetadata(const char *driverName, uint64_t machineIdent, FileType thumbType,
uint32_t thumbWidth, uint32_t thumbHeight, const bytebuf &thumbData) = 0;
uint32_t thumbWidth, uint32_t thumbHeight, const bytebuf &thumbData,
uint64_t timeBase, double timeFreq) = 0;
DOCUMENT(R"(Opens a capture for replay locally and returns a handle to the capture. Only supported
for handles opened with a native ``rdc`` capture, otherwise this will fail.
+2 -1
View File
@@ -1197,7 +1197,8 @@ RDCFile *RenderDoc::CreateRDC(RDCDriver driver, uint32_t frameNum, const FramePi
EncodePixelsPNG(outRaw, outPng);
}
ret->SetData(driver, ToStr(driver).c_str(), OSUtility::GetMachineIdent(), &outPng);
ret->SetData(driver, ToStr(driver).c_str(), OSUtility::GetMachineIdent(), &outPng, m_TimeBase,
m_TimeFrequency);
FileIO::CreateParentDirectory(m_CurrentLogFile);
-19
View File
@@ -410,25 +410,6 @@ public:
void Initialise();
void RemoveHooks();
// these timestamp parameters are set while capturing to the base tick-count and tick frequency of
// the global timer, which will be saved with any new capture to convert timestamps and durations
// in ticks into microseconds. Older captures serialised timestamps and durations as microseconds
// directly. On replay these are set when the capture is first loaded and used to convert any
// serialised timestamps and durations. If an old capture is serialised, they will be set to base
// = 0 and frequency = 1.0 to ensure no conversion happens.
void SetGlobalTimestampParameters(uint64_t base, double frequency)
{
if(IsReplayApp())
{
m_TimeBase = base;
m_TimeFrequency = frequency;
}
}
void GetGlobalTimestampParameters(uint64_t &base, double &frequency)
{
base = m_TimeBase;
frequency = m_TimeFrequency;
}
const GlobalEnvironment &GetGlobalEnvironment() { return m_GlobalEnv; }
void InitialiseReplay(GlobalEnvironment env, const rdcarray<rdcstr> &args);
void ShutdownReplay();
+4 -4
View File
@@ -270,8 +270,8 @@ static void ActiveRemoteClientThread(ClientThread *threadData,
if(RemoteServer_DebugLogging())
{
reader.ConfigureStructuredExport(&GetRemoteServerChunkName, false);
writer.ConfigureStructuredExport(&GetRemoteServerChunkName, false);
reader.ConfigureStructuredExport(&GetRemoteServerChunkName, false, 0, 1.0);
writer.ConfigureStructuredExport(&GetRemoteServerChunkName, false, 0, 1.0);
rdcstr filename = FileIO::GetTempFolderFilename() + "/RenderDoc/RemoteServer_Server.log";
@@ -1228,8 +1228,8 @@ RemoteServer::RemoteServer(Network::Socket *sock, const rdcstr &deviceID)
if(RemoteServer_DebugLogging())
{
reader->ConfigureStructuredExport(&GetRemoteServerChunkName, false);
writer->ConfigureStructuredExport(&GetRemoteServerChunkName, false);
reader->ConfigureStructuredExport(&GetRemoteServerChunkName, false, 0, 1.0);
writer->ConfigureStructuredExport(&GetRemoteServerChunkName, false, 0, 1.0);
rdcstr filename = FileIO::GetTempFolderFilename() + "/RenderDoc/RemoteServer_Client.log";
+2 -1
View File
@@ -1209,7 +1209,8 @@ ReplayStatus WrappedID3D11DeviceContext::ReplayLog(CaptureState readType, uint32
if(IsLoading(m_State) || IsStructuredExporting(m_State))
{
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State));
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State),
m_pDevice->GetTimeBase(), m_pDevice->GetTimeFrequency());
ser.GetStructuredFile().Swap(m_pDevice->GetStructuredFile());
+2
View File
@@ -204,6 +204,8 @@ private:
rdcarray<Annotation> m_AnnotationQueue;
Threading::CriticalSection m_AnnotLock;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile = NULL;
uint64_t m_CurChunkOffset;
+13 -1
View File
@@ -1222,6 +1222,18 @@ ReplayStatus WrappedID3D11Device::ReadLogInitialisation(RDCFile *rdc, bool store
StreamReader *reader = rdc->ReadSection(sectionIdx);
if(IsStructuredExporting(m_State))
{
// when structured exporting don't do any timebase conversion
m_TimeBase = 0;
m_TimeFrequency = 1.0;
}
else
{
m_TimeBase = rdc->GetTimestampBase();
m_TimeFrequency = rdc->GetTimestampFrequency();
}
if(reader->IsErrored())
{
delete reader;
@@ -1233,7 +1245,7 @@ ReplayStatus WrappedID3D11Device::ReadLogInitialisation(RDCFile *rdc, bool store
ser.SetStringDatabase(&m_StringDB);
ser.SetUserData(GetResourceManager());
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers);
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers, m_TimeBase, m_TimeFrequency);
m_StructuredFile = &ser.GetStructuredFile();
+4
View File
@@ -381,6 +381,8 @@ private:
CaptureFailReason m_FailedReason;
uint32_t m_Failures = 0;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile = NULL;
SDFile m_StoredStructuredData;
@@ -444,6 +446,8 @@ public:
void UnlockForChunkRemoval();
SDFile &GetStructuredFile() { return *m_StructuredFile; }
uint64_t GetTimeBase() { return m_TimeBase; }
double GetTimeFrequency() { return m_TimeFrequency; }
void FirstFrame(IDXGISwapper *swapper);
rdcarray<DebugMessage> GetDebugMessages();
@@ -126,6 +126,8 @@ class WrappedID3D12CommandQueue : public ID3D12CommandQueue,
StreamReader *m_FrameReader = NULL;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile = NULL;
// command recording/replay data shared between queues and lists
+2 -1
View File
@@ -841,7 +841,8 @@ ReplayStatus WrappedID3D12CommandQueue::ReplayLog(CaptureState readType, uint32_
if(IsLoading(m_State) || IsStructuredExporting(m_State))
{
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State));
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State),
m_pDevice->GetTimeBase(), m_pDevice->GetTimeFrequency());
ser.GetStructuredFile().Swap(m_pDevice->GetStructuredFile());
+2
View File
@@ -346,6 +346,8 @@ struct D3D12CommandData
ResourceId m_LastPresentedImage;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile;
std::map<ResourceId, rdcarray<EventUsage> > m_ResourceUses;
+13 -1
View File
@@ -3193,6 +3193,18 @@ ReplayStatus WrappedID3D12Device::ReadLogInitialisation(RDCFile *rdc, bool store
StreamReader *reader = rdc->ReadSection(sectionIdx);
if(IsStructuredExporting(m_State))
{
// when structured exporting don't do any timebase conversion
m_TimeBase = 0;
m_TimeFrequency = 1.0;
}
else
{
m_TimeBase = rdc->GetTimestampBase();
m_TimeFrequency = rdc->GetTimestampFrequency();
}
if(reader->IsErrored())
{
delete reader;
@@ -3206,7 +3218,7 @@ ReplayStatus WrappedID3D12Device::ReadLogInitialisation(RDCFile *rdc, bool store
ser.SetStringDatabase(&m_StringDB);
ser.SetUserData(GetResourceManager());
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers);
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers, m_TimeBase, m_TimeFrequency);
m_StructuredFile = &ser.GetStructuredFile();
+4
View File
@@ -438,6 +438,8 @@ private:
rdcarray<DebugMessage> m_DebugMessages;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile = NULL;
SDFile m_StoredStructuredData;
@@ -684,6 +686,8 @@ public:
m_State = CaptureState::StructuredExport;
}
SDFile &GetStructuredFile() { return *m_StructuredFile; }
uint64_t GetTimeBase() { return m_TimeBase; }
double GetTimeFrequency() { return m_TimeFrequency; }
// interface for DXGI
virtual IUnknown *GetRealIUnknown() { return GetReal(); }
void *GetFrameCapturerDevice() { return (ID3D12Device *)this; }
+15 -2
View File
@@ -3243,6 +3243,18 @@ ReplayStatus WrappedOpenGL::ReadLogInitialisation(RDCFile *rdc, bool storeStruct
StreamReader *reader = rdc->ReadSection(sectionIdx);
if(IsStructuredExporting(m_State))
{
// when structured exporting don't do any timebase conversion
m_TimeBase = 0;
m_TimeFrequency = 1.0;
}
else
{
m_TimeBase = rdc->GetTimestampBase();
m_TimeFrequency = rdc->GetTimestampFrequency();
}
if(reader->IsErrored())
{
delete reader;
@@ -3254,7 +3266,7 @@ ReplayStatus WrappedOpenGL::ReadLogInitialisation(RDCFile *rdc, bool storeStruct
ser.SetStringDatabase(&m_StringDB);
ser.SetUserData(GetResourceManager());
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers);
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers, m_TimeBase, m_TimeFrequency);
m_StructuredFile = &ser.GetStructuredFile();
@@ -5014,7 +5026,8 @@ ReplayStatus WrappedOpenGL::ContextReplayLog(CaptureState readType, uint32_t sta
if(IsLoading(m_State) || IsStructuredExporting(m_State))
{
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State));
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State), m_TimeBase,
m_TimeFrequency);
ser.GetStructuredFile().Swap(*m_StructuredFile);
+2
View File
@@ -209,6 +209,8 @@ private:
GLResourceManager *m_ResourceManager;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile;
SDFile m_StoredStructuredData;
+15 -2
View File
@@ -2211,6 +2211,18 @@ ReplayStatus WrappedVulkan::ReadLogInitialisation(RDCFile *rdc, bool storeStruct
StreamReader *reader = rdc->ReadSection(sectionIdx);
if(IsStructuredExporting(m_State))
{
// when structured exporting don't do any timebase conversion
m_TimeBase = 0;
m_TimeFrequency = 1.0;
}
else
{
m_TimeBase = rdc->GetTimestampBase();
m_TimeFrequency = rdc->GetTimestampFrequency();
}
if(reader->IsErrored())
{
delete reader;
@@ -2222,7 +2234,7 @@ ReplayStatus WrappedVulkan::ReadLogInitialisation(RDCFile *rdc, bool storeStruct
ser.SetStringDatabase(&m_StringDB);
ser.SetUserData(GetResourceManager());
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers);
ser.ConfigureStructuredExport(&GetChunkName, storeStructuredBuffers, m_TimeBase, m_TimeFrequency);
m_StructuredFile = &ser.GetStructuredFile();
@@ -2411,7 +2423,8 @@ ReplayStatus WrappedVulkan::ContextReplayLog(CaptureState readType, uint32_t sta
if(IsLoading(m_State) || IsStructuredExporting(m_State))
{
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State));
ser.ConfigureStructuredExport(&GetChunkName, IsStructuredExporting(m_State), m_TimeBase,
m_TimeFrequency);
ser.GetStructuredFile().Swap(*m_StructuredFile);
+2
View File
@@ -348,6 +348,8 @@ private:
VulkanDrawcallCallback *m_DrawcallCallback;
void *m_SubmitChain;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0f;
SDFile *m_StructuredFile;
SDFile m_StoredStructuredData;
+8 -4
View File
@@ -113,11 +113,14 @@ public:
ReplaySupport LocalReplaySupport() { return m_Support; }
rdcstr DriverName() { return m_DriverName; }
const char *RecordedMachineIdent() { return m_Ident.c_str(); }
uint64_t TimestampBase() { return m_RDC ? m_RDC->GetTimestampBase() : 0; }
double TimestampFrequency() { return m_RDC ? m_RDC->GetTimestampFrequency() : 1.0; }
rdcpair<ReplayStatus, IReplayController *> OpenCapture(const ReplayOptions &opts,
RENDERDOC_ProgressCallback progress);
void SetMetadata(const char *driverName, uint64_t machineIdent, FileType thumbType,
uint32_t thumbWidth, uint32_t thumbHeight, const bytebuf &thumbData);
uint32_t thumbWidth, uint32_t thumbHeight, const bytebuf &thumbData,
uint64_t timeBase, double timeFreq);
ReplayStatus Convert(const char *filename, const char *filetype, const SDFile *file,
RENDERDOC_ProgressCallback progress);
@@ -369,7 +372,8 @@ rdcpair<ReplayStatus, IReplayController *> CaptureFile::OpenCapture(const Replay
}
void CaptureFile::SetMetadata(const char *driverName, uint64_t machineIdent, FileType thumbType,
uint32_t thumbWidth, uint32_t thumbHeight, const bytebuf &thumbData)
uint32_t thumbWidth, uint32_t thumbHeight, const bytebuf &thumbData,
uint64_t timeBase, double timeFreq)
{
if(m_RDC)
{
@@ -395,7 +399,7 @@ void CaptureFile::SetMetadata(const char *driverName, uint64_t machineIdent, Fil
}
m_RDC = new RDCFile;
m_RDC->SetData(driver, driverName, machineIdent, thumb);
m_RDC->SetData(driver, driverName, machineIdent, thumb, timeBase, timeFreq);
}
ReplayStatus CaptureFile::Convert(const char *filename, const char *filetype, const SDFile *file,
@@ -438,7 +442,7 @@ ReplayStatus CaptureFile::Convert(const char *filename, const char *filetype, co
RDCFile output;
output.SetData(m_RDC->GetDriver(), m_RDC->GetDriverName().c_str(), m_RDC->GetMachineIdent(),
&m_RDC->GetThumbnail());
&m_RDC->GetThumbnail(), m_RDC->GetTimestampBase(), m_RDC->GetTimestampFrequency());
output.Create(filename);
+24 -1
View File
@@ -310,6 +310,11 @@ static ReplayStatus Structured2XML(const char *filename, const RDCFile &file, ui
else
RDCERR("Unexpected thumbnail format %s", ToStr(th.format).c_str());
}
pugi::xml_node xTimebase = xHeader.append_child("timebase");
xTimebase.append_attribute("base") = file.GetTimestampBase();
xTimebase.append_attribute("frequency") = file.GetTimestampFrequency();
}
if(progress)
@@ -595,6 +600,24 @@ static ReplayStatus XML2Structured(const char *xml, const ThumbTypeAndData &thum
return ReplayStatus::FileCorrupted;
}
pugi::xml_node xTimebase = xThumbnail.next_sibling();
uint64_t timeBase = 0;
double timeFreq = 1.0;
// newer XML documents have the timebase here, allow conversion without it
if(xTimebase)
{
if(strcmp(xTimebase.name(), "timebase") != 0)
{
RDCERR("Malformed document, expected driver node");
return ReplayStatus::FileCorrupted;
}
timeBase = xTimebase.attribute("base").as_ullong();
timeFreq = xTimebase.attribute("frequency").as_double();
}
RDCThumb th;
th.format = thumb.format;
th.width = (uint16_t)xThumbnail.attribute("width").as_uint();
@@ -608,7 +631,7 @@ static ReplayStatus XML2Structured(const char *xml, const ThumbTypeAndData &thum
rdcthumb = &th;
}
rdc->SetData(driver, driverName.c_str(), machineIdent, rdcthumb);
rdc->SetData(driver, driverName.c_str(), machineIdent, rdcthumb, timeBase, timeFreq);
}
if(progress)
+9 -5
View File
@@ -297,6 +297,8 @@ void RDCFile::Open(const char *path)
m_Driver = RDCDriver::Image;
m_DriverName = "Image";
m_MachineIdent = 0;
m_TimeBase = 0;
m_TimeFrequency = 1.0;
return;
}
}
@@ -419,9 +421,8 @@ void RDCFile::Init(StreamReader &reader)
}
}
// explicitly set this, so if we load an old capture with no timebase it gets reset back to a good
// default state even if we previously opened a capture with a timebase
RenderDoc::Inst().SetGlobalTimestampParameters(timeBase.timeBase, timeBase.timeFreq);
m_TimeBase = timeBase.timeBase;
m_TimeFrequency = timeBase.timeFreq;
m_Driver = meta.driverID;
m_DriverName = driverName;
@@ -664,7 +665,7 @@ bool RDCFile::CopyFileTo(const char *filename)
}
void RDCFile::SetData(RDCDriver driver, const char *driverName, uint64_t machineIdent,
const RDCThumb *thumb)
const RDCThumb *thumb, uint64_t timeBase, double timeFreq)
{
m_Driver = driver;
m_DriverName = driverName;
@@ -673,6 +674,8 @@ void RDCFile::SetData(RDCDriver driver, const char *driverName, uint64_t machine
{
m_Thumb = *thumb;
}
m_TimeBase = timeBase;
m_TimeFrequency = timeFreq;
}
void RDCFile::Create(const char *filename)
@@ -751,7 +754,8 @@ void RDCFile::Create(const char *filename)
sizeof(CaptureTimeBase);
CaptureTimeBase timeBase;
RenderDoc::Inst().GetGlobalTimestampParameters(timeBase.timeBase, timeBase.timeFreq);
timeBase.timeBase = m_TimeBase;
timeBase.timeFreq = m_TimeFrequency;
{
StreamWriter writer(m_File, Ownership::Nothing);
+5 -1
View File
@@ -78,7 +78,7 @@ public:
// Sets the parameters of an RDCFile in memory.
void SetData(RDCDriver driver, const char *driverName, uint64_t machineIdent,
const RDCThumb *thumb);
const RDCThumb *thumb, uint64_t timeBase, double timeFreq);
// creates a new file with current properties, file will be overwritten if it already exists
void Create(const char *filename);
@@ -88,6 +88,8 @@ public:
RDCDriver GetDriver() const { return m_Driver; }
const rdcstr &GetDriverName() const { return m_DriverName; }
uint64_t GetMachineIdent() const { return m_MachineIdent; }
uint64_t GetTimestampBase() const { return m_TimeBase; }
double GetTimestampFrequency() const { return m_TimeFrequency; }
const RDCThumb &GetThumbnail() const { return m_Thumb; }
int SectionIndex(SectionType type) const;
int SectionIndex(const char *name) const;
@@ -114,6 +116,8 @@ private:
RDCDriver m_Driver = RDCDriver::Unknown;
rdcstr m_DriverName;
uint64_t m_MachineIdent = 0;
uint64_t m_TimeBase = 0;
double m_TimeFrequency = 1.0;
RDCThumb m_Thumb;
ContainerError m_Error = ContainerError::NoError;
-2
View File
@@ -97,8 +97,6 @@ Serialiser<SerialiserMode::Reading>::Serialiser(StreamReader *reader, Ownership
if(rootStructuredObj)
m_StructureStack.push_back(rootStructuredObj);
RenderDoc::Inst().GetGlobalTimestampParameters(m_TimerBase, m_TimerFrequency);
}
template <>
+5 -2
View File
@@ -163,11 +163,14 @@ public:
//////////////////////////////////////////
// Public serialisation interface
void ConfigureStructuredExport(ChunkLookup lookup, bool includeBuffers)
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);
@@ -1377,7 +1380,7 @@ public:
StructuredSerialiser(SDObject *obj, ChunkLookup lookup)
: Serialiser(new StreamReader(StreamReader::DummyStream), Ownership::Stream, obj)
{
ConfigureStructuredExport(lookup, false);
ConfigureStructuredExport(lookup, false, 0, 1.0);
SetStreamingMode(true);
SetDummy(true);
}
+4 -4
View File
@@ -189,7 +189,7 @@ TEST_CASE("Read/write via structured of basic types", "[serialiser]")
ChunkLookup testChunkLoop = [](uint32_t) -> rdcstr { return "TestChunk"; };
ser.ConfigureStructuredExport(testChunkLoop, true);
ser.ConfigureStructuredExport(testChunkLoop, true, 0, 1.0);
int64_t a;
uint64_t b;
@@ -654,7 +654,7 @@ TEST_CASE("Read/write chunk metadata", "[serialiser]")
ChunkLookup testChunkLoop = [](uint32_t) -> rdcstr { return "TestChunk"; };
ser.ConfigureStructuredExport(testChunkLoop, true);
ser.ConfigureStructuredExport(testChunkLoop, true, 0, 1.0);
ser.ReadChunk<uint32_t>();
@@ -908,7 +908,7 @@ TEST_CASE("Read/write container types", "[serialiser][structured]")
{
ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);
ser.ConfigureStructuredExport([](uint32_t) -> rdcstr { return "TestChunk"; }, true);
ser.ConfigureStructuredExport([](uint32_t) -> rdcstr { return "TestChunk"; }, true, 0, 1.0);
ser.ReadChunk<uint32_t>();
{
@@ -1188,7 +1188,7 @@ TEST_CASE("Read/write complex types", "[serialiser][structured]")
{
ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);
ser.ConfigureStructuredExport([](uint32_t) -> rdcstr { return "TestChunk"; }, true);
ser.ConfigureStructuredExport([](uint32_t) -> rdcstr { return "TestChunk"; }, true, 0, 1.0);
ser.ReadChunk<uint32_t>();
{