Commit Graph

492 Commits

Author SHA1 Message Date
Sylpheed RE agent
cdb9e5a001 re: decode the stage table set — phases, routes, sub-objectives, AI parameters
Following the real stage record (not the _Test template dumped earlier) reaches
the whole mission-parameter layer, all of it in the same self-describing IDXD
container as the squadron roster.

The big one: a stage is divided into Phase_N blocks -- three for Stage 02, each
with its own map path, map mesh, asteroid definition and background -- and
Route_S<NN>.tbl holds the arrival paths, with records named

    Route_<squadron>_p<phase><kind>

tying a UnitGroup squadron id to a phase and to a time-stamped keyframed path of
(time, quat x4, pos x3). Route_ADN101_p1F is 3 frames at t = 0, 20, 30. The
identity len(fields) == FrameCount * 8 + 1 holds for 1449 of 1449 route records
across the 28 stages that have one, and 16/16 for FormationSet_S02.

Also decoded: SUBObjectiveSettings (per-objective bonus points by difficulty,
unlock item id, HUD strings) and AIParams (34 profiles, firing/guard/muster/
counter ranges plus 14 manoeuvre weights for Squad-type AI). The AIParams
numbers are exact original values from static RE and are portable as they are.

Adds tools/re-capture/stagetbl.py, which resolves a stage record by content and
can --follow every table it names, and commits two dumps as evidence.

Refuted and kept: the eight-value keyframe is the common case, not universal.
Formation_Fleet_01 has FrameCount=1 with 136 positional fields and
Formation_Fleet_02 has FrameCount=8 with 32, so a parser must not assume the
stride.

Corrects stage-definition-table.md, which was written from the _Test template
and is missing EnumerateSubobjective, EnumerateAIParams, BackGroundID and the
WingmanIconID fields the real record carries.

Not settled: what advances a phase -- the stage declares Phase_1..3 and routes
are phase-tagged, but nothing static says what ends one. That is a question for
the oracle, not for more static reading. Also open: the route-name kind letters
F/S/A/M/B, what activates a sub-objective, and StageMessageSet_S<NN>.tbl, which
does not resolve in GP_MAIN_GAME_E.pak.
2026-08-24 11:15:34 +00:00
Sylpheed RE agent
b39aabf5c3 re: decode the UnitGroup squadron roster, and correct yesterday's container reading
stage\UnitGroup_S<NN>.tbl is now fully readable. A squadron record is Count
member tuples -- (unit model, message set, n, identity/nameplate) -- followed by
five named fields: Count, SideID, AIID, FormationID, DisableInterval. The
property entries carry their own field names inline, so the tag hash never has
to be inverted.

Two independent self-checks validate it corpus-wide, 1160/1160 each across all
28 stage tables on the disc:

  - the length identity len(fields) == Count * 4 + 5, which is what pins the
    member-tuple width at 4 and the named-field count at 5;
  - agreement with the file's own Enumerate_Squadrons roster, which maps record
    key to squadron id independently of the per-record string offset.

Adds tools/re-capture/unitgroup.py (pure static, runs no emulator) with a
--all --check self-check mode, and commits the Stage 02 dump as evidence.

Corrections to the container layout written yesterday, all three wrong:

  - the 20-byte "(tag, 0, 0, count, size) section header" does not exist. It
    was the file's last 16-byte record followed by a plain npool word. The
    corrected layout is uniform across all 28 files; the old one failed on 9.
  - squadron ids do not use a separate string base. Every offset in the file is
    relative to the one string pool. The earlier "109 of 111" score was an
    artefact of the uniform 7-byte id stride and had silently shifted every
    name by three entries, which is why 17 TC*-named squadrons came out as
    SideID=ADAN. The roster record refuted it outright.
  - the roster is not always the last record; 9 stages put it elsewhere, so it
    is found by its missing Count.

Refuted and kept: the 4-byte record key is not the squadron id's name hash
(0 of 112).

Not settled: what the key encodes, the member tuple's third field n, and where
the arrival interval values live. DisableInterval is only a per-squadron flag
(Yes for 31 of 1160); the durations, triggers and arrival positions are not in
this file. Formation_*.tbl and EnumSquadron_Test.tbl are next.
2026-08-24 11:07:33 +00:00
Sylpheed RE agent
7482142abf re: find the mission wave data — stage records and the UnitGroup squadron roster
Sweeping the 811 unnamed IDXD objects in GP_MAIN_GAME_E.pak by schema turned up
schema 3c9ae32e: the per-stage definition record. 23 of them, one per stage,
each naming its background, resource package, collision set, message set,
nameplates, MapMesh/MapPath and EnumerateSquadron = UnitGroup_S<NN>.tbl.

