diff --git a/src/Viewer/CMakeLists.txt b/src/Viewer/CMakeLists.txt index 95f4d604a..acc1b4422 100644 --- a/src/Viewer/CMakeLists.txt +++ b/src/Viewer/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES WindowSystemAdapter.cxx fg_os_osgviewer.cxx fgviewer.cxx + ViewPropertyEvaluator.cxx splash.cxx view.cxx viewmgr.cxx diff --git a/src/Viewer/ViewPropertyEvaluator.cxx b/src/Viewer/ViewPropertyEvaluator.cxx new file mode 100644 index 000000000..b67059f5e --- /dev/null +++ b/src/Viewer/ViewPropertyEvaluator.cxx @@ -0,0 +1,687 @@ +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include "ViewPropertyEvaluator.hxx" + +#include "Main/globals.hxx" + +#include +#include + + +namespace ViewPropertyEvaluator { + + /* We represent a spec as graph, using alternating Sequence and Node + objects so that different specs share common information; e.g. this ensures + that we don't install more than one listener for the same SGPropertyNode. + + Evaluating top-level nodes: + + Currently ViewPropertyEvaluator::getDoubleValue() will always + reevaluate the top-level SGPropertyNode by calling its getDoubleValue() + member. This usually gives the desired behaviour because most + final property nodes that we are used with don't appear to make + valueChanged() callbacks, and it's anyway probably more efficient to + not use such callbacks for rapidly-changing values. + + However it would be good to be clearer about this, e.g. maybe we could + have a second bracket notation to indicate that we should evaluate and + cache the SGPropertyNode but not its string/double value. E.g.: + + ViewPropertyEvaluator::getDoubleValue( + "{(/sim/view[0]/config/root)/position/altitude-ft}" + ); + + - would not attempt to install a valueChanged() callback for the + top-level SGPropertyNode. +*/ + + struct Sequence; + struct Node; + + struct Sequence + { + Sequence(); + + std::vector> _nodes; + std::vector _parents; + bool _rescan; + std::string _value; + }; + + struct Node : SGPropertyChangeListener + { + explicit Node(const char* spec); + + /* SGPropertyChangeListener callback. */ + void valueChanged(SGPropertyNode* node); + + const char* _begin; + const char* _end; + bool _rescan; + std::string _value; + + std::vector _parents; + + // Only used if _begin.._end is (...). + std::shared_ptr _child; + SGPropertyNode_ptr _sgnode; + SGPropertyNode_ptr _sgnode_listen; + }; + + /* Helper for dumping a Sequence to an ostream. Prefixes all lines with + . If is true, recursively shows all child sequences and + nodes. */ + struct SequenceDump + { + SequenceDump(const Sequence& sequence, const std::string& indent="", bool deep=false); + + const Sequence& _sequence; + const std::string& _indent; + bool _deep; + + friend std::ostream& operator << (std::ostream& out, const SequenceDump& self); + }; + + /* Helper for dumping a Node to an ostream. Prefixes all lines with + . If is true, recursively shows all child sequences and + nodes. */ + struct NodeDump + { + NodeDump(const Node& node, const std::string& indent="", bool deep=false); + + const Node& _node; + const std::string& _indent; + bool _deep; + + friend std::ostream& operator << (std::ostream& out, const NodeDump& self); + }; + + /* Support for debug statistics. */ + struct Debug + { + /* Support for tracking how many property system accesses we are making. */ + struct Stat + { + Stat() : n(0) {} + int n; + }; + + /* Increments counter for . Periodically outputs stats with + SG_LOG(SG_VIEW, SG_DEBUG, ...) and detailed information about Sequences + and Nodes with SG_LOG(SG_VIEW, SG_BULK, ...). */ + void statsAdd(const char* name); + void statsReset(); + struct StatsShow {}; + friend std::ostream& operator << (std::ostream& out, const StatsShow&); + + /* Track how many listeners we have. */ + void listensAdd(SGPropertyNode_ptr node); + void listensRemove(SGPropertyNode_ptr node); + + time_t statsT0 = 0; + std::map> stats; + std::vector listens; + }; + + Debug debug; + + /* Forces this node and all of its sequence and node parents to be + re-read the next time they are evaluated - e.g. the next call of + getNodeStringValue() will call getSequenceStringValue() on _child and + write the result into the <_value> member before returning <_value>. */ + void rescanNode(Node& node); + + /* Forces this sequence and all of its node and sequence parents to + be re-read the next time they are evaluated - e.g. the next call of + getSequenceStringValue() will call getNodeStringValue() on each child + node and concatenate the results into the <_value> member before + returning <_value>. */ + void rescanSequence(Sequence& sequence); + + /* Returns Sequence for spec starting at , which can be subsequently + used to evaluate the spec efficiently. We require that the string + will remain unchanged forever. */ + std::shared_ptr getSequence(const char* spec); + + /* Returns evaluated spec for using caching. Mainly used + internally. + + If node spec is of the form "<...>", we look up the value in the + property system. */ + const std::string& getNodeStringValue(Node& node); + + /* Returns evaluated spec for using caching. Mainly used + internally. Concatenates the string value of each child node. */ + const std::string& getSequenceStringValue(Sequence& sequence); + + /* must be from a spec with a top-level '(...)'. Returns the + specified property-tree node. */ + SGPropertyNode* getSequenceNode(Sequence& sequence); + + double getSequenceDoubleValue(Sequence& sequence, double default_=0); + + /* Finds end of section of spec starting at , to be used as the + region of a Sequence; this will contain one or more regions corresponding + to a Node. Any ')' without a corresponding '(' is treated as a terminator. + */ + const char* getSequenceEnd(const char* spec) + { + int nesting = 0; + for (const char* s=spec; ; ++s) { + if (*s == 0) { + assert(nesting == 0); + return s; + } + if (*s == '(') { + nesting += 1; + } + if (*s == ')') { + if (nesting == 0) { + return s; + } + nesting -= 1; + } + } + } + + /* Finds end of section of spec starting at , to be used as the + region of a Node. + + We parse things similarly to getSequenceEnd() except that we also terminate + early in the following situations: + + The first character is not '(' and we find a '('. + + The first character is '(' and we find the corresponding ')'. + */ + const char* getNodeEnd(const char* spec) + { + int nesting = 0; + for (const char* s=spec; ; ++s) { + if (*s == 0) { + assert(nesting == 0); + return s; + } + if (*s == '(') { + if (spec[0] != '(') { + return s; + } + nesting += 1; + } + if (*s == ')') { + if (nesting == 0) { + return s; + } + nesting -= 1; + if (spec[0] == '(' && nesting == 0) { + return s + 1; + } + } + } + } + + /* Raw constructor. We only set up basic values; the rest is done + by getNodeInternal(). */ + Node::Node(const char* spec) + : + _begin(spec), + _end(getNodeEnd(_begin)), + _rescan(true) + { + } + + void Node::valueChanged(SGPropertyNode* node) + { + debug.statsAdd("valueChanged"); + SG_LOG(SG_VIEW, SG_DEBUG, "valueChanged():" + << " node->_sgnode_listen->getPath()='" << _sgnode_listen->getPath() << "'" + << " node->getPath()='" << node->getPath() << "'" + ); + rescanNode(*this); + } + + Sequence::Sequence() + : + _rescan(true) + { + } + + void rescanNode(Node& node) + { + node._rescan = true; + for (auto sequence: node._parents) { + rescanSequence(*sequence); + } + } + + void rescanSequence(Sequence& sequence) + { + sequence._rescan = true; + for (auto node: sequence._parents) { + rescanNode(*node); + } + } + + /* Caches of various structures so that we can look things up quickly. + */ + + /* This is the main cache. It uses raw C string pointers (pointing + to specs) as keys, so lookups only require a handful of pointer + comparisons. */ + std::map> spec_to_sequence; + + /* These are only used when parsing new specs and creating new nodes and + sequencies, so are not speed-critical. */ + std::map> string_to_sequence; + std::map> string_to_node; + + + /* Finds or creates new Sequence for (possibly initial) portion of . + */ + std::shared_ptr getSequenceInternal(const char* spec, Node* parent); + + /* Finds or creates new Node for (possibly initial) portion of . + */ + std::shared_ptr getNodeInternal(const char* spec, Sequence* parent); + + std::shared_ptr getSequenceInternal(const char* spec, Node* parent) + { + if (spec[0] == 0 || spec[0] == ')') { + return NULL; + } + + std::shared_ptr sequence; + + std::string spec_string(spec, getSequenceEnd(spec)); + auto it = string_to_sequence.find(spec_string); + if (it == string_to_sequence.end()) { + sequence.reset(new Sequence); + for(const char* s = spec;;) { + std::shared_ptr node + = getNodeInternal(s, sequence.get() /*parent*/); + if (!node) break; + sequence->_nodes.push_back(node); + s += (node->_end - node->_begin); + } + string_to_sequence[spec_string] = sequence; + } + else { + sequence = it->second; + } + + if (parent) { + auto it = std::find( + sequence->_parents.begin(), + sequence->_parents.end(), + parent + ); + if (it == sequence->_parents.end()) { + sequence->_parents.push_back(parent); + } + } + return sequence; + } + + std::shared_ptr getNodeInternal(const char* spec, Sequence* parent) + { + if (spec[0] == 0 || spec[0] == ')') { + return NULL; + } + + std::shared_ptr node; + + std::string s(spec, getNodeEnd(spec)); + auto it = string_to_node.find(s); + + if (it == string_to_node.end()) { + node.reset(new Node(spec)); + if (node->_begin[0] == '(') { + node->_child = getSequenceInternal(node->_begin + 1, node.get() /*parent*/); + } + string_to_node[s] = node; + } + else { + node = it->second; + } + + if (parent) { + if (std::find(node->_parents.begin(), node->_parents.end(), parent) + == node->_parents.end()) { + node->_parents.push_back(parent); + } + } + + return node; + } + + /* Finds or creates new Sequence for (possibly initial) portion of + and sets spec_to_sequence[spec] so that it can be quickly looked up in + future. */ + std::shared_ptr getSequence(const char* spec) + { + auto it = spec_to_sequence.find(spec); + if (it != spec_to_sequence.end()) { + return it->second; + } + + std::shared_ptr sequence + = getSequenceInternal(spec, NULL /*parent*/); + spec_to_sequence[spec] = sequence; + SG_LOG(SG_VIEW, SG_DEBUG, + "Created new sequence:\n" + << SequenceDump(*sequence, " ", true /*deep*/) + ); + return sequence; + } + + /* Evaluates to find path and returns corresponding SGPropertyNode* + in global properties. If is true, we install a listener on + the returned node, so that we force a rescan of all the node's parent + Sequence's if its value changes. */ + SGPropertyNode* getNodeSGNode(Node& node, bool cache=true) + { + assert(node._begin[0] == '('); + assert(node.child); + if (node._rescan) { + node._rescan = false; + if (!node._sgnode || node._child->_rescan) { + const std::string& path = getSequenceStringValue(*node._child); + SGPropertyNode* sgnode = NULL; + if (path != "") { + debug.statsAdd( "propertypath_getNode"); + sgnode = globals->get_props()->getNode(path); + if (!sgnode) { + debug.statsAdd( "propertypath_getNode_failed"); + SG_LOG(SG_VIEW, SG_DEBUG, ": getNodeSGNode(): getNode() failed, path='" << path << "'"); + } + } + if (sgnode != node._sgnode) { + if (node._sgnode_listen) { + node._sgnode_listen->removeChangeListener(&node); + debug.listensRemove(node._sgnode_listen); + node._sgnode_listen = NULL; + } + if (node._sgnode) { + node._sgnode->removeChangeListener(&node); + } + node._sgnode = sgnode; + if (node._sgnode && cache) { + node._sgnode_listen = node._sgnode; + node._sgnode->addChangeListener(&node, false /*initial*/); + debug.listensAdd(node._sgnode); + } + } + if (!node._sgnode && path != "") { + /* Ideally we'd ask Simgear's property system to call us + back if was created, but this is non-trivial, and + not actually required when we are used by view.cxx. */ + } + } + } + return node._sgnode; + } + + const std::string& getNodeStringValue(Node& node) + { + if (node._rescan) { + if (node._begin[0] == '(') { + getNodeSGNode(node); + if (node._sgnode) { + debug.statsAdd( "property_getStringValue"); + node._value = node._sgnode->getStringValue(); + } + else { + node._value = ""; + } + } + else { + node._rescan = false; + node._value = std::string(node._begin, node._end); + } + } + + return node._value; + } + + const std::string& getSequenceStringValue(Sequence& sequence) + { + if (sequence._rescan) { + sequence._rescan = false; + sequence._value = ""; + for (auto node: sequence._nodes) { + sequence._value += getNodeStringValue(*node); + } + } + return sequence._value; + } + + /* Assumes that sequence has a single child Node object with a non-empty + Sequence child object whose value is a path in the property system. */ + SGPropertyNode* getSequenceNode(Sequence& sequence) + { + assert(sequence._nodes.size() == 1); + Node& node = *sequence._nodes.front(); + return getNodeSGNode(node, false /*cache*/); + } + + /* This is different from getSequenceStringValue() in that it assumes that + a spec has a single top-level (...) and so the top-level sequence has a + single child Node object which in turn has a Sequence child object whose + value is a path in the property system. + + We always call the SGPropertyNode's getDoubleValue() method - we don't + cache the double value. But the underlying SGPropertyNode* will be cached. + */ + double getSequenceDoubleValue(Sequence& sequence, double default_) + { + SGPropertyNode* node = getSequenceNode(sequence); + if (node) { + return node->getDoubleValue(); + } + return default_; + } + + const std::string& getStringValue(const char* spec) + { + std::shared_ptr sequence = getSequence(spec); + return getSequenceStringValue(*sequence); + } + + double getDoubleValue(const char* spec, double default_) + { + std::shared_ptr sequence = getSequence(spec); + if (sequence->_nodes.size() != 1 || sequence->_nodes.front()->_begin[0] != '(') { + SG_LOG(SG_VIEW, SG_DEBUG, "bad sequence for getDoubleValue() - must have outermost '(...)': '" << spec); + abort(); + } + double ret = getSequenceDoubleValue(*sequence, default_); + return ret; + } + + std::ostream& operator << (std::ostream& out, const Dump& dump) + { + out << "ViewPropertyEvaluator\n"; + out << " Number of specs: " << spec_to_sequence.size() << ":\n"; + int i = 0; + for (auto it: spec_to_sequence) { + out << " " << (i+1) << "/" << spec_to_sequence.size() << ": spec: " << it.first << "\n"; + out << SequenceDump(*it.second, " ", true /*deep*/); + i += 1; + } + out << " Number of sequences: " << string_to_sequence.size() << "\n"; + i = 0; + for (auto it: string_to_sequence) { + out << " " << (i+1) << "/" << string_to_sequence.size() + << ": spec='" << it.first << "'" + << ": " << SequenceDump(*it.second, "", false /*deep*/) + ; + i += 1; + } + out << " Number of nodes: " << string_to_node.size() << "\n"; + i = 0; + for (auto it: string_to_node) { + out << " " << (i+1) << "/" << string_to_node.size() + << ": spec='" << it.first << "'" + << ": " << NodeDump(*it.second, "", false /*deep*/) + ; + i += 1; + } + out << " Number of listens: " << debug.listens.size() << "\n"; + i = 0; + for (auto it: debug.listens) { + out << " " << (i+1) << "/" << debug.listens.size() + << ": " << it->getPath() + << "\n"; + i += 1; + } + return out; + } + + void clear() + { + spec_to_sequence.clear(); + string_to_sequence.clear(); + string_to_node.clear(); + + debug = Debug(); + } + + /* === Everything below here is for diagnostics and/or debugging. */ + + SequenceDump::SequenceDump(const Sequence& sequence, const std::string& indent, bool deep) + : + _sequence(sequence), + _indent(indent), + _deep(deep) + {} + + NodeDump::NodeDump(const Node& node, const std::string& indent, bool deep) + : + _node(node), + _indent(indent), + _deep(deep) + {} + + + void Debug::listensAdd(SGPropertyNode_ptr node) + { + debug.listens.push_back(node); + } + + void Debug::listensRemove(SGPropertyNode_ptr node) + { + auto it = std::find(debug.listens.begin(), debug.listens.end(), node); + if (it == debug.listens.end()) { + SG_LOG(SG_VIEW, SG_ALERT, "Unable to find node in debug.listens"); + } + else { + debug.listens.erase(it); + } + } + + std::ostream& operator << (std::ostream& out, const Debug::StatsShow&) + { + time_t t = time(NULL); + time_t dt = t - debug.statsT0; + if (dt == 0) dt = 1; + out << "ViewPropertyEvaluator stats: dt=" << dt << "\n"; + for (auto it: debug.stats) { + const std::string& name = it.first; + int n = it.second->n; + out << " : n=" << n << " n/sec=" << (1.0 * n / dt) << ": " << name << "\n"; + } + return out; + } + + void Debug::statsReset() { + for (auto it: debug.stats) { + it.second->n = 0; + } + debug.statsT0 = time(NULL); + } + + void Debug::statsAdd(const char* name) { + if (debug.statsT0 == 0) debug.statsT0 = time(NULL); + std::shared_ptr& stat = debug.stats[name]; + if (!stat) { + stat.reset(new(Debug::Stat)); + } + stat->n += 1; + + if (1) { + static time_t t0 = time(NULL); + time_t t = time(NULL); + if (t - t0 > 10) { + t0 = t; + SG_LOG(SG_VIEW, SG_DEBUG, StatsShow()); + statsReset(); + + /* Output all specs with SG_BULK. */ + SG_LOG(SG_VIEW, SG_BULK, Dump()); + } + } + } + + std::ostream& operator << (std::ostream& out, const SequenceDump& self) + { + std::string spec; + for (auto node: self._sequence._nodes) { + spec += std::string(node->_begin, node->_end); + } + + out << self._indent + << "Sequence at " << &self._sequence << ":" + << " _rescan=" << self._sequence._rescan + << " _parents.size()=" << self._sequence._parents.size() + << " _nodes.size()=" << self._sequence._nodes.size() + << " _value='" << self._sequence._value << "'" + << " spec='" << spec << "'" + << "\n"; + if (self._deep) { + for (auto node: self._sequence._nodes) { + out << NodeDump(*node, self._indent + " ", self._deep); + } + } + return out; + } + + std::ostream& operator << (std::ostream& out, const NodeDump& self) + { + out << self._indent + << "Node at " << &self._node << ":" + << " _rescan=" << self._node._rescan + << " _parents.size()=" << self._node._parents.size() + << " _child=" << self._node._child.get() + << " _value='" << self._node._value << "'" + << " _sgnode=" << self._node._sgnode + ; + if (self._node._sgnode) { + out + << " (path=" << self._node._sgnode->getPath() + << ", value=" << self._node._sgnode->getStringValue() + << ")"; + } + out + << " _begin.._end='" << std::string(self._node._begin, self._node._end) << "'" + << "\n"; + if (self._deep) { + if (self._node._child) { + out << SequenceDump(*self._node._child, self._indent + " ", self._deep); + } + } + return out; + } + +} diff --git a/src/Viewer/ViewPropertyEvaluator.hxx b/src/Viewer/ViewPropertyEvaluator.hxx new file mode 100644 index 000000000..c9ab3ee29 --- /dev/null +++ b/src/Viewer/ViewPropertyEvaluator.hxx @@ -0,0 +1,120 @@ +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#pragma once + +#include "simgear/props/props.hxx" + +#include +#include +#include +#include + + +namespace ViewPropertyEvaluator { + + /* + Overview: + + We provide efficient evaluation of 'nested properties', where the path + of the property to be evaluated is determined by the value of other, + possibly dynamically-changing, properties. + + Details: + + Code is expected to specify a nested property as a C string 'spec' + using (...) to denote evaluation of a substring as a property path + where required. We use the raw address of C string specs as keys in an + internal std::map, to provide fast lookup of previously-created specs + using just a small number of raw pointer comparisons. This requires + that the C string specs remain valid and unchanged; typically they will + be immediate strings specifed directly in source code. + + We set up listeners to detect changes to relevant property nodes so + that we can force re-evaluation of dependent nested properties when + required. Thus most lookups end up not touching the property system at + all, and instead access cached values directly. + + For example this: + + SGPropertyNode* node = ViewPropertyEvaluator::getDoubleValue( + "((/sim/view[0]/config/root)/position/altitude-ft)" + ); + + - will behave like this: + + globals->get_props()->getDoubleValue( + globals->get_props()->getStringValue("/sim/view[0]/config/root") + + "/position/altitude-ft" + ); + + In this example, an internal listener will be set up to detect changes + to property '/sim/view[0]/config/root'; if this listener has not fired, + we will use a cached SGPropertyNode* directly without querying the + property tree. + + Note that with ViewPropertyEvaluator::getDoubleValue, while the final + SGPropertyNode* is cached, its value is not cached. Instead we always + call SGPropertyNode::getDoubleValue(). This is in anticipation of + typical usage where the final double values will change frequently, so + caching its value becomes less useful. + + Missing property nodes: + + If a property node does not exist with the required path, we use "" as + the value. + + We do not attempt to detect the creation of such property nodes, + because simgear's doesn't really support this. Typically the path will + depend on other properties which do exist, and which change when the + missing property node is created, in which case we will re-evaluate + correctly. + */ + + + /* Evaluates a spec as a string. The returned reference will be valid for + ever (until ViewPropertyEvaluator::clear() is called); its value will be + unchanged until the next time the spec (or a spec that depends on it) is + evaluated. + + For example, getStringValue("/sim/chase-distance-m") will return + "/sim/chase-distance-m", while getStringValue("(/sim/chase-distance-m)") + will return "-25" or similar, depending on the aircraft. + */ + const std::string& getStringValue(const char* spec); + + /* Evaluates a spec as a double. Only makes sense if has top-level + "(...)". + + For example, getDoubleValue("(/sim/chase-distance-m)") will return -25.0 or + similar, depending on the aircraft. + + When this function is used, it doesn't install a listener for the top-level + node, instead it always calls ->getDoubleValue(). + */ + double getDoubleValue(const char* spec, double default_=0); + + + /* Outputs detailed information about all specs that have been seen. + + E.g.: + SG_LOG(SG_VIEW, SG_DEBUG, "ViewPropertyEvaluator:\n" << ViewPropertyEvaluator::Dump()); + */ + struct Dump {}; + std::ostream& operator << (std::ostream& out, const Dump& dump); + + /* Clears all internal state. */ + void clear(); + +}