Commit Graph
59 Commits
Author SHA1 Message Date
CydandClaude Fable 5 07eb74d1f7 BT410 5.3.80: joint articulation is live -- 22 nodes, and the twist reaches the board
RecurseSKLFile now builds a joint renderable for any node whose page name
resolves to a live skeleton Joint: HingeX/Y/Z -> HingeRenderable (watching
Joint::GetHinge), Ball -> BallJointRenderable (watching GetEulerAngles),
otherwise the static path.  Each holds the rest offset in one DCS and the live
rotation in a child DCS, and its Execute diffs the watched value and calls
DPL_FLUSH_DCS -- the engine's own mechanism (L4VIDRND.CPP:1026+).  The value
comes from the mech's JointSubsystem via ResolveJoint, so sim and renderer
read one source.  Gated on BT_JOINTS while it proves out.

  [skl] video\max.skl -> 26 nodes, 1 objects, 1 eye, 22 articulated

The bridge reported anim_abs=1 joints=0 twist=+0.00 before; it now reports
joints=1 twist=-0.86, matching the game's [torso] twist=-0.856.  With the mech
stationary, frames that differed by 0.0% now differ by 62-80%.

A crash it exposed: Mech::ResolveJoint passed segment->GetJointIndex()
straight to GetJoint unchecked, and a segment with no joint reports -1 --
GetNthImplementation then indexes [base + -1*4] and dies (guest 00426A1D).
Torso never hit it because it only asks for its own authored joint name; the
walk asks for every page.  Now bounds-checked against GetJointCount.

Open: the canopy does not stay rigid in the view, though it and the eye hang
off the same articulated node.  Cancelling the bridge's cage compensation
(CAGE_TWIST_SIGN=0) did not close it.  Leading hypothesis: SetupCull builds
worldToEyeMatrix from GetSegmentToWorld(siteeyepoint) -- the SIMULATION's
segment transform -- independent of the render tree, so the canopy follows our
render chain and the eye follows the sim's, and they diverge whenever one
carries the twist and the other does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 20:46:18 -05:00
CydandClaude Fable 5 8769dae0ac BT410 5.3.79: the twist is live in the sim and absent from the wire
pod_render_twist.conf isolates it -- throttle and turn zero, torso sweeping,
[sim] confirming spd=0, so only the torso joint can move the view.  Five
frames across a full sweep differ by 0-74 pixels: the view does not move.
The same run's telemetry has the twist sweeping the full authored range
(-0.895 to -0.022 rad, cmdL/cmdR alternating, joint resolved) and the bridge
reports anim_abs=1, joints=0 -- only the root articulates.

The cause: RecurseSKLFile builds each skeleton node's DCS with a baked matrix
and flushes it once at build time, and nothing re-flushes a node when its
joint angle changes.  TorsoSimulation faithfully calls SetRotation() every
frame, updating the Joint object, but no renderable reads that back into the
node's dpl_DCS.  The simulation twists; the board never hears.

The missing piece is the per-frame joint renderable -- the engine's
ChildOffsetRenderable family (L4VIDRND.CPP:930) exists for exactly this, and
our RootRenderable already proves the pattern for the hull.  That one brick
unlocks the torso twist (and the cockpit eye panning with it, since the eye
hangs off that chain), the leg gait, and weapon-pod aim.  Until then the mech
translates through the world as a rigid body -- which is what every frame so
far has shown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 20:13:12 -05:00
CydandClaude Fable 5 3c289ff10a BT410 5.3.78: the torso twists from the button path; two buffer overruns fixed
BT_FORCE_TORSO sweeps the twist BUTTONS -- the members the streamed direct
mappings write -- so the chain runs without a hand on the RIO.  Live on the
pod: twist sweeps -0.015 to -0.891 rad and back as cmdL/cmdR alternate, at
the resource rate 0.873 rad/s, inside the authored +/-2.443 limits, with
jointtorso resolved.  End to end: attribute id -> direct-mapping destination
-> TorsoSimulation integrator -> skeleton joint -> the canopy eye hanging off
that chain.  5.3.68's table and 5.3.71's integrator both confirmed against
real button semantics.

Two buffer overruns, one of them mine.  Reading TORSO.CPP back caught the
ctor still zeroing dynamicsState[22] after I shrank the array to [16] by
carving the command members out of its front: 24 bytes past the end of every
Torso, on the heap, every mission, since 5.3.68.  A sweep for the same shape
across BT/BT_L4/MUNGA found a pre-existing one -- MECHMPPR.CPP zeroing
reserved[22] against reserved[21].

Both now bound by ELEMENTS().  The rule for this tree: any array carved out
of a reserve block must have its initialiser bound by ELEMENTS, because the
carve is exactly the edit that silently invalidates a literal.

Neither explains the residual host fault (it predates 5.3.68 at 3/3), but
both were real corruption running in every mission -- and the Torso one was
introduced by the very fix that cut the fault rate, which is worth
remembering whenever a rate MOVES instead of going to zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:52:38 -05:00
CydandClaude Fable 5 e69d761fb8 BT410 5.3.77: the overrun drain loop is not the trigger either
serialnamedpipe's P_RX_BLOCKED path drains its whole backlog when the guest
hasn't read in time -- directserial's `while (doReceive());`.  That looked
like the trigger, since a named pipe's backlog is unbounded where a real
port's is capped by the line rate.  Bounded it to one byte (real UART overrun
semantics) and re-ran the conf that had faulted 3/3: FAULT 283s, FAULT 226s,
FAULT 204s.  Killed.

The measurement that explains why it was never plausible: overruns during a
run are 1-3 per report period, because vRIO sends about a byte every 1-3ms --
the backlog is shallow and the loop had nothing to teleport.  The 494/499
counts all land after the game exits.  I read the code and inferred a burst
without measuring the queue depth it operates on.

Default restored to drain-all: it is the validated directserial behaviour the
RIO's rxpollus/rxburst tuning was calibrated against, and changing it on a
dead hypothesis would risk real-cockpit timing for nothing.
VPX_RX_OVERRUN_ONE=1 opts into the bounded form.

