The LevelUp 737-800's loading is not tail-heavy: in the frame the generator writes, the empty CG sits at 21.1 % MAC, two crew bring it to 19.4 % and a typical 160,000 lb load to 22.1 %, so an aft %MAC on the MSFS 2024 EFB comes from how MSFS locates the wing's reference chord, not from the loads. The most likely cause is wing_sweep = 25, which is Boeing's quarter-chord sweep written into a parameter the SDK defines as leading-edge sweep, while the generator built wing_pos_apex_lon from the 27.59° leading-edge value; any reference chord built from apex plus sweep then lands 1.31 ft too far forward and every reading shifts +10 points of MAC, to about 31 % empty and 32–35 % loaded, at or past the 32.95 % aft limit. A second fault is more dangerous: aero_center_lift = -60.73 is written "from datum" although the SDK measures it from the model origin, an error introduced in session 3 when the model origin moved to the empty CG and the other cfg positions were left as they were; read as documented, it puts the wing about 60 ft behind the CG. One change removes both faults, along with every documentation conflict that can produce readings above 100 %: set reference_datum_position = 0, 0, 0 (every position shifts +60.22 ft longitudinally and +2.05 ft vertically), aero_center_lift = -0.51, wing_sweep = 27.59 and wing_cg_refchord = 13.04, after which every candidate way MSFS might compute %MAC returns 21.1 %. Only then is it worth tuning pitch trim, the flap tables (whose lift and drag columns the generator writes swapped), induced drag and inertia, and a seven-step in-sim sequence shows which mechanism MSFS actually uses.
The loads sit at 21 % MAC, so the aft reading is a measuring error#
The generator converts every X-Plane position with lon = −z, lat = x, vert = y, using a datum at the .acf origin at the nose, and writes reference_datum_position = 60.22, 0, 2.05 and empty_weight_CG_position = -60.22, 0, -2.05 (generate_configs.py:219-229; flight_model.cfg:9-15). The converter moves the glTF root by (0, 0.6248, 18.3551) m so that the .acf empty CG lands exactly on the mesh origin (convert_aircraft.py:131-145). That matches the SDK, which defines reference_datum_position as "the position of the Datum Reference Point relative to the model center" and measures the empty CG, station_load.N and fuel-tank positions from that datum, in feet, positive forward, right and up (MSFS 2024 flight_model.cfg; Modular Fuel System). The sign and frame of every mass input are correct as the reference page documents them.
The wing yardstick also agrees with Boeing. The generator builds a trapezoidal reference wing from S = 1,341.15 ft², b = 112.58 ft and an 18.25 ft root chord (taper 0.306), which gives a 13.04 ft MAC located 23.16 ft outboard. It keeps the X-Plane wing's aerodynamic centre, which puts LEMAC 57.48 ft and the apex 45.37 ft behind the datum (generate_configs.py:78-98). MSFS reconstructs the same trapezoid, with c_tip = 2S/b − c_root and c̄ = 2(1+λ+λ²)/(3(1+λ))·c_root (Flight Model Physics Primer), and 13.04 ft is within 0.4 % of the type certificate's 155.81 in (3.96 m) (EASA TCDS IM.A.120). On this wing the payload brackets the CG rather than skewing it. The four passenger zones average 17 % MAC, a 45/55 cargo split centres at 46.8 %, the centre tank sits at 1.7 % and the main tanks at 39.7 %, so even 10 t of main-tank fuel adds only about 2.8 points (scen.py, using the station and tank values in flight_model.cfg:22-30, 72-74).
The EFB number therefore identifies the mechanism. The table gives what MSFS would display under each way it could read the current file. "Loaded" is 160,237 lb: 15 t of passengers, 3 t of cargo, 10 t of main fuel and 3 t of centre fuel.
| How MSFS reads the file | Empty | Two crew | Loaded |
|---|---|---|---|
| As the generator intends (LEMAC −57.48, chord 13.04) | 21.1 % | 19.4 % | 22.1 % |
| Same, but EFB "mean chord" = S/b (11.91 ft) | 23.0 % | 21.2 % | 24.2 % |
Reference chord from apex + wing_sweep taken as leading-edge sweep |
31.1 % | 29.4 % | 32.1 % |
| Apex + sweep and an S/b chord | 34.0 % | 32.2 % | 35.1 % |
| Stations read relative to the model centre (tutorial wording) | 21.1 % | 21.3 % | ≈138 % |
| Datum sign reversed, or empty CG read relative to the model | ≈ +480 % | ≈ +480 % | ≈ +290 to +480 % |
aero_center_lift read relative to the model origin, as the SDK documents |
≈ −441 % (far forward) | −443 % | −440 % |
Four rows read aft: the two sweep rows (moderately aft, near the limit), the station row (aft only once loaded) and the datum row (absurdly aft even when empty). The SDK-literal reading of aero_center_lift reads far forward, which means the most clearly documented error is not the one that produces an aft display.
Two generator lines break MSFS 2024's sweep and frame rules#
Quarter-chord sweep in a leading-edge slot moves LEMAC 1.31 ft forward#
The SDK says wing_sweep is "the angle the leading edge of the wing makes with a horizontal line perpendicular to the fuselage" (MSFS 2024 flight_model.cfg). The generator writes the reference value wing_sweep_c4_deg = 25 ("APD: 25 deg quarter-chord sweep") but builds the apex from the leading-edge angle, tan Λ_LE = tan 25° + (1−λ)/(AR(1+λ)), which is 27.59° (generate_configs.py:90, 351; reference_b738.py:27). The primer locates the reference chord exactly this way, z_ref chord = z_wing apex − (b/6)((1+2λ)/(1+λ))·tan Λ₀, with Λ₀ the leading-edge sweep in the visual-model frame. It adds that placing this chord "is important for a correct CG indication in gauges. However, it does not affect flight dynamics" (Primer). That remark only makes sense if the gauge chord can be placed separately from the flying wing, which points to the apex input, and community posts likewise say the apex position mainly affects the CG gauge (AVSIM). With 25° read as a leading-edge angle, LEMAC moves forward by 23.16·(tan 27.59° − tan 25°) = 1.31 ft, or 10.0 points of MAC, giving 31 % empty and 32 % loaded against an aft limit of 32.95 %.
The evidence on whether MSFS 2024 still uses the apex is mixed. Its reference page no longer lists wing_pos_apex_lon, the 2020 page called it "not used", and the FlyByWire A32NX comments the apex out while its "30 % CG" annotation reproduces to 29.98 % with LEMAC = aero_center_lift + 0.25·MAC (MSFS 2020 geometry; FBW A32NX flight_model.cfg). One hypothesis fits all of it: MSFS uses the apex when one is given and otherwise derives LEMAC from aero_center_lift. Under it the A32NX reads 30 % and this aircraft reads 31 %, which makes the sweep mismatch the single most likely explanation for an EFB that shows the CG at or just past the aft limit. A missing wing_cg_refchord can add 2–3 points on top. The SDK says this chord is "only used to calculate %MAC in the EFB" and defaults to a "mean chord" computed by the sim, so if that default is S/b (11.91 ft) rather than c̄ (13.04 ft), every deviation from LEMAC is stretched by 9 % (MSFS 2024 flight_model.cfg).
The session-3 origin move broke aero_center_lift#
The SDK is explicit that aero_center_lift "is positioned relative to the (0,0,0) position in the 3D model reference, not the Reference Datum position", and that it and compute_aero_center are the only parameters not measured from the datum (MSFS 2024 flight_model.cfg). A devsupport reply on the SU9-era documentation confirmed the value is "an offset in ft from the VMO, the point of coordinates 0,0,0" (devsupport SU9 thread). The generator writes -60.73 ; ft from datum (generate_configs.py:427).
That value was correct while the mesh origin sat at the nose. Session 3 moved the models so that the empty CG became the origin, set the datum 60.22 ft forward, and recorded "all other cfg positions unchanged" (PROGRESS_LOG.md, session 3). The datum-relative positions survived that change; this one did not. Read as documented, the wing's 25 % MAC point now lies 60.73 ft behind the CG at datum −120.95, about 1 ft behind the tailplane's quarter chord (−119.81), so the display would read about −441 % and the aircraft would be too nose-heavy to rotate. It is uncertain whether MSFS 2024 honours the documented frame: an A380X developer found the same aero_center_lift behaved differently in 2020 and 2024 (devsupport A380X thread). The robust answer is a value that is correct under both readings, and only a zero datum provides one.
The same non-zero datum causes two more conflicts. The SDK tutorial says station loads are positioned "relative to the center of the model", while the reference page and primer say "from the Datum Reference Point" (Weight and Balance tutorial); if the tutorial is right, a loaded aircraft reads about 138 %. Any sign slip on the datum reads about +480 %. All three conflicts disappear when the datum and the model origin coincide. The SDK recommends that layout for modular aircraft, FBW's A32NX uses it, and PMDG's 2024 737 reportedly does too (Tuning The Flight Model; PMDG forum).
One datum and four values make every reading agree at 21 %#
The fixes below are ordered by combined likelihood and impact. Rows 1–4 belong in one build: row 3 is the likeliest cause of the reading the user sees, and rows 1–2 carry the biggest flight-model risk.
| # | Parameter | Now | Set to | Explains the aft EFB? | Flight impact |
|---|---|---|---|---|---|
| 1 | reference_datum_position and every position |
60.22, 0, 2.05 (nose) | 0, 0, 0; lon +60.22 and vert +2.05 everywhere: empty CG 0, 0, 0; crew +51.62; pax +29.85 / +10.30 / −9.25 / −28.80; cargo +32.72 / −32.88; tanks +2.52 (centre), −2.43 (mains); gear +46.64 / −3.85; htail −59.59 | Removes the >100 % readings (low likelihood, huge magnitude) | None by itself; removes every frame ambiguity |
| 2 | aero_center_lift |
−60.73 "from datum" | −0.51 | No (as documented it reads −441 %) | Critical: the documented reading puts the wing 60 ft aft |
| 3 | wing_sweep (+ wing_pos_apex_lon) |
25 (quarter chord); apex −45.37 | 27.59 (leading edge); apex +14.85 | Yes, most likely: +10 points | Display; small aerodynamic change |
| 4 | wing_cg_refchord |
absent | 13.04 | +2–3 points if the default chord is S/b | Display only |
| 5 | Passenger station_load type |
3 (Passenger) | 0, as FBW uses for 3,300–6,600 lb cabin zones | Indirectly, by forcing payload into the holds | Loading usability |
| 6 | CG_forward_limit / CG_aft_limit |
0.0303 / 0.3295 | 0.06 / 0.32 | No; it only moves the band | Display |
| 7 | Tank positions, [FUEL_SYSTEM] Version |
datum frame; 4 | model frame; 7 or 8 after testing | No | Low |
| 8 | htail_incidence, trim limits |
0; 9.3 / 8.5 | tuned for zero trim in cruise; about 12.9 / 4.2 | No | High (trim, cruise drag) |
| 9 | Flap rows and scalars | lift/drag fields swapped; lift_scalar 1 / 0.25 |
swap the fields; 1.432 for the trailing-edge flaps, ≈0.60 for the leading-edge devices; retune each stage | No | High (takeoff, landing) |
| 10 | Drag polar, Mach table | induced_drag_scalar 1, CD0 0.0185, steep Mach rise |
≈1.4, ≈0.022, Gur-model shape | No | Medium |
| 11 | Inertia, dihedral, stall | 1.72 / 0.59 / 2.29 M slug-ft²; 10.26°; StallDef 2 / 3 | ≈2.5 / 1.0–1.4 / 3.2–3.8 M; 6°; 0.9 / 1.1 | No | Medium |
A zero datum is one generator line and leaves the model untouched#
Row 1 costs one change in the generator. Every position flows through Ctx.lon/Ctx.pos and geo.acf_to_msfs, so subtracting model_origin there moves the cfg, engines, cameras, lights, interactive points and the exported geometry JSON together; whatever reads that JSON needs the same check (generate_configs.py:68-72, 1184-1186; geometry.py:30-45). The glTF stays as it is, because its origin is already the empty CG. Rows 2–4 then make the two possible gauge mechanisms agree. The aero_center_lift-based LEMAC is −0.51 + 3.26 = +2.75 ft, and the apex-based LEMAC is 14.85 − 23.16·tan 27.59° = +2.74 ft, so both give 21.1 % against a 13.04 ft chord. That sits where the SDK tutorial wants it: an empty CG "usually" lies near 20 %, and one outside 10–35 % "may result in an uncontrollable aircraft" (Weight and Balance tutorial).
The 220 lb passenger cap and the 3 % forward limit are generator artefacts#
Stations. The station positions are fine; their type is not. The tutorial says "pilot, copilot and passenger stations are automatically limited to 220 lbs in the UI", so four type-3 zones can hold only 880 lb of passengers, and any payload a user does add lands in the holds, whose combined centroid sits at 46.8 % MAC. That is a real, non-artefact way to push the CG aft. FBW's cabin zones use type 0 with 3,300–6,600 lb weights (FBW A32NX flight_model.cfg). Keep the zero default weights the SDK asks for on variable stations.
CG limits. The generator maps the .acf CG z-limits onto the 13.04 ft reference wing while holding the aerodynamic centre fixed, which turns X-Plane's own 6.0–31.9 % (on its 15.07 ft MAC) into 3.0–33.0 %; the forward figure is an artefact of the change of wing. Zibo's FMS rejects a ZFW %MAC outside 6–36 and its 737-800 takeoff-trim tables span 6–36 % (B738.a_fms.lua; B738.calc.lua). A Boeing patent cites an aft limit of 27.36 % MAC at 174,000 lb (US20150100227A1). MSFS has only a fixed band, so 6 % / 32 % is the defensible single envelope.
Tanks. The tank arms come straight from the .acf. Centre fuel sits just behind LEMAC and main fuel sits aft, so burning centre fuel first moves the CG aft, as on the aircraft, and the tanks need only the frame shift. The 2024 fuel-system versions (6–8, with version 7 fixing a freeze) are a separate, optional upgrade (MSFS 2024 flight_model.cfg).
Real data confirms the wing but flags dihedral, trim travel and inertia#
| Quantity | Generated | Real 737-800 evidence | Verdict |
|---|---|---|---|
| MAC | 13.04 ft (156.4 in) | 155.81 in (EASA TCDS) | matches |
| Area / AR / quarter-chord sweep | 1,341 ft² / 9.45 / 25° | 124.6 m² / 9.45 / 25.02° (b737.org.uk) | matches |
| LEMAC aft of the .acf origin | 689.7 in | 685.2 in in Zibo's frame (LEMAC BA 627.1 in, empty CG at 664.5 in) to ≈702 in inferred from the 198-in forward plug (X-Plane.org; NTSB) | within uncertainty |
| Empty CG | 21.1 % | Zibo default 24.0 % (B738.a_fms.lua:41) | acceptable; 3-point cockpit offset |
| Dihedral | 10.26° | 6° (b737.org.uk) | too high |
| Stabiliser travel | 9.3 / 8.5 (.acf) | 12.9° leading edge down / 4.2° leading edge up (Satcom Guru) | real travel is asymmetric |
| Empty pitch / roll / yaw inertia | 1.72 / 0.59 / 2.29 M slug-ft² | 2.50 / 1.38 / 3.76 M "dry" (Wolfram); A320 2.45 / 0.99 / 3.17 M (FBW) | 30–57 % below Wolfram, 28–40 % below the A320 |
Public data cannot pin the absolute LEMAC station to better than about ±10 in (±6 % MAC), so keep the generator's geometry-derived 21 %. Moving the displayed value to Zibo's 24 % would mean moving aero_center_lift, and with it the static margin.
Trim, flap and drag tables need retuning once the wing sits right#
htail_incidence must make cruise trim zero, because legacy pitch coefficients do nothing#
The modern flight model ignores the legacy pitch_moment_* coefficients, except the sign of pitch_moment_delta_elevator, whose magnitude the autopilot still uses (MSFS 2024 flight_model.cfg). Pitch balance therefore comes from geometry: where aero_center_lift sits relative to the CG, the tail, and htail_incidence. No trim tuning means anything until row 2 is fixed, and the knobs FDE_TUNING.md names for pitch trim and rotation (pitch_moment_pitch_damping, pitch_moment_flaps) do nothing (FDE_TUNING.md:25). The SDK wants htail_incidence set so the aircraft "flies level at cruise speed without any required elevator input and zero elevator trim", and it computes all lift and drag normalisation at that incidence, so leaving it at 0 also skews the cruise drag target (MSFS 2024 flight_model.cfg). Set it by flying 65 t at a CG of about 21 % at FL350 and M0.785, reading the trimmed elevator plus trim deflection, folding that into htail_incidence, and repeating until cruise needs neither.
On the 737 stabiliser one unit is about 1° and 4 units is roughly 0° incidence (Satcom Guru). Set the trim limits to that asymmetric travel, about 12.9° nose-up and 4.2° nose-down, then tune elevator_trim_effectiveness; FBW's A320 runs 13.5 / 4 with an effectiveness of 3.89. Check the result against the SDK's takeoff-trim test, in which the aircraft must not rotate by itself at or before VR and should self-rotate only about 30 % above it (Testing The Aircraft). Take the trim value from Zibo's own table: 5.0–5.5 units at 60–70 t, 21 % MAC, flaps 5 and 26K (B738.calc.lua).
Swapped flap columns leave flaps 5 almost without trailing-edge lift#
The SDK order for flaps-position.i is position, airspeed limit, drag scalar, lift scalar (MSFS 2024 flight_model.cfg). The generator writes te_lift third and te_drag fourth (generate_configs.py:563), so the sim reads flaps 5 as lift 0.10 and drag 0.28 instead of the reverse. On top of the swap, the SDK says MSFS multiplies lift_coef_flaps and drag_coef_flaps by the flap deflection in radians, and asks for lift_scalar = drag_scalar = 1 / (maximum deflection in radians) to cancel that (Basic Aerodynamics). The trailing-edge section uses 1 instead of 1.432, so full flap delivers ΔCL 0.77 rather than the intended 1.1, and scaling each stage by its deflection leaves flaps 5 with about 0.01 of trailing-edge ΔCL.
The targets come from VREF data: CLmax of about 2.40 at flaps 40, 2.18 at flaps 30 and 1.3–1.4 clean (Infinite Flight VREF table). Stage scalars should be greater than 1 at intermediate detents, as FBW's are. The 8-field form adds two useful controls: "add aft feet" sets the pitch change with flap, and "add incidence" sets how much flap lift applies at 0° AoA, which governs approach attitude. Aim for about 0 to +2.5° pitch at flaps 30, a liftoff attitude of 8.2° at flaps 5, and margin to the 11° tail-strike attitude (PPRuNe; ATSB AO-2018-011).
Induced drag is too low, Mach drag too high and inertia too small#
The SDK says induced_drag_scalar must exceed 1 whenever CL0 ≠ 0, and warns that a value near 1 lets the aircraft "glide very easily at slow speeds" (Basic Aerodynamics). Fitting OpenAP's B738 polar (CD0 0.019, k 0.042) at CL0 0.15 gives CD0 ≈ 0.022 and a scalar of ≈ 1.43 (OpenAP). By that polar, the current 0.0185 / 1.0 underestimates 65 t cruise drag by about 13 % even with its Mach term. Tune the final values against measured cruise N1 and fuel flow rather than OpenAP alone. The Mach table adds roughly 2.5–8 times the Gur-model wave drag between M0.78 and 0.84, on top of MSFS's built-in Mach drag (OpenAP drag.py); validate it with a level-speed test at maximum cruise thrust. StallDef_StartRatio/EndRatio of 2 and 3 push flow separation to twice the stall AoA, against defaults of 0.9 and 1.1, so restore the defaults unless the current values were chosen on purpose. Raise the inertia toward the Wolfram and FBW figures and set the dihedral to 6° before trimming pitch_stability, which the SDK says can only add damping and also raises turbulence response.
Seven in-sim steps confirm the diagnosis#
Run steps 1–3, 6 and 7 on the current build first, then again after rows 1–4. Steps 4 and 5 are temporary hand-edits to the built package, reverted afterwards, so the generator stays the source of truth. The empty-CG %MAC also appears in the SimObject Editor's Weight debug window, which draws tanks and stations (Weight and Balance tutorial), and the CG PERCENT SimVar reports the same quantity in percent_over_100 (Flight Model SimVars).
| Step | Action | Healthy / after the fix | A different result means |
|---|---|---|---|
| 1 | Cold and dark, zero fuel and payload, default crew. Read the EFB and the debug empty-CG %MAC; note where the debug view draws the wing aero centre | 21.1 % empty, 19.4 % with crew | 29–34 %: sweep/apex. ≈ +480 % or pinned aft: datum sign or CG frame. ≈ −440 % or pinned forward, with the aero centre drawn at the tail: aero_center_lift read in the documented model frame |
| 2 | Load about 15 t of passengers across the four zones, then 3 t of cargo | Each zone accepts more than 220 lb; 18.8 %, then 20.2 % | A zone stops at 220 lb: type-3 cap. More than 100 %: stations read relative to the model |
| 3 | Add 10 t to the main tanks, then 3 t to the centre | +2.8 points, then −0.9 | Wrong direction or tens of points: tank frame |
| 4 | Change wing_sweep from 25 to 27.59 and reload |
Current build drops 10 points: the EFB uses apex + sweep | No change: the EFB derives LEMAC from aero_center_lift |
| 5 | Move the apex +1 ft, then aero_center_lift +1 ft; add wing_cg_refchord = 13.04 |
Whichever input moves the reading by +7.7 points sets LEMAC | Reading rescales by 11.91/13.04: the default chord was S/b |
| 6 | Compare CG PERCENT with the EFB |
Equal | Different: wing_cg_refchord applies to the EFB only |
| 7 | Take off at 65–70 t, CG ≈ 21 %, flaps 5, trim 5–5.5 units; in cruise, move the CG forward through the EFB | No self-rotation before VR; 2–3 °/s to ≈ 8.2° at liftoff; more nose-up trim as the CG moves forward | Cannot rotate with full aft stick: wing far aft. Early self-rotation or a tail strike: wing too far forward |
Conclusion#
The documentation conflicts can only be settled by running the sim, but the fix does not have to wait for that. The cfg can be written so that it gives the same answer whichever way MSFS reads it: a zero datum, an apex that matches a leading-edge wing_sweep, and an explicit wing_cg_refchord. The session-3 regression also shows the generator needs a guard: a unit test that computes %MAC through each reading tabulated above and asserts that all return 21.1 % would catch the next frame change that updates every position except the one measured from the model origin.
Two consequences reach beyond the EFB. Any pitch, trim or flap-pitch tuning done on the current file was done on a wing that the documented reading places 60 ft out of position, so restart it after rows 1–4 and point FDE_TUNING.md at the modern knobs. And Zibo's FMS computes ZFW %MAC and takeoff trim from a 24.0 % default empty arm plus the user's CG input, with payload on that same arm, so it never sees the CG that MSFS loads. Once MSFS's own %MAC is trustworthy, the FMS port should read the CG PERCENT SimVar so that the cockpit's trim and the sim's physics use the same CG.