1
0
Fork 0

src/Aircraft/: interpolate view properties when replaying.

This might avoid some uneven changes to viewing angle when replay frame rate
differs from recording frame rate.
This commit is contained in:
Julian Smith 2021-12-06 00:23:49 +00:00
parent 1ffc9fb6bf
commit dd7cbf56cc
4 changed files with 278 additions and 94 deletions

View file

@ -187,7 +187,37 @@ static bool ReadFGReplayData2(
return true;
}
std::unique_ptr<FGReplayData> ReadFGReplayData(
/* Removes items more than <n> away from <it>. <n> can be -ve. */
template<typename Container, typename Iterator>
static void remove_far_away(Container& container, Iterator it, int n)
{
SG_LOG(SG_GENERAL, SG_DEBUG, "container.size()=" << container.size());
if (n > 0)
{
for (int i=0; i<n; ++i)
{
if (it == container.end()) return;
++it;
}
container.erase(it, container.end());
}
else
{
for (int i=0; i<-n-1; ++i)
{
if (it == container.begin()) return;
--it;
}
container.erase(container.begin(), it);
}
SG_LOG(SG_GENERAL, SG_DEBUG, "container.size()=" << container.size());
}
/* Returns FGReplayData for frame at specified position in file. Uses
continuous.m_in_pos_to_frame as a cache, and trims this cache using
remove_far_away(). */
static std::shared_ptr<FGReplayData> ReadFGReplayData(
Continuous& continuous,
std::ifstream& in,
size_t pos,
SGPropertyNode* config,
@ -197,40 +227,83 @@ std::unique_ptr<FGReplayData> ReadFGReplayData(
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::shared_ptr<FGReplayData> ret;
auto it = continuous.m_in_pos_to_frame.find(pos);
std::unique_ptr<FGReplayData> ret(new FGReplayData);
readRaw(in, ret->sim_time);
if (!in)
if (it != continuous.m_in_pos_to_frame.end())
{
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read fgtape frame at offset " << pos);
return nullptr;
if (0
|| (load_signals && !it->second->load_signals)
|| (load_multiplayer && !it->second->load_multiplayer)
|| (load_extra_properties && !it->second->load_extra_properties)
)
{
/* This frame is in the continuous.m_in_pos_to_frame cache, but
doesn't contain all of the required items, so we need to reload. */
continuous.m_in_pos_to_frame.erase(it);
it = continuous.m_in_pos_to_frame.end();
}
}
bool ok;
if (in_compression)
if (it == continuous.m_in_pos_to_frame.end())
{
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());
/* Load FGReplayData at offset <pos>.
We 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);
ret.reset(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;
}
it = continuous.m_in_pos_to_frame.lower_bound(pos);
it = continuous.m_in_pos_to_frame.insert(it, std::make_pair(pos, ret));
/* Delete faraway items. */
size_t size_old = continuous.m_in_pos_to_frame.size();
int n = 2;
size_t size_max = 2*n - 1;
remove_far_away(continuous.m_in_pos_to_frame, it, n);
remove_far_away(continuous.m_in_pos_to_frame, it, -n);
size_t size_new = continuous.m_in_pos_to_frame.size();
SG_LOG(SG_GENERAL, SG_DEBUG, ""
<< " n=" << size_old
<< " size_max=" << size_max
<< " size_old=" << size_old
<< " size_new=" << size_new
);
assert(size_new <= size_max);
}
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;
ret = it->second;
}
return ret;
}
@ -514,7 +587,17 @@ SGPropertyNode_ptr continuousWriteHeader(
return config;
}
bool replayContinuousInternal(
/* 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.
*/
static bool replayContinuousInternal(
Continuous& continuous,
FGFlightRecorder* recorder,
double time,
@ -529,7 +612,8 @@ bool replayContinuousInternal(
int* ysize
)
{
std::unique_ptr<FGReplayData> replay_data = ReadFGReplayData(
std::shared_ptr<FGReplayData> replay_data = ReadFGReplayData(
continuous,
continuous.m_in,
offset,
continuous.m_in_config,
@ -544,10 +628,11 @@ bool replayContinuousInternal(
return false;
}
assert(replay_data.get());
std::unique_ptr<FGReplayData> replay_data_old;
std::shared_ptr<FGReplayData> replay_data_old;
if (offset_old)
{
replay_data_old = ReadFGReplayData(
continuous,
continuous.m_in,
offset_old,
continuous.m_in_config,
@ -557,11 +642,12 @@ bool replayContinuousInternal(
continuous.m_in_compression
);
}
if (replay_extra_properties) SG_LOG(SG_SYSTEMS, SG_BULK,
if (replay_extra_properties) SG_LOG(SG_SYSTEMS, SG_DEBUG,
"replay():"
<< " time=" << time
<< " offset=" << offset
<< " offset_old=" << offset_old
<< " replay_data_old=" << replay_data_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()
@ -733,12 +819,19 @@ bool replayContinuous(FGReplayInternal& self, double time)
if (replay_this_frame)
{
size_t pos_prev = 0;
if (p_before != self.m_continuous->m_in_time_to_frameinfo.begin())
{
auto p_before_prev = p_before;
--p_before_prev;
pos_prev = p_before_prev->second.offset;
}
bool ok = replayContinuousInternal(
*self.m_continuous,
self.m_flight_recorder.get(),
p_before->first,
p_before->second.offset,
0 /*offset_old*/,
pos_prev /*offset_old*/,
false /*replay_signals*/,
p_before->first > time - multiplayer_recent /*replay_multiplayer*/,
true /*replay_extra_properties*/,

View file

@ -27,6 +27,8 @@ struct Continuous : SGPropertyChangeListener
SGPropertyNode_ptr m_in_config;
double m_in_time_last = 0;
double m_in_frame_time_last = 0;
std::map<size_t, std::shared_ptr<FGReplayData>>
m_in_pos_to_frame;
std::ifstream m_indexing_in;
std::streampos m_indexing_pos;
@ -48,29 +50,15 @@ struct Continuous : SGPropertyChangeListener
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.
//
/* 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.
//
/* Writes one frame of continuous record information. */
bool continuousWriteFrame(
Continuous& continuous,
FGReplayData* r,
@ -78,18 +66,17 @@ bool continuousWriteFrame(
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().
//
/* 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,
@ -98,32 +85,12 @@ SGPropertyNode_ptr continuousWriteHeader(
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
);
/* Replays one frame from Continuous recording.
Returns true on success, otherwise we failed to read from Continuous recording.
*/
bool replayContinuous(FGReplayInternal& self, double time);
/* Stops any video recording that was started because of
continuous->m_replay_create_video. */
void continuous_replay_video_end(Continuous& continuous);

View file

@ -737,6 +737,83 @@ static void setInt(const std::string& value, int* out)
}
}
/* Converts string to double, writing to out-para <out> and setting <ok> to
true. If conversion fails or fails to use the entire input string, sets ok to
false. */
static void string_to_double(const std::string& s, double& out, bool& ok)
{
errno = 0;
char* end;
out = strtod(s.c_str(), &end);
if (errno || !end || *end != 0)
{
ok = false;
return;
}
ok = true;
}
/* Updates property <path> to <value_next_string>. If property is also in
<frame_prev> and values look like floating point, we interpolate using <ratio>.
*/
static void replayProperty(
const std::string& path,
const std::string& value_next_string,
const FGReplayData* frame_prev,
double ratio
)
{
SGPropertyNode* p = globals->get_props()->getNode(path, true /*create*/);
bool done = false;
if (frame_prev)
{
/* Check whether <path> is in frame_prev's list of property changes. */
SG_LOG(SG_SYSTEMS, SG_DEBUG, "p && _pLastBuffer");
auto p_prev_it = frame_prev->replay_extra_property_changes.find(path);
if (p_prev_it != frame_prev->replay_extra_property_changes.end())
{
/* Property <path> is also in frame_prev. */
SG_LOG(SG_SYSTEMS, SG_DEBUG, "property in frame_prev and frame_next:"
<< " " << path);
const std::string& value_prev_string = p_prev_it->second;
bool value_prev_ok;
bool value_next_ok;
double value_prev;
double value_next;
string_to_double(value_prev_string, value_prev, value_prev_ok);
string_to_double(value_next_string, value_next, value_next_ok);
if (value_prev_ok && value_next_ok)
{
/* Both values look like floating point so we interpolate. */
SG_LOG(SG_SYSTEMS, SG_DEBUG, "property is fp");
TInterpolation interpolation = TInterpolation::linear;
if (simgear::strutils::ends_with(path, "-deg"))
{
interpolation = TInterpolation::angular_deg;
}
else if (simgear::strutils::ends_with(path, "-rad"))
{
interpolation = TInterpolation::angular_rad;
}
SG_LOG(SG_SYSTEMS, SG_DEBUG, "calling weighting() ratio=" << ratio);
double value_interpolated = weighting(
interpolation,
ratio,
value_prev,
value_next
);
SG_LOG(SG_GENERAL, SG_DEBUG, "Interpolating " << path
<< ": [" << value_prev << " .. " << value_next
<< "] => " << value_interpolated
);
globals->get_props()->setDoubleValue(path, value_interpolated);
done = true;
}
}
}
if (!done) p->setStringValue(path, value_next_string);
}
/** Replay.
* Restore all properties with data from given buffer. */
void
@ -750,11 +827,10 @@ FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer,
{
const char* pLastBuffer = (_pLastBuffer && !_pLastBuffer->raw_data.empty()) ? &_pLastBuffer->raw_data.front() : nullptr;
const char* pBuffer = (_pNextBuffer && !_pNextBuffer->raw_data.empty()) ? &_pNextBuffer->raw_data.front() : nullptr;
double ratio = 1.0;
if (pBuffer) {
/* Replay signals. */
int Offset = 0;
double ratio = 1.0;
if (pLastBuffer)
{
double NextSimTime = _pNextBuffer->sim_time;
@ -953,13 +1029,57 @@ FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer,
if (replay_main_view) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime
<< " replaying view change: " << path << "=" << value);
globals->get_props()->setStringValue(path, value);
/* Interpolate floating point values if possible. */
replayProperty(path, value, _pLastBuffer, ratio);
}
}
else if (replay_extra_properties) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime
<< " replaying extra_property change: " << path << "=" << value);
globals->get_props()->setStringValue(path, value);
SGPropertyNode* p = globals->get_props()->getNode(path, true /*create*/);
bool done = false;
if (p && _pLastBuffer) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "p && _pLastBuffer");
auto p_prev_it = _pLastBuffer->replay_extra_property_changes.find(path);
if (p_prev_it != _pLastBuffer->replay_extra_property_changes.end()) {
/* Property <path> is in both _pLastBuffer and
_pNextBuffer, so if it is floating point, we will
interpolate. */
SG_LOG(SG_SYSTEMS, SG_DEBUG, "property in _pLastBuffer and _pNextBuffer:"
<< " " << path);
const std::string& valus_prev = p_prev_it->second;
size_t value_prev_len;
size_t value_next_len;
double value_prev = std::stod(valus_prev, &value_prev_len);
double value_next = std::stod(value, &value_next_len);
if (value_prev_len == valus_prev.size() && value_next_len == value.size()) {
/* Both values look like floating point so we will
interpolate. */
SG_LOG(SG_SYSTEMS, SG_DEBUG, "property is fp");
TInterpolation interpolation = TInterpolation::linear;
if (simgear::strutils::ends_with(path, "-deg")) {
interpolation = TInterpolation::angular_deg;
}
else if (simgear::strutils::ends_with(path, "-rad")) {
interpolation = TInterpolation::angular_rad;
}
SG_LOG(SG_SYSTEMS, SG_DEBUG, "calling weighting()");
double value_interpolated = weighting(
interpolation,
ratio,
value_prev,
value_next
);
SG_LOG(SG_GENERAL, SG_DEBUG, "Interpolating " << path
<< ": [" << value_prev << " .. " << value_next
<< "] => " << value_interpolated
);
globals->get_props()->setDoubleValue(path, value_interpolated);
done = true;
}
}
}
if (!done) p->setStringValue(path, value);
}
}
}

View file

@ -46,6 +46,10 @@ extern const char* const FlightRecorderFileMagic;
/* Data for a single frame. */
struct FGReplayData
{
bool load_signals;
bool load_multiplayer;
bool load_extra_properties;
double sim_time;
// Our aircraft state.