1
0
Fork 0

Refactoring of record/replay code.

The main FGReplay subsystem class now contains just the public API for
record/replay, plus a pointer to instance of new ReplayInternal implementation
class. Also moved some Continuous-specific code into new source file.

src/Aircraft/CMakeLists.txt
    Updated for new files.
src/Aircraft/continuous.cxx
src/Aircraft/continuous.hxx
    New, code specific to Continuous record/replay.
src/Aircraft/flightrecorder.hxx
    Minor changes.
src/Aircraft/replay-internal.cxx
src/Aircraft/replay-internal.hxx
    New, contains implementation of record/replay.
src/Aircraft/replay.cxx
src/Aircraft/replay.hxx
    Modified to be just concerned with public record/replay API. The
    implementation now defers to the ReplayInternal instance. Added support for
    auto create-video when loading Continuous recording.
src/Main/fg_init.cxx
    resetStatisticsProperties() is now in FGReplay, not FGReplayData - the
    latter is not in the replay.hxx header any more.
This commit is contained in:
Julian Smith 2021-12-01 23:38:17 +00:00
parent c1a34ac4ce
commit 6d3254647a
9 changed files with 3375 additions and 2855 deletions

View file

@ -4,18 +4,22 @@ set(SOURCES
controls.cxx
replay.cxx
flightrecorder.cxx
FlightHistory.cxx
initialstate.cxx
FlightHistory.cxx
initialstate.cxx
AircraftPerformance.cxx
replay-internal.cxx
continuous.cxx
)
set(HEADERS
controls.hxx
replay.hxx
flightrecorder.hxx
FlightHistory.hxx
initialstate.hxx
FlightHistory.hxx
initialstate.hxx
AircraftPerformance.hxx
continuous.hxx
replay-internal.hxx
)

865
src/Aircraft/continuous.cxx Normal file
View file

