1
0
Fork 0

Added record/replay of extra properties, with specific support for window size/position and view settings.

Recording of extra properties is only supported in Continuous recordings.

Modified Continuous recording format to allow future forwards
compatibility. See docs-mini/README-recordings.md for details. This breaks
compatibility with previously-generated Continuous recordings, but this only
affects next.

To reduce overhead we record all extra property values in the first frame and
then later frames contain only extra property changes. When replaying, if the
user jumps backwards we replay all extra property changes since the start of
the recording. Similarly if the user jumps forwards, we replay any intervening
extra property changes.

Recording extra properties:
    This is enabled by:
        /sim/replay/record-extra-properties

    The extra properties that are recorded are identified by the property
    paths in the values of /sim/replay/record-extra-properties-paths/path[]
    properties. We record the entire tree for each specified path.

Recording of main window position size:
    We have specific support for record and replay of main window position/size.

    This is enabled by:
        /sim/replay/record-main-window

Recording of main window view:
    We have specific support for recording the view type and direction/zoom
    settings.

    This is enabled by:
        /sim/replay/record-main-view

    We record the /sim/current-view/ property tree, excluding some subtrees
    that continuously vary but are not required for replaying of the view.

When replaying, we allow separate control of what extra property changes are
replayed, with:

    /sim/replay/replay-extra-property-changes
    /sim/replay/replay-extra-property-removal
    /sim/replay/replay-main-window-position
    /sim/replay/replay-main-window-size
    /sim/replay/replay-main-view

We work around some problems caused by the use of tied properties when
replaying changes to view-number.

Window position issue:

    When replaying window position and size changes, things get a little tricky
    because osgViewer::GraphicsWindow::setWindowRectangle() takes a position
    for the entire window, but osgGA::GUIEventAdapter::RESIZE events contain
    the position of the interior of the window; for example the y values will
    differ by the height of the window's titlebar. This can cause windows to
    move progressively further down each time they are positioned or resized.

    There doesn't seem to be a way of finding the size of a window's
    furniture directly. So instead this commit adds a new method
    osgGA::GUIEventAdapter::setWindowRectangleInteriorWithCorrection() which
    wraps osgViewer::GraphicsWindow::setWindowRectangle(). We listen for the
    following osgGA::GUIEventAdapter::RESIZE event and calculate corrections
    for x and y position that are used for subsequent calls.

docs-mini/README-recordings.md:
    Updated to document new Continuous format.

scripts/python/recordreplay.py:
    New script to test various aspects of record/replay.

Other:

    We now create convenience softlink to most recent continuous recording, using
    SGPath::makeLink(). Note that SGPath::makeLink() currently does nothing on
    Windows.

    Changed format of Continuous recordings to contain a single property tree
    in header. This is much simpler than having separate Config and Meta trees.
This commit is contained in:
Julian Smith 2020-12-24 14:21:38 +00:00
parent 1ff0ce2222
commit 31ec727872
10 changed files with 1467 additions and 211 deletions

View file

