Branches, hooks or diffs: three ways to patch an engine
Every team that needs a modified engine picks one of three approaches, usually by accident. We maintained all three at various points between 2025 and 2026, and each one fails in a place that tells you whether it fits your team.
Approach one: a long-lived fork branch
Clone the engine, commit your changes, rebase onto each upstream release. The attraction is that it uses tools everyone already knows and keeps history. The failure arrives at the first rebase after six months of changes: forty commits, three of them named “fix build”, and no way to tell which one causes a test to fail after the move.
$ git rebase upstream/main
# 14 conflicts in 9 files
# resolved by hand over two days
$ git log --oneline | head
8f1c4a2 fix build again
0c9b7e1 fix build
...
A branch also makes review hard for anyone outside the team. The diff of a fork against upstream includes every upstream change since the fork point, so a reviewer sees thousands of unrelated lines around the twelve that matter. We tried tagging our own commits and diffing tags; it works until somebody rebases and the tags point at new objects.
Approach two: runtime hooks only
Do not touch the source. Ship an extension or an injected library that changes behaviour at runtime. This is genuinely the best option for a large class of changes: intercepting a request, adjusting a rendered tree, adding a panel to a developer tool. It requires no build, so it tracks upstream automatically and never conflicts.
It fails for anything decided at compile time. If a feature is behind a build flag, the code that implements it may not be in the binary at all; no hook can call it. The same applies to code paths removed by the optimizer and to debug information. We measured one case where a runtime hook could hide a UI element but could not stop the corresponding 11 MB of renderer code from being compiled and shipped. For a team whose goal is a smaller local build, that is a fatal limit.
Approach three: a directory of diffs
Keep the engine checkout clean and apply a short list of unified diffs, each with a header describing its base, dependencies and target files. The checkout is disposable; the series is the artefact you version, review and test.
patches/
series.toml
0100-build-flags.patch
0200-debug-hooks.patch
0300-devtools-extensions.patch
The advantage shows up in review. A pull request that adds one patch is one screen of diff plus a header, and the reviewer can apply it to a clean tree with one command. The lockfile gives the whole combination a name, so a bug report says “series 4c1e, revision 8f2c41d9” instead of “my build”.
The cost is discipline. A patch must have one purpose, or it becomes a branch in miniature. A patch must state its base, or the three-way merge invents context. A patch must be re-tested when upstream moves, and no tool can decide for you whether the moved code still does what you meant.
How they fail
| Approach | First failure mode | Recovery cost |
|---|---|---|
| Fork branch | Rebase conflicts pile up; history stops explaining the change | Days per upstream release once the branch is old |
| Runtime hooks | A change needs compile-time code that is not in the binary | Rewrite the change as a build patch, if the flag exists at all |
| Diff series | Patches drift from upstream and conflict one by one | Minutes per patch, bounded by the number of patches |
The recovery cost is the useful column. A branch hides its conflicts until you rebase, then presents them all at once. A series surfaces a conflict on the patch that caused it, which is usually one file and one hunk.
What we do not claim
A diff series is not automatically reproducible. We had a directory of patches for eight months before the lockfile existed, and during that time two builds from the same series could differ because one of them had an edited patch that nobody committed. The format made the problem visible; the lockfile solved it. Read the post on reproducibility for the rest.
Nor is a series the right answer for a team of one who changes two lines twice a year. A local branch and a note in a text file is cheaper, and no tool should talk you out of the simplest thing that works. The series starts paying for itself when more than one person builds, or when the same series has to produce the same result on a machine you do not own.
Choosing
Write down the changes you need. If all of them are runtime, ship a hook and stop reading. If more than two are compile-time, and more than one person builds, use a series: the format is described in Patch format, and the built-in catalogue gives you working examples to copy. If you are somewhere in between, start with a fork branch, keep each change in its own commit, and switch when the first rebase takes longer than a day.