@ -0,0 +1,865 @@
#include "continuous.hxx"
#include <Aircraft/flightrecorder.hxx>
#include <Main/fg_props.hxx>
#include <MultiPlayer/mpmessages.hxx>
#include <Viewer/FGEventHandler.hxx>
#include <Viewer/renderer.hxx>
#include <Viewer/viewmgr.hxx>
#include <simgear/io/iostreams/zlibstream.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/structure/commands.hxx>
#include <osgViewer/ViewerBase>
#include <assert.h>
#include <string.h>
Continuous::Continuous(std::shared_ptr<FGFlightRecorder> flight_recorder)
:
m_flight_recorder(flight_recorder)
{
SGPropertyNode* record_continuous = fgGetNode("/sim/replay/record-continuous", true);
SGPropertyNode* fdm_initialized = fgGetNode("/sim/signals/fdm-initialized", true);
record_continuous->addChangeListener(this, true /*initial*/);
fdm_initialized->addChangeListener(this, true /*initial*/);
}
// Reads binary data from a stream into an instance of a type.
template<typename T>
static void readRaw(std::istream& in, T& data)
{
in.read(reinterpret_cast<char*>(&data), sizeof(data));
}
// Writes instance of a type as binary data to a stream.
template<typename T>
static void writeRaw(std::ostream& out, const T& data)
{
out.write(reinterpret_cast<const char*>(&data), sizeof(data));
}
// Reads uncompressed vector<char> from file. Throws if length field is longer
// than <max_length>.
template<typename SizeType>
static SizeType VectorRead(std::istream& in, std::vector<char>& out, uint32_t max_length=(1<<31))
{
SizeType length;
readRaw(in, length);
if (sizeof(length) + length > max_length)
{
SG_LOG(SG_SYSTEMS, SG_ALERT, "recording data vector too long."
<< " max_length=" << max_length
<< " sizeof(length)=" << sizeof(length)
<< " length=" << length
);
throw std::runtime_error("Failed to read vector in recording");
}
out.resize(length);
in.read(&out.front(), length);
return sizeof(length) + length;
}
static int16_t read_int16(std::istream& in, size_t& pos)
{
int16_t a;
readRaw(in, a);
pos += sizeof(a);
return a;
}
static std::string read_string(std::istream& in, size_t& pos)
{
int16_t length = read_int16(in, pos);
std::vector<char> path(length);
in.read(&path[0], length);
pos += length;
std::string ret(&path[0], length);
return ret;
}
static int PropertiesWrite(SGPropertyNode* root, std::ostream& out)
{
stringstream buffer;
writeProperties(buffer, root, true /*write_all*/);
uint32_t buffer_len = buffer.str().size() + 1;
writeRaw(out, buffer_len);
out.write(buffer.str().c_str(), buffer_len);
return 0;
}
// Reads extra-property change items in next <length> bytes. Throws if we don't
// exactly read <length> bytes.
static void ReadFGReplayDataExtraProperties(std::istream& in, FGReplayData* replay_data, uint32_t length)
{
SG_LOG(SG_SYSTEMS, SG_BULK, "reading extra-properties. length=" << length);
size_t pos=0;
for(;;)
{
if (pos == length)
{
break;
}
if (pos > length)
{
SG_LOG(SG_SYSTEMS, SG_ALERT, "Overrun while reading extra-properties:"
" length=" << length << ": pos=" << pos);
in.setstate(std::ios_base::failbit);
break;
}
SG_LOG(SG_SYSTEMS, SG_BULK, "length=" << length<< " pos=" << pos);
std::string path = read_string(in, pos);
if (path == "")
{
path = read_string(in, pos);
SG_LOG(SG_SYSTEMS, SG_DEBUG, "property deleted: " << path);
replay_data->replay_extra_property_removals.push_back(path);
}
else
{
std::string value = read_string(in, pos);
SG_LOG(SG_SYSTEMS, SG_DEBUG, "property changed: " << path << "=" << value);
replay_data->replay_extra_property_changes[path] = value;
}
}
}
static bool ReadFGReplayData2(
std::istream& in,
SGPropertyNode* config,
bool load_signals,
bool load_multiplayer,
bool load_extra_properties,
FGReplayData* ret
)
{
ret->raw_data.resize(0);
for (auto data: config->getChildren("data"))
{
const char* data_type = data->getStringValue();
SG_LOG(SG_SYSTEMS, SG_BULK, "in.tellg()=" << in.tellg() << " data_type=" << data_type);
uint32_t length;
readRaw(in, length);
SG_LOG(SG_SYSTEMS, SG_DEBUG, "length=" << length);
if (!in) break;
if (load_signals && !strcmp(data_type, "signals"))
{
ret->raw_data.resize(length);
in.read(&ret->raw_data.front(), ret->raw_data.size());
}
else if (load_multiplayer && !strcmp(data_type, "multiplayer"))
{
/* Multiplayer information is a vector of vectors. */
ret->multiplayer_messages.clear();
uint32_t pos = 0;
for(;;)
{
assert(pos <= length);
if (pos == length) break;
std::shared_ptr<std::vector<char>> v(new std::vector<char>);
ret->multiplayer_messages.push_back(v);
pos += VectorRead<uint16_t>(in, *ret->multiplayer_messages.back(), length - pos);
SG_LOG(SG_SYSTEMS, SG_BULK, "replaying multiplayer data"
<< " ret->sim_time=" << ret->sim_time
<< " length=" << length
<< " pos=" << pos
<< " callsign=" << ((T_MsgHdr*) &v->front())->Callsign
);
}
}
else if (load_extra_properties && !strcmp(data_type, "extra-properties"))
{
ReadFGReplayDataExtraProperties(in, ret, length);
}
else
{
SG_LOG(SG_GENERAL, SG_BULK, "Skipping unrecognised/unwanted data: " << data_type);
in.seekg(length, std::ios_base::cur);
}
if (!in) break;
}
if (!in)
{
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read fgtape data");
return false;
}
return true;
}
std::unique_ptr<FGReplayData> ReadFGReplayData(
std::ifstream& in,
size_t pos,
SGPropertyNode* config,
bool load_signals,
bool load_multiplayer,
bool load_extra_properties,
int 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);
readRaw(in, ret->sim_time);
if (!in)
{
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read fgtape frame at offset " << pos);
return nullptr;
}
bool ok;
if (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;
}
// 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;
};
static void writeFrame2(FGReplayData* r, std::ostream& out, SGPropertyNode_ptr config)
{
for (auto data: config->getChildren("data"))
{
const char* data_type = data->getStringValue();
if (!strcmp(data_type, "signals"))
{
uint32_t signals_size = r->raw_data.size();
writeRaw(out, signals_size);
out.write(&r->raw_data.front(), r->raw_data.size());
}
else if (!strcmp(data_type, "multiplayer"))
{
uint32_t length = 0;
for (auto message: r->multiplayer_messages)
{
length += sizeof(uint16_t) + message->size();
}
SG_LOG(SG_SYSTEMS, SG_DEBUG, "data_type=" << data_type << " out.tellp()=" << out.tellp()
<< " length=" << length);
writeRaw(out, length);
for (auto message: r->multiplayer_messages)
{
uint16_t message_size = message->size();
writeRaw(out, message_size);
out.write(&message->front(), message_size);
}
}
else if (!strcmp(data_type, "extra-properties"))
{
uint32_t length = r->extra_properties.size();
SG_LOG(SG_SYSTEMS, SG_DEBUG, "data_type=" << data_type << " out.tellp()=" << out.tellp()
<< " length=" << length);
writeRaw(out, length);
out.write(&r->extra_properties[0], length);
}
else
{
SG_LOG(SG_SYSTEMS, SG_ALERT, "unrecognised data_type=" << data_type);
assert(0);
}
}
}
bool continuousWriteFrame(Continuous& continuous, 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;
}
writeRaw(out, r->sim_time);
if (continuous.m_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;
if (!out) ok = false;
return ok;
}
SGPropertyNode_ptr continuousWriteHeader(
Continuous& continuous,
FGFlightRecorder* flight_recorder,
std::ofstream& out,
const SGPath& path,
FGTapeType tape_type
)
{
continuous.m_out_compression = fgGetInt("/sim/replay/record-continuous-compression");
SGPropertyNode_ptr config = saveSetup(NULL /*Extra*/, path, 0 /*Duration*/,
tape_type, continuous.m_out_compression);
SGPropertyNode* signals = config->getNode("signals", true /*create*/);
flight_recorder->getConfig(signals);
out.open(path.c_str(), std::ofstream::binary | std::ofstream::trunc);
out.write(FlightRecorderFileMagic, strlen(FlightRecorderFileMagic)+1);
PropertiesWrite(config, out);
if (tape_type == FGTapeType_CONTINUOUS)
{
// Ensure that all recorded properties are written in first frame.
//
flight_recorder->resetExtraProperties();
}
if (!out)
{
out.close();
config = nullptr;
}
return config;
}
bool replayContinuousInternal(
Continuous& continuous,
FGFlightRecorder* recorder,
double time,
size_t offset,
size_t offset_old,
bool replay_signals,
bool replay_multiplayer,
bool replay_extra_properties,
int* xpos,
int* ypos,
int* xsize,
int* ysize
)
{
std::unique_ptr<FGReplayData> replay_data = ReadFGReplayData(
continuous.m_in,
offset,
continuous.m_in_config,
replay_signals,
replay_multiplayer,
replay_extra_properties,
continuous.m_in_compression
);
if (!replay_data)
{
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to read fgtape frame at offset=" << offset << " time=" << time);
return false;
}
assert(replay_data.get());
std::unique_ptr<FGReplayData> replay_data_old;
if (offset_old)
{
replay_data_old = ReadFGReplayData(
continuous.m_in,
offset_old,
continuous.m_in_config,
replay_signals,
replay_multiplayer,
replay_extra_properties,
continuous.m_in_compression
);
}
if (replay_extra_properties) SG_LOG(SG_SYSTEMS, SG_BULK,
"replay():"
<< " time=" << time
<< " offset=" << offset
<< " offset_old=" << offset_old
<< " replay_data->raw_data.size()=" << replay_data->raw_data.size()
<< " replay_data->multiplayer_messages.size()=" << replay_data->multiplayer_messages.size()
<< " replay_data->extra_properties.size()=" << replay_data->extra_properties.size()
<< " replay_data->replay_extra_property_changes.size()=" << replay_data->replay_extra_property_changes.size()
);
recorder->replay(time, replay_data.get(), replay_data_old.get(), xpos, ypos, xsize, ysize);
return true;
}
// fixme: this is duplicated in replay.cxx.
static void popupTip(const char* message, int delay)
{
SGPropertyNode_ptr args(new SGPropertyNode);
args->setStringValue("label", message);
args->setIntValue("delay", delay);
globals->get_commands()->execute("show-message", args);
}
bool replayContinuous(FGReplayInternal& self, double time)
{
// We need to detect whether replay() updates the values for the main
// window's position and size.
int xpos0 = self.m_sim_startup_xpos->getIntValue();
int ypos0 = self.m_sim_startup_xpos->getIntValue();
int xsize0 = self.m_sim_startup_xpos->getIntValue();
int ysize0 = self.m_sim_startup_xpos->getIntValue();
int xpos = xpos0;
int ypos = ypos0;
int xsize = xsize0;
int ysize = ysize0;
double multiplayer_recent = 3;
// We replay all frames from just after the previously-replayed frame,
// in order to replay extra properties and multiplayer aircraft
// correctly.
//
double t_begin = self.m_continuous->m_in_frame_time_last;
if (time < self.m_continuous->m_in_time_last)
{
// We have gone backwards, e.g. user has clicked on the back
// buttons in the Replay dialogue.
//
if (self.m_continuous->m_in_multiplayer)
{
// Continuous recording has multiplayer data, so replay recent
// ones.
//
t_begin = time - multiplayer_recent;
}
if (self.m_continuous->m_in_extra_properties)
{
// Continuous recording has property changes. we need to replay
// all property changes from the beginning.
//
t_begin = -1;
}
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Have gone backwards."
<< " m_in_time_last=" << self.m_continuous->m_in_time_last
<< " time=" << time
<< " t_begin=" << t_begin
<< " m_in_extra_properties=" << self.m_continuous->m_in_extra_properties
);
}
// Prepare to replay signals from Continuoue recording file. We want
// to find a pair of frames that straddle the requested <time> so that
// we can interpolate.
//
auto p = self.m_continuous->m_in_time_to_frameinfo.lower_bound(time);
bool ret = false;
size_t offset;
size_t offset_prev = 0;
if (p == self.m_continuous->m_in_time_to_frameinfo.end())
{
// We are at end of recording; replay last frame.
--p;
offset = p->second.offset;
ret = true;
}
else if (p->first > time)
{
// Look for preceding item.
if (p == self.m_continuous->m_in_time_to_frameinfo.begin())
{
// <time> is before beginning of recording.
offset = p->second.offset;
}
else
{
// Interpolate between pair of items that straddle <time>.
auto prev = p;
--prev;
offset_prev = prev->second.offset;
offset = p->second.offset;
}
}
else
{
// Exact match.
offset = p->second.offset;
}
// Before interpolating signals, we replay all property changes from
// all frame times t satisfying t_prop_begin < t < time. We also replay
// all recent multiplayer packets in this range, i.e. for which t >
// time - multiplayer_recent.
//
// todo: figure out how to interpolate view position/direction, to
// smooth things out if replay fps is different from record fps e.g.
// with new fixed dt support.
//
for (auto p_before = self.m_continuous->m_in_time_to_frameinfo.upper_bound(t_begin);
p_before != self.m_continuous->m_in_time_to_frameinfo.end();
++p_before)
{
if (p_before->first >= p->first)
{
break;
}
// Replaying a frame is expensive because we read frame data
// from disc each time. So we only replay this frame if it has
// extra_properties, or if it has multiplayer packets and we are
// within <multiplayer_recent> seconds of current time.
//
bool replay_this_frame = p_before->second.has_extra_properties;
if (p_before->second.has_multiplayer && p_before->first > time - multiplayer_recent)
{
replay_this_frame = true;
}
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Looking at extra property changes."
<< " replay_this_frame=" << replay_this_frame
<< " m_continuous->m_in_time_last=" << self.m_continuous->m_in_time_last
<< " m_continuous->m_in_frame_time_last=" << self.m_continuous->m_in_frame_time_last
<< " time=" << time
<< " t_begin=" << t_begin
<< " p_before->first=" << p_before->first
<< " p_before->second=" << p_before->second
);
if (replay_this_frame)
{
bool ok = replayContinuousInternal(
*self.m_continuous,
self.m_flight_recorder.get(),
p_before->first,
p_before->second.offset,
0 /*offset_old*/,
false /*replay_signals*/,
p_before->first > time - multiplayer_recent /*replay_multiplayer*/,
true /*replay_extra_properties*/,
&xpos,
&ypos,
&xsize,
&ysize
);
if (!ok)
{
if (!self.m_replay_error->getBoolValue())
{
popupTip("Replay failed: cannot read fgtape data", 10);
self.m_replay_error->setBoolValue(true);
}
return true;
}
}
}
/* Now replay signals, interpolating between frames atoffset_prev and
offset. */
bool ok = replayContinuousInternal(
*self.m_continuous,
self.m_flight_recorder.get(),
time,
offset,
offset_prev /*offset_old*/,
true /*replay_signals*/,
true /*replay_multiplayer*/,
true /*replay_extra_properties*/,
&xpos,
&ypos,
&xsize,
&ysize
);
if (!ok)
{
if (!self.m_replay_error->getBoolValue())
{
popupTip("Replay failed: cannot read fgtape data", 10);
self.m_replay_error->setBoolValue(true);
}
return true;
}
if (0
|| xpos != xpos0
|| ypos != ypos0
|| xsize != xsize0
|| ysize != ysize0
)
{
// Move/resize the main window to reflect the updated values.
globals->get_props()->setIntValue("/sim/startup/xpos", xpos);
globals->get_props()->setIntValue("/sim/startup/ypos", ypos);
globals->get_props()->setIntValue("/sim/startup/xsize", xsize);
globals->get_props()->setIntValue("/sim/startup/ysize", ysize);
osgViewer::ViewerBase* viewer_base = globals->get_renderer()->getViewerBase();
if (viewer_base)
{
std::vector<osgViewer::GraphicsWindow*> windows;
viewer_base->getWindows(windows);
osgViewer::GraphicsWindow* window = windows[0];
// We use FGEventHandler::setWindowRectangle() to move the
// window, because it knows how to convert from window work-area
// coordinates to window-including-furniture coordinates.
//
flightgear::FGEventHandler* event_handler = globals->get_renderer()->getEventHandler();
event_handler->setWindowRectangleInteriorWithCorrection(window, xpos, ypos, xsize, ysize);
}
}
self.m_continuous->m_in_time_last = time;
self.m_continuous->m_in_frame_time_last = p->first;
return ret;
}
/* SGPropertyChangeListener callback for detecing when FDM is initialised and
for when continuous recording is started or stopped. */
void Continuous::valueChanged(SGPropertyNode * node)
{
bool prop_continuous = fgGetBool("/sim/replay/record-continuous");
bool prop_fdm = fgGetBool("/sim/signals/fdm-initialized");
bool continuous = prop_continuous && prop_fdm;
if (continuous == (m_out.is_open() ? true : false))
{
// No change.
return;
}
if (m_out.is_open())
{
// Stop existing continuous recording.
SG_LOG(SG_SYSTEMS, SG_ALERT, "Stopping continuous recording");
m_out.close();
popupTip("Continuous record to file stopped", 5 /*delay*/);
}
if (continuous)
{
// Start continuous recording.
SGPath path_timeless;
SGPath path = makeSavePath(FGTapeType_CONTINUOUS, &path_timeless);
m_out_config = continuousWriteHeader(
*this,
m_flight_recorder.get(),
m_out,
path,
FGTapeType_CONTINUOUS
);
if (!m_out_config)
{
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to start continuous recording");
popupTip("Continuous record to file failed to start", 5 /*delay*/);
return;
}
SG_LOG(SG_SYSTEMS, SG_ALERT, "Starting continuous recording");
/* Make a convenience link to the recording. E.g.
harrier-gr3-continuous.fgtape -> harrier-gr3-20201224-005034-continuous.fgtape.
Link destination is in same directory as link so we use leafname
path.file(). */
path_timeless.remove();
bool ok = path_timeless.makeLink(path.file());
if (!ok)
{
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);
if (m_out_compression)
{
popupTip("Continuous+compressed record to file started", 5 /*delay*/);
}
else
{
popupTip("Continuous record to file started", 5 /*delay*/);
}
}
}