The roadmap now carries a fault ledger of every dead hypothesis so none get
re-run.  What survives: a deterministic DPMI-host path walking a
{next,handler} chain into a node whose pointer is an unhooked IVT value,
entered under RIO interrupt load.  Naming the owning routine needs a trace of
entries to host 0x66CF, clean vs faulting -- a DPMI32VM reversing
sub-project, now decoupled from the reconstruction (vRIO down = 100%
reliable, logged conf = ~70%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:33:05 -05:00
CydandClaude Fable 5 d378148c14 BT410 5.3.76: THE COCKPIT, FROM THE PILOT'S SEAT
cockpit_max.png: the MAX_COP canopy seen from the eyepoint on the live pod --
window frames, A-pillars, the sill across the bottom, and the arena city out
through the glass at 30fps.  The run's log line:

    [skl] video\max.skl -> 26 nodes, 1 objects, 1 eye

Exactly the predicted shape: the X-variant carries the same 26-node chain as
the body skeleton, so eye composition and torso twist are identical, with ONE
object -- the canopy shell -- instead of nineteen body parts, and the eye on
jointeye inside it.

This closes the arc that began with a mech statue at the world origin and a
camera frozen beside it: entity -> RootRenderable -> skeleton under its DCS ->
cockpit variant for the inside view -> eye on the canopy joint.  What a pilot
saw in 1996, rendered by the reconstruction through the emulated Division
board.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:59:06 -05:00
CydandClaude Fable 5 edb1670900 BT410 5.3.75: the quiet conf faults 3/3 -- logging SUPPRESSES the fault
Hypothesis: our BT_* logging inflates the load window, the window is the
exposure, so a quiet run should load faster and fault less.  Both halves
wrong.  pod_render_quiet -- which differs from pod_render_rec by exactly the
six BT_* logging vars -- faulted 3/3, same signature, against ~25-40% for the
logged conf.

That is the strongest evidence yet that the residual is a timing-sensitive
HOST bug rather than anything about our data: writing to COM3 changes when
interrupts land relative to the DPMI host's handler-chain walk, and nothing
about our attribute tables or renderables could plausibly be modulated by
whether we print to a serial port.  Interrupt phase can.

Two immediate consequences: pod_render_rec is the LESS fault-prone rig, so
use it; and the 'shipped survives' baseline weakens further, since shipped
also writes plenty to COM3 and so sits on the suppressed side of the same
effect on top of its shorter window.

The wall-clock numbers also correct an earlier assumption: 150-275s
boot+load, not ~60s.  The 60s figure was the load DRAIN in game ticks; DOS
boot, two diagnose passes and card init dominate the rest.

Next is clearly emulator-side: the host keeps no EBP frames (the frame walk
returned one bogus entry), so naming the routine that owns the chain needs a
breakpoint-style trace of entries to 0x66CF with the head node, diffing a
clean run against a faulting one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:51:35 -05:00
CydandClaude Fable 5 e97862507c BT410 5.3.74: the faulting instruction decoded -- a handler-chain walk with a bad node
The SegPhys-corrected dump read real code at last (the earlier zeros were the
probe reading EIP as a bare linear address; CS=00FF has a base):

    66D4  push 0 / push edi / push ebx / push esi
    66D9  call dword near [ebx+4]     <- faults
    66DC  cmp eax,0 / jz done
    66E5  mov ebx,[ebx] / jmp loop

A linked-list walk with a per-node callback -- node = {next@+0, handler@+4},
four args, "handled" on nonzero.  A DPMI exception/interrupt handler chain.

The arithmetic closes exactly: the read is DS_base + ebx + 4, and
7000FA64 - F000CA64 = 80003000, so DS base is 0x80003000 and the famous cr2
is that wrap.  My earlier '[EBX+0x3004]' reading was fabricated from the cr2
alone -- there is no 0x3004 displacement, which is why the constant appeared
in no binary.

So: a chain node's NEXT pointer holds F000CA60, the value parked in unhooked
IVT slots -- the walk expected a terminator and got an interrupt vector, then
read its +4 as a function pointer.  Identical registers on every catch, so
one deterministic path, entered often enough that our 60s load window catches
it 25-40% of the time while shipped's 15s window mostly does not.

Open: which routine owns the chain, and where the bogus next came from.  The
probe now walks the EBP frame chain to name the caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:34:22 -05:00
CydandClaude Fable 5 2ce3528f1c BT410 5.3.73: the bad pointer is the emulator's default-handler stub
The IVT scan on the second probe catch settles EBX: F000:CA60 is the
emulator's default unhandled-interrupt callback -- the value filling every
vector nobody hooked (it matched ~56 of them), while the hardware-IRQ block
IVT[08-0F] holds the DPMI host's own 0D1F reflector stubs.  The serial IRQs
are properly hooked; the host read one of the UNHOOKED vectors' default value
and probed [value + 0x3004] as a flat pointer, wrapping to the famous cr2.

Also corrected: the first dump read ESP/EIP as bare linear addresses -- the
'stack' bytes were DOS conventional memory (they decode as 16-bit real-mode
code), since SS=0107 has a nonzero base.  SegPhys-corrected dumps are built
and armed for the next catch.  The zeros at the faulting EIP are consistent
with DPMI32VM being a virtual-memory host that pages its own regions --
'Reference to a page you don't own' is its pager's abort message for an
address outside every region it owns.

Nailed: deterministic host-side path; the serial stream correlates as
exposure, not as trigger bytes; the bad read is [unhooked-vector-default +
0x3004].  Open: which vector, which host routine -- the corrected dumps
should name the return chain on the next catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:24:27 -05:00
CydandClaude Fable 5 f2687a3146 BT410 5.3.72: the fault caught in the act; the inside view gets its cockpit
VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched
linear address, dump guest registers, the last 64 serial RX deliveries with
guest cs:eip at each, the code bytes at the faulting EIP and the stack top.
serialnamedpipe's doReceive feeds the ring.  Armed in podrun.sh and
launch_pod.ps1.

The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4
in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment
F000 offset CA60, a BIOS default interrupt handler -- and the faulting access
is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2
7000FA64.  The host probes a word 0x3004 bytes past a real-mode vector value
treated as a flat pointer: harmless for its own low-memory handlers, a fault
for BIOS F000:xxxx defaults.  The serial ring shows a steady 1-byte/1-3ms
vRIO stream with nothing special at the fault -- the stream determines which
vectors get walked, not the crash itself.  The 0x3004 appears nowhere in
DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code
bytes at EIP; faulthunt.sh loops runs until the next catch.

Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet
discriminating.

The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant
naming convention (MAD->MAX etc., all 64 skeletons present) selects the same
25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell
with the PUNCH-texel windows from the capture forensics.  The donor names the
same mechanism from the decomp side (inside = SkeletonType_A with '_cop'
selection).  Fallback to the body skeleton when no X file exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:09:22 -05:00
CydandClaude Fable 5 bd1070110b BT410 5.3.71: torso button commands wired into the sim; residual fault rate measured
TorsoSimulation now carries the donor's digital command handling (binary
@004b5cf0): elevate up/down, twist left/right with limit clamps, the
centre-button recenter latch slewing home across frames, and the ramp
machinery kept verbatim including the binary's punchline -- the shipped build
unconditionally overwrites the ramp with 1.0f, authored dead weight preserved
as the binary's shape.  recenterActive carved from the dynamicsState reserve.
With 5.3.68's table fix the RIO/TM twist buttons land in these members and
move the torso, and the canopy eye rides the twist chain.

Also corrected in the roadmap: the 5.3.70 TriggerState 'wart' was not a wart
-- CheckFireEdge already compares fireImpulse's bit pattern as a signed int,
sign-correct for button ints and floats alike, and the donor binds and types
the member identically.

Fault rate, measured: riostreak.sh ran four consecutive vRIO-live runs --
CLEAN/CLEAN/FAULT/CLEAN, faults only ever in the load window (ticks 42-53k),
clean runs always past launch.  Post-fix tally 8 runs / 2 faults (~25%; was
3/3 before the Torso fix).  Our load window is ~60s where shipped's is ~15s:
if the residual is a constant-rate hazard during load, shipped's expected
per-run rate is only ~7% -- 'shipped survives' may be exposure luck, and the
residual is most likely an emulator-fork serial/DPMI interaction rather than
a game defect.  Next probe: hook the fork's page-fault path on the fixed cr2
and dump recent serial-IRQ deliveries + guest cs:ip history, plus a
shipped-exe streak for the baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 15:44:12 -05:00
CydandClaude Fable 5 8b9c2e6b11 BT410 5.3.70: the full direct-mapping contract audited; the fault is not fully dead
The canopy-eye confirming run (5th post-Torso-fix run with vRIO live) faulted
with the old signature during the load drain -- four clean runs then one
fault.  The two-runs rule keeps earning its keep.

The BT_MAP_LOG audit now prints the whole resolved contract, not just NULLs.
The L4 list carries 13 direct mappings: mapper stick/throttle/reverse/looks
(ids 3/4/6/10-12), Torso torsoCenter (id 14, matching the donor), and
TriggerState (id 19 = the known 0x13 binding) on six weapons.  Every
destination resolves and every size is right -- the 8-byte joystick write
lands in an 8-byte ControlsJoystick, buttons are ints into ints.

One type wart noted, not fixed: MechWeapon binds TriggerState onto
fireImpulse, a Scalar -- a ButtonGroup direct writes an int bit-pattern, so a
real trigger press stores 1.4e-45f.  Size-safe, but the fire FSM will never
read a real button press as a pull; fire worked in tests only via the
BT_FORCE_FIRE hook, which writes a proper float.  The RIO trigger needs an
int home for TriggerState.

Surviving fault theories, in current order: an emulator/DPMI-host serial-IRQ
interaction whose probability guest timing merely modulates (the fault's
constants -- fixed host EIP, fixed cr2, always during the load drain -- fit
this better than the corruption story ever did, since the smash point never
moved across builds where our BSS moved); a second corruption source outside
the direct mappings; sampling noise.  riostreak.sh is measuring the post-fix
rate over four consecutive runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 15:34:31 -05:00
CydandClaude Fable 5 d4f105f42d BT410 5.3.69: the cockpit eye sits in the canopy -- site handling in the walk
RecurseSKLFile now walks "site=" children.  Sites get no draw component --
which is why the walk's 26 nodes matched the real capture all along: sites
were never DCS nodes -- and siteeyepoint spawns the cockpit camera with the
donor's exact construction (bt411 btl4vid.cpp:462, decomp FUN_004579a8):
offset = the site's own local rest transform, parent = the site's PARENT
joint's DCS, not the hull root.  World orientation, torso twist and gait all
arrive through chain composition.  The root-DCS eye remains only as the
zero-construction fallback for skeletons with no siteeyepoint.

Measured on the live pod with the RIO streaming: [eye] cockpit eye on
'jointeye', 26 nodes / 19 objects / 1 eye, and the bridge camera sits at
Y = hull + 7.7 -- the exact chain sum jointlocal 5.29 + hip 0.37 + torso 0.31
+ jointeye 1.69.  canopy_eye1.png is the pilot's view, the mech's own nose
wedge visible below the sightline.  Own-mech cage handling is the next layer.

Also: eye_count threading through ReadSKLFile/RecurseSKLFile, and the [skl]
summary now reports it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 15:18:27 -05:00
CydandClaude Fable 5 8231bee58c BT410 5.3.68: THE LIVE-RIO FAULT IS FIXED -- it and the TM crash were one defect
The [map] audit named it in one run: subsystem 17 = Torso, attribute ids 12/13
unresolved.  The donor's decompiled torso.hpp carries the authentic enum with
binary offsets -- ids 3..15, including StickPosition(9), TorsoUp/Down/
Left/Right(10-13), TorsoCenter(14), MotionState(15).  Our table stopped at
seven entries with MotionState at id 9, on the strength of a comment claiming
the rest were messages.

