Files
BT411/context/cockpit-view.md
T
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00

202 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: cockpit-view
title: "First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint"
status: living
source_sections: "task #55 (2026-07-10); reference/decomp part_007.c/part_013.c/part_014.c/part_011.c; engine/MUNGA_L4/bgfload.cpp, L4VIDRND.cpp; game/reconstructed/btl4vid.cpp"
related_topics: [rendering, bgf-format, gauges-hud, subsystems, decomp-reference]
key_terms: [material-ramp, siteeyepoint]
open_questions: ["gyro tail: gait jolt/rumble [T3 gates], sway model, torso-pitch eye writer, damageForce fill", "exact authored frame colour: the unlit constant (0.13,0.12,0.15) is T3 tuned-to-footage (BT_COP_FRAME overrides; the ramp-index approach + BT_COP_RAMP_L are RETIRED)"]
---
# First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint
The pod's only view was the cockpit eyepoint looking out through a per-mech canopy shell.
Task #55 reconstructed both halves — the CANOPY rendering and the EYE camera — from the binary.
Verified in-game on all 8 mechs (Thor vs gameplay footage). [T2]
## The canopy shell (*_COP.BGF)
- One per mech (12: AVX BLX FIX JAX LOX MAX OWX RAX SNX STX THX VUX; model→skeleton table in
BTL4.RES @~3232850: `madcat=mad`, `blkhawk=blh`, … — the INSIDE skeleton is the X-variant, so
Madcat = `MAX_COP`). It is the torso segment's SkeletonType_A mesh; the inside view loads
EXACTLY ONE segment mesh (the `_cop`) — 25 others hidden. [T2]
- Material: `<pfx>skin:blakskn_dz_{u,r,l}torso_mtl` — DIFFUSE/AMBIENT (0,0,0) + RAMP_REF
`softer`, **NO texture** (BLXSKIN.BMF is 238 bytes total; byte-verified). The 56 nonzero-UV
verts in BLX_COP sample nothing (planar-projection authoring leftover). [T1]
- Geogroups carry the `SV_SPECIAL " dz_*torso PUNCH "` token → `dpl_Punchize` (see
[[bgf-format]] PUNCH). The board opcode is a constant — the frame/window discriminator is the
GEOMETRY, not a key. [T1]
- **Every `_cop` is an OPEN STRUT LATTICE** (boundary-edge ratios, per-patch edge namespacing:
38-59% across all 12). The struts ARE the frame; the openings ARE the windows. Beware the
edge-counting trap: counting edges across patches (patch-local indices collide) undercounts
boundaries catastrophically (BLX "7%" wrong → 59% right). [T1]
- **Rendering (FINAL treatment 2026-07-11; bgfload.cpp keyed by filename `meshIsCop`):**
1. **The PUNCH STENCIL-CUT kit** (see §FINAL below + [[bgf-format]]): the TAG_PATCH scan
hash-pairs the punch geogroup's chunks into mask / hull / twin roles; L4D3D executes the
3-step stencil cut (D24S8). `BT_COP_PLATES=1` disables (diag).
2. **Double-sided** like all no-normal geometry — the earlier single-siding + per-face inward
winding existed solely to fight the solid-box look, which the un-cut PLATES caused, not the
winding; both are REMOVED (bgfload.cpp:533 comment). `BT_COP_SINGLE=1` restores
single-siding, `BT_COP_FLIP=1` flips its winding (diag only).
3. **UNLIT constant frame colour** (the map-lighting bug fix, 2026-07-11): the frame batch is
routed through the renderer's pure-emissive branch (black diffuse + emissive
`(0.13,0.12,0.15)` [T3 tuned-to-footage]; `BT_COP_FRAME="r g b"` overrides). The previous
lit path multiplied the colour by the MAP light — cavern-night `ambient=0.8` washed the
frame gray (user-reported). `BT_COP_RAMP_L` + the flat-ramp-index approach are RETIRED.
- The shell SHOWS by default in the inside view (btl4vid.cpp ApplyViewSkeleton);
`BT_HIDE_COCKPIT=1` hides it (diag).
- **Wrong turns, recorded so they stay dead:** (a) "windows are punch texels" — no texel exists;
(b) "the punch patch IS the visible frame / drop-punch removes the cockpit" — an artifact of
testing with the broken pre-inverse eye; (c) a view-direction face-cut heuristic — not
data-derivable; (d) "drop the whole punch geogroup = the windows" and (e) "duplicate pairs =
plates, hide both" — both superseded by the firmware-decoded STENCIL-CUT kit (§FINAL below),
which is the authentic mechanism.
## The authentic eyepoint (decomp-verified [T1])
Pipeline: `eyeWorld = parentSegmentWorld · baseOffset · R(EyepointRotation)`; `VIEW =
inverse(eyeWorld)`. No LookAt anywhere.
- **Eye ctor = FUN_004579a8** (part_007.c:9274, = DPLEyeRenderable). Caller (part_014.c:5525-66,
BTL4VID.CPP) gates on segment name `"siteeyepoint"`, passes:
- offset matrix = the segment's **GetBaseOffset()** (segment+0x74, the LOCAL rest transform) —
NOT GetSegmentToEntity, NOT an upright basis;
- parent = the eyepoint segment's **parent draw component** (`dcs_array[GetParentIndex()]`) —
liveness reaches the eye through this parent chain in the PORT TOO (task #58 FINAL): the
twist path is `Torso::PushTwist → Joint::SetRotation → hinge rotationAmount →
HingeRenderable::Execute` (multiplies the live hinge into the matrix stack), and the eye
executes inside that scope (`jointtorso → jointeye → siteeyepoint`; VERIFIED: with the real
jointtorso swept, the active cockpit `[eyefwd]` swings ~60° and the torso hinge renderable
sits in the eye's parent chain). The CAMERA yaws with the twist, the canopy rides the same
subtree, the crosshair stays screen-centered — see [[combat-damage]] Targeting.
⚠ TWO instrumentation traps produced false "eye is frozen" readings first: (1) the headless
default camera is the CHASE eye (root-parented, same DPLEyeRenderable class — gate probes on
the COCKPIT eye being live); (2) `BT_FORCE_TORSO` used to override the joint names with the
BLH demo pair (`jointshakey2`/`jointtshadow`), silently redirecting a MadCat sweep into the
SHADOW hinge — the hook now only fills joints the resource left unresolved. An explicit
eye-side twist compose added on those false readings DOUBLE-ROTATED the view (2× vs the 1×
canopy) and is retired;
- the `EyepointRotation` entity attribute (EulerAngles; Mech member, init 0).
- **Combine order:** `FUN_0040b104(dst,A,B)` computes `dst = B × A` (translation from B) →
the eye local = `baseOffset × R`, R applied FIRST in eye-local space. Recomputed only when the
euler CHANGES (FUN_00457b48 @part_007.c:9360, epsilon gate).
- **Per-frame view = FUN_004c22c4** (part_013.c:11742): rebuilds eyeWorld from the view-DCS's
live accumulated transform, then `FUN_0040b244` = TRUE AFFINE INVERSE → the view matrix, then a
UNIFORM zoom scale. The forward/up axes fall out of the eye's own basis — the renderer never
picks an axis. (Our old hand-rolled `D3DXMatrixLookAtRH` with forward=+Z/up=+Y row extraction
was exactly why some mechs aimed into the canopy — see [[reconstruction-gotchas]].)
- Port implementation: btl4vid.cpp (eye build: offset_matrix + parent_DCS), L4VIDRND.cpp
DPLEyeRenderable::Execute (order `Orient × R`, `D3DXMatrixInverse` on the live path — note the
ctor also holds a copy of this code but its view write is DEAD; patch the Execute).
## Status per mech + the gyro (task #56 — gyro now LIVE)
- **Madcat (MAX_COP): verified good on pure defaults** — dark domed frame (top arch +
cross-braces + A-pillars + dash), world through the openings. Un-regressed with the gyro live.
- **The gyro is RE-ENABLED and clean** (task #56): ctor field-map + integrators + writers
reconstructed byte-exact from @004b3778/@004b2ec0/@004b30ec/@004b34ec, layout
static_assert-locked (sizeof==0x3D0), joint-write dispatch moved to the MECH performance tail
(`GyroFrameJointWrite`, matching the binary calls @0x4aaf74/83), gyro↔torso pitch link wired
(`gyro+0x258 = &torso currentTwist`). Runtime: both joints resolve type 5, Performance
installed (flags 0x100), `WriteMechJoint eyePos=(0,0,0) body=(0,0,0)` finite every frame — the
old NaN is gone. Mechanism: WriteMechJoint drives ONE node (`jointeye`, which parents
`siteeyepoint`): TRANSLATION = the eye position spring, ROTATION = the body tip spring.
WriteEyeJoint is a MULTIPLICATIVE sway attenuator on `jointlocal`'s animated rotation
(gated legAnimState!=0; must run after the animation pass). [T2 runtime-verified]
- **Steady-offset hypothesis DISPROVEN [T2]:** the .MDL spring targets are SYMMETRIC
(pos=(0.1,0.15,0.1), neg=(0.1,0.15,0.1) — same for madcat AND bhk1) → the eye equilibrium
is (0,0,0) and the clamps limit gyro motion to ±0.1..0.15u. The gyro is a BOUNCE/sway
mechanism, NOT a large steady offset. The authentic resting eye = raw baseOffset for every mech.
- **FINAL (2026-07-11, i860-firmware-decoded): PUNCH is a three-chunk STENCIL-CUT KIT.** The
VREND.MNG board firmware's 'damageize' handler (@0xf040f6f8, disassembled with a custom i860
tool) writes the dpl_Punchize token triple {dmg_set, undmg2enbl|texture, dmg_clear} onto the
punch geogroup's first three geometry chunks IN FILE ORDER: chunk1 = the detailed
window-aperture shapes = an invisible per-pixel MASK; chunk2 = the coarse HULL = the ONLY
colour-drawn geometry, drawn where NOT masked; chunk3 = a byte-identical hull twin = the
damage-reset record, never visible. The visible canopy = hull-minus-apertures. Port:
bgfload identifies the within-patch twin pair (hull + skip) and tags the non-paired pmeshes as
masks; L4D3D draws mask (stencil 1, no colour/z-write), hull (stencil-reject), mask (stencil 0)
on a D24S8 device; the cut never touches z, so later-drawn entities show through apertures.
BT_COP_PLATES=1 disables the kit (diag). In-game, all 8 mechs on pure defaults [T2]:
connected dark frames with clear viewports; thor (the footage mech) shows the big central
viewport + surrounding frame exactly as filmed. TWO earlier readings are superseded and were
WRONG: "punch patches = windows, drop them" (left floating frame fragments) and "duplicate
pairs = plates, hide both" (hid the HULL and drew the MASK = the inversion the user caught).
- **Hit-bounce: LIVE and verified [T2] (2026-07-11).** The damage→gyro fan-out **FUN_004b2980**
was re-disassembled byte-exact from the raw image (capstone; scratchpad/dis_4b2980.py) and
reconstructed as `Gyroscope::ApplyDamageResponse` (gyro.cpp): zero-damage + Collision(0) no-op;
hit direction = `Damage::damageForce` or a RANDOM horizontal fallback when ~zero (our senders
don't fill damageForce yet — the fallback is binary-legal; directional fidelity = fill it later);
rotated by the yaw-only torso-twist frame; per-type scaling `amount / damageMultiplier[type] *
damageResponse[type].{trans,pitchRoll,yaw,vibration}` (Explosive alone multiplies burstCount);
clamp 1.3; four kicks — impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
**vibrationDirection@0x390 = (0,1,0)** — the "animationOffset/Scale/Phase scalars" were this
unit vector all along), verticalImpulse(yaw, dir). Call sites wired: the take-damage hub
(mech.cpp TakeDamageMessageHandler, FIRST action, binary @0x4a0264), the crushable-icon crunch
(mech4.cpp, torque 0.4 along Δv + up-impulse 0.2, @4aa81e/@4aa86c), the firing recoil
(projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,1.5) × damage/16, @4bc136-4bc19c).
Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped
oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed.
**Quantitatively verified (2026-07-11 MP trace):** `BT_GYRO_TRACE=1` logs the integrator state
every displaced frame (`[gtrace]`); a 25-hit autofire session (scratchpad/GYRO_TRACE.png)
shows per-hit kick → damped oscillation → settle-to-zero-and-stay, peaks ±0.028u X/Z ±0.005u Y
(well inside the ±0.1/0.15 clamps), body pitch/roll ±0.02 rad, zero NaN/drift — and the
measured frequency ratio matches the authored constants: Y period 9.5 frames vs X/Z ~75 ≈
√(springK.y/springK.x)=√60≈7.75× as dictated by springK=(0.2,12,0.2). [T2]
- **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE
(@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate
naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without
re-verifying the gate semantics); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0);
(c) the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4 @0x4aafab) + the
mapper glance-look states (FUN_004afd10); (d) senders filling `Damage::damageForce` (beam:
impactmuzzle; projectile: velocity) for directional bounce fidelity; (e) FireWeapon's heat add
(pre-existing separate gap). `gyroRumbleTimer@0x5c4` was re-typed from the mis-named
`int clipLoadGuard` (binary uses it as a float rumble countdown).
## Offset cockpits — the placement-model verification [T1/T2]
Some mechs have laterally OFFSET cockpits, and the data + our port agree exactly (2026-07-11):
`[jointeye]` rest translation per SKL vs the `_COP` canopy X-center — **thor: tranx=+1.13, canopy
center +1.13 (exact)**; loki 0.25/0.15; owens +0.19/+0.16; the other five ≈0/≈0. Our in-game
measured eye (`[EYECHK]`) reproduces the Thor offset to the hundredth (lateral 1.13 from the mech
origin). The eye chain (baseOffset + parent-segment DCS composition) is therefore CORRECT per-mech;
the canopy-render question was subsequently resolved by the stencil-cut kit — all 8 render
authentically (see the FINAL paragraph above).
## The playable roster is 8, not 12 [T1]
`BTL4.RES` contains exactly 8 mech model resources (avatar, bhk1/blkhawk, loki, madcat, owens,
sunder, thor, vulture — each with `<name>`, variants `<pfx>1/2`, and a `<pfx>dead`). Strider /
Raptor / Firestarter / Blackjack have `_COP` meshes, SKLs and gauge configs but NO model resource
(cut/other-version leftovers); forcing them via `BT_FORCE_MODEL` feeds NULL to
`ResourceDescription::Lock` → AV in `CreatePlayerVehicle` (cdb-verified). Valid `BT_FORCE_MODEL`
names: `avatar bhk1 loki madcat owens sunder thor vulture` (+ variants ava1/lok1/lok2/mad1/mad2/
own1/snd1/thr1/vul1/blkhawk).
## The authentic view field (FOV) — viewangle is HORIZONTAL [T1]
`BTDPL.INI [dpl_defaults] viewangle=60.0` is the **horizontal** field of view: the original Tesla
call (preserved in the DPLRenderer::SetView comment, L4VIDEO.cpp) is
`dpl_SetViewProjection(view, -1, -aspect, +1, +aspect, 1/tan(viewangle/2))` — horizontal
half-extent 1.0, vertical scaled by aspect. Authentic frustum at 4:3 = **60° × 46.8°**. The
port had fed the 60 to D3DX as the VERTICAL fov (75° × 60° at 4:3, worse on widescreen) — the
world read ~25% farther away ("camera too far back" reports) and the canopy frames were
massively over-revealed. Fixed (`BTFovYFromHorizontal`, all 5 projection sites incl. sky +
resize): the horizontal field stays 60° at any window shape. The aim/pick feed reads the live
projection (`gBTAimP22`) so targeting adapts automatically (combat re-verified). Credit: a
user-community report ("frames were never this revealed") + the resolution/FOV hypothesis.
## Diagnostics / env gates
`BT_FORCE_MODEL=<name>` (force the player mech: madcat/owens/sunder/…; btl4mssn.cpp),
`BT_HIDE_COCKPIT`, `BT_COP_PLATES` (disable the stencil kit), `BT_COP_FRAME="r g b"` (frame
colour override), `BT_COP_SINGLE` / `BT_COP_FLIP` (single-siding diag), `BT_COP_DEBUG` (paint
the shell green), `BT_COP_DUMP` (per-batch punch/bbox), `BT_EYE_FWD=<f>` (eye position probe),
`BT_GYRO_LOG` / `BT_GYRO_TRACE` (gyro bring-up log / per-frame integrator trace, gyro.cpp),
`[EYECHK]` log (eye world pos + look dir + entity pos, first frames).
(RETIRED: `BT_COP_DOUBLE`, `BT_COP_RAMP_L` — removed with the stencil-cut/unlit-frame landing.)
## Key Relationships
- Uses: [[bgf-format]] (PUNCH, vertex tags) · [[rendering]] (ramp model, skeleton branch)
- Feeds: [[gauges-hud]] (the HUD overlays this view) · [[subsystems]] (gyro, task #56)
- Gotchas: [[reconstruction-gotchas]] §14 (LookAt axis guess), §15 (per-patch edge counting)