That resolves two open threads at once:

  - MapPath = test.rgn hashes to 0x3506e972, a REGN object in MiscBin.pak, and
    MapMesh = test.col to 0x2cf7eb47, an MCOL object. REGN is a stage's map
    path data; MCOL is its collision mesh.

  - stage\UnitGroup_S02.tbl (0x019fd129, in all six language paks) is the
    Stage 02 squadron roster: 112 records, 112 squadron IDs, and a field
    vocabulary of FormationID / AIID / SideID / Count / DisableInterval, plus
    the unit model (UN_e010_ADAN_Attacker_S and friends, which match the XBG7
    mesh names we already decode), the MessageSet and the pilot character.

DisableInterval is the first direct evidence of the arrival-timing knob, which
is what the user's reframing predicted: the mission has a schedule with
parameters, not a fixed roster.

Container layout is only partly read. The 112x16 entry array was confirmed by
its boundary — keys increase for exactly 112 entries and break at 0x708, where
the next section header sits — not assumed. pak dump mislabels this file's
first key as its schema.

Refuted and recorded: the 16-byte record key is not the squadron ID's name
hash. name_hash("TCN001") = 0xd639f1a4 but the keys start 0x659aff47; 0 of 112
match.

Still open: the per-record payload fields, the meaning of the key, where the
interval values actually live, and the missing S17-S23 stage records.
2026-08-24 10:52:35 +00:00
Sylpheed RE agent
bd9e12c091 docs: update the REGN line - POF0 fixups in, two payload guesses out
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:40:01 +00:00
Sylpheed RE agent
2ab0f1647d formats: REGN carries a POF0 fixup table, and two payload readings are refuted
Every one of the 11 objects contains POF0 near the tail, at exactly
header[0x04] + 16 - an 11 of 11 relation. POF0 is a pointer-offset fixup table,
so the file is a serialised C++ object graph the loader patches on load, which
also explains why the offsets inside the cell index are absolute FILE offsets.
header[0x04] is therefore the size of the data area.

Two readings of the cell payload are recorded as refuted rather than dropped,
because both were tempting and both came from the smallest object alone: the f32
at record +0x1c is NOT a bounding-sphere radius (ratio to sqrt(3)*half-extent is
1.001 on that one object and 0.13-0.27 on the other ten), and a record's
(count, offset) pairs do NOT point at leaf arrays of count*4 bytes (0 of 11
objects clean). What survives is descriptive only: the payload is dominated by
float data, and the printable runs a string scan finds are float high-bytes
rather than text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:39:47 +00:00
Sylpheed RE agent
051e165cc1 docs: rule DefTables and the XPR2 manifests out of the wave-scheduler search
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:32:02 +00:00
Sylpheed RE agent
0d3305f075 formats: REGN section 3 is the cell index, self-checked on all 11 objects
The fourth section is one 8-byte (count, offset) record per grid cell, followed
by the 32-byte records it points at. The check: the lowest offset any cell refers
to equals align16(offsets[3] + cells*8) on 11 of 11 objects - and the alignment
term is visible rather than assumed because the three 5x5x5 maps have 125*8 =
1000 bytes of index, which is not 16-aligned, so their payload starts 8 bytes
later than the six 10x10x10 maps'.

Two further invariants from the same sweep: every occupied cell has count exactly
1 (total items == occupied cells on all 11, so it is one record per cell rather
than a bucket list), and counts[4] equals occupied cells + 2 exactly on all 11 -
the +2 unexplained and recorded as such.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:31:42 +00:00
Sylpheed RE agent
3e9e765ce0 docs: open the wave-scheduler item - the counter is a schedule, not a roster
Raised by the user: a mission has no fixed enemy count, so what to look for is an
algorithm with parameters rather than a list of objectives - which also explains
why every per-entity flag search came back empty. Records the places searched so
far (MiscBin's REGN/MCOL, DefTables' three unnamed schemas), the false lead
(STAGES = Static.slb is a sound bank), and what is still unsearched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:23:18 +00:00
Sylpheed RE agent
3c1d1d99f9 formats: decode the REGN header - a per-map spatial grid, new to the corpus
hidden/MiscBin.pak was undocumented: 40 entries, none name-resolved, holding 11
REGN objects and 11 MCOL objects (plus 18 others). Eleven of each pairs them one
per map.

The REGN header decodes to a bbox, an extent, a cell size, grid dimensions, six
counts and four section offsets - and it self-checks twice over all 11 objects:
extent == cell x dims holds exactly 11 of 11, and counts[3] equals the cell count
(1000 on every 10x10x10 map, 125 on every 5x5x5). Two independent fields
reproducing the same grid is what makes it a decode rather than a guess.

