Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/cflags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4282,3 +4282,6 @@ src/levels/02_maktar_nebula/L02_003FE8B8-g8.c -O2 -G8 -ffunction-sections -frac2
src/levels/02_maktar_nebula/L02_003FDDE8-g8.c -O2 -G8 -ffunction-sections -fno-gcse
src/levels/11_joba/L11_00443940-g8.c -O2 -G8 -ffunction-sections -fno-gcse
src/levels/shared/L13_0041AD00-g8.c -O2 -G8 -ffunction-sections -frac2-qcopy -frac2-qcopy-barrier -fno-sched-spec -Wa,--r5900-la-addend=sn
src/us2/game/002D3138-g8.c -O2 -G8 -fno-expensive-optimizations -ffunction-sections -fno-builtin
src/us2/levels/02_maktar_nebula/L02_00400E04-g8.c -O2 -G8 -ffunction-sections -frac2-gcse-unsplit-edges -frac2-keep-block-order
src/pal/levels/shared/L08_003DF82C-g8.c -O2 -G8 -ffunction-sections -malign-loops=2
28 changes: 28 additions & 0 deletions docs/contributing/matching-blockers.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,34 @@ the 2.2f constant and the mode load, the last two tie, and the load is allocated
out of the block gives retail's pair but moves the FPRs. When two integer registers swap in a
block with few locals, count its quantities before trying spellings.

### A PAL constant that reorders local allocation

**10 November 2026, retained near miss.** `func_PAL_L09_0040FD68` (PAL, level 9, default
`-O2 -G8 -ffunction-sections`) is the USA `func_L09_0040AC48` with 50 Hz constants, and is
984/984 bytes with one `$v0`/`$v1` and `$f3`/`$f4` swap in its tick test. The extra `2.0f` of
`(x * 300.0f + 2.0f) * 0.16666667f` is scheduled by sched1 before the counter's store. That pushes
the conversion after it, so `ticks` loses local-alloc priority to `ticks + 1`, and the converted
value ties `2.0f`, which is born first. Enumerating the block's 16,098 legal orders under
`floor_log2(refs)*refs/(death-birth)` leaves 2 that give retail, both with every constant set
before the conversion and the conversion before the store. About 55 forms (operand orders,
struct-member accesses, counter types and casts, post-increments, named constant locals, an
inline timer accessor, `/ 6.0f` under `-ffast-math`) and six profiles (`-fstrict-aliasing`,
`-fno-schedule-insns` among them) were measured; none moves sched1. The matched `func_PAL_L17_003CBDF4` timer test shows sched1 hoisting all three constants
when no store-to-load chain dominates; here both `lwc1` depend on the counter's `sh`. The `-dS`
trace shows the r5900 model issuing constant moves on the memory unit with `lhu`; at t = 1 the
store-to-load chain makes `lhu` outrank `300.0f`, which every retail-compatible order needs first,
and retail keeps that dependence. So retail's RTL must differ before local-alloc (reference
counts or a transient insn), not its schedule. Retained in
`nonmatching/levels/func_PAL_L09_0040FD68.c`. A private cc1 with a quantity dump in `block_alloc` (identical assembly with
the dump off) confirms it: no suggested registers; `ticks` 3000 against `ticks + 1` 5000, and
the conversion ties `2.0f` at 1250 and loses on quantity number. A second pass (round 7, about 80 block compiles
and 34 profiles) measured that one extra insn between the `lhu` and the conversion, tied to
`ticks`, gives retail's whole assignment: the post-increment's `andi` and a `long` round trip's
`dsll`/`dsra` both do, as real code. Retail fits such an insn that emits nothing, such as a move
or `extendsidi2` between equal registers; cse, combine and shared variables remove every copy tried.
**Next:** a matched function whose retail shows the same `lhu`/`addiu`/`sh`/`mtc1` idiom with a
wider or re-typed counter.

### 16-bit counters read through a wider local