The streamed control mappings bind BY ID as direct WRITE destinations, so the
truncation produced two different crashes from one cause: in TM mode the
button ids 12/13 resolved NULL (the boot write-fault the moment the joystick
polled), and in RIO mode the analog id 9 resolved to our motionState
StateIndicator -- every analog packet from a live vRIO wrote raw floats over a
watcher-socketed object, and the corrupted chains walked into unmapped memory
~30-40s later.  That was the 'intermittent' pod fault at the fixed DPMI-host
address.  vRIO down = no analog = no corruption, which is why the norio conf
launched: every observation from the whole hunt drops out of this mechanism.

Fix: the full 13-entry donor table; five int command members carved from the
dynamicsState reserve (class size unchanged); ctor zeros them.

Verified, two agreeing runs each way: TM mode launches with a clean audit and
the Thrustmaster joystick driving the torso (stickY=0.907 -> torsoElev 0.349);
RIO mode with vRIO STREAMING launches, zero faults, weapons cycling.
pod_render_rec is no longer poisoned and the norio workaround is obsolete.

The audit-before-the-archive-call pattern (BT_MAP_LOG walks the same streamed
table CreateStreamedMappings consumes, naming what will not resolve) turned a
two-day intermittent-corruption hunt into a one-run lookup.  The streamed RES
tables are binding contracts on our attribute enums.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:41:52 -05:00
CydandClaude Fable 5 f30acc6a94 BT410 5.3.67: pose_chase was upside down -- harness bug, wire exonerated
The operator caught it: the 5.3.65 image showed the mech hanging from its own
ground-shadow plate (-y is up in the bridge world).  The follow-up pinned down
what was and wasn't wrong, and the reconstruction survives untouched.

Right, by three independent wire-level proofs: MAD.SKL offsets are a coherent
Y-up model (hip +5.29, knees descending, torso ascending -- anatomical only
read +Y-up); the BGF vertex extents agree (foot meshes extend -1.0 below their
ankle to the sole, torso +3.8 up to the missile pods); and the SHIPPED pod's
wire carries the same signs (hip +6.21, knees negative, vehicle root a pure
yaw with det +1).  Our wire and shipped's are convention-identical -- no flip
exists between game and board in either game.

Wrong: the ad-hoc chase camera.  It borrowed the aerial up-hint from the
calibrated live-bridge world (where -y is up) and applied it to a raw
scene-space render of Y-up wire data; the symmetric checkerboard ground
disguised the inversion.  The shipped capture rendered upside-down through the
same harness, which is what cleared the wire.

Both conventions now documented in the roadmap: wire/scene space is Y-up in
both games; the live bridge presents -y-up via its calibrated mirror family
(FP_RIGHT_SIGN / CAGE_TWIST_SIGN, COCKPIT-CAGE-NOTES.md).

Tooling: pose_probe.py replaces the throwaway harness (updir=+1 scene space
default, -1 for the bridge sense).  pose_chase_upfixed.png is the corrected
milestone image: the MadCat right side up -- chicken-walker legs, purple knee
actuators, feet planted, shadow under the feet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 10:41:34 -05:00
CydandClaude Fable 5 1233a1d5f6 BT410 5.3.66: RIO-fault eliminations, and TM mode's own NULL-deref surfaced
Three clean eliminations on the live-RIO fault, all on the ack-fixed emulator:
not the ack heuristic (reproduces after the fix), not the debug logging
(pod_render_quiet faults identically with no BT_* env), and not the failed
test-mode handshake (the quiet run's init PASSED and it faulted anyway).

The Thrustmaster isolator -- L4CONTROLS=THRUSTMASTER so the game never opens
the RIO while vRIO keeps streaming into serial1 -- was blocked by its own
discovery: TM mode dies at boot in ControlsInstanceDirectOf<int>::Update+0x6,
mov eax,[eax+0x1c] with EAX=0, at guest 00485E1A.  That address resolves
cleanly in our map and the loader names BTL4REC.EXE CODE 0x75E1A directly: a
real defect in our controls wiring for the non-RIO path, and its own
reconstruction brick.

Standing facts for the hunt: our exe + live vRIO = fault in ~30-40s; shipped
exe + the same stream on the same emulator = fine (two baselines).  The ISR
and LBE4ControlsManager are archive code; ours in the per-event path are the
mapper (MECHMPPR) and the event/receiver plumbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 10:24:22 -05:00
CydandClaude Fable 5 e846717283 BT410 5.3.65: THE MECH STANDS -- wire matrix convention proven from engine source
pose_chase.png: the MadCat assembled from its 19 parts, standing in the arena
at its live articulated position.  Feet, legs, torso, weapon pods, canopy
plate.  Not a collapsed stack at the origin, which is what every prior frame
actually showed.

The convention is no longer inferred -- it is read from the engine.
RootRenderable's ctor writes an entity pose into a DCS with
*(Matrix4x4*)dpl_GetDCSMatrix(myDCS) = localToWorld, so
Matrix4x4::operator=(const AffineMatrix&) IS the wire layout: row-major,
rotation in rows 0-2, zeros in column 3, translation in ROW 3 (entries
12/13/14).

The old 3/7/11 choice rested on two errors, both corrected in the code
comments: AffineMatrix does keep translation at 3/7/11, but because it is
COLUMN-major -- citing it as precedent for a row-major layout read the storage
and ignored the indexer (AFFNMTRX.HPP:99).  And 'the last row made the maths
blow up' came from the contaminated bisect; those crashes were the RIO fault.

Rotation now composes through the engine's own Matrix4x4::operator=(const
EulerAngles&) from the page's pitch/yaw/roll, and the node write uses the
RootRenderable idiom (dpl_GetDCSMatrix + assign).

Also banked: the chase-frame fifobridge variant -- camera framed on the
articulated root instead of the arena bounds -- as the offline proof harness
for any future pose question.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 10:02:06 -05:00
CydandClaude Fable 5 d143f833ab BT410 5.3.64: THE COCKPIT EYE IS LIVE -- root+eye composition, and the emulator
deadlock it exposed

The camera gap root-caused and fixed through three layers:

LAYER 1 (ours): "chain the engine after the skeleton" was a non-fix.  The
engine's MakeEntityRenderables only accepts Object/Rubble resources, so
chaining it with a Skeleton printed "wrong video resource type" and built
NOTHING -- the fifodump proved it: zero vr_flush_dcs_artic records.  The real
fix mirrors the engine's Mover composition (L4VIDEO.CPP:4795-4860) inside our
mech case: a Dynamic RootRenderable (whose ctor seeds its DCS from
localToWorld and whose Execute re-flushes on change -- the ONLY source of
per-frame wire articulation), the skeleton hung UNDER its DCS via ReadSKLFile's
new parent_dcs parameter, and for the inside view a DPLEyeRenderable on that
root with the published EyepointRotation.  Also explains the mystery monolith
in the first frame: the skeleton was parked at the world origin with the
camera inside its shins.

LAYER 2 (1995 library, read from our own linked symbols): dpl_DrawSceneComplete
= velocirender_frameack(0), and frameack's unsolicited-reply path prints
"dpl error - unsolicited input during frame ack", adds the 1995 authors' own
puzzled "flush artic??" when the stray action is 0x1f, and calls exit(9).
Documented before it ever fires.

LAYER 3 (emulator, the actual deadlock): with per-frame articulation flowing
for the first time, the game stopped drawing at frame 232 -- one 0x1f burst
per frame forever, no receives, no error.  The VPX device feeds the frame ack
only after 6 CONSECUTIVE empty polls and reset that counter on EVERY
outputData write; per-frame articulation writes made the count unreachable, so
dpl_DrawSceneComplete never went true.  Fix: the reset is gated on
!frame_outstanding -- once a draw is outstanding the only receive the game
will do next is the frame ack.  The iserver-drain concern the reset guarded is
boot-time only, when no frame is outstanding.

Verified: two agreeing runs of ours on the fixed emulator (611 draws / 222
artic batches in run 2 -- the 232 wall is gone), camera travelling with the
mech (cam -329.7,0,60.5 -> -362.3,0,54.5 across 8s), anim_abs 0 -> 1 on the
bridge, view now INSIDE the cockpit cage.  Shipped binary re-run on the fixed
emulator: launches and runs, no regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 09:46:23 -05:00
CydandClaude Fable 5 af010c18c2 BT410 5.3.63: the mech gets both skeleton and engine renderables
MakeEntityRenderables no longer stops after the skeleton.  It builds ours AND
chains DPLRenderer, which is what creates the RootRenderable and the
DPLEyeRenderable the camera follows.  Verified over three runs:

  [mer] 208 class=3001 view=1 res=1
     [vid] type=1 file='mad.skl'
  [skl] video\mad.skl -> 26 nodes, 19 objects
     [chain] into DPLRenderer, skeleton=yes, stack at 0x12dde7