123
src/Aircraft/continuous.hxx Normal file
View file

@ -0,0 +1,123 @@
#pragma once
#include "replay-internal.hxx"
#include <simgear/props/props.hxx>
#include <fstream>
#include <mutex>
#include <thread>
struct Continuous : SGPropertyChangeListener
{
Continuous(std::shared_ptr<FGFlightRecorder> flight_recorder);
/* Callback for SGPropertyChangeListener. */
void valueChanged(SGPropertyNode * node) override;
std::shared_ptr<FGFlightRecorder> m_flight_recorder;
std::ifstream m_in;
bool m_in_multiplayer = false;
bool m_in_extra_properties = false;
std::mutex m_in_time_to_frameinfo_lock;
std::map<double, FGFrameInfo> m_in_time_to_frameinfo;
SGPropertyNode_ptr m_in_config;
double m_in_time_last = 0;
double m_in_frame_time_last = 0;
std::ifstream m_indexing_in;
std::streampos m_indexing_pos;
// Only used for gathering statistics that are then written into
// properties.
//
int m_num_frames_extra_properties = 0;
int m_num_frames_multiplayer = 0;
// For writing Continuous fgtape file.
SGPropertyNode_ptr m_out_config;
std::ofstream m_out;
int m_out_compression = 0;
int m_in_compression = 0;
};
// Attempts to load Continuous recording header properties into
// <properties>. If in is null we use internal std::fstream, otherwise we
// use *in.
//
// Returns 0 on success, +1 if we may succeed after further download, or -1
// if recording is not a Continuous recording.
//
int loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties);
// Reads all or part of a FGReplayData from Continuous file.
std::unique_ptr<FGReplayData> ReadFGReplayData(
std::ifstream& in,
size_t pos,
SGPropertyNode* config,
bool load_signals,
bool load_multiplayer,
bool load_extra_properties,
int in_compression
);
// Writes one frame of continuous record information.
//
bool continuousWriteFrame(
Continuous& continuous,
FGReplayData* r,
std::ostream& out,
SGPropertyNode_ptr config
);
// Opens continuous recording file and writes header.
//
// If MetaData is unset, we initialise it by calling saveSetup(). Otherwise
// should be already set up.
//
// If Config is unset, we make it point to a new node populated by
// m_pRecorder->getConfig(). Otherwise it should be already set up to point to
// such information.
//
// If path_override is not "", we use it as the path (instead of the path
// determined by saveSetup().
//
SGPropertyNode_ptr continuousWriteHeader(
Continuous& continuous,
FGFlightRecorder* m_pRecorder,
std::ofstream& out,
const SGPath& path,
FGTapeType tape_type
);
// Replays one frame from Continuous recording. <offset> and <offset_old>
// are offsets in file of frames that are >= and < <time> respectively.
// <offset_old> may be 0, in which case it is ignored.
//
// We load the frame(s) from disc, omitting some data depending on
// replay_signals, replay_multiplayer and replay_extra_properties. Then call
// m_pRecorder->replay(), which updates the global state.
//
// Returns true on success, otherwise we failed to read from Continuous
// recording.
//
bool replayContinuousInternal(
Continuous& continuous,
FGFlightRecorder* recorder,
double time,
size_t offset,
size_t offset_old,
bool replay_signals,
bool replay_multiplayer,
bool replay_extra_properties,
int* xpos,
int* ypos,
int* xsize,
int* ysize
);
bool replayContinuous(FGReplayInternal& self, double time);

