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

header24 Bsection tablecount × 48 Bsimpayloadgeometrypayloadtextures, …02424 + count·48each entry: tag[16] · version u16 · offset u64 · packed u64 · raw u64header: magic "BLVL" · container version u32 · blake3-64 content hash · section count u32
A file is a fixed header, a table with one 48-byte entry per section, and the payloads the table points at. Sections need not be in table order and may leave gaps.

A file is a 64-byte header, a reserved table region, and the sections' payloads.

offsetsizefield
08magic BLVL\0\0\0\0
84u32 container version, currently 0
124u32 flags, written 0, ignored by readers
168u64 identity hash
248u64 table digest
324u32 section count, at most 1024
364u32 reserved, written 0, ignored by readers
408u64 table offset
488u64 payload floor
568u64 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:

offsetsizefield
016tag: ASCII [a-z0-9._-], NUL-padded, first byte non-NUL
162u16 payload version
182u16 flags, bit 0 identity, the rest written 0
204u32 reserved, written 0, ignored by readers
248u64 offset of the payload in the file
328u64 packed size of the payload
408u64 raw size of the payload once inflated
4816blake3 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.

chunk 0zstd, ≤ 4 MiB rawchunk 1raw when smallerindex 012 Bindex 112 Bcountu32index entry: packed u32 · raw u32 · codec u8 (0 raw, 2 zstd) · 3 zero bytesread the count from the last 4 bytes, walk the index backwards from there, inflate chunks in parallel
The chunk index sits at the end of the payload, so a writer streams chunks out without knowing their count up front, and a reader finds the index from the tail.

Tags

Tags without a dot are reserved for this specification. A game's own sections use a vendor prefix, eds.blocks for example, and a reader that does not know a tag skips it. A tool that reads a level and writes it back carries every section it did not understand through untouched, identity flag and all, so a game's private data survives a generic rewrite and the map keeps its name.

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.

tagrequiredroot tablecarries
simyesblvl.sim.Simname, bounds, kill plane, colliders, breakables, doors, triggers, teleport destinations, spawn points, hurt volumes, metadata
geometryyesblvl.geometry.Geometrymaterials, vertices, indices, draw batches
texturesblvl.Imagesthe images materials index
lightmapsblvl.lightmaps.Lightmapslightmap atlases
skiesblvl.skies.Skiesskybox cubemaps
minimapblvl.minimap.Minimapa top-down plan and the rectangle it covers
lightingblvl.lighting.Lightinglighting profiles, derived lights, the light grid, sky visibility, reflection probes, punctual lights, fog volumes, light styles
entitiesblvl.entities.Entitiesthe source map's entities as classname plus key/value pairs
cubemapsblvl.Imagesthe cubemaps reflection probes index
acousticsblvl.acoustics.Acousticswhere sound can travel, and the reverberation of the room it travels through
propsblvl.props.Propsplaced models with their own meshes and hulls
camerasblvl.cameras.Camerasauthored viewpoints: a name, a purpose, a position and angles
sitesblvl.sites.Sitesobjective 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.

01234567
  • 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
collider.flags, low byte. Bits 4 and 5 are independent: a clip brush stops the player and not a bullet, a nodraw wall is drawn by nobody and still stops a bullet.
012345678910
  • 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
gametypes on colliders, breakables, doors, triggers, spawns and draw batches. An absent field means every match type.
aerowalk collidersopen in viewer ↗
sim.colliders and sim.spawns drawn over the geometry of aerowalk: white boxes are solid, cyan is sky, magenta is a clip brush, yellow lets shots through.
training range collidersopen in viewer ↗
The EDS training range, a native bake: every collider is a hull the game expands at load.

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.

posVec30normalVec312uvVec224lm_uvVec232colorRgba840tangentTangent4452
Vertex: 52 bytes, no padding, copied straight into a vertex buffer on a little-endian machine.

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.

colorL0 + |L1|BC6H or Gamma8l1[0]R: L1 / colorl1[1]G: L1 / colorl1[2]B: L1 / colorsun_visibilityoptional, .rbump[3], styles[]each l1 texel is a tangent-space vector in -1..1, stored as (w · 127 + 128) / 255E = color · max(1 − |w|, 0) + max(w · n, 0) · colorn is the shading normal in the surface's tangent frame; with no normal map n = (0, 0, 1) and E ≈ color.Multiply by profile.lightmap_scale × lightmap.gain.A page without l1 decodes to exactly its flat irradiance.
Band-1 spherical harmonics in the tangent frame: three dot products, no square root and no divide, and a normal map bends the baked light instead of being ignored by it.
aerowalk litopen in viewer ↗
albedo × lightmap, the Gamma8 path: the page's bytes are multiplied in gamma space and clipped, the way idTech3 did.
aerowalk lightmapopen in viewer ↗
The lightmap alone. Quake 3 stores it dim and the renderer doubles it twice: once for the map's overbright bits, once at the screen.

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.

ut4_uptownopen in viewer ↗
Urban Terror's ut4_uptown, from its first spawn. Where a bake carries no cameras section a reader frames the level from a spawn point, as the viewer does here.

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

  1. Read 24 bytes, check the magic and version, read the count.
  2. Read the table, validate it as above.
  3. For each section you want, seek to its offset, read packed bytes, inflate the chunks into raw bytes, verify the FlatBuffer, convert.
  4. 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.

offsetsizefield
08magic BSYM\0\0\0\0
84u32 symbols version, currently 0
128u64 content hash of the .blvl this describes
204u32 raw size of the payload
24one 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.