Three map sizes exist: half-extent 250000 with 50 km cells, 50000 with 10 km
cells, and 25000 with 10 km cells on a 5x5x5 grid.

Written without overclaiming against the question that prompted it: a mission's
enemy count rises and falls, so a scheduler with parameters exists somewhere, and
a per-map cell grid is the kind of structure such a thing is indexed by - but the
four data sections are unread and nothing here shows spawn parameters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:22:54 +00:00
Sylpheed RE agent
6a8c366a11 formats: correct the glyph class - a SECONDARY vtable of adjustor thunks
The first write-up called 0x820B2A64's 32 entries "methods" and the three xrefs
to it "construction sites". The DB's disassembly refutes both: every slot points
at a 16-byte adjustor thunk - lwz r11,-4(r3); subf r3,r11,r3; b <method> - which
is the PPC/MSVC thunk for a SECONDARY base subobject. So this is a secondary
vtable and the class uses multiple inheritance, and sub_823C3148, listed as a
constructor because it references the table, is actually the branch target of one
of the table's own thunks.

Following the 32 branch targets instead: 32 distinct addresses, 18 of them known
function starts, in two clusters (0x823c3xxx beside the thunks and 0x823e3xxx),
and NONE of them appears in any of the 1150 vtables the DB classified - so
nothing places this class in a named hierarchy, the disc's RTTI having no class
names. The two-cluster split is noted as a reading rather than a measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:16:12 +00:00
Sylpheed RE agent
108eedf124 docs: record the HUD glyph quad decode in the backlog
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:07:57 +00:00
Sylpheed RE agent
00a06abc77 formats: decode the HUD glyph quad behind the objective counter
Following the pointers that move with REMAINING OB lands on four identical
objects whose first word is 0x820B2A64. Read live: that address holds 32
consecutive code pointers in 0x823c43b0..0x823c45a0 terminated by 0xfffffffc - a
vtable - and sylpheed.db has three ref xrefs to it from sub_823C3060,
sub_823C3148 and sub_823C31E0, i.e. three construction sites, the same pattern
the splash item vtable shows. No name: the disc's RTTI carries none.

The instance is a textured quad: a pixel size at +0x1c/+0x20 (34 x 42 for a
digit) and four vertices at +0x30/+0x48/+0x60/+0x78, each a colour and a UV pair.
The four UVs measured are the corners of an axis-aligned rectangle in order, all
0xffffffff.

The atlas size is a check rather than a guess: the UV rectangle is 0.0265 x
0.0547, and multiplying by 1280 x 768 gives 33.9 x 42.0 against the 34.0 x 42.0
stored in the object's own size fields - two independent fields agreeing to a
rounding step.

Also recorded: these objects churn. Re-reading an old pointer target a minute
later returned unrelated data, so this is a snapshot of a live pool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 10:07:43 +00:00
Sylpheed RE agent
2fe76bd523 docs: close the EMULATOR GONE item - it was the Stop hook, and note the one-turn rule
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:49:20 +00:00
Sylpheed RE agent
ac17398515 formats+tools: the counter's neighbours are its own rendered digits
With the per-entity searches refuted at word and bit level, the question became
which object owns the counter. Sampling +-0x200 around it across a 4->8
transition: the control interval moved 0 of 256 words, and the step moved nine -
the counter plus four words holding ASCII '4' -> '8' NUL-padded, and four
pointers into 0xbcad2xxx that swap with them. Read live at HUD 008, all four
character slots hold '8'.

So the neighbourhood is the HUD's rendered text for this counter, which reframes
the address: it is the HUD widget's value rather than "the mission's own
objective counter" as this file called it.

Recorded against that, because it is already measured: there is no separate
mission-side copy moving on the same step. ob_hunt scans all of guest memory and
requires a match across two transitions, and it left exactly one address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:48:57 +00:00
Sylpheed RE agent
01468b804a docs: the mystery SIGKILL was this project's own Stop hook
/work/.claude/settings.json defines a Stop hook that kill -9s every xenia_canary
at the end of each agent turn and prints "Stop hook killed N stale xenia
process(es)". So every run that "died mysteriously" died at a turn boundary,
which is why the timings looked random from inside the run (810 s, 54 s, 486 s)
and why nothing in the guest, the cgroup or the host explained them.

What survives is the measurement and not the story: the memory readings were real
and did refute memory pressure - cgroup at 6.5 of 7.0 GB, host 12 GB free at the
moment of a kill - but they were refuting a cause that was never in play.

The method lesson is recorded because it cost three iterations: when a process
dies at a SESSION boundary, check the harness before instrumenting the guest. A
failure mode was documented, a hypothesis raised, a per-poll memory sampler
written and committed, and host and cgroup counters read - all downstream of the
assumption that the kill came from outside the agent. The hook's own message had
been printed after every turn.

