技术文档

ND route, symbols and map (zibomod's OpenGL ND, its symbol datarefs, its navaid / airport map) — builds 61–64

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

Status 2026-11-07: zibomod's ND drawing translated with the LNAV (wasm/src/core/lnav/) and checked against the oracle: the route flown in docs/LNAV.md's scenario, the captain's ND in MAP at 40 NM then PLN, 119 drawn frames, every vertex and colour of the drawing the same as zibomod's OpenGL calls (and the LNAV replay still 0 differences, strict too). In the systems module; the NDs draw it on a canvas.

What it is#

Zibo's ND is two parts. The panel's gauges (PANEL_3D programs, our avionics/src/instruments/ND) draw the compass, the aircraft symbol, the text, and the symbols zibomod places through datarefs (nd/wpt_*, object_*, ...). The route itself — the active (magenta) and modified (white dashed) legs, the turn arcs, RF / AF legs, holds, procedure turns, the runway, the PLN mode's range circles — zibomod draws with OpenGL straight onto the panel texture, in its draw callback after the panel (MainDrawCallback → NDRender_Main_opengl → NDRender_opengl: glOrtho(0, 1, 0, 1), glBegin(GL_LINES), glVertex2d, glColor4fv, glLineWidth).

Its parts (all translated, tools/re/lnav_functions.txt):

  • NDRender_xplane / NDRender_xplane2 (in the flight loop, between the navigation block and fms_turn): the ND's centre and scale (nd_ctr_x, nd_ctr_y), the aircraft's place on it (nd_x_cur, nd_y_cur), the symbol datarefs (wpt_*, decel_*, tc_*, ...);
  • NDRender_opengl (123 KB of code): per leg DrawRouteLine_wpt / DrawRouteLine_wpt2 (the dashed modified route), DrawRouteArcCtr / DrawRouteCircCtr (turns, arcs, holds), DrawRunway, DrawCircle, clipped to the ND (Clip_rect, Clip_circle, MaskND*), placed by TransformX / TransformY on the panel texture's ND rectangles (calc_panel_coord: captain x 521..1021, y 8..508 of 2048, y up; first officer y 520..1020).

How it runs here#

Systems::updateLnav (systems_lnav.cpp), in zibomod's order: the navigation block (lnav_frame); the ND's inputs (ndInputs: what MainFlightLoopCallback copies before NDRender_xplane — the EFIS mode and range, the headings, the DC buses, the brightness); NDRender_xplane, NDRender_xplane2; fms_turn. Then sendNdDrawing (15 times a second): Lnav::drawNd() runs NDRender_opengl with lnav.h's GL recorder (the same stream xphost/src/gl_capture.cpp records from zibomod), encodes each primitive as a line mode r g b a width n x y ... (colour 0–255, vertices in panel texture pixels) and sends it on the CommBus (LU737_ND_GL) when it changed. avionics/src/du/NdDraw.ts: each ND (ND.tsx, ND_FO.tsx) draws the lines on a canvas over its program, clipped to its own rectangle (the panel.json rect: captain [530, 1552, 1010, 2039]).

The EFIS's mode, range, DATA / POS / TFC reach the FMS store as Zibo's glareshield datarefs (systems_fms.cpp addFmsGlareshieldSources), where the drawing reads them.

How it is checked#

wasm/test/test_lnav_replay.cpp lnav_replay_matches_oracle, extended:

  • the oracle (xphost/src/sci_test.cpp) hooks NDRender_Main_opengl → NDRender_opengl ("nd": the drawing's inputs, 3,125 columns) and, each 30th frame ("trace_gl": 30), runs the draw callback with gl_capture and logs its stream ([gl] <frame> <count> <floats>; each 30th: the drawing changes each frame the aircraft moves, the whole stream each frame would be 22 MB of golden data);
  • the replay runs NDRender_xplane / NDRender_xplane2 where zibomod does, checks what they computed against the drawing's inputs (lnav_nd_check), loads the inputs, runs drawNd and compares the stream float by float (16 ulps): 119 frames, 0 differ;
  • test_fms.cpp lnav_flies_the_route_in_the_systems: in the systems module, after LNAV engages, the CommBus drawing has magenta lines and every vertex inside the NDs.

The scenario (tools/re/lnav_scenario.py): the captain's range up twice (40 NM) before LNAV, PLN for the last two legs.

Translation fixes this needed (tools, not hand edits)#

  • decompile.py: a function returning a double straight from arithmetic (TransformX: addsd %xmm1,%xmm0 before ret) is typed double; Ghidra had typed it float and its callers stored the coordinates as integers.
  • port_cpp.py: a global read through a general register (mov last_x,%rax) and written with movsd is a double, not a pointer (last_x had become a long long: every arc collapsed to a vertical line).
  • lnav_fixups.py: the RGBA colour arrays on the stack Ghidra splits into locals and merges into 8-byte pieces (CONCAT44) become float arrays; two int counters in one 8-byte local; a stack array Ghidra declares short with its tail as separate locals (NDRender_xplane2's six-element dataref buffers: XPLMGetDatavf wrote past it); std::operator+ / += / std::to_string; glLineWidth's lost argument (2.0f from .rodata); the negate mask's other operand order.
  • gen_lnav.sh: a signature Ghidra wraps over lines (interset_line_circle).
  • calc_panel_coord (the NDs' rectangles, set once at zibomod's start) translated too; initLnav calls it.

Found on the way (the FMS, not only the ND)#

  • The inserted points' positions (fms_add_data_2..6: the latitude, longitude, bearings, distance of a point the navigation adds to the route, which the Lua FMS copies into the modified route) were published as integers' bits: Ghidra wrote their 4-byte copies as *(int *)… = legs_lat[i], which C++ converts (47.36 → 47 → read back as 6.6e-44). In build 60 too: after a DIRECT TO the modified route could hold a point at latitude 0. Fixed in the tools: port_cpp.py types an array handed to XPLMSetDatavf as floats as float; lnav_fixups.py turns an unknown-4-byte store of a float into a float array into a float store (70 stores, only these arrays).
  • Clip_rect (the line clipper) never ends on a NaN end point; that NaN came from the point above. zibomod would hang X-Plane there; ours does not draw a NaN line (lnav_fixups.py, the only behaviour change, for NaN only).

Checked by eye#

avionics/scripts/render-du.mjs takes LU737_ND_GL=<file> (a drawing as the systems module sends it) and feeds it through a CommBus stub. The headless ND renders its program black at present (also at build 60: the renderer, not this change), so the oracle's drawing (frame 4050, MAP) was laid over docs/nd_cruise.png: the route starts at the aircraft symbol's apex and the track line's ticks fall on the range rings (placement and scale right).

The oracle's captain ND draws nothing once the route has sequenced to its last leg (LNVAE → KPDX, frame 6150 on, in PLN): zibomod's own behaviour in this scenario, which ours repeats; to be looked at with a longer route.

The symbols beside the route (build 62)#

The waypoints' labels (name, altitude / FL, ETA, RNP: nd/wpt_id00m …, wpt_alt, wpt_eta, wpt_rnp, the m / w magenta / white pairs, 20 per ND), their positions and kinds (wpt_x, wpt_y, wpt_type), T/C, T/D, S/C, E/D, DECEL (tc_*, td_*, sc_*, ed_*, decel_*), the GPS / IRS positions are zibomod's: the ND parts write them (NDRender_xplane, NDRender_opengl → displ_type_id), the ND programs read them as datarefs.

  • Translation: the labels are C strings sprintf / strcpy'd into 16-byte buffers (char B738DR_wpt_id00m[16]; Ghidra had typed them float[4], port_cpp.py now types a strcpy / sprintf destination char). lnav_fixups.py c_strings routes them to real str_sprintf / str_strcpy (lnav.h) and puts Ghidra's leading floating-point arguments back in the format's order (sprintf((double)rnp, buf, "RNP %4.2f")); the debug traces' lines and NDRender_xplane2's FIX page labels (into split locals) stay no-ops.
  • Checked: the oracle's nd_out hook (after NDRender_opengl) traces the 402 symbol datarefs the NDs read (labels byte by byte); the replay compares them after its drawing: 119 frames, 0 differ (strict too).
  • To the NDs: lnav_sync.py lists them (kNdSymbols, avionics/src/du/NdSymbols.ts); Systems::sendNdSymbols sends the changed ones on the CommBus (LU737_ND_SYM, path<TAB>value lines, all each 2 s) with the drawing; NdSources.ts keeps them (ndSymbols) and the ND programs' sources read them (numbers) and ND_STRINGS (labels) instead of the ground values. Test lnav_flies_the_route_in_the_systems: 516 values, LNVAA labelled.

The map: stations, airports, fixes (build 63)#

STA, ARPT and WPT on the ND are zibomod's too (nd_earth_perf / _calc / _display, nd_apt_perf / _calc / _display), on navdata zibomod reads itself at its start (read_earth_navdat, read_earth_fix, read_earth_fix2, read_apt: X-Plane's Custom Data/earth_nav.dat, earth_fix.dat, user_fix_georef.dat, Zibo's B738X_apt.dat): 100-byte records in a 50 MB table (earth_nav), 28-byte airports (apt_nav), ILS / GLS. Each frame, between the navigation block and NDRender_xplane (MainFlightLoopCallback 0x17ee30; Lnav::nd_map_frame): the time-sliced search near the aircraft into two pages (nd_*_perf: 25 rows a frame), then the positions (nd_*_calc) while the Lua FMS's xfirst_time2 counts 1..4, the symbols (nd_*_display: nd/object_*, nd/apt_*) at 0. All translated (the loaders too: their std::ifstream / std::getline on C stdio, lnav.h).

  • In the systems module: the navdata provider's files in the FMS's work folder (\work\xp\Custom Data\…), read again each time the FMS loads them; the symbols go with the others (LU737_ND_SYM: 576 values now).
  • The EFIS's STA / ARPT / WPT / WXR reach the FMS store as Zibo's glareshield datarefs (EFIS/EFIS_vor_on, EFIS_airport_on, EFIS_fix_on, EFIS_wx_on, the first officer's under EFIS/fo/).
  • zibomod skips earth_nav's first 80 rows (its custom waypoints' place): irrelevant with the world's 350,000; the test worlds start with 85 stations far away.

Checked:

  • lnav_loaders_match_zibomod: the oracle dumps zibomod's tables after its start ([dump]); ours from the same files: earth_nav, ils_nav, gls_nav, apt_nav byte for byte (two padding bytes of an ILS row zibomod fills from its uninitialised stack excepted).
  • The replay: the map's inputs at the oracle's map hook (with lnav_out, before nd_earth_perf), the map's own state (its pages, 132 KB each, untraced) the replay's after the first row; its symbols in nd_out.
  • lnav_flies_the_route_in_the_systems: SEA (STA) and an airport ahead (ARPT) on the captain's ND; KSEA behind the aircraft is not drawn, as zibomod does.

Translation tools this needed (port_cpp.py, lnav_fixups.py; no hand edits):

  • arrays of records (fields of different types at byte offsets, a stride that is no element size): bytes; C string buffers (strcpy / sprintf destinations) stay bytes whatever the instructions' votes;
  • a function whose code relies on its stack layout (a record copied into adjacent locals, C strings across them): its locals laid out as zibomod's frame in one byte array (frame_locals; the frame's top at 8 mod 16 as on x86-64), arrays as pointers into it (the code reads past them);
  • Ghidra's CONCATxy (bit concatenation), 6-byte unknowns, float fields read and written through 4-byte unknown copies (*(int *) of a float: its bits), the C string library on stack addresses, .rodata literals in strncmp / strcat;
  • the native tests build with -fno-strict-aliasing (the WASM too) and without the alignment sanitizer: the translated code reads unaligned pieces of records as zibomod does (x86 and WASM allow it).

Nearby navdata (build 64, owner's choice: plan 1 + 4)#

zibomod reads the whole world at X-Plane's start (350,000 rows in MSFS: one frame of parsing, a 50 MB table). Its map only uses rows within 145 NM (stations) / 25 NM (fixes) after a +-8 degree filter, so the systems module reads only the rows near the aircraft (lnav/near_navdata.h):

  • a box of +-10 degrees of latitude around the aircraft (longitude widened by the latitude), read 2 ms a frame from the navdata provider's files (no stall: about 2 s for the world in the native test; WASM a few seconds), then loaded into zibomod's tables at once (9 ms natively for ~4,000 rows) — through zibomod's own loaders, the kept text served in place of the file (Lnav::fileTexts);
  • read again when the FMS loads the files again or the aircraft is 4 degrees from the box's centre; the map keeps the old rows until the new ones are in;
  • earth_nav sized to the rows read (char *earth_nav, Lnav::earthNavRows_): 0.4 MB instead of 50 MB;
  • the navaid file's first 80 data rows are kept as they are: zibomod's map skips earth_nav's first 80 rows;
  • after a load, zibomod's first-frame search (first_time 0 for a frame): the map's pages are filled at once (its search otherwise takes 25 rows a frame: minutes for the world, a few frames for the box).

Checked: lnav_replay_near_navdata is the oracle replay with the map's navdata read this way (the 5 far stations after the first 80 rows dropped): 0 differences, as with all of it. near_navdata_reads_the_world_in_steps: a synthetic world of 380,000 rows read in 60 frames of 2 ms, 4,381 rows kept; the reload distance.

Picture#

docs/nd_map.png: the same frame with the map (STA, ARPT, WPT on): zibomod's stations (LNV VOR, OLM VORTAC), airports (KTCM; KOLM under OLM: the test world puts them together), fixes (LNVAA behind, LNVAC) as its symbol datarefs give them, through our ND program.

docs/nd_route_labels.png: the captain's ND (MAP, 40 NM) with the oracle's frame 4050 — zibomod's drawing and symbol datarefs as the systems module sends them — through our ND program:

LU737_ND_GL=<drawing> LU737_ND_SYM=<symbols> LU737_RENDER_VARS='{"A:PLANE HEADING DEGREES MAGNETIC|degrees": 206.6, ...}' \
    node avionics/scripts/render-du.mjs ND <outDir> cruise

(the heading, track and EFIS range set to the frame's; the rest — wind, GS, the snapshot's other values — from the ND's cruise snapshot). The renderer had painted every DU black since the displays' power gating (BUS_SRC, build 40s): it now gives the DU buses power.

Not yet#

  • The FIX page's circles / radials (fix_*, NDRender_xplane2): its labels are written now (its stack frame laid out as zibomod's) but no scenario enters a FIX page yet: not checked.
  • TCAS, terrain, weather.
  • The NDs' BRT knobs: full (the drawing's alpha 1; Zibo's default knob 0.8).
  • POS: the GPS / IRS positions are the aircraft's (MSFS has no separate sensor drift).
  • VOR / ADF bearing lines (simDR_nav1_bearing): not given yet.