mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-27 22:07:20 +01:00
219 lines
11 KiB
Markdown
219 lines
11 KiB
Markdown
# 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 `.c` file, 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 `ninja` or `configure.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
|
|
|
|
1. Before touching any `.h`, run
|
|
`grep -rl "<header filename>" source/ --include=*.c`. If anything else
|
|
includes it, default to putting the declaration in the `.c`.
|
|
2. 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. `units`
|
|
returned to 189/189 with all 50 of weapons' exact functions intact.
|
|
3. **Never** "fix" this by relying on a coincidental ordering that happens to
|
|
restore the victim.
|
|
4. 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))` from `struct guard_state_data` in
|
|
`actions.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:
|
|
|
|
1. A full `ninja` build. Its semantic gate fails closed if any previously exact
|
|
function stops matching.
|
|
2. `python tools/campaign/board.py` for the byte and function totals.
|
|
3. A **per-function regression sweep**: snapshot the exact set of every unit
|
|
from `build/split` vs `build/base` before 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, N` means a local's scope
|
|
or type is wrong. It is never scheduling noise. This is what reopened
|
|
`_generate_prime_numbers` after 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 /Os`
|
|
and `/O1 /Ob2` all shrink the function to 319 bytes and `/Ox /Os` explodes it
|
|
to 1255, while the campaign flags give the closest result at 368.
|
|
- **Shared-header inlines must stay non-`static`.** Converting the 89
|
|
`__inline` definitions in `real_math.h` (plus 6 elsewhere) to
|
|
`static __inline` costs **21 objects and 119 functions**: `static` lets 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_actor` required
|
|
`real_vector3d *` to `real_point3d *` casts to reach `point_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 = i` to 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_update` shape 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
|
|
the `cseries.h` `MIN` macro 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.
|