Projektziel: Das Garmin G1000 in FlightGear (FG1000) lief bei mir so zäh, dass die Cessna im Flug mit 1 bis 2 Bildern pro Sekunde kaum noch steuerbar war. Mit einem kleinen Messwerkzeug in Nasal habe ich gesucht, wo die Rechenzeit hingeht, und zwei Fehler im FG1000 gefunden. Zwei kleine Änderungen machen das G1000 deutlich flüssiger: im Reiseflug 18,5 bis 35 statt 11 Bilder pro Sekunde, also etwa so flüssig wie die Cessna ohne G1000; parkend etwa 18 statt 10. Unten gibt es das Patch-Skript zum Herunterladen und die Änderungen zum Nachmachen von Hand.

G1000 in der Cessna 172 im Reiseflug: links das PFD mit den magentafarbenen Flight-Director-Balken, rechts das MFD mit Karte und GPS-Kurs
Bild 1 - G1000 der Cessna 172 im Reiseflug nach dem Patch: links das PFD mit den magentafarbenen Flight-Director-Balken, rechts das MFD mit Karte und GPS-Kurs nach Leipzig

Download: fg1000_patch.zip (Python 3, getestet mit FlightGear 2024.1.7 und der Cessna c172p-fg1000-gfc)

Ausgangslage

Gemessen habe ich auf derselben Strecke (Erfurt nach Leipzig, Autopilot mit GPS-Kurs) und an derselben Stelle auf dem Vorfeld, mit gleichen Grafikeinstellungen. Die normale Cessna 172 mit Rundinstrumenten schafft im Flug im Schnitt 30 Bilder pro Sekunde. Die gleiche Cessna mit G1000 kam auf 10 Bilder pro Sekunde am Boden und im Flug nur noch auf 1 bis 2. Die Grafikkarte war dabei kaum ausgelastet.

Das ist typisch für FlightGear: Flugphysik, die Nasal-Skripte der Flugzeuge und das Zeichnen der Glas-Displays (Canvas) laufen nacheinander auf einem einzigen Prozessorkern. Ist dieser Kern voll, wartet die Grafikkarte. Eine schnellere Grafikkarte hilft dann nicht, weniger Arbeit auf dem Hauptkern schon.

Wie das FG1000 seine Daten verteilt

Das FG1000 ist fast vollständig in Nasal geschrieben, der Skriptsprache von FlightGear. Die Displayseiten lesen die Werte nicht selbst aus dem Property-Baum. Sogenannte Publisher sammeln die Werte (Fahrt, Höhe, Frequenzen, Autopilot-Modi …) und verschicken sie als Nachricht über Emesary, das Nachrichtensystem von FlightGear. Jede Nachricht geht an alle angemeldeten Empfänger, beim G1000 der Cessna sind das gut 20: die Controller von PFD und MFD, die Karte, die Motoranzeige, die Direct-To-Seite und so weiter.

Es gibt zwei Arten von Publishern:

  • PeriodicPropertyPublisher prüft die Werte in festen Abständen (zum Beispiel 10-mal pro Sekunde) und schickt nur, was sich um mehr als eine Toleranz geändert hat.
  • TriggeredPropertyPublisher hängt an jeden Wert einen Zuhörer (setlistener) und schickt eine Nachricht, sobald sich der Wert ändert. Das ist für Werte gedacht, die sich selten ändern, etwa Frequenzen oder Autopilot-Modi.

Messen: Rechenzeit pro Empfänger

Um zu sehen, wer die Zeit verbraucht, habe ich ein kleines Nasal-Skript geschrieben. Es legt sich um die Receive-Funktion jedes Emesary-Empfängers, misst die Zeit mit maketimestamp() und schreibt alle 20 Sekunden eine Rangliste ins Log (Millisekunden pro Sekunde und Nachrichten pro Sekunde):


