Files
goldeneye-pc-port/docs/HANDOFF-switch-macro.md
T
James Dansereau 5ea46fabc1 PC port: full compile + link milestone (ge007.x86_64.exe builds clean)
All ~235 translation units now compile and the target links under MSYS2
GCC 16 + CMake/Ninja (ninja ge007 -k 0: 236/236, zero errors). N64 build
untouched; every src/ change is -DPORT-gated.

Portability fixes (findings D7-D17 in docs/PCPortResearch.md section 11):
- AI X-macro system: SWITCH() call sites activated to the author's
  hand-written byte-identical references under PORT; port/shim/
  bondaicommands.h + bondconstants.h + CPPLib.h re-emit the IDO paste
  tricks (isSubroutine, file-record macros, generated CALL) GCC-clean.
- 64-bit pointer-in-word initializers: monAnim jump targets now go
  through an index table resolved by the interpreter (chrai.h/propobj.c);
  UsetuplenZ.c credits pointer PORT-gated to 0.
- IDO printf engine compiled for PC (xprintf/xlitob/xldtob) with a
  shadow xstdio.h resolving the _Printf prototype clash.
- Host-libc gaps stubbed in port/: bcopy/bzero, __libm_qnan_f,
  tlbmanage*, g_ViXScales/YScales; chrObjRandom PRNG ported from .s.

Asset data strategy (D16): single-file assets compiled for real
(animation tables, fonts, textures, rareware logo); obseg per-file .c
excluded (colliding file-local globals the N64 build isolates via
per-ELF linking); 758 ROM-derived table symbols + ramrom_* + segment
markers stubbed in port/src/assetstubs.c / n64stubs.c.

fast3d is a no-op placeholder (port/src/gfxstub.c) until Phase 2 brings
in the real software RSP. Binary not yet runnable end-to-end: ROM
loading in romdata.c is still a TODO.
2026-08-20 20:27:06 -04:00

