技术文档

M5c FMS / CDU survey

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

Survey of 2026-11-05 (read-only research for the M5c architecture decision, docs/M5C_PLAN.md). The scratchpad tools it names were temporary; the benchmark tools kept are in tools/fms_spike/. : Zibo's Lua FMS and how to bring it to MSFS 2024

Read-only survey. Nothing in /home/user/levelups-737 was changed. Every experiment ran in the scratchpad: symlinked copies of the aircraft folder for the oracle, synthetic navdata, and a Python venv with lupa 2.8, which bundles PUC Lua 5.1, PUC Lua 5.3 and LuaJIT 2.1. fengari 0.1.4 was installed from npm. Paths below are relative to the repository root. a_fms means plugins/xlua/scripts/B738.a_fms/B738.a_fms.lua.

Scratchpad tools (all under /tmp/claude-0/-home-user-levelups-737/d3160648-1f74-51b2-9cba-e42b1a6ad1e2/scratchpad/tools/):

  • api.py, xref.py: XLua API extraction and cross-reference against tools/re/zibomod_*.json.
  • harness.lua: a minimal XLua host written in Lua (datarefs, commands, timers).
  • compat53.lua, io_shim.lua: the Lua 5.1 compatibility shims for a 5.3 VM.
  • scenario.lua: a CDU script. It powers the FMC, enters a route through key commands, fills PERF INIT and executes.
  • run_scen.py (lupa), run_fengari.js (node): drivers for the scenario.
  • cdu_probe.py: drives the real oracle through the replay protocol and reads the CDU lines.
  • gen_nav.py: writes synthetic X-Plane navdata.