The operational rule that follows: an emulator experiment must COMPLETE INSIDE A
SINGLE TURN. Nothing survives the boundary, so runs cannot be left for the next
tick, and experiments whose evidence arrives in the first minutes are the ones
that work - which is exactly why the two-pass bit-level test succeeded where the
long freeze-watches did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:48:23 +00:00
Sylpheed RE agent
bd69659a7d docs: close the bit-level flag question in the backlog
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:32:28 +00:00
Sylpheed RE agent
5a2faf09e5 docs: the bit-level OB flag is refuted - two transitions, no common survivor
The short-run strategy paid off: both counter steps happen in the first minutes
of flight, so one run yielded two complete passes before anything froze or was
killed. Pass 1 scanned at counter 4 and filtered on 4->8, leaving 29 of 710
candidates; pass 2 scanned at 8 and filtered on 8->12, leaving 2 of 197. The
intersection of the two survivor sets is EMPTY.

So no per-entity bit, in either polarity, anywhere in -0x400..+0xC00 around the
position triple, has a population that tracks REMAINING OB. With the word-level
refutation that closes the "objectives are marked in the entity object" family as
far as this method reaches.

Also recorded: 16 of pass 1's 29 survivors are the SAME word, pos+0x0250, with
sixteen different bits - the signature of a shared value rather than a flag, and
the same offset the word-level sweep found shared by 12 attackers. And a caveat
kept rather than smoothed: pass 2's B-sample HUD read 8 while RAM read 12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:31:46 +00:00
Sylpheed RE agent
c90383dd07 docs: a third SIGKILL, traced live, refutes the memory explanation
Run 9 died the same way at t=486s, and the watcher's sampler had been running
throughout: at the kill the cgroup held 6.5 GB of its 7.0 GB limit, the host had
12 GB free, and oom_kill was 0. So the memory-pressure suspicion recorded
earlier is refuted - three kills, no OOM record on any, and the one with a full
trace shows headroom on both the cgroup and the host.

Also checked and clean: no stray process of mine was running, and the kills do
not fall on a clock (08:31, 09:00, 09:14 - gaps of ~29 and ~14 minutes).

Recorded as a container-level blocker rather than chased further, because nothing
in here can attribute a SIGKILL to its sender: no dmesg, no audit, and
ptrace_scope=1 limits strace to my own descendants. The practical response is to
stop designing experiments that need a long run - the counter transitions the OB
work needs all happen in the first minutes of flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:19:46 +00:00
Sylpheed RE agent
1b2bd3b22b docs+tools: the silent death is a SIGKILL from outside, and it is still not OOM
Run 8 died 54 s into its boot and this time bash named it: "176880 Killed" on the
run-canary line, which is SIGKILL. So the third failure mode is not an internal
fault - something outside the process is killing it.

And it is still not the OOM killer. Checked immediately after: oom_kill remained
0 and the allocation-stall counter did not move from 4421, so during run 8 the
cgroup never reached its limit (5.35 GB of 7 GiB), and the host had 13.8 GB
available. Two kills, no OOM evidence either time.

Rather than keep guessing after the fact, freeze_watch.sh now samples host
MemAvailable, cgroup memory.current and oom_kill on every poll and dumps the last
five samples when the process disappears - so the next occurrence carries its own
contemporaneous reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:07:11 +00:00
Sylpheed RE agent
bb81be7a02 docs: the v2 baseline holds on a second healthy run
Run 7 ended NO FREEZE within 1100s with the same shape as run 5: the same three
threads, the same ceiling of 13 distinct objects, and not one non-SUCCESS window
in 544 across both runs. The v1 stuck-wait side reported its usual single pair.

Run tally since v2 is three runs and no frozen sample - GAME OVER at ~22 min,
EMULATOR GONE at 810 s, and no freeze in 1100 s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:00:18 +00:00
Sylpheed RE agent
58086feea2 docs: a third failure mode - the emulator dies silently, and OOM does not explain it
Run 6 ended neither frozen nor in GAME OVER: freeze_watch reported EMULATOR GONE
at 810s and the 1.1 MB Canary log simply stops mid-activity, with ZERO
occurrences of CRASH, Access Violation, GUEST-THROW, assert, Fatal or abort, and
no shutdown line.

Memory pressure is recorded as a suspect and explicitly not as a conclusion. The
cgroup had reached its 7 GiB ceiling (memory.peak == memory.max) with 4421
allocation stalls at the limit, and page cache - inflated by the disc-wide format
sweeps that read every .pak - was 4.68 GB of it. But memory.events:oom_kill is 0,
so the cgroup OOM killer did not do this, and nothing identifies what did.