200 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF — SWITCH/CALL macro port (D7/D8) — IN PROGRESS
## Status: BLOCKED on SWITCH expansion mangling (root cause now fully understood)
The `ge007` PC build fails on `src/game/chraidata.c` (the **only** file that uses
`SWITCH()`; 4 call sites) due to IDO-vs-GCC preprocessor differences in the AI-list
macro system. The `isSubroutine`/`MODELSKELETON` shims are done and working. The
**variadic `SWITCH` shim produces a mangled expansion** — this session confirmed the
exact mechanism and uncovered the *intended design* of the macro, which reframes the
fix.
---
## What was done this session (investigation + empirical confirmation)
### 1. PD port is NOT a reference (kills the previous top-priority lead)
`PD_PORT_CHECKOUT` has **no `bondaicommands.h` / `aicommands*.h`
at all** — Perfect Dark is a different game with no AI-list macro system. Do not look
there for a SWITCH shim. (It does have `include/PR/os_message.h` with `IS_EMPTY`,
unrelated.)
### 2. Empirically CONFIRMED the content-splitting mechanism
Minimal test `build-pc/swtest.c` (still on disk): a `PlayAnimation(A..E)` macro that
expands to a trailing-comma list, a 6-param `IMPL`, and a variadic
`SWITCH(VAR, ...) → IMPL(VAR, __VA_ARGS__, ,,,,,)`. Result:
```
error: macro 'IMPL' passed 16 arguments, but takes just 6
```
**Mechanism (now proven, not hypothesized):**
1. At SWITCH parse time the content `PlayAnimation(...) BREAK` is **one** argument
(its commas are inside `PlayAnimation`'s parens).
2. C rule: each macro arg is **fully macro-expanded before substitution**. The content
expands to a **top-level comma list** (`AI_PlayAnimation , ... , BREAK`).
3. Forwarding `__VA_ARGS__` into `_PORT_SWITCH_IMPL(...)` **re-parses** that token
stream at top-level commas → the content is **split into N separate args**, which
mis-fill `CASE0..CASE_CONTENTF`. The trailing comma of `PlayAnimation` adds one
extra empty arg (hence 16, not 15).
### 3. Preprocessed the real file — mangled output confirmed
`gcc -E src/game/chraidata.c` (full flags in §Build) shows at the line-183 call:
```
AI_SetNewRandom 0}; Error: Switch Limited to 15 elements + 1 default u8 (183) & 0x00FF[] = {
((0) & 0xFF00) >> 8 ((0) & 0x00FF,lblNext) 0x10(lblDone)
AI_Label , lblNext , AI_PlayAnimation (((ANIM_scratching_leg) & 0xFF00) >> 8,lblNext)
((183) & 0xFF00) >> 8(lblDone) AI_Label , lblNext , AI_GotoNext , ANIM_scratching_leg , & 0x00FF(lblDone) ...
```
Content fragments each get `(lblNext)`/`(lblDone)` spliced in; `CASEF` gets filled
(triggers "Switch Limited"); a fragment forms `BREAK(<2 args>)` → "BREAK passed 2
arguments" at chraidata.c:202.
### 4. Discovered the INTENDED DESIGN of the content (key reframing)
The content is **not** meant to survive as a single arg. It is a **template whose last
token is a dangling function-like macro name**, and the SWITCH body **appends
`(lblDone)` to complete that call**:
- `BREAK(LABEL)` → `GotoNext(LABEL)` → `AI_GotoNext , LABEL ,` (aicommands2.h:70)
- `GotoNext(LABEL)` → `AI_GotoNext , LABEL ,` (aicommands2.h:52)
- `PlayAnimation(...)` → `AI_PlayAnimation , CharArrayFrom16(..) , ... , INTERPOL ,`
(trailing comma; aicommands2.h:386)
- `Label(ID)` → `AI_Label , ID ,` (aicommands2.h:137)
- `SetNewRandom()` → `AI_SetNewRandom ,` (aicommands2.h:1555)
- `IFRandomGreaterThan(BYTE,LBL)` → `AI_IFRandomGreaterThan , BYTE , LBL ,` (aicommands2.h:1607)
- `IFNewRandomGreaterThan(BYTE,LBL)` → `SetNewRandom()IFRandomGreaterThan(BYTE,LBL)` (aicommands2.h:1623)
Game code writes the content as e.g. `PlayAnimation(...) BREAK` (line 183) or
`TRYFiringRun` / `... TRYFireAtBondKneeling` (line 305) — i.e. a run of macro
calls/names **ending in a bare function-like name** (`BREAK`, `TRYFiringRun`,
`TRYFireAtBondKneeling`). The body line
`EXPAND_ARGS_STACK(CASE_CONTENTx)(lblDone)` therefore expands the dangling name into
`NAME(lblDone)`.
**Consequences for the port:**
- `EXPAND_ARGS_STACK(CASE_CONTENTx)` is *not* a 1-arg call in practice: after
substitution the content's comma list fills `A,B,C,...` of its 33 params and the
rest are empty (IDO leniency). Its body re-joins the non-empty args with `COMMA()`
→ it is effectively an **identity / full-expansion pass** over the content. So the
shim's replacement of `EXPAND_ARGS_STACK(CASE_CONTENTx)` with bare `CASE_CONTENTx`
is semantically fine *if* the content arrives intact.
- `IS_EMPTY(CASE_CONTENTx)` becomes a **multi-arg call** to a 1-param macro after
substitution. IDO tolerated it (first-arg → `_IS_EMPTY_<tok>_` undefined → 0 =
"not empty", which is the correct answer when content is present). GCC hard-errors
(the `pasting '_' and '('` at CPPLib.h:257 is this).
### 5. Exact arg counts of the 4 call sites (corrects the old "4–25" note)
Counted top-level commas (comments stripped, paren-aware):
| line | args | shape |
|---|---|---|
| 183 | 20 | VAR + 5×(CASE,VAL,CONTENT) + `,,`default + CONTENT |
| 236 | 14 | VAR + 3×triple + `,,`default + CONTENT |
| 305 | 11 | VAR + 6×triple + `,,` + CONTENT + `,,` + CONTENT (two defaults) |
| 623 | 26 | VAR + 8×triple + `,,`default + CONTENT |
So the range is **11–26 args**, always `VAR` + a run of `(CASE, VAL, CONTENT)` triples
where a "default" is encoded as `,,` (empty CASE, empty VAL) before a CONTENT.
---
## The core blocker (precise statement)
The content must reach the body **as one argument** (so `IS_EMPTY`/`EXPAND_ARGS_STACK`
see it whole and `(lblDone)` lands on the dangling name). But:
- It is **one arg only at parse time**; C expands it to a top-level comma list before
substitution (step 2 above).
- Any forwarding of that expanded list through `__VA_ARGS__` into a fixed-param macro
**re-parses** it and splits it. That is exactly what the current shim does.
The original 49-param SWITCH avoided this because the content was bound to a **named
param** (`CASE_CONTENTx`) at parse time and substituted directly — never re-parsed.
GCC won't let us call a 49-param macro with 11–26 args, which is why we went variadic,
which reintroduces the re-parse. **That tension is the whole problem.**
---
## Directions for the next session (in suggested order)
1. **Protect the content from re-parsing by wrapping, not by re-parsing.**
The only C protection for a comma list is parentheses. Investigate a `SWITCH(VAR,
...)` that forwards each arg **parenthesized** so the content's commas can't split
it, then strips the parens in the body. The open sub-problem: identifying which
args are content vs. simple CASE/VAL without already having split them. Note CASE
and VAL are always simple single tokens (`IFRandomGreaterThan`, `250`); only
CONTENT carries commas. A positional scheme (content = every 3rd arg) may be
exploitable *if* the wrap happens before expansion — verify whether that's
achievable at all, since expansion precedes any body processing.
2. **Write a fresh GCC-clean SWITCH that emits byte-identical output** (abandon the
"verbatim original body" approach). First capture the *clean* IDO output for one
call (e.g. hand-expand the line-236 14-arg call, or reconstruct from the body
semantics now understood), then design a variadic macro that reproduces it. This
sidesteps `IS_EMPTY`/`EXPAND_ARGS_STACK` multi-arg issues entirely if the new body
doesn't route content through them.
3. **Shim `CPPLib.h`** (new `port/shim/CPPLib.h`, PORT-only, includes the real one)
to make `IS_EMPTY`/`_IS_EMPTY`/`EXPAND_ARGS_STACK` tolerant of multi-arg /
short-arg calls (variadic + first-token semantics). This is likely needed *in
addition to* whichever content-protection scheme is chosen, because even an intact
content becomes a multi-arg `IS_EMPTY`/`EXPAND_ARGS_STACK` call after substitution.
Keep the N64 build untouched (shim is PORT-gated).
4. **Verify byte-identity** by diffing the PC preprocessed `m_IdleAnimations[]` (and
the other 3 arrays) against a reference expansion. There is no IDO binary to diff
against, so the reference must be reconstructed from the body semantics (direction
2) — treat "compiles + plausible AI byte layout" as the practical bar, and record
the reasoning.
---
## Files touched / present
| File | State |
|---|---|
| `port/shim/bondaicommands.h` | D7 shim. `isSubroutine` ✅. `SWITCH` variadic → **mangled** (this is the blocker). |
| `port/shim/bondconstants.h` | D8 shim. `MODELSKELETON`/`New_ModelSkeleton` ✅. |
| `build-pc/swtest.c` | Throwaway minimal repro of the split (16-vs-6 args). Keep until fixed, then delete. |
## Build / preprocess commands
```sh
export PATH="/c/msys64/mingw64/bin:$PATH"
cd build-pc && cmake --build . --target ge007 2>&1 | grep -iE "error|FAILED" | grep -vE "Building C object"
```
Preprocess just the failing file (flags mirror `build-pc/compile_commands.json`):
```sh
cd REPO_ROOT
gcc -DAVOID_UB=1 -DBUGFIX_R0 -DBYTEMATCH -DLANG_US -DLEFTOVERDEBUG -DLEFTOVERSPECTRUM \
-DPAL=0 -DPLATFORM_64BIT=1 -DPORT=1 -DREFRESH_NTSC -DVERSION_US -D_LANGUAGE_C=1 \
-Iport/shim -I. -Iinclude -Iinclude/PR -Isrc -Isrc/game -Isrc/libultra -Isrc/libultra/audio \
-Iport/include -Ibuild-pc/port/include -std=c11 -funsigned-char \
-include build-pc/port/include/versioninfo.h -E src/game/chraidata.c
```
## Key references
- `src/bondaicommands.h` ~790–810 — original 49-param `SWITCH` body (ground truth for
intended expansion).
- `src/aicommands2.h` — all AI command macros (lines cited in §4 above).
- `include/CPPLib.h` — `IS_EMPTY`/`_IS_EMPTY` (256–258), `IS_PROBE`/`PROBE` (227–231),
`IF_VA`/`_IF_VA_0/1` (~328, 330–331), `EXPAND_ARGS_STACK` (337+), `EVAL` (85–98),
`COMMA` (16), `CAT` (190).
- `src/game/chraidata.c:183,236,305,623` — the 4 `SWITCH` call sites.
- `docs/PCPortResearch.md` §11 — findings log; record the new findings as **D9+**
(D7 = isSubroutine/SWITCH shim, D8 = MODELSKELETON, both already logged).
## Constraints (from AGENTS.md)
- Game code (`src/game/*.c`, `src/*.c`) compiles **UNMODIFIED**. All fixes go in `port/`.
- Shims are `PORT`-only and inert for the N64 build.
- `port/shim` is first on the include path, so `<bondaicommands.h>` /
`<bondconstants.h>` (and, if added, `<CPPLib.h>`) resolve to the shims; shims
include the real headers via `src/...` / `include/...` paths.
- Output must be byte-identical to the original IDO expansion.