The blvl level format
This page is the specification as it is kept in the repository, with the FlatBuffers schema of every section unfolded beside the prose that describes it, and with examples you can open in the viewer.
A .blvl file is one baked FPS map: the triangles a renderer draws, the
convex solids a player collides with, the lightmaps and probes that light it,
the spawn points, doors, breakables and triggers a game rule set reads, and
whatever else the game that made it wants to carry along. It is written once
by a bake and read many times, by clients that want everything and by servers
that want only the solids.
Everything is little-endian. Units are Quake units (32 units to the metre), the up axis is +Y, the coordinate system is right-handed, and forward is -Z. A bake from another engine has already turned that engine's axes and scale into this one.
Container
A file is a 64-byte header, a reserved table region, and the sections' payloads.
| offset | size | field |
|---|---|---|
| 0 | 8 | magic BLVL\0\0\0\0 |
| 8 | 4 | u32 container version, currently 0 |
| 12 | 4 | u32 flags, written 0, ignored by readers |
| 16 | 8 | u64 identity hash |
| 24 | 8 | u64 table digest |
| 32 | 4 | u32 section count, at most 1024 |
| 36 | 4 | u32 reserved, written 0, ignored by readers |
| 40 | 8 | u64 table offset |
| 48 | 8 | u64 payload floor |
| 56 | 8 | u64 reserved, written 0, ignored by readers |
The table holds one 64-byte entry per section, so every 64-bit field in it is 8-byte aligned and the digest starts on a 16-byte boundary:
| offset | size | field |
|---|---|---|
| 0 | 16 | tag: ASCII [a-z0-9._-], NUL-padded, first byte non-NUL |
| 16 | 2 | u16 payload version |
| 18 | 2 | u16 flags, bit 0 identity, the rest written 0 |
| 20 | 4 | u32 reserved, written 0, ignored by readers |
| 24 | 8 | u64 offset of the payload in the file |
| 32 | 8 | u64 packed size of the payload |
| 40 | 8 | u64 raw size of the payload once inflated |
| 48 | 16 | blake3 digest of the packed payload, first 16 bytes |
The table sits at table offset, always inside the reserved region between the
header and the payload floor. Payloads start at or after the floor. A writer
reserves 2 * count + 16 slots rounded up to 4 KiB, so a section can be added,
replaced or dropped later without moving a single payload byte; a real map
reserves exactly 4 KiB. A reader that has the header and the table has
everything it needs to place every section, and a streaming reader gets that
after one bounded read of at most 128 KiB.
A reader refuses a file whose magic or container version it does not know, whose payload floor or table offset is not a table boundary or falls outside the reserved region, whose count is past the reserved capacity, whose entries overlap, run below the floor or past the end of the file, repeat a tag, or claim more than 2 GiB raw for one section.
Unreferenced bytes
A file may carry bytes no entry refers to: the slack in the reserved region,
the padding that aligns an appended payload, and the remains of a section that
was replaced. A reader ignores them. It refuses a file that carries more than
max(1 MiB, covered / 16) of them, where covered is the payload floor plus
every section's packed size, and it sizes its inflation budget from covered
rather than from the length of the file, so padding buys an attacker nothing.
Identity and integrity
Each entry carries a blake3 digest of its section's packed bytes. Every byte a reader ever reads is covered by one, and checking a section costs only that section.
The identity hash is blake3 over a canonical record set: 16 bytes of magic, version and record count, then one 48-byte record per section whose identity flag is set, sorted by raw tag bytes, each holding the tag, the payload version, the raw size and the section digest. It is the level's name on the wire: two files with the same identity are the same map, whatever else they carry. Offset, packed size, table position and file length are all left out, so adding, replacing or dropping a section outside identity leaves the name alone. Because it is a hash of the stored bytes, changing the compressor renames every map; the zstd level and the chunk size are part of the format.
A core tag must have the identity flag set and a reader refuses a file where one does not, so a level's own sections can never be quietly demoted out of its name. A game's sections opt in: the ones a match depends on belong in identity, a thumbnail does not.
The table digest is blake3 over 16 bytes of magic, version and count followed by the whole reserved region, entries and slack alike. It pins the offsets and the sections outside identity, which identity deliberately does not, and it changes on every edit. It never goes on the wire.
A reader with the header and the table can check the table digest and recompute the identity before a single payload byte arrives. A streaming reader verifies each section the moment its last byte lands, rather than learning at the end of a 150 MB download that something was wrong.
Editing a baked level
A section outside identity can be added, replaced or dropped in place. The payload goes at the end of the file, the table grows into the reserved slack or is rewritten whole to a free part of the region, and the edit commits with a single write of the 24 bytes at offset 24: table digest, count and table offset. Those 24 bytes sit inside the first sector, so the commit cannot tear. Everything written before the commit is unreferenced until it lands, so a crash at any point leaves the previous level intact.
An in-place edit never writes offset 16. A level cannot be renamed by editing it, only by baking it again.
Section payloads
A payload is a run of chunks, each holding at most 4 MiB of raw data, followed by a chunk index and a u32 chunk count. Each index entry is 12 bytes: u32 packed size, u32 raw size, u8 codec (0 raw, 1 zstd), 3 bytes zero. Chunks are independent, so a reader inflates them in parallel and a writer never holds a whole section to compress it. A chunk is stored raw when compressing it would not have made it smaller.
Core sections
Each core section is one FlatBuffer root table, described by the schema of the
same name under crates/blvl/schema/. Those schemas are the
definition of the section contents; a reader in any language starts from
them. Readers verify a payload with the FlatBuffers verifier before reading
it, and treat every optional table field as absent-means-empty.
Every section is at payload version 0. They are versioned one at a time, so a schema change bumps only the section it touches, and a reader that does not know a version drops that section rather than refusing the file, unless the section is required.
| tag | required | root table | carries |
|---|---|---|---|
sim | yes | blvl.sim.Sim | name, bounds, kill plane, colliders, breakables, doors, triggers, teleport destinations, spawn points, hurt volumes, metadata |
geometry | yes | blvl.geometry.Geometry | materials, vertices, indices, draw batches |
textures | blvl.Images | the images materials index | |
lightmaps | blvl.lightmaps.Lightmaps | lightmap atlases | |
skies | blvl.skies.Skies | skybox cubemaps | |
minimap | blvl.minimap.Minimap | a top-down plan and the rectangle it covers | |
lighting | blvl.lighting.Lighting | lighting profiles, derived lights, the light grid, sky visibility, reflection probes, punctual lights, fog volumes, light styles | |
entities | blvl.entities.Entities | the source map's entities as classname plus key/value pairs | |
cubemaps | blvl.Images | the cubemaps reflection probes index | |
acoustics | blvl.acoustics.Acoustics | where sound can travel, and the reverberation of the room it travels through | |
props | blvl.props.Props | placed models with their own meshes and hulls | |
cameras | blvl.cameras.Cameras | authored viewpoints: a name, a purpose, a position and angles | |
sites | blvl.sites.Sites | objective areas: bomb sites and the like, as named boxes |
A server reads sim alone and never inflates the rest. A client reads them
all, and a streaming client can decode textures while the tail of the file
is still arriving, because the table tells it where each section ends.
sim
colliders are convex solids. Each carries its axis-aligned bounds, a flags
word, and one of two shapes: Planes, every outward plane as a unit normal
and a distance with the axial planes and edge bevels already included, or
Hull, the vertices and face loops for the reader to expand. Face planes
alone do not bound a swept box, so a reader that traces boxes needs the six
axial planes and one bevel plane per edge crossed with each box axis; a hull
leaves that expansion to the loader because for a mesh-derived map the bevels
outnumber the faces six to one. A hull's face loops have to be wound around
each face: the loader reads a face's plane off the loop's own area, and a loop
listed corner by corner instead crosses itself, encloses nothing, and yields
no plane at all. Six faces lost that way leave a box at an angle standing as
its own bounding box, since the axial planes are all that is left of it. A
loader may wind a loop it is given crossed, and this one does.
flags bit 0 is a ladder: climbable, and shots pass through it. Bits 1..2 are
the surface, 0 concrete, 1 metal, 2 wood, for footsteps and impacts. Bit 3 is
sky. Bit 4 is a clip brush, solid but never drawn. Bit 5 says a shot passes
through: the solid holds the player but not a bullet, the way an engine's
player clip, bot clip and grate do, and every ladder sets it too. Bits 4 and 5
are independent, and a game that traces shots against everything solid gets
them wrong in opposite directions: a nodraw wall is drawn by nobody and still
stops a bullet, and a player clip is invisible and does not. Bit 6 is water: a
swimmable volume a game never sweeps or shoots against, only asks whether a
point is inside. A water collider's material names the geometry.materials
entry of its surface so a reader underwater can fog with that material's
water block. Bit 7 is player-through: the solid stops a shot but not the
player, the way collision an engine keeps for projectiles and abilities alone
does (Valorant's IgnoreOnlyPawn art, whose pawns walk on invisible plates
instead). A game's movement sweeps skip it and its shots do not.
A collider may name the breakable or the door it belongs to and the entry
of collider_sources that says where it came from. A door's planes are its
closed pose; the game moves them.
A collider, breakable, door, trigger, spawn point and draw batch may each carry
gametypes, a mask of the match types it belongs to. An arena map is often
several layouts in one file: a wall that only stands in capture the flag, a
door only the bomb round opens, spawn points a team game uses and a
free-for-all does not. The source engines gate those on the gametype the server
runs, and a reader that stacks every layout on top of the others seals passages
that should be open. Bit 0 is free-for-all, then last man standing, team
deathmatch, team survivor, follow the leader, capture and hold, capture the
flag, bomb, jump, freeze tag and gun game. An absent field is every match type,
which is what the map's own world geometry carries and what an engine with no
such notion produces, so a reader that ignores the field gets the whole map and
today's behaviour. A game filters the pieces it draws and sweeps by the mask;
the mask is per piece and not per section, so a match type can be switched
without reloading the file. A door may also name the team an engine
reserved it for, spelled as the source map spelled it.
breakables are things a shot removes from the world. kind is what one is
made of, so a game can pick the sound it makes and the debris it leaves:
Glass, Wood, Metal, Stone, Tile, Flesh, or Other when the map
never said. It is the source engine's own material class where the engine
names one, and read out of the material's name where it does not; a reader
that wants a better guess for Other can fall back on the surface bits of
the colliders that belong to the breakable. A breakable that was parented to a
door names it in door; its lo and hi are the box in the door's closed
pose, so a reader that has moved the door puts that box through the same
transform before it spawns debris. doors slide along a direction or swing
about a hinge, on a timer or on use. triggers are the two Quake volumes
every arena game has: a push that replaces the player's velocity and a
teleport to one of teleport_dests. spawns carry a position, a yaw, the
kind of spawn the source map declared and an optional team.
hurt_volumes are the places a map hurts whoever stands in them: the kill
zones under and around a level that end a fall long before kill_y would,
and the lava pits and hurt triggers that wear a player down. Each has a
name from the source map, a kind, and a shape: the box lo..hi when
planes is empty, otherwise the convex intersection of the outward
half-spaces in planes (the same n·p - d <= 0 convention as a collider's)
with lo..hi as its bounds, since a rotated box would kill players on
honest ground if it were read as its bounds alone. A Kill volume kills on
contact. A Damage volume takes amount hit points every interval seconds
for as long as the player stays inside, the numbers the source engine used.
A game tests its player box against the volume and is free to do so with the
box's corners against the planes. kill_y remains the floor below all of
them.
meta records the author, description, creation time, the tool that wrote the
file and the engine the map came from: Native, Q3, Urt, Source,
Source2, Valorant, Ow2, Cod4, Css, Csgo, Tf2, Hl2,
Deadlock, Halo2, Mw2 or Reflex. Source is a Source 1 map whose game
the bake could not tell apart; Css, Csgo, Tf2 and Hl2 are the same
engine with the game named, Deadlock is a Source 2 map whose lightmaps are
per-channel L1 rather than irradiance plus direction, Halo2 is a Halo 2
Classic map whose lightmap is the game's own gamma bytes doubled by the
renderer, and Mw2 is Modern Warfare 2, IW4, drawn like Call of Duty 4. A
renderer picks its lighting model from this field.
- bit 0 ladder · climbable, shots pass
- bit 1-2 surface · 0 concrete, 1 metal, 2 wood
- bit 3 sky
- bit 4 clip · solid, never drawn
- bit 5 shot-through · holds the player, not a bullet
- bit 6 water · point-in-volume only
- bit 7 player-through · stops a shot, not the player
- bit 0 free-for-all
- bit 1 last man standing
- bit 2 team deathmatch
- bit 3 team survivor
- bit 4 follow the leader
- bit 5 capture and hold
- bit 6 capture the flag
- bit 7 bomb
- bit 8 jump
- bit 9 freeze tag
- bit 10 gun game
geometry
vertices is a vector of 52-byte structs laid out as position, normal, two
texture coordinates (material and lightmap), an RGBA8 colour and a packed
tangent, so a reader on a little-endian machine can copy it straight into a
vertex buffer. The tangent is snorm16 xyz plus a sign in w, the handedness the
shader needs to rebuild the frame from the vertex normal. indices is a vector
of u32 triangles. Each batch is one draw: a run of indices, the material it
uses, its bounds, the breakable or door it belongs to so a renderer can stop
drawing what has shattered and move what has opened, and optionally the entry
of lighting.fog_volumes the surface is fogged by (fog; idTech3 assigns fog
per surface, so a bake splits batches by material and fog), and the
gametypes mask described under sim (a bake splits batches by that too, so a
renderer can drop a wall that belongs to another match type without touching
the rest of the draw).
A material names its texture and carries the flags a renderer needs before it
draws: the sort key (lower first; 3 opaque, 4 decal, 9 additive), the blend
mode, whether it writes depth, the alpha test, two-sidedness, polygon offset,
environment mapping, and whether it is a sky. Multiply is the blend mode a
dirt or stain decal wants: the renderer scales what is already in the frame by
the material's albedo and lights the decal not at all, which is what idTech3's
blendFunc filter and a Source decalmodulate do. Batches are sorted by that
key, translucent last. The PBR block records what the bake could learn about
the surface without deciding anything: the source engine's surface parameters,
its emitted light, and indices of normal, occlusion/roughness/metal, emissive
and second-albedo textures when the bake found them, plus scalar factors.
maps may also name a specular texture (RGB specular colour or mask, alpha
gloss) and a detail texture.
water is present on a water material and carries the fog colour, the fog
start and end in map units, the reflect and refract tints and amounts, and two
UV scroll rates, in the source engine's own values; absent means the engine
said nothing.
shading is an optional table of the source engine's own material parameters,
kept in that engine's units so a renderer can run its formula rather than an
approximation. Every field has a default that means "the engine's default":
phong_exponent (0 when the exponent comes from the specular map's alpha),
phong_boost 1, phong_fresnel (three Fresnel range stops), phong_albedo_tint,
envmap_tint, envmap_mask (None, Texture for a separate mask,
BaseAlpha or NormalAlpha for a mask in that texture's alpha),
envmap_contrast, envmap_saturation 1, selfillum_tint, rim_boost,
rim_exponent 4, lightwarp (a texture index, the 1D ramp N·L is read
through), detail_blend (Source's detail blend modes 0..9), detail_scale 1,
detail_factor 1, seamless_scale (0 for none), halflambert, lightmapped
(true; false for a surface the engine lights per vertex), and anim. The
absent Vec3 fields read as zero.
anim is idTech3 shader animation: tcmods, each a kind (scroll, scale,
rotate, turb, stretch as the source shader numbered them) with four scalar
arguments and an optional wave; rgb_wave and alpha_wave; frames, the
texture indices of an animated map, at fps; and deform, a vertex deform
with its kind, spread, wave, vector and bulge. A wave is a function index and
base, amp, phase, freq, exactly as the shader script wrote them.
textures and cubemaps
An image is a width, a height, an encoding, a colour space, a mip count, a
layer count and one byte blob. Encodings are PNG, JPEG, raw RGBA8, and the
block compressed BC1, BC3, BC4, BC5, BC6H and BC7. Block-compressed images hold
their whole mip chain, level-major and layer-minor: all layers of level 0,
then all layers of level 1, and so on, each level ceil(w/4) * ceil(h/4) * block bytes where w and h halve per level and never drop below 1. A
cubemap is an image with six layers in the +X, -X, +Y, -Y, +Z, -Z order.
lightmaps
A lightmap is band-1 irradiance spherical harmonics in the surface's tangent
frame. color is the peak irradiance L0 + |L1| and each of the three
optional l1 planes is that channel's L1 / color, so every component lands
in -1..1 and a shader reads E = color * (1 - |w|) + max(dot(w, n), 0) * color. A lightmap without l1 is flat. sun_visibility is an optional
per-texel visibility of the profile's sun, and gain is the multiplier a
reader applies to the colour plane to recover the authored range. It is 1.0
for Urban Terror, Source 1, Source 2, VALORANT, Overwatch 2, Call of Duty 4
and Modern Warfare 2; 2.0 for Quake 3, whose maps are authored two overbright
bits above what the hardware applied; 2.0 for Halo 2, whose renderer
multiplies texture by lightmap in gamma space and doubles the product the way
the game's own modulate-2x combiner did; and 2.2 for Reflex Arena. A Quake 3
map that names its own overbright shifts that 2.0 with it.
encoding says what the colour plane's bytes are. LinearHdr, the default
and what every older file holds, is linear light in the block format's own
range. Gamma8 is the game's own 8-bit gamma-space bytes stored as sRGB
(idTech3, IW3 and Halo 2 lightmaps), which a renderer multiplies with the
texture in gamma space and shifts by the profile's overbright, or scales by the
lightmap's gain, rather than decoding to linear.
bump is the three Source 1 bump-basis pages, exactly three images or none,
in the basis order (0.8165, 0, 0.5774), (-0.4082, 0.7071, 0.5774),
(-0.4082, -0.7071, 0.5774); a normal-mapped surface weights them by the
squared dot of its tangent-space normal with each basis vector. styles are
up to three extra colour pages, each tagged with the light style (an index of
lighting.light_styles) whose 10 Hz value scales it before it is added to
color; style 0 is color itself and is never listed.
lighting
A lighting profile is one way to light the map; profile 0 is always the map as it was authored. It names its lightmap and sky, scales the lightmap and the lights, carries an ambient term, an optional sun (direction toward the sun, colour, intensity), fog, and optionally the sky's own irradiance as 27 floats: nine L2 spherical harmonic coefficients of RGB.
sun_units says what unit sun.intensity is in. Legacy, the default and
what every older file holds, is the renderer-side scale bakes used before the
per-engine data existed (a value clamped to 20..200). Source1, Source2,
Ue4, Iw3 and Ow2 are that engine's own unit, carried raw so the
renderer for that engine can apply its own formula. sun_shadow_channel is
the channel of the lightmap's sun_visibility (or the light grid's) that
holds the sun's baked shadow mask when the sun is a stationary light whose
diffuse is evaluated at runtime, and sun_baked is true when the lightmap
already holds the sun's diffuse (the Sun struct is frozen, so these live on
the profile).
Fog: fog_color and fog_density are the exponential fog every profile has.
fog_mode picks the engine's curve: Exp (the default), Linear between
fog_start and fog_end capped at fog_max (Source 1), ExpStart
(exponential from fog_start, IW3), Height (UE4 exponential height fog with
fog_height_falloff), or Gradient (Source 2). The scalar fields are absent
when the engine has no such parameter.
post is the map's own post-processing block: tonemap (an operator index
the renderer for that engine understands), a fixed exposure (1) or the
bounded auto-exposure range exposure_min..exposure_max with its adaptation
exposure_rate (1) and exposure_compensation; bloom bloom_scale,
bloom_threshold and bloom_exponent; IW3's film tweak film_contrast (1),
film_brightness, film_desaturation, light_tint and dark_tint; the
display gamma (2.2); a colour-grading lut texture; UE4's film curve
film_slope, film_toe, film_shoulder, film_black_clip,
film_white_clip; and vignette. A profile without post is graded the way
the renderer graded every file before this block existed.
lights are derived from emissive surfaces. The light_grid is the volume
that lights whatever is not level geometry: three images over a regular grid
holding ambient colour, directional colour and direction, which every grid
carries whatever its kind. AmbientDirectional is exactly those three
images (idTech3, and the default). AmbientCube adds faces, six images in
the +X, -X, +Y, -Y, +Z, -Z order holding the colour arriving from each axis
(Source 1 leaf ambient, Source 2 light probe volumes, IW3's model lighting),
with the triple derived from them so an older reader still works. Sh2 is a
per-channel band-1 grid (UE4's indirect lighting cache) stored the same way.
A grid may carry its own sun_visibility image, and its encoding follows
the lightmap's meaning. gain is what the stored bytes are multiplied by: a
volume whose light runs past one is normalised into the page and scaled back
by the reader, so an ambient cube built from a sun and its lights keeps its
range instead of clipping. sky_vis is a voxel grid of how much sky each cell
sees. Reflection probes name a cubemap, the box they influence and an optional
parallax shape.
Punctual lights are the point and spot lights the source map placed, with
whether the lightmap already holds their diffuse. falloff is how intensity
decays with distance: SmoothFalloff (absent, or the table with no fields)
is the smooth inverse-square window the renderer always used; SourceQuadratic
is Source 1's 1 / (c + l*d + q*d*d) with the fifty and hundred percent
distances the map authored; Iw3Falloff is IW3's linear window to radius.
fog_volumes are idTech3 fog brushes: an axis-aligned box, the fog colour and
the depth at which it turns opaque (fog = sqrt(clamp(dist / depth, 0, 1))).
Batches point at them by index. light_styles are the light style patterns
(a..z strings, m is full brightness, sampled at 10 Hz) that lightmap
style layers and switchable lights refer to by index.
entities
The source map's entities, kept as text: a classname and key/value pairs. Nothing in the core interprets them; they are the evidence a game reads for whatever the core sections do not model.
acoustics
A lattice of probes standing on the walkable floors at ear height, linked by edges where one probe can see the next with every door held open, and grouped into rooms by how alike their surroundings are. A game finds the probe nearest a sound and the one nearest the listener, walks the edges between them, and gets the distance the sound actually travelled rather than the straight line through the wall.
A probe carries its position, the room it belongs to, the share of rays cast
from it that hit something within 45 m as a byte over 255, and the mean
distance those rays ran in metres capped at 255. first_edge indexes edges;
a probe's edges run to the next probe's first_edge, and the last probe's to
the end.
An edge is one-way and always has its opposite in the list, so a walk needs no
reverse index. len_m is the distance between the two probes. door is the
door team standing between them, 65535 for none, so a reader can charge for
a shut door without tracing again; a door's team is the lowest index of the
leaves that make up one doorway. narrow marks an edge that a
72 by 72 qu box cannot pass along, which is a gap rather than an opening.
A room's rt60_s is its reverberation time and damping how much of a
reflection it keeps, both derived from the mean enclosed and size_m of its
probes. outdoor is set when the room's probes see the sky more often than
not.
The section is absent when a bake produced no probes, which is the case for a level with no walkable floor.
props
Models placed in the level that are not part of the world mesh: each has its own vertices, indices and collision hulls, and each placement says where it sits, whether it can move, and how solid it is. Absent when the bake placed none.
cameras
Viewpoints somebody chose for the level, so a game can show it from a good
angle without guessing. Each camera has a name, a purpose, a pos in
Quake units (+Y up, -Z forward), yaw and pitch in degrees, roll in
degrees about the view axis, and fov_x, the horizontal field of view in
degrees as a game quotes it for a 4:3 view. A yaw of 0 looks down -Z and a
positive yaw turns toward -X; a positive pitch looks up. This is the same
convention the blvl probe --pose argument uses.
purpose is one of thumbnail, overview, menu or other. A reader that
wants a view for one purpose takes the first camera that has it, and frames
the level itself when none does. Bakers write these from a file kept beside
the recipe rather than from the source map, so a rebake carries them
unchanged.
A core section is not carried through by a writer that predates it: a tool
built before cameras existed drops the section when it rewrites a level.
Extensions survive that; core sections do not.
sites
Objective areas the source map marks out: a bomb site, a hostage zone, a
capture point. Each site has a name (A, B, ...), a kind (bombsite,
hostage, capture, flag or other) and an axis-aligned box lo..hi in
Quake units. A site that covers several boxes appears once per box under the
same name, and a reader unions them. Absent when the map marks none.
Reading a level
- Read 24 bytes, check the magic and version, read the count.
- Read the table, validate it as above.
- For each section you want, seek to its offset, read
packedbytes, inflate the chunks intorawbytes, verify the FlatBuffer, convert. - Skip any tag you do not know; keep its bytes if you intend to write the file back.
The Rust crate blvl in this repository is the reference reader and
writer. blvl-trace expands colliders into the planes a swept-box trace
needs and is the reference for what Hull means.
Symbols sidecar (.bsym)
A bake may write a second file beside the level, <name>.bsym, that says
where every collider and every run of triangles came from in the source map:
the brush, face, patch, prop or physics shape, the engine's own surface and
contents flags, the entity that placed it, and the material before the bake
merged it, with its shader and texture paths. It is a debug artefact. Nothing
in the level refers to it, it is never hashed into the level, a game runs
without it, and a shipping build may leave it out.
| offset | size | field |
|---|---|---|
| 0 | 8 | magic BSYM\0\0\0\0 |
| 8 | 4 | u32 symbols version, currently 0 |
| 12 | 8 | u64 content hash of the .blvl this describes |
| 20 | 4 | u32 raw size of the payload |
| 24 | one zstd frame holding a blvl.symbols.Symbols FlatBuffer |
The hash is the level's own header hash. A reader compares it with the level it has and treats a mismatch as stale: symbols never describe a level other than the one they were written for. A reader that sees a version other than its own reports that and reads nothing.
colliders is dense and parallel to the level's sim.colliders. tri_runs
is sorted by first_tri, contiguous, and covers every triangle of
geometry.indices; a batch starts in the run holding first_index / 3. Both
name rows of sources, one per source element (a brush, a face, a patch, a
prop instance, a physics shape), which name materials, the pre-merge
material table whose baked field is the level material it ended up as. Every
32-bit id field uses 0xFFFFFFFF for none. Struct layouts are frozen within a
version; new data arrives as new optional table fields, and a struct or
semantic change bumps the version.
The Rust crate blvl reads and writes it (blvl::symbols),
and blvl-diag turns a level and its symbols into checks and text dumps for
blvl probe and for games.