Two operational notes fall out: /dev/shm/xenia_memory_* survives a dead run (342
MB resident) and run-canary only clears it at launch, and vm.drop_caches is not
writable here, so page cache can only be left to the kernel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 08:35:23 +00:00
Sylpheed RE agent
5699b25fc4 docs: record the v2 probe's baseline in the freeze item, and what it rules out
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 08:32:06 +00:00
Sylpheed RE agent
a89cb27644 docs: drop a placeholder link that should never have been written
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 08:14:27 +00:00
Sylpheed RE agent
5fb4da4c43 docs+tools: the call-rate probe's baseline already refutes the timeout reading
v2 counts every KeWaitForSingleObject call per thread per second, tracks how many
distinct objects it saw, and logs the last result. Its HEALTHY baseline is a
result on its own: over a full 22-minute run the main thread cleared 500 calls/s
in 224 separate windows, peaking at 1235 calls/s over up to THIRTEEN distinct
objects, and the last result was X_STATUS_SUCCESS in all 314 windows. Not one
timeout.

So the game normally does hundreds of successful waits a second across many
objects - exactly the blind spot v1 could not see, and the reason a timeout-streak
counter reported the same single poller whether the game was frozen or healthy.

Stated as a consequence rather than a triumph: 500/s is NOT self-selecting, since
the main thread clears it constantly, so the freeze signal has to be a different
shape - a thread far above 1235/s, a new thread, or a window whose result is not
SUCCESS. That still needs a frozen sample; run 5 ended in GAME OVER at ~22 min
without freezing.

freeze_watch.sh now summarises the rate probe per thread when it captures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 08:14:11 +00:00
Sylpheed RE agent
51c8e49f7b docs: the freeze item now has a captured freeze and a refuted hypothesis
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 07:40:32 +00:00
Sylpheed RE agent
3ecb3432f1 docs: the freeze was caught and the stuck-wait probe reports nothing new
The fourth run froze 9 seconds into the watcher's window, in flight, and the
probe built for that moment showed the healthy-run baseline and nothing else: one
pair, the same poller on the same object VA as every healthy run, only the thread
handle differing. No new (thread, object) pair appeared.

So the hypothesis the probe was built to catch is refuted - the freeze is not a
guest thread looping on KeWaitForSingleObject timeouts against ONE object - while
the CPU signature is unchanged from the gdb run: 1255 ticks over 10 s, 401 in the
TimerQueue thread and 292/280 in two guest threads.

What survives is stated as two specific blind spots of the instrument rather than
a shrug: the waits may cycle over DIFFERENT objects, which resets the streak and
makes them invisible to a same-object counter; or they may SUCCEED rather than
time out, which leaves a timeout counter nothing to count and would fit the
kernel-log evidence of a self-suspending worker cycling thousands of times
successfully.

Next is a v2 that counts calls per thread per second regardless of object or
result. The freeze lottery paid out on the first attempt this time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 07:40:09 +00:00
Sylpheed RE agent
a40a0b7b01 docs: looking closer at the frame-rate word weakened it, and that is the result
The 16.16 reading of +0x04 rested on twelve bundles at 30.0. They are not twelve
witnesses: they are TWO bundles - pghud_range_main_em/emeff and
pghud_range_nose_em/emeff, both dur 30 - repeated across six language PAKs. A
30-frame flash is equally consistent with 0.5 s at 60 and 1 s at 30, so the
observation that "30.0 bundles are shorter" says almost nothing.

Worse for the clean fixed-point reading: the four bundles at 0x3C0001 would be
60.0000152 fps, which nobody authors. They are py_ranking_jump/py_ranking_next
dialogs, all dur 60. So the better reading is <rate:16>.<flag:16> - a rate-like
number in the high half and a small low field that is 0 on 2843 bundles and 1 on
four, meaning unknown.

Also stated: nothing in this container can settle it by timing, because the
emulator runs on software Vulkan far from real time - a stopwatch would measure
lavapipe rather than the game.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 07:34:56 +00:00
Sylpheed RE agent
f3612b982b docs: promote +0x08 to confirmed, keep the frame-rate reading amber
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:40:46 +00:00
Sylpheed RE agent
192a23a481 formats: +0x08 is the animation length - checked against the keyframe times
The rate/duration reading of the bundle header came from the values alone, so it
was checked against something the file states independently: the keyframe `time`
fields in the placement region.

Over the 2313 bundles that have both, the largest keyframe time is <= +0x08 in
EVERY one, none exceeds it, and 444 reach it exactly. The refutation attempt is
in the same data: a large unrelated constant would bound every time too, but the
max_time/+0x08 ratio would then pile up near zero - instead it peaks at exactly
1.0 with 520 bundles. Asserted, including the shape of that histogram, so the
bound cannot later be waved through as vacuous.