0. Summary#

  1. The FMS is mostly Zibo's Lua, but not only. a_fms has 172,900 lines and 1,383 functions. It runs almost entirely in its per-frame after_physics (171531–172838). It also contains things that are not FMS:

    • MMR and nav radio tuning
    • parts of the IRS
    • EFB options and config files
    • datalink / CPDLC
    • chocks and GPU
    • flight-control axes and doors

    The lateral path geometry is computed in zibomod, not in Lua. These functions are not yet translated: calc_next_turn (238 KB of x86 code), fms_turn, calc_path, ap_vnav, vnav_calc_speed and vnav_init. The Lua writes the laminar/B738/fms/legs_* arrays, and zibomod computes the turn segments from them (legs_seg*, dir_seg*) plus the LNAV/VNAV commands. Every option (A, A-js, B) still needs these zibomod functions translated with the existing tools/re pipeline. That is about 350 KB of code, roughly 22k generated lines at the AFDS ratio.

  2. Lua compatibility is very good. The FMS scripts use no bit, no ffi, no goto, no pairs/ipairs, no unpack, no loadstring, no require, no coroutines and no metatables of their own.

    • Line 1 of every script is jit.off(), so Zibo runs in the LuaJIT interpreter.
    • All 18 scripts compile under PUC Lua 5.1.
    • a_fms runs unmodified under PUC 5.1 with an XLua shim written in Lua.
    • The CDU pages match the oracle exactly. N1 LIMIT was compared line by line.
  3. Lua 5.3 (= fengari semantics) with shims gives identical CDU output. A 30-second scenario was run under PUC 5.1 and under PUC 5.3 with compat53.lua. It covers N1 LIMIT, RTE, LEGS, PERF INIT, TAKEOFF REF, CLB/CRZ/DES and PROGRESS, with a route both on the ground and airborne. All CDU lines matched exactly (0 differing lines out of 156). It needed these shims:

    • 1,082 string.format('%d', non-integer) truncations
    • 8 math.random(float) truncations
    • math.atan2, plus log10, cosh, sinh and tanh (only atan2 is used)
    • table.getn, setfenv, jit.off
    • for fengari, also io.open and os.date("!%y")

    fengari itself also produced identical pages, apart from one -0.0 printed as 0.0. But it is far too slow (item 6).

  4. Per-frame cost is the main risk of any embedded-Lua route. About 80 % of it is XLua's Lua-side glue, not Zibo's logic. Measured on native x86-64:

    Scenario LuaJIT-interp PUC 5.1 PUC 5.3
    FMC powered, no route 0.52 ms/frame 1.1–1.3 ms/frame 1.1 ms/frame
    6-leg route 0.9 ms/frame 2.1–2.9 ms/frame 1.6–2.0 ms/frame
    41-leg route 2.2 ms/frame 6.6–6.9 ms/frame –

    fengari (node 22) is far slower: 22.8 ms/frame with no route and 38–42 ms/frame with 6 legs.

    The sampling profile under PUC 5.1 with a 41-leg route:

    • 62 % in init.lua namespace_read (init.lua:413)
    • 9 % in namespace_write (init.lua:376)
    • 8.5 % in dref_array_write (init.lua:45)

    Navdata loading at flight start is the second risk. With a synthetic AIRAC-sized set (250k fixes, 25k navaids, 120k airway segments), read_navdata2 and its siblings parse the files character by character:

    • LuaJIT-interp: 9.3 s
    • PUC 5.1: 15.5 s
    • PUC 5.3: 11.1 s (32 s with the Lua-level shims)
    • fengari in node: 339 s, with a 1.7 GB heap

    The parse adds about 100 MB to the Lua heap.

  5. The oracle already runs the FMS. xphost loads xlua.xpl with all 15 script modules. The FMS can be driven with laminar/B738/button/fmc1_* commands, and the LineNN_* strings can be read through the replay bridge (string section 0x82). Shown here: MENU, APPROACH REF, RTE and N1 LIMIT pages from the real oracle. It has no navdata. The Lua reads X-Plane files relative to the X-Plane root; none exist here, and the repository's B738X_*.dat caches are empty.

  6. Recommendation: A-wasm. Run PUC Lua 5.1 inside WASM with:

    • error recovery through Binaryen-asyncify setjmp/longjmp (the approach CRuby's WASI port uses)
    • the XLua namespace and dataref glue rewritten in C
    • native navdata loaders that build Zibo's tables from MSFS facilities

    Gate it with a spike in MSFS. The fallback is B-auto: a mechanical Lua→C++ translation with a Lua-value runtime, verified against the oracle. A-js on fengari is ruled out by measurement:

    • 22.8 ms/frame idle, 38–42 ms/frame with a 6-leg route
    • 339 s navdata parse
    • 1.7 GB heap in node/V8

    A hand port (B) is about 100k Lua lines for phase-1 identity, so it is the last resort. Details in §9 and §10.


1. XLua API surface#

1.1 What XLua itself provides (plugins/xlua/init.lua, xlua.xpl strings)#

  • C functions (from the strings in plugins/xlua/lin_x64/xlua.xpl):
    • Datarefs: XLuaFindDataRef, XLuaCreateDataRef(name,type,"yes"/"no",notifier), XLuaGetDataRefType, XLuaGetNumber/SetNumber, XLuaGetString/SetString, XLuaGetArray/SetArray.
    • Commands: XLuaFindCommand, XLuaCreateCommand, XLuaReplaceCommand, XLuaWrapCommand, XLuaFilterCommand, XLuaCommandStart/Stop/Once.
    • Timers: XLuaCreateTimer, XLuaRunTimer(t,delay,repeat), XLuaIsTimerScheduled, XLuaGetTimerRemaining.
    • Other: XLuaGetCode, XLuaReloadOnFlightChange.
    • The binary is LuaJIT 2.1.0-beta3, and reads sim/time/is_in_replay.
  • Lua glue in init.lua:
    • dataref wrappers dref_array_read/write (37/45), wrap_dref_number/string/any(_deferred) (65–150)
    • find_dataref (168), create_dataref (174)
    • command objects with :start/:stop/:once (187–249)
    • find_command, create_command, replace_command, wrap_command, filter_command (221–244)
    • timers keyed by function: run_timer, stop_timer, is_timer_scheduled, run_at_interval, run_after_time (254–283)
    • the namespace: create_namespace (430) with namespace_read (413) and namespace_write (376) as __index/__newindex, plus __len/__pairs/__ipairs. Every script global lives in ns.values and is read through a Lua metamethod. Datarefs are "props" with __get/__set. Bare tables assigned to a global are wrapped into sub-namespaces unless raw_table(name) was called.
    • run_module_in_namespace (480) uses setfenv. It provides make_prop, dofile, real_table and raw_table.
    • setup_callback_var (507) sets SIM_PERIOD and IN_REPLAY. do_callout (511) runs the callbacks.
  • Callbacks that XLua calls (binary strings): aircraft_load, aircraft_unload, flight_start, flight_crash, before_physics, after_physics, after_replay.
  • Each script module runs in its own Lua state. Datarefs are the only channel between scripts.

Comment lines are excluded (tool api.py). The first group of columns is datarefs; the second is commands.

Script lines dataref create (with handler) dataref find cmd create cmd find replace/wrap callbacks
B738.a_fms 172,900 1,572 (1,064 num, 309 string, 199 array) 499 448 212 46 0 aircraft_unload 170230, flight_start 170237, after_physics 171531, after_replay 172839
B738.calc (V-speeds, N1 limits, trim, speed bands) 7,022 73 12 96 0 0 0 flight_start 6881, after_physics 6944
B738.LATLON (IRS, ISDU; already ported to wasm/src/core/irs) 4,233 71 5 73 29 0 0 3719, 3905
B738.anav (rain, windshield, HUD effects: not FMS) 1,933 38 14 61 1 1 0 1771, 1800, 1920

Other details of a_fms:

  • Timers:
    • run_at_interval 2 (B738_ff_approx 1 s and B738_gw_approach 5 s, set up in flight_start)
    • run_after_time 55 (radio decoding 6.5 s, ILS/GLS ident blanking, rte copy 0.5 s, VNAV timers 2.5 s, …)
    • is_timer_scheduled 112, stop_timer 75
  • raw_table 520 calls.
  • SIM_PERIOD 69 uses, IN_REPLAY 1. dofile only as a comment (172897).
  • :once() on command objects 14 times.

What a_fms finds: 448 datarefs.

  • 155 sim/…
  • 191 zibomod
  • 32 + 3 + 2 + 1 FMOD-pack Lua
  • 27 glareshield (MCP and EFIS, already ported to C++)
  • 21 B738.calc
  • 14 hoppiebridge/* (optional plugin)
  • 1 LATLON, 1 tablet

X-Plane datarefs used by the FMS (sim/…, 154 distinct in a_fms; list in w/simrefs.txt):

  • position: lat, lon, elevation, local_x/y/z/v*, mag_psi, magnetic_variation, groundspeed
  • gauges: IAS, alt, VVI, baro pilot/copilot/stby, wind, ground track, RA, AHARS attitude
  • radios: nav1/2 and adf1/2 frequencies, OBS/course
  • autopilot: dials, altitude_mode, source
  • weight: m_fuel[], m_fuel_total, m_total, m_fixed, m_stations
  • engines: N1, N2, FF, thro_use, burning fuel, reverser; throttle_ratio, prop_mode
  • flap handle, parking brake, gear deploy and on_ground
  • forces: faxil_*
  • temperatures, weather pressure and temperature
  • joystick axes and null zones
  • time/paused, prefs startup_running, livery path/index, rel_gps, kill_map_fms_line, hide_yoke

X-Plane commands: sim/autopilot/altitude_hold, knots_mach_toggle, level_change, vertical_speed_pre_sel, sim/operation/pause_toggle, sim/systems/no_smoking_toggle. The other 40 found commands are laminar/b738/fmodpack/* sounds.

calc, LATLON and anav use 25, 17 and 24 sim/ datarefs (w/simrefs.txt).

Commands created by a_fms:

  • 69 CDU keys × 2: laminar/B738/button/fmc1_* and fmc2_*.
    • LSKs 1L…6R, 0–9, period, minus, slash, A–Z, SP, clr, del
    • prev_page, next_page, init_ref, menu, n1_lim, rte, legs, fix, clb, crz, des, dep_app, hold, prog, exec
    • For example create_command("laminar/B738/button/fmc1_1L", …, B738_fmc1_1L_CMDhandler) at 105624.
  • 74 others:
    • MMR panel keys laminar/B738/push_button/mmr1_* and mmr2_*
    • fms/set_flap_*, fms/load_cfg*, fms/save_cfg*, reset_fmc, alt_interv
    • fms_light_pilot/fo, chock, pause_td, nosewheel and toe-brake axes

2. LuaJIT-specific features and 5.1 / 5.3 portability#

Counts below exclude comment lines.

Idiom a_fms calc LATLON anav Note
jit.off() line 1 line 1 line 1 line 1 stub jit = {off=function() end}
bit.*, ffi, goto, require, loadstring, unpack, coroutines, pairs, ipairs, next, select, debug.* 0 0 0 0 none
setfenv 0 (only XLua's init.lua) 5.3: emulate with debug.upvaluejoin on _ENV (done in compat53.lua)
table.getn 12 (51391–52062, on raw_table legs_data2/8) 0 0 0 5.3: #t
# length operator 1 0 0 0
math.mod( / math.pow 0 / 0 0 0 0 only math.modf(os.clock()) (124772, 164141)
math.atan2 72 missing in 5.3/fengari without compat: math.atan(y,x)
math.random / randomseed 6 / 2 (medical timer 124775, ANP 164144) 4 0 4 5.3 errors on float args (8 hits in the scenario); the RNG differs anyway
string.format total / with %d 2,504 / 1,987 2 / 0 0 0 5.3/fengari error on %d with a non-integer float. The scenario hit 1,082 such calls. Shim: truncate toward zero. PUC 5.1 on wasm32 needs LUA_INTFRM_T = long long, because long is 32-bit there.
tostring( 568 0 0 0 about 300 in debug dumps (dump_leg_append, legs.txt); the rest in file writers (apt/rnw cache 9060–9146, FMS-plan export 16723–16734, save_fmc). Under 5.3 an integral float prints 5.0.
lines with .. 5,326 0 52 0 concatenating a computed float prints 5.0 in 5.3. Displayed numbers almost all go through string.format, so the scenario showed no difference. Internal keys or strings may still differ: patch fengari's number→string to %.14g (one function) instead of hunting call sites.
% operator (lines) about 618 5.3: integer % 0 raises an error (5.1 gives nan). Mostly % 360 with constants. Low risk.
string.sub / string.byte / string.len 2,645 / 115 / 969 46 (len) 5.3 errors on non-integral float indices. The shims saw 0 hits, but keep them: they cost about 2× in the char-by-char parsers.
os.date / os.clock / os.time 10 (!%y, !%m, !%d, !%Y: 8428–8446 AIRAC, 131758 clock, 135256) / 2 / 0 0/1/0 0 0/1/0 fengari's os.date cannot do !%y (it crashed at 8429), so shim it. os.clock is CPU time; only used to seed random.
io.open 162 (§3) 0 0 1 (debug append) fengari has no io.open; provide a host reader

Compilation was checked with PUC Lua 5.1 and LuaJIT 2.1 (tools/parse51.py): all 18 scripts plus init.lua compile. a_fms compiles in 0.09 s (PUC, native) and 0.9 s (fengari in node).

LuaJIT → PUC 5.1 differences that matter:

  • %d with values ≥ 2^31 on wasm32, as above.
  • tostring of NaN/inf (-nan versus nan).
  • The RNG.
  • The 5.1 limits (200 locals, 250 registers, 2^18 constants per function) are not hit.

Runtime check: the harness run_route.py under PUC 5.1 entered a route and PERF INIT through the CDU keys and executed. The pages matched LuaJIT-interp exactly.


3. Navdata and files#

3.1 Startup sequence#

flight_start runs this once per aircraft load, guarded by plane_already_loaded == 0 (a_fms 170605–170650):

  1. update_config (167446).
  2. detect_apt_dat (8453) reads Log.txt line 1 and Custom Scenery/scenery_packs.ini to build a stamp. It compares the stamp with the 4-line header of the caches <Aircraft_Path>/B738X_apt.dat, B738X_rnw.dat, B738X_gate.dat and B738X_jetway.dat: three stamp lines, then v1.1.
  3. If the stamp does not match, create_apt_rnw_dat2 (8630) parses the whole apt.dat: Custom Scenery/Global Airports/…, Global Scenery/Global Airports/Earth nav data/apt.dat or Resources/default scenery/default apt dat/…, plus the scenery packs. It rewrites the four caches.
  4. read_rnw_dat (9491) builds rnw_data[n] = {icao, rwy, lat1, lon1, lat2, lon2, length, crs, 0, rwy} from 8-word lines.
  5. check_navdata (7105) picks Custom Data/ if it has earth_nav.dat, earth_fix.dat and earth_awy.dat; otherwise Resources/default data/. It then calls read_navdata2 (7332), which reads:
    • Custom Scenery/Global Airports/Earth nav data/earth_nav.dat (GA runway fixes)
    • Custom Data/user_nav.dat (it writes this file when custom ILS data is in use)
    • Custom Data/airports.dat (CIFP airports: transition altitude/level, magnetic variation)
    • Custom Data/runways.dat
    • earth_nav.dat, which builds:
      • earth_nav[n] = {type 1 VOR / 2 / 3 NDB / 5 DME, lat, lon, id, icao, freq, name, region, magvar}
      • ils_nav[n] (10 fields), gls_nav[n] (11 fields)
    • earth_fix.dat (type 4 rows appended to earth_nav)
    • user_fix_georef.dat
    • earth_hold.dat, which builds hold_nav
  6. load_cust_wpt reads Custom Data/B738X_wptx.dat.
  7. read_apt_dat (9394) builds apt_data[n] = {icao, lat, lon, ta, tl, longest_rwy} from 6-word lines.
  8. read_awy_dat2 (9572) builds awy_data[n] = {awy, from, to, from_rc, to_rc, …} from earth_awy.dat.
  9. create_idx_table (8177) builds the first-character index tables idx_earth_nav[byte][k], idx_apt, idx_ils, idx_ils_icao, idx_gls, idx_gls_icao, idx_rnw, idx_awy and idx_hold. Element [99999] holds the count.

3.2 On-demand reads#

  • CIFP: Custom Data/CIFP/<ICAO>.dat or Resources/default data/CIFP/<ICAO>.dat. 27 + 27 call sites, for example read_ref_list_icao 52599 → read_ref_data 38756 and read_des_data 39804. A file is parsed when an origin/destination is entered or a route is loaded. The parse splits on commas and handles RWY:, SID:, STAR: and APPCH: lines. It builds:
    • ref_data_rwy, ref_data_sid, ref_data_sid_tns, ref_data_star, ref_data_star_tns, ref_data_app, ref_data_app_tns, ref_ed_app
    • rte_sid, and the des_* equivalents for the destination
    • selection lists: create_sid_list 110538, create_star_list 111084, create_app_list 111462
  • Flight plans: Output/FMS plans/*.fms, *.fmx, *.fml and b738x.xml (SimBrief/datalink), read and written by import_fms_v11 15397, import_fmx 16801, import_fml 17230, export_fms 16699 and export_fmx 18014. Directory listings come from zibomod (zibomod/filename_list*).
  • Saved FMC state: B738X_00.dat … _08.dat (and per variant) via save_fmc 33212 and load_fmc 34897.
  • Configuration: b738x.cfg, b738x_hw.cfg, b738x_fmod.cfg, b738x_acf.cfg, b738x_status.dat and b738x_state.dat (170430–170482), plus Output/preferences/.
  • Other:
    • METAR.rwx (ATIS)
    • Log.txt
    • debug output: legs.txt, idle_path, Flight_Recorder.txt, Data_records.txt
    • zibomod.cpp: check_DR 171153, a developer check that reads nothing when the file is absent

3.3 Size and time of the in-memory navdata#

rows Lua heap parse time
this measurement (synthetic: 250k fixes, 25k navaids, 120k airway segments, tools/gen_nav.py) about 275k earth_nav rows of 9 fields, plus awy_data +90 MB (LuaJIT), +105 MB (PUC 5.1, 64-bit) LuaJIT-interp 9.3 s, PUC 5.1 15.5–16.2 s, PUC 5.3 11.1 s
the real oracle (xphost process RSS, all plugins) 289 MB with this data, against about 30 MB without 9.5 s extra startup

Real X-Plane 12 / Navigraph counts are of the same order. They are not available here; check them once the data exists.

3.4 Implications for a navdata provider#

The rest of the FMS sees only these Lua tables:

  • earth_nav, ils_nav, gls_nav, hold_nav, apt_data, rnw_data, awy_data and idx_*
  • ref_data_*, des_data_* and rte_sid/star/app from CIFP

The lookup functions all scan these tables: find_navaid 23030, find_navaid2 23569, find_awy 9682, find_awy_path 9825, nav_data_find 52270, find_ref_rnw_data 13314 and find_id_app_data 13672.

So a provider can work either way:

  • (i) Generate X-Plane-format text files (earth_nav/fix/awy/hold, CIFP/<ICAO>.dat, airports.dat, runways.dat and the B738X_apt/rnw caches with the v1.1 header) from MSFS facility data. The Lua then runs untouched, and the oracle can read the same files through --xplane.
  • (ii) Replace the loader functions with native ones that build identical tables: read_navdata2, read_apt_dat, read_rnw_dat, read_awy_dat2, create_idx_table, and for CIFP read_ref_data/read_des_data. This removes the 9–30 s parse.

Both can be checked row by row against the Lua loaders running on the generated files.

MSFS side (third_party/msfs2024-sdk/SimConnect/SimConnect.h):

  • SimConnect_RequestAllFacilities(type) (line 1118) for AIRPORT, WAYPOINT, NDB and VOR lists.
  • SimConnect_RequestFacilityData(_EX1) with data types ROUTE (airways per waypoint), RUNWAY, DEPARTURE, ARRIVAL, APPROACH and their leg types, VOR, NDB, WAYPOINT and HOLDING_PATTERN.

Airways are only available per waypoint, so a complete earth_awy.dat needs a one-time background crawl, cached in \work. CIFP can be produced on demand per airport, but MSFS requests are asynchronous while the Lua reads synchronously. Prefetch the origin/destination; otherwise the first entry returns NOT IN DATA BASE. MSFS leg data (ARINC 424 path terminators, fixes, altitude/speed constraints) maps onto X-Plane CIFP fields. Converting the procedure data is the main fidelity work of the provider.


4. CDU output and input#

  • Screen text: string datarefs per CDU (fmc1 = captain, fmc2 = F/O), created at a_fms 4892–5030.

    Dataref Meaning
    laminar/B738/fmc1/LineNN_L large white, NN = 00 title … 06
    …_S small white
    …_X small label row above line NN
    …_I inverse
    …_M magenta
    …_G green
    …_C cyan; only Line00_C
    …_LX, …_GX large / green label rows, 01–06
    Line04_SI small inverse
    Line_entry scratchpad, white; also messages such as "NOT IN DATA BASE"
    Line_entry_I scratchpad, inverse

    57 datarefs per CDU. The page functions write line0_l…line6_x globals. B738_fmc_display (130543) and B738_fmc_display2 (130727) copy them with txt_trunc(…, 24) when display_update == 1. Page dispatch is in B738_fmc_disp_capt (130906) and B738_fmc_disp_fo (131115).

  • Lights:

    Light Dataref Where it is set / drawn
    EXEC laminar/B738/indicators/fmc_exec_lights and …_fo (5098; drawn by objects/737_cockpit_ind.obj); also laminar/B738/fmod/fmc_exec_lights* (5096) B738_exec_light_cpt 166929, _fo 167088
    MSG laminar/B738/fmc/fmc_message, fmc_message2, fmc_message_warn, _warn2 (5702–5705) queue in add_fmc_msg 136323, B738_fmc_msg 134926, B738_fmc_clr_msg 135881, B738_fmc_light 135812
    OFST / DSPY / FAIL fmc/fmc_offset, fmc_dspy, fmc_fail (5707–5712)
  • Keys: laminar/B738/button/fmc1_* and fmc2_* (§1.2), with handlers B738_fmc1_<key>_CMDhandler(phase, duration). The cockpit model clicks them: 69 per side in b738_cockpit.obj.

  • PANEL_3D (737_80NG.acf from line 171370): a gen_rotary FMC background (IMAGE FMC/fmc, SHOW_EQUAL 1 laminar/B738/fmc1/fmc1_show), then GROUP Line S L I M with one gen_text per layer. For example LINE 0 L at POS 560.5/1228.5, IMAGE TYPE/TEXT/TXT.fmc.big.white, OFFSET_X −540. Each fmc1/fmc2 LineNN_* dataref appears once.

    • tools/msfs/build_display.py:46–47 maps the regions LU_CDU_CPT (1,549)–(549,1017) and LU_CDU_FO (550,549)–(1099,1017).
    • build/LU738_INTERIOR.manifest.json panel_surfaces has SCREEN_LU_CDU_CPT and SCREEN_LU_CDU_FO (obj line 32201, 2 triangles each).
  • Work in progress, uncommitted, in another session:

    • avionics/src/du/CduSources.ts already maps all LineNN_* refs to a CduText store.
    • avionics/src/instruments/CDU, CDU_FO exist.
    • fmc1_show is temporarily set from BUS_XFR1/2.

    So the renderer only needs the strings. There is no string path from WASM yet: L:vars are numeric. With the FMS in JS (A-js) the strings are local to JS. With the FMS in WASM, they would go over CommBus.


5. Interfaces between the FMS and the rest (zibomod / the C++ systems)#

Tool xref.py, data in w/xref.json.

5.1 What the FMS writes and zibomod reads#

308 of a_fms's 1,572 datarefs are read by zibomod (tools/re/zibomod_reads.json). 174 of them are AFDS inputs (tools/re/afds_external.json). calc adds 18 (12 AFDS), LATLON 18 and anav 1. Main groups:

  • Flight phase / modes: FMS/flight_phase, FMS/N1_mode, FMS/descent_mode, FMS/activate_pitch_mode, fms/no_perf, fms/end_route, fms/fpln_acive, fms/missed_app_act, fms/lnav_prior_arm, fms/vnav_disconnect, fms/lnav_disconnect, FMS/descent_now.
  • Speeds:
    • autopilot/fmc_climb_speed(_l, _l2, _mach), fmc_cruise_speed(_mach), fmc_descent_speed(_l_act, _mach, _init)
    • fmc_climb_r_speed1/alt1, fmc_vnav_v2_20
    • FMS/vref_15/25/30/40, FMS/approach_speed, approach_wind_corr
    • fms/rest_wpt_spd*, fms/vnav_desc_spd_disable
  • Altitudes: autopilot/fmc_cruise_alt, FMS/accel_height, eo_accel_height, throttle_red_alt, cutback*, aerodrom_altitude, fms/rest_wpt_alt*, next_rest_wpt_alt*, ed_alt, rnav_alt.
  • VNAV path:
    • fms/vnav_idx, vnav_td_*, vnav_decel_dist, vnav_fl100_*
    • vnav_alt_err(2, 99), vnav_pth_fpm(99), vnav_vvi(99), vnav_err_pfd, vnav_step_dist
    • gp_vvi, gp_err_pfd, dist_to_ed/dest, lnav_dist_next
  • LNAV: fms/fac_xtrack, fac_trk, gps_track_degtm, rnp, vrnp, vanp, act_wpt_gp, vnav/legs_num, legs_offset*, rte12_*.
  • Thrust: FMS/approach_flaps(_set), takeoff_flaps, v1_set, fms/lock_thrust, sync_thrust. The N1 limits come from calc: engine/calc/thr_*_N1.
  • Nav receivers: nav/mmr_*, gls*, laminar/radios/pilot|copilot/nav_*, mmr/cpt|fo/act_mode|value. The FMS computes the MMR/ILS/GLS deviations. systems_afds.cpp:493 already hand-ported mmr_active_mode.
  • EFB options: hardware_thr_lever, yoke_disco_ap, vnav_common, … These are already C++ defaults in wasm/src/core/systems_afds.cpp:156–181.
  • Not FMS at all but created in a_fms: irs/irs_mode (LATLON writes it), autopilot/course_pilot (MCP), ap_vvi_dial, preselected_alt, flt_ctrls/* axes, doors 737u/doors/*, chocks, GPU. These are ownership conflicts with modules already ported to C++.

wasm/src/core/systems_afds.cpp sets the FMS inputs to Zibo's no-route defaults:

  • 156–181: accel_alt 1000, fms_N1_mode 7, no_perf 1, end_route 1, fac_xtrack 999, fmc_cruise_alt 0, …
  • 420–435: N1 limits 0.952 / 0.956 / 0.935
  • 269: fmcWarn never

The AFDS port has a name-reflection table (AfdsGen::vars, member() at systems_afds.cpp:127). Together with zibomod_reads.json (symbol → dataref) it lets any FMS host bind dataref names to the C++ fields.

5.2 What the FMS reads from zibomod#

191 datarefs:

  • IRS positions and times (irs/*_lat/lon/pos, zulu time); some are already C++ (irs/).
  • AP mode statuses (ap/*_status, autopilot/*), FMS/vnav_speed, flaps_speed.
  • electric/mcdu1_on/mcdu2_on, AC transfer buses, TAT/TAS, fuel temperature, eicas_ff1/2, zibomod/mag_var.
  • Filename lists.
  • The leg and path arrays: fms/legs_lat, legs_lon, legs_type, legs_alt_rest*, legs_spd_calc*, legs_crs_mag, legs_eta, legs_rnp*, legs_radii_*, legs_turn, legs_df_crs*, legs_bypass, legs_af_*, legs_seg1/2/3_* and dir_seg1/2/3_*. These are [256] arrays; 174 fms/legs* refs in total, 107 created by XLua and 67 by zibomod.

zibomod's FMS functions, not yet ported (symbol sizes from nm -S):

Function Size
calc_next_turn 237,784 B
ap_vnav 36,494 B (stubbed in wasm/src/core/afds/afds.cpp)
fms_turn 25,054 B
calc_path 21,725 B
vnav_calc_speed 14,877 B
nd_earth_display 13,887 B
nd_earth_calc 7,626 B
read_earth_navdat 5,724 B
vnav_init 5,189 B
clr_legs_data, offset_xtrack, calc_speed_turn, calc_abeam_bypass_wpt, Find_plane_path, turn_to_leg, between_wpts, calc_dir_abeam* smaller

The AFDS already contains ap_lnav, ap_lnav_cmd, ap_lnav_vnav_armed, ap_vnav_cmd and lnav_vnav_horiz_vert.

zibomod also reads navdata itself for the ND map (read_earth_fix, read_earth_navdat).

Every architecture needs this group translated with tools/re/gen_*.sh. The AFDS translation ratio (284 KB → 17.8k lines) suggests about 22k lines.


6. Functional blocks of a_fms#

Tool blocks.py. Sizes are from each function to the next; the header (globals and datarefs) is lines 1–3445.

Block Lines Fns Span Phase 1?
Header: globals, tables, dataref and command creation (+ dataref handlers 3445–6729) 3,445 + 2,881 407 1–6729 yes
CDU key handlers (fmc1 ≈ 59256–84942, fmc2 ≈ 84942–105600; legs_1_lsk, legs_x_lsk, rte_*_lsk 53335–59256) 51,418 (F/O half is a copy) 144 53335–105600 yes (captain side; F/O later)
Route / legs editing: rte_add_*, fpln_add_leg*, dir_*, via_*, find_navaid*, find_awy*, B738_calc_rte*, dec_lat_lon2, B738_mod_change2, entry_*, set_spd_alt_rest* 16,461 84 9682–53335 yes
Procedures / CIFP: read_ref/des_data, rte_add_sid2/star2/app2, create_*_list, runways 8,186 36 13314–145094 yes
Navdata load / index 3,297 41 7105–9682 replaced by the provider
Performance / pages: B738_fmc_perf 118180, takeoff 117008, approach 117735, n1_limit 118627, climb 119153, cruise 119898, descent 122003, progress 123137, legs99 113766, dep99 110761, arr99 111933, hold 116324, fix 109693, pos_init 108435, rte_init 108756, ident, nav_status, altern, nearest 14,370 41 107609–124204 yes (except altn, nearest, hold, fix)
VNAV path: B738_vnav_calc 137456 (4,002 lines), B738_vnav_calc_mod 141894, create_idle_path(2) 153573 / 155714, B738_vnav_pth3 157719, B738_calc_vnav_spd 163108, B738_restrict_data 149880, calc_step_point, fuel flow 15,920 25 136963–164252 yes
Flight phase / thrust: B738_flight_phase3 132756, B738_N1_thrust_set 134561, B738_fmc_calc 159541, B738_calc 131441, B738_legs_step*, B738_fmc_on 6,109 13 131441–164023 yes
Display / messages / EXEC (§4) 5,521 17 50929–167088 yes
Nav receivers: MMR / ILS / GLS / VOR auto-tune, pfd_*, B738_fix_calc 4,908 24 19484–167991 partly (ILS approach), partly ported in C++
Offsets 1,149 6 18257–19442 no
Import / export / datalink / CPDLC / ATIS / ACARS / save-load FMC 16,938 91 14379–171357 no (SimBrief later)
Config / options / status files 8,149 20 41062–48681 defaults only
Init and callbacks: B738_init2 164597 (2,007 lines), flight_start 170237, after_physics 171531 (1,308 lines) 4,327 8 164581–172839 yes
Other (352 small helpers) 7,175 352 partly

Rough phase-1 subset for a hand port (B):

  • captain keys ≈ 30k
  • route ≈ 16k
  • procedures ≈ 8k
  • pages ≈ 12k
  • VNAV ≈ 16k
  • phase / thrust ≈ 6k
  • display ≈ 5k
  • receivers ≈ 3k
  • init ≈ 4k
  • header, handlers and helpers ≈ 8k

Total about 100k Lua lines, plus B738.calc's N1 and V-speed parts (about 3–4k), plus the zibomod translation (§5).


7. The oracle#

7.1 What already works#

  • xphost/src/main.cpp:213 loads NavDB from --xplane. That is C++ for the XPLM nav API used by zibomod (navdb.cpp:52–60: Custom Data or Resources/default data earth_nav/fix.dat, and apt.dat).

  • It loads every plugins/*/lin_x64/*.xpl (main.cpp:225–240), so XLua runs all 15 modules including a_fms. The log shows I/LUA: Running scripts/B738.a_fms/B738.a_fms.lua.

  • The working directory is the X-Plane root (main.cpp:200–204, "the Lua FMS opens nav data relative to it"). zibomod's Aircraft_Path resolved to that root here, and the B738X_*.dat caches were written there.

  • No X-Plane navdata exists in the repository or on this machine. The repository's B738X_*.dat files are 4 empty header lines (gitignored), so the oracle FMS knows no airport. The demonstration showed NOT IN DATA BASE for KSFO and NAV DATA OUT OF DATE with synthetic files.

  • Driving the CDU works today. The replay protocol has these records:

    • 0x01 declare
    • 0x02 frame
    • 0x04 command begin/end, e.g. laminar/B738/button/fmc1_*
    • reply section 0x82 for string datarefs (bridge.cpp:397–410, 539–543)

    tools/cdu_probe.py read MENU, APPROACH REF, RTE 1 (typing KSFO → NOT IN DATA BASE) and N1 LIMIT from the real oracle. The N1 LIMIT page equals the PUC 5.1 harness output character for character.

  • The --sci-test JSON runner sends commands ({"cmd": …}).

7.2 Gaps#

  • print and trace only format numbers (sci_test.cpp:287 show() uses %g).
  • probe skips string datarefs (sci_test.cpp:351).

7.3 What comparing would take#

  1. Add string support to print/trace in sci_test.cpp (small), or use the replay bridge as cdu_probe.py does.
  2. Generate X-Plane-format navdata from MSFS data with the provider (§3.4), and run the oracle with --xplane <dir> on those files. Both sides then use identical data. Add a Log.txt / scenery_packs.ini stamp so the caches are accepted, or the 4-line empty header as now.
  3. Write CDU scenario scripts: keys → expected LineNN_* per frame, plus the FMS→AFDS numeric outputs (§5.1 list, e.g. via zibo: memory reads or the datarefs). Compare frame by frame, as the IRS and AFDS differential tests already do.
  4. For flight phases, feed recorded MSFS flight states as set inputs.

8. Feasibility and risks#

8.1 Per-frame work#

Every frame, after_physics (171531–172838) calls, in order:

  • B738_fmc_on, the EXEC lights
  • B738_calc_rte2, B738_calc_rte8
  • B738_gls, B738_calc, calc_step_point
  • B738_fmc_calc, B738_mod_change2, B738_vnav_calc, B738_vnav_calc_mod, B738_vnav_pth3
  • B738_restrict_data, B738_fix_calc, B738_vor_sel, B738_ils_disagree
  • B738_displ_rnw2, B738_displ_fix, B738_displ_acf (ND symbols)
  • B738_fmc_disp_capt, B738_fmc_disp_fo
  • B738_flight_phase3, the N1 functions, B738_irs_sys
  • B738_fmc_msg, B738_fmc_clr_msg, B738_vnav_desc_spd, B738_wind, B738_calc_vnav_spd
  • B738_state_func, check_lat_offset, B738_calc_rte, B738_legs_step(2)
  • datalink, ATC

The cost scales with the number of legs.

8.2 Measurements (native x86-64; harness with Lua-side XLua glue and dataref stubs)#

VM powered, no route 6 legs, ground / air 41 legs, ground / air navdata parse at flight start Lua heap after parse
LuaJIT 2.1, jit.off (= X-Plane) 0.52–0.69 ms 0.90 / 0.85 ms 2.24 / 2.15 ms 9.3 s 98 MB
PUC Lua 5.1 1.15–1.33 ms 2.25–2.86 / 2.06–2.48 ms 6.6 / 6.9 ms 15.5–16.2 s 114 MB
PUC Lua 5.3 + compat53.lua 1.09 ms 1.65 / 2.00 ms – 11.1 s (32 s with the Lua-level string.sub shim) –
fengari 0.1.4 + same shims (node 22, V8) 22.8 ms 41.8 / 38.3 ms (not run) 339 s node heap 1.7 GB
Oracle (real XLua + LuaJIT inside xphost) a_fms ≈ 1.25 ms/frame (1.92 ms all plugins minus 0.67 ms without a_fms), cold, no route +9.5 s RSS 289 MB against about 30 MB

The sampling profile (PUC 5.1, 41 legs) puts 62 % + 9 % + 8.5 % in init.lua's namespace and dataref glue and under 20 % in Zibo's own functions: legs_fill, legs_fill2, legs_fill3 about 7 %, then B738_displ_fix, check_fix_data, fly_over_wpt2. A native (C or JS) implementation of namespace_read/namespace_write/the dataref accessors, with identical semantics, is therefore the biggest single speed-up for A and A-js.

8.3 Risks of (A) Lua 5.1 in our WASM module#

  • No setjmp/longjmp (MSFS 2024 sysroot) and -fno-exceptions / MVP-only (wasm/tools/check_wasm.py, wasm/build.sh:34–39). Lua's error recovery (luaD_throw, pcall) would be lost, so any Lua runtime error traps the whole systems module. Errors do happen: the oracle log shows a HUD_FIX Lua error at every start. Ways around it:
    • Binaryen asyncify-based setjmp/longjmp. wasm-opt is not in the toolchain now; this costs size and speed.
    • Running the Lua in a separate WASM gauge, so a trap only kills the FMS.
    • An AOT Lua→C++ translation with a dynamic value type and error codes. This also removes the interpreter overhead, at the cost of a large module.
  • Performance: PUC 5.1 is 2.5–3× slower than LuaJIT-interp. WASM in MSFS adds an unknown factor. Long routes (60–100 legs with procedures) would cost about 10–15 ms per frame native unless the glue is native.
  • Startup: 15 s+ of navdata parsing on the sim thread. It must be replaced by native loaders (§3.4 ii).
  • wasm32: long is 32-bit, so %d breaks above 2^31. Heap about 100 MB.
  • Dataref ownership: a_fms creates datarefs that C++ modules own: IRS mode, MCP course, radios/MMR, options, flight-control axes. The host needs a dataref store with per-name ownership. C++ modules must write into it, and Lua writes to C++-owned names must be dropped or arbitrated.
  • B738.calc must run too (21 N1 / V-speed datarefs the FMS reads), which overlaps the C++ speeds/ port.

8.4 (A-js) Lua 5.3 on fengari in a JS instrument#

  • Works with shims. Identical CDU output in the 5.3 comparison above.

  • Gaps to shim:

    • The string.format %d truncation: 1,987 call sites; it happens about 1,082 times in a 30 s scenario.
    • Integer arguments to math.random and string.*.
    • Missing math.atan2 (72 call sites).
    • table.getn (12), setfenv (XLua glue only), jit.
    • os.date('!%y') (10).
    • io.open (162 call sites; fengari has no file I/O: build the files or tables from facility data in JS).
  • Patch fengari's float→string conversion to %.14g. This covers tostring (568) and number concatenation (in about 5.3k .. lines) without touching call sites.

  • XLua's namespace_len and __pairs metamethods are honoured in 5.3 but ignored in 5.1. That matters only for # and table.getn on namespace proxies; Zibo applies them to raw_tables, so there is no effect.

  • Error recovery is free in JS (pcall works). No setjmp problem.

  • Strings are already in JS for the CDU renderer.

  • The AFDS lives in WASM. About 300 FMS→AFDS values per frame, plus the 256-element leg arrays to zibomod's path code and the path results back, must cross CommBus. This needs a compact binary frame protocol: about 2–20 KB per frame, depending on whether the leg arrays are sent only on change.

  • Speed rules fengari out. Measured under node 22 (V8, the fastest JS engine available; MSFS's Coherent will not be faster), running the same scenario (tools/run_fengari.js):

    • 22.8 ms/frame with no route
    • 38–42 ms/frame with a 6-leg route
    • 339 s for the navdata parse at flight start
    • 1.7 GB of JS heap

    That is about 17–20× slower than PUC 5.1, and 40× slower than LuaJIT-interp. The CDU output still matched 5.1 except one cell: -0.0 printed as 0.0 (sprintf-js loses the sign of negative zero).

    Moving the XLua glue into JS would not close a 10–20× gap. A-js is only viable with a Lua→JS transpiler (AOT) instead of fengari's interpreter. That has the same engineering profile as B-auto below, but runs in JS rather than WASM.

  • Navdata in JS: the JS FacilityLoader gives the same data. fengari's per-value objects make about 100 MB of Lua tables cost gigabytes of JS heap.

8.5 (B) Hand rewrite on MSFS facility data#

  • About 100k Lua lines for phase-1 identity (§6), plus the duplicated F/O CDU later.
  • Every page's formatting, rounding and message logic must be reproduced and verified with traces.
  • Even at an optimistic 1–2k verified lines per day this is months. The FMS code is highly stateful (about 3,400 lines of globals), so exactness needs the same differential-test harness as A.
  • Advantages: native speed, no VM, no shims.
  • Middle way (B-auto): mechanically translate the Lua into C++ or TS with a dynamic value type. The code is regular: globals, numbers, strings, plain tables, no closures beyond timers, no pairs, no metatables. This is the same idea as the zibomod translation, but needs a Lua-semantics runtime (string coercion, %.14g, nil, 1-based tables with holes).

9. Recommendation#

Option Identity Speed (measured / estimated) Main blockers Effort
A-wasm: PUC 5.1 in WASM unmodified Zibo Lua; 5.1 = LuaJIT semantics (CDU identical) 1.2–6.9 ms/frame native with Lua-side glue; about 3× less with C glue; × WASM factor (unknown, measure) no setjmp/longjmp → asyncify; 15 s navdata parse → native loaders; dataref ownership XLua host in C (about 1–2k lines), asyncify build step, navdata provider, dataref bus
A-js: fengari unmodified, plus about 10 shims and the %.14g patch; output identical except -0.0 23–42 ms/frame, 339 s start, 1.7 GB (node) interpreter speed: fatal –
B-auto: Lua → C++ (or JS) AOT translation with a Lua-value runtime mechanical, verified by oracle traces expected faster than PUC; no VM building a correct translator for Zibo's Lua subset; large module translator plus runtime; generated code is not hand-edited
B: hand rewrite must be proven page by page native about 100k Lua lines for phase 1 months

Choose A-wasm, behind a 2–3 day spike gate. The gate:

  • Lua 5.1.5 built with the MSFS SDK flags.
  • setjmp/longjmp via wasm-opt --asyncify, verified that pcall catches an error.
  • Module features still MVP (wasm/tools/check_wasm.py).
  • a_fms running with a C XLua host on a 40-leg route at about 2 ms/frame or less in MSFS.
  • Navdata tables built natively in about 2 s or less.

Why A-wasm:

  1. It is the only route measured to give identical Zibo behaviour today without a translation project. PUC 5.1 matched the oracle's CDU pages.
  2. Its per-frame cost is dominated by glue we can rewrite in C without touching Zibo's code.
  3. The rest of the stack already lives in the same module: AFDS, systems, and the reflection by name. Binding about 500 cross-module datarefs is in-process, with no CommBus frames for 256-element leg arrays.

If the gate fails (asyncify unusable in MSFS, or WASM too slow), go to B-auto. The oracle traces, the navdata provider, the dataref bus and the zibomod translation are reused unchanged.

Keep B (hand-written) for what is already C++ (IRS, MCP, MMR selection, options) and arbitrate those names in the dataref bus.

10. Plan (A-wasm)#

  1. Spike / gate:
    • Vendor Lua 5.1.5 into third_party/.
    • Use LUA_INTFRM_T = long long, and no io/os beyond what is shimmed.
    • Build it with the SDK flags, plus a Binaryen wasm-opt --asyncify pass for setjmp/longjmp. Restrict it with asyncify-imports / onlylist to the Lua core.
    • Write the XLua host in C++: datarefs, commands, timers with float sim time, callbacks, SIM_PERIOD/IN_REPLAY, and the namespace and dataref wrappers as C metamethods with init.lua's exact semantics (prop tables, sub-namespace wrapping, raw_table).
    • Run a_fms and calc on the scenario in MSFS. Measure ms/frame, heap and startup.
  2. Dataref bus:
    • A name-keyed store in the systems module.
    • Bindings to AfdsGen members via member() and zibomod_reads.json, and to the other C++ modules.
    • An ownership table: C++-owned names (IRS, MCP, MMR, options) win; Lua writes to them are dropped or logged.
    • Remove the FMS defaults in systems_afds.cpp:156–181 and 420–435.
  3. zibomod FMS path and VNAV via tools/re:
    • fms_turn, calc_next_turn, calc_path, Find_plane_path, turn_to_leg, between_wpts, calc_dir_abeam*, clr_legs_data, offset_xtrack, calc_speed_turn, calc_abeam_bypass_wpt
    • ap_vnav (replace the stub), vnav_calc_speed, vnav_init
    • with replay differential tests
  4. Navdata provider:
    • (a) A generator writing X-Plane-format files from MSFS facility data (SimConnect RequestAllFacilities and RequestFacilityData: ROUTE, RUNWAY, DEPARTURE, ARRIVAL, APPROACH, HOLDING), for the oracle and as a \work cache.
    • (b) Native replacements for read_navdata2, read_apt_dat, read_rnw_dat, read_awy_dat2, create_idx_table and CIFP read_ref_data/read_des_data that fill identical Lua tables. Prefetch procedures asynchronously.
    • (c) Table-equality tests against the Lua loaders on the generated files.
  5. Oracle:
    • String support in --sci-test print/trace.
    • Run with --xplane <generated data>.
    • CDU scenario scripts: cold & dark → IDENT/POS/RTE/DEP-ARR/PERF/N1/TAKEOFF → take-off → climb → cruise → descent → ILS. Compare frame by frame: every LineNN_* plus the FMS→AFDS outputs (§5.1).
  6. Display and keys:
    • Stream the CDU strings from WASM to the CDU instrument over CommBus, change-only, about 57 strings × 2.
    • Fill CduText (avionics/src/du/CduSources.ts).
    • Route the cockpit key events to fmc1/2_* commands.
    • Drive the EXEC (indicators/fmc_exec_lights) and MSG/OFST/DSPY/FAIL lights.
  7. Phase-1 flight in MSFS. Then F/O CDU, options and config files, SimBrief/flight-plan import, and the ND route (zibomod nd_earth_*, DrawRoute*).