foreach (var r; emesary.GlobalTransmitter.Recipients) {
    r._orig = r.Receive;
    r.Receive = func(n) {
        var ts = maketimestamp();
        ts.stamp();
        var res = call(me._orig, [n], me);
        var key = me.Ident ~ " | " ~ n.NotificationType ~ " " ~ n.Event_Id;
        zeit[key] = (zeit[key] or 0) + ts.elapsedUSec();
        anzahl[key] = (anzahl[key] or 0) + 1;
        return res;
    };
}

Das Ergebnis bei der parkenden Cessna war eindeutig: rund 970 Nachrichten pro Sekunde, die zusammen 250 ms Rechenzeit pro Sekunde kosteten, also ein Viertel der gesamten Zeit nur für das Verteilen. Bei 6 Bildern pro Sekunde sind das über 150 Nachrichten pro Bild, und jede geht an 20 Empfänger.

Empfänger · NachrichtenartRechenzeitNachrichten
PFD-Controller · FMS-Daten58 ms/s483 /s
PFD-Controller · Funk/Navigation55 ms/s1468 /s
MFD-Umgebung · FMS-Daten40 ms/s483 /s
MFD-Umgebung · Funk/Navigation15 ms/s1468 /s
… 20 weitere Empfängerje 2–9 ms/sje 241–734 /s

Danach habe ich gezählt, welche Werte in den Nachrichten stehen. Zehn Werte wurden jeweils 240-mal pro Sekunde verschickt: Name, Typ und Flughafen der Funkstation für COM1 und COM2 sowie „Autopilot an“ und „Flight Director an“. Also genau die Werte, die sich im Flug praktisch nie ändern.

Fehler 1: „bei jedem Schreiben“ statt „bei Änderung“

Der TriggeredPropertyPublisher in Interfaces/PropertyPublisher.nas meldet seine Zuhörer so an:


# Set up a listener triggering on create (to ensure all values are set at
# start of day) and only on changed values.  These are the last two
# arguments to the setlistener call.
var listener = setlistener(propmap.getPropPath(), func(p) { me.publish(p); }, 1, 1);

Der Kommentar sagt „nur bei geänderten Werten“. Der vierte Parameter von setlistener bedeutet aber:

WertZuhörer wird aufgerufen …
0nur wenn sich der Wert wirklich ändert
1 (Standard)bei jedem Schreiben, auch mit demselben Wert
2wie 1, zusätzlich bei Änderungen an Kind-Knoten

Die Funkgeräte und der Autopilot schreiben ihre Werte in jedem Simulationsschritt neu, oft mehrmals pro Bild, auch wenn sich nichts ändert. Mit 1 löst jedes dieser Schreiben eine Nachricht an alle Displays aus. Die Prüfung hasChanged() im Publisher hilft auch nicht: Ohne Toleranz gibt sie immer „geändert“ zurück. Die Korrektur ist eine Ziffer:


var listener = setlistener(propmap.getPropPath(), func(p) { me.publish(p); }, 1, 0);

Damit nichts verloren geht, schickt der Publisher ohnehin alle 5 Sekunden alle Werte noch einmal komplett (publishAll). Nach der Änderung brauchte das Verteilen parkend nur noch 9 ms statt 250 ms pro Sekunde, und die Bildrate stieg von 10 auf 15 bis 21.

Fehler 2: Flight-Director-Sollwerte bei jeder Änderung

Im Flug blieb es aber bei 11 Bildern pro Sekunde. Auffällig: Die Bildrate fiel genau in dem Moment von 22 auf 11, in dem der Autopilot in den GPS-Modus schaltete. Die Messung im Reiseflug zeigte zwei weitere Werte mit jeweils 240 Nachrichten pro Sekunde: AutopilotTargetPitch und AutopilotTargetRoll, die Sollwerte für die Flight-Director-Balken im PFD (die magentafarbenen Balken in Bild 1).