Stated precisely because the units are a separate claim: what is proven is that
+0x08 is the animation length in the SAME UNIT as a keyframe's time. That the
unit is frames still rests on the values (30/60/120/1200) and on the 16.16
reading of +0x04, which stays amber - the only new evidence for it is that the
twelve 30.0 bundles cap at +0x08 = 30 while the 2843 60.0 ones reach 1440, and
twelve bundles is not a demonstration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:40:28 +00:00
Sylpheed RE agent
3a9807f393 docs: close the screen-vs-fragment question against the header, and record what the header does say
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:26:19 +00:00
Sylpheed RE agent
27fcb69fdf formats: sweep the bundle header - no screen flag, but three of its words decode
The backlog asked what makes a bundle a screen rather than a fragment, and the
obvious suspect was the 32-byte header. Swept over all 2859 composable bundles
with a real declaration table.

The answer to the question is NO, and it is asserted rather than argued: no bit
of the flags word at +0x10 labels a screen. The best any bit manages is bit 13 -
403 bundles, 179 of them carrying a full-screen element, a 44% hit rate against a
12.8% base - and the commonest bit is set on 91% of everything. Enrichment, not a
marker.

The sweep found more than it was asked for, though. The header is not dead space:
+0x18 is 1280 on 2829 bundles and +0x1c is 720 on 2823 - the design resolution at
bundle level, the same pair the parser already reads out of a .rat record, and
asserted here. And +0x04 takes only three values, 0x3C0000 on 2843 and 0x1E0000
on 12, which are exactly 60.0 and 30.0 in 16.16 fixed point, with +0x08 taking
30/1200/120/60 - a frame rate and a duration in frames would fit a format whose
records are keyframe lists. That reading is marked amber: it comes from the
values alone and is not verified against an animation.

Also recorded, since the file will not say: element counts are min 1, median 2,
p75 5, p95 23, max 56, and only 365 bundles carry a full-screen element. The
population is mostly fragments and the separation is shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:26:06 +00:00
Sylpheed RE agent
92dd1da65a docs: the opt-link question has an answer and a stated coverage limit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:09:30 +00:00
Sylpheed RE agent
d9a2229217 formats: what opt links, measured - a record-to-record reference, focus is one use of it
Two readings were on record and both were wrong in different directions: the
structure doc called it "normal state -> focused state" from a single example,
and the backlog called it "refuted as focus; unexplained otherwise".

Classified every link reachable from a declaration table: 1467 links, and ALL
1467 resolve to a RATC child of their own bundle, ALL are .rat -> .rat, none
dangle and none self-link. 1076 (73%) match the <stem>f focus pattern; the other
391 are chains between effect records - px_bunk_eff01 -> pjex_eff -> pjex_eff07,
pveff01 -> pjeff02 -> pjeff21 - which also explains why only 227 targets are
themselves declared elements: the middle of a chain is, the end is not.

So `opt ` is a record-to-record reference within the bundle, and focus is its
commonest use rather than its meaning.

Coverage is stated rather than glossed: the bundles hold 18718 raw `opt ` tags
against the 1467 classified, because opt_link reads the first tag of a DECLARED
element's record. Roughly 92% of occurrences sit deeper in the chains (or are
byte coincidences - the scan is unaligned) and are untested. The numbers are
asserted so the answer cannot drift back into an anecdote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:09:18 +00:00
Sylpheed RE agent
f160b416ff docs: close the focused-state question against the declaration table, and log a third freeze-free run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 03:50:58 +00:00
Sylpheed RE agent
2d28226b30 formats: the declaration entry does not mark a focused state - swept and asserted
The backlog's cheapest open question about the 60-byte entry: kind is a flags
word (0x10 untextured primitive, 0x4 repeated instance, 0x3002 button record), so
a focus bit would be the obvious answer and the name-pairing in
mark_focused_states would be a convention standing in for a real field.

It is not. Over every screen build on the disc: 54 name-paired focused/base
pairs, all 54 with IDENTICAL kind (all 0x0), NO bit ever set on the focused entry
and clear on its base, and the only words of the entry that ever differ are +48
and +52 - the pivot. The two entries differ in where the sprite sits and in
nothing else.

The test asserts all four numbers rather than just printing them, so the negative
is pinned instead of decaying back into a suspicion. Also noted: these buttons
carry kind = 0x0, so the documented 0x3002 "button record" belongs to the .rat
records and not to the .t32 sprites a menu draws.

