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 (herelnav_frame): reads the route, fills its leg name / path terminator strings, sequences the legs, runsfmc_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'sap_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.
tools/re/decompile.py(Ghidra) overtools/re/lnav_functions.txt(70 functions, 516 KB of code;calc_next_turnalone is 237 KB) andMainFlightLoopCallback.tools/re/lnav_frame.py: the navigation block ofMainFlightLoopCallback(theif (simDR_pause == 0)afterPFD_stall_system) as a function of its own,lnav_frame, with the locals it uses.tools/re/port_cpp.py: the classLnavGen/ methods (wasm/src/core/lnav/lnav_gen.*).tools/re/lnav_fixups.py: zibomod'sstd::stringcode (leg names and path terminators arestd::stringarrays compared with literals:"TF","DISCONTINUITY", ...) mapped to helpers inlnav.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.tools/re/lnav_handles.py: the dataref path of every handle the code reads or writes (wasm/src/core/lnav/lnav_handles.inc, fromtools/re/zibomod_handles.json: the strings zibomod passes toXPLMFindDataRefand the globals it keeps the handles in, 3,998 of them).tools/re/gen_lnav.shruns 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 filewasm/test/data/lnav_trace_oracle.txt.gz. - The oracle (
xphost/src/sci_test.cpp): call-site hooks lnav (afterPFD_stall_system), lnav_out (beforend_earth_perf), turn / turn_out (aroundfms_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)asfmod(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#
- More scenarios: a SID / STAR with RF / AF legs, holds, an offset, direct-to with an intercept course.
- The owner's flight: LNAV in MSFS (the servo flying the flight director's roll).