re: REGN is a tetrahedral navigation mesh — the cell→geometry link, decoded

Found the reader. It is the deserialiser, not a consumer, and it answers the
question twice: the fixup base is chunk+0x10 (82465198 addi r3,r31,16), so
every offset previously recorded was read 16 bytes early — which is why twenty
correlation tests sat at chance — and the POF0 table names every pointer word
in the file.

Six sections, not four. cell {count,item*} → item {n@+0x10, refs*@+0x14} →
array of pointers into section 1 → a 96-byte tetrahedron. Section 2 is a face:
plane, its three vertices, the two tetrahedra either side (0xFFFF = hull) and
their face slots.

All 11 objects: face passes through exactly 3 of its tet's 4 vertices in
253 722/253 722 (random control 0.07–2.2 %); portal cost == face-centroid
distance in 380 460/380 460; sphere reaches its cell 98.7–100 % vs 18–28 %
with transposed axes.

Refuted and kept: 'REGN'/'MCOL' are never built as constants in the executable
(0x474E occurs zero times in 1.87 M instructions), so no magic-dispatch site
exists; and "zero portal-pair float marks a hull edge" shows no lift at all.
Still open: the runtime consumer of the grid, the second portal float, and the
four flag bytes at tetrahedron +0x54.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-26 07:53:13 +00:00
parent 0d51cdb403
commit b9643c63f6
3 changed files with 316 additions and 6 deletions

View File