@ -10,13 +10,29 @@ 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.
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, and (as of 2020-12-23) may contain information about arbitrary properties.
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.
Recovery recordings are essentially single-frame Continuous recordings. When enabled, Flightgear creates them periodically to allow recovery of a session if Flightgear crashes.
## Properties
* `/sim/replay/tape-directory` - where to save recordings.
* `/sim/replay/record-multiplayer` - if true, we include multiplayer information in Normal and Continuous recordings.
* Normal recordings:
* `/sim/replay/buffer/high-res-time` - period for high resolution recording.
* `/sim/replay/buffer/medium-res-time` - period for medium resolution.
* `/sim/replay/buffer/low-res-time` - period for low resolution.
* `/sim/replay/buffer/medium-res-sample-dt` - sample period for medium resolution.
* `/sim/replay/buffer/low-res-sample-dt` - sample period for low resolution.
* Continuous recordings:
* `/sim/replay/record-continuous` - if true, do continuous record to file.
* `/sim/replay/record-extra-properties` - if true, we include selected properties in recordings.
* Recovery recordings:
* `/sim/replay/record-recovery-period` - if non-zero, we update recovery recording in specified interval.
## Code
The code that creates recordings is a not particularly clean or easy to work with.
The code that creates recordings is not particularly clean or easy to work with.
It consists mainly of two source files:
@ -29,43 +45,63 @@ Despite their names these files are both involved with record and replay. `src/A
## File format
Flightgear recording files have the following structure:
### Normal recordings
* Header:
* A zero-terminated magic string: `FlightGear Flight Recorder Tape` (see the `FlightRecorderFileMagic`).
* A Meta property tree containing a single `meta` node with various child nodes.
* A zero-terminated magic string: `FlightGear Flight Recorder Tape` (variable `FlightRecorderFileMagic`).
* A Meta property tree containing a `meta` node with various child nodes.
* A Config property tree containing information about what signals will be contained in each frame.
Each signal is a property; signals are used for the main recorded information such as position and orientation of the user's aircraft, and positions of flight surfaces, gear up/down etc. Aircraft may define their own customised set of signals.
The Meta and Config property trees are each written as a binary length integer followed by a text representation of the properties.
For Continuous recordings, the header is written by `FGReplay::continuousWriteHeader()`.
The Meta and Config property trees are each written as `<length:64><text>` where `<text>` is a text representation of the properties. `<text>` is terminated with a zero which is included in the `<length:64>` field.
* A series of frames, each containg the data in a `FGReplayData` instance, looking like:
* Frame time as a binary double.
* Signals information as described in the header's Config property tree, in a fixed-size binary format.
* Signals information as described in the header's `Config` property tree, represented as a 64-bit length followed by binary data.
* If the header's Meta property tree has a child node `multiplayer` set to true, we append recorded information about multiplayer aircraft. This will be a binary integer containing the number of multiplayer messages to follow then, for each message, the message size followed by the message contents. The message contents are exactly the multiplayer packet as received from the network.
All data after the header is in a single gzipped stream.
* **[Work in progress as of 2020-12-23]** (Continuous recordings only) If the header's Meta property tree has a child node `record-properties` set to true, we append a variable list of property changes:
### Continuous recordings
* Length of following property-change data.
* Header:
* `<length><path><length><string_value>`
* 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 `meta` node with various child nodes.
* `data[]` nodes describing the data items in each frame in the order in which they occur. Supported values are:
* `signals` - core information about the user's aircraft.
* `multiplayer` - information about multiplayer aircraft.
* `extra-properties` - information about extra properties.
* A `signals` node containing layout information for signals, in the same format as for Normal recordings.
The header is written by `FGReplay::continuousWriteHeader()`.
* A series of frames, each containg the data in a `FGReplayData` instance, looking like:
* 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.
Removal of a property is encoded as an item consisting of a zero integer followed by `<length><path>`.
All numbers here are 16 bit binary.
* For `signals`, `<data>` is binary data for the core aircraft properties.
* For `multiplayer`, `<data>` is a list of `<length:16><packet>` items where `<packet>` is a multiplayer packet as received from the network.
In Normal recordings, all data after the header is in a single gzipped stream.
* For `extra-properties`, `<data>` is a list of property changes, each one being:
* `<length:16><path><length:16><value>` - property <path> has changed to `<value>`.
Removal of a property is encoded as `<0:16><length:16><path>`.
Continuous recordings do not use compression, in order to simplify random access when replaying.
@ -74,12 +110,14 @@ 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 a mapping in memory from frame times to offsets within the file, so that we can support the user jumping forwards and backwards in the recording.
If the recording contains extra properties, we also build a cache of the locations of frames that have a non-empty extra-properties item, again to support jumping around in time.
## Multiplayer
### Recording while replaying:
If the user replays part of their history while we are saving to a Continuous recording, and the Continuous recording includes multiplayer information, then we carry on receiving multiplayer information from the network and writing it to the Continuous recording. The user's aircraft is recorded as being stationary while the user was replaying.
If the user replays part of their history while we are saving to a Continuous recording, and the Continuous recording includes multiplayer information, then we carry on receiving multiplayer information from the network and writing it to the Continuous recording. The user's aircraft is recorded as being stationary for the period when the user was replaying.
### Replaying

395
scripts/python/recordreplay.py Executable file
View file

@ -0,0 +1,395 @@
#!/usr/bin/env python3
'''
Test script for record/replay. Only tested on Unix.
E.g.:
./flightgear/scripts/python/recordreplay.py -f run_fgfs.sh
Args:
--all
Run all tests (this is default).
--continuous BOOLS
--extra-properties BOOLS
--it-max <it>
Set min iteration (inclusive) to run; useful when fixing tests and
retrying.
--it-min <it>
Set max iteration (exclusive) to run; useful when fixing tests and
retrying.
--main-view BOOLS
--multiplayer
-f <fgfs>
The fgfs executable to use. Default assumes the Walk build system.
--f-old <fgfs-old>
A second fgfs executable. If specified we run all tests twice, first
using <fgfs-old> to create the recording and <fgfs> to replay it,
second the other way round.
BOOLS is comma-sparated list of 0 or 1, with 1 activating the particular
feature. So for example '--continuous 0' tests normal recording/replay',
'--continuous 1' tests continuous recording/replay, and continuous 0,1'
tests both.
We test all combinations of continuous, extra-properties, main-view and
multiplayer recordings. For each test we check that we can create a
recording, and replay it in a new fgfs instance. When replaying we check
a small number of basic things such as the recording length, and whether
extra-properties are replayed.
'''
import os
import signal
import sys
import time
import FlightGear
def log(text):
print(text, file=sys.stderr)
sys.stderr.flush()
g_cleanup = []
class Fg:
'''
Runs flightgear. self.fg is a FlightGear.FlightGear instance, which uses
telnet to communicate with Flightgear.
'''
def __init__(self, aircraft, args, env=None):
'''
aircraft:
Specified as: --aircraft={aircraft}. This is separate from <args>
because we need to know the name of recording files.
args:
Miscellenous args either space-separated name=value strings or a
dict.
env:
Environment to set. If DISPLAY is not in <env> we add 'DISPLAY=:0'.
'''
self.pid = None
self.aircraft = aircraft
args += f' --aircraft={aircraft}'
port = 5500
args += f' --telnet={port}'
args2 = args.split()
if isinstance(env, str):
env2 = dict()
for nv in env.split():
n, v = nv.split('=', 1)
env2[n] = v
env = env2
environ = os.environ.copy()
if env:
environ.update(env)
if 'DISPLAY' not in environ:
environ['DISPLAY'] = ':0'
# Run flightgear in new process, telling it to open telnet server.
self.pid = os.fork()
if self.pid==0:
log(f'calling os.exec(): {" ".join(args2)}')
os.execve(args2[0], args2, environ)
# Connect to flightgear's telnet server.
timeout = 15
t0 = time.time()
while 1:
time.sleep(1)
dt = time.time() - t0
if dt > timeout:
text = f'Timeout trying to connect. timeout={timeout}'
log(text)
raise Exception(text)
try:
log('Connecting... ')
self.fg = FlightGear.FlightGear('localhost', port)
log(f'Connected. timeout={timeout} dt={dt:.1f}')
return
except Exception as e:
log(f'Failed to connect timeout={timeout} dt={dt:.1f}: {e}')
def waitfor(self, name, value, timeout=30):
'''
Waits for specified property to be <value>. Returns time taken.
'''
t0 = time.time()
while 1:
time.sleep(1)
dt = time.time() - t0
try:
v = self.fg[name]
log(f'Waiting for {name}={value} current value: {v}. timeout={timeout} dt={dt:.1f}')
except Exception as e:
log(f'Failed to get value of property {name}: {e}. timeout={timeout} dt={dt:.1f}')
v = None
if v == value:
return dt
if dt > timeout:
raise Exception(f'Timeout waiting for {name}={value}; current value: {v}. timeout={timeout}')
def run_command(self, command):
self.fg.telnet._putcmd(command)
ret = self.fg.telnet._getresp()
log(f'command={command!r} ret: {ret}')
return ret
def close(self):
assert self.pid
sig = signal.SIGTERM
log(f'Sending sig={sig} to pid={self.pid}')
os.kill(self.pid, sig)
log(f'Waiting for pid={self.pid}')
r = os.waitpid(self.pid, 0)
log(f'waitpid => {r}')
self.pid = None
def __del__(self):
if self.pid:
log('*** Fg.__del__() calling self.close()')
self.close()
def make_recording(
fg,
continuous=0,
extra_properties=0,
main_view=0,
length=5,
):
'''
Makes a recording, and returns its path.
We check that the recording file is newly created.
'''
t = time.time()
if continuous:
assert not fg.fg['/sim/replay/record-continuous']
fg.fg['/sim/replay/record-continuous'] = 1
t0 = time.time()
while 1:
if time.time() > t0 + length:
break
fg.run_command('run view-step step=1')
time.sleep(1)
fg.fg['/sim/replay/record-continuous'] = 0
path = f'{fg.aircraft}-continuous.fgtape'
time.sleep(1)
else:
# Normal recording will have effectively already started, so we sleep
# for the remaining time. This is a little inaccurate though because it
# looks like normal recording starts a little after t=0, e.g. at t=0.5.
#
# Also, it looks like /sim/time/elapsed-sec doesn't quite track real
# time, so we sometimes need to sleep a little longer.
#
while 1:
t = fg.fg['/sim/time/elapsed-sec']
log(f'/sim/time/elapsed-sec={t}')
if t > length:
break
time.sleep(length - t + 0.5)
log(f'/sim/time/elapsed-sec={t}')
fg.fg.telnet._putcmd('run save-tape tape-data/starttime= tape-data/stoptime=')
response = fg.fg.telnet._getresp()
log(f'response: {response!r}')
path = f'{fg.aircraft}.fgtape'
# Check recording is new.
os.system(f'ls -lL {path}')
s = os.stat(path, follow_symlinks=True)
assert s.st_mtime > t
path2 = os.readlink(path)
log(f'path={path} path2={path2}')
return path
def test_record_replay(
fgfs_save,
fgfs_load,
multiplayer,
continuous,
extra_properties,
main_view,
length,
):
if not fgfs_load:
fgfs_load = fgfs
log(f'=== save: {fgfs_save}')
log(f'=== load: {fgfs_load}')
log(f'=== --multiplayer {multiplayer} --continuous {continuous} --extra-properties {extra_properties} --main-view {main_view}')
log(f'===')
aircraft = 'harrier-gr3'
args = f'--state=vto --airport=egtk'
args += f' --prop:bool:/sim/replay/record-extra-properties={extra_properties}'
args += f' --prop:bool:/sim/replay/record-main-view={main_view}'
args += f' --prop:bool:/sim/replay/record-main-window=0'
# Start Flightgear.
fg = Fg(aircraft, f'{fgfs_save} {args}',
#env='SG_LOG_DELTAS=flightgear/src/Network/props.cxx=4',
)
fg.waitfor('/sim/fdm-initialized', 1, timeout=45)
assert fg.fg['sim/replay/record-extra-properties'] == extra_properties
assert fg.fg['sim/replay/record-main-view'] == main_view
log(f'sim/replay/record-extra-properties={fg.fg["sim/replay/record-extra-properties"]}')
# Save recording:
path = make_recording(fg,
continuous=continuous,
extra_properties=extra_properties,
main_view=main_view,
length=length,
)
g_cleanup.append(lambda: os.remove(path))
fg.close()
# Load recording into new Flightgear.
path = f'{aircraft}-continuous.fgtape' if continuous else f'{aircraft}.fgtape'
fg = Fg(aircraft, f'{fgfs_load} {args} --load-tape={path}')
fg.waitfor('/sim/fdm-initialized', 1, timeout=45)
fg.waitfor('/sim/replay/replay-state', 1)
# Check replay time is ok.
rtime_begin = fg.fg['/sim/replay/start-time']
rtime_end = fg.fg['/sim/replay/end-time']
rtime = rtime_end - rtime_begin
log(f'rtime={rtime_begin}..{rtime_end}, recording length: {rtime}, length={length}')
assert rtime > length-1 and rtime < length+2, \
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']
log(f'num_frames_extra_properties={num_frames_extra_properties}')
if continuous:
if main_view:
assert num_frames_extra_properties > 1
else:
assert num_frames_extra_properties == 0
else:
assert num_frames_extra_properties in (0, None), \
f'num_frames_extra_properties={num_frames_extra_properties}'
time.sleep(length)
fg.close()
os.remove(path)
log('Test passed')
if __name__ == '__main__':
fgfs = f'./build-walk/fgfs.exe-run.sh'
fgfs_old = None
continuous_s = [0, 1]
extra_properties_s = [0, 1]
main_view_s = [0, 1]
multiplayer_s = [0, 1]
fgfs_reverse_s = [0]
it_min = None
it_max = None
if len(sys.argv) == 1:
do_all = True
args = iter(sys.argv[1:])
while 1:
try:
arg = next(args)
except StopIteration:
break
if arg == '--all':
do_all = True
elif arg == '--continuous':
continuous_s = map(int, next(args).split(','))
elif arg == '--extra-properties':
extra_properties_s = map(int, next(args).split(','))
elif arg == '--it-max':
it_max = int(next(args))
elif arg == '--it-min':
it_min = int(next(args))
elif arg == '--main-view':
main_view_s = map(int, next(args).split(','))
elif arg == '--multiplayer':
multiplayer_s = map(int, next(args).split(','))
elif arg == '-f':
fgfs = next(args)
elif arg == '--f-old':
fgfs_old = next(args)
fgfs_reverse = [0, 1]
else:
raise Exception(f'Unrecognised arg: {arg!r}')
try:
if fgfs_old:
for fgfs1, fgfs2 in [(fgfs, fgfs_old), (fgfs_old, fgfs)]:
for multiplayer in 0, 1:
test_record_replay(
fgfs1,
fgfs2,
multiplayer,
continuous=0,
extra_properties=0,
main_view=0,
length=10,
)
else:
its_max = len(multiplayer_s) * len(continuous_s) * len(extra_properties_s) * len(main_view_s) * len(fgfs_reverse_s)
it = 0
for multiplayer in multiplayer_s:
for continuous in continuous_s:
for extra_properties in extra_properties_s:
for main_view in main_view_s:
for fgfs_reverse in fgfs_reverse_s:
if fgfs_reverse:
fgfs_save = fgfs_old
fgfs_load = fgfs
else:
fgfs_save = fgfs
fgfs_load = fgfs_old
ok = True
if it_min is not None:
if it < it_min:
ok = False
if it_max is not None:
if it >= it_max:
ok = False
log('')
log(f'===')
log(f'=== {it}/{its_max}')
if ok:
test_record_replay(
fgfs_save,
fgfs_load,
multiplayer=multiplayer,
continuous=continuous,
extra_properties=extra_properties,
main_view=main_view,
length=10
)
it += 1
finally:
pass
# If everything passed, cleanup. Otherwise leave recordings in place, as
# they can be useful for debugging.
#
for f in g_cleanup:
try:
f()
except:
pass
if 0:
# This path can be used to check we cleanup properly after an error.
fg = Fg('./build-walk/fgfs.exe-run.sh --aircraft=harrier-gr3 --airport=egtk')
time.sleep(5)
assert 0

View file

@ -42,9 +42,190 @@
using namespace FlightRecorder;
using std::string;
// Appends raw data to <out>.
static void s_Append(std::vector<char>& out, const void* data, size_t data_len)
{
size_t pos = out.size();
out.resize(pos + data_len);
memcpy(&out[pos], data, data_len);
}
// Appends int16 number to <out>.
static void s_AppendNumber(std::vector<char>& out, int16_t number)
{
s_Append(out, &number, sizeof(number));
}
// Appends string to <out> as int16 length followed by chars.
static void s_AppendString(std::vector<char>& out, const std::string& s)
{
s_AppendNumber(out, s.size());
s_Append(out, s.c_str(), s.size());
}
// Updates property <b> so that it is identical to property <a> and writes
// information on all differences (creation, value changes and deletion) to
// <out>.
//
// We call ourselves recursively for child nodes.
//
// We are careful to preserve indices.
//
// We treat all property values as text.
//
static void s_RecordPropertyDiffs(
std::vector<char>& out,
SGPropertyNode* a,
SGPropertyNode* b,
const std::vector<const char*>* path_exclude_prefixes
)
{
assert(a);
assert(b);
assert(!strcmp(a->getName(), b->getName()));
assert(a->getPath() == b->getPath());
assert(a->getIndex() == b->getIndex());
if (path_exclude_prefixes) {
for (const char* path_exclude_prefix: *path_exclude_prefixes) {
if (simgear::strutils::starts_with(a->getPath(true /*simplify*/), path_exclude_prefix)) {
SG_LOG(SG_SYSTEMS, SG_BULK, "Ignoring: " << a->getPath(true /*simplify*/));
return;
}
}
}
// If values differ, write a's value to out and change b's value to a's value.
const char* a_value = a->getStringValue();
const char* b_value = b->getStringValue();
if (strcmp(a_value, b_value)) {
// Values are different so write out node <a> and update b.
SG_LOG(SG_SYSTEMS, SG_DEBUG, "recording property change:"
<< a->getPath()
<< ": " << b->getStringValue()
<< " => " << a->getStringValue()
);
s_AppendString(out, a->getPath(true /*simplify*/));
s_AppendString(out, a->getStringValue());
b->setStringValue(a_value);
}
// Look at all child nodes of <b>, removing any that are not in <a>.
int bn = b->nChildren();
for (int i=0; i<bn; ++i) {
SGPropertyNode* bc = b->getChild(i);
SGPropertyNode* ac = a->getChild(bc->getName(), bc->getIndex(), false /*create*/);
if (!ac) {
// Child node is in b but not in a; we write out special
// information about the deleted node and remove from b.
s_AppendString(out, "");
s_AppendString(out, bc->getPath(true /*simplify*/));
b->removeChild(bc);
}
}
// Look at all child nodes of <a>, copying across to <b> as required.
//
int an = a->nChildren();
for (int i=0; i<an; ++i) {
SGPropertyNode* ac = a->getChild(i);
SGPropertyNode* bc = b->getChild(ac->getName(), ac->getIndex(), true /*create*/);
// Recurse.
s_RecordPropertyDiffs(out, ac, bc, path_exclude_prefixes);
}
}
// Takes care of writing extra-properties to FGReplayData if we are doing a
// Continuous recording with extra-properties.
//
// We write different properties depending on properties such as
// /sim/replay/record-main-view.
//
struct RecordExtraProperties
{
RecordExtraProperties()
:
m_record_extra_properties(fgGetNode("/sim/replay/record-extra-properties", true /*create*/)),
m_record_main_window(fgGetNode("/sim/replay/record-main-window", true /*create*/)),
m_record_main_view(fgGetNode("/sim/replay/record-main-view", true /*create*/)),
m_record_extra_properties_paths(fgGetNode("/sim/replay/record-extra-properties-paths", true /*create*/))
{
}
// Added info about property changes to *ReplayData if we are recording
// them.
//
void capture(SGPropertyNode_ptr RecordExtraPropertiesReference, FGReplayData* ReplayData)
{
if (m_record_extra_properties->getBoolValue()) {
// Record extra properties specified in
// /sim/replay/record-extra-properties-paths/path[].
//
auto paths = m_record_extra_properties_paths->getChildren("path");
for (SGPropertyNode* n: paths) {
// We don't try to handle indices or deletion for top-level
// property node.
//
std::string path = n->getStringValue();
SGPropertyNode* a = globals->get_props()->getNode(path, false /*create*/);
if (!a) {
SG_LOG(SG_SYSTEMS, SG_ALERT, "property does not exist: " << path);
continue;
}
SGPropertyNode* b = RecordExtraPropertiesReference->getNode(path, true /*create*/);
s_RecordPropertyDiffs(ReplayData->extra_properties, a, b, NULL /*path_exclude_prefixes*/);
}
}
if (m_record_main_window->getBoolValue()) {
// Record size/position of main window.
//
static std::vector<const char*> s_paths = {
"sim/startup/xpos",
"sim/startup/ypos",
"sim/startup/xsize",
"sim/startup/ysize"
};
for (const char* path: s_paths) {
SGPropertyNode* a = globals->get_props()->getNode(path, true /*create*/);
SGPropertyNode* b = RecordExtraPropertiesReference->getNode(path, true /*create*/);
s_RecordPropertyDiffs(ReplayData->extra_properties, a, b, NULL /*path_exclude_prefixes*/);
}
}
if (m_record_main_view->getBoolValue()) {
// Record main window view.
//
static std::vector<const char*> s_excludes = {
"/sim/current-view/debug/",
"/sim/current-view/raw-orientation",
"/sim/current-view/viewer-"
};
const char* path = "sim/current-view";
SGPropertyNode* a = globals->get_props()->getNode(path, true /*create*/);
SGPropertyNode* b = RecordExtraPropertiesReference->getNode(path, true /*create*/);
s_RecordPropertyDiffs(ReplayData->extra_properties, a, b, &s_excludes);
}
}
private:
SGPropertyNode_ptr m_record_extra_properties;
SGPropertyNode_ptr m_record_main_window;
SGPropertyNode_ptr m_record_main_view;
SGPropertyNode_ptr m_record_extra_properties_paths;
};
static std::shared_ptr<RecordExtraProperties> s_record_extra_properties;
FGFlightRecorder::FGFlightRecorder(const char* pConfigName) :
m_RecorderNode(fgGetNode("/sim/flight-recorder", true)),
m_ReplayMultiplayer(fgGetNode("/sim/replay/multiplayer", true)),
m_RecordContinuous(fgGetNode("/sim/replay/record-continuous", true)),
m_RecordExtraProperties(fgGetNode("/sim/replay/record-extra-properties", true)),
m_TotalRecordSize(0),
m_ConfigName(pConfigName),
m_usingDefaultConfig(false),
@ -54,6 +235,7 @@ FGFlightRecorder::FGFlightRecorder(const char* pConfigName) :
FGFlightRecorder::~FGFlightRecorder()
{
s_record_extra_properties.reset();
}
void
@ -122,6 +304,8 @@ FGFlightRecorder::reinit(SGPropertyNode_ptr ConfigNode)
// expose size of actual flight recorder record
m_RecorderNode->setIntValue("record-size", m_TotalRecordSize);
SG_LOG(SG_SYSTEMS, SG_INFO, "FlightRecorder: record size is " << m_TotalRecordSize << " bytes");
s_record_extra_properties.reset(new RecordExtraProperties);
}
/** Check if SignalList already contains the given property */
@ -448,6 +632,17 @@ FGFlightRecorder::capture(double SimTime, FGReplayData* ReplayData)
ReplayData->multiplayer_messages.push_back( MultiplayerMessage);
}
}
// Add extra properties if we are doing continuous recording.
//
if (m_RecordContinuous->getBoolValue()) {
if (!m_RecordExtraPropertiesReference) {
SG_LOG(SG_SYSTEMS, SG_ALERT, "m_RecordPropertiesReference is null");
m_RecordExtraPropertiesReference = new SGPropertyNode;
}
s_record_extra_properties->capture(m_RecordExtraPropertiesReference, ReplayData);
}
ReplayData->UpdateStats();
// Note that if we are replaying, <ReplayData> will contain the last live
@ -500,10 +695,36 @@ weighting(TInterpolation interpolation, double ratio, double v1,double v2)
}
}
void
FGFlightRecorder::resetExtraProperties()
{
SG_LOG(SG_SYSTEMS, SG_ALERT, "Clearing m_RecordExtraPropertiesReference");
m_RecordExtraPropertiesReference = nullptr;
}
// Converts string to int, ignoring errors and doing nothing if out-para <out>
// is null.
static void setInt(const std::string& value, int* out)
{
if (!out) return;
try {
*out = std::stoi(value);
}
catch (std::exception& e) {
SG_LOG(SG_SYSTEMS, SG_ALERT, "Ignoring failed conversion of '" << value << "' to int: " << e.what());
}
}
/** Replay.
* Restore all properties with data from given buffer. */
void
FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer, const FGReplayData* _pLastBuffer)
FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer,
const FGReplayData* _pLastBuffer,
int* main_window_xpos,
int* main_window_ypos,
int* main_window_xsize,
int* main_window_ysize
)
{
const char* pLastBuffer = (_pLastBuffer) ? &_pLastBuffer->raw_data.front() : nullptr;
const char* pBuffer = (_pNextBuffer) ? &_pNextBuffer->raw_data.front() : nullptr;
@ -611,7 +832,7 @@ FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer, const
m_CaptureBool[i].Signal->setBoolValue(0 != (pFlags[i>>3] & (1 << (i&7))));
}
}
// Replay any multiplayer messages. But don't send the same multiplayer
// messages repeatedly when we are called with a timestamp that ends up
// picking the same _pNextBuffer as last time.
@ -623,6 +844,73 @@ FGFlightRecorder::replay(double SimTime, const FGReplayData* _pNextBuffer, const
m_MultiplayMgr->pushMessageHistory(multiplayer_message);
}
}
// Replay property changes.
//
bool replay_extra_property_removal = globals->get_props()->getBoolValue("sim/replay/replay-extra-property-removal");
bool replay_extra_property_changes = globals->get_props()->getBoolValue("sim/replay/replay-extra-property-changes");
bool replay_main_view = globals->get_props()->getBoolValue("sim/replay/replay-main-view");
bool replay_main_window_position = globals->get_props()->getBoolValue("sim/replay/replay-main-window-position");
bool replay_main_window_size = globals->get_props()->getBoolValue("sim/replay/replay-main-window-size");
if (replay_extra_property_removal) {
for (auto extra_property_removed_path: _pNextBuffer->replay_extra_property_removals) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "replaying extra property removal: " << extra_property_removed_path);
globals->get_props()->removeChild(extra_property_removed_path);
}
}
// Apply any changes to /sim/current-view/view-number* first. This is a
// hack to avoid problems where setting view-number appears to make the
// view code change other things such as pitch-offset-deg internally. So if
// the recorded property changes list view-number after pitch-offset-deg,
// we will end up overwriting the recorded change to pitch-offset-deg.
//
// The ultimate cause of this problem is that pitch-offset-deg is a tied
// property so when recording we don't always pick up all changes.
//
if (replay_main_view) {
for (auto prop_change: _pNextBuffer->replay_extra_property_changes) {
const std::string& path = prop_change.first;
const std::string& value = prop_change.second;
if (simgear::strutils::starts_with(path, "/sim/current-view/view-number")) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "*** SimTime=" << SimTime << " replaying view " << path << "=" << value);
globals->get_props()->setStringValue(path, value);
}
}
}
for (auto prop_change: _pNextBuffer->replay_extra_property_changes) {
const std::string& path = prop_change.first;
const std::string& value = prop_change.second;
if (0) {}
else if (path == "/sim/startup/xpos") {
if (replay_main_window_position) setInt(value, main_window_xpos);
}
else if (path == "/sim/startup/ypos") {
if (replay_main_window_position) setInt(value, main_window_ypos);
}
else if (path == "/sim/startup/xsize") {
if (replay_main_window_size) setInt(value, main_window_xsize);
}
else if (path == "/sim/startup/ysize") {
if (replay_main_window_size) setInt(value, main_window_ysize);
}
else if (simgear::strutils::starts_with(path, "/sim/current-view/")) {
if (replay_main_view) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime
<< " replaying view change: " << path << "=" << value);
globals->get_props()->setStringValue(path, value);
}
}
else if (replay_extra_property_changes) {
SG_LOG(SG_SYSTEMS, SG_DEBUG, "SimTime=" << SimTime
<< " replaying extra_property change: " << path << "=" << value);
globals->get_props()->setStringValue(path, value);
}
}
}
int

View file

@ -57,11 +57,19 @@ public:
void reinit (void);
void reinit (SGPropertyNode_ptr ConfigNode);
FGReplayData* capture (double SimTime, FGReplayData* pRecycledBuffer);
// Updates main_window_* out-params if we find window move/resize events
// and replay of such events is enabled.
void replay (double SimTime, const FGReplayData* pNextBuffer,
const FGReplayData* pLastBuffer = NULL);
const FGReplayData* pLastBuffer,
int* main_window_xpos,
int* main_window_ypos,
int* main_window_xsize,
int* main_window_ysize
);
int getRecordSize (void) { return m_TotalRecordSize;}
void getConfig (SGPropertyNode* root);
void resetExtraProperties();
private:
SGPropertyNode_ptr getDefault(void);
@ -78,7 +86,14 @@ private:
SGPropertyNode_ptr m_RecorderNode;
SGPropertyNode_ptr m_ConfigNode;
SGPropertyNode_ptr m_ReplayMultiplayer;
SGPropertyNode_ptr m_RecordContinuous;
SGPropertyNode_ptr m_RecordExtraProperties;
// This contains copy of all properties that we are recording, so that we
// can send only differences.
//
SGPropertyNode_ptr m_RecordExtraPropertiesReference;
FlightRecorder::TSignalList m_CaptureDouble;
FlightRecorder::TSignalList m_CaptureFloat;
FlightRecorder::TSignalList m_CaptureInteger;

File diff suppressed because it is too large Load diff

View file

@ -50,6 +50,9 @@ struct FGReplayData {
// 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();
@ -81,6 +84,15 @@ typedef struct {
std::string speaker;
} FGReplayMessages;
enum FGTapeType
{
FGTapeType_NORMAL,
FGTapeType_CONTINUOUS,
FGTapeType_RECOVERY,
};
struct FGFrameInfo;
typedef std::deque < FGReplayData *> replay_list_type;
typedef std::vector < FGReplayMessages > replay_messages_type;
@ -114,8 +126,27 @@ 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=0);
void replay(double time, FGReplayData* pCurrentFrame, FGReplayData* pOldFrame=NULL);
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();
@ -129,13 +160,12 @@ private:
bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory);
bool saveTape(const SGPath& Filename, SGPropertyNode_ptr MetaData);
bool loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta);
bool continuousWriteHeader(
SGPropertyNode_ptr continuousWriteHeader(
std::ofstream& out,
SGPropertyNode_ptr& myMetaData,
SGPropertyNode_ptr& Config,
const SGPath& path
const SGPath& path,
FGTapeType tape_type
);
bool continuousWriteFrame(FGReplayData* r, std::ostream& out);
bool continuousWriteFrame(FGReplayData* r, std::ostream& out, SGPropertyNode_ptr meta);
double sim_time;
double last_mt_time;
@ -161,6 +191,11 @@ private:
SGPropertyNode_ptr replay_multiplayer;
SGPropertyNode_ptr recovery_period;
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
@ -177,11 +212,16 @@ private:
void valueChanged(SGPropertyNode * node);
// Things for replaying from uncompressed fgtape file.
std::ifstream m_continuous_in;
std::map<double, size_t> m_continuous_time_to_offset;
std::ifstream m_continuous_in;
bool m_continuous_in_multiplayer;
bool m_continuous_in_extra_properties;
std::map<double, FGFrameInfo> m_continuous_in_time_to_frameinfo;
SGPropertyNode_ptr m_continuous_in_config;
double m_continuous_in_time_last;
// For writing uncompressed fgtape file.
std::ofstream m_continuous_out;
SGPropertyNode_ptr m_continuous_out_config;
std::ofstream m_continuous_out;
};
#endif // _FG_REPLAY_HXX

View file

@ -161,6 +161,26 @@ bool isMainWindow(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& us)
}
void FGEventHandler::setWindowRectangleInteriorWithCorrection(osgViewer::GraphicsWindow* window, int x, int y, int width, int height)
{
// Store (x y) in our state so that our handle() event handler can
// compare the requested position with the actual position and update
// (m_setWindowRectangle_delta_x m_setWindowRectangle_delta_y) accordingly.
//
SG_LOG(SG_VIEW, SG_DEBUG, "FGEventHandler::setWindowRectangle(): pos=(" << x << " " << y << ")"
<< " delta=(" << m_setWindowRectangle_delta_x << " " << m_setWindowRectangle_delta_y << ")"
);
m_setWindowRectangle_called = true;
m_setWindowRectangle_called_x = x;
m_setWindowRectangle_called_y = y;
window->setWindowRectangle(
x - m_setWindowRectangle_delta_x,
y - m_setWindowRectangle_delta_y,
width,
height
);
}
bool FGEventHandler::handle(const osgGA::GUIEventAdapter& ea,
osgGA::GUIActionAdapter& us)
{
@ -274,8 +294,29 @@ bool FGEventHandler::handle(const osgGA::GUIEventAdapter& ea,
return true;
}
CameraGroup::getDefault()->resized();
if (resizable)
globals->get_renderer()->resize(ea.getWindowWidth(), ea.getWindowHeight());
if (resizable) {
if (m_setWindowRectangle_called) {
// Update m_setWindowRectangle_delta_x and
// m_setWindowRectangle_delta_y so that our
// setWindowRectangle() compensates for the window furniture
// differences in future calls.
//
m_setWindowRectangle_called = false;
int error_x = ea.getWindowX() - m_setWindowRectangle_called_x;
int error_y = ea.getWindowY() - m_setWindowRectangle_called_y;
SG_LOG(SG_VIEW, SG_BULK, "m_setWindowRectangle_called is set:"
<< " m_setWindowRectangle_delta=("
<< m_setWindowRectangle_delta_x << " " << m_setWindowRectangle_delta_y << ")"
<< " m_setWindowRectangle_called_=("
<< m_setWindowRectangle_called_x << " " << m_setWindowRectangle_called_y << ")"
<< " ea.getWindow=(" << ea.getWindowX() << " " << ea.getWindowY() << ")"
<< " error=(" << error_x << " " << error_y << ")"
);
m_setWindowRectangle_delta_x += error_x;
m_setWindowRectangle_delta_y += error_y;
}
globals->get_renderer()->resize(ea.getWindowWidth(), ea.getWindowHeight(), ea.getWindowX(), ea.getWindowY());
}
statsHandler->handle(ea, us);
#ifdef SG_MAC
// work around OSG Cocoa-Viewer issue with resize event handling,

View file

@ -99,6 +99,11 @@ public:
void reset();
void clear();
// Wrapper for osgViewer::GraphicsWindow::setWindowRectangle() that takes
// coordinates excluding window furniture.
//
void setWindowRectangleInteriorWithCorrection(osgViewer::GraphicsWindow* window, int x, int y, int width, int height);
static int translateKey(const osgGA::GUIEventAdapter& ea);
static int translateModifiers(const osgGA::GUIEventAdapter& ea);
@ -122,6 +127,12 @@ protected:
void handleStats(osgGA::GUIActionAdapter& us);
bool changeStatsCameraRenderOrder;
SGPropertyNode_ptr _display, _print;
private:
bool m_setWindowRectangle_called = false;
int m_setWindowRectangle_called_x = 0;
int m_setWindowRectangle_called_y = 0;
int m_setWindowRectangle_delta_x = 0;
int m_setWindowRectangle_delta_y = 0;
};
bool eventToWindowCoords(const osgGA::GUIEventAdapter* ea, double& x, double& y);

View file

@ -498,6 +498,8 @@ FGRenderer::init( void )
_xsize = fgGetNode("/sim/startup/xsize", true);
_ysize = fgGetNode("/sim/startup/ysize", true);
_xpos = fgGetNode("/sim/startup/xpos", true);
_ypos = fgGetNode("/sim/startup/ypos", true);
_splash_alpha = fgGetNode("/sim/startup/splash-alpha", true);
_horizon_effect = fgGetNode("/sim/rendering/horizon-effect", true);
@ -905,16 +907,14 @@ FGRenderer::updateSky()
}
void
FGRenderer::resize( int width, int height )
FGRenderer::resize( int width, int height, int x, int y )
{
int curWidth = _xsize->getIntValue(),
curHeight = _ysize->getIntValue();
SG_LOG(SG_VIEW, SG_DEBUG, "FGRenderer::resize: new size " << width << " x " << height);
if ((curHeight != height) || (curWidth != width)) {
// must guard setting these, or PLIB-PUI fails with too many live interfaces
_xsize->setIntValue(width);
_ysize->setIntValue(height);
}
if (width != _xsize->getIntValue()) _xsize->setIntValue(width);
if (height != _ysize->getIntValue()) _ysize->setIntValue(height);
if (x != _xpos->getIntValue()) _xpos->setIntValue(x);
if (y != _ypos->getIntValue()) _ypos->setIntValue(y);
// update splash node if present
_splash->resize(width, height);
@ -925,6 +925,12 @@ FGRenderer::resize( int width, int height )
#endif
}
void
FGRenderer::resize( int width, int height )
{
resize(width, height, _xpos->getIntValue(), _ypos->getIntValue());
}
typedef osgUtil::LineSegmentIntersector::Intersection Intersection;
SGVec2d uvFromIntersection(const Intersection& hit)
{

View file

@ -58,7 +58,8 @@ public:
void setupView();
void resize(int width, int height );
void resize(int width, int height);
void resize(int width, int height, int x, int y);
void update();
@ -116,6 +117,7 @@ protected:
SGPropertyNode_ptr _textures;
SGPropertyNode_ptr _cloud_status, _visibility_m;
SGPropertyNode_ptr _xsize, _ysize;
SGPropertyNode_ptr _xpos, _ypos;
SGPropertyNode_ptr _panel_hotspots, _sim_delta_sec, _horizon_effect, _altitude_ft;
SGPropertyNode_ptr _virtual_cockpit;
SGTimeStamp _splash_time;