No fault, mission launches, sim runs.  Worth noting separately: the engine's
eyepoint construction -- prime suspect for the page fault through most of a
session -- runs perfectly here, with the stack at 0x12dde7, right beside the
main loop's frame.  Both are consistent with the corrected finding that the
fault is a live-RIO artifact at a fixed DPMI host address.

The camera still does not move: two frames six seconds apart differ by zero
pixels while [sim] shows the mech travelling from (487,20,288) to (493,20,301),
and the bridge reads cam (0.0, 10.0, 0.0) -- world origin plus eye height.  So
the eye renderable was necessary but not sufficient.

Eliminated by reading our own code rather than guessing: the viewpoint entity
IS the mech (BTL4APP.CPP:354), the video renderer IS linked to it
(APP.CPP:1337), and SetupCull would Fail() loudly if the mech lacked
"siteeyepoint" or EyepointRotation -- neither Fail appears.  That leaves the
path from SetupCull's worldToEyeMatrix to what the bridge reads off the wire,
which is bridge territory rather than reconstruction territory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 08:36:51 -05:00
CydandClaude Fable 5 3c42766f48 BT410 5.3.62: correction -- the fault address was never in our code
Every conclusion that named EulerAngles::operator=(const LinearMatrix&) as the
fault site was wrong, including the disassembly built on top of it.  Three
independent proofs:

1. ROTATION.CPP is ours, so the function was instrumented directly -- print
   this, &matrix and a local's address on the first twelve calls plus any wild
   pointer.  The run faulted with ZERO [euler] lines: never called.

2. After the probe was added, 0x66D9 disassembles to a two-byte conditional
   jump (jnl) that touches no memory.  The dump reports ErrCode 0004, a data
   READ.

3. EIP 0x66D9 and cr2 0x7000FA64 are identical to the byte across every build
   this session, including builds where the code at that offset changed
   completely.  A fault in our CODE segment moves when the code moves.  This
   one does not -- it lives in the DPMI host, which loads low and never gets
   relinked.

The map lookup was a coincidence: our _TEXT is section-relative from 0, so its
offsets overlap the host's low addresses, and the map will name a function for
any small number.  Before resolving a fault address against the map, confirm it
belongs to our segment -- cheapest check is whether it moves on relink.

What is actually true, by same-binary A/B:

    vRIO DOWN, serial1 still a namedpipe -> mission LAUNCHES
    vRIO UP,   same conf, same binary    -> FAULT