Diese beiden Werte ändern sich tatsächlich: Der Autopilot rechnet sie in jedem Regelschritt neu, sobald er aktiv lenkt. Hier hilft der Zuhörer-Typ also nicht. Für die Anzeige der Balken reichen aber 10 Aktualisierungen pro Sekunde völlig. In Interfaces/GFC700Publisher.nas kommen die beiden Werte deshalb in einen eigenen periodischen Publisher (10-mal pro Sekunde, Toleranz 0,2 Grad):


  # vorher:
  # obj.addPropMap("AutopilotTargetPitch", "/autopilot/settings/target-pitch-deg");
  # obj.addPropMap("AutopilotTargetRoll", "/autopilot/settings/target-roll-deg");
  obj._fdPublisher = PeriodicPropertyPublisher.new(notifications.PFDEventNotification.FMSData, 0.1);
  obj._fdPublisher.addPropMap("AutopilotTargetPitch", "/autopilot/settings/target-pitch-deg", 0.2);
  obj._fdPublisher.addPropMap("AutopilotTargetRoll", "/autopilot/settings/target-roll-deg", 0.2);

Weil der GFC700-Publisher sonst nur den Triggered-Publisher startet, bekommt er eigene start- und stop-Funktionen, die beide starten:


start : func() {
  call(TriggeredPropertyPublisher.start, [], me);
  me._fdPublisher.start();
},

stop : func() {
  call(TriggeredPropertyPublisher.stop, [], me);
  me._fdPublisher.stop();
},

Ergebnis

Gemessen im Reiseflug auf 3500 ft mit GPS-Autopilot, Bildrate alle 5 Sekunden aufgezeichnet:

Cessna 172 im ReiseflugBilder pro SekundeVerteilen der Nachrichten
G1000 ursprünglich, mit Karte1–2–
G1000 ursprünglich, Karte aus11nicht gemessen (parkend 250 ms/s)
nach Fehler 111–12≈ 120 ms/s
nach Fehler 1 und 2, Karte an (Flug 1)18,5 (12–23)8 ms/s
nach Fehler 1 und 2, Karte an (Flug 2)30,6 (23–34)nicht gemessen
zum Vergleich: Cessna ohne G100030–

Zwischen zwei Flügen mit demselben Patch schwankt die Bildrate deutlich (18,5 und 30,6 im Mittel). Beim ersten Flug lief auf dem Rechner nebenher einiges andere; FlightGear teilt sich den Prozessor mit allen anderen Programmen. Im zweiten Flug war die G1000-Cessna etwa so schnell wie die Cessna ohne G1000. Die OpenStreetMap-Hintergrundkarte im MFD, die ich vorher abgeschaltet hatte, kostet jetzt kaum noch etwas.

Und die Grafikeinstellungen? Im zweiten Flug habe ich im Reiseflug nacheinander Schatten, dynamisches Licht, 3D-Wolken und die halbe Detailreichweite abgeschaltet (je 100 Sekunden Messung) und danach alles wieder eingeschaltet: 30,6 → 31,3 → 33,8 → 36,1 → 36,6 → wieder alles an: 35,2 Bilder pro Sekunde. Der Anstieg kam also nicht von den Einstellungen, sondern vom Flug selbst (anderes Gelände, fertig geladene Landschaft). Weil der Prozessor der Engpass ist, bringen diese Einstellungen hier höchstens 1 bis 3 Bilder pro Sekunde. Nach dem Patch habe ich die ganze Strecke mit dem Autopiloten geflogen (Steigflug mit VS, Höhe einfangen, GPS-Kurs, Landekurs und Gleitpfad): Alle Anzeigen und Modi funktionieren wie vorher.

Nachtrag: Sturzflug beim Einfangen des Gleitpfads

Bei den Testflügen fiel noch etwas anderes auf: Sobald der Autopilot im Anflug den Gleitpfad einfing, sackte die Cessna jedes Mal rund 200 ft durch. Die Aufzeichnung zeigt, was passiert: Die Nase geht in zwei Sekunden von +5,6° auf −16,7°, die Cessna sinkt kurz mit bis zu 2700 ft/min und fängt sich erst nach etwa 30 Sekunden.

