Files
BFM-decomp/cookbook/C0041.md
T

4.2 KiB
Raw Blame History

§38 — The WHALE func_80144B9C (770 ins): the -O0 struct-assign memcpy idiom + the -O0 reach-134 ×134 rollout (Phase 24 T7 §G, cheap Opus — no Fable5, no calls.c)

The single biggest byte-weight lever in the fleet (770 ins × reach-134 ≈ +1.6%). It sat at close=2 for a whole session, tagged "needs gcc-2.7.2 calls.c + Fable5" — but a cheap Opus one-liner cracked it. Three durable lessons:

(1) THE CRACK — a STRUCT ASSIGNMENT, not an explicit memcpy() call, for the -O0 block-move. The whale is an -O0 function (prologue 21F0A003; match_one at -O2 reads only 458/770 — compile it -O0). Its 2-insn residual was a memcpy of a 0x24-byte struct. The tell: the target marshals the memcpy args through temp pseudos — lw $v0=src ; lui/addiu $v1=&dst ; addu $a0,$v1,$zero ; addu $a1,$v0,$zero — which an explicit memcpy(&dst, src, 0x24) call does NOT emit (it loads $a0/$a1 directly). That precompute is the signature of gcc's emit_block_move → emit_library_call(memcpy): the original C was a struct assignment dst = *src_ptr; where sizeof(struct)==0x24. gcc-2.7.2 -O0 expands a > MOVE_RATIO-word struct copy to a memcpy library call whose args go through copy_to_mode_reg (pseudos) then addu into the arg regs = the exact 2 extra moves. So: an -O0 memcpy whose target precomputes dst/src into pseudos + addus them into $a0/$a1 (vs a direct load) ⇒ write a struct-assign, not an explicit memcpy(). (The prior session tried every call form — casts, K&R, builtin ±-fno-builtin — but never the struct-assign; no calls.c was needed.)

(2) THE -O0 REACH-134 ×134 ROLLOUT — per-overlay -O0 split + a shared HEADER (not a DEFINE_ macro). An -O0 function that is reach-134 (byte-identical in all 134 overlays because it refs only SHARED globals, not per-overlay data — unlike the Phase-20 overlay-local -O0 cluster) still propagates ×134, but NOT via a DEFINE_func_X() in an -O2 TU: it only matches at -O0 and gcc-2.7.2 has no per-fn -O0 pragma. Mechanism (tools/rollout_whale_o0.py, tools/split_whale.py): each overlay's single .c is splat-emitted in vram order, so a line-based split at the whale's INCLUDE_ASM line carves it with zero item-parsing (before → keeps the <ov> name + nonmatchings/<ov> asm paths; whale → its own -O0 object <ov>_o0b; after → <ov>_after with asm paths rewritten). The whale's C lives ONCE in a shared header src/shared/func_80144B9C.h (NOT a DEFINE_ macro — its 7 local typedefs make a 200-line \-continued macro fragile; a plain header is clean, and the typedefs stay TU-local because <ov>_o0b.c includes only common.h + this header, never engine_core.h). One Makefile wildcard rule $(WHALE_O0B_OBJS): CC1FLAGS := -O0 compiles every <ov>_o0b.o at -O0. Registered as an h_exact dedup group whose source is the header — this works because dedup_integrate.group_members keys only on (binary, vram); a header-share is as valid as a macro-share. Reusable for any -O0 reach-134 giant (verify h_exact reach FIRST, R14 — an -O0 fn is only ×134 if it refs shared globals; the Phase-20 -O0 cluster was overlay-local ×1).

(3) THE memcpy SYMBOL — an __asm__ label, not a shared rename, and never a two-symbol alias. The struct-assign emits jal memcpy (gcc hardcodes the libfunc name). 0x8005C324 IS memcpy (libc2/MEMCPY.o) but the overlays auto-name it func_8005C324. Resolve it in the overlay-only symbol file symbols.resident.txt (memcpy = 0x8005C324) — NOT symbols.us.txt, because main DEFINES memcpy via MEMCPY.o and a shared symbol there would multiple-define in main's link. For an EXPLICIT same-address call elsewhere (the engine_core.h block-copy macro's variable-size memcpys, which can't be struct-assigns), keep the non-builtin C name func_8005C324 but give its extern an __asm__("memcpy") label: the literal identifier memcpy triggers gcc's built-in-memcpy codegen → byte mismatch, whereas the asm-label emits the same jal memcpy under a non-builtin identifier (only a benign "conflicting types for built-in memcpy" warning). splat REJECTS two symbols at one address (func_8005C324 + memcpy in one symbol file → error reading … at extract), so the single-symbol + asm-label is the fix, never an alias pair.