The trigger is a LIVE RIO stream, not an unplugged cable.  The emulator reports
continuous serial1 RX overruns, the fault lands wherever the app happened to be
(terrain one run, the mech's inside view another), and the shipped binary
survives the identical conditions.  A fault at a fixed host address, at
arbitrary points in the app, requiring an interrupt-driven stream, is an
interrupt re-entrancy signature rather than a wild pointer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 08:22:15 -05:00
CydandClaude Fable 5 d5ae9c4851 BT410 5.3.61: the camera never moves -- our mech case skips the engine's eyepoint
Two frames captured seconds apart are pixel-identical while [sim] shows the
mech travelling from (180.4, 10, -358.7) to (-604.8, 0, -352.4), with the
bridge title reading cam (0.0, 10.0, 0.0) throughout.  The scene renders, the
eyepoint is never driven.

Our own MakeEntityRenderables explains it.  When ReadSKLFile finds a Skeleton
entry it sets handled = True and the chain to DPLRenderer::MakeEntityRenderables
is skipped -- but that is the call which builds the RootRenderable and the
DPLEyeRenderable the camera follows (L4VIDEO.CPP:4548-4566).  We hand the board
a skeleton with no root and no eye.

That also closes the loop on the fault: the eyepoint construction we skip is
the same code that faults when it runs.  The mech case must end up doing BOTH,
so the EulerAngles::operator=(const LinearMatrix&) fault has to be understood
before the mech can be seen from its own cockpit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 23:34:10 -05:00
CydandClaude Fable 5 9c959e6891 BT410 5.3.60: first rendered frame from a running reconstructed mission
emulator/render-bridge/first_mission_frame.png -- arena city, textured
buildings, horizon, 28fps, captured from the GL bridge while our build ran a
live mission with the mech walking.  The skeleton went in with it:
[skl] video\mad.skl -> 26 nodes, 19 objects.

Corrects yesterday's RIO framing.  I called serial1 an unplugged cable.  Wrong
twice: VRio.App is running on this rig (the dev-rig default since 7/17), and
the emulator log recorded real byte counts on that port -- RX overruns of 471,
503, 528 -- which an unconnected pipe cannot produce.  The port was LIVE and
streaming during every crashing run.

So the defect is not that we mishandle a disconnected port, it is that we
mishandle a live RIO stream, which is worse: every production pod has a real
RIO.  The shipped binary on the same rig logs 'lost RIO analog request', keeps
sending characters, and runs the mission; ours logs 'RIO never came back from
test mode!' and dies partway through the load.  The overrun counts say the
guest is not draining the port fast enough.  L4RIO.CPP's handshake is authentic
archive code, but MechRIOMapper (BT/MECHMPPR.CPP) is ours -- that is the thread.

pod_render_norio.conf stays the way to get a running mission today, but it is a
workaround: it disables the cockpit's primary input device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 23:33:08 -05:00
CydandClaude Fable 5 475ae3106e BT410 5.3.59: A MISSION RUNS -- the RIO serial port was blocking every load
pod_render_norio.conf is pod_render_rec.conf with one line changed,
serial1=disabled instead of the vrio named pipe.  Same binary, same egg.  With
it, our build launches and the mech walks:

  [launch] state=2 minPriorityEmpty=1 ticks=85316
  [queues] p0=- p1=- p2=BUSY p3=- p4=-
  BTL4Application::RunMissionMessageHandler
  Turning Plasma Score Display On
  [sim] pos=(180.466,10,-358.703) yaw=-0.275605 spd=14.4
  [sim] pos=(251.959,10,-250.498) yaw=-0.89103  spd=14.4

No fault, 290 log lines and counting.  This is the first time the
reconstruction has reached a running mission on the pod.

The hang was never starvation, a deadlock, or a refill loop -- it was VOLUME,
the first candidate I listed and then talked myself out of.  530 renderer
events queue at priority 0 during load; BackgroundTasks::Execute runs exactly
one task per call round-robin over seven tasks, and the 1ms frame budget is
always blown here so no extra background passes happen.  That is ~9 events per
real second, so a load legitimately takes ~60s.  The fault arrived at ~40s,
before the backlog could clear.  Every 'the queue never drains' reading was
really 'the process dies before it can'.

The disproof was already in hand: the post tally climbed to 530 and went FLAT,
which means nothing was refilling.  p0=BUSY with nextReady=1 is equally
consistent with a large finite backlog still draining, and I read it as refill.

Why the RIO port: the pipe has no server attached, which should behave as an
unplugged cable.  The emulator log shows steady serial1 RX overruns, our build
prints 'RIO never came back from test mode!', and the SHIPPED binary prints
'lost RIO analog request' and launches anyway on this identical rig.  So an
unplugged RIO is survivable and our handling of it is not -- a real defect, not
a rig artifact, since a pod with an unplugged RIO cable should still boot.

The wild reference at EulerAngles::operator=(const LinearMatrix&)+0x19 is still
unexplained, but it is now reproducible on demand by enabling serial1 rather
than being a coin flip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 23:23:29 -05:00
CydandClaude Fable 5 d3b332a62a BT410 5.3.58: the load gate is blocked by a REFILL loop on priority 0, not starvation
Per-priority occupancy, measured every second until the fault:

  [queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1

That single line kills the two leading theories.  p3/p4 empty means nothing
above priority 0 is competing, so the pump is free to serve it -- no
starvation.  nextReady=1 means PeekAtNextEvent always has a READY event, so
priority 0 is not holding a timed event whose alarm never comes due -- no
deadlock.  What remains is refill: priority 0 is replenished as fast as the
pump drains it, ~143 events per simulated second.

It is also fully deterministic.  Two runs of the same binary reported
pump=352/1499 at frame 1001 and 495/2643 at frame 2002, identical to the byte.
So 'crashes about half the time' was never a race -- it was comparing runs that
differed in binary or conf.

Only InterestManager::PostRendererEvent posts at priority 0 (it maps every
renderer event there while the app is not RunningMission), so naming the
message names the flooder.  Application::Post now tallies priority-0 posts by
message ID and reports the busiest three.  Notably, all three producers of
NotifyOfNewInterestingEntity are entity-CREATION paths, so if that ID dominates
then something is creating entities in a loop -- which would explain the
unbounded growth behind the fault as well as the hang.

pod_render_noskl.conf now carries the same probes, so one instrumented binary
can be run with the skeleton walk on and off.  Walk-off runs are the only
configuration of ours known to reach 'Turning Plasma Score Display On'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 22:20:11 -05:00
CydandClaude Fable 5 4cf09917ce BT410 5.3.57: the crash is deterministic, and the real blocker is the load gate
The previous commit's attribution was wrong and is corrected in the roadmap.

WRONG: 'the crash is the skeleton walk's' rested on 0 crashes in 2 runs with
the walk disabled -- a 25% coin flip presented as evidence.  The oldest
preserved dump has no [skl] line and ends on the old 'couldn't figure out how
to MakeEntityRenderables' fallback, so it predates the walk and faults
identically.

WRONG: 'it lands at different points each time'.  Every dump carries the same
numbers to the byte (cr2 7000FA64, EIP 66D9).  What varies is how far the log
gets, not where the fault is.

Resolving the address through btl4opt.map names it exactly:
EulerAngles::operator=(const LinearMatrix&)+0x19, a read through the matrix
reference.  A binary scan finds all three call sites of that operator pass a
stack local, so the 1.79GB pointer still has no static explanation -- that is
now its own open item rather than a guess.

Two theories killed cleanly by the new BT_STACK_LOG probe and a binary scan:
ESP drift (drift=0 over 2002 frames; exactly one callee-cleans function exists
in the whole binary) and an undersized stack (our PE and the shipped one have
identical 1MB/8K geometry).

What the probe found matters more: the application is parked in state=2, which
is LoadingMission, not WaitingForLaunch.  Priority 0 is where the interest
manager queues renderer events during load, the gate needs that priority empty,
and it never empties -- so the mission never launches and the renderer holds a
blank screen by design.  The fault arrives ~30s into that wait.  Runs that DO
launch never print a single [launch] line.  So 'crashes half the time' and
'hangs during load' are one event seen twice.

A 15x disagreement between two clocks looked like a reconstruction slip --
BTL4.CPP passes GetTicksPerSecond() where ApplicationManager wants a frame
rate.  Checked against the shipped binary before touching it: same instruction
sequence, same kind of static float pushed.  Authentic.  Documented so nobody
'fixes' it.

Also swept every subsystem DefaultData against its real C++ base.  Fifteen
chain past their immediate parent, but fourteen skip only classes that add no
handlers and no attributes, and no class's attribute-ID base disagrees with its
index chain -- so there are no gap slots there.  The one real defect: Generator
is a HeatSink but chained to Subsystem::MessageHandlers, so it ignored every
ToggleCooling message.  Fixed (compiles next build).

Tooling: podrun.sh stages the build over BTL4REC.EXE, exports the host-side VPX
board env -- without which the run dies at the iserver handshake rather than
merely rendering nothing -- and archives every run's log, marking it -CRASH
when it faulted.  Before this the only preserved dump was an accident, on a rig
where each run costs four minutes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 21:41:38 -05:00
CydandClaude Fable 5 f54e1a3c19 BT410 5.3.56: the intermittent pod crash is the skeleton walk's -- attributed by same-binary A/B
Added BT_NO_SKL, which skips the skeleton build so ONE executable can be run
with and without it.  That is the only clean way to test an intermittent
fault, and it settles attribution:

  walk enabled   ~50% of runs die (page fault at 66D9) at DIFFERENT points
  walk disabled  0 crashes in 2 runs
  shipped exe    0 crashes in 2 runs, same rig and conf

So it is our new code, not the rig, and the varying fault point points at
memory corruption surfacing later rather than a bad instruction in the walk.

Hypotheses killed by reading the private library headers: the matrix array is
the right size (dpl_MATRIX is float32[4][4]); and neither s_dplobject nor the
common dpl_node header retains an object name, weakening the dangling-string
theory (a private name cache inside the library is still possible and is the
best surviving suspect).

Also checked something that would have been much worse than a crash:
~NotationFile REWRITES its file when dirty, and these are the game's shipped
.SKL data files.  MAD.SKL is untouched, and ReadSKLFile now carries the
engine's own Verify(!IsDirty()) guard.

Next tests are listed cheapest-first in the roadmap: keep the NotationFile
alive, copy the Object= names, count board allocations, and re-check whether
MakeEntityRenderables runs twice per mech.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 20:45:28 -05:00
CydandClaude Fable 5 15d509c8a8 BT410 5.3.52: frame rate measured properly -- the missing mech skeleton is the real gap
The pod updates every few seconds.  Measured by sampling a head window every
2s (emulator/render-bridge/headrate.py): OURS changed in 5 of 12 samples,
SHIPPED in 1 of 12.  So the reconstruction is not slower than the shipped
binary -- the emulated board is just expensive.  Caveat recorded with it:
ours was being driven by the throttle hooks while the shipped exe ignores
them and sat parked, so treat those as same-order, not a win.

What did cost us 3x was mine: BT_MECH_LOG/BT_LAUNCH_LOG write per-frame lines
and DEBUG_STREAM=cout is redirected to COM3, so every one goes through an
emulated serial port.  Turning them off took the wire from 480 to ~1480
bytes/sec.  pod_render_quiet.conf is the conf to use for timing work.

And a correction to my own earlier framing: wire bytes/sec is NOT a frame
rate.  Shipped pushes ~8900 B/s against our ~1480 and the difference is
CONTENT, not speed -- shipped submits the mech and we do not:

    OURS:    L4VIDEO.cpp wrong video resource type for object mad.skl
    SHIPPED: (no such line)

The mech's model resource is a SKELETON.  The engine's default
MakeEntityRenderables accepts only Object/Rubble and rejects anything else
with exactly that message, because skeletons are the GAME renderer's job.  So
our arena renders but the MECH IS ABSENT, and with it the cockpit interior --
one unimplemented path explaining both the missing model and the lower wire
volume.

Next brick is now precisely scoped: answer MechClassID by reading the .SKL
notation pages into a dpl_DCS hierarchy and hanging the per-node .BGF
geometry off it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:04:35 -05:00
CydandClaude Fable 5 09192367c0 BT410 5.3.51: THE 3-D WORLD RENDERS -- reconstruction to emulated board to a picture
emulator/render-bridge/first-3d-frame.png: the arena from the pod -- sky,
horizon, ground, the arena structures along the skyline -- at ~31fps on the
VelociRender bridge.

The whole chain now works in our build: MakeVideoRenderer ->
BTL4VideoRenderer over DPLRenderer -> board boot -> Renderer::LoadMission ->
DPLReadEnvironment for the art paths -> 40/40 arena objects loaded -> mission
launch -> per-frame submission over the wire.

The mistake worth remembering: the run was never stalled after
InitializePlayerLink.  I called that from a 150-second sample and it was just
too short.  CheckLoadMessageHandler reposts every second and will not advance
until the min-priority event queue drains, and with the video renderer in the
mission that takes minutes.  A BT_LAUNCH_LOG trace showed the queue draining
and then both RunMissionMessageHandler calls, the plasma display, and the
first sensor tick -- matching the shipped binary line for line.  The renderer
deliberately holds a blank screen until RunningMission (L4VIDEO.CPP:5100), so
'black' was the app still loading, not a rendering bug.

Zero unbuildable entities, zero geometry failures.  The frame is static only
because the mech is parked with no input, and the camera in the bridge title
is the bridge's own viewer, not the game's eyepoint.

Also banked the launch-gate trace (BT_LAUNCH_LOG) behind an env var.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:30:20 -05:00
CydandClaude Fable 5 060078e4ae BT410 5.3.50: geometry loads -- the bring-up no-op was suppressing the base
DPLRenderer::LoadMissionImplementation (L4VIDEO.CPP:6007) is not empty: it
calls DPLReadEnvironment (which opens L4DPLCFG/btdpl.ini and sets the dPL
object/material/texmap paths from the objectpath= entries) and then
LoadNameBitmaps.  The 'bring-up no-op' installed earlier therefore did not do
nothing -- it REPLACED that, so the loader never learned where the art lives
and every dpl_LoadObject returned NULL.

I justified that no-op by pointing at VideoRenderer's bare Tell and
GaugeRenderer's identical one; neither is our base.  Check the ACTUAL base
before overriding in this engine and chain it unless there is a reason not
to.  DPLReadEnvironment is private to DPLRenderer, so chaining is the only
way a game renderer can reach it at all.

  before: 40x 'couldn't load object', 'NULL instance', run ends,
          wire ~24 bytes/sec
  after:  0 failures, run continues, wire ~12 KB/s (1.17MB climbing),
          bridge live at 88fps

Proved it was ours and not the rig by running the SHIPPED exe under the same
conf: it loaded every object.  That A/B is cheap and is the right first move
whenever the pod misbehaves.

Still black: the bridge camera sits at its default (0,10,0), so the wire
carries state and object loads rather than a populated scene.  The mech and
arena entities still need MakeEntityRenderables bodies.

Rig improvements, both from the user: nosound is fine on the SLOW clock (it
is the FAST SOS clock that needs the AWE32), which saves ~4 min and ~300MB of
audio taps per run; and serial3=file + '> COM3' gives a live unbuffered log,
so a run that does NOT crash is finally readable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 09:09:04 -05:00
CydandClaude Fable 5 32023a918f BT410 5.3.48: watcher ladder complete; the engine names the next brick
With every authored watcher resolving, the pod run reaches the mission, draws
the FULL COCKPIT with the board booted and audio running, and exits GAME-RC=0
with no Fail.  It builds no 3-D scene, and the engine says why:

  Entity 1:1 class3035 couldn't figure out how to MakeEntityRenderables
  L4VIDEO.cpp couldn't load object sky.bgf  (then the whole arena)
  NULL instance

VideoRenderer::MakeEntityRenderables (VIDREND.CPP:231) is the BOTTOM of a
virtual chain -- its comment says so outright -- and BTL4VideoRenderer does
not override it, so every entity falls through, no scene graph is built, and
the geometry that would hang off it never loads.

CORRECTS an earlier note in the roadmap: I recorded the first full-rig run as
proving '.BGF loading was never a reconstruction problem, only a missing
bridge', because the complaint count was zero.  It was zero because the run
Failed at a watcher long before reaching geometry.  Now that it gets there,
the loads are attempted and fail -- for want of renderables, not the bridge.

Next brick is the real btl4vid body, shape pinned by the surviving sibling
header RPL4VID.HPP: override MakeEntityRenderables, plus ReadSKLFile /
RecurseSKLFile to walk the .SKL skeleton pages into a dpl_DCS hierarchy and
load the .BGF geometry per node.  Renderable content from the BT411 donor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 08:22:44 -05:00
CydandClaude Fable 5 14da9489c2 BT410 5.3.43: full pod rig -- geometry loads, ladder advances to the audio watchers
Under the documented rig (launch_pod.ps1 with the GL bridge up) the 'couldn't
load object' count went to ZERO.  .BGF loading was never a reconstruction
problem, only a missing bridge.

What blocks now is a series of authored AttributeWatchers: BTL4.RES binds
them BY NAME and the engine Fails outright on any that does not resolve, so
each is a name our subsystems must publish.  Five rungs climbed, each
run-verified: UnstablePercentage, SpeedEffect (Myomers table), AnimationState
+ CollisionState/CollisionNormal + ReduceButton (Mech), and the full Torso
table (all six authentic names mapped onto existing members).

The method that makes this cheap is banked: the shipped binary's string pool
carries each class's attribute names CONTIGUOUSLY in ID ORDER after the class
name, and in every case so far our member declarations sit in the same order
-- which confirms the layout and lets ids be pinned rather than guessed.
Intersecting those names with BTL4.RES gives the exact work list.

A THIRD miswired SharedData found on the way: Myomers carried Subsystem's
tables where it derives from PoweredSubsystem, the same defect as
MissileLauncher (5.3.33).  Worth a sweep across every subsystem.

Staged member TYPES are provisional and documented as such: AttributeWatcherOf
reads *(T*)attributePointer and the instantiation comes from the resource,
which the string pool does not reveal.  Nothing drives them yet, so settle the
types with the models that write them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 01:03:36 -05:00
CydandClaude Fable 5 740ef2b0a7 BT410 Phase 5.3.32: the per-head A/B -- three engineering heads now exact
The gauge framebuffer is PLANE-PACKED: L4GAUGE.CFG gives each port a bit
mask, not a rectangle, so all six heads share the same x/y and are separated
only by bit -- the packing the VDB splits into the pod's physical screens.
Comparing RGB was therefore measuring nothing useful; the "magenta artifact"
just meant the wrong heads were lit at that pixel.

New instruments (emulator/render-bridge/gauge-ab/, with a README):
  planes.py  per-head scoreboard -- shipped vs ours, missing vs extra, per
             port mask, recovering the 16-bit word from DOSBox's RGB565
  BT_VIS_LOG a complete cockpit widget map from the single dispatch every
             gauge widget is built through (MethodDescription::Execute),
             printing name + port + authored position
  ab.sh      stages the fresh build over BTL4REC.EXE and kills any running
             DOSBox before launching -- see below

The fix: the Comm page's pilot roster is the POD roster (the viewpoint mech's
ControlsMapper pilot array, mapper attributes 15/16), not the mission's
player list.  It is zero in a solo mission, so the shipped page draws empty;
ours resolved the local player and painted its icon in colour 0xff.  The gate
belongs in the row source, not PilotList::Execute -- Execute's empty branch
still has to run, because erasing is what the shipped page draws.

  head    shipped    ours   missing   extra          (was)
  Eng1      17981   17981         0       0     (0 / 1030)
  Eng2      17981   17981         0       0
  Eng3      17981   17981         0       0
  Comm      15656   13557      2099       0     (2099 / 1253)
  Heat      21374   20913       566     105     (566 / 990)

Title-band magenta 404 -> 0.  The 2099 Comm pixels we are missing were
missing before the gate, so it is a strict improvement.

Method note, which cost more than the bug: the rig's confs run BTL4REC.EXE
out of the mount while the build writes build410/btl4opt.exe, and nothing
connected the two.  Three measurements ran an hour-old binary, so a change
that "did nothing" three times had never been tested -- and the correct first
hypothesis was discarded on that evidence.  ab.sh now re-stages every launch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:04:43 -05:00
CydandClaude Fable 5 42ff5c3856 BT410 Phase 5.3.29: the panel titles -- 94.8% identical to the shipped cockpit
Reading the art settled where the titles live: qsensors/qmyomers/qermlas/
qppc/qstrk6.pcc are 299x26 TITLE BANNERS, while the qblh0..7.pcc strings
the cfg hands vehicleSubSystems are 77x120 MECH ICONS (already lit by the
5.3.28 placement fix).  So the authored auxScreenLabel IS the panel title
-- it just belongs on the MFD panel, not only the ENG page.

SubsystemCluster now builds a titleBanner child from the cached label.
Placement was MEASURED against the shipped framebuffer through the A/B rig
(BackgroundBitmap places by left/BOTTOM): a bring-up BT_TITLE_DY knob, then
a green-pixel correlation over the title band, then baked at x+0x09,
y+0xb3 from the panel origin.  "SENSOR CLUSTER", "MYOMERS", "TYPE II 65
TONS" and both "ERMED LASER RANGE 500M" banners now render where the
shipped binary puts them.

Score progression vs the shipped cockpit on identical GAUGE content:
  5.3.26 birth            90.1% identical / 82% coverage
  5.3.28 authored screens 93.2% / 85%
  5.3.29 titles tuned     94.8% / 91%

Also: grab.ps1 now focuses the target window before capturing -- the
screen-copy grabbed whatever was in front, which cost two junk captures
(a white frame and an unrelated 3-D app window).

Gauge fight 11/11 rounds and smoke both clean, zero faults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 23:09:15 -05:00
CydandClaude Fable 5 24ea06574c BT410 Phase 5.3.28: the visual A/B pass -- 93% pixel-identical to the shipped cockpit
Ran the reconstructed exe and the shipped ALPHA_1 binary against the SAME
GAUGE content, same mount, gauges on, and diffed the 640x480x16
framebuffers.  OUR EXE DRAWS THE COCKPIT: 90.1% pixel-identical on the
first run, and every difference traced to a single root cause.

The authored aux-screen block (screen number / background placement /
label) lives in the subsystem resource -- which is freed with the Mech
ctor's stream buffer (the 5.3.24 use-after-free landmine).  Both this tree
and the BT411 port had stubbed it, which is why BT411's displays show the
same artifacting.  PoweredSubsystem now caches the block at ctor time with
accessors, and btl4gau2 uses the AUTHORED screen instead of a roster-order
stand-in and builds the placement strip art.  Panels snapped onto their
authored screens -- weapon dials now land on the same panels as the
shipped exe, mech icons appear on the sensor/myomer panels, generator
letters correct -- taking the match to 93.2% identical / 85% coverage.

Remaining differences are donor-inherited stubs: the panel title art (now
proven to come from the q-strip statusImage path, not auxScreenLabel --
the label blit is wired and guarded regardless), the pilot name bitmap,
and some lamp frames.

The A/B rig is banked at emulator/render-bridge/gauge-ab/ (both confs +
the window-grab script) so the comparison is repeatable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:29:01 -05:00
CydandClaude Opus 4.8 844c11f792 emulator: in-fork plasma display (serial2=plasma) window in the explode layout
Bring the pod's 128x32 plasma readout in-fork, completing the self-contained
glass cockpit -- no vPLASMA process, no pipe.

- serialplasma.{cpp,h}: CSerialPlasma, a serial<n>=plasma backend that parses
  the game's ESC P graphics stream (L4PLASMA streams whole changed rows;
  receive-only, no replies) into a 128x32 1bpp framebuffer. Transcribed from
  vPLASMA's PlasmaProtocol + VPlasmaDevice (graphics path); the factory
  text-mode/fonts aren't used by the game and aren't rendered, but their escape
  operands are still consumed so the parser can't desync. Seam PLASMA_GetFrame()
  for the renderer.
