1
0
Fork 0

Added support for compressed continuous recordings.

src/Aircraft/replay.*:
    If /sim/replay/record-continuous-compression is true, we compress each
    frame's data as a separate raw zlib stream.

    Requires latest simgear's simgear/io/iostreams/zlibstream.cxx for
    decompression with ZLibCompressionFormat::ZLIB_RAW. Haven't figured out how
    to extend simgear's code to provide a compressing ostream so for now we
    have our own local compression code.

    We open popup and set sim/replay/replay-error=true if we fail to read
    compressed data.

scripts/python/recordreplay.py:
    Added test of compressed continuous recordings.

docs-mini/README-recordings.md:
    Added information about compressed format.
This commit is contained in:
Julian Smith 2021-06-10 18:16:47 +01:00
parent 08390be391
commit 7d414886e0
4 changed files with 410 additions and 116 deletions

View file

@ -14,7 +14,7 @@ As of 2020-12-22, there are three kinds of recordings:
Normal recordings are compressed and contain frames at varying intervals with more recent frames being closer together in time. They are generated from the in-memory recording that Flightgear always maintains. They may contain multiplayer information. Normal recordings are compressed and contain frames at varying intervals with more recent frames being closer together in time. They are generated from the in-memory recording that Flightgear always maintains. They may contain multiplayer information.
Continuous recordings are uncompressed and written directly to a file while Flightgear runs, giving high resolution with near unlimited recording time. They may contain multiplayer information. As of 2020-12-23 they may contain information about extra properties, allowing replay of views and main window position/size. Continuous recordings are written directly to a file while Flightgear runs, giving high resolution with near unlimited recording time. They may contain multiplayer information. As of 2020-12-23 they may contain information about extra properties, allowing replay of views and main window position/size. As of 2021-06-26 each frame's data can be compressed.
Recovery recordings are essentially single-frame Continuous recordings. When enabled, Flightgear creates them periodically to allow recovery of a session if Flightgear crashes. Recovery recordings are essentially single-frame Continuous recordings. When enabled, Flightgear creates them periodically to allow recovery of a session if Flightgear crashes.
@ -32,6 +32,7 @@ Recovery recordings are essentially single-frame Continuous recordings. When ena
* `/sim/replay/record-continuous` - if true, do continuous record to file. * `/sim/replay/record-continuous` - if true, do continuous record to file.
* `/sim/replay/record-signals` - if true (the default), include signals for user aircraft - these are the core values used to replay the user aircraft. * `/sim/replay/record-signals` - if true (the default), include signals for user aircraft - these are the core values used to replay the user aircraft.
* `/sim/replay/record-extra-properties` - if true, we include selected properties in recordings. * `/sim/replay/record-extra-properties` - if true, we include selected properties in recordings.
* `/sim/replay/record-continuous-compression` - if 1, we compress each frame's data.
* Recovery recordings: * Recovery recordings:
* `/sim/replay/record-recovery-period` - if non-zero, we update recovery recording in specified interval. * `/sim/replay/record-recovery-period` - if non-zero, we update recovery recording in specified interval.
@ -74,15 +75,13 @@ Normal recordings are written as a compressed gzip stream using `simgear::gzCont
### Continuous recordings ### Continuous recordings
Continuous recordings do not use compression, in order to simplify random access when replaying.
* Header: * Header:
* A zero-terminated magic string: `FlightGear Flight Recorder Tape` (variable `FlightRecorderFileMagic`). * A zero-terminated magic string: `FlightGear Flight Recorder Tape` (variable `FlightRecorderFileMagic`).
* A property tree represented as a `uint32` length followed by zero-terminated text. This contains: * A property tree represented as a `uint32` length followed by zero-terminated text. This contains:
* A `meta` node with various child nodes. * A `meta` node with various child nodes. If this contains `continuous-compression` with value `1`, then each frame's data is compressed..
* `data[]` nodes describing the data items in each frame in the order in which they occur. Supported values are: * `data[]` nodes describing the data items in each frame in the order in which they occur. Supported values are:
@ -98,7 +97,15 @@ Continuous recordings do not use compression, in order to simplify random access
* Frame time as a binary double. * Frame time as a binary double.
* A list of ordered `<length:32><data>` items as described by the `data[]` nodes in the header. This format allows Flightgear to skip data items that it doesn't understand if loading a recording that was created by a newer version. * If compression is used:
* uint8_t flags.
* Bit 0: this frame has signals.
* Bit 1: this frame has multiplayer information.
* Bit 2: this frame has extrap properties.
* uint32_t compressed-size.
* Frame data (can be compressed): a list of ordered `<length:32><data>` items as described by the `data[]` nodes in the header. This format allows Flightgear to skip data items that it doesn't understand if loading a recording that was created by a newer version.
* For `signals`, `<data>` is binary data for the core aircraft properties. * For `signals`, `<data>` is binary data for the core aircraft properties.
@ -115,6 +122,8 @@ Continuous recordings do not use compression, in order to simplify random access
When a Continuous recording is loaded, `FGReplay::loadTape()` first steps through the entire file, building up an index in memory that maps from frame times to a struct containing the offset of the frame in the file plus information on whether the frame has multiplayer and/or extra-properties information. This allows us to support the user jumping forwards and backwards in the recording. When a Continuous recording is loaded, `FGReplay::loadTape()` first steps through the entire file, building up an index in memory that maps from frame times to a struct containing the offset of the frame in the file plus information on whether the frame has multiplayer and/or extra-properties information. This allows us to support the user jumping forwards and backwards in the recording.
If the recording uses compression, indexing uses the uint8_t flags and uint32_t compressed-size fields and does not need to decompress each frame's data.
If we are replaying from a URL, indexing takes place in the background (by requesting callbacks from the download's `simgear::HTTP::FileRequest`) and replay starts immediately. Thus we avoid having to wait until the entire recording has been downloaded before starting replay. If we are replaying from a URL, indexing takes place in the background (by requesting callbacks from the download's `simgear::HTTP::FileRequest`) and replay starts immediately. Thus we avoid having to wait until the entire recording has been downloaded before starting replay.

View file

@ -230,7 +230,7 @@ class Fg:
def make_recording( def make_recording(
fg, fg,
continuous=0, continuous=0, # 2 means continuous with compression.
extra_properties=0, extra_properties=0,
main_view=0, main_view=0,
length=5, length=5,
@ -244,13 +244,15 @@ def make_recording(
fg.fg['/sim/replay/record-signals'] = True # Just in case they are disabled by user. fg.fg['/sim/replay/record-signals'] = True # Just in case they are disabled by user.
if continuous: if continuous:
assert not fg.fg['/sim/replay/record-continuous'] assert not fg.fg['/sim/replay/record-continuous']
if continuous == 2:
fg.fg['/sim/replay/record-continuous-compression'] = 1
fg.fg['/sim/replay/record-continuous'] = 1 fg.fg['/sim/replay/record-continuous'] = 1
t0 = time.time() t0 = time.time()
while 1: while 1:
if time.time() > t0 + length: if time.time() > t0 + length:
break break
fg.run_command('run view-step step=1')
time.sleep(1) time.sleep(1)
fg.run_command('run view-step step=1')
fg.fg['/sim/replay/record-continuous'] = 0 fg.fg['/sim/replay/record-continuous'] = 0
path = f'{g_tapedir}/{fg.aircraft}-continuous.fgtape' path = f'{g_tapedir}/{fg.aircraft}-continuous.fgtape'
time.sleep(1) time.sleep(1)
@ -332,26 +334,44 @@ def test_record_replay(
fg.waitfor('/sim/fdm-initialized', 1, timeout=45) fg.waitfor('/sim/fdm-initialized', 1, timeout=45)
fg.waitfor('/sim/replay/replay-state', 1) fg.waitfor('/sim/replay/replay-state', 1)
t0 = time.time()
# Check replay time is ok. # Check replay time is ok.
rtime_begin = fg.fg['/sim/replay/start-time'] rtime_begin = fg.fg['/sim/replay/start-time']
rtime_end = fg.fg['/sim/replay/end-time'] rtime_end = fg.fg['/sim/replay/end-time']
rtime = rtime_end - rtime_begin rtime = rtime_end - rtime_begin
log(f'rtime={rtime_begin}..{rtime_end}, recording length: {rtime}, length={length}') log(f'rtime={rtime_begin}..{rtime_end}, recording length: {rtime}, length={length}')
assert rtime > length-1 and rtime < length+2, \ assert rtime > length-1 and rtime <= length+2, \
f'length={length} rtime_begin={rtime_begin} rtime_end={rtime_end} rtime={rtime}' f'length={length} rtime_begin={rtime_begin} rtime_end={rtime_end} rtime={rtime}'
num_frames_extra_properties = fg.fg['/sim/replay/continuous-stats-num-frames-extra-properties'] num_frames_extra_properties = fg.fg['/sim/replay/continuous-stats-num-frames-extra-properties']
log(f'num_frames_extra_properties={num_frames_extra_properties}') log(f'num_frames_extra_properties={num_frames_extra_properties}')
if continuous: if continuous:
if main_view: if main_view:
assert num_frames_extra_properties > 1 assert num_frames_extra_properties > 1, f'num_frames_extra_properties={num_frames_extra_properties}'
else: else:
assert num_frames_extra_properties == 0 assert num_frames_extra_properties == 0
else: else:
assert num_frames_extra_properties in (0, None), \ assert num_frames_extra_properties in (0, None), \
f'num_frames_extra_properties={num_frames_extra_properties}' f'num_frames_extra_properties={num_frames_extra_properties}'
time.sleep(length) fg.run_command('run dialog-show dialog-name=replay')
while 1:
t = time.time()
if t < t0 + length - 1:
pass
# Disabled because it seems that Flightgear starts replaying before
# we see replay-state set to 1 because scenery loading blocks
# things.
#
#assert not fg.fg['/sim/replay/replay-state-eof'], f'Replay has finished too early; lenth={length} t-t0={t-t0}'
if t > t0 + length + 1:
assert fg.fg['/sim/replay/replay-state-eof'], f'Replay has not finished on time; lenth={length} t-t0={t-t0}'
break
e = fg.fg['sim/replay/replay-error']
assert not e, f'Replay failed: e={e}'
time.sleep(1)
fg.close() fg.close()
@ -676,7 +696,7 @@ if __name__ == '__main__':
fgfs_old = None fgfs_old = None
do_test = 'all' do_test = 'all'
continuous_s = [0, 1] continuous_s = [0, 1, 2] # 2 is continuous with compression.
extra_properties_s = [0, 1] extra_properties_s = [0, 1]
main_view_s = [0, 1] main_view_s = [0, 1]
multiplayer_s = [0, 1] multiplayer_s = [0, 1]
@ -698,19 +718,20 @@ if __name__ == '__main__':
elif arg == '--carrier': elif arg == '--carrier':
do_test = 'carrier' do_test = 'carrier'
elif arg == '--continuous': elif arg == '--continuous':
continuous_s = map(int, next(args).split(',')) continuous_s = [int(x) for x in next(args).split(',')]
log(f'continuous_s={continuous_s}')
elif arg == '--tape-dir': elif arg == '--tape-dir':
g_tapedir = next(args) g_tapedir = next(args)
elif arg == '--extra-properties': elif arg == '--extra-properties':
extra_properties_s = map(int, next(args).split(',')) extra_properties_s = [int(x) for x in next(args).split(',')]
elif arg == '--it-max': elif arg == '--it-max':
it_max = int(next(args)) it_max = int(next(args))
elif arg == '--it-min': elif arg == '--it-min':
it_min = int(next(args)) it_min = int(next(args))
elif arg == '--main-view': elif arg == '--main-view':
main_view_s = map(int, next(args).split(',')) main_view_s = [int(x) for x in next(args).split(',')]
elif arg == '--multiplayer': elif arg == '--multiplayer':
multiplayer_s = map(int, next(args).split(',')) multiplayer_s = [int(x) for x in next(args).split(',')]
elif arg == '-f': elif arg == '-f':
fgfs = next(args) fgfs = next(args)
elif arg == '--f-old': elif arg == '--f-old':
@ -748,6 +769,7 @@ if __name__ == '__main__':
length=10, length=10,
) )
else: else:
log(f'continuous_s={continuous_s}')
its_max = len(multiplayer_s) * len(continuous_s) * len(extra_properties_s) * len(main_view_s) * len(fgfs_reverse_s) its_max = len(multiplayer_s) * len(continuous_s) * len(extra_properties_s) * len(main_view_s) * len(fgfs_reverse_s)
it = 0 it = 0
for multiplayer in multiplayer_s: for multiplayer in multiplayer_s:

View file

@ -29,12 +29,20 @@
#include <float.h> #include <float.h>
#include <string.h> #include <string.h>
#include <memory>
#include <iostream>
#include <streambuf>
#include <sstream>
#include <zlib.h>
#include <osgViewer/ViewerBase> #include <osgViewer/ViewerBase>
#include <simgear/constants.h> #include <simgear/constants.h>
#include <simgear/structure/exception.hxx> #include <simgear/structure/exception.hxx>
#include <simgear/props/props_io.hxx> #include <simgear/props/props_io.hxx>
#include <simgear/io/iostreams/gzcontainerfile.hxx> #include <simgear/io/iostreams/gzcontainerfile.hxx>
#include <simgear/io/iostreams/zlibstream.hxx>
#include <simgear/misc/sg_dir.hxx> #include <simgear/misc/sg_dir.hxx>
#include <simgear/misc/stdint.hxx> #include <simgear/misc/stdint.hxx>
#include <simgear/misc/strutils.hxx> #include <simgear/misc/strutils.hxx>
@ -151,6 +159,7 @@ FGReplay::FGReplay() :
last_lt_time(0.0), last_lt_time(0.0),
last_msg_time(0), last_msg_time(0),
last_replay_state(0), last_replay_state(0),
replay_error(fgGetNode("sim/replay/replay-error", true)),
m_sim_startup_xpos(fgGetNode("sim/startup/xpos", true)), m_sim_startup_xpos(fgGetNode("sim/startup/xpos", true)),
m_sim_startup_ypos(fgGetNode("sim/startup/ypos", true)), m_sim_startup_ypos(fgGetNode("sim/startup/ypos", true)),
m_sim_startup_xsize(fgGetNode("sim/startup/xsize", true)), m_sim_startup_xsize(fgGetNode("sim/startup/xsize", true)),
@ -299,7 +308,12 @@ void FGReplay::valueChanged(SGPropertyNode * node)
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to create link " << path_timeless.c_str() << " => " << path.file()); SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to create link " << path_timeless.c_str() << " => " << path.file());
} }
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Starting continuous recording to " << path); SG_LOG(SG_SYSTEMS, SG_DEBUG, "Starting continuous recording to " << path);
popupTip("Continuous record to file started", 5 /*delay*/); if (m_continuous_out_compression) {
popupTip("Continuous+compressed record to file started", 5 /*delay*/);
}
else {
popupTip("Continuous record to file started", 5 /*delay*/);
}
} }
} }
@ -683,6 +697,133 @@ saveRawReplayData(gzContainerWriter& output, const replay_list_type& ReplayData,
} }
/* streambuf that compresses using deflate(). */
struct compression_streambuf : std::streambuf
{
compression_streambuf(
std::ostream& out,
size_t buffer_uncompressed_size,
size_t buffer_compressed_size
)
:
std::streambuf(),
out(out),
buffer_uncompressed(new char[buffer_uncompressed_size]),
buffer_uncompressed_size(buffer_uncompressed_size),
buffer_compressed(new char[buffer_compressed_size]),
buffer_compressed_size(buffer_compressed_size)
{
zstream.zalloc = nullptr;
zstream.zfree = nullptr;
zstream.opaque = nullptr;
zstream.next_in = nullptr;
zstream.avail_in = 0;
zstream.next_out = (unsigned char*) &buffer_compressed[0];
zstream.avail_out = buffer_compressed_size;
int e = deflateInit2(
&zstream,
Z_DEFAULT_COMPRESSION,
Z_DEFLATED,
-15 /*windowBits*/,
8 /*memLevel*/,
Z_DEFAULT_STRATEGY
);
if (e != Z_OK)
{
throw std::runtime_error("deflateInit2() failed");
}
// We leave space for one character to simplify overflow().
setp(&buffer_uncompressed[0], &buffer_uncompressed[0] + buffer_uncompressed_size - 1);
}
// Flush compressed data to .out and reset zstream.next_out.
void _flush()
{
// Send all data in .buffer_compressed to .out.
size_t n = (char*) zstream.next_out - &buffer_compressed[0];
out.write(&buffer_compressed[0], n);
zstream.next_out = (unsigned char*) &buffer_compressed[0];
zstream.avail_out = buffer_compressed_size;
}
/* Compresses specified bytes from buffer_uncompressed into
buffer_compressed, flushing to .out as necessary. Returns true if we get
EOF writing to .out. */
bool _deflate(size_t n, bool flush)
{
assert(this->pbase() == &buffer_uncompressed[0]);
zstream.next_in = (unsigned char*) &buffer_uncompressed[0];
zstream.avail_in = n;
for(;;)
{
if (!flush && !zstream.avail_in) break;
if (!zstream.avail_out) _flush();
int e = deflate(&zstream, (!zstream.avail_in && flush) ? Z_FINISH : Z_NO_FLUSH);
if (e != Z_OK && e != Z_STREAM_END)
{
throw std::runtime_error("zip_deflate() failed");
}
if (e == Z_STREAM_END) break;
}
if (flush) _flush();
// We leave space for one character to simplify overflow().
setp(&buffer_uncompressed[0], &buffer_uncompressed[0] + buffer_uncompressed_size - 1);
if (!out) return true; // EOF.
return false;
}
int overflow(int c) override
{
// We've deliberately left space for one character, into which we write <c>.
assert(this->pptr() == &buffer_uncompressed[0] + buffer_uncompressed_size - 1);
*this->pptr() = (char) c;
if (_deflate(buffer_uncompressed_size, false /*flush*/)) return EOF;
return c;
}
int sync() override
{
_deflate(pptr() - &buffer_uncompressed[0], true /*flush*/);
return 0;
}
~compression_streambuf()
{
deflateEnd(&zstream);
}
std::ostream& out;
z_stream zstream;
std::unique_ptr<char[]> buffer_uncompressed;
size_t buffer_uncompressed_size;
std::unique_ptr<char[]> buffer_compressed;
size_t buffer_compressed_size;
};
/* Accepts uncompressed data via .write(), operator<< etc, and writes
compressed data to the supplied std::ostream. */
struct compression_ostream : std::ostream
{
compression_ostream(
std::ostream& out,
size_t buffer_uncompressed_size,
size_t buffer_compressed_size
)
:
std::ostream(&streambuf),
streambuf(out, buffer_uncompressed_size, buffer_compressed_size)
{
}
compression_streambuf streambuf;
};
// Sets things up for writing to a normal or continuous fgtape file. // Sets things up for writing to a normal or continuous fgtape file.
// //
// extra: // extra:
@ -702,7 +843,8 @@ static SGPropertyNode_ptr saveSetup(
const SGPropertyNode* extra, const SGPropertyNode* extra,
const SGPath& path, const SGPath& path,
double duration, double duration,
FGTapeType tape_type FGTapeType tape_type,
int continuous_compression=0
) )
{ {
SGPropertyNode_ptr config; SGPropertyNode_ptr config;
@ -722,7 +864,9 @@ static SGPropertyNode_ptr saveSetup(
meta->setStringValue("aircraft-fdm", fgGetString("/sim/flight-model", "")); meta->setStringValue("aircraft-fdm", fgGetString("/sim/flight-model", ""));
meta->setStringValue("closest-airport-id", fgGetString("/sim/airport/closest-airport-id", "")); meta->setStringValue("closest-airport-id", fgGetString("/sim/airport/closest-airport-id", ""));
meta->setStringValue("aircraft-version", fgGetString("/sim/aircraft-version", "(undefined)")); meta->setStringValue("aircraft-version", fgGetString("/sim/aircraft-version", "(undefined)"));
if (tape_type == FGTapeType_CONTINUOUS) {
meta->setIntValue("continuous-compression", continuous_compression);
}
// add information on the tape's recording duration // add information on the tape's recording duration
meta->setDoubleValue("tape-duration", duration); meta->setDoubleValue("tape-duration", duration);
char StrBuffer[30]; char StrBuffer[30];
@ -783,7 +927,9 @@ SGPropertyNode_ptr FGReplay::continuousWriteHeader(
FGTapeType tape_type FGTapeType tape_type
) )
{ {
SGPropertyNode_ptr config = saveSetup(NULL /*Extra*/, path, 0 /*Duration*/, tape_type); m_continuous_out_compression = fgGetInt("/sim/replay/record-continuous-compression");
SGPropertyNode_ptr config = saveSetup(NULL /*Extra*/, path, 0 /*Duration*/,
tape_type, m_continuous_out_compression);
SGPropertyNode* signals = config->getNode("signals", true /*create*/); SGPropertyNode* signals = config->getNode("signals", true /*create*/);
m_pRecorder->getConfig(signals); m_pRecorder->getConfig(signals);
@ -804,48 +950,8 @@ SGPropertyNode_ptr FGReplay::continuousWriteHeader(
return config; return config;
} }
static void writeFrame2(FGReplayData* r, std::ostream& out, SGPropertyNode_ptr config)
// Writes one frame of continuous record information.
//
bool
FGReplay::continuousWriteFrame(FGReplayData* r, std::ostream& out, SGPropertyNode_ptr config)
{ {
SG_LOG(SG_SYSTEMS, SG_BULK, "writing frame."
<< " out.tellp()=" << out.tellp()
<< " r->sim_time=" << r->sim_time
);
// Don't write frame if no data to write.
bool r_has_data = false;
for (auto data: config->getChildren("data")) {
const char* data_type = data->getStringValue();
if (!strcmp(data_type, "signals")) {
r_has_data = true;
break;
}
else if (!strcmp(data_type, "multiplayer")) {
if (!r->multiplayer_messages.empty()) {
r_has_data = true;
break;
}
}
else if (!strcmp(data_type, "extra-properties")) {
if (!r->extra_properties.empty()) {
r_has_data = true;
break;
}
}
else {
SG_LOG(SG_SYSTEMS, SG_ALERT, "unrecognised data_type=" << data_type);
assert(0);
}
}
if (!r_has_data) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Not writing frame because no data to write");
return true;
}
out.write(reinterpret_cast<char*>(&r->sim_time), sizeof(r->sim_time));
for (auto data: config->getChildren("data")) { for (auto data: config->getChildren("data")) {
const char* data_type = data->getStringValue(); const char* data_type = data->getStringValue();
if (!strcmp(data_type, "signals")) { if (!strcmp(data_type, "signals")) {
@ -880,6 +986,71 @@ FGReplay::continuousWriteFrame(FGReplayData* r, std::ostream& out, SGPropertyNod
} }
} }
}
// Writes one frame of continuous record information.
//
bool
FGReplay::continuousWriteFrame(FGReplayData* r, std::ostream& out, SGPropertyNode_ptr config)
{
SG_LOG(SG_SYSTEMS, SG_BULK, "writing frame."
<< " out.tellp()=" << out.tellp()
<< " r->sim_time=" << r->sim_time
);
// Don't write frame if no data to write.
//bool r_has_data = false;
bool has_signals = false;
bool has_multiplayer = false;
bool has_extra_properties = false;
for (auto data: config->getChildren("data")) {
const char* data_type = data->getStringValue();
if (!strcmp(data_type, "signals")) {
has_signals = true;
}
else if (!strcmp(data_type, "multiplayer")) {
if (!r->multiplayer_messages.empty()) {
has_multiplayer = true;
}
}
else if (!strcmp(data_type, "extra-properties")) {
if (!r->extra_properties.empty()) {
has_extra_properties = true;
}
}
else {
SG_LOG(SG_SYSTEMS, SG_ALERT, "unrecognised data_type=" << data_type);
assert(0);
}
}
if (!has_signals && !has_multiplayer && !has_extra_properties) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Not writing frame because no data to write");
return true;
}
out.write(reinterpret_cast<char*>(&r->sim_time), sizeof(r->sim_time));
if (m_continuous_out_compression) {
uint8_t flags = 0;
if (has_signals) flags |= 1;
if (has_multiplayer) flags |= 2;
if (has_extra_properties) flags |= 4;
out.write((char*) &flags, sizeof(flags));
/* We need to first write the size of the compressed data so compress
to a temporary ostringstream first. */
std::ostringstream compressed;
compression_ostream out_compressing(compressed, 1024, 1024);
writeFrame2(r, out_compressing, config);
out_compressing.flush();
uint32_t compressed_size = compressed.str().size();
out.write((char*) &compressed_size, sizeof(compressed_size));
out.write((char*) compressed.str().c_str(), compressed.str().size());
}
else
{
writeFrame2(r, out, config);
}
bool ok = true; bool ok = true;
if (!out) ok = false; if (!out) ok = false;
return ok; return ok;
@ -1553,14 +1724,14 @@ FGReplay::replay(double time, FGReplayData* pCurrentFrame, FGReplayData* pOldFra
m_pRecorder->replay(time, pCurrentFrame, pOldFrame, xpos, ypos, xsize, ysize); m_pRecorder->replay(time, pCurrentFrame, pOldFrame, xpos, ypos, xsize, ysize);
} }
static int16_t read_int16(std::ifstream& in, size_t& pos) static int16_t read_int16(std::istream& in, size_t& pos)
{ {
int16_t a; int16_t a;
in.read(reinterpret_cast<char*>(&a), sizeof(a)); in.read(reinterpret_cast<char*>(&a), sizeof(a));
pos += sizeof(a); pos += sizeof(a);
return a; return a;
} }
static std::string read_string(std::ifstream& in, size_t& pos) static std::string read_string(std::istream& in, size_t& pos)
{ {
int16_t length = read_int16(in, pos); int16_t length = read_int16(in, pos);
std::vector<char> path(length); std::vector<char> path(length);
@ -1572,7 +1743,7 @@ static std::string read_string(std::ifstream& in, size_t& pos)
/* Reads extra-property change items in next <length> bytes. Throws if we don't /* Reads extra-property change items in next <length> bytes. Throws if we don't
exactly read <length> bytes. */ exactly read <length> bytes. */
static void ReadFGReplayDataExtraProperties(std::ifstream& in, FGReplayData* replay_data, uint32_t length) static void ReadFGReplayDataExtraProperties(std::istream& in, FGReplayData* replay_data, uint32_t length)
{ {
SG_LOG(SG_SYSTEMS, SG_BULK, "reading extra-properties. length=" << length); SG_LOG(SG_SYSTEMS, SG_BULK, "reading extra-properties. length=" << length);
size_t pos=0; size_t pos=0;
@ -1600,34 +1771,23 @@ static void ReadFGReplayDataExtraProperties(std::ifstream& in, FGReplayData* rep
} }
} }
/* Reads all or part of a FGReplayData from uncompressed file. */ static bool ReadFGReplayData2(
static std::unique_ptr<FGReplayData> ReadFGReplayData( std::istream& in,
std::ifstream& in,
size_t pos,
SGPropertyNode* config, SGPropertyNode* config,
bool load_signals, bool load_signals,
bool load_multiplayer, bool load_multiplayer,
bool load_extra_properties bool load_extra_properties,
FGReplayData* ret
) )
{ {
/* Need to clear any eof bit, otherwise seekg() will not work (which is
pretty unhelpful). E.g. see:
https://stackoverflow.com/questions/16364301/whats-wrong-with-the-ifstream-seekg
*/
SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame. pos=" << pos);
in.clear();
in.seekg(pos);
std::unique_ptr<FGReplayData> ret(new FGReplayData);
in.read(reinterpret_cast<char*>(&ret->sim_time), sizeof(ret->sim_time));
ret->raw_data.resize(0); ret->raw_data.resize(0);
for (auto data: config->getChildren("data")) { for (auto data: config->getChildren("data")) {
const char* data_type = data->getStringValue(); const char* data_type = data->getStringValue();
SG_LOG(SG_SYSTEMS, SG_DEBUG, "in.tellg()=" << in.tellg() << " data_type=" << data_type); SG_LOG(SG_SYSTEMS, SG_BULK, "in.tellg()=" << in.tellg() << " data_type=" << data_type);
uint32_t length; uint32_t length;
in.read(reinterpret_cast<char*>(&length), sizeof(length)); in.read(reinterpret_cast<char*>(&length), sizeof(length));
SG_LOG(SG_SYSTEMS, SG_DEBUG, "length=" << length); SG_LOG(SG_SYSTEMS, SG_DEBUG, "length=" << length);
if (!in) break;
if (load_signals && !strcmp(data_type, "signals")) { if (load_signals && !strcmp(data_type, "signals")) {
ret->raw_data.resize(length); ret->raw_data.resize(length);
in.read(&ret->raw_data.front(), ret->raw_data.size()); in.read(&ret->raw_data.front(), ret->raw_data.size());
@ -1653,14 +1813,64 @@ static std::unique_ptr<FGReplayData> ReadFGReplayData(
} }
} }
else if (load_extra_properties && !strcmp(data_type, "extra-properties")) { else if (load_extra_properties && !strcmp(data_type, "extra-properties")) {
ReadFGReplayDataExtraProperties(in, ret.get(), length); ReadFGReplayDataExtraProperties(in, ret, length);
} }
else { else {
SG_LOG(SG_GENERAL, SG_BULK, "Skipping unrecognised data: " << data_type); SG_LOG(SG_GENERAL, SG_BULK, "Skipping unrecognised/unwanted data: " << data_type);
in.seekg(length, std::ios_base::cur); in.seekg(length, std::ios_base::cur);
} }
} }
if (!in) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read fgtape data");
return false;
}
return true;
}
/* Reads all or part of a FGReplayData from uncompressed file. */
static std::unique_ptr<FGReplayData> ReadFGReplayData(
std::ifstream& in,
size_t pos,
SGPropertyNode* config,
bool load_signals,
bool load_multiplayer,
bool load_extra_properties,
int m_continuous_in_compression
)
{
/* Need to clear any eof bit, otherwise seekg() will not work (which is
pretty unhelpful). E.g. see:
https://stackoverflow.com/questions/16364301/whats-wrong-with-the-ifstream-seekg
*/
SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame. pos=" << pos);
in.clear();
in.seekg(pos);
std::unique_ptr<FGReplayData> ret(new FGReplayData);
in.read(reinterpret_cast<char*>(&ret->sim_time), sizeof(ret->sim_time));
if (!in) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read fgtape frame at offset " << pos);
return nullptr;
}
bool ok;
if (m_continuous_in_compression)
{
uint8_t flags;
uint32_t compressed_size;
in.read((char*) &flags, sizeof(flags));
in.read((char*) &compressed_size, sizeof(compressed_size));
simgear::ZlibDecompressorIStream in_decompress(in, SGPath(), simgear::ZLibCompressionFormat::ZLIB_RAW);
ok = ReadFGReplayData2(in_decompress, config, load_signals, load_multiplayer, load_extra_properties, ret.get());
}
else
{
ok = ReadFGReplayData2(in, config, load_signals, load_multiplayer, load_extra_properties, ret.get());
}
if (!ok) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read fgtape frame at offset " << pos);
return nullptr;
}
return ret; return ret;
} }
@ -1691,8 +1901,17 @@ void FGReplay::replay(
m_continuous_in_config, m_continuous_in_config,
replay_signals, replay_signals,
replay_multiplayer, replay_multiplayer,
replay_extra_properties replay_extra_properties,
m_continuous_in_compression
); );
if (!replay_data) {
if (!replay_error->getBoolValue()) {
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to read fgtape frame at offset=" << offset << " time=" << time);
popupTip("Replay failed: cannot read fgtape data", 10);
replay_error->setBoolValue(true);
}
return;
}
assert(replay_data.get()); assert(replay_data.get());
std::unique_ptr<FGReplayData> replay_data_old; std::unique_ptr<FGReplayData> replay_data_old;
if (offset_old) { if (offset_old) {
@ -1702,7 +1921,8 @@ void FGReplay::replay(
m_continuous_in_config, m_continuous_in_config,
replay_signals, replay_signals,
replay_multiplayer, replay_multiplayer,
replay_extra_properties replay_extra_properties,
m_continuous_in_compression
); );
} }
if (replay_extra_properties) SG_LOG(SG_SYSTEMS, SG_BULK, if (replay_extra_properties) SG_LOG(SG_SYSTEMS, SG_BULK,
@ -1964,7 +2184,7 @@ int FGReplay::loadContinuousHeader(const std::string& path, std::istream* in, SG
ok = true; ok = true;
} }
catch (std::exception& e) { catch (std::exception& e) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read Config properties in: " << path); SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read Config properties in: " << path << ": " << e.what());
} }
if (!ok) { if (!ok) {
// Failed to read properties, so indicate that further download is needed. // Failed to read properties, so indicate that further download is needed.
@ -1981,6 +2201,7 @@ void FGReplay::indexContinuousRecording(const void* data, size_t numbytes)
<< " data=" << data << " data=" << data
<< " numbytes=" << numbytes << " numbytes=" << numbytes
<< " m_continuous_indexing_pos=" << m_continuous_indexing_pos << " m_continuous_indexing_pos=" << m_continuous_indexing_pos
<< " m_continuous_in_compression=" << m_continuous_in_compression
<< " m_continuous_in_time_to_frameinfo.size()=" << m_continuous_in_time_to_frameinfo.size() << " m_continuous_in_time_to_frameinfo.size()=" << m_continuous_in_time_to_frameinfo.size()
); );
time_t t0 = time(NULL); time_t t0 = time(NULL);
@ -2011,40 +2232,76 @@ void FGReplay::indexContinuousRecording(const void* data, size_t numbytes)
<< " m_continuous_indexing_in.tellg()=" << m_continuous_indexing_in.tellg() << " m_continuous_indexing_in.tellg()=" << m_continuous_indexing_in.tellg()
<< " sim_time=" << sim_time << " sim_time=" << sim_time
); );
FGFrameInfo frameinfo; FGFrameInfo frameinfo;
frameinfo.offset = m_continuous_indexing_pos; frameinfo.offset = m_continuous_indexing_pos;
if (m_continuous_in_compression)
auto datas = m_continuous_in_config->getChildren("data"); {
SG_LOG(SG_SYSTEMS, SG_BULK, "datas.size()=" << datas.size()); // Skip compressed frame data without decompressing it.
for (auto data: datas) { uint8_t flags;
uint32_t length; m_continuous_indexing_in.read((char*) &flags, sizeof(flags));
m_continuous_indexing_in.read(reinterpret_cast<char*>(&length), sizeof(length)); frameinfo.has_signals = flags & 1;
SG_LOG(SG_SYSTEMS, SG_BULK, frameinfo.has_multiplayer = flags & 2;
"m_continuous_in.tellg()=" << m_continuous_indexing_in.tellg() frameinfo.has_extra_properties = flags & 4;
<< " Skipping data_type=" << data->getStringValue()
<< " length=" << length if (frameinfo.has_signals)
); {
// Move forward <length> bytes. stats["signals"].num_frames += 1;
m_continuous_indexing_in.seekg(length, std::ios_base::cur);
if (!m_continuous_indexing_in) {
// Dont add bogus info to <stats>.
break;
} }
if (length) { if (frameinfo.has_multiplayer)
stats[data->getStringValue()].num_frames += 1; {
stats[data->getStringValue()].bytes += length; stats["multiplayer"].num_frames += 1;
if (!strcmp(data->getStringValue(), "signals")) { ++m_num_frames_multiplayer;
frameinfo.has_signals = true; m_continuous_in_multiplayer = true;
}
if (frameinfo.has_extra_properties)
{
stats["extra-properties"].num_frames += 1;
++m_num_frames_extra_properties;
m_continuous_in_extra_properties = true;
}
uint32_t compressed_size;
m_continuous_indexing_in.read((char*) &compressed_size, sizeof(compressed_size));
SG_LOG(SG_SYSTEMS, SG_BULK, "compressed_size=" << compressed_size);
m_continuous_indexing_in.seekg(compressed_size, std::ios_base::cur);
}
else
{
// Skip frame data.
auto datas = m_continuous_in_config->getChildren("data");
SG_LOG(SG_SYSTEMS, SG_BULK, "datas.size()=" << datas.size());
for (auto data: datas) {
uint32_t length;
m_continuous_indexing_in.read(reinterpret_cast<char*>(&length), sizeof(length));
SG_LOG(SG_SYSTEMS, SG_BULK,
"m_continuous_in.tellg()=" << m_continuous_indexing_in.tellg()
<< " Skipping data_type=" << data->getStringValue()
<< " length=" << length
);
// Move forward <length> bytes.
m_continuous_indexing_in.seekg(length, std::ios_base::cur);
if (!m_continuous_indexing_in) {
// Dont add bogus info to <stats>.
break;
} }
else if (!strcmp(data->getStringValue(), "multiplayer")) { if (length) {
frameinfo.has_multiplayer = true; stats[data->getStringValue()].num_frames += 1;
++m_num_frames_multiplayer; stats[data->getStringValue()].bytes += length;
m_continuous_in_multiplayer = true; if (!strcmp(data->getStringValue(), "signals")) {
} frameinfo.has_signals = true;
else if (!strcmp(data->getStringValue(), "extra-properties")) { }
frameinfo.has_extra_properties = true; else if (!strcmp(data->getStringValue(), "multiplayer")) {
++m_num_frames_extra_properties; frameinfo.has_multiplayer = true;
m_continuous_in_extra_properties = true; ++m_num_frames_multiplayer;
m_continuous_in_multiplayer = true;
}
else if (!strcmp(data->getStringValue(), "extra-properties")) {
frameinfo.has_extra_properties = true;
++m_num_frames_extra_properties;
m_continuous_in_extra_properties = true;
}
} }
} }
} }
@ -2134,7 +2391,8 @@ FGReplay::loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMet
{ {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "loading Preview=" << Preview << " Filename=" << Filename); SG_LOG(SG_SYSTEMS, SG_DEBUG, "loading Preview=" << Preview << " Filename=" << Filename);
/* Try to load as uncompressed Continuous recording first. */ /* Try to load a Continuous recording first. */
replay_error->setBoolValue(false);
std::ifstream in_preview; std::ifstream in_preview;
std::ifstream& in(Preview ? in_preview : m_continuous_in); std::ifstream& in(Preview ? in_preview : m_continuous_in);
in.open(Filename.str()); in.open(Filename.str());
@ -2165,6 +2423,8 @@ FGReplay::loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMet
m_num_frames_multiplayer = 0; m_num_frames_multiplayer = 0;
m_continuous_indexing_in.open(Filename.str()); m_continuous_indexing_in.open(Filename.str());
m_continuous_indexing_pos = in.tellg(); m_continuous_indexing_pos = in.tellg();
m_continuous_in_compression = m_continuous_in_config->getNode("meta/continuous-compression", true /*create*/)->getIntValue();
SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_continuous_in_compression=" << m_continuous_in_compression);
SG_LOG(SG_SYSTEMS, SG_DEBUG, "filerequest=" << filerequest.get()); SG_LOG(SG_SYSTEMS, SG_DEBUG, "filerequest=" << filerequest.get());
// Make an in-memory index of the recording. // Make an in-memory index of the recording.

View file

@ -231,6 +231,7 @@ private:
SGPropertyNode_ptr speed_up; SGPropertyNode_ptr speed_up;
SGPropertyNode_ptr replay_multiplayer; SGPropertyNode_ptr replay_multiplayer;
SGPropertyNode_ptr recovery_period; SGPropertyNode_ptr recovery_period;
SGPropertyNode_ptr replay_error;
SGPropertyNode_ptr m_sim_startup_xpos; SGPropertyNode_ptr m_sim_startup_xpos;
SGPropertyNode_ptr m_sim_startup_ypos; SGPropertyNode_ptr m_sim_startup_ypos;
@ -274,6 +275,8 @@ private:
// For writing uncompressed fgtape file. // For writing uncompressed fgtape file.
SGPropertyNode_ptr m_continuous_out_config; SGPropertyNode_ptr m_continuous_out_config;
std::ofstream m_continuous_out; std::ofstream m_continuous_out;
int m_continuous_out_compression;
int m_continuous_in_compression;
SGPropertyNode_ptr m_simple_time_enabled; SGPropertyNode_ptr m_simple_time_enabled;
}; };