View file

@ -25,7 +25,7 @@
#include <simgear/props/props.hxx>
#include <MultiPlayer/multiplaymgr.hxx>
#include "replay.hxx"
#include "replay-internal.hxx"
namespace FlightRecorder
{

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,245 @@
// replay.hxx - a system to record and replay FlightGear flights
//
// Written by Curtis Olson, started July 2003.
//
// Copyright (C) 2003 Curtis L. Olson - http://www.flightgear.org/~curt
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// $Id$
#pragma once
#include <mutex>
#include <simgear/compiler.h>
#include <simgear/math/sg_types.hxx>
#include <simgear/props/props.hxx>
#include <simgear/structure/subsystem_mgr.hxx>
#include <simgear/io/iostreams/gzcontainerfile.hxx>
#include <simgear/io/HTTPFileRequest.hxx>
#include <MultiPlayer/multiplaymgr.hxx>
#include <deque>
#include <vector>
class FGFlightRecorder;
/** Magic string to verify valid FG flight recorder tapes. */
extern const char* const FlightRecorderFileMagic;
/* Data for a single frame. */
struct FGReplayData
{
double sim_time;
// Our aircraft state.
std::vector<char> raw_data;
// Incoming multiplayer messages, if any.
std::vector<std::shared_ptr<std::vector<char>>> multiplayer_messages;
// Serialised information about extra property changes, only used when
// making a Continuous recording - we write this raw data into frame data
// in the Continuous recording file.
std::vector<char> extra_properties;
// Information about extra property changes, only used when replaying. We
// populate these when loading a frame.
std::map<std::string, std::string> replay_extra_property_changes;
std::vector<std::string> replay_extra_property_removals;
// Updates static statistics defined below.
void UpdateStats();
// Resets out static property nodes; to be called by fgStartNewReset().
static void resetStatisticsProperties();
FGReplayData();
~FGReplayData();
size_t m_bytes_raw_data = 0;
size_t m_bytes_multiplayer_messages = 0;
size_t m_num_multiplayer_messages = 0;
// Statistics about replay data, also properties /sim/replay/datastats_*.
static size_t s_num;
static size_t s_bytes_raw_data;
static size_t s_bytes_multiplayer_messages;
static size_t s_num_multiplayer_messages;
static SGPropertyNode_ptr s_prop_num;
static SGPropertyNode_ptr s_prop_bytes_raw_data;
static SGPropertyNode_ptr s_prop_bytes_multiplayer_messages;
static SGPropertyNode_ptr s_prop_num_multiplayer_messages;
};
typedef struct
{
double sim_time;
std::string message;
std::string speaker;
} FGReplayMessages;
enum FGTapeType
{
FGTapeType_NORMAL,
FGTapeType_CONTINUOUS,
FGTapeType_RECOVERY,
};
/* Index entry when replaying Continuous recording. */
struct FGFrameInfo
{
size_t offset;
bool has_signals = false;
bool has_multiplayer = false;
bool has_extra_properties = false;
};
std::ostream& operator << (std::ostream& out, const FGFrameInfo& frame_info);
struct FGReplayInternal
{
FGReplayInternal();
virtual ~FGReplayInternal();
/* Methods that implement the FGReplay API. */
void bind();
void init();
void reinit() ;
void unbind();
void update(double dt);
static const char* staticSubsystemClassId() { return "replay"; }
bool start(bool NewTape=false);
bool saveTape(const SGPropertyNode* ConfigData);
bool loadTape(const SGPropertyNode* ConfigData);
static int loadContinuousHeader(
const std::string& path,
std::istream* in,
SGPropertyNode* properties
);
bool loadTape(
const SGPath& filename,
bool preview,
SGPropertyNode& meta_meta,
simgear::HTTP::FileRequestRef file_request=nullptr
);
static std::string makeTapePath(const std::string& tape_name);
/* Callback for SGPropertyChangeListener. */
//void valueChanged(SGPropertyNode * node) override;
/* Internal state. */
double m_sim_time;
double m_last_mt_time;
double m_last_lt_time;
double m_last_msg_time;
int m_last_replay_state;
bool m_was_finished_already;
std::deque<FGReplayData*> m_short_term;
std::deque<FGReplayData*> m_medium_term;
std::deque<FGReplayData*> m_long_term;
std::deque<FGReplayData*> m_recycler;
std::vector<FGReplayMessages> m_replay_messages;
std::vector<FGReplayMessages>::iterator m_current_msg;
SGPropertyNode_ptr m_disable_replay;
SGPropertyNode_ptr m_replay_master;
SGPropertyNode_ptr m_replay_master_eof;
SGPropertyNode_ptr m_replay_time;
SGPropertyNode_ptr m_replay_time_str;
SGPropertyNode_ptr m_replay_looped;
SGPropertyNode_ptr m_replay_duration_act;
SGPropertyNode_ptr m_speed_up;
SGPropertyNode_ptr m_replay_multiplayer;
SGPropertyNode_ptr m_recovery_period;
SGPropertyNode_ptr m_replay_error;
SGPropertyNode_ptr m_record_normal_begin; // Time of first in-memory recorded frame.
SGPropertyNode_ptr m_record_normal_end;
SGPropertyNode_ptr m_log_frame_times;
SGPropertyNode_ptr m_sim_startup_xpos;
SGPropertyNode_ptr m_sim_startup_ypos;
SGPropertyNode_ptr m_sim_startup_xsize;
SGPropertyNode_ptr m_sim_startup_ysize;
SGPropertyNode_ptr m_simple_time_enabled;
double m_replay_time_prev; // Used to detect jumps while replaying.
/* short term sample rate is as every frame. */
double m_high_res_time; // default: 60 secs of high res data
double m_medium_res_time; // default: 10 mins of 1 fps data
double m_low_res_time; // default: 1 hr of 10 spf data
double m_medium_sample_rate; // medium term sample rate (sec)
double m_long_sample_rate; // long term sample rate (sec)
std::shared_ptr<FGFlightRecorder> m_flight_recorder;
/* Things for Continuous recording/replay support. */
std::unique_ptr<struct Continuous> m_continuous;
FGMultiplayMgr* m_MultiplayMgr;
};
/* Sets things up for writing to a normal or continuous fgtape file.
extra:
NULL or extra information when we are called from fgdata gui, e.g. with
the flight description entered by the user in the save dialogue.
path:
Path of fgtape file. We return nullptr if this file already exists.
duration:
Duration of recording. Zero if we are starting a continuous recording.
tape_type:
.
continuous_compression:
Whether to use compression if tape_type is FGTapeType_CONTINUOUS.
Returns:
A new SGPropertyNode suitable as prefix of recording. If
extra:user-data exists, it will appear as meta/user-data.
*/
SGPropertyNode_ptr saveSetup(
const SGPropertyNode* extra,
const SGPath& path,
double duration,
FGTapeType tape_type,
int continuous_compression=0
);
/* Returns a path using different formats depending on <type>:
FGTapeType_NORMAL: <tape-directory>/<aircraft-type>-<date>-<time>.fgtape
FGTapeType_CONTINUOUS: <tape-directory>/<aircraft-type>-<date>-<time>-continuous.fgtape
FGTapeType_RECOVERY: <tape-directory>/<aircraft-type>-recovery.fgtape
*/
SGPath makeSavePath(FGTapeType type, SGPath* path_timeless=nullptr);

File diff suppressed because it is too large Load diff

View file

@ -20,268 +20,81 @@
//
// $Id$
#pragma once
#ifndef _FG_REPLAY_HXX
#define _FG_REPLAY_HXX 1
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <mutex>
#include <simgear/compiler.h>
#include <simgear/math/sg_types.hxx>
#include <simgear/props/props.hxx>
#include <simgear/structure/subsystem_mgr.hxx>
#include <simgear/io/iostreams/gzcontainerfile.hxx>
#include <simgear/io/HTTPFileRequest.hxx>
#include <MultiPlayer/multiplaymgr.hxx>
#include <deque>
#include <vector>
class FGFlightRecorder;
struct FGReplayData {
double sim_time;
// Our aircraft state.
std::vector<char> raw_data;
// Incoming multiplayer messages.
std::vector<std::shared_ptr<std::vector<char>>> multiplayer_messages;
std::vector<char> extra_properties;
std::map<std::string, std::string> replay_extra_property_changes;
std::vector<std::string> replay_extra_property_removals;
// Updates static statistics defined below.
void UpdateStats();
// Resets out static property nodes; to be called by fgStartNewReset().
static void resetStatisticsProperties();
FGReplayData();
~FGReplayData();
size_t m_bytes_raw_data = 0;
size_t m_bytes_multiplayer_messages = 0;
size_t m_num_multiplayer_messages = 0;
// Statistics about replay data, also properties /sim/replay/datastats_*.
static size_t s_num;
static size_t s_bytes_raw_data;
static size_t s_bytes_multiplayer_messages;
static size_t s_num_multiplayer_messages;
static SGPropertyNode_ptr s_prop_num;
static SGPropertyNode_ptr s_prop_bytes_raw_data;
static SGPropertyNode_ptr s_prop_bytes_multiplayer_messages;
static SGPropertyNode_ptr s_prop_num_multiplayer_messages;
};
typedef struct {
double sim_time;
std::string message;
std::string speaker;
} FGReplayMessages;
enum FGTapeType
/* A recording/replay module for FlightGear flights. */
struct FGReplay : SGSubsystem
{
FGTapeType_NORMAL,
FGTapeType_CONTINUOUS,
FGTapeType_RECOVERY,
};
struct FGFrameInfo;
typedef std::deque < FGReplayData *> replay_list_type;
typedef std::vector < FGReplayMessages > replay_messages_type;
/**
* A recording/replay module for FlightGear flights
*
*/
class FGReplay : public SGSubsystem, SGPropertyChangeListener
{
public:
FGReplay ();
virtual ~FGReplay();
// Subsystem API.
/* Subsystem API. */
void bind() override;
void init() override;
void reinit() override;
void unbind() override;
void update(double dt) override;
// Subsystem identification.
/* Subsystem identification. */
static const char* staticSubsystemClassId() { return "replay"; }
bool start(bool NewTape=false);
/* For built-in 'replay' command - replay using in-memory Normal recording.
new_tape: If true, we start at beginning of tape, otherwise we start at
loop interval.
*/
bool start(bool new_tape=false);
/* For save and load tape operations from Flightgear GUI. */
bool saveTape(const SGPropertyNode* ConfigData);
bool loadTape(const SGPropertyNode* ConfigData);
// If filerequest is set, the local file is a Continuous recording and
// it might increase in size as downloading progresses, so we need to
// incrementally index the file until the file request has finished the
// download.
//
/* Attempts to load Continuous recording header properties into
<properties>. If in is null we use internal std::fstream, otherwise we use
*in.
Returns 0 on success, +1 if we may succeed after further download, or -1 if
recording is not a Continuous recording.
For command line --load-tape=...
*/
static int loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties);
/* Start replaying a flight recorder tape from disk.
filename
Path of recording.
preview
If true we read the header (and return it in <meta_meta> but do not
start replaying.
meta_meta
Filled in with contents of recording header's "meta" tree.
filerequest
If not null we use this to get called back as download of file
progresses, so that we can index the recording. Only useful for
Continuous recordings.
*/
bool loadTape(
const SGPath& Filename,
bool Preview,
SGPropertyNode& MetaMeta,
simgear::HTTP::FileRequestRef filerequest=nullptr
const SGPath& filename,
bool preview,
SGPropertyNode& meta_meta,
simgear::HTTP::FileRequestRef file_request=nullptr
);
// Attempts to load Continuous recording header properties into
// <properties>. If in is null we use internal std::fstream, otherwise we
// use *in.
//
// Returns 0 on success, +1 if we may succeed after further download, or -1
// if recording is not a Continuous recording.
//
static int loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties);
/* Prepends /sim/replay/tape-directory and/or appends .fgtape etc.
// Prepends /sim/replay/tape-directory and/or appends .fgtape etc.
//
For command line --load-tape=... */
static std::string makeTapePath(const std::string& tape_name);
private:
void clear();
FGReplayData* record(double time);
void interpolate(double time, const replay_list_type &list);
void replay(
double time,
size_t offset,
size_t offset_old,
bool replay_signals,
bool replay_multiplayer,
bool replay_extra_properties,
int* xpos=nullptr,
int* ypos=nullptr,
int* xsize=nullptr,
int* ysize=nullptr
);
void replay(
double time,
FGReplayData* pCurrentFrame,
FGReplayData* pOldFrame=nullptr,
int* xpos=nullptr,
int* ypos=nullptr,
int* xsize=nullptr,
int* ysize=nullptr
);
void guiMessage(const char* message);
void loadMessages();
void fillRecycler();
bool replay( double time );
void replayMessage( double time );
double get_start_time();
double get_end_time();
bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory);
bool saveTape(const SGPath& Filename, SGPropertyNode_ptr MetaData);
/* Resets out static property nodes; to be called by fgStartNewReset(). */
static void resetStatisticsProperties();
// Build up in-memory cache of simulator time to file offset, so we can
// handle random access.
//
// We also cache any frames that modify extra-properties.
//
// Can be called multiple times, e.g. if recording is being downlaoded.
//
void indexContinuousRecording(const void* data, size_t numbytes);
// Callback for use with simgear::HTTP::FileRequest::setCallback().
//
static void call_indexContinuousRecording(void* ref, const void* data, size_t numbytes);
SGPropertyNode_ptr continuousWriteHeader(
std::ofstream& out,
const SGPath& path,
FGTapeType tape_type
);
bool continuousWriteFrame(FGReplayData* r, std::ostream& out, SGPropertyNode_ptr meta);
double sim_time;
double last_mt_time;
double last_lt_time;
double last_msg_time;
replay_messages_type::iterator current_msg;
int last_replay_state;
bool was_finished_already;
replay_list_type short_term;
replay_list_type medium_term;
replay_list_type long_term;
replay_list_type recycler;
replay_messages_type replay_messages;
SGPropertyNode_ptr disable_replay;
SGPropertyNode_ptr replay_master;
SGPropertyNode_ptr replay_master_eof;
SGPropertyNode_ptr replay_time;
SGPropertyNode_ptr replay_time_str;
SGPropertyNode_ptr replay_looped;
SGPropertyNode_ptr replay_duration_act;
SGPropertyNode_ptr speed_up;
SGPropertyNode_ptr replay_multiplayer;
SGPropertyNode_ptr recovery_period;
SGPropertyNode_ptr replay_error;
SGPropertyNode_ptr m_record_normal_begin; // Time of first in-memory recorded frame.
SGPropertyNode_ptr m_record_normal_end;
SGPropertyNode_ptr log_frame_times;
SGPropertyNode_ptr m_sim_startup_xpos;
SGPropertyNode_ptr m_sim_startup_ypos;
SGPropertyNode_ptr m_sim_startup_xsize;
SGPropertyNode_ptr m_sim_startup_ysize;
double replay_time_prev; // Used to detect jumps while replaying.
double m_high_res_time; // default: 60 secs of high res data
double m_medium_res_time; // default: 10 mins of 1 fps data
double m_low_res_time; // default: 1 hr of 10 spf data
// short term sample rate is as every frame
double m_medium_sample_rate; // medium term sample rate (sec)
double m_long_sample_rate; // long term sample rate (sec)
FGFlightRecorder* m_pRecorder;
FGMultiplayMgr* m_MultiplayMgr;
void valueChanged(SGPropertyNode * node);
// Things for replaying from uncompressed fgtape file.
std::ifstream m_continuous_in;
bool m_continuous_in_multiplayer;
bool m_continuous_in_extra_properties;
std::mutex m_continuous_in_time_to_frameinfo_lock;
std::map<double, FGFrameInfo> m_continuous_in_time_to_frameinfo;
SGPropertyNode_ptr m_continuous_in_config;
double m_continuous_in_time_last;
double m_continuous_in_frame_time_last;
std::ifstream m_continuous_indexing_in;
std::streampos m_continuous_indexing_pos;
// Only used for gathering statistics that are then written into
// properties.
//
int m_num_frames_extra_properties = 0;
int m_num_frames_multiplayer = 0;
// For writing uncompressed fgtape file.
SGPropertyNode_ptr m_continuous_out_config;
std::ofstream m_continuous_out;
int m_continuous_out_compression;
int m_continuous_in_compression;
SGPropertyNode_ptr m_simple_time_enabled;
std::unique_ptr<struct FGReplayInternal> m_internal;
};
#endif // _FG_REPLAY_HXX

View file

@ -1349,7 +1349,7 @@ void fgStartNewReset()
globals->set_matlib(NULL);
flightgear::unregisterMainLoopProperties();
FGReplayData::resetStatisticsProperties();
FGReplay::resetStatisticsProperties();
simgear::clearSharedTreeGeometry();
simgear::clearEffectCache();