- vpxlog.cpp: draw the framebuffer as an amber-on-black window in the explode
  layout, lazily created once a plasma port exists, parked centered under the
  lower-left MFD (VPX_PLASMA="x,y,w,h" overrides); WS_EX_NOACTIVATE so it can't
  steal DOSBox focus.
- Register SERIAL_TYPE_PLASMA (serialport.h/.cpp, dosbox.cpp -- documented in
  the vpx-device README step 3d, since those stock files live in the git-ignored
  src tree).
- Confs: serial2 in the _rio confs + deploy templates switches from
  namedpipe pipe:vplasma to the in-fork plasma.

Live-validated 2026-07-24. Real pods keep directserial realport:COM2; the
standalone vPLASMA app keeps namedpipe pipe:vplasma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:48:07 -05:00
CydandClaude Opus 4.8 73b597df35 emulator: in-fork virtual RIO cockpit board (serial1=rio)
Emulate the cockpit RIO board natively inside the DOSBox-X fork, driven by
an SDL game controller + host keyboard -- no external vRIO process and no
named pipe. The game<->board round trip runs in-process at the emulated
UART's own cadence, so the serial round-trip dropouts (livelock, TXMAXIDLE,
rxburst, 15s retry) can't occur.

- serialrio.{cpp,h}: RIO 9600-8N1 device state machine + protocol codec +
  input mapping, transcribed from the vRIO app (VRioDevice + Protocol +
  InputRouter/BindingProfileFormat), minus transport/pacer/UI/locks.
  B0 = gamepad (5 axes + joystick column). B1 = keyboard field: MFD banks on
  the letter rows, F-keys = Secondary/Screen, numpad = flight controls,
  LShift/LCtrl throttle slew; PAUSE/ScrollLock toggle panel<->DOS; vRIO-
  grammar bindings file via bindings:/VWE_RIO_BINDINGS.
