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:
parent
1ff0ce2222
commit
31ec727872
10 changed files with 1467 additions and 211 deletions
|
@ -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
395
scripts/python/recordreplay.py
Executable 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue