acdream/tools/walk-oracle/oh/README.md
Erik b62dbf035c tools(walk-oracle): capture the per-land-cell draw order (LC/SC lines) for S3 chunk 3
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 06:31:54 +02:00

291 lines
16 KiB
Markdown

# OH1 walk-oracle capture kit (2026-09-02)
Draft cdb capture scripts for Campaign OVERHAUL's OH1 evidence gap: the four
OH1 contract docs
(`docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md`,
`oh1-built-mesh-view-contract.md`, `oh1-alpha-list-contract.md`,
`oh1-depth-lifecycle.md`) establish retail's built-mesh/view/stamp, alpha
FIFO, and depth-lifecycle contracts from the named decomp plus live Ghidra
arbitration, but they explicitly say (`oh1-retail-world-contract.md` s10):
"Existing FW0 captures cover only frame/pose/DrawInside/DrawCells/
landscape/building entry. Full semantic equality requires a bounded richer
retail capture before the owning later slices close." This kit is that
richer capture: it extends the proven FW0 F/P/LS/BLD/DI/DC per-frame shape
with three new families (cell-shell/object-cell stamps, part/mesh
admission, and the alpha-FIFO/depth-counter state machine) without changing
any existing line format.
**These scripts are DRAFTS pending the recon pass below. Do not treat any
offset as final until `oh-recon.cdb`'s `dt` output confirms it against a
live attach.** See "Open questions" at the end before running a real
session.
## Files
| File | Purpose |
|---|---|
| `oh-recon.cdb` | No breakpoints. `dt`/`x`/`uf`/`dd` dumps that confirm every offset the three capture scripts assume. Run this FIRST, once per session. |
| `oh-capture-walk.cdb.template` | FW0's F/P/LS/BLD/DI/DC lines, byte-identical, plus new `EC`/`OC` lines (cell-shell and object-cell draw-stamp events) and, added 2026-09-03 for S3 chunk 3, `LC`/`SC` lines: one per `RenderDeviceD3D::DrawLandCell` (`0x0059f120`) / `DrawSortCell` (`0x0059f140`) call with the land cell's `m_DID` — retail's far-to-near landscape draw order within `LS`. 5 frames. |
| `oh-capture-parts.cdb.template` | F/P lines plus new `PD`/`DM` lines (per-part and per-mesh admission/GfxObj identity). The high-hit-rate script. 3 frames. |
| `oh-capture-alpha-depth.cdb.template` | F/P lines plus new `AM`/`FL`/`PM`/`PC` lines (the two alpha FIFO lists and the persistent portal-mask depth counter). 5 frames. |
| `oh-run-capture.ps1` | Runner: `-Script -Log -Frames`. Verifies the live acclient.exe pairs with `refs/acclient.pdb` (aborts on mismatch), substitutes `<LOG>`/`<FRAMES>`, launches `cdb -pn acclient.exe -cf <script>` in the background. |
## Prerequisites
1. `C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe` installed
(Microsoft Store WinDbg).
2. The retail client running the **2013 v11.4186 EoR build** (PDB GUID
`9e847e2f-777c-4bd9-886c-22256bb87f32`), in-world, connected to local ACE
on `127.0.0.1:9000`. The runner checks this for you and aborts on
mismatch, but confirm before starting — a mismatched attach silently
corrupts every offset in these scripts.
3. `docs/research/named-retail/acclient.h` and `acclient_2013_pseudo_c.txt`
available (already in the repo) if you need to cross-check a line
number cited in a script's comments.
## The four poses
Reused verbatim from `docs/research/2026-08-30-fw-walk-oracle/README.md`'s
trace table — these are the exact in-world spots already used for the FW0
oracle fixtures, so a human who captured those poses before can return to
the same spots from memory; there is no additional GPS/coordinate record
beyond these descriptions in this repo.
| Pose | Description | Root shape |
|---|---|---|
| `holtburg-doorway-still` | Standing in a cottage doorway, still | INTERIOR (`DI a9b4013f`) |
| `terrace-edge` | Waterfall ledge (the #456 acceptance pose), still | OUTDOOR |
| `cathedral-arrival` | Cathedral portal-in arrival, still | INTERIOR (`DI f4180106`) |
| `foundry-deep` | Bottom of the Holtburg foundry, still | INTERIOR (`DI a9b40176`) |
All four are **still** poses (no movement during capture) — this kit only
targets the still-fixture family, matching FW0's own "still fixtures gate
first" precedent (pose-application timing sensitivity in moving fixtures is
a separate, harder problem this kit does not attempt).
## Operator procedure
**Budget: one hour total** for all four poses. Each pose's three scripts
(walk, parts, alpha-depth) run back to back and take well under a minute of
actual capture time each; the time goes to repositioning, verifying detach,
and the occasional retry.
### 0. Once per session
Run recon first, before touching any pose:
```powershell
cd tools\walk-oracle\oh
.\oh-run-capture.ps1 -Script oh-recon.cdb -Log C:\path\to\oh-recon.log
```
Wait for it to finish (recon has no breakpoints, so it runs to completion
in well under a second once attached — watch for the `oh-recon.log.console`
file to stop growing, or just wait ~5 seconds). Read `oh-recon.log` and:
- confirm every `dt` offset matches what the capture scripts' comments
assume (see each template's own "OFFSETS (VERIFY-IN-RECON)" block);
- read the `dt acclient!DBObj` output and confirm `m_DID` is at `+0x28`
(the lead derived this from the header on 2026-09-02 and the parts
template is pre-filled with it; if `dt` disagrees, fix the two `0x28`
reads in `oh-capture-parts.cdb.template` before running it);
- glance at the three `uf` disassembly dumps
(`AddMeshToAlphaList`/`DrawPortalPolyInternal`/`FlushAlphaList`): the
named pseudo-C declares all three as plain static functions with no
receiver and no non-default convention (`acclient_2013_pseudo_c.txt`
424920, 424490, 425941), i.e. `__cdecl` with arguments at `[esp+4]`
upward; the templates read `uint8` arguments with `by()` (one byte) so
the untouched upper bytes of a pushed byte cannot pollute the value.
Only if the prologue shows `ecx`/`edx` being consumed as arguments does
the template need changing.
If either open question resolves DIFFERENTLY than assumed, fix the
template before capturing — do not capture first and "adjust the parser
later"; a wrong stack offset produces plausible-looking garbage, not an
obvious crash.
### 1. Per pose
For each of the four poses, in order (doorway-still, terrace-edge,
cathedral-arrival, foundry-deep — matches the FW0 capture order, no
particular reason to change it):
1. Position the retail character/camera at the pose (see table above) and
hold still.
2. Run the walk capture:
```powershell
.\oh-run-capture.ps1 -Script oh-capture-walk.cdb.template `
-Log C:\path\to\<pose>.walk.log -Frames 5
```
3. **Wait for detach** before moving or running the next script. The
runner returns immediately (it launches cdb in the background), so
watch for completion instead of assuming it's done:
```powershell
while (Get-Process cdb -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 500 }
```
or simply tail the log and watch it stop growing at `F 5`:
```powershell
Get-Content C:\path\to\<pose>.walk.log -Tail 20 -Wait
```
4. Run the parts capture (stay in the same pose):
```powershell
.\oh-run-capture.ps1 -Script oh-capture-parts.cdb.template `
-Log C:\path\to\<pose>.parts.log -Frames 3
```
Wait for detach the same way.
5. Run the alpha-depth capture (stay in the same pose):
```powershell
.\oh-run-capture.ps1 -Script oh-capture-alpha-depth.cdb.template `
-Log C:\path\to\<pose>.alphadepth.log -Frames 5
```
Wait for detach.
6. Move to the next pose and repeat.
Do not run two of these scripts against the same live process
simultaneously — cdb's `-pn` attach is exclusive, and the second launch
will simply fail to attach rather than queue.
## Expected line counts
For `oh-capture-walk.cdb.template`'s F/P/LS/BLD/DI/DC lines, real per-frame
counts from the ORIGINAL FW0 40-frame captures of these exact four poses
give a concrete baseline (frame 40 in every FW0 log is truncated by the
auto-detach race, so these are per-COMPLETE-frame averages over frames
1-39; this kit's own 5-frame captures will end at a different, cleaner
boundary since they use the fall-through recipe instead of FW0's
non-firing `qd`, but the per-frame magnitude should still hold):
| Pose | DI/frame | DC/frame | LS/frame | BLD/frame |
|---|---:|---:|---:|---:|
| `holtburg-doorway-still` | 1 | ~6 | 1 | ~13 |
| `terrace-edge` | 0 (outdoor root) | ~2 | 1 | ~14 |
| `cathedral-arrival` | 1 | ~3 | 1 | ~10 |
| `foundry-deep` | 1 | 1 | 1 | ~12 |
`EC`/`OC` (new in the walk script) have **no prior baseline** — this is
their first capture. Expect a similar order of magnitude to `DC`/`BLD`
(shell/object-cell draws roughly track the cell/building roster each frame
walks); a flat **zero** `EC` or `OC` count for an interior pose is the real
red flag, not an exact mismatch against the table above.
`PD`/`DM` (parts script) and `AM`/`FL`/`PM`/`PC` (alpha-depth script) also
have **no prior baseline**:
- `PD`/`DM` are per-resident-part, not per-cell/building — expect
hundreds to low thousands of lines across 3 frames in a town pose
(doorway-still, terrace-edge, cathedral-arrival) and noticeably fewer in
the sparser foundry-deep interior. This is exactly why the parts script
defaults to 3 frames instead of 5.
- `FL` is bounded by known call sites
(`oh1-alpha-list-contract.md` s7): expect roughly
(buildings-in-view-this-frame) + 1 (top-level `SmartBox::RenderNormalMode`
flush) + (0 or 1, depending on whether that frame's `PView::DrawCells`
had an outside view) per frame — so order 10-16 for the town poses,
fewer for foundry-deep.
- `AM` and `PM` depend entirely on how much transparent/portal content is
in view at that specific pose (falls, torches, doorway apertures) and
could legitimately be zero for a pose with no transparent surfaces in
frame — a zero count here is not automatically a bug.
## If retail lags or ACE disconnects
The parts script is the risky one: it traps on every part draw (hundreds
per frame), so expect retail to freeze for a few seconds during its three
frames. Run it last for each pose, and if ACE drops the session once, use
`-Frames 2` on the retry rather than abandoning the pose.
Per `claude-memory/project_retail_debugger.md`'s "High hit rates lag the
game" watchout: `PD`/`DM` in the parts script are the highest-risk
breakpoints in this kit (fired once per resident part draw, potentially
thousands of times per frame in a dense town pose). If you observe visible
stutter, input lag, or an ACE disconnect during a capture:
1. Let the current capture finish or auto-detach if possible — do not
force-kill cdb (see CLAUDE.md: `Stop-Process -Force` on cdb takes the
attached retail client down with it via `TerminateProcess`).
2. Re-run with a SHORTER frame count for the offending script:
- `oh-capture-walk.cdb.template`: try `-Frames 3` instead of 5.
- `oh-capture-parts.cdb.template`: try `-Frames 1` instead of 3 — a
single frame is still enough to sanity-check the `PD`/`DM` line
format and the `<GFXOBJ_DID_OFF>` offset.
- `oh-capture-alpha-depth.cdb.template`: try `-Frames 3` instead of 5.
3. If ACE actually disconnected, follow CLAUDE.md's "Logout-before-
reconnect" guidance before relaunching retail — a hard-killed session
can stay logged in on ACE's side for several minutes.
4. If a specific pose keeps lagging even at the reduced frame count,
capture that pose's walk/alpha-depth scripts (cheap) and skip its parts
capture (expensive) rather than spending the whole hour on one pose.
## New line-kind prefixes (glossary)
All existing FW0 prefixes (`F`, `P`, `LS`, `BLD`, `DI`, `DC`) are
byte-identical to
`docs/research/2026-08-30-fw-walk-oracle/README.md`'s "Trace format"
section — not repeated here.
| Prefix | Script | Source breakpoint | Meaning |
|---|---|---|---|
| `EC` | walk | `RenderDeviceD3D::DrawEnvCell` @`0x0059F170` | A built EnvCell shell's draw-stamp fired this frame; `<cellid>` = the cell's `m_DID`. |
| `OC` | walk | `RenderDeviceD3D::DrawObjCellForDummies` @`0x005A0760` | An object-cell's shadow-part list was walked this frame; `<cellid>` = the cell's `m_DID`. |
| `PD` | parts | `CPhysicsPart::Draw` @`0x0050D7A0` | A physics part's draw call, with its degrade-resolved GfxObj identity and (best-effort) containing cell. |
| `DM` | parts | `RenderDeviceD3D::DrawMeshInternal` @`0x0059F360` | The actual admission/stamp/submit call for one GfxObj part, with its GfxObj identity directly from the argument (no degrade-level resolution needed). |
| `AM` | alpha-depth | `D3DPolyRender::AddMeshToAlphaList` @`0x0059C230` | One subset appended to the CLIP or ALPHA FIFO list. |
| `FL` | alpha-depth | `D3DPolyRender::FlushAlphaList` @`0x0059D2E0` | A flush-barrier call, with the return address to identify the caller site. |
| `PM` | alpha-depth | `D3DPolyRender::DrawPortalPolyInternal` @`0x0059BC90` | A far-Z building punch or true-depth exit-seal attempt, with the counter value before this call's own possible increment. |
| `PC` | alpha-depth | `PView::DrawCells` @`0x005A4840` | The persistent `portalsDrawnCount`/`forceClear` state sampled at this eligible call's entry. |
## Open questions a human must settle before a real session
1. **GfxObj DataID offset — RESOLVED 2026-09-02 (lead).** `DBObj : Interface`
lays out `vfptr@0`, `m_dataCategory@4`, `m_bLoaded@8`, the 8-byte
`m_timeStamp@0x10`, `m_pNext@0x18`, `m_pLast@0x1C`, `m_pMaintainer@0x20`,
`m_numLinks@0x24`, `m_DID@0x28`. `DBObj` is the first base of both
`CGfxObj` and `CObjCell` (via `SerializeUsingPackDBObj`), and `+0x28` is
exactly the `m_DID` offset every FW0 capture already read on cells. The
parts template is pre-filled with `0x28`; recon's `dt acclient!DBObj`
re-confirms it.
2. **Calling convention — RESOLVED 2026-09-02 (lead) from the named
pseudo-C signatures.** `AddMeshToAlphaList(MeshBuffer*, int32, CSurface*,
uint8, uint8, uint8)` (424920), `DrawPortalPolyInternal(CPolygon*, uint8)`
(424490), and `FlushAlphaList(float)` (425941) are static with the
default convention: no receiver, arguments at `[esp+4]` upward. The
`AM` line now also prints the `CSurface*` at `[esp+0xC]`, and every
`uint8` argument is read with `by()`. The recon `uf` dumps remain as a
sanity check.
3. **`CPhysicsPart+0x14` (`deg_level`).** Derived only from
`acclient.h:31151-31173`'s field order (confirmed consistent with its
neighbors `draw_state+0x1C` and `gfxobj+0x20`, both of which OH1
independently Ghidra-cited), but never independently Ghidra-checked
itself anywhere in the OH1 evidence docs. Low risk, still worth a glance
at `oh-recon.cdb`'s `dt acclient!CPhysicsPart` output.
4. **`DrawObjCell` vs. `DrawObjCellForDummies` cell-argument location.**
The task that produced this kit flagged this exact question as an
example of the class of ambiguity to watch for. It turned out NOT to be
ambiguous once the pseudo-C signatures were read directly
(`acclient_2013_pseudo_c.txt:429177,429198`): both take the cell
identity as a plain stack argument at `[esp+4]`, and
`oh-capture-walk.cdb.template` uses `DrawObjCellForDummies` (the
function `PView::DrawCells` step 10 actually calls, per
`oh1-depth-lifecycle.md`'s exact frame ordering) rather than the inner
`DrawObjCell`/`DrawPartCell` helpers. Recorded here as a "resolved, but
worth a second pair of eyes" item rather than a true open question,
since it was resolved by reading text rather than by a live `dt`/`uf`.
5. **The `PD`/`DM` lines' `cell=` field can be stale.** It is set by a
silent `RenderDeviceD3D::DrawPartCell` hook and simply carries forward
whatever cell it last saw — it can be wrong for the first part drawn
after a building's forced portal-only pass (`DrawBuilding`'s
`Draw(part, 1)` call) and before that frame's first ordinary object-cell
walk. Treat it as advisory, not authoritative; a real port needs the
actual call-stack correlation this draft does not attempt.
6. **`oh-capture-alpha-depth.cdb.template` does not instrument the exact
mid-`PView::DrawCells` PCs** (`0x5A4885` stamp increment, `0x5A488C`
counter read, `0x5A489E` reset, `0x5A48A9` conditional clear) that
`oh1-depth-lifecycle.md`'s own Ghidra arbitration table cites. It
samples the global `portalsDrawnCount`/`forceClear` memory cells at
`PView::DrawCells`' function ENTRY instead (the `PC` line), which is
sufficient to reconstruct before/after values across consecutive
eligible calls (per that same doc's "Consecutive-call example" table)
without guessing which register holds the value at an arbitrary
mid-function instruction — but it means this kit does not directly
observe the mid-frame stamp increment itself. If a later slice needs
that specific event, it needs its own recon pass over those four PCs.