- vpx-device/README.md: device notes + apply steps (the DOSBox-X src tree is
  git-ignored, so this dir is the tracked source of truth; the three sdlmain
  keyboard-hook edits are documented there).
- net_{loop,rp}_rio.conf + deploy templates; render-bridge/
  gauge_arena_rio_sound.conf (standalone -egg trim); pod-launch --rio
  (mutually exclusive with --pipe).

Additive: real pods keep directserial realport:COM1; vRIO-over-pipe keeps
namedpipe pipe:vrio. Validated live 2026-07-22 (pad + keyboard field +
console networking) on the dist install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:43:41 -05:00
CydandClaude Opus 4.8 b447e71864 launch_live.ps1: enlarge exploded cockpit windows +25% (0.75 scale)
Per user request, bumped the cockpit MFD/radar windows from 0.6 to 0.75 of
native 640x480 (MFD 384->480, radar 288->360 logical). Live-verified on the
secondary: MFDs now render 331x266 (was 267x218, ~+24% after the primary's DPI
downscale), radar 251x346, all six still fit the 1707x1067 secondary with
margin, renderer unchanged on the primary. No wire stall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:37:43 -05:00
CydandClaude Opus 4.8 20dd8440d3 launch_live.ps1: -Explode puts the cockpit VDB windows on the secondary display
Adds -Explode: the pentapus cockpit instruments (radar + 5 mono MFDs, the VDB
video-splitter decode) render on the SECONDARY monitor while the reconstructed
i860+GPU 3D renderer stays the main out-the-window view on the primary. The
window rects auto-fit to whatever the non-primary display is (detected via
System.Windows.Forms.Screen, so it survives a monitor rearrange), scaled to
~0.6 and offset into that display's virtual-desktop region; the DOSBox SVGA
screen parks clear of the cluster.

The cockpit windows live in vpxlog's rt_main thread, which only starts under
VPX_RENDER=1 -- the same flag whose DOSBox-thread scene-graph decode previously
stalled wire delivery (fixed earlier by removing it). Feared this would bite
again; MEASURED and it does NOT: with VPX_EXPLODE + VPX_NOMAIN the heavy main-
window 3D render (rt_draw, gated on the main window existing) is skipped, so
rt_main only does the cheap per-tick MFD/radar blits and the DOSBox thread keeps
up. Live-verified: all 6 cockpit windows placed on the secondary (DISPLAY1),
renderer on the primary, wire advancing normally to cmd 23k+ (no command-8
stall), frames flowing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:25:47 -05:00
CydandClaude Opus 4.8 ef7db9f2e4 Fix live-render lag: bulk memory read + masked texture compositing (~2.1x)
User-visible bug: connected the reconstructed renderer to the REAL running pod
for the first time (M4d demo) and the render fell increasingly behind the live
game, plus looked near-grayscale. Root-caused with direct profiling instead of
guessing:

1. VPX_RENDER=1 was set in launch_live.ps1 (copied from launch_pod.ps1's
   defaults without thinking it through) -- it starts vpxlog.cpp's OWN native
   GL render thread (rt_main), which contends with our renderer for the same
   CPU/GL resources. Live-caught: with it set, wire delivery stalled at wire
   command 8 for 90+s; removing it, the same mission reached command 21,488
   in the same window. This was the dominant cause of the visible lag.

2. gpu_raster.build_prims() scanned the whole per-draw program window
   (0x08158000-0x08170000, ~24k words) via a Python-level r32() call per word
   every frame. Added emu860c.dump_range(lo,hi)->bytes (one bulk C read) and
   rewrote build_prims to index a numpy array instead -- verified byte-identical
   dump_range output first, then reconfirmed the whole render is STILL
   bit-identical to the CPU reference (frames 0/5/11, differ>24 = 0.000%).