**1 November 2026, local match.** `x++ > n` on a `u16` field keeps the old value in a HImode pseudo
Expand Down
1 change: 1 addition & 0 deletions nonmatching/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,4 @@ the placement rules are in `tools/fcheck.py`'s docstring; its tests are
| [`func_PAL_L04_0041B5C0`](levels/func_PAL_L04_0041B5C0.c) | 652 | 9 November 2026: PAL, now 652/652 bytes, 0/2 places, 29 real rows (lead follow-ups: kx set after the corner coordinates; x, then the first label coordinate, then y gives retail's interleave; left: y's shifts recomputed after the dead test's boundary, so no $v1 copy; entry load pair; closing x1/y1 registers). First pass 648/652. Menu box with two labels, same shape as USA func_L04_00416E38 (menu_coord idiom). Retail loads the 371.0f constant after the width call and schedules the D_001A73C0/gp-offset loads before the slt; mine hoists them differently. Needs -G8 (L04_0041A660-g8.c). |
| [`func_PAL_L03_003CB058`](levels/func_PAL_L03_003CB058.c) | 656 | 9 November 2026: PAL port of func_L03_003C7018 (USA best C, own -Wa,--r5900-la-addend=sn unit): 656/656 bytes, PAL 0/1, 13 masked positions. PAL retail equals USA retail apart from addresses; same leftovers (draw 2 ori/f15, buffer addiu after the key copy, first sprintf rematerialises the buffer). Outer-scope buffer gives 11 but swaps the frame. |
| [`func_PAL_L03_00426DE8`](levels/func_PAL_L03_00426DE8.c) | 412 | 9 November 2026: PAL port of func_L03_00421930 (USA flat C, own -fno-schedule-insns unit): 412/412 bytes, PAL 0/1, 12 masked positions, all the USA p s1 / base s3 register swap. PAL retail equals USA retail apart from addresses. |
| [`func_PAL_L09_0040FD68`](levels/func_PAL_L09_0040FD68.c) | 984 | 10 November 2026: PAL port of func_L09_0040AC48 (50 Hz constants, timer as (x * 300.0f + 2.0f) * 0.16666667f): 984/984 bytes, 0/1, only case 3's tick test swaps $v0/$v1 and $f3/$f4: local-alloc priority after sched1 (2 of 16,098 legal block orders give retail's assignment, all with the constants set first). About 55 compiles, then about 80 block compiles in round 7: retail's assignment is reproduced by an extra insn between the lhu and the conversion that is tied to ticks (andi or dsll/dsra in the measured forms), so retail fits one that emits no code; no lawful source for it found. Post-increment gives retail's registers plus an andi. Needs -G8. |
182 changes: 182 additions & 0 deletions nonmatching/levels/func_PAL_L09_0040FD68.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* 10 November 2026, round 7 (about 80 scratch compiles of case 3 in batches, 4 check.py runs): no
match; the C below is unchanged and still the best form (984/984 bytes, the same four register
rows). New model, measured: an extra GPR insn X between the lhu and the conversion
gives retail's whole assignment. With X the conversion is ready one cycle later, so sched1
emits lhu, 300.0f, +1, 2.0f, sh, X, 1/6, cvt; X's result is tied to ticks (ticks dies in X),
so the quantity has 5 refs (7142 against 5000 for ticks + 1) and takes $v0, and the converted
ticks is born after 1/6 (1428 = 1428, 1/6 first) and beats 2.0f (1111): $f2, $f3, $f4, $f5 as
retail. Two forms show it, both with X as real code: the post-increment (X = andi 0xFFFF, also
through a 16-bit bit-field and through an inline accessor) and long wide = ticks;
(float)(int)wide (X = truncdisi2, dsll/dsra; the sign extension before it is folded into a
paradoxical subreg). Retail therefore fits an X that emits nothing: mips_move_1word prints no
code for a move or an extendsidi2 whose two registers are equal. No lawful source found for
one: cse replaces every in-block copy, combine folds a copy with a single use into
floatsisf2, a copy into a variable shared with another case (next, sound, speed, accel) makes
that variable global (no tie; $s2, $a1 or $f20 appear), and floatdisf2 needs double float.
Without effect (block identical to the C below): the limit, sum or converted ticks through
shared or new locals, compare reversed or negated (c.le.s), statement expression, comma form,
+= 1, T = T + 1, an unsigned short local, inline helpers for the tick, the conversion, the
compare and the 50 Hz scale, (x * 60.0f) * 5.0f (adds a multiply), and 34 flag profiles
(-fno-gcse, -fno-regmove, -fno-rerun-cse-after-loop, -fno-sched-interblock, -fno-sched-spec,
-fno-schedule-insns2, -fno-cse-follow-jumps, -fno-cse-skip-blocks, -fno-thread-jumps,
-fforce-addr, -fno-force-mem, -ffloat-store, -fcaller-saves, -O3, -O1 with both schedulers,
-frac2-keep-block-order, -mno-split-addresses and others). Worse: the limit or the sum
computed before the store (the two lwc1 move to the top); (float)(unsigned int)ticks (bltz
fix-up path); old = ticks++ (a daddu copy); T++ after the read (loads first). A do {} while (0)
around the if doubles the references inside it (flow weights by loop depth) and gives ticks
$v0 and ticks + 1 $v1, but its loop note is a scheduling barrier (constants after the
conversion, $f5 for it), and around the whole case it changes nothing. Open point: whether
sched2 would put cvt back before 1/6 once X is gone (retail's final order) is not measured.
Next: find what real construct leaves a tied no-op move (or SI-to-DI extension) of the counter
at local-alloc, e.g. in a matched USA or PAL function whose retail code shows the same
lhu/addiu/sh/mtc1 idiom with a wider or re-typed counter; otherwise leave it. */
/* 10 November 2026, round 6 (instrumented cc1): a private cc1 built from the same sources with a
quantity dump in local-alloc.c's block_alloc (control: identical assembly with and without the
dump) prints case 3's block exactly as modelled. No quantity has a suggested register. ticks
(r89, refs 3, birth 4, death 14) has priority 3000 against ticks + 1 (r159, 5000), so ticks + 1
takes $v0. The converted ticks (r160, 14..30) ties 2.0f (r166, 10..26) at 1250 and loses on
quantity number, so 2.0f takes $f3. 1/6 (r168, 1666) takes $f2 and 300.0f (r164, 1111) $f5.
Retail needs ticks above ticks + 1 and the conversion above 2.0f. Measured probes, not
proposals (an invented test is not acceptable without retail evidence): a dead test on ticks is
gone before allocation (no change), and a later use of ticks makes it a global pseudo, so
ticks + 1 keeps $v0. Next: a source difference with retail evidence that changes these counts,
e.g. an inline or macro shape for the 50 Hz conversion that the PAL code shares elsewhere. */
/* 10 November 2026, round 5 (5 compiles): matched PAL code that reads the same u16 frame counter
(src/pal/levels/18_damosel/L18_003FF850.c) uses an inline moby_timer() accessor and writes
the 50 Hz conversion as (x * 5.0f + 2.0f) / 6.0f in a -ffast-math unit. Here: the accessor
(ticks = moby_timer(obj)) is unchanged; moby_timer(obj) - 1 after the store is 992 bytes;
(x * 300.0f + 2.0f) / 6.0f is 980 bytes without -ffast-math and, with it, compiles to the
multiply form, so the swap stays. About 55 compiles in all. Next: an instrumented cc1 printing
the case-3 block's REG_N_REFS and qty priorities, to find what retail's source changes before
local-alloc. */
/* 10 November 2026, round 4 (5 compiles plus the sched1 trace): casts and widths on the counter
((unsigned short)(ticks + 1), + 1u, += 1, (float)(unsigned short)ticks, an s16 local) change
nothing (the s16 local adds an andi). The -dS trace shows why the schedule cannot move. The r5900 model
issues the const_double moves on the memory unit with lhu, sh and the loads, and at t = 1 it
picks lhu (300) over 300.0f (316), because only lhu's chain runs through the sh and the two
dependent lwc1. Both retail-compatible orders need 300.0f at t = 1, which rank_for_schedule's
priority rule excludes while that dependence exists, and retail keeps the loads after the
sh, so the dependence is there too. The round-3 "global pseudo" account would need real uses of
those values in other blocks, which retail does not have: treat it as unlikely. Conclusion:
retail's RTL differs before local-alloc (ticks with more references, or an insn that is gone by
the final code), not in its schedule. Next: find what in PAL's source adds a reference to
the counter, e.g. compare other PAL ports whose USA original reads a u16 timer field. */
/* 10 November 2026, round 3 (2 compiles plus dumps): compared with the matched PAL timer test in
func_PAL_L17_003CBDF4 (src/pal/levels/17_smolg/L17_003CBDF4-tilt-g8.c). There sched1 hoists the
300.0f, 2.0f and 1/6 constants to the start of their block, as the two retail-compatible orders
here need. Here the counter's sh is the critical path because both lwc1 depend on it, so the
store and its inputs take the early slots. A const-qualified pointer for the two timer fields does
not mark the loads unchanging (no change). A second account fits retail exactly: if ticks + 1, 300.0f and
2.0f were global pseudos (live in more than this block, as when cse reuses a constant register
across an extended block), local-alloc would give ticks $v0, 1/6 $f2 and the converted ticks
$f3, and global-alloc then $v1, $f4 and $f5, which is retail's assignment. Next: look for the source shape
that makes those three pseudos non-local without adding instructions. */
/* 10 November 2026, round 2 (about 30 more compiles): the swap is local-alloc priority, not source
order. tools/regalloc.py shows case 3's block after sched1 as lhu, 300.0f, +1, 2.0f, sh, cvt,
1/6, the two lwc1 (which depend on the sh: obj and data may alias), add, mul, add, mul, c.lt.
With floor_log2(refs)*refs/(death-birth), ticks (3 refs, 5 insns) loses $v0 to ticks+1 (2 refs,
2 insns), and the converted ticks ties 2.0f, which is born first and takes $f3. The USA original
schedules the conversion before the sh (no 2.0f there), so ticks wins, as in retail. Of the
16,098 orders the block's dependencies allow, only 2 give retail's assignment under that
priority: 300.0f, lhu, 2.0f, +1, 1/6, cvt, sh, then the loads. Every constant is set before the
conversion, and the conversion comes before the store. Tried without effect: operand orders of the
compare and the 50 Hz expression, the counter and the timer fields as struct members (alias
classes), unsigned short/unsigned int counters, read-back and pre-increment forms, the
constants as named locals (cse folds them back), and the profiles -frac2-gcse-unsplit-edges,
-fno-expensive-optimizations, -fno-schedule-insns (988 bytes), -g, -fstrict-aliasing (case 2
and the loads move). Post-increments give retail's registers only with an andi; the recorded
wider-local remedy ("16-bit counters read through a wider local") gives a daddu copy instead.
Next: an RTL difference that makes sched1 set the constants first, e.g. what retail's other
PAL timer conversions look like before sched1 in a matched function with the same idiom. */
/* 10 November 2026: func_PAL_L09_0040FD68 (984 bytes, level 9), PAL. 984/984 bytes, 0 of 1 places:
only case 3's tick test differs, a register swap. Retail keeps the tick count in $v0 (its +1 in
$v1) and in $f3, with 2.0f in $f4 and 300.0f in $f5; this C gives $v1/$v0 and $f4/$f3. Tried
(10 compiles): the comparison reversed; (float)(count)++; the post-increment into ticks (exact
registers, but an extra andi 0xFFFF before the conversion); a pointer to the count; the
assignment inside the store; the limit or the converted ticks in a named local; the 50 Hz
conversion as an inline helper. Next: a form that reads the count once as retail does without
the extra zero-extension of the post-increment. Needs -G8 (its own L09_0040FD68-g8.c unit). */
#include "common.h"

/* func_PAL_L09_0040FD68, the PAL copy of func_L09_0040AC48 (src/levels/09_dobbo/L09_0040AC48-g8.c):
the same C at 50 Hz, where 0.000277777785f becomes 0.00040000002f, the 240-frame hold becomes
200, the timer's 60.0f becomes the scaled (x * 300.0f + 2.0f) * 0.16666667f, and
func_L09_0040B0C8 becomes func_PAL_L09_00410200. */
extern void func_002A7750(void *, int, int);
extern int func_002B3868(void *);
extern void func_L09_0040B008(char *);
extern void func_PAL_L09_00410200(char *);
extern int func_002E5CF8(int, int, void *);
extern void func_002E59F8(int);
extern void func_002AA548(float *, float *, float, float, float, float);
extern float D_L09_001B15A8 __attribute__((sda));
extern float D_L09_001B15AC __attribute__((sda));
extern char D_00188660[];
typedef struct { float value[2]; float rate[2]; } DobboDoorMotion;
void func_PAL_L09_0040FD68(char *obj) {
char *data = *(char **)(obj+0x68);
int next, next4, sound, ticks;
float speed, accel, speed4, accel4;
char *entry;
switch (*(unsigned char *)(obj+0x20)) {
case 0:
*(int *)(obj+0x98) = 0;
*(unsigned short *)(obj+0x34) = (*(unsigned short *)(obj+0x34) | 0x2141) & 0xEFFF;
func_002A7750(obj,1,-1);
break;
case 1:
if (*(unsigned char *)(obj+0xBC)) func_002A7750(obj,2,-1);
else {
*(int *)(obj+0x98) = 0;
*(unsigned short *)(obj+0x34) = (*(unsigned short *)(obj+0x34) | 0x41) & 0xEFFF;
}
break;
case 2:
if (func_002B3868(obj)) {
*(unsigned short *)(obj+0x34) = (*(unsigned short *)(obj+0x34) & 0xFFBE) | 0x1000;
*(int *)(obj+0x98) = *(int *)(*(char **)(obj+0x24)+0x10);
func_L09_0040B008(obj);
*(int *)(data+0x1C) = func_002E5CF8(0,4,obj);
}
next = (*(int *)(data+0x14)+1)&1;
speed = D_L09_001B15A8 * 0.00040000002f;
accel = D_L09_001B15AC * 0.00040000002f;
if (((DobboDoorMotion *)data)->value[*(int *)(data+0x14)] < 1.0f)
func_002AA548(&((DobboDoorMotion *)data)->value[*(int *)(data+0x14)],(float *)(data+8)+*(int *)(data+0x14),1.0f,speed,accel,1.0f);
if (((DobboDoorMotion *)data)->value[*(int *)(data+0x14)] > 0.6f)
func_002AA548(&((DobboDoorMotion *)data)->value[next],(float *)(data+8)+next,1.0f,speed,accel,1.0f);
func_PAL_L09_00410200(obj);
if (((DobboDoorMotion *)data)->value[next] >= 1.0f) func_002A7750(obj,3,-1);
break;
case 3:
if (func_002B3868(obj)) *(int *)(data+0x18) = 200;
ticks = *(unsigned short *)(obj+0x96);
*(unsigned short *)(obj+0x96) = ticks + 1;
if ((float)ticks > ((*(float *)(data+0x20)+*(float *)(data+0x24))*300.0f + 2.0f) * 0.16666667f)
func_002A7750(obj,4,-1);
func_PAL_L09_00410200(obj);
break;
case 4:
next4 = (*(int *)(data+0x14)+1)&1;
speed4 = D_L09_001B15A8 * 0.00040000002f;
accel4 = D_L09_001B15AC * 0.00040000002f;
if (((DobboDoorMotion *)data)->value[next4] > 0.2f)
func_002AA548(&((DobboDoorMotion *)data)->value[next4],(float *)(data+8)+*(int *)(data+0x14),0.2f,speed4,accel4,1.0f);
if (((DobboDoorMotion *)data)->value[next4] < 0.6f)
func_002AA548(&((DobboDoorMotion *)data)->value[*(int *)(data+0x14)],(float *)(data+8)+next4,0.2f,speed4,accel4,1.0f);
func_PAL_L09_00410200(obj);
if (((DobboDoorMotion *)data)->value[*(int *)(data+0x14)] <= 0.2f) {
obj[0xBC] = 0;
*(int *)(obj+0x98) = 0;
*(unsigned short *)(obj+0x34) = (*(unsigned short *)(obj+0x34) | 0x41) & 0xEFFF;
func_002A7750(obj,1,-1);
sound = *(int *)(data+0x1C);
if (sound != -1) {
entry = D_00188660 + sound*0x70;
if (*(char **)(entry+0x88) == obj && *(unsigned char *)(entry+0x74)) func_002E59F8(sound);
}
*(int *)(data+0x1C) = -1;
}
break;
}
}
5 changes: 4 additions & 1 deletion src/pal/game/002A0F68.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#include "include_asm.h"

INCLUDE_ASM("asm/pal/game", func_PAL_002A0F68);
/* Original assembly: the PAL copy of func_002A05A8 (src/game/002A0430.c), the same routine with
two mul.s by 1.2f (50 Hz) added. Like it: 26 trapping add/addi/sub/dsub, $at as a working
register, and two cvt.w.s words in an encoding the compiler never emits. */
ASM_FUNC("asm/pal/game", func_PAL_002A0F68);
5 changes: 4 additions & 1 deletion src/pal/levels/shared/L01_00305C50.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#include "include_asm.h"

INCLUDE_ASM("asm/pal/levels", func_PAL_L01_00305C50);
/* Original assembly: the first 720 bytes are func_0029D4D0 (src/game/0029BCC8.c, original assembly:
a qmtc2 VU0 op, 15 trapping add/addi and three alternate entries), followed by an 84-byte tail
with a fourth alternate entry, a trapping addi and two teq traps. */
ASM_FUNC("asm/pal/levels", func_PAL_L01_00305C50);
Loading
Loading