Status 2026-10-31, milestone M2. Source: the Linux build of zibomod
(plugins/zibomod/lin_x64/zibomod.xpl, read with tools/re/decompile.py and
its disassembly). Rewrite: wasm/src/core/engine/cfm56_7b.h/.cpp
(namespace lu737::engine::cfm56). Proof: wasm/test/test_cfm56_7b_zibo.cpp
calls every original function directly in the plugin and requires the same
float bits from the rewrite; all 25 functions match bit for bit on every
tested input, including NaN, ±inf, negative and out-of-range inputs.
| zibomod | rewrite | computes |
|---|---|---|
Rescale |
rescale |
clamped linear map |
Rescale_c |
rescaleLinear |
unclamped linear map (extrapolation) |
Limit |
limit |
bound a value |
calc_tamb_by_alt |
isaTemperature |
ISA static temperature from altitude |
calc_tat_by_mach |
totalTemperature |
TAT from static temperature and Mach |
corrected_cn1_to_n1 |
correctedToN1 |
physical N1 from corrected N1 |
corrected_n1_to_cn1 |
n1ToCorrected |
corrected N1 using the ISA TAT at altitude/Mach |
corrected_n1_to_cn1_tat |
n1ToCorrectedTat |
corrected N1 using a measured TAT |
n1_thruse_norm / thruse_n1_norm |
n1ToThrustUse / thrustUseToN1 |
N1 ↔ normalised thrust |
cfm56_7b_cn2_to_cn1 / cfm56_7b_cn1_to_cn2 |
cn2ToCn1 / cn1ToCn2 |
corrected N2 (rpm) ↔ corrected N1 |
cfm56_7b_cegt_to_cn1 / cfm56_7b_cn1_to_cegt |
cegtToCn1 / cn1ToCegt |
corrected EGT (K) ↔ corrected N1 |
cfm56_7b_thrust / cfm56_7b_cn1 |
cn1ToThrust / thrustToCn1 |
corrected N1 ↔ net thrust (lbf) |
cfm56_7b_fuel_flow / cfm56_7b_ff_thr |
thrustToFuelFlow / fuelFlowToThrust |
net thrust ↔ fuel flow |
cfm56_7b_min_idle_ff |
minIdleFuelFlow |
fuel flow at minimum idle |
min_idle_thrust |
minIdleThrust |
net thrust at minimum idle |
All of them are pure functions of their arguments plus, for the table
functions, the global plane_on_ground (an explicit onGround argument in
the rewrite). zibomod is compiled without optimisation, so its float32 and
double operations happen exactly in source order; the rewrite keeps that
order, which is why the results are identical.
1. Conventions#
Inputs as Zibo passes them. Altitude is pressure altitude in feet
(zibomod's simDR_pressure_altitude), Mach is simDR_mach_no, TAT is
simDR_TAT in °C. N1 is in %, N2 in rpm (Zibo's
gauges show N2 % = rpm / 14,460 × 100), EGT in kelvin (the gauge subtracts
273.15), thrust in lbf. "Corrected" values are referred to ISA sea level
(15 °C); Zibo converts with the corrected_* functions before and after
the tables.
Ground Mach. Every table function first does
if (plane_on_ground == 1 && Mach < 0.08) Mach = 0 (comparison in double).
Sub-idle floor. cfm56_7b_cn2_to_cn1 and cfm56_7b_cegt_to_cn1 return
19.1 (float 19.1f) when the result is below 20 and plane_on_ground == 0;
the original also formats a message ("Zibomod plugin: cn2 to cn1 limited =
…", "… cegt to cn1 limited = …" with the inputs and simDR_TAT) and sends it
to XPLMDebugString. The rewrite applies the floor without logging.
Band cascades. The tables are split into Mach (and altitude) bands by
nested tests written if (m <= 0.2) … else if (m <= 0.3) …. The test
against 0.5 compares floats (m <= 0.5f); all other Mach thresholds promote
the Mach number to double ((double)m <= 0.3), so for example Mach 0.3f
(0.30000001) is above the 0.3 threshold. Altitude thresholds are floats.
Because every source test is an ordinary C comparison, a NaN fails every
test and ends in the last band; the rewrite writes each test in the same
direction (x < xs[0], m <= t, alt < 20000, …) so NaN takes the same
path.
Lookups. A breakpoint lookup finds the first segment whose upper
breakpoint is ≥ x (searching up from index 1). Inside a segment the value is
interpolated with Rescale (clamped) or Rescale_c (linear), depending on
the function (stated below). Above the last breakpoint (and for NaN) the
last segment is extrapolated with Rescale_c. Below the first breakpoint
the functions either extrapolate the first segment with Rescale_c, hold
the first output value, or do no test at all (the first segment's
Rescale then holds its first value). Tables whose rows are not monotonic
(EGT rows dip in places) are searched the same way: the first segment whose
upper breakpoint is ≥ x wins.
2. Primitives#
Rescale(x0, y0, x1, y1, x): ifx1 >= x0:x0 > x→ y0,x > x1→ y1; otherwise (also for NaN in x0/x1):x > x0→ y0,x1 > x→ y1. Thenx1 == x0→ y0, else(x - x0) * (y1 - y0) / (x1 - x0) + y0in float.Rescale_c(x0, y0, x1, y1, x):d = x1 - x0;d != 0→(x - x0) * ((y1 - y0) / d) + y0, else 0 (also 0 for d == 0 with any x).Limit(v, lo, hi):lo > hi && v > hi→ hi;lo > v→ lo;v > hi→ hi; else v.calc_tamb_by_alt(alt)=Rescale(0, 15, 37000, -56.5, alt)(°C).calc_tat_by_mach(t, M)=(float)(((double)t + 273.15) * (1 + 0.2·M·M) - 273.15), computed in double.
Callers: Rescale/Rescale_c/Limit everywhere; calc_tamb_by_alt in
corrected_n1_to_cn1, corrected_ff2, ff_corr_coef, Thr_shift;
calc_tat_by_mach in corrected_n1_to_cn1, Thr_shift.
3. Temperature corrections of N1#
Exponent k of the temperature ratio θ: 0.5 below 30 % N1, then linear
(Rescale) through 90 → 0.485, 95 → 0.475, 98 → 0.45, 100 → 0.42,
101 → 0.4175, and 0.4175 above 101 %. The exponents are float constants
(0.485f …) in the interpolation and double constants (0.485 …) where
corrected_cn1_to_n1 moves the breakpoints.
corrected_cn1_to_n1(cN1, θ)→ N1 (%). θ ≤ 0 → 0. The breakpoints are moved to corrected N1:b = (float)(N / pow((double)θ, e))for (N, e) = (30, 0.5), (90, 0.485), (95, 0.475), (98, 0.45), (100, 0.42), (101, 0.4175) (libmpow); k is interpolated between the moved breakpoints (testsb > cN1), resultpowf(θ, k) * cN1. Zibo passes θ = (TAT + 273.15) / 288.15. Callers:Engine_n1_n2,Starter,Thr_shift(6).corrected_n1_to_cn1(N1, alt, M, ref)→ corrected N1. θ = (float)((ISA-TAT(alt, M) + 273.15) / (ref + 273.15)) with ISA-TAT =calc_tat_by_mach(calc_tamb_by_alt(alt), M); θ ≤ 0 → 0; k from N1; result N1 / powf(θ, k) if powf(θ, k) > 0, else N1. Zibo passes ref = 15. (The original also computes an unusedpow(θ, 0.5).) Callers:Engine_n1_n2,EFBRender_opengl(16, the EFB performance pages).corrected_n1_to_cn1_tat(N1, TAT, ref)→ the same with θ = (TAT + 273.15) / (ref + 273.15). Callers:Engine_model(6),Gauge_N1_N2_EGT(2).
4. Thrust use (n1_thruse_norm, thruse_n1_norm)#
One curve of 58 points: normalised thrust 0 … 1.12 (steps 0.001 to 0.02,
0.01 to 0.2, then 0.05; last 1.12) against N1 19.019285 … 104.741905 %
(kThrustUse, kThrustUseN1). The first four N1 values are equal.
n1_thruse_norm(N1): first i with N1 < N1[i]: i = 0 → 0, else Rescale
of segment i-1..i; past the end → 1.12. thruse_n1_norm(T): the inverse
with T < T[i], i = 0 → 19.019285, past the end → 104.741905. Callers:
Calc_request_n1, Calc_request_thrust (autothrottle N1/thrust requests).
5. N2 tables (cfm56_7b_cn2_to_cn1, cfm56_7b_cn1_to_cn2)#
Arguments (x, pressure altitude ft, Mach); cn2_to_cn1 takes corrected N2
in rpm and returns corrected N1 %, cn1_to_cn2 the inverse. x is limited
to 0 … 16,000 rpm resp. 0 … 108 %, Mach to 0 … 0.9f (after the ground rule).
Tables (rpm). Zibo adds a trim row to the base rows at start-up (a trim of exactly 0 would count as 1); the values after the trim are:
Below 20,000 ft — corrected N1 19, 20, 30, 40, 50, 60 % (kN2LowN1), trim
+290, +290, +500, +450, +320, +200:
| Mach | 19 | 20 | 30 | 40 | 50 | 60 |
|---|---|---|---|---|---|---|
| 0.2 | 8290 | 8490 | 10700 | 11750 | 12220 | 12600 |
| 0.3 | 8140 | 8340 | 10600 | 11750 | 12220 | 12600 |
| 0.4 | 8140 | 8340 | 10350 | 11650 | 12170 | 12600 |
| 0.5 | 8090 | 8290 | 10150 | 11650 | 12120 | 12600 |
| 0.6 | 7340 | 7540 | 9900 | 11400 | 12120 | 12600 |
From 20,000 ft — corrected N1 30, 40, 50, 60 % (kN2HighN1), trim +500,
+450, +320, +200:
| Mach | 30 | 40 | 50 | 60 |
|---|---|---|---|---|
| ≤ 0.6 | 9600 | 11250 | 12095 | 12500 |
| 0.7 | 9000 | 10950 | 12170 | 12500 |
| 0.8 | 8750 | 10825 | 11820 | 12500 |
| 0.9 | 8300 | 10650 | 11670 | 12500 |
Mach bands (m <= 0.2, <= 0.3, <= 0.4, <= 0.5f, <= 0.6, <= 0.7,
<= 0.8, else): two low-altitude rows and two high-altitude rows with the
Mach of the band edges. Below Mach 0.2 the low rows are a Mach 0 row
extrapolated (Rescale_c) from the 0.2/0.3 rows and the 0.2 row; above 0.6
both low rows are the 0.6 row; up to 0.6 both high rows are the "≤ 0.6" row.
cn2_to_cn1:
- below 20,000 ft: threshold
thr = Rescale(0, 12220, 20000, 12500, alt). N2 ≥ thr (or NaN): above the tables, linear (Rescale_c) to 108 % at 14,755 rpm from (12600 rpm, 60 %) at sea level and to 108 % at 15,050 rpm from (12500, 60 %) at 20,000 ft, blended in altitude withRescale. Otherwise by Mach: ≤ 0.5f: low tables (segmentRescale, below and above extrapolated); ≤ 0.6: the upper-Mach low row is first blended between 19,000 and 20,000 ft towards the high table read at the low breakpoints (Rescalein altitude, no effect below 19,000 ft), then the low tables with 19 % held below the first breakpoint; < 0.7: the lower-Mach high row is blended the same way towards the low table, then the high tables (extrapolated below/above); ≥ 0.7: high tables. - from 20,000 ft: N2 ≥ 12,500 → linear to 108 % at 15,050 rpm. Otherwise
Mach ≤ 0.6: the same blend as above, which at this altitude replaces the
upper-Mach low row by the high table, then the low tables (hold below);
≤ 0.7 or above: high tables. The final value is
Rescalebetween the two Mach rows' results.
cn1_to_cn2 mirrors this with N1 as the input axis (segments Rescale,
extrapolated below/above): below 20,000 ft N1 ≥ 60 → Rescale(60, 12600, 108, 14755) at sea level and Rescale(60, 12500, 108, 15050) at 20,000 ft,
blended with Rescale; from 20,000 ft N1 ≥ 60 → Rescale(60, 12500, 108, 15050, N1).
Callers: cn2_to_cn1 in Engine_n1_n2, Starter, Thr_shift (3);
cn1_to_cn2 in Engine_n1_n2 (3), Gauge_N1_N2_EGT (2, N2 gauge).
6. EGT tables (cfm56_7b_cegt_to_cn1, cfm56_7b_cn1_to_cegt)#
Arguments (x, pressure altitude ft, Mach); cn1_to_cegt takes corrected N1
% and returns corrected EGT in K, cegt_to_cn1 the inverse. Mach limited to
0 … 0.9f, x not limited.
- Up to 25,000 ft (
alt <= 25000): N1 axis of 19 points (20, 30, 40, 42.5, … 55, 60 … 105) × rows for Mach 0.2, 0.3, 0.4, 0.5, 0.6 (kEgtLowN1,kEgtLow, 635 … 1155 K). Bands<= 0.3,<= 0.4,<= 0.5f, else; the 0.2-0.3 band extrapolates down to Mach 0, the 0.5-0.6 band up. - From 19,000 ft (
alt >= 19000): N1 axis of 23 points (40 … 90 in 2.5 % steps, 95, 100, 105, 110) × rows for Mach 0.6, 0.7, 0.8, 0.9 (kEgtHighN1,kEgtHigh, 638 … 1260 K). Bands<= 0.7,<= 0.8, else; read only when(double)m <= 0.9(always, after the limit). - Every step uses
Rescale_c: segments, below the first and above the last breakpoint, and the Mach interpolation (so Mach 0 extrapolates the 0.2/0.3 rows). The result is the low table below 20,000 ft,Rescalebetween the two from 20,000 to 25,000 ft, and the high table from 25,000 ft.
cegt_to_cn1 reads the same rows backwards (EGT row as the input axis) and
then applies the in-flight 19.1 % floor. Callers: cn1_to_cegt in
Gauge_N1_N2_EGT (EGT gauge: Rescale(0, TAT, 58, cEGT - 273.15, p) with a
per-engine parameter p); cegt_to_cn1 in Thr_shift (3).
7. Thrust tables (cfm56_7b_thrust, cfm56_7b_cn1)#
Arguments (x, Mach), no altitude; Mach is not limited. cfm56_7b_thrust
takes corrected N1 % and returns net thrust in lbf; cfm56_7b_cn1 the
inverse. N1 axis of 20 points: 0, 20, 25, 30 … 105, 107.5 (kThrustN1);
rows for Mach 0.0 … 0.9 (kThrust, −3150 … 27,800 lbf; 27,600 lbf at
107.5 % and Mach 0).
Bands <= 0.1 … <= 0.9 (0.5 as float) pick two adjacent rows. Above Mach
0.9: if m <= 1.0f, the band 0.9-1.0 with the Mach 0.9 row and, for
cfm56_7b_thrust, a Mach 1.0 row built per point as
(float)(T − ((double)(2850f + T) · 0.9 − 2850)); cfm56_7b_cn1 leaves that
row at 0. Above Mach 1.0 (and NaN) there are no rows and both functions
return 0. Lookup: no test below the first breakpoint, segments Rescale,
extrapolation above; final Rescale between the Mach rows.
Callers: cfm56_7b_thrust in Engine_model (3, thrust of each engine from
corrected_n1_to_cn1_tat(N1, TAT, 15), scaled by engine-on and 27,300/27,000)
and EFBRender_opengl (8); cfm56_7b_cn1 in Engine_n1_n2 (2).
8. Fuel-flow tables (cfm56_7b_fuel_flow, cfm56_7b_ff_thr)#
cfm56_7b_fuel_flow(thrust lbf, alt ft, Mach) → fuel flow;
cfm56_7b_ff_thr(fuel flow, alt ft, Mach) → net thrust lbf. Altitude
limited to 0 … 41,000 ft, Mach not limited. Thrust axis 0, 1, 16, 18, 20,
22, 24, 25 (1000 lbf; kFfThrust; fuel_flow divides its input by 1000f,
ff_thr multiplies its result by 1000f). 70 rows of 8 fuel flows (kFf),
in blocks by altitude:
| block | rows | Mach | block | rows | Mach |
|---|---|---|---|---|---|
| 0 ft | 0-6 | 0.0-0.6 | 25,000 ft | 31-35 | 0.4-0.8 |
| 5,000 ft | 7-13 | 0.0-0.6 | 29,000 ft | 36-39 | 0.5-0.8 |
| 10,000 ft | 14-19 | 0.2-0.7 | 31,000 … 41,000 ft (2,000 ft steps) | 40-69 | 0.5-0.9 each |
| 15,000 ft | 20-25 | 0.2-0.7 | |||
| 20,000 ft | 26-30 | 0.4-0.8 |
Altitude bands alt <= 5000, <= 10000, … <= 39000, else (39-41,000).
In each band a Mach cascade picks four rows: two at the band's lower
altitude (Mach a0, a1) and two at the upper (Mach b0, b1). Where an
altitude block has no row for the Mach, the nearest pair is used and
extrapolated (e.g. 15-20,000 ft below Mach 0.3 reads the 20,000 ft rows at
Mach 0.4/0.5). The full selection is the kBand* data in the rewrite.
fuel_flow: lookup on the thrust axis (below the first breakpoint
Rescale_c of the first segment, segments Rescale, above Rescale_c of
the last); at the found segment each of the four rows is interpolated, then
Rescale_c in Mach at each altitude and Rescale_c in altitude.
ff_thr: first the two altitude rows are interpolated in Mach with
Rescale_c, then each is searched as the input axis (below/above
extrapolated, segments Rescale), then Rescale_c in altitude, × 1000.
The two functions differ in the 5,000-15,000 ft bands (section 10).
Callers: fuel_flow in Engine_model (2, then corrected_ff2);
ff_thr in Engine_n1_n2 (2, input divided by ff_corr_coef(alt), result
fed to cfm56_7b_cn1).
9. Minimum idle (cfm56_7b_min_idle_ff, min_idle_thrust)#
Arguments (pressure altitude ft, Mach). Mach axis 0 … 0.9 in 0.1 steps
(kMinIdleMach), rows at 0, 5,000, 10,000, 15,000, 20,000, 25,000, 29,000,
31,000 … 41,000 ft (kMinIdleAlt). Fuel flow 810 … 330 (kMinIdleFf), net
thrust 2,625 … −1,675 lbf (kMinIdleThrust).
min_idle_ff: altitude not limited, bandsalt < 5000…alt < 39000, else; Mach not limited:m < 0→ first column; Mach segmentsRescale; above 0.9 nothing is found and both rows read 0; finalRescalein altitude (holds outside 0 … 41,000).min_idle_thrust: altitude limited to 0 … 41,000, bandsalt <= 5000…; Mach limited to 0 … 0.9f;(double)m >= 0.9→ last column; finalRescalein altitude.
Neither function has a caller in this zibomod build.
10. Zibo quirks reproduced#
Kept on purpose (the rewrite must behave like Zibo); each is marked "Zibo:" in the code.
- Swapped axes in synthesised fuel-flow rows. Where Zibo builds a
missing low-Mach row of the 10,000 ft or 15,000 ft block it calls
Rescale_c(T[r], 0.2, T[r+1], 0.3, M)— the fuel flows as the x axis and the Mach numbers as y — so the "row" holds numbers around 0.2-0.3 instead of fuel flows. Effect:fuel_flowat 10,000-15,000 ft below Mach 0.2 returns meaningless small or negative values (5,000 lbf at 12,000 ft, Mach 0.05 → −0.41);fuel_flowandff_thrat 5,000-10,000 ft below Mach 0.1 (andff_thrbelow 0.2) interpolate towards such a row at 10,000 ft. In flight these Mach numbers do not occur at those altitudes; on the ground at high airports they do. - Dead synthesis in
ff_thrat 10,000-15,000 ft: the Mach 0-0.2 rows are built and then always overwritten by the Mach 0.2-0.3 rows (missingelse); the rewrite does not build them. - Zero idle fuel flow of synthesised rows: forced to 0 at
10,000-15,000 ft below Mach 0.1 in
fuel_flowonly when not on the ground, at 5,000-10,000 ft (both functions) only on the ground. cn1_to_cn2above 20,000 ft below Mach 0.2 blends towards high-altitude rows that were never filled (all 0): N2 is roughly halved (40 % N1 at 30,000 ft: Mach 0.3 → 11,750 rpm, Mach 0.1 → 5,875 rpm).cn2_to_cn1above 20,000 ft, Mach 0.6-0.7: the blend of the high row searches only 5 of the 6 low breakpoints, so the 60 % point reuses the 50 % value. Without effect: this path only runs from 20,000 ft, where the blend returns the high-row value unchanged.- Mach 1.0 thrust rows: built from the Mach 0.9 row in
cfm56_7b_thrust, left at 0 incfm56_7b_cn1; above Mach 1.0 both return 0.min_idle_ffreturns 0 above Mach 0.9. - Mixed float/double thresholds (section 1) and the in-flight 19.1 %
floor of
cn2_to_cn1/cegt_to_cn1.
11. Tests and results#
wasm/build.sh test builds one runner (ASan/UBSan) with all native tests;
the differential tests dlopen the original plugin (harness
wasm/test/zibo_direct.h/.cpp: ELF .symtab lookup by plain, mangled or
demangled name, plane_on_ground set before each batch, XPLMDebugString
stub). Each function is compared over the Cartesian product of input axes
(a uniform grid plus every table breakpoint and band threshold with both
float neighbours, ±0, ±1e30, ±inf, NaN, negatives and out-of-range values),
plus 300,000 random points per function (per ground state for the table
functions, which run with plane_on_ground 0 and 1). Equality is bit for bit; NaN must meet NaN.
| function | calls | result |
|---|---|---|
| Rescale / Rescale_c / Limit | 2.6 M / 2.6 M / 1.2 M | bit-exact |
| calc_tamb_by_alt / calc_tat_by_mach | 0.30 M / 0.35 M | bit-exact |
| corrected_cn1_to_n1 / corrected_n1_to_cn1 / corrected_n1_to_cn1_tat | 0.67 M / 1.7 M / 0.64 M | bit-exact |
| n1_thruse_norm / thruse_n1_norm | 0.15 M / 0.20 M | bit-exact |
| cfm56_7b_cn2_to_cn1 / cn1_to_cn2 | 6.6 M / 6.2 M | bit-exact |
| cfm56_7b_cn1_to_cegt / cegt_to_cn1 | 7.5 M / 8.4 M | bit-exact |
| cfm56_7b_thrust / cfm56_7b_cn1 | 1.4 M / 2.9 M | bit-exact |
| cfm56_7b_fuel_flow / ff_thr | 6.5 M / 6.7 M | bit-exact |
| cfm56_7b_min_idle_ff / min_idle_thrust | 0.9 M / 0.9 M | bit-exact |
The results are also bit-exact with clang 18 at -O2 instead of g++ -O1.
wasm/test/test_cfm56_7b.cpp pins original values (and the quirks above)
without the plugin.
WASM. The module builds the same source with clang for wasm32 and the
SDK's wasi-libc. A one-off check (not in CI) ran the engine code compiled
for wasm32 under Node's WASI against the native build: 9.6 M table
evaluations and 1.6 M correction evaluations gave identical bits. The only
library calls are pow/powf in the corrections; wasi-libc (musl) and
glibc both derive these from Arm's optimized-routines (error below 1 ulp,
not correctly rounded), so a last-bit difference between the two libms
remains possible in rare cases; Zibo itself runs with each platform's libm
(MSVC's on Windows).
12. Open questions#
- Fuel-flow units: the tables' magnitudes fit lb/h at take-off and kg/h in
cruise;
corrected_ff2andff_corr_coef(temperature/altitude corrections, specified in the engine spec §10.1) do no unit conversion;Engine_modeltreats the table value as lb/h (× 0.45359237 / 3600→ kg/s), which reproduces the oracle's 0.082 kg/s ground-idle fuel flow. min_idle_ffandmin_idle_thrustare unused in this build; keep them for completeness or drop them when the engine model is specified.