@@ -1,8 +1,16 @@
# `REGN` — a per-map spatial grid (and `MCOL` beside it)
# `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it)
**Status: ✅ `CONFIRMED` for the header**, which self-checks on all 11 objects on
the disc. ❔ the four data sections are undecoded. **New to this corpus** — no
document mentioned `REGN`, `MCOL` or `hidden/MiscBin.pak` before 2026-08-24.
**Status: ✅ `CONFIRMED`.** A `REGN` object is a **tetrahedral navigation mesh
of the whole play volume** — vertices, tetrahedra, faces with full adjacency,
per-tetrahedron portal costs — plus a uniform grid for point location. The
decode is complete except for two small fields; see
[the 2026-08-26 section](#-2026-08-26--the-reader-found-regn-is-a-tetrahedral-navigation-mesh),
which supersedes the offsets in the older sections below (they were read
**16 bytes early** — the loader's fixup base is `chunk + 0x10`). The older
sections are kept because their refutations are still instructive.
**New to this corpus** — no document mentioned `REGN`, `MCOL` or
`hidden/MiscBin.pak` before 2026-08-24.
## Where it is
@@ -373,4 +381,285 @@ nothing rises above chance.
object in the executable and watch which fields it dereferences. That is static
PE work (`/work/*.pe`, offset = VA 0x82000000) of the same kind that cracked
the `.slb` packing phase, and it is the honest next step rather than a
twenty-first correlation.
twenty-first correlation.
---
# ✅ 2026-08-26 — the reader found: `REGN` is a **tetrahedral navigation mesh**
The previous section closed with "find what reads a `REGN` object in the
executable and watch which fields it dereferences". That worked, and it did not
need the consumer: the **deserialiser** answers the question, because the file
carries its own pointer map and the deserialiser tells you how to read it.
## ✅ The reader — `sub_82465110` / `sub_82465138` / `sub_82465200`
Three functions, all in the resource module around `0x82460000`:
```
sub_82465110 find_pof0(chunk)
82465110 lwz r11, 4(r3) ; datasize
82465118 add r11, r11, r3
82465120 addi r3, r11, 16 ; -> chunk + 16 + datasize
82465124 lwz r11, 0(r3)
82465114 lis r10, 0x504F / 8246511c ori r10, r10, 0x4630 ; 'POF0'
82465128 cmplw cr6, r11, r10
8246512c beqlr cr6 ; else return 0
```
```
sub_82465138 relocate_chunk_chain(chunk)
82465164 lbz r11, 8(r31) / clrlwi r11,r11,31 ; already-relocated bit
82465178 bl 0x82465110 ; find the POF0 chunk
82465194 addi r4, r11, 16 ; POF0 payload
82465198 lwz r5, 4(r11) ; POF0 payload size
8246519c addi r3, r31, 16 ; ← THE FIXUP BASE = chunk + 0x10
824651a0 bl 0x82465200
824651a8 ori r11, r11, 0x1 / stb r11, 8(r31) ; set the bit
```
```
sub_82465200 apply_pof0(base=r3, table=r4, size=r5)
82465224 clrrwi r8, r10, 6 ; top two bits of the lead byte select
82465228 cmplwi cr6, r8, 0x40 ; 0x40 → 6-bit delta, 1 byte
82465230 cmplwi cr6, r8, 0x80 ; 0x80 → 14-bit delta, 2 bytes
82465238 cmplwi cr6, r8, 0xC0 ; 0xC0 → 22-bit delta, 3 bytes
82465258 add r11, r10, r11 ; running WORD index, never reset
8246525c slwi r8, r11, 2
82465260 lwzx r10, r8, r3 ; slot = base[word]
82465264 cmplwi cr6, r10, 0x0
8246526c add r10, r10, r3 ; *slot += base (skipped when *slot == 0)
82465270 stwx r10, r8, r3
```
Two things follow, and both are load-bearing:
1. **The fixup base is `chunk + 0x10`, not `chunk + 0`** (`82465198`:
`addi r3, r31, 16`). Every stored "offset" in a `REGN` file is relative to
file offset `0x10`. **Everything on this page above was read 16 bytes early.**
That single error is why twenty correlation tests returned chance.
2. **The `POF0` table is an exact list of which words are pointers.** It is not
a heuristic — it is the data the retail loader itself walks. Decoding it
gives the pointer graph directly, with no guessing.
`sub_82465138` is reached from exactly two callers, `sub_82461018` (vtable slot
9 of the class at `0x820af8bc`, the pak/resource file class) and
`sub_82461DE8`; both store `chunk + 16` as the object's data pointer, which
confirms the same `+0x10` base from the other side.
### 🔴 …and the magic is never compared
Worth recording because it is what sent the search to the fixup table:
**`'REGN'` and `'MCOL'` are not constructed anywhere in the executable.** The
title builds its four-character tags as `lis`/`ori` pairs, and a sweep of every
such pair recovers 156 tags — `RATC`, `T8aD`, `XBG7`, `IPFB`, `IDXD`, `LSTA`,
`POF0`, `PRMD`, `TBMD`, `WMV3` … — but neither of these. Nor does either half
appear as an immediate anywhere: `0x474E` (`'GN'`) occurs **zero** times in
1 865 751 instructions, and the flat PE image contains the byte string `REGN`
**zero** times. So no magic-dispatch site exists to find; `.rgn` objects are
handed to the map code by the stage record, not identified by their tag.
## ✅ The header is 0x70 bytes at `chunk + 0x10`, with **six** sections
Corrected, and re-derived from the `POF0` table, which relocates exactly six
header words (`0x70`, `0x74`, `0x78`, `0x7c`, `0x80`, `0x84`) on 11 of 11:
```
chunk +0x00 char[4] 'REGN'
+0x04 u32 data size (POF0 chunk at +0x04-value + 0x10)
+0x08 u8 flags; bit0 = "already relocated" (set by sub_82465138)
data = chunk + 0x10:
+0x00 f32[4] bbox min (w = 1.0)
+0x10 f32[4] bbox max
+0x20 f32[4] extent
+0x30 f32[4] cell size
+0x40 u32[4] grid dims
+0x50 u16[6] record counts, one per section
+0x60 ptr[6] section pointers
```
The old page had four sections because it read the last two pointers as data.
There are six, their strides are `12, 96, 48, 8, 32, 4`, and each section's
span divided by its stride is its `counts[]` entry exactly — 11 of 11, with the
only slack being 16-byte alignment padding and, for the face list, **two
all-zero sentinel records** (which is the unexplained "constant 96-byte tail"
from the section above: 2 × 48).
| # | contents | stride | count |
|---|---|---|---|
| 0 | vertices | 12 | `counts[0]` |
| 1 | **tetrahedra** | 96 | `counts[1]` |
| 2 | **faces** (plane + adjacency) | 48 | `counts[2]` |
| 3 | cell index, one per cell | 8 | `counts[3]` = cells |
| 4 | cell items, one per **occupied** cell | 32 | `counts[4]` |
| 5 | tetrahedron references | 4 | `counts[5]` |
## ✅ How a cell reaches its geometry — the question, answered
The `POF0` table places every pointer in the file, and there are only four
kinds. Verified on all 11 objects by `tools/re-capture/regn_decode.py verify`:
* the six header words, and nothing else in the header;
* **one pointer per occupied cell**, at section-3 record offset **`+4`**
— so a cell is `{ u32 count; item* }` and the *count* word is not a pointer;
* **exactly one pointer per section-4 record, at offset `+0x14`** — the 32-byte
cell item is `{ …, u32 n @+0x10, tetref* @+0x14, … }`;
* **every word of section 5**, all `counts[5]` of them.
So the chain is:
```
position ─▶ cell (x,y,z) index = (z·dimY + y)·dimX + x
─▶ sec3[index] {count, item*}
─▶ item {…, n @+0x10, refs* @+0x14, …}
─▶ refs[0 .. n) each a pointer into section 1
─▶ tetrahedron
```
Four independent checks, all 11 of 11 objects:
* **section-5 targets land on section-1 record boundaries** — every one of
**261 000** pointers is `sec1 + 96·k` with zero remainder;
* **the reference arrays are packed contiguously in cell order** —
`item[i].refs == sec5 + 4·Σ item[j<i].n`, exact, and the total equals
`counts[5]`;
* `counts[4]` equals the number of relocated section-3 pointers equals the
number of occupied cells;
* **the cell ordering is x-fastest, then y, then z.** A tetrahedron's bounding
sphere reaches the box of a cell that lists it in **98.7 100 %** of all
261 000 references; transposing the axes — the natural control — drops that
to **18 28 %**.
The list is a strict subset of "every cell the bounding sphere touches"
(Jaccard ≈ 0.4 1.0), i.e. the build used a tighter test than sphere-vs-box.
That is expected and is not claimed as decoded.
## ✅ Section 1 is a **tetrahedron** — 96 bytes
```
+0x00 f32[3] bounding-sphere centre
+0x0c f32 bounding-sphere radius
+0x10 u16[4] vertex indices → section 0
+0x18 u16[4] face indices → section 2
+0x20 6 × { f32 cost; f32 ❔ } one pair per face pair, in the order
(0,1) (0,2) (0,3) (1,2) (1,3) (2,3)
+0x50 u32 the record's own index
+0x54 u8[4] flags, values 0/1
```
**The check that makes this a decode, not a reading**: each of a tetrahedron's
four faces must pass through exactly three of its four vertices. Over
**253 722** (tetrahedron, face) pairs on the disc, the count of vertices lying
on the plane is **3 in 100.00 % of cases**. The control — the same test with a
randomly chosen face of the same object — gives 3 in **0.07 2.2 %**, and
touches no vertex at all 96 % of the time.
Two more, both 11 of 11: the bounding sphere encloses all four vertices in
100 % of the 63 410 records (with ~0.1 % padding), and the word at `+0x50` is
the record's own index in 100 %.
### ✅ The six floats at `+0x20` are the portal-graph edge costs
For face `i` of a tetrahedron, let `omit(i)` be the one vertex not on it. Then
cost[k] == | V[omit(i)] V[omit(j)] | / 3 for the k-th pair (i<j)
which is exactly the **distance between the centroids of faces `i` and `j`**
the A\* step cost of crossing the tetrahedron from one portal to another. It
holds to `1e-4` relative in **380 460 of 380 460** values, all 11 objects.
❔ The second float of each pair is unread. It is a genuine varied float, zero
in 8 26 % of slots. **🔴 Refuted**: "zero marks a hull edge" — `P(zero)` is
the same for edges on the hull as for interior ones (e.g. 2.9 % / 16.4 %
against 16.7 % / 64.0 % — no lift at all). A clearance width is the obvious
reading and is *not* established.
## ✅ Section 2 is a **face** — a plane plus the adjacency graph
```
+0x00 f32[3] unit normal
+0x0c f32 d
+0x10 f32[3] a point on the plane
+0x1c f32 1.0
+0x20 u16[3] the face's three vertex indices → section 0
+0x26 u16 the face's own index
+0x28 u16[2] the tetrahedra on either side; 0xFFFF = hull (no neighbour)
+0x2c u16[2] which of that tetrahedron's four face slots this is
```
The old page's plane reading was the same 16 bytes early, which is why it
reported "four zeros" at the front: those were the *previous* record's four
integer words, read as floats.
Verified over every face of every object (11 of 11, 100.00 % each):
* the word at `+0x26` is the record's own index;
* for each non-`0xFFFF` side, `tet[side].faces[slot[side]] == this face`;
* the face's three vertices are a subset of that tetrahedron's four.
And the Euler relation closes: `4·T == 2·(F B) + B` exactly on every object
(e.g. `4 × 1172 = 4688`, `F = 2584`, `B = 480` hull halves).
## ✅ What a `REGN` object is
A stage's `MapPath` is a **tetrahedral navigation mesh of the whole play
volume**, plus a uniform grid for point location:
* vertices, tetrahedra, and faces form a conforming tet mesh with full
face adjacency and hull markers;
* each tetrahedron carries the six face-to-face traversal costs, so A\* over
the portal graph needs no geometry at query time;
* the uniform grid answers "which tetrahedra could contain this point" in O(1),
which is what a flight game needs to localise a ship into the mesh each frame.
The 100 km-cube smoke-test map (`e993b93e`, `counts = [8, 6, 18, …]`) is the
textbook **six-tetrahedron decomposition of a cube** — 8 corners, 6 tets, 18
distinct faces (24 faces less the 6 shared internally) — which is as strong a
confirmation of the reading as the statistics are.
## 🔴 Corrections to the sections above, kept rather than deleted
Every one of these was a real observation; each fails for the same single
reason, and that is worth having on the page.
| earlier claim | status |
|---|---|
| "four section offsets at `0x70`" | 🔴 there are **six**, `0x70``0x84` |
| "the first section offset is always `0x80`" | 🔴 the *value* is `0x80`; the section is at `0x90` — the base is `+0x10` |
| "`counts[4]` = occupied cells **+ 2**, unexplained" | 🔴 artefact of the base error; `counts[4]` = occupied cells exactly |
| "the constant 96-byte tail after the plane list" | ✅ explained: two all-zero sentinel face records |
| "record `+0x1c` is a bounding-sphere radius (ratio 1.001 on one object, 0.130.27 on the rest)" | 🟡 **half right**`+0x0c` *is* a bounding-sphere radius, of the *tetrahedron*; the 1.001 was a coincidence of that map, where every tet shares the cube's circumsphere |
| "`(count, offset)` pairs point at leaf arrays of `count × 4` bytes" | 🟡 **right mechanism, wrong record** — it is the cell *item* at `+0x10`/`+0x14` pointing into section 5, not the cell index |
| "slots 811 are integers (denormal as float)" | ✅ correct, and they are the four vertex + four face `u16` indices |
| "slot 7 is a local scale, ~14× smaller than the inter-node distance" | ✅ correct as measured — it is a *tetrahedron's* bounding radius, which is exactly that small |
| "the BVH reading is refuted, no node sphere contains another" | ✅ stands, and is now explained: these are sibling tetrahedra, not a tree |
| "the cell payload holds section-1 indices" | ✅ stands — it holds a **pointer to an array of pointers**, and no index anywhere |
| "the static-correlation avenue is exhausted" | ✅ stands for correlations; the file's own fixup table was never a correlation |
## ❔ Still open
* the second float of each portal pair at tetrahedron `+0x24`, `+0x2c`, … ;
* the four flag bytes at tetrahedron `+0x54` (values 0/1);
* the exact predicate the tools used to decide cell membership (tighter than
sphere-vs-box);
* **the runtime consumer.** The deserialiser is found and proven; the code that
*queries* the grid is not. Searches that came up empty, so they are not
repeated: functions loading the header field groups off one base register
(only stack frames match); `vctsxs`/`vctuxs` float→int conversion (16
functions, only one outside the XDK ranges); and the magic-comparison route,
which cannot exist (above). The likely reason the field-offset search fails
is that a `float4`-aligned header is read with VMX loads, which carry no
useful displacement signature.
## Tooling
`tools/re-capture/regn_decode.py` — standalone, stdlib only.
```
regn_decode.py list hidden/MiscBin.pak
regn_decode.py dump hidden/MiscBin.pak [name_hash]
regn_decode.py verify hidden/MiscBin.pak # every check quoted above
```
`verify` prints, per object: the `POF0` pointer-slot shape, the reference-array
packing, the face/vertex incidence with its random control, the sphere/cell
agreement with its transposed-axis control, and the portal-cost identity.