Closes the declaration table, not the question - the focused state could still be
marked in the .rat record, the RATC child stream, or only in the game's code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 03:50:40 +00:00
Sylpheed RE agent
68a3fed554 docs: note that catching a freeze is a per-run lottery, and the watcher that waits for it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 03:14:25 +00:00
Sylpheed RE agent
0ef0e77b1e docs: the stuck-wait control holds on a second run, and the freeze did not come
A second 25-minute Stage 02 run under --log_stuck_waits ended NO FREEZE within
1500s, with the probe again reporting exactly one pair - thread F800004C on
object BE56BB5C - 24 lines against the first run's 27. The healthy-run control is
therefore measured twice and independently, so anything else the probe prints
during a freeze is signal.

Also recorded: two consecutive runs did not freeze, and the earlier freezes came
in a cluster. Roughly half the runs that reached flight ended early across the
session, but not evenly, so "wait for a freeze" is a ~30-minute lottery per run
rather than a dependable step. Instrument, watcher and control are in place; one
frozen sample is what is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 03:14:14 +00:00
Sylpheed RE agent
a58c8665e0 docs: the freeze item has an instrument and a control now
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 02:31:15 +00:00
Sylpheed RE agent
61acc81b3c docs: correct the canary commit hash after dropping an unrelated submodule change
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 02:31:03 +00:00
Sylpheed RE agent
4b6b72ca7b docs: the stuck-wait probe and its healthy-run baseline
The instrument is built (canary auto/re-wait-timeout-probe 83ad0eb3f, binary
archived at /sylph-home/re/bin/waitprobe): consecutive timeouts on the same
object, per thread, logged at 100 and then every 500 - self-selecting where the
global kHighFrequency flag was unusable.

The control is measured: a 25-minute Stage 02 run that never froze produced 27
lines, ALL one thread polling one Event at guest VA BE56BB5C with a ~30 ms
timeout, whose streak reached 8000 while the game ran perfectly. So the probe is
quiet, not silent - the first draft of the cvar help said silent and that is
corrected here - and the freeze signal will be a NEW (thread, object) pair rather
than the presence of output.

Not settled: the freeze did not reproduce this run, so there is no frozen sample
to compare against yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 02:30:29 +00:00
Sylpheed RE agent
315b7e1561 docs: point the freeze item at the targeted log line rather than the global flag
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 01:49:18 +00:00
Sylpheed RE agent
80dc6352b6 docs: logging the wait target is blocked by the flag's own cost
KeWaitForSingleObject is kHighFrequency, so it is silent unless
--log_high_frequency_kernel_calls=true - which confirms the corpus's earlier
"kHighFrequency waits are simply unlogged" note. With the flag the calls do
appear (1944 lines in 40 s), but the emulator slows so far that after SEVENTEEN
minutes the screen was still black and skip_intro had not seen even the intro
movie, with 175 MB of log written. A 2400 s boot budget did not help; the boot
was not going to finish.

Recorded with the numbers so nobody pays them twice, along with the two cheaper
routes: a targeted Canary log line that fires only after a wait has timed out N
times on the same thread (self-selecting, free on a healthy run, one small patch
plus a build that is already configured), or digging the PPCContext out of a
stack frame under gdb - possible but fragile, since the Release binary has symtab
only and no DWARF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 01:49:05 +00:00
Sylpheed RE agent
cd2704c2ac docs: the freeze entry gets a measurement instead of a plan
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 01:13:13 +00:00
Sylpheed RE agent
7150827e96 tools+docs: the freeze from inside - all 79 threads are in a wait, and two of them burn CPU cycling
The gdb route works, and the recipe is written down: run-canary execs $XENIA_BIN,
so a wrapper that execs "gdb --args <real binary>" keeps the lockfile, the flags
and the process name (gdb forks and execs the real binary, so ps -C xenia_canary
still finds the inferior) while satisfying ptrace_scope=1 by being the parent.
The Release binary is not stripped - 26595 symtab entries - so frames have names.
The handle SIGSEGV/SIGBUS/SIG32-35 lines are mandatory: xenia uses SIGSEGV for
guest memory watches and the RT signals for thread suspend.

A run froze after ~4 minutes of flight, screen still "flight" rather than GAME
OVER, and all 79 threads had backtraces. EVERY ONE is in a wait - guest threads
in KeWaitForSingleObject / NtWaitForSingleObjectEx / SelfSuspend, the GPU command
processor parked idle, the main thread in poll().

It is nevertheless burning 1253 ticks per 10 s: 403 in the TimerQueue thread
(nanosleep inside TimerThreadMain) and 290 + 274 in two guest threads that the
backtrace shows blocked in KeWaitForSingleObject. A thread genuinely blocked
cannot burn 28% of a core, so those two are CYCLING - a timed wait that expires
and is re-entered - with the timer thread servicing them hot. Three samples
minutes apart show identical frames.

