The files a build writes

Last updated · how this page is sourced

A scenery build produces four kinds of file. Two are plain text and can be read with zcat, which makes them the fastest way to find out where a build went wrong. The other two are binary, and one of them changed format recently in a way nothing documents.

The inputs have their own formats too: apt.dat for airports, and SRTM .hgt for elevation.

The chain

FileWritten byContainsReadable?
.hgtn/aInput. Raw SRTM heights, big-endian int16, no headerBinary
.arr.gzhgtchopOne tile’s height gridBinary
.fit.gzterrafitThe fitted mesh nodesText
.btg.gztg-constructThe terrain itselfBinary
.stgtg-constructWhat FlightGear loads for a tileText

.fit.gz; the one to check first

$ zcat 942050.fit.gz | head -3
1000
-122.50000000 +37.50000000 0.00
-122.50000000 +37.55250000 96.00

A node count, then one longitude latitude elevation per line. If a tile’s elevations are all zero, or the coordinates are somewhere you did not ask for, the problem is upstream of the terrain builder and you can see it in one command.

.stg; what FlightGear actually loads

Also plain text, and more interesting than it sounds:

OBJECT_BASE 942050.btg
OBJECT KSFO.btg
OBJECT_SHARED Models/Airport/windsock_lit.xml -122.382294 37.610056 2.6 0.00
OBJECT_SHARED Models/Airport/beacon.xml -122.383256 37.636909 0.3 0.00
OBJECT_SIGN {@L}D{@R}10R-28L -122.383436 37.621469 2.0 328.08 3
OBJECT_SIGN {@L}F1{@Y,^l}B{^r} -122.378337 37.616397 2.6 85.14 3

OBJECT_BASE is the terrain mesh for the tile, OBJECT lines are airports laid on top, OBJECT_SHARED places a model from $FG_ROOT at a coordinate, and OBJECT_SIGN is a taxiway sign with its legend encoded; @L switches to the location style, @R to runway, @Y to direction, so {@L}D{@R}10R-28L is the sign at taxiway D pointing at runway 10R-28L.

If your scenery loads but an airport is missing, the .stg for that tile tells you whether it was ever written.

.btg.gz; the terrain

Gzipped binary. The authority is SimGear’s SGBinObject (simgear/io/sg_binobj.cxx), and reading that source is how the layout below was established, then confirmed by parsing 97 files from two builds.

Header

uint32   header      little-endian: 'S' in the top byte, 'G' in the next,
                      version in the low 16 bits
uint32   creation time
         object count  version >= 10 : int32
                       version >=  7 : uint16
                       older         : int16

The magic is easy to get wrong. Read the first four bytes as a little-endian uint32 and the version is the low half, the letters the high half; so a version 7 file starts with the bytes 07 00 47 53. Checking the first two bytes for “SG” will fail on every valid file.

Objects

Then, per object:

uint8    object type
         property count, element count
                       version >= 10 : uint32, uint32
                       version >=  7 : uint16, uint16
                       older         : int16,  int16
per property:  uint8 type, uint32 byte count, bytes
per element:   uint32 byte count, bytes

Object types, from the same source:

CodeObject
0Bounding sphere
1, 2, 3, 4Vertex, normal, texture coordinate and colour lists
5, 6Vertex-attribute float and integer lists
9Points; lighting, in practice
10, 11, 12Triangle faces, strips and fans

Version 10, and why you will meet both

Version 10 widens the counts to 32 bits. Support for it landed in SimGear in December 2023, as part of the World Scenery 3.0 work.

The reason to care: one build can write both. The two builds described on this site produced 97 files between them; the terrain tiles came out at version 10, the airport files at version 7. A reader that assumes one version will fail on half of them, and the failure looks like corruption rather than like a version it does not know.

A prediction that turned out wrong is worth recording: FlightGear 2020.3.16 predates version 10 by three years, so it was expected to reject those tiles. It reads them without complaint.

Checking your output

Walking the structure is enough to catch a truncated or corrupt file, and it needs no libraries; the parse either lands exactly on end-of-file or it does not. validate-btg.py, published with the build record, does this in about sixty lines:

files checked       : 44
parse cleanly to EOF: 44
BTG versions seen   : [7]
total vertices      : 115,989

And for the land-cover build, 53 files, versions 7 and 10, 1,265,905 vertices.

Reading the material names out of the triangle-face properties is a useful second check, because it tells you what the terrain is actually made of. Without land cover the build contained airport paint and ocean and little else; lf_* line features, pa_* pavement, Ocean, Airport. With it, the terrain materials appear: Default, Lake, EvergreenForest, Scrub, Marsh, Grassland, Urban. Eighty-five distinct materials against seventy-five.

That difference is visible in the simulator too, and the gallery shows it, but the material list tells you before you render anything.

What this does not cover. The meaning of the bytes inside each element: vertex layouts, index widths, texture coordinate conventions. The FlightGear wiki’s BTG page goes into that, and sg_binobj.cxx is the authority for both. What is here is the structure, which is what you need to check a file rather than to render one.