3. Profiling after (1) showed the per-texture compositing loop was the real
   remaining cost: 62ms/frame doing full-832x512-frame fancy-indexed texture
   sampling for EACH of 13 textures, regardless of how few pixels use each one.
   Added dpl_sampler.composite_masked (sample+composite only the pixels a
   texture actually covers; verified equivalent to composite() on a partial
   mask in dpl_sampler's own conformance test) and hoisted the invariant
    out of the loop.

Combined: render time 95.8ms -> 54.9ms/frame (~1.75x from this change alone,
~2.1x vs the original ~117ms/frame baseline), still bit-identical to the CPU
reference throughout.

launch_live.ps1: removed VPX_RENDER, renamed the automatic-variable shadow
 ->  (flagged by PSScriptAnalyzer), added -Pin <fifodump> to
pre-load the full texture set (bit-identical to offline) instead of the
incremental default, for a demo that wants correct color immediately rather
than waiting on the live mission's own upload order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:07:55 -05:00
CydandClaude Opus 4.8 a689bce660 Fix launch_live.ps1: don't enable Dave's native render thread
VPX_RENDER=1 was copied in from launch_pod.ps1's defaults without thinking it
through -- it starts vpxlog.cpp's OWN native GL render thread (rt_main),
which is redundant with (and contends against) our reconstructed renderer
riding the FIFOSOCK tee. Live-caught: with it set, wire delivery stalled at
command 8 for 90+s (window rendered, but slowly/poorly per live observation);
removing it, the same mission reached command 21,488 (10 frames) in the same
window. My two earlier successful ad-hoc bring-up tests had already removed
this env var by hand -- the script just hadn't caught up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:53:58 -05:00
CydandClaude Opus 4.8 7b1866a5cb M4d: real-pod live bring-up WORKS + namedpipe serial (retire realport)
The reconstructed renderer now runs live behind the REAL game: dosbox-x.exe
(arena mission) streams its wire over vpxlog's VPX_FIFOSOCK and live_render.py
renders it in real time -- 18-20 live frames from the actual running game,
coherent perspective-correct arena. The real binary opens the FIFOSOCK listener
at device init and accepts our client (verified standalone first).

launch_live.ps1: turnkey one-command bring-up (lean pod + reconstructed renderer
on the socket, no pentapus/bridge; -Sound / -Port / -PodOnly / -Present).

Serial: switch the render-bridge confs from 'directserial realport:COM1/COM2'
(com0com retired, COM1/COM2 gone) to the fork's namedpipe backend
(serial1=namedpipe pipe:vrio, serial2=namedpipe pipe:vplasma). DOSBox is the
pipe client; the vRIO/vPLASMA apps are servers; an unconnected pipe = unplugged
cable so the mission still renders. Live-verified: both pipes CONNECTED to
running servers (Serial1 -> \.\pipe\vrio, Serial2 -> \.\pipe\vplasma), RIO
input flowing, game rendered faster than the COM-less run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:33:57 -05:00
CydandClaude Fable 5 1323397a50 pvision solved (texture-value ramp) + i860/hardware reverse-engineering
Predator/IR vision: reverse-engineered from the original firmware and
confirmed by the build team -- it is the Division board's TEXTURE-VALUE RAMP
mode (a "check your texture maps" diagnostic the devs hijacked), NOT a
grayscale squash or a false-colour palette. Located in VREND.MNG (effect
handler @0xe6c0, wire action 0x1b, type -1 ON / -2 OFF); ramp colours from
VR_DRAW.C. Renderer reworked to match: vrview_gl now does the 4-ramp
lerp(color0,color1,luminance(texel)) in the mesh pass (grayscale+defog
removed). Live-rendered on a new night-clear arena egg; crew A/B verdict
pending.

Firmware-decomp toolchain (emulator/firmware-decomp/), all built from the
project's own artifacts and validated:
- coff860.py    i860 COFF reader (symbols/sections), names match AS860 source
- derive860.py  derives the i860 opcode map from matched .S<->.O pairs
- dis860.py     i860 disassembler (98% on clean ground truth; proven on
                VREND.MNG -- velocirender_statistics decodes correctly)
- sigmatch860.py reloc-invariant signature matcher onto the stripped image
- i860-encoding.md / FIRMWARE-SYMBOLS.txt / README.md

PVISION-IMPLEMENTATION-GUIDE.md: self-contained hand-off for the BT411 team.

HARDWARE-ARCHITECTURE.md + hardware-photos/ (15 board shots): the Division
VelociRender card is a 2-board stack driving a 3-processor pipeline --
INMOS IMS T425-J25S (comms/control, runs vrendmon.btl) + Intel i860 XP-50 (FP
geometry, runs vrender.mng) + Division PXPL IGC 5.2 ASIC with ~48x PXPL EMC
5.1 (UNC Pixel-Planes-5 SIMD array; "EMC" = the firmware's configEMCs) +
Analog Devices ADV7150 RAMDAC + NTSC. Plus the VWE Video Distribution Board
(P/N 1404: AMD MACH130 + 3x Brooktree Bt477) for the 3-VGA-head cockpit split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 22:29:53 -05:00
CydandClaude Opus 4.8 5593d57d48 Renderer: IR/thermal palette, bilinear default, searchlight (fog) + night egg
- IR / predator-vision thermal: the cockpit IR button toggles it via the
  wire (dpl_Effect on action 0x1b, mode -1 ON / -2 OFF -> board.pvision).
  vrview_gl present pass remaps scene luminance to a thermal palette
  (default heat/Predator; mono|green|amber via VRVIEW_PVISION_PALETTE;
  VRVIEW_PVISION / bridge 'v' key force it on). Exact palette + HUD-exempt
  pending crew review.
- Texture filter default flipped to bilinear (operator preference); the
  i860 board itself point-sampled, so VRVIEW_FILTER=nearest reverts. (A CRT
  present-pass bleed/scanline prototype was built and rejected; removed.)
- Searchlight: no code needed -- night A/B proved it is a VIEW-FOG push-back
  (fog near 5->60, far 400->500 on), already rendered by the existing view
  fog path. Not a light cone. Added TESTNITE.EGG (arena1/night/fog) +
  gauge_arena_night_pipe.conf for the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:49:34 -05:00
CydandClaude Opus 4.8 aadda7f9a5 Cockpit camera complete + namedpipe transport + 832x512 native res
Render bridge (live_bridge.py, vrview_gl.py):
- Hat glances render (left/right frame the canopy, hat-down = clean rear).
  Root bug was a stale _ckpt['fix'] key -> KeyError every glance frame ->
  render aborted (screen froze on hold, snapped back on release). The glance
  itself is the authentic eye-DCS action-0x1f reflush fp_cam already applies.
- Torso twist turret-true (root-axis yaw, zero parallax); lasers follow torso.
- Rear glance drops the canopy shell for a clean view (original-hardware
  behavior); mission-fade shroud 9fd hidden.
- Wireframe debug mode (VRVIEW_WIREFRAME / 'w' key), scene-pass scoped.
- Renderer output = 832x512, the dPL3 board's native framebuffer res.

DOSBox-X fork: namedpipe serial backend (serialnamedpipe.cpp/.h) for
vRIO/vPLASMA, replacing com0com; overlapped non-blocking I/O; typed frames
(0x00 data / 0x01 DTR+RTS). Tracked copies + apply steps in vpx-device.

Docs: COCKPIT-CAGE-NOTES (full glance/twist/rear forensics), XP-PORT-PLAN
(back-burnered), RIO-NOTES (namedpipe + keypad), pipe/egg conf variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:08:15 -05:00
CydandClaude Fable 5 198b73eb96 Deploy: freeze the render bridge to renderer.exe -- archive is now complete
render-bridge/freeze.ps1: PyInstaller onefile of live_bridge.py + Dave's
vrboard/vrview/vrview_gl (+glcontext collected for moderngl) -> dist\
renderer.exe (~33 MB). GL backend live-verified from a pure-dist launch
(no --root/--renderer overrides) on 2026-07-10. package.ps1 bundles the
frozen exe automatically when present; DEPLOYMENT-PLAN's renderer OPEN
item is resolved. Ignore the build-artifact dirs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:57:14 -05:00
CydandClaude Fable 5 d1635d4694 Deploy: drop -SwapMFD switch -- edit the RECTS x-coords to swap MFD heads (simpler)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:55:06 -05:00
CydandClaude Fable 5 2d5f1562bd Deploy: -SwapMFD flag to trade the two MFD head positions (win3<->win4)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:53:46 -05:00
CydandClaude Fable 5 125d835a05 Deploy: pod_deploy.ps1 cockpit window layout + focus_dosbox.ps1 (renderer topmost, DOSBox focused)
Real-pod deployment launcher for the original VDB + octopus 4-head wiring:
VPX_COCKPIT borderless head windows (radar/win0 800,0; 3-color MFD/win4 1440,0;
2-color MFD/win3 2080,0; all 640x480) + the GL bridge as the main out-the-window
view (0,0, 800x600 via new BRIDGE_W/BRIDGE_H in live_bridge.py). Editable RECTS
at the top of the script for other rigs.

focus_dosbox.ps1 restores the deployment window state: renderer -> always-on-top
(WS_EX_TOPMOST, game view never covered) + DOSBox-X -> 10,10 with foreground
focus (keeps the emu thread at foreground priority so the RIO doesn't starve).
Coexist cleanly: topmost renderer stays above the focused-but-non-topmost DOSBox.
pod_deploy calls it last; re-run anytime the stack is disturbed. LAUNCH.md
documents both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:44:45 -05:00
CydandClaude Fable 5 e1b43fb6d5 Bridge: VRVIEW_MIPMAP option + launch_pod -Mipmap flag (RP checkerboard shimmer)
RP's high-frequency checkerboard track floor aliases/moires without mipmaps
(reads as "flashing geometry" in motion; user's texture-interpolation call).
vrview_gl _tex() now builds mipmaps + trilinear/anisotropic filtering under
VRVIEW_MIPMAP=1 (keeps point-sample look up close; off by default = authentic
i860, BT-identical). launch_pod.ps1 gains -Mipmap so it's set at BOOT -- never
restart the bridge mid-mission (GL-context churn starves the RIO ACK deadline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:04:35 -05:00
CydandClaude Fable 5 162fa39117 Death camera: forensics, capture, and probes (fix not yet applied)
Pilot sees the escape-pod/wreck interior at death instead of the portal
ride. Findings (DEATH-SEQUENCE-NOTES.md): two death variants exist --
respawn deaths transit the chain camera to the portal at the origin
(fp_cam's <100m vehicle-distance guard rejects exactly that ride = the
fix target); mission-end deaths (tonight's capture) have no transit on
the wire at all, just the fog fade at the wreck. At death the 0x1f
articulation batches switch to driving origin-anchored nodes = the
portal diorama animating; type-3 view flushes decode as projection+fog
only (hither/yon at floats 12/13). Reference capture
captures/netdeath-20260708.fifodump (full networked mission incl. death)
+ replay probes in probes/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:13:12 -05:00
CydandClaude Fable 5 1b041d5a9e Bridge: frame coalescing -- the view can no longer fall behind real time
The main loop rendered EVERY draw_scene in arrival order, so any render
slowdown played the mission slower than real time and the view drifted
seconds behind the game (user: "renderer fell way out of sync"); the
backpressure hides in the socket, so backlog= stayed 0. Now each pass
slices all complete records, applies every state record in order, and
presents only the newest frame; superseded presents are counted in the
new skipped= report field. Live verdict over a full mission:
frames=10169 skipped=3 backlog=0B -- locked to the wire.

Also from the tuner session: seat trim granularity 0.1 (0.5 was too
jumpy) and a HUD size trim (vrview_gl u_scale + VRVIEW_HUDSCALE env,
live +/- keys) -- tuners stay enabled for pilot feedback; seat trim
turns out to be per-mech (Mad Cat vs Thor want different values).

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