Im GS-Modus regelt der GFC700 die Steigrate auf einen Sollwert, den der ILS-Empfänger von FlightGear berechnet (/instrumentation/nav[0]/gs-rate-of-climb). Ein Test mit zehn Messungen pro Sekunde zeigte: Im ersten Rechenschritt mit gültigem Gleitpfad liefert der Empfänger +56 865 ft/min, danach +1117 ft/min und erst dann sinnvolle Werte um −200 ft/min. Der Regler zieht also zuerst kräftig hoch und schießt danach in den Sturzflug über.

Der Empfänger ist in C++ geschrieben, der Autopilot aber in XML. Dort lässt sich der Sollwert abfangen, bevor er beim Regler ankommt: ein Filter begrenzt ihn auf −1500 bis +300 ft/min und lässt ihn sich höchstens um 4 ft/s pro Sekunde ändern. Der GS-Regler bekommt den gefilterten Wert:


<filter>
  <name>GS-Sollsteigrate NAV1 begrenzt</name>
  <type>noise-spike</type>
  <input>
    <prop>/instrumentation/nav[0]/gs-rate-of-climb</prop>
    <min>-25.0</min>   <!-- ft/s, also -1500 ft/min -->
    <max>5.0</max>     <!-- +300 ft/min -->
  </input>
  <output><prop>/autopilot/internal/gs-rate-of-climb-nav0</prop></output>
  <max-rate-of-change>4.0</max-rate-of-change>
</filter>

<!-- im GS-Regler: -->
<reference>
  <prop>/autopilot/internal/gs-rate-of-climb-nav0</prop>
</reference>

Ergebnis im nächsten Flug: Beim Einfangen bleibt die Nase zwischen −0,8° und +3,5°, die Cessna geht ruhig mit rund −240 ft/min in den Sinkflug. Die Änderung steckt ebenfalls im Patch-Skript (Datei GFC700.xml).

Selbst ausprobieren

Mit dem Skript: fg1000_patch.zip herunterladen, entpacken und fg1000_patch.py mit dem fgdata-Ordner aufrufen (das ist der Ordner, in dem die Datei version liegt):


python fg1000_patch.py "C:\Pfad\zu\fgdata_2024_1"               # Patch einspielen
python fg1000_patch.py "C:\Pfad\zu\fgdata_2024_1" --ohne-karte  # zusätzlich OSM-Karte aus
python fg1000_patch.py "C:\Pfad\zu\fgdata_2024_1" --zurueck     # Originale zurück

Das Skript sichert jede Datei vorher als .orig, ändert nur die beschriebenen Stellen und macht nichts, wenn es sie nicht genau so findet (zum Beispiel in einer anderen FlightGear-Version). Ein Update von fgdata überschreibt die Änderungen; dann das Skript einfach noch einmal ausführen.

Von Hand: In fgdata/Aircraft/Instruments-3d/FG1000/Nasal/Interfaces/ in PropertyPublisher.nas die 1, 1); am Ende der setlistener-Zeile durch 1, 0); ersetzen, in GFC700Publisher.nas die Änderungen von oben übernehmen und in FG1000/GFC700.xml den Filter vor die beiden GS-Regler setzen. Schon die erste Änderung allein bringt am Boden fast die doppelte Bildrate.

Fazit

Beide Fehler stecken in wenigen Zeilen, kosten aber zusammen mehr Zeit als alle Displays. Geraten hätte ich zuerst auf das Zeichnen der Karte; die Messung pro Empfänger hat stattdessen nach wenigen Minuten auf die Nachrichten gezeigt. Das Muster ist allgemein: Ein Zuhörer, der „bei jedem Schreiben“ statt „bei Änderung“ feuert, fällt beim Programmieren nicht auf, solange der beobachtete Wert selten geschrieben wird.