route_ports_on: semantic checks depend on battle RNG timing and on frame-exact after: replays #34

Closed
opened 2026-10-29 23:26:09 +00:00 by Gatchan · 4 comments
Owner

route_ports_on.* (run_route.py --port all=on --check --semantic) cannot pass once any field port is on, independently of the port's correctness. Found while validating the field ports of #32 (PR from field-batch1).

Evidence (JP title_to_first_battle_win, one port on at a time)

Each port is shadow-exact over the three routes and fuzzing (0 mismatch), yet each moves the frame where the first battle starts, so the battle RNG seed (game time) changes and the party HP after the battle changes:

Port on alone First battle starts Party HP after the battle Semantic check
none (movie) 8005 63, 68, 70 -
field/DecTimers 7998 53, 68, 70 FAILED
field/DecTimersMenuBattle 8005 63, 68, 70 (battle ends 9503, movie 9490) ok
field/CalcScrollPos 8009 63, 68, 70 ok
field/GetObjMapPtr 8005 63, 68, 70 ok
field/UpdateCtrlFlags 8018 63, 68, 70 ok
field/UpdateObjFrame 8006 63, 68, 56 FAILED

DecTimers is a 20-instruction per-frame leaf: removing its CPU time shifts the route by 7 frames (sound handshake / event waits, docs/validation.md "Why on is not compared frame by frame"). Which ports "pass" is luck.

With all ports on: title US ok; title JP fails (party_hp); field_menu_tour US fails (party_hp); field_menu_tour JP gets stuck at frame 10156: the route starts with after:title_to_first_battle_win, i.e. a frame-exact replay of the title movie, which no longer ends on the field when ports change timing.

Two causes

  1. party_hp after a random battle is not a semantic invariant once CPU time changes (battle seed = game time at battle start, lessons.md §5).
  2. Routes starting after:<route> replay that route's movie input by input, under ports on.

Possible fixes (decision needed)

  • Replay the after: movie with ports off, switch the ports on at the route's own start; or re-run the previous route's behaviour instead of its movie.
  • Make the battle checkpoint timing-independent (battle won, party alive, monsters dead) or compare party_hp only in exact (non-semantic) checks.
`route_ports_on.*` (run_route.py --port all=on --check <movie> --semantic) cannot pass once any field port is on, independently of the port's correctness. Found while validating the field ports of #32 (PR from `field-batch1`). ## Evidence (JP title_to_first_battle_win, one port on at a time) Each port is shadow-exact over the three routes and fuzzing (0 mismatch), yet each moves the frame where the first battle starts, so the battle RNG seed (game time) changes and the party HP after the battle changes: | Port on alone | First battle starts | Party HP after the battle | Semantic check | |---|---|---|---| | none (movie) | 8005 | 63, 68, 70 | - | | field/DecTimers | 7998 | 53, 68, 70 | FAILED | | field/DecTimersMenuBattle | 8005 | 63, 68, 70 (battle ends 9503, movie 9490) | ok | | field/CalcScrollPos | 8009 | 63, 68, 70 | ok | | field/GetObjMapPtr | 8005 | 63, 68, 70 | ok | | field/UpdateCtrlFlags | 8018 | 63, 68, 70 | ok | | field/UpdateObjFrame | 8006 | 63, 68, 56 | FAILED | `DecTimers` is a 20-instruction per-frame leaf: removing its CPU time shifts the route by 7 frames (sound handshake / event waits, docs/validation.md "Why on is not compared frame by frame"). Which ports "pass" is luck. With all ports on: title US ok; title JP fails (party_hp); field_menu_tour US fails (party_hp); field_menu_tour JP gets stuck at frame 10156: the route starts with `after:title_to_first_battle_win`, i.e. a frame-exact replay of the title movie, which no longer ends on the field when ports change timing. ## Two causes 1. `party_hp` after a random battle is not a semantic invariant once CPU time changes (battle seed = game time at battle start, lessons.md §5). 2. Routes starting `after:<route>` replay that route's movie input by input, under ports on. ## Possible fixes (decision needed) - Replay the `after:` movie with ports off, switch the ports on at the route's own start; or re-run the previous route's behaviour instead of its movie. - Make the battle checkpoint timing-independent (battle won, party alive, monsters dead) or compare `party_hp` only in exact (non-semantic) checks.
Author
Owner