That refines the earlier "the guest is spinning, not deadlocked": the CPU burn is
real but it is in the WAIT PATH inside the kernel layer, not in guest code. The
shape is an event that never gets signalled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 01:13:01 +00:00
Sylpheed RE agent
b41727951e docs: fourth bit-level attempt - frozen again, but caught in a minute
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 00:41:59 +00:00
Sylpheed RE agent
a27dddf93d docs+tools: scope the guest-PC experiment properly, and let ob_bitflag find the counter itself
The freeze's named next step is more expensive than it looked, and the reasons
are recorded before anyone starts it: xenia's stack walker is a TWENTY-LINE STUB
on POSIX (Create logs "unimplemented" and returns nullptr), so
ThreadDebugInfo::guest_pc is never filled here; PPCContext carries no live PC
either. A reverse host->guest map is buildable - the code cache already learns
the mapping in OnCodePlaced - but that plus a way to sample another thread's RIP
is a real emulator feature, not a patch.

The cheap route that does exist: gdb is installed and ptrace_scope is 1, so
attaching to a running emulator is refused but launching it UNDER gdb is not -
run-canary execs $XENIA_BIN, so a wrapper that execs "gdb --args <real binary>"
keeps the lockfile and flags and makes gdb the parent. That would separate
"spinning in guest JIT code" from "spinning in a xenia loop", which is the fork
this is stuck on. Not attempted yet.

ob_bitflag now locates the counter itself from the three addresses measured so
far, instead of refusing when the default one is wrong. That costs no
transitions, and with roughly half of all runs ending early, transitions are the
scarce resource.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 00:41:47 +00:00
Sylpheed RE agent
6b3442ce8c docs: record the bit-level attempts and that run attrition is the real cost now
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 00:20:35 +00:00
Sylpheed RE agent
ca133c0220 tools+docs: the bit-level OB test, and a liveness hole - GAME OVER animates
ob_bitflag.py is the follow-on the word-level refutation named: for every 4-byte
offset in the window and every one of its 32 bits, count how many entities have
it set, keep the pairs whose count is exactly the counter, and require them to
match again after a transition. Both polarities, since an objective could be
marked by a bit that is CLEAR on it.

Three runs, no verification, and the reasons are recorded: run 1 gave 187 + 33
candidates at counter 4 and then reported "the counter never moved" for 700 s -
about a mission that had ENDED in GAME OVER partway through; run 2 hit the same
dead mission; run 3 had the counter at a different address (the guard refused,
correctly) and then froze after one filter.

The hole is closed. frozen() asks whether the guest is ANIMATING, and the GAME
OVER screen animates happily - mean colour (114,22,63) - so every liveness check
passed while the mission was over. frozen.in_flight() classifies the screen with
screen_id, and ob_hunt/ob_flag/ob_bitflag now abort with NO LONGER IN FLIGHT.
That is the second confident negative in this investigation that was really about
a dead world, so the rule is written down: before believing "X never happened",
show that the thing that would produce X was still running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 00:20:04 +00:00
Sylpheed RE agent
f6febd1bd6 docs: close the flag hypothesis in the backlog, and name the bit-level follow-on
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-23 23:21:23 +00:00
Sylpheed RE agent
26421d5266 docs+tools: the per-entity OB flag is refuted, and the counter decrements when the player kills
With the pilot finally shooting, the experiment completed both halves. Sample A
at counter 12 over 120 entities gave 2 offsets where exactly 12 entities share a
value; the counter then went 12 -> 11 and NEITHER survived. So within +-0x400 of
an entity's position triple there is no 4-byte word whose shared-value population
tracks REMAINING OB.

The limits are recorded as part of the result, because they bound it: the test
asks which entities share an EXACT 32-bit value, so a single bit ORed into a word
that also carries health or a timer would never show up - a bit-level version of
the same differential is the follow-on. Anything outside the window, or on
entities that entities2 cannot see (it types by position CHANGING, so stationary
objectives are invisible), is untested too, and the populations differed a lot
between samples - 120 against 194.

Separately: REMAINING OB went 12 -> 11, the first decrement of this whole
investigation, while pilot.py logged 411 fire=1 samples and the HUD reached YOU
KILLED WARPLANES 0003. Stated carefully - it does NOT show the counter counts
kills, since an earlier run had the hostile population fall by a third with no
movement; it shows some kills close something the counter tracks.

Two robustness fixes: ob_flag retries an empty entity sample (one void run was
caused by exactly that), and Pad releases everything on interpreter exit - a
file-backed pad PERSISTS after its writer dies, so a tool killed mid-press would
leave a button held and the game would walk through menus on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-23 23:21:08 +00:00