Open reverse-engineering threads
The live list of this project's unsolved mysteries. Every row is a reverse-engineering question about how Legaia's runtime works that is still contested, with what is already known and the concrete next step that would close it. If you are looking for a way to contribute, this page is the invitation.
How to use these three pages
Reverse engineering a sealed game produces three kinds of knowledge: questions still being worked, questions answered, and readings that turned out to be wrong. Each has its own page. Before starting a hunt, check all three - if a question is not on this page, it is probably already answered or already disproved.
| Page | Holds | Read it when |
|---|---|---|
| This page | Live hunts: open, partial, mostly resolved | You are picking up work, or want to know whether a question is still contested. |
| Settled threads | Answered questions, each carrying an evidence grade | You need the answer to something, or are about to build on a claim and want to know how firmly it is pinned. |
| Do not re-walk | Falsified hypotheses, reasoning intact | A reading of the bytes looks obvious and you want to check nobody has already disproved it. |
A falsified row is kept with its reasoning, because the reasoning is the deliverable - "the world-map slot-4 bodies are coastline wireframes" is a very plausible reading of those bytes, and knowing why it is wrong is worth more than the row it occupies.
Where the live hunts are
Each live hunt sits in one area of the game. The table counts them per area and by status, so you can see at a glance where the open questions cluster before reading any of them. Pick an area, jump to its table, and read a row's last column: it names the probe, dump or capture that would close the question.
| Area | Live | By status | What the area covers | |
|---|---|---|---|---|
| Field / locomotion | 0 | nothing open | Walking towns and dungeons, scripted events, scene boundaries: the field overlay (PROT 0897) and its script VM. | |
| Battle / rendering | 23 | 21 open 2 partial | The battle overlay, its screen chrome and the cast-module band; the overworld's rendering questions are filed here too. | |
| Audio / BGM | 2 | 1 open 1 partial | Music, sound effects and streamed voice: the SEQ sequencer, the VAB banks, and the field-VM opcodes that drive them. | |
| Title / boot / overlays | 0 | nothing open | Boot, the title screen, and which overlay image sits in which RAM window at which moment. | |
| Containers / data blobs | 0 | nothing open | What a PROT entry's bytes are: the packer's layout, inherited tails, and which image a table belongs to. | |
| Measurement + tooling | 1 | 1 open | The project's own instruments - save states, reference sweeps, census tools - whose answers decide which other rows can be trusted. | |
| All areas | 26 |
The counts, the areas and every row below are rendered from docs/reference/open-rev-eng-threads.md when the site is built, so a thread that opens or closes in the docs changes this table in the same build. A thread is either a row of an area's table or a write-up of its own whose first line states its status; a write-up that a row links to is that row's long form and is not counted twice.
An area with nothing open stays on the page and says so - "this area has nothing open" is worth knowing before you start a hunt in it - and points at the settled and falsified pages for that area.
What an evidence grade means
When a thread closes it moves to settled threads carrying a grade: a label for what kind of evidence its answer actually rests on. Grades are explained here because they are the first thing to check when a settled fact is load-bearing for live work, and because a thread on this page is only closable by evidence of the strongest two kinds.
Where a row cites more than one kind, it is graded by the weakest load-bearing claim - the one that breaks the conclusion if it is wrong.
| Grade | The row cites |
|---|---|
| disassembly | Instructions, addresses, opcode encodings, branch or store sequences. The strongest grade. |
| capture | A runtime capture, save state, probe, firehose, or disc-derived oracle. |
| decompiled-C | Ghidra's C output, a FUN_x(...) call signature, a Ghidra label or plate comment, or a claim about store order / store count / a boolean operator with no instruction behind it. |
| inference | Reasoning from surrounding facts, corpus absence, or analogy, with no direct evidence cited. |
decompiled-C is the re-audit bucket, not a wrong-answer bucket. It marks a claim nobody has confirmed against instructions. Most are probably right; the point is that none has been checked. The C is a rendering: dropped register arguments, || printed as nested ifs, reordered or omitted stores, and hand-written Ghidra annotations read as fact have each already put a wrong statement on these pages. The catalogue of rendering artifacts on the Ghidra tooling page is also the grading rubric. When a decompiled-C row is load-bearing for something you are about to build, re-derive it from the disassembly first.
Status conventions, and how a thread is laid out
| Status | Meaning |
|---|---|
| open | Active hunt. A concrete next step exists; the row names it. |
| partial | The main result is pinned; a residual sub-question remains. |
| mostly resolved | The mechanism is pinned; one leg is unconfirmed. |
Many rows qualify the status in parentheses - partial (transcode closed), open (narrowed) - naming how far it got. Read the parenthetical.
Each area below opens with a table of one-line rows. A thread whose write-up outgrows a table cell keeps its one-liner in the table and links to a subsection after it; the full analysis - every address, capture and falsification - lives in that subsection under its own Status line, with the long evidence folded into a disclosure so the page scans.
Recently corrected
Claims that stood on these pages and were overturned. A claim that was wrong once is the cheapest place to look for one that is still wrong.
Readings a re-audit against the disassembly or a capture recently overturned. They are listed here as well as in the registers, because a claim that was wrong once is the cheapest place to look for a claim that is still wrong. The list stays short: an older correction leaves it and keeps its row in the settled or falsified register.
- The member who finished the fight strikes the victory pose, not the leader. The action SM's victory arm writes the acting slot
ctx[+0x13]last:FUN_801E295C'ss5isctx + 0x11(0x801E2994), so itssb v1,0x2(s5)stores (0x801E66EC,0x801E6740,0x801E6760) arectx[+0x13]stores. A search for0x13(offsets missed them and read the leader as the poser (settled). - Worn gear teaches a summon, and the port never ran it. The stat aggregator's grant and revoke scans were documented and tagged, but no code wrote the spell list: a Talisman taught nothing and a final-form Ra-Seru no Meta, Terra or Ozma. Running
FUN_80042558off the disc against the port showed it, and the same instrument showed level-ups never rolling their stat spread (method). - The record's
+0x08array is spell experience, and+0x00/+0x04are XP words.FUN_801E70BCcompares it against0x8007656C(lw 0x5D0(v0)at0x801E7208), so "no consumer has been traced" was stale (battle.md). - The
0x75EACD input volume has a writer.FUN_8003D764state6setsa0 = 0x75at0x8003D92Cand reaches the sharedjal 0x8003EE7Cat0x8003D9F4; a scan for117loaded just before a call missed it, and the audio page's open item said the port mixed every line at unity while the port already mixed cues at that level (settled). - The gold shop's 99 cap lives in the list, not the stepper. The glitch page read the stackable path as capped by the stepper's
min(gold / price, 99, 99 - held); run off the disc, the stepperFUN_801DB7F4opened at 99 held still buys the copy the list seeded (0x801DB2B4). Its "every build has a reachable sibling" reading overstated it: the USA list builder marks a row built at 99 (0x80030F0C) and the list refuses it before either buy path runs (glitches, shop). - The port's text widths were the draw layout's, not
FUN_80035F04's. The balloon spawner and the world-map labels carriedFUN_80035F04's tag overFont::layout, which draws author and0xCEescapes with other advances; the measurer run off the disc matchesFont::measure(dialog-font). The Muscle Dome page's reading of the same call as a screen projection is corrected too: HUD element0x1Ameasures the fighter's name (muscle-dome). - Every
+0x16Ereader reads the whole halfword. Seven port readers took one of the port's two halves (raw bits, tracker), so a Confuse the tracker held never reached the pickers and a cured Stone never put its palette back (status word, palette). .MAPand.PCHare field files, not sound-tool intermediates. The two strings in the sound-driver cluster are referenced once each, by the field-file loaderFUN_8001F7C0(0x8001F834,0x8001F8CC): they name a scene's walkability map and walk-on trigger sidecar, both on the retail disc (settled, falsified).- No retail entry is a truncated TMD. The
tmd_size_prefixclass matches nothing on the corrected extraction: all 182 TMD-led stream entries hold the whole mesh, so there was never a tail for a loader to supply (settled, falsified). - The captured "four-level jump" is one level. Record
+0x00is the cumulative XP and+0x04the next threshold, whichFUN_801E9504rewrites (0x801E9FA0); the diff's365 -> 730is Vahn's L3 to L4 threshold beside a level byte2 -> 3(settled). - A four-slot event list's beat is decided by the disc, and is rarely
0. The read index is the low byte of the entry's RAM address, and the battle heap places each monster block by a fixed law: ids load upward (0x800543EC..0x800549F0), each block is carved from the top of the free node, and the first ends at0x800B9A50with one member seated. Demon Fly's attack reads209, longer than its clip; the port had read0for every fight. The juggle window's measured overrun is the pass grid, not a cursor carry - the commit zeroes the cursor at0x8004B068(settled, falsified).
Field / locomotion
Walking towns and dungeons, scripted events, scene boundaries: the field overlay (PROT 0897) and its script VM.
No open threads in this area.
Answered questions: settled field / locomotion, settled text / dialog; falsified readings: field / locomotion, menus / UI.
Battle / rendering
The battle overlay, its screen chrome and the cast-module band; the overworld's rendering questions are filed here too.
| Thread | Status | What would close it |
|---|---|---|
| Why do a few strikes and casts fall under the measured frame step? | open (measured) | Medusa's strike, Windhash, Spoon's Hyper Heal and Moldy Worm's strike read lower once the ring feeds ordinary frames: on Medusa a party member mid-fade near the lens moves the replay's gate, on Moldy Worm and Devil Pump a bearing unit. Death Trigger parks in |
| What keeps the span law off by default? | open (measured) | Priced from the polygons it links (a frame's work, |
| What parts Gimard's late arms from retail? | open (measured) | Arm 12's stand, its coin and arm 10's victim show are ported (cast-module), and arm 12 reads |
Why do the heal cue pillars draw larger in the engine than on battle_menu_rugi_barra_retail? | open (narrowed) | The four live parts (records |
Why does retail's step fall from 4 to 3 partway through Ozma's arm 21? | open (measured) | Four retail recordings of the |
| Which creature seats do the directors still not carry? | open (narrowed) | Reading each cast's |
| What does a real console's frame cost read? | partial (measured) | The entry is |
Why do setup frames at step 3 fit 4 yaw units below the walk back? | open (measured) | Over the |
| Which framing style did the earlier actor's Spirit leave? | open (narrowed) | The earlier actor is the RAM's: |
What is left of minigame_dance_retail_go_out's .985? | open (measured) | The beams, the drop shadows and the clip phase are retail's (dance). Of the |
Why does Zeto's idle in zeto_big_wave_mid_cast show one pass older than the display lag says? | open (narrowed) | A film of the cut shows the cut pass drawing the advanced cursor ( |
| Which capture-band bodies still run their arms in a pass each? | open (narrowed) | PROT 0963 is directed arm by arm (mid-cast film |
| What is left of Koru's Gigaton Press film? | open (narrowed to the frame span) |
|
| What is left of the battle camera against retail? | open (measured) | Ranked by |
Why does battle_jou_cort_181_strike draw two walk rings where retail draws one? | open (measured) | The effect-script walk reads the crossing cursor (the effect script), so Cort's walk ( |
| What is left of Zenoir's Fire Claw film? | open (narrowed) | A second recording from |
| What holds Seru arms low on every film seat? | open (measured) | Gobu Gobu Seru arms low plain, on |
| Why do Caruban's Fire Breath stills sit four yaw units off? | open (narrowed) | The camera law is settled forward by a film; what is left is the near bodies' placement or the displayed pass (below). A |
| What holds the Delilas and Cort casts' arms low on every film seat? | open (narrowed) | Seven classes are closed, the latest Blazing Slash's flames: its stager seats a cone pair per emitter call, not the whole set once (Blazing Slash). On |
| Why does Big Wave's still read darker over the sheet's rows? | open (narrowed) | The upper frame is closed: PROT 0898's band wash now draws at retail's alpha, scroll and dither (the presentation calls, the screen-space dither), rows |
Long write-ups in this area
Caruban Fire Breath stills four yaw units off
open (narrowed) battle_rugi_caruban_2_ / _3_fire_breath_mid_cast read image .931 / .938, camera 1.000. The displayed pose is the RAM's table walked back two step-3 passes (case 0xA re-arms at ceil(gap / 6), the yaw endpoint climbing 24 a pass), and the walls fit it best. Near bodies fit four yaw units further back (.988 / .982).
The _2_ state is saved mid draw tick: seats 2 / 3 hold view translations +0x2C on the camera one step-3 pass back on the table's increments (pitch 43, yaw 3508, to a unit), which pins the walk back's first pass.
The law the walk back inverts holds forward to the unit. A film of the cast from the _2_ state, with Caruban's clip 9 seated on the state's own cursor 40 and the party on the state's idle cursors, walks case 0xA onto retail's yaw pass for pass (3499, 3514, 3535, 3556, 3580), with pitch, TR and focus, and hands on to the group shot 0xB at the first hit to the unit (the cast driver's shots). So the four units are not the yaw law: they belong to the near bodies' own placement on the displayed frame (Vahn's and Gala's idle pose or ground pair), or to the pass the screen shows. Grade capture.
At step 3 every gap a pass leaves has a root on two increments, 3 units apart, and the RAM keeps neither. Taking their midpoint in every walk back lifts these two stills to .966 / .977 but lowers 58 others (zora_glare_petrify_pre by .038), so the walk back keeps its single root until a film pins which root each pass stood on.
A walked-back pose about twenty units of yaw off
open (narrowed) The yaw half is settled (settled): battle_map01_gomboo_steal_mid_cast's 3610 is the walk back's own, and two films confirm the re-arm law the walk back inverts, yaw and focus alike. What is left is the near-camera ghosts' placement below.
On the party-art stills the ghost's placement is the displayed camera, not the body or a near-lens draw rule (battle-actor-rendering). battle_ropeway_party_gala_flying_knee_late reads .985 on the walk back with the arm before's heading a retail film shows (yaw 2152 against 2143), and battle_rikuroa_party_vahn_chain_open draws its missing band on a camera eight yaw units short of its walk back. What no RAM image holds is that earlier heading: a hit's re-face overwrites the target's, and the rings keep no older one. Grade capture.
The other capture once listed here, battle_geremi_ogre_strike (190 against 168), was a locked walker nearest on ring slot 3, whose displayed pass is the walk's last and whose table chased the walk's bearing (retail-compare); its walk back now lands on 189. battle_jou_cort_181_strike's peak about 10 units past its walk back was Cort's body, drawn on the displayed frame's ring pair once seated there (retail-compare, .870 to .957); on that seat its yaw sweep reads .963 at 826.
The frame step is not what holds these strikes low: lowering it (LEGAIA_BATTLE_PARTY_STEP=0) leaves Aluru, Cort 181, Poisonous Worm, Gilium Lv2, battle_melee_hit_spark and Moldy Worm unchanged (Moldy Worm's residue was the walk back's endpoint, retail-compare).
Three strike children peak on their walk back already: battle_map02_poisonous_worm_strike (3328; focus +-20 reads no better), battle_noaru_gilium_lv2_strike (652, on the idle's cursor 0, which a sweep of 0..40 confirms) and battle_korb2_medusa_strike. Poisonous Worm's residue is a translucent body where retail has none: monster seat 4, ghosted (+0x08 = 0x83000000), which the 3328 camera seats at the frame's centre (root SX about 182, SZ about 1939, far from every GTE limit) and the older of retail's two ordering tables (OT 0x800F4B08, CLUT row 485) holds at the right edge, x = 262..339. That table is not the displayed frame's: its ground-grid packets (POLY_GT4, CLUT 0x77C0) fit a camera between yaw 2880 and 3050 (the lattice fit has more than one minimum), the newer table's (0x800F099C) the RAM's own 2551 to under a pixel, and the VRAM buffer at y = 4 shows that framing while the displayed one at y = 244 shows the 3328 the walk back gives. So it is the frame between the displayed one and the RAM's, and the displayed frame's packets are no longer in RAM; the ghost's position on screen has to be read off the image. Read off the image, the walk back holds: a focus sweep of +-120 round (-298, 331) at yaw 3328 peaks on it (.867, the next best .863), and the yaw is the chase law's (2551 back two passes of ceil(gap / 12) a vsync at step 3, 2882 then 3323; the older table's grid fits 2883). What the three map02 strikes share instead is the sky. On every map01 / map02 / map03 battle state the engine's top rows read about +7 per channel over retail's (an offset, not a gain: e = 0.99 r + 7.3 on red), where every other stage reads within 2, bright ones included (town01, noaru, stone). Under the block tolerance that is harmless on its own, and it puts the near-camera ghosts over the sky past it: taking the offset off the upper frame lifts battle_map02_poisonous_worm_strike .867 to .912, lip_kid to .876, death_wings to .887. The offset was the backdrop's depth cue: the dispatcher cues a backdrop at IR0 = weight | 1 and the DPCS floors, so its 0x80 words reach the GPU as 0x7F, a 5-bit step under the texel (battle-stage-camera). Cued that way the sky offset is gone and the three states read .949, .890 and .888; what is left on Lip Kid and Death Wings is the near-camera ghosts' placement. Grade disassembly + capture. The same cue costs 27 non-overworld captures one block each (battle_jou_vera_v3_heal_mid_cast .954 to .949 the most): every such block already read 10 to 20 darker in the engine before the cue moved it a unit, and the darkness is not one draw. On jou_vera_v3_heal it is the floor grid: retail's newer table's POLY_GT4 packets predict the buffer at y = 4 to under half a 5-bit step, and the displayed buffer at y = 244 reads 1.6 steps brighter, so the frame on screen was drawn on a higher ambient base than the RAM's floor 0x20. displayed_ambient_base keeps a floored base as read, and the backdrop weight (576, three step-3 passes of the latch) bounds the base's start from above only, so the displayed base needs the dim's starting value, which no word keeps. On the bylon / kor / taiku captures the blocks sit on the slot-1 horizon mist band's edges. Grade capture. It is a placement residue, not a near-lens draw rule: the near body sits about 500 world units from the eye, where a small error in the eye's position moves it several times as far as it moves Noa or the worm. A yaw near 3100 with the same focus puts it at the edge but moves the rest of the frame (image .84 to .53), so the eye's position, not its yaw alone, is what is off. Grade capture.
Recordings confirm both halves of the law (inc = ceil(gap_before / 0xC), re-armed every pass, the yaw endpoint drifting with the counter and the focus endpoint with the bodies). A walk back that knows each earlier pass's endpoint lands on the displayed focus; one that holds the RAM's endpoint does not, by up to 36 units on a party's return walk 0x20. The same film shows the return's held passes arming case 8 on the knocked-back target: each pass's focus endpoint is the target's body pair as that pass read it, so the endpoint moves the whole of the target's slide. walker_end_shift reads the return state too, taking case 8's whole target step there and case 7's half step as on the strike loop (retail-compare); of the corpus's four 0x20 captures battle_melee_hit_spark reads image .919 to .954 and rim_elm_gimard_seru_capture_before .994 to .984, the other two do not move. Grade capture. battle_map02_poisonous_worm_strike and battle_nilboa_aluru_v2_strike peak on the law's yaw, so their residue is not the camera; Aluru's was the readout bar and actor plaque one pass short of landing on screen (retail-compare, 0.865 to 0.988). Grade capture.
Xain's frame at Bloody Horns' band entry
partial battle_tunnelc_xain_bloody_horns_mid_cast (image .855, camera 1.000) shows Xain square to the camera with a translucent body behind him (Vahn, seat 0, ghost bits 0x83000000 in the RAM). The RAM's pass is arm 0's, and retail's table reads that arm's case 6 from yaw 2850, rising 68 a vsync onto 3658. The frame on screen is two passes older, inside 0x6F.
The direction of the turn is settled: the replay's camera framed the incoming seat in the Done band's last pass and turned down through 0 from yaw 1012, where retail's held the finished actor's shot through the setup states and turned up (between two actors). The replay now turns up too, but reaches arm 0 at yaw 3052 where retail stands at 2850: on the chase law that is about five passes, or an earlier actor's shot other than the replay's (Noa's Spirit at yaw 2228).
The capture now stands on the gate's own pass: past arm 0 it is placed by the close-up accumulator ctx[+0x87C] the arm's clip commit zeroed (image .719 to .799), and the headless channel takes retail's live camera inside the walk arm's case-6 follow (camera .878 to 1.000, retail-compare). A cast film seated on each slot's recorded facing reads .873, and with the last arm's double drain .885. No yaw over the whole turn exceeded .76 in the image before those seats, so the rest of the residue is Xain's and Vahn's placement.
A film from a state a round earlier that reads the camera through the earlier actor's Done band would name the pose the band set out from; the earlier actor itself is settled. Grade capture.
Xain's pose is half of what was left. The rings say the walk clip committed inside the display lag: the cursor ring reads 16, 0, 0, 288 and the clip-record ring the walk on its newest two slots, the idle on the third, with the position ring (502, 192), (546, 197), (590, 202). Arm 0 queues the walk with the +0x1DC bump (0x801F72C4..0x801F72D0), so the frame on screen shows the idle on the ring's pair, where the replay showed its own idle at 176 on the RAM's pair. Posed on the rings, Xain reads .845 (.871 on the idle's last keyframe one slot older, so the slot the screen shows is not pinned to one keyframe). Grade disassembly + capture.
The other half is Noa (seat 1): retail draws her as the ghost, the engine solid. Her +0x8 is clear in the RAM, which fits the ghost pass's pose one pass before the RAM's (yaw 2850, her planar distance 1302 against a radius 1258), and the walked-back pose the image child hands the pass (yaw 2578) clears her too. Ghosting her needs a pose with yaw about 2986 or more at that focus and depth, the RAM's own, or a depth past 5600. Forced, it reads .006 higher. The image scan agrees that the screen's camera is not the walk back's: with Xain posed, the replay's own pose (yaw 3052, focus x 535) reads .868, and the walk back's (2714, 615) .742. A film across the band's entry would give the pose.
The cast's own band entry is not the residue. A film of Bloody Horns across its entry, from battle_tunnelc_xain_strike on a recording that carries each slot's facing, reads 0x6F .996, arm 0 .998 and arm 1 .920 (1.000 with --rand-track), .981 over the whole cast; its older recording without the facings skipped the walk and read .865. Terio Punch's entry from battle_tunnelc_xain_bull_charge_mid_cast tracks retail's camera to the unit through 0x6E, 0x6F and arm 0 once the engine child targets the group code 8 the cast retargets to (the bare default 0x0C00 depth, FUN_801F0348); on the recorded seat 3 it framed Xain's own 0xD00 and ghosted Vahn. What the still lacks is therefore the replay's history ahead of the band, the earlier actor's Done band and the pose Noa's ghost test read there, and no state of this fight sits earlier. Grade capture.
The film from the state (.947) reads .986 on retail's own frame steps, so the rest of it is the frame step. Retail opens the walk arm at step 2 (the state's ring holds 371..495), the engine at 3 (its walk frames cost 600 and more), and Xain's walk stops one step further on, (-420, 111) against (-401, 119), which moves every close-up of arms 3 to 5. And the line 0x158 arm 2 cues (0x801F751C, 317 vsyncs) loads the frame on vsyncs: retail's arms 3 to 5 run seven-vsync frames under it and reach arm 6 after the clip has ended (cost 563 from vsync 384, step 3), where the engine's table arms run their four and arm 6 spends about 100 ticks at step 4 under the clip's load. Grade capture.
Zenoir's walk over a small window
Status: open (narrowed). Retail's arithmetic lets Zenoir's creature step over a victim's range window (510 units a pass against a window 448 across, +0x1F = 14) and hold arm 8 for good (cast-module.md). The port hit this in about one Zenoir cast in two hundred and guards it.
The pace is retail's: every 0x86 recording on disk, from three fights (the Gobu Gobu state, the tower and Kor), runs arm 8 at step 4 on frames clamped at 0x2BC (700), after three step-3 passes, so the creature's stride is the 510 units the window can miss. Whether a retail fight misses depends only on where a victim with +0x1F = 14 (monsters 27, 36 and 63) stands on that stride's lattice. Grade capture.
Evidence that closes it: a PCSX-Redux recording of Zenoir against monster 27, 36 or 63 reading the creature's +0x34 through arm 8. The probe cannot inject a party cast from a strike state of those fights (battle_jou_theeder_v3_strike and kin): its seat test reads the acting byte at 0x0A, which still names the previous actor there, so the recording needs a state saved at a party member's command prompt in one of them.
Zeto's Big Wave idle, one pass older
The RAM holds Zeto's idle cursor at 60 (rate +0x21D 4, Big Wave's halving; all sixteen frame-history entries step 2), so two passes of display lag put the shown frame at 52. The image reads .962 with Zeto seated there and .980 seated at 48, while a display lag one pass longer wrecks the pan's framing (.462). The same extra pass costs cort_mystic_shield_mid_cast and moves the other Cort capture-band states by under .002, so it is not a corpus-wide lag.
It is not the double buffer's phase: the floor grid of the table under the current record index (0x8007BF30 + i * 0x74, index gp+0x434) fits the RAM's camera, so the state was saved mid-pass and the screen is two passes back, as assumed (cort_mystic_shield_mid_cast reads the same way, horn_summon_mid_cast the other: its newest floor sits under the other record). Nor is it the module's pacing: a seat-0 Big Wave recorded on both sides (LEGAIA_POKE32 sends the cast down Big Wave's branch) holds arms 0 and 4 one pass each and arms 5 to 8 within two vsyncs of retail's game clock. What is left is the pose the frame drew, and the obvious reading of it does not hold across states: battle_gimard_tail_fire_b, saved mid-pass too, draws its knockdown exactly two advances behind the RAM's cursor (72 against 56), where Zeto's idle needs three. A per-pass RAM sequence of a seat-0 Big Wave around arm 0 cannot separate the two either: the arm-0 cut moves the camera on the same pass the rate halves, so the body packets' motion between tables is mostly the camera's. A stretch where the camera holds still and a body's rate or clip changes (a commit that zeroes the cursor) would show which advance the drawn pose stands on.
That stretch exists and the drawn pose is not the odd one out. A PCSX-Redux film of a seat-0 Big Wave (autorun_drawn_pose_film.lua with LEGAIA_SPELL=0x56, LEGAIA_POKE32=0x801C8FE4=1) logs every pass of the band 0x6E..0x71: each seat's anim tick advances its cursor and the same pass's decode draws the advanced value, party and monster alike, through the caster's rate halving on the 0x70 entry and a monster commit. No pass skips a decode or draws an older cursor, so the extra advance is not a draw-order effect; what is left is the instant the state was saved inside a long pass (a step-3 pass runs across vsyncs, and a save between the party's ticks and the monster's splits the seats) or the image metric preferring a neighbouring sway pose. zora_glare_petrify_pre reads the same way round for one seat only: Vahn fits one advance behind the RAM (96 of 108), Zora two (168 of 216), in band 0x6F.
A save can split the seats, but not that way round. The films put a vsync, the instant a save state is cut, between the ticks and the decodes, between two seats' decodes and after the flip, and once inside a seat's tick: on rim_elm_gimard_victory's art commit, Vahn's tick (which commits clip 0x11 and halves every seat's rate) runs across a vsync before Gimard's tick starts. The actor list ticks the nodes in descending address order, party seat 0 (0x800835E4) first and the monster rows after it, in both filmed fights, so a save inside the list leaves the party one advance further from the screen than the monsters: Vahn two back and Zora one, the reverse of the fits, and no split puts a seat three back as Zeto's 48 needs.
Nor is it the decode. FUN_8004998C builds its pose in the battle context's scratch block (s2 = *(gp+0xA0C) + 0x6F4, twelve bytes a part, the centroid at +0x120), and Zeto is the last body the state's pass decoded: all sixteen parts there are the blend of cursor 60 (frame 3, fraction 12) to the halfword, angles through the short-arc guards, and the body pair (-1, 912) is that pose's centroid turned by the heading. The port's blend gives the same pose at the same cursor. Which pass the RAM's body pair belongs to also tells where in the pass a state was cut: Zeto's is the live cursor's (saved after the draw), and the frame driver's order - ticks, the view build FUN_800172C0, the render passes, then FUN_80016B6C's VSync, PutDispEnv, PutDrawEnv and DrawOTag of the table just built (0x800171C0..0x80017288) - puts the screen two tables behind the cursor at either instant. The fit itself is sharp: Zeto seated at 36 / 42 / 46 / 48 / 50 / 52 / 56 reads .877 / .948 / .963 / .980 / .970 / .962 / .929, and moving the camera's depth (the Big Wave pan's 26 a vsync) does not trade against it - at 52 the best depth is thirteen units out (.966), at 48 the RAM's walk-back stays best. The near plate filling the close-up is in both ordering tables the RAM holds, drawn last over the chest. The other framebuffer (the draw area, (0, 4)) holds a wide shot with no HUD that matches neither table, so it is no witness to the lag.
Nor is it the draw. Both ordering tables, rasterised in retail's chain order against the state's own VRAM (scripts/mednafen/ot-raster.py), match the port's depth-tested frame at 1.000: the newest table (record 1, the current index) is the port at cursor 60 on the RAM's camera, the other at cursor 56 with the camera one pass back (TR z 52 nearer). No primitive is missing, extra, ordered differently or drawn with another attribute, and the two tables step the cursor 4 and the pan 26 a vsync, as the RAM's cursor ring and tween say. The residual is the displayed frame alone, and that frame is PROT 0946's arm-0 pass: arm 4 adds scalar << 6 (512) to the countdown 0x801F7F20 and arm 5 drains scalar * step (16) a pass (0x801F6F10..0x801F6F5C), so the RAM's 496 is arm 5's first pass, the older table arm 4's, and the screen arm 0's cut, whose TR z prescale(z / 2) = 2150 is the camera the image child already draws. On that one pass the port's frame fits only off the walk: cursor 48 with the camera's yaw +6 reads .993, every other seat lower, against the 1.000 of the two passes after it. What the cut pass draws differently (the yaw the action SM's counter leaves before arm 0's snap lands, or a body matrix built ahead of the snap) needs a film of the cut itself.
The engine's film of the cut, against the PCSX-Redux recording of garmel_zeto_pre_big_wave, scored pair by pair: the camera is retail's to the unit through the cut (yaw 0, pitch 32, TR z 2380 on the pass the recording reads it, then 2492, 2604, a step-4 pass each), and both sides draw the close-up first on that pass, which retail shows two passes later. The cut pair still reads .546, and its whole residue is Zeto's pose: the film's Zeto idle cursor stands at 114 where the recording's reads 78 (rate 8 halving to 4 on the cut on both sides), three passes ahead. One of the three is the opening (the engine runs 0x0A..0x28 in three passes where retail runs two), the rest the film's seat of the state's cursor. The film cannot weigh 48 against 52 until Zeto's idle stands on the recording's cursor at the cut. Grade capture.
Two of the three were the engine's. A --physical film from garmel_zeto_pre_big_wave seats Zeto on the state's own cursor (212, its loop budget +0x176 spent as retail's), and Zeto then reads 224 on the 0x0C pass and 12 on 0x28's, where retail runs 228, 236, 0. Nothing committed a clip: the action seed's per-pass pose(Idle) request found no idle marker on the player battle setup had installed and built a fresh idle player at keyframe 0. Every body's first idle request after setup did the same, so every fight's idle phases restarted on each body's first turn. FUN_801D5854 writes no anim field (its stores go to the battle context's framing words, none to the actor's +0x1D9..+0x1DC or the node's cursor +0x68), so retail's idle runs on. With the idle kept through the request, the film reads .874 (was .824), the cut pair .849, and Zeto stands at 90 on the cut pass against the recording's 78: the one pass the engine's opening runs longer (0x0A..0x28 in three passes where retail runs two). The engine now keeps it: battle setup seats each body's idle at the phase it drew (World::seat_battle_setup_idle), so the SM's requests find the loop playing; the film has not been re-shot on that seat (settled). Grade capture (the film) and disassembly (FUN_801D5854's stores, see ghidra/scripts/funcs/overlay_battle_action_801d5854.txt).
The cut has been filmed, and it draws neither an older cursor nor a camera short of the snap. From garmel_zeto_pre_big_wave (Zeto's own AI pick, PCSX-Redux), autorun_cast_sequence.lua logs the camera per vsync and autorun_drawn_pose_film.lua the anim ticks and decodes per pass. Arms 0 to 4 run in one pass: the module counter ctx[+0x279] reads 0 after one pass and 4 after the next, and that next pass halves Zeto's rate (8 to 4), lands the cut on its endpoint (yaw 3956 to 0, the table's increment 140 clamped; TR z 4714 to 2380), ticks Zeto from 72 to 78 at the halved rate and decodes 78. The cut is first on screen two passes later. The mid-cast state's own cursor ring holds the same edge: 60, 56, 52, 48, 36, 24, rate 4 from the 48 to 52 step, so its cut pass is the one that drew 52, two passes behind the RAM's 60, as the display lag says. The fit's 48 is therefore not the drawn cursor: the cut pass draws 52 on a camera already on its endpoint, and the image's preference for 48 with yaw +6 is another residual of that frame (the body's matrix, or the metric trading sway against framing). Grade capture. Next: the engine's film of the same cast scored at the cut's pair, against a recording that opens on cast begin 0x28 as the oracle pairs it (this one opens on the state's 0x0A, and the oracle pairs it at .46 throughout).
Answered questions: settled battle / arts / level-up, settled rendering / camera, settled world map; falsified readings: battle, rendering / camera.
Audio / BGM
Music, sound effects and streamed voice: the SEQ sequencer, the VAB banks, and the field-VM opcodes that drive them.
Answered questions: settled audio; falsified readings: audio / sound driver.
Long write-ups in this area
How long does a summon's seek hold its voice?
Status: partial (measured)
The drive's two seeks ahead of the head cue are modelled (the drive holds a summon's voice), and so is their order against the side-band read: the selector seeks once the stage ctx[+0x276] drops, and the module's seek arm polls the stage before its own seek (the selector seeks when the side-band drops). With those, the cast channel fits Theeder's, Nighto's and Gilium's voices 6.5 to 7.8 vsyncs after mednafen's and Orb's and Swordie's within 3.2 of it (the seek window, where it sits), inside the cast channel's seek window. What is not modelled is the band ahead of the module: the stage's drop against arm 1 is 8 vsyncs in on Theeder, about 7 on Gilium, 4 on Swordie and 0 on Orb, where the port's stand-in for the read (30 vsyncs a slot and a first-slot seek) lands it within 5 of each. The band ahead of the module is not one length either: from the state to the flash, Theeder's 0x33 runs 7 vsyncs longer in the port (the invoke clip's cursor holds 3 ticks after its commit and its first effect record fires at cursor ~128), and Swordie's 0x34 5 longer to arm 1, while Orb's matches. 0x34's hold on the caster's +0x1D9 is the port's now: the invoke clip's natural end commits idle in the pass that crosses it, and 0x34 leaves on the next (0x80047A48..0x80047B58, 0x801E4AD4..0x801E4ADC), which is 60 vsyncs on the 0x86 film as on retail; the first-slot seek is refitted to 7 on that band. What closes the rest: the invoke clip's first passes after its commit against retail's (the caster's +0x68 cursor beside ctx[+0x87C]).
On the PCSX-Redux summon films the module's own poll arm waits 4 vsyncs where the mednafen-fitted model holds it 22 (Nova, Freed) to 30 (Orb) ticks. The cast-sequence oracle absorbs the wait itself, but what the poll's pass starts (Nova's vortex, Orb's star burst) then runs that much behind, and the arm after the poll scores .32 (Nova) and .48 (Orb) on frames that otherwise match. Which drive is right is a real console's seek time; the frames are not evidence either way. The disassembly settles the order and not the length: the module's seek is FUN_8003EAE4 (FUN_8005C160(2, ...), the CdlSetloc, then FUN_8005C034(0x15, ...), CdlSeekL, 0x8003EB50..0x8003EB6C) and its poll FUN_8003F2B8(1) is a bare CdSync(1) against CdlComplete (jal 0x8005BE8C, 0x8003F2FC), with no constant between them. How long the arm holds is the drive's answer, and mednafen's CalcSeekTime (the law the port's model takes) is that emulator's measured drive.
The two emulators do not disagree as two drive models. PCSX-Redux's CdlSeekL / CdlSeekP (src/core/cdrom.cc) schedule the seek's completion at 0x800 CPU cycles when the previous seek has finished and cdReadTime * 4 (four sectors at single speed, 53 ms) otherwise, whatever the distance, beside a comment list of per-game timing fixes; it models no seek at all. Mednafen's seek time grows with the distance (CalcSeekTime). On Theeder the two read: PCSX-Redux (0x82 cast from party_basic_attack_vs_gobu_gobu) arm 1 at 433, arm 2 one vsync later, the voice at 553; mednafen (theeder_summon_mid_cast, drive-timeline.py) arm 1 at +86, the side-band stage dropping 8 vsyncs into it, arm 2 at +118, the voice at +229: arm 1's hold is 1 vsync against 32, and arm 2, which waits on the band word, 119 against 110. The port keeps mednafen's law, the one model of a drive; PCSX-Redux's number is not evidence about a console. What closes it is still a real console's seek time, and the band ahead of the module above.
Why does Theeder's score not subtract out of its recording?
Status: open (measured)
theeder_summon_mid_cast reads 0.71 on the cast channel with the state's Monaural seated and its cast offset free to a frame step. What is left is the score's held notes, not its keying and not its sound:
- The score keys alike from its top:
seq-differentialon track2013reads3354of3354key-ons withkey 0. - Band by band (30 Hz to 22 kHz, octaves) the track render's level is the recording's within
1dB over the first four seconds, so no part of the spectrum is missing. - Split note by note (
audio-note-fit.py), every note keyed after the state fits at offset0, and the gain of each falls from1.0to0.55from2.1to2.9s - the duck's161 / 215through the square taper. Two notes held from before the state, on tone0x8008, are what the whole-mix correlation loses: re-phasing those two alone lifts the first four seconds from0.61to0.88. - At the state's own instant the port's voice for one of them (pitch
0x3AB) stands six ADPCM blocks past the state's - one vsync at its pitch,91vsyncs after its key-on - while two others on the same tone keyed151and69vsyncs back stand where retail's do.
So retail keyed one note of that score a vsync later than a run from the top does. Standing the held notes on the state's SPU instead (decoder, pitch counter, envelope, one vsync short of the match for the port's KON lead) moved the cast row to 0.79 but cost the clean track rows a little, and it was not kept. Two readings are ruled out. The silent history's phase: seated on the state's index, the keying differential reads no key or voice row on 2013 and the cast row does not move. A parked KON: the KON path never defers (a key-on is never deferred). A skipped calc would put retail's voice ahead, not behind, and the calc runs once a vsync wherever it has been watched. What is left is a calc doubled in one vsync, or a voice whose playback stood still for one (a re-key or a pitch write on the same voice). A summon cast does not disturb the calc on PCSX-Redux: a Theeder cast (0x82 from party_basic_attack_vs_gobu_gobu, cast-sequence.py --calc-clock) enters FUN_80062F98 exactly once a vsync across 1451 vsyncs, the module's seek arms and the side-band's loads included, and every KON write comes from the flush (0x80065F7C). That fight plays the battle theme, not 2013, and mednafen's calc has not been watched over a cast. What closes it: a state from the battle that leads into this cast, run under the same clock with a watch on that voice's pitch and KON bits; the library holds no state of that fight before the cast.
Title / boot / overlays
Boot, the title screen, and which overlay image sits in which RAM window at which moment.
No open threads. Answered questions: settled title / boot / overlays; falsified readings: title / boot / overlays, no overlay function lives below 0x801CE818.
Containers / data blobs
What a PROT entry's bytes are: the packer's layout, inherited tails, and which image a table belongs to.
No open threads. Answered questions: settled measurement + corpus, settled animation; falsified readings: containers / placeholder slots.
Measurement + tooling
The project's own instruments - save states, reference sweeps, census tools - whose answers decide which other rows can be trusted.
One live hunt, and it is about the instruments rather than the game: a capture-graded claim is only as good as the save state it was measured on.
| Thread | Status | What would close it |
|---|---|---|
| Which save-state library entries still hold a patched executable? | open (narrowed) - audited and tagged; the rest need human play | A state made on a patched disc keeps that build's |
Answered questions: settled measurement + corpus; falsified readings: measurement readings.
When to add a row
A thread belongs here when:
- There is something specific that would close it - a probe to run, a dump to read, a function to port. "Generally understand X better" is not closable; skip.
- The next step is non-obvious from the code or git log. If
grepwould surface it, no row needed. - The detail lives elsewhere (a docs page, a Ghidra dump). The row is the pointer, not the analysis.
When a thread closes, move its answer to settled threads with an evidence grade, move any reading it disproved to do not re-walk with its reasoning, and delete the row here. Do not leave a "closed here" note behind: this page lists live hunts only.