narshe_to_world_map with all ports on (field ports of #32 + battle/Rand):

  • US: the after:title replay held this time; the route reached Terra waking (frame 44482), then got stuck at frame 75223: "Marshal not beaten in 3 attempts" (battle outcomes depend on the game-time seed).
  • JP: stuck at frame 10156, like field_menu_tour JP (the after:title_to_first_battle_win movie replay no longer ends on the field).
    In shadow the same ports are exact over the whole route on both versions (0 mismatch).
narshe_to_world_map with all ports on (field ports of #32 + battle/Rand): - US: the after:title replay held this time; the route reached Terra waking (frame 44482), then got stuck at frame 75223: "Marshal not beaten in 3 attempts" (battle outcomes depend on the game-time seed). - JP: stuck at frame 10156, like field_menu_tour JP (the after:title_to_first_battle_win movie replay no longer ends on the field). In shadow the same ports are exact over the whole route on both versions (0 mismatch).
Author
Owner

Decision (user, 2026-11-02), after an independent review of plan and strategy:

  1. Semantic checkpoints become timing-invariant facts: mode, map, event bits, party composition, scripted-battle outcome (party alive, monsters dead). No HP/MP or other RNG-dependent value after a battle; the exclusion is written in docs/validation.md. Same definition will serve the native build.
  2. Ports-on routes never replay an after: movie: they start from the SRAM start points made by the interpreter (#26).

Also in scope: battle policy more robust (more attempts / input jitter), rom.ports divergences classified against the timing census (first divergence at an unlisted site = finding), validation doctrine (shadow = exact proof; interpreter movies = frame-exact; ports-on/native = route goal + timing-invariant checkpoints) in docs/validation.md. Cycle charging in bridges is not used.

Then PR #35 is updated from main and rechecked. Delegated now (branch ports-on-checks).

Decision (user, 2026-11-02), after an independent review of plan and strategy: 1. Semantic checkpoints become timing-invariant facts: mode, map, event bits, party composition, scripted-battle outcome (party alive, monsters dead). No HP/MP or other RNG-dependent value after a battle; the exclusion is written in `docs/validation.md`. Same definition will serve the native build. 2. Ports-on routes never replay an `after:` movie: they start from the SRAM start points made by the interpreter (#26). Also in scope: battle policy more robust (more attempts / input jitter), `rom.ports` divergences classified against the timing census (first divergence at an unlisted site = finding), validation doctrine (shadow = exact proof; interpreter movies = frame-exact; ports-on/native = route goal + timing-invariant checkpoints) in `docs/validation.md`. Cycle charging in bridges is not used. Then PR #35 is updated from `main` and rechecked. Delegated now (branch `ports-on-checks`).
Author
Owner

Fixed by PR #44 (merged, b3d3f47).

Check definition, before -> after

  • Semantic checkpoint: every key incl. party HP after battles -> timing-invariant facts only (mode, map/position at the goal, party, event bits, battles: {...: "won"}, save slot); a route's EXACT_ONLY keys (party HP) are compared by exact checks only. Exclusion and justification in docs/validation.md (doctrine section).
  • Chaining: after:<route> movie replays removed (run_route refuses them); chained routes start from SRAM start points. New producer route title_to_mines_save (power-on -> first battle -> mines save point (33, 22), slot 1; US 23 834 / JP 23 368 frames) feeds field_menu_tour and narshe_to_world_map.
  • Mandatory battles: behaviours.win_battle, 8 attempts with jitter (first attempt unchanged, movies keep their inputs).
  • rom.ports: first divergence classified against the timing census (E4/E5/E8 sites, US and JP addresses), unlisted site -> FINDING line, still a report.
  • One more timing dependency found and fixed: narshe_to_world_map ended at a world position that depended on input timing (JP ports-on (84, 36) vs (84, 35)); it now saves on a fixed tile (84, 36).

Evidence

  • release ctest, all labels, -j2: 61/61 passed (6 582 s wall, of which ~33 min regenerating the 4 SRAM start points once).
  • route_ports_on (all ports on = battle/Rand), US/JP: title_to_first_battle_win ok/ok, title_to_mines_save ok/ok, field_menu_tour ok/ok, narshe_to_world_map ok/ok, narshe_to_figaro ok/ok.
  • rom.ports: US frame 8792, WRAM $131E, PC $C5:06AF -> E8 (sound driver); JP frame 8678, $131E, PC $C5:06B2 -> E8. No finding.
  • debug (gcc ASan/UBSan) ctest -LE slow 24/24; clang ctest -LE slow 24/24, 0 warnings. The debug slow suite is not runnable here (ASan emulator ~12x slower: ~20 h); debug rom.ports was OOM-killed by an unrelated build on the machine and is being rerun.
Fixed by PR #44 (merged, `b3d3f47`). **Check definition, before -> after** - Semantic checkpoint: every key incl. party HP after battles -> timing-invariant facts only (mode, map/position at the goal, party, event bits, `battles: {...: "won"}`, save slot); a route's `EXACT_ONLY` keys (party HP) are compared by exact checks only. Exclusion and justification in `docs/validation.md` (doctrine section). - Chaining: `after:<route>` movie replays removed (run_route refuses them); chained routes start from SRAM start points. New producer route `title_to_mines_save` (power-on -> first battle -> mines save point (33, 22), slot 1; US 23 834 / JP 23 368 frames) feeds `field_menu_tour` and `narshe_to_world_map`. - Mandatory battles: `behaviours.win_battle`, 8 attempts with jitter (first attempt unchanged, movies keep their inputs). - `rom.ports`: first divergence classified against the timing census (E4/E5/E8 sites, US and JP addresses), unlisted site -> `FINDING` line, still a report. - One more timing dependency found and fixed: `narshe_to_world_map` ended at a world position that depended on input timing (JP ports-on (84, 36) vs (84, 35)); it now saves on a fixed tile (84, 36). **Evidence** - release ctest, all labels, -j2: 61/61 passed (6 582 s wall, of which ~33 min regenerating the 4 SRAM start points once). - route_ports_on (all ports on = battle/Rand), US/JP: title_to_first_battle_win ok/ok, title_to_mines_save ok/ok, field_menu_tour ok/ok, narshe_to_world_map ok/ok, narshe_to_figaro ok/ok. - rom.ports: US frame 8792, WRAM $131E, PC $C5:06AF -> E8 (sound driver); JP frame 8678, $131E, PC $C5:06B2 -> E8. No finding. - debug (gcc ASan/UBSan) ctest -LE slow 24/24; clang ctest -LE slow 24/24, 0 warnings. The debug slow suite is not runnable here (ASan emulator ~12x slower: ~20 h); debug rom.ports was OOM-killed by an unrelated build on the machine and is being rerun.
Author
Owner

Correction to the evidence above: the debug (ASan) rom.ports runs were not OOM-killed; they were stopped on purpose because under ASan the emulator is ~12x slower (5 611 s without finishing, past the 1 800 s ctest timeout). It is slowness, not a hang. rom/slow tests are validated with the release build (rom.ports 335 s, passed, classified E8); debug covers ctest -LE slow (24/24).

Correction to the evidence above: the debug (ASan) rom.ports runs were not OOM-killed; they were stopped on purpose because under ASan the emulator is ~12x slower (5 611 s without finishing, past the 1 800 s ctest timeout). It is slowness, not a hang. rom/slow tests are validated with the release build (rom.ports 335 s, passed, classified E8); debug covers ctest -LE slow (24/24).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Gatchan/FF6Portage#34
No description provided.