11 KiB
Campaign throughput lessons (2026-08-30 / 2026-08-31)
Operational lessons from a run that took the board from 669,879 to 714,807 matched bytes (+44,928) and 4,829 to 4,973 credited functions (+144) with zero regressions, verified by a full build plus a per-function sweep of all 569 built units.
This document is about throughput and hazards - how to close many functions
in parallel without breaking what already works. For per-function matching
technique see matching_methodology.md and
exact_match_acceleration_playbook.md.
1. Write unwritten functions before fighting residuals
This is the single highest-leverage finding. Measured across four waves:
| wave | scope | approach | bytes gained | yield |
|---|---|---|---|---|
| 1 | 71,112 B residual | close residuals | 11,040 | 16% |
| 3 | 27,920 B "sha-only" residual | close residuals | 3,168 | 11% |
| 4 | ~160,000 B unwritten | write missing functions | 30,720 | far larger scope |
Wave 3 deliberately picked the closest residuals on the board - functions whose padded size and relocation count already matched, so only instruction selection differed - and still returned only 11%. Those residuals are typically a single register-allocation or x87 stack-discipline decision, and they resist source-level control.
An unwritten function has no wrong prior to undo. You read the target disassembly and write natural C89. That is a fundamentally easier problem.
The board currently holds roughly 1,000,000 bytes of unwritten functions against 100,000 bytes of residual. Work the unwritten pool first.
Within a unit, start with the small functions. Eight 300-byte closures beat one failed heroic attempt at a 3,000-byte function.
2. Parallel fan-out architecture
tools/campaign/gate.py <unit> compiles one translation unit to a
PID-named object under scratch/ and never writes to build/. That makes it
safe to run from many workers simultaneously.
The architecture that worked:
- One worker per
.cfile, files strictly disjoint. Never two workers in one file. - Every worker measures only with
gate.py. Useful flags:--fn <FUNC>,--disas <FUNC>,--source <FILE>(compile a variant without touching the real file),--edits <json>. - No worker runs
ninjaorconfigure.py. A shared build directory cannot take concurrent writers. - No worker commits. The orchestrator does one full build, one board measurement, and one commit at the end.
- Workers record
gate.py's exact count before starting and revert anything that lowers it.
--disas hides insert/delete holes
gate.py --disas is an index-aligned zip, not an alignment diff. Once
instruction counts diverge, every later row reads as different and a genuinely
inserted or deleted block is invisible. Use scratch/alndiff.py (difflib
SequenceMatcher over normalized capstone streams, branch targets normalized):
python scratch/build1.py <unit>.c scratch/cur.obj
python scratch/alndiff.py <unit> <FUNC> scratch/cur.obj
A 48-instruction hole in _ai_debug_render_actor was only visible this way.
Ignore .rdata / __real@... annotation mismatches in that output - those are
symbol-naming artifacts, not code differences.
3. Header edits perturb objects that never name the added types
"Edit only your own object's header" is not a safe rule.
source/items/weapons.h is included by ten other .c files. Adding two
interface-state structs to it silently dropped units.obj from 189/189 to
188/189, breaking _unit_preprocess_node_orientations - a function that
names none of those types and calls none of the new functions.
Bisecting the three added blocks:
| added to weapons.h | units.obj |
|---|---|
| enum only | 189 ok |
| structs only | 188 BREAKS |
| protos only | 189 ok |
| enum + structs | 189 ok |
| structs + protos | 188 BREAKS |
| all three | 188 BREAKS |
Adding the enum in front of the structs repairs it. The effect is not semantic - it is the C2 allocator's definition-position sensitivity. What perturbs codegen is where declarations sit, not what they mean.
Rules that follow
- Before touching any
.h, rungrep -rl "<header filename>" source/ --include=*.c. If anything else includes it, default to putting the declaration in the.c. - Definitions private to one TU belong in that TU. The weapons fix: structs
moved into
weapons.c; the header kept the enum, the prototypes, and a forward declaration so the prototype's struct tag has file scope.unitsreturned to 189/189 with all 50 of weapons' exact functions intact. - Never "fix" this by relying on a coincidental ordering that happens to restore the victim.
- If a header edit is genuinely required, prove it: build shadow header
trees (patched and pristine), compile every dependent unit against both,
and compare per-function size, relocation count and normalized hash. This
was done to drop
__declspec(align(4))fromstruct guard_state_datainactions.h(24 includers) and it held up under independent re-check.
Some cases have no local remedy
Adding two trailing fields to struct _projectile_datum in projectiles.h -
fields January demonstrably uses at composite offsets 0x1FC and 0x200 - drops
units.obj to 188/189. The type is public, so internalising it in the .c
does not apply. About 1,152 bytes of projectile work is gated on this. That is
an owner decision; do not trade away a complete object for it quietly.
4. Verification discipline
Worker self-reports are systematically optimistic. One wave's rolled-up "confirmed bytes" read 18,544 against 3,168 actually closed, because verifiers counted pre-existing exact functions alongside new ones.
The only numbers that count:
- A full
ninjabuild. Its semantic gate fails closed if any previously exact function stops matching. python tools/campaign/board.pyfor the byte and function totals.- A per-function regression sweep: snapshot the exact set of every unit
from
build/splitvsbuild/basebefore the run, and re-compare after. Object-level counts hide one object lost against another gained.
board.py's per-object listing is truncated - an object appearing in or
disappearing from it is not evidence of a change. Use the sweep.
5. Cross-cutting codegen facts confirmed this run
- Frame size is structural. A differing
sub esp, Nmeans a local's scope or type is wrong. It is never scheduling noise. This is what reopened_generate_prime_numbersafter a prior session had declared evidence exhaustion on it (81.61% -> 91.15%). - Local declaration order is inert for stack-slot assignment - proven byte-identical across four permutations. Do not spend waves permuting declarations.
- The zero-register pin. January frequently keeps a zero live in a register
and spends it repeatedly (
mov [mem],reg,push reg,cmp reg,zeroreg) where our build rematerialises immediates. Pinning a zero consumes a callee-saved register, so this surfaces downstream as stack-slot permutations and duplicated or merged blocks rather than as a local byte difference. It is not a compiler flag: on_lra_allocate,/O1,/O2 /Osand/O1 /Ob2all shrink the function to 319 bytes and/Ox /Osexplodes it to 1255, while the campaign flags give the closest result at 368. - Shared-header inlines must stay non-
static. Converting the 89__inlinedefinitions inreal_math.h(plus 6 elsewhere) tostatic __inlinecosts 21 objects and 119 functions:staticlets VC7 dead-code the out-of-line copy and inline more aggressively, changing call sites. This also kills the theory that our ~20-28 surplus COMDATs per object come from storage class - non-static is what reproduces January's bytes, so the surplus-COMDAT question remains open and is a whole-tree property, not something an individual object's reconstruction introduced.
6. Guards against fake matching
A byte match bought with source Bungie would not have written is worth less
than an honest residual. Rejected on sight: volatile, register, #pragma,
barriers, compiler-flag changes, raw numeric struct offsets, type punning or
representation casts, undefined behaviour, byte forcing, and logic that is
nonsensical for the function's stated job and only coincidentally matches.
Three live examples from this run, all correctly refused:
- The only shape that narrowed
_ai_debug_render_actorrequiredreal_vector3d *toreal_point3d *casts to reachpoint_from_line3d. That is representation punning, so it was reverted - and re-reading the numbers, it only reached 24,864 of 24,976 bytes anyway. It was never a closure, just a 16-byte improvement on a 128-byte gap. - Writing an early return as
*num_primes = ito deliberately spend a live zero is a coincidence generator, not a reconstruction. XDK 3911 folds it to identical code and it was already on a do-not-repeat list. - A
vehicle_updateshape that forced an increment into both arms of a clamp using two invented file statics measured the right size only because the shortfall was cancelled downstream by alignment padding. The real shape is thecseries.hMINmacro with the constant on the left and no local increment at all.
Inline assembly is allowed sparingly and unit-locally in small helper
and math functions. The admitted case is fast_ftol (fld / fistp), because
/QIfist lowers every C float-to-int conversion through a 64-bit fistp qword
and a 32-bit inline fistp is never compiler output. Keep it static __inline
in the .c that needs it - never in a shared header.
7. Reusable scratch tooling
Left in scratch/ by this run (note that scratch/ is per-worktree and is not
committed, so these exist only in the worktree that produced them):
| file | purpose |
|---|---|
alndiff.py |
true alignment diff over normalized capstone streams |
build1.py |
compile one unit with the real build flags to a chosen .obj |
cs_build.py, cs_gate.py, cs_blast2.py |
shadow-header-tree blast-radius harness |
pn_score.py |
score source variants by byte divergence and first-diff offset |
pn_try.py |
batch-apply source variants through gate.py and report |
Use the committed tools/fake_match_scan.py for fake-match review; unlike the
scratch helpers above, it is available in every checkout.
A caution learned twice the hard way: Python heredocs in this environment eat
one level of backslashes. Build Windows path separators with chr(92) rather
than writing them literally, or you will silently emit a single-backslash
__FILE__ string where the source needs a doubled one and break relocation
identity against the target's string literal.