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 againsttools/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#
The FMS is mostly Zibo's Lua, but not only.
a_fmshas 172,900 lines and 1,383 functions. It runs almost entirely in its per-frameafter_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_speedandvnav_init. The Lua writes thelaminar/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 existingtools/repipeline. That is about 350 KB of code, roughly 22k generated lines at the AFDS ratio.Lua compatibility is very good. The FMS scripts use no
bit, noffi, nogoto, nopairs/ipairs, nounpack, noloadstring, norequire, 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_fmsruns 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.
- Line 1 of every script is
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, pluslog10,cosh,sinhandtanh(onlyatan2is used)table.getn,setfenv,jit.off- for fengari, also
io.openandos.date("!%y")
fengari itself also produced identical pages, apart from one
-0.0printed as0.0. But it is far too slow (item 6).- 1,082
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.luanamespace_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_navdata2and 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.
- 62 % in
The oracle already runs the FMS.
xphostloadsxlua.xplwith all 15 script modules. The FMS can be driven withlaminar/B738/button/fmc1_*commands, and theLineNN_*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'sB738X_*.datcaches are empty.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.
- error recovery through Binaryen-asyncify
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.
- Datarefs:
- 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) withnamespace_read(413) andnamespace_write(376) as__index/__newindex, plus__len/__pairs/__ipairs. Every script global lives inns.valuesand is read through a Lua metamethod. Datarefs are "props" with__get/__set. Bare tables assigned to a global are wrapped into sub-namespaces unlessraw_table(name)was called. run_module_in_namespace(480) usessetfenv. It providesmake_prop,dofile,real_tableandraw_table.setup_callback_var(507) setsSIM_PERIODandIN_REPLAY.do_callout(511) runs the callbacks.
- dataref wrappers
- 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.
1.2 Usage per FMS-related script#
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_interval2 (B738_ff_approx1 s andB738_gw_approach5 s, set up inflight_start)run_after_time55 (radio decoding 6.5 s, ILS/GLS ident blanking, rte copy 0.5 s, VNAV timers 2.5 s, …)is_timer_scheduled112,stop_timer75
raw_table520 calls.SIM_PERIOD69 uses,IN_REPLAY1.dofileonly 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, prefsstartup_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_*andfmc2_*.- 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.
- LSKs
- 74 others:
- MMR panel keys
laminar/B738/push_button/mmr1_*andmmr2_* fms/set_flap_*,fms/load_cfg*,fms/save_cfg*,reset_fmc,alt_intervfms_light_pilot/fo, chock,pause_td, nosewheel and toe-brake axes
- MMR panel keys
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:
%dwith values ≥ 2^31 on wasm32, as above.tostringof NaN/inf (-nanversusnan).- 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):
update_config(167446).detect_apt_dat(8453) readsLog.txtline 1 andCustom Scenery/scenery_packs.inito 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.datandB738X_jetway.dat: three stamp lines, thenv1.1.- 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.datorResources/default scenery/default apt dat/…, plus the scenery packs. It rewrites the four caches. read_rnw_dat(9491) buildsrnw_data[n] = {icao, rwy, lat1, lon1, lat2, lon2, length, crs, 0, rwy}from 8-word lines.check_navdata(7105) picksCustom Data/if it hasearth_nav.dat,earth_fix.datandearth_awy.dat; otherwiseResources/default data/. It then callsread_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.datearth_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 toearth_nav)user_fix_georef.datearth_hold.dat, which buildshold_nav
load_cust_wptreadsCustom Data/B738X_wptx.dat.read_apt_dat(9394) buildsapt_data[n] = {icao, lat, lon, ta, tl, longest_rwy}from 6-word lines.read_awy_dat2(9572) buildsawy_data[n] = {awy, from, to, from_rc, to_rc, …}fromearth_awy.dat.create_idx_table(8177) builds the first-character index tablesidx_earth_nav[byte][k],idx_apt,idx_ils,idx_ils_icao,idx_gls,idx_gls_icao,idx_rnw,idx_awyandidx_hold. Element[99999]holds the count.
3.2 On-demand reads#
- CIFP:
Custom Data/CIFP/<ICAO>.datorResources/default data/CIFP/<ICAO>.dat. 27 + 27 call sites, for exampleread_ref_list_icao52599 →read_ref_data38756 andread_des_data39804. A file is parsed when an origin/destination is entered or a route is loaded. The parse splits on commas and handlesRWY:,SID:,STAR:andAPPCH: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_apprte_sid, and thedes_*equivalents for the destination- selection lists:
create_sid_list110538,create_star_list111084,create_app_list111462
- Flight plans:
Output/FMS plans/*.fms,*.fmx,*.fmlandb738x.xml(SimBrief/datalink), read and written byimport_fms_v1115397,import_fmx16801,import_fml17230,export_fms16699 andexport_fmx18014. Directory listings come from zibomod (zibomod/filename_list*). - Saved FMC state:
B738X_00.dat … _08.dat(and per variant) viasave_fmc33212 andload_fmc34897. - Configuration:
b738x.cfg,b738x_hw.cfg,b738x_fmod.cfg,b738x_acf.cfg,b738x_status.datandb738x_state.dat(170430–170482), plusOutput/preferences/. - Other:
METAR.rwx(ATIS)Log.txt- debug output:
legs.txt,idle_path,Flight_Recorder.txt,Data_records.txt zibomod.cpp:check_DR171153, 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_dataandidx_*ref_data_*,des_data_*andrte_sid/star/appfrom 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.datand theB738X_apt/rnwcaches with thev1.1header) 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 CIFPread_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 ata_fms4892–5030.Dataref Meaning laminar/B738/fmc1/LineNN_Llarge white, NN = 00 title … 06 …_Ssmall white …_Xsmall label row above line NN …_Iinverse …_Mmagenta …_Ggreen …_Ccyan; only Line00_C…_LX,…_GXlarge / green label rows, 01–06 Line04_SIsmall inverse Line_entryscratchpad, white; also messages such as "NOT IN DATA BASE" Line_entry_Iscratchpad, inverse 57 datarefs per CDU. The page functions write
line0_l…line6_xglobals.B738_fmc_display(130543) andB738_fmc_display2(130727) copy them withtxt_trunc(…, 24)whendisplay_update == 1. Page dispatch is inB738_fmc_disp_capt(130906) andB738_fmc_disp_fo(131115).Lights:
Light Dataref Where it is set / drawn EXEC laminar/B738/indicators/fmc_exec_lightsand…_fo(5098; drawn byobjects/737_cockpit_ind.obj); alsolaminar/B738/fmod/fmc_exec_lights*(5096)B738_exec_light_cpt166929,_fo167088MSG laminar/B738/fmc/fmc_message,fmc_message2,fmc_message_warn,_warn2(5702–5705)queue in add_fmc_msg136323,B738_fmc_msg134926,B738_fmc_clr_msg135881,B738_fmc_light135812OFST / DSPY / FAIL fmc/fmc_offset,fmc_dspy,fmc_fail(5707–5712)Keys:
laminar/B738/button/fmc1_*andfmc2_*(§1.2), with handlersB738_fmc1_<key>_CMDhandler(phase, duration). The cockpit model clicks them: 69 per side inb738_cockpit.obj.PANEL_3D (
737_80NG.acffrom line 171370): agen_rotary FMCbackground (IMAGE FMC/fmc,SHOW_EQUAL 1 laminar/B738/fmc1/fmc1_show), thenGROUP Line S L I Mwith onegen_textper layer. For exampleLINE 0 Lat POS 560.5/1228.5,IMAGE TYPE/TEXT/TXT.fmc.big.white, OFFSET_X −540. Eachfmc1/fmc2LineNN_*dataref appears once.tools/msfs/build_display.py:46–47maps the regionsLU_CDU_CPT(1,549)–(549,1017) andLU_CDU_FO(550,549)–(1099,1017).build/LU738_INTERIOR.manifest.jsonpanel_surfaceshasSCREEN_LU_CDU_CPTandSCREEN_LU_CDU_FO(obj line 32201, 2 triangles each).
Work in progress, uncommitted, in another session:
avionics/src/du/CduSources.tsalready maps allLineNN_*refs to aCduTextstore.avionics/src/instruments/CDU,CDU_FOexist.fmc1_showis temporarily set fromBUS_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_20FMS/vref_15/25/30/40,FMS/approach_speed,approach_wind_corrfms/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_distgp_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:493already hand-portedmmr_active_mode. - EFB options:
hardware_thr_lever,yoke_disco_ap,vnav_common, … These are already C++ defaults inwasm/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, doors737u/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_alt1000,fms_N1_mode7,no_perf1,end_route1,fac_xtrack999,fmc_cruise_alt0, … - 420–435: N1 limits 0.952 / 0.956 / 0.935
- 269:
fmcWarnnever
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_*anddir_seg1/2/3_*. These are[256]arrays; 174fms/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:213loadsNavDBfrom--xplane. That is C++ for the XPLM nav API used by zibomod (navdb.cpp:52–60:Custom DataorResources/default dataearth_nav/fix.dat, andapt.dat).It loads every
plugins/*/lin_x64/*.xpl(main.cpp:225–240), so XLua runs all 15 modules includinga_fms. The log showsI/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_Pathresolved to that root here, and theB738X_*.datcaches were written there.No X-Plane navdata exists in the repository or on this machine. The repository's
B738X_*.datfiles are 4 empty header lines (gitignored), so the oracle FMS knows no airport. The demonstration showedNOT IN DATA BASEfor KSFO andNAV DATA OUT OF DATEwith synthetic files.Driving the CDU works today. The replay protocol has these records:
0x01declare0x02frame0x04command begin/end, e.g.laminar/B738/button/fmc1_*- reply section
0x82for string datarefs (bridge.cpp:397–410, 539–543)
tools/cdu_probe.pyread 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-testJSON runner sends commands ({"cmd": …}).
7.2 Gaps#
printandtraceonly format numbers (sci_test.cpp:287show()uses%g).probeskips string datarefs (sci_test.cpp:351).
7.3 What comparing would take#
- Add string support to
print/traceinsci_test.cpp(small), or use the replay bridge ascdu_probe.pydoes. - 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 aLog.txt/scenery_packs.inistamp so the caches are accepted, or the 4-line empty header as now. - Write CDU scenario scripts: keys → expected
LineNN_*per frame, plus the FMS→AFDS numeric outputs (§5.1 list, e.g. viazibo:memory reads or the datarefs). Compare frame by frame, as the IRS and AFDS differential tests already do. - For flight phases, feed recorded MSFS flight states as
setinputs.
8. Feasibility and risks#
8.1 Per-frame work#
Every frame, after_physics (171531–172838) calls, in order:
B738_fmc_on, the EXEC lightsB738_calc_rte2,B738_calc_rte8B738_gls,B738_calc,calc_step_pointB738_fmc_calc,B738_mod_change2,B738_vnav_calc,B738_vnav_calc_mod,B738_vnav_pth3B738_restrict_data,B738_fix_calc,B738_vor_sel,B738_ils_disagreeB738_displ_rnw2,B738_displ_fix,B738_displ_acf(ND symbols)B738_fmc_disp_capt,B738_fmc_disp_foB738_flight_phase3, the N1 functions,B738_irs_sysB738_fmc_msg,B738_fmc_clr_msg,B738_vnav_desc_spd,B738_wind,B738_calc_vnav_spdB738_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-optis 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.
- Binaryen asyncify-based setjmp/longjmp.
- 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:
longis 32-bit, so%dbreaks above 2^31. Heap about 100 MB. - Dataref ownership:
a_fmscreates 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.calcmust 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%dtruncation: 1,987 call sites; it happens about 1,082 times in a 30 s scenario. - Integer arguments to
math.randomandstring.*. - 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).
- The
Patch fengari's float→string conversion to
%.14g. This coverstostring(568) and number concatenation (in about 5.3k..lines) without touching call sites.XLua's
namespace_lenand__pairsmetamethods are honoured in 5.3 but ignored in 5.1. That matters only for#andtable.getnon namespace proxies; Zibo applies them toraw_tables, so there is no effect.Error recovery is free in JS (
pcallworks). Nosetjmpproblem.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.0printed as0.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
FacilityLoadergives 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/longjmpviawasm-opt --asyncify, verified thatpcallcatches an error.- Module features still MVP (
wasm/tools/check_wasm.py). a_fmsrunning 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:
- 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.
- Its per-frame cost is dominated by glue we can rewrite in C without touching Zibo's code.
- 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)#
- Spike / gate:
- Vendor Lua 5.1.5 into
third_party/. - Use
LUA_INTFRM_T=long long, and noio/osbeyond what is shimmed. - Build it with the SDK flags, plus a Binaryen
wasm-opt --asyncifypass forsetjmp/longjmp. Restrict it withasyncify-imports/onlylistto 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_fmsandcalcon the scenario in MSFS. Measure ms/frame, heap and startup.
- Vendor Lua 5.1.5 into
- Dataref bus:
- A name-keyed store in the systems module.
- Bindings to
AfdsGenmembers viamember()andzibomod_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–181and 420–435.
- 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_wptap_vnav(replace the stub),vnav_calc_speed,vnav_init- with replay differential tests
- Navdata provider:
- (a) A generator writing X-Plane-format files from MSFS facility data (SimConnect
RequestAllFacilitiesandRequestFacilityData: ROUTE, RUNWAY, DEPARTURE, ARRIVAL, APPROACH, HOLDING), for the oracle and as a\workcache. - (b) Native replacements for
read_navdata2,read_apt_dat,read_rnw_dat,read_awy_dat2,create_idx_tableand CIFPread_ref_data/read_des_datathat fill identical Lua tables. Prefetch procedures asynchronously. - (c) Table-equality tests against the Lua loaders on the generated files.
- (a) A generator writing X-Plane-format files from MSFS facility data (SimConnect
- Oracle:
- String support in
--sci-testprint/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).
- String support in
- 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.
- 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*).