技术文档

LNAV (FMS lateral navigation) — M5

此页为项目的英文技术文档原文。 docs/LNAV.md

Status 2026-11-07 (build 59): zibomod's FMS navigation translated to C++ (wasm/src/core/lnav/) and checked against the oracle: a route flown with LNAV, 3,566 frames, every input and state global the same (strict: from the oracle's first state on, ours only). In the systems module from build 60 (below).

What it is#

Zibo's Lua FMS (B738.a_fms) builds the route: the legs with their positions, path terminators, restrictions, holds, procedure turns, offsets, and publishes them as datarefs (laminar/B738/fms/legs*, legs_pth, the *_2 / *_3 copies for RTE 2 and the modified route). zibomod (C++) turns them into the path the aircraft flies and the commands the autopilot follows:

  • MainFlightLoopCallback's navigation block (here lnav_frame): reads the route, fills its leg name / path terminator strings, sequences the legs, runs fmc_calc (the active leg, flight phase), calc_next_turn / calc_path (turns, arcs, RF / AF legs, holds, procedure turns, direct-to, intercepts), calc_abeam_bypass_wpt, next_offset / next_offset_seq / next_hold_phase / next_pi_phase, calc_direct_trk;
  • fms_turn (later in the frame): cross-track error and the course / bank to fly (nav_mode, idx_corr, ...), which the AFDS's ap_lnav / ap_lnav_cmd (already translated, afds/) turn into the roll command.

How it is ported#

As the AFDS (docs/AFDS.md): mechanically, then checked against the oracle.

  1. tools/re/decompile.py (Ghidra) over tools/re/lnav_functions.txt (70 functions, 516 KB of code; calc_next_turn alone is 237 KB) and MainFlightLoopCallback.
  2. tools/re/lnav_frame.py: the navigation block of MainFlightLoopCallback (the if (simDR_pause == 0) after PFD_stall_system) as a function of its own, lnav_frame, with the locals it uses.
  3. tools/re/port_cpp.py: the class LnavGen / methods (wasm/src/core/lnav/lnav_gen.*).
  4. tools/re/lnav_fixups.py: zibomod's std::string code (leg names and path terminators are std::string arrays compared with literals: "TF", "DISCONTINUITY", ...) mapped to helpers in lnav.h; the literals read from .rodata; zibomod's debug traces (if (fms_test == 9999.0f)) emptied; packed flag bytes; sign masks; the stack protector's checks.
  5. tools/re/lnav_handles.py: the dataref path of every handle the code reads or writes (wasm/src/core/lnav/lnav_handles.inc, from tools/re/zibomod_handles.json: the strings zibomod passes to XPLMFindDataRef and the globals it keeps the handles in, 3,998 of them).
  6. tools/re/gen_lnav.sh runs it all (about 17 minutes). Generated files are not edited by hand.

