Lighting bakes are one of the more misunderstood categories of game assets from a version control perspective. They're large (a full production bake for a complex environment can run 2–8 GB), they're derived (they're generated from scene geometry, light placements, and Lightmass settings — not hand-authored content), and they're fragile with respect to branch operations in ways that catch teams off guard.
The specific failure mode we're going to work through: a stale lighting bake silently ships to a demo build because the merge path from a feature branch to the release candidate didn't account for bake provenance. This isn't a hypothetical — it's a pattern we see repeatedly in teams that haven't explicitly designed their branch topology with bake lifecycle in mind.
Why Bakes Are a Version Control Problem
Unreal Engine's Lightmass bake output — the .uasset files in MapName_BuiltData packages alongside your level file — is a content artifact that depends on the state of the scene at bake time. Change any light actor, move any static mesh, adjust any Lightmass World Settings, and the existing bake is stale. The engine will show you warning indicators in the editor viewport, but those warnings don't follow the file through VCS operations.
When you check a bake file into version control, you're storing a snapshot of derived content that is only valid relative to a specific commit of its source scene. This is different from hand-authored assets, where the file itself is the source of truth. A bake file's source of truth is the scene state that generated it — and that scene state is identified by its commit hash, not by the bake file's own commit hash.
Most version control workflows don't track this dependency. They treat the bake package as just another binary asset. That gap is where stale bakes slip through.
The Merge Path That Ships a Stale Bake
Here's the specific topology that causes the problem, simplified:
mainis at commit A.MapHub_BuiltData.uassetis the production bake, valid for commit A.- A level designer branches to
feature/env-pass-dockyardand spends a week adjusting static mesh placements in the dockyard area. They do not rebake because the bake job takes 4 hours and it's not blocking their layout work. - Meanwhile, a lighting artist on
maindoes a full production rebake at commit B.MapHub_BuiltData.uassetis updated onmain. This is the correct bake for the current production build. - The feature branch merges into a release candidate branch. The merge picks up the level geometry changes from
feature/env-pass-dockyard. But which bake wins the merge? If the merge resolution picks the feature branch's version ofMapHub_BuiltData.uasset(the old pre-dockyard bake from commit A), the production bake from commit B is gone. The RC now has updated dockyard geometry and the wrong bake. - The RC goes to certification. The stale bake ships. QA catches it — or doesn't.
Note that at no point did anyone make a deliberate mistake. The conflict resolution on a binary file is arbitrary (last-write-wins, or the merge resolving side wins), and the bake dependency relationship wasn't encoded anywhere in the VCS.
Branch Topology That Prevents This
The core fix is structural: don't store production bakes on feature branches. Feature branches work on scene content; bakes happen only on designated integration branches where the scene state is stable enough to bake from.
A practical topology for a team of 15–30 people:
main— always-shippable. Bakes are up to date at all times. Bake update is a dedicated commit with message formatbuild(lightmass): rebake MapHub v2.3 from commit abc1234. The commit reference in the message is the scene-state commit the bake was generated from.feature/*— no bakes committed. Feature branches carry only scene edits. TheMapHub_BuiltData.uassetpath is excluded from commits on feature branches via a pre-commit hook or path exclusion rule.rc/*— release candidate branches. Receives merges frommainonly, not directly from feature branches. Scene changes come in via main, where bakes have been updated to account for them.
The gate between feature and main is: bake validation passes on main after the feature branch merges. Not before the merge, not on the feature branch itself. This sequence ensures the bake on main is always valid for the current scene state on main.
Enforcing Bake Path Exclusion
Asking level designers not to commit bake files is not a process that survives rotation of team members or crunch. It needs toolchain enforcement.
Diversion lets you define path exclusion rules per branch pattern. A configuration like:
# diversion branch policy
branch_pattern: "feature/*"
exclude_paths:
- "Content/Maps/**/*_BuiltData.uasset"
- "Content/Maps/**/*_BuiltData/*"
...will reject any commit on a feature branch that touches the built data paths. The developer gets an error at commit time, not at merge time. This is the earliest possible intervention point.
If you're on Perforce, an equivalent enforcement can be achieved via a Perforce submit trigger that rejects submits matching the bake path pattern on feature streams. The trigger runs server-side on submit, so it's not bypassable by client configuration.
What Diversion Surfaces on Merge
Even with path exclusion in place, teams occasionally get into states where bake provenance is uncertain — particularly after hotfix merges or cherry-picks that touch both scene geometry and bake files. Diversion tracks the commit ancestry of each file independently and can surface warnings when a bake file's most recent update commit predates scene geometry changes on the same level.
The warning appears at merge validation time, before the merge commits: "MapHub_BuiltData.uasset last updated at commit abc1234 (7 days ago). MapHub.umap has 4 commits since abc1234 that modified static mesh actor data. Consider rebaking before merging to this branch."
We're not saying automated bake staleness detection replaces lighting artist review. A lighting artist knows whether a geometry change is significant enough to require a rebake — Diversion's warning is a prompt, not a gate. But having the staleness signal at merge time rather than discovering it during a QA playtest is the practical difference between catching it cheaply and catching it expensively.
The Derived Asset Principle
Lighting bakes are the most common derived asset that teams version control, but the same logic applies to navigation mesh data (NavMesh.bin or the equivalent in your engine), precomputed visibility data, and shader cache packages. All of these share the property: they're generated from primary content, they're large enough that regenerating them is slow, and they become stale when their inputs change.
The pattern for all derived assets: store them in VCS (the regeneration cost is too high to skip on every sync), mark their source-state provenance explicitly in the commit message, enforce that updates happen only on integration branches not on feature branches, and build tooling that surfaces staleness at merge time rather than at build or ship time. The cost of doing this right is a few hours of branch policy configuration. The cost of not doing it shows up in pre-ship week when you're chasing down why the lighting looks wrong on a level nobody touched in two weeks.