decompile.py now also finds the return value of functions with a stack protector (the value is loaded before the canary check, not before the epilogue): get_act_crs / get_next_crs return the course, calc_path / calc_region* / Exit_AF / next_offset / calc_next_turn a flag or index. Without it their callers used "lost" values (Ghidra's extraout_XMM0).

wasm/src/core/lnav/lnav.{h,cpp} (hand-written): the string arrays and helpers, the dataref hooks over a store (the integration gives the Fms's XLua store: the route the Lua FMS publishes, and the values zibomod writes back for it), magnetic variation from the host, and calc_position: zibomod's FMS position blend (GPS / IRS / radio, ANP, VOR / DME auto-tuning) is not translated (Ghidra could not type its station-list string code); the position the navigation uses is the aircraft's, as zibomod's with GPS.

Checked against the oracle (build 59)#

  • The scenario (tools/re/lnav_scenario.py -> xphost/test/sci/lnav_trace.json): airborne at FL100 near KSEA, RTE KSEA / KPDX with five fixes on the CDU, ACTIVATE, EXEC, DIRECT TO the first, LNAV and CMD A; the aircraft moved along the route (scripted; 8 times its ground speed to keep the trace small). The world: our navdata writer (lnav_oracle_world, wasm/test/test_lnav_replay.cpp), the same files for both. Trace names: tools/re/lnav_trace_names.py (zibomod's globals the code reads at the block's start / fms_turn, the datarefs it reads itself, its std::string arrays; its state at their ends). Golden file wasm/test/data/lnav_trace_oracle.txt.gz.
  • The oracle (xphost/src/sci_test.cpp): call-site hooks lnav (after PFD_stall_system), lnav_out (before nd_earth_perf), turn / turn_out (around fms_turn); zibo:s: reads zibomod's std::string globals (libstdc++ layout; the symbols carry the cxx11 ABI tag), str: string datarefs ([strace] lines); doubles at 17 digits.
  • lnav_replay_matches_oracle: each frame the inputs, lnav_frame, the block's state compared, fms_turn's inputs, fms_turn, its state compared; the state resynced (default) or not (LU737_LNAV_STRICT=1: only what zibomod's other code changed between frames is loaded). Both: 0 differences. LU737_LNAV_DEBUG=<frame> (_NAMES=a,b) prints ours and the oracle's.
  • lnav_functions_match_zibomod: the leaf functions against zibomod's own (dlopen), random inputs: distance, bearing, abeam points, intersections, arcs, speeds, turns, Rescale / Limit, calc_corr_nav_4 / _5.
  • Floats within 16 ulps: Ghidra prints zibomod's float sums folded (s = s + t; fmod(a + s + 360) as fmod(a + s + t + 360)) and loses their association, a few ulps (0.0001 degrees of course); the faults below were far larger.

Faults found and fixed (in the tools where they come from, port_cpp.py / lnav_fixups.py, and in the generated files by the same rules):

Fault Effect Fix
simDR_latitude typed long long (its value moved through a general register as an argument) positions wrong by degrees values (simDR_ / B738DR_) never vote "handle"
&legs_s[0][+32] (a byte offset on a std::string array) crash element = offset / 32
(long long)i * 4 + &arr[0] (Ghidra's byte offset, C++ scales again) 130 reads at 4 x the index (rt_abeam_dist_bp, legs_*) the multiplier dropped where it is the element's size
deg_to_rad / rad_to_deg hand-written with a float constant every angle off in the 7th digit zibomod's: pi as 3.141592653589, in double
Rescale / Limit hand-written wrong for reversed ranges the AFDS's translation (oracle-checked)
XPLMGetDatab zero-filled the buffer legs_path / legs_id tails as X-Plane: only the dataref's bytes

Known: get_act_crs is once called with -1 (zibomod reads before its arrays; here the same objects for the string arrays, another member for legs_direct_to); no difference in the trace. calc_position (the FMS position blend) is not translated: the aircraft's position, as zibomod's with GPS.

In the systems module (build 60)#

wasm/src/core/systems_lnav.cpp. Each frame, as zibomod's MainFlightLoopCallback: the AFDS's Glareshield part (Afds::frameEarly), then the navigation (lnav_frame, fms_turn), then frameLate (ap_speed_accel, the flight director). Only while the FMS runs (its route).

zibomod keeps one set of globals; the ports keep a copy each. The links (lnav/lnav_sync.inc, generated by tools/re/lnav_sync.py from the translations, zibomod's registrations and its disassembly):

Link What When
kLnavFromAfds (19) the X-Plane values and AFDS state the navigation reads (ground speed, headings, roll, wind, cmd_roll_mode, ...) before the navigation
kLnavPreReads (43) MainFlightLoopCallback's reads before the block (legs_change, offset, direct-to, hold, TOC / TOD: what the Lua FMS writes) before
position, headings, magnetic variation MSFS: east positive, true - magnetic (Zibo's Lua: magnetic = true - variation) before
the datarefs it reads and writes through its handles (the route fms/legs*, idx_corr, rt_xtrack, ...) the FMS's XLua store; kept locally when the FMS has no such dataref during
kLnavDatarefs (82) zibomod's registered datarefs of the navigation's globals (the path the FMS pages read): the Lua's changes in, the navigation's values out before / after
kLnavToAfds (4) nav_mode, hold_phase, pi_phase, idx_corr5 after fms_turn
kAfdsReads (145) zibomod's Read_B738_DR for the AFDS: the datarefs the Lua FMS creates or the navigation writes (legs_num, end_route, fpln_active, the courses to fly idx_corr / idx_corr2 a frame later, accel height, N1 mode, ...); not what the AFDS writes itself (idx_corr3) nor what afdsInputs gives it before the AFDS

L:LU737_LNAV_MS: the navigation's time per frame. Test lnav_flies_the_route_in_the_systems (test_fms.cpp): airborne heading 180 near KSEA, the route on the CDU, DIRECT TO, FD, LNAV, CMD A: LNAV engaged (roll mode 4), the course to fly 200.6 towards LNVAA, the flight director 8 degrees right.

Found on the way: afds_gen.cpp B738_eng_n1_set passed one of two adjacent stack floats as an array of two (tools/re/afds_fixups.py, now in gen_afds.sh); it ran once the FMS's N1 mode reached the AFDS.

Next#

  1. More scenarios: a SID / STAR with RF / AF legs, holds, an offset, direct-to with an intercept course.
  2. The owner's flight: LNAV in MSFS (the servo flying the flight director's roll).