BT410 5.3.131: the instability reaches the pilot -- the sway loop closes, and every watcher in the mech turns out to be inert

The gyro's sway slew (@004b275c) was already reconstructed and already
read the cell the master perf writes -- swayBias -- which nothing had
ever written.  So the mechInstability member I added to the gyro in
5.3.127 was a duplicate of that same cell, the second such mistake this
arc after realMaxSpeed/runSpeedMax.  The lesson repeats: grep our own
tree for an existing carve before adding a member for a binary offset.

Merged, and the loop closes: the instability model now publishes into
swayBias, so an unstable mech shakes its pilot harder through code that
was already correct and waiting for a writer.  Also corrected: the
binary's impaired gate has three terms where ours had two -- a gyro
destroyed outright swings to the impaired sway base, not only an
unpowered or heat-failed one.

Verified on the rig (BT_SWAY_LOG): bias arrives, the angle slews toward
base+bias and stays inside the authored band, zero faults.

AND THE LOG IMMEDIATELY EARNED ITS KEEP.  It showed impaired=1 for an
entire clean run.  Instrumenting the three terms: state=0, heat=0 -- so
the new term is inert and correct -- but watchdog=0 where Ready is 4.
PowerWatcher::UpdateWatch falls back to 0 when its watched link resolves
NULL, and watchedLink is default-constructed and bound NOWHERE in the
tree, for PowerWatcher and HeatWatcher alike.

So the whole watcher family is inert: every watcher resolves NULL, every
PowerWatcher reports its target unpowered, and the gyro has been sitting
on its destruction-grade sway base since the sway model landed --
invisibly, until this log existed.  The fix needs the streamed watch
index and a real ctor bind; guessing a binding is precisely what this
project forbids, so it is recorded with its evidence for the next
sitting rather than improvised now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-10 08:12:23 -05:00
co-authored by Claude Fable 5
parent f25abf1da5
commit 4ad97b5d41
5 changed files with 173 additions and 10 deletions
@@ -0,0 +1,87 @@
# pod_render_norio.conf -- pod_render_rec with the RIO serial port OFF.
#
# The emulator log shows a steady stream of serial1 RX OVERRUN errors on
# the RIO pipe, and controls post their events at HighEventPriority
# UNCONDITIONALLY (CONTROLS.HPP:250) -- so a chattering RIO port would
# flood a priority the background pump always serves first, starving the
# priority-0 renderer events the load gate waits on. This conf tests that
# by removing the port entirely. Everything else is identical to the rec
# conf, so a launch here and a hang there isolates the RIO.
#
[sdl]
output=opengl
# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop;
# with the retry patches a rare dropout self-recovers (see gauge_rio.conf).
priority=higher,higher
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief
# emulation-thread stalls (RIO retry recovery) don't audibly chop
rate=44100
blocksize=1024
prebuffer=60
[serial]
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
# few-ms ACK deadline is met; plasma display on COM2 (real pod has both).
# VWE fork namedpipe backend (com0com/realport retired -- COM1/COM2 gone):
# DOSBox = pipe client (retry), vRIO/vPLASMA apps = servers; an unconnected
# pipe behaves as an unplugged cable so the mission still runs. serialnamedpipe.h
serial1=disabled
serial2=namedpipe pipe:vplasma
# live UNBUFFERED game output: DOS char devices are not buffered, so
# redirecting stdout to COM3 lands every line immediately. A normal
# '> file' redirect stays 0 bytes until the process exits, which hides
# all progress on a run that does NOT crash.
serial3=file file:C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
c:
cd \REL410\BT
set VIDEOFORMAT=svga
rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per
rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS
rem driver relies on; skipping it left the cards uninitialized (silent).
rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the
rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is
rem dumped from a real card. diagnose /s kept (passes, sets mixer config).
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO
rem attached; stdout redirected so mission-load progress survives kills.
set BT_JOINTS=1
set L4VIEWEXT=1
set BT_STACK_LOG=1
set BT_MECH_LOG=1
set BT_SUPERSTOP_SOAK=1
set BT_SWAY_LOG=1
set BT_GAUGE_LOG=1
set BT_FORCE_TURN=0
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s n p
32rtm.exe -x
BTL4REC.EXE -egg testarn.egg > COM3
echo GAME-RC=%errorlevel% >> RC.TXT
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+23 -1
View File
@@ -109,7 +109,6 @@ Gyroscope::Gyroscope(
externalPitchPtr = &spare0;
}
vibrationDirection = Vector3D(0.0f, 1.0f, 0.0f);
mechInstability = 0.0f;
swayAngle = 0.0f;
swayVelocity = 0.0f;
swayActive = 0;
@@ -184,8 +183,14 @@ void
// The impaired test reads the gyro's OWN mirrored alarms -- the two
// UpdateWatch just drove: the voltage watchdog and the heat watch.
//
//
// 5.3.131: the binary's gate (@004b275c) has THREE terms and the first
// was missing here -- a gyro DESTROYED outright swings to the impaired
// base too, not only an unpowered or heat-failed one.
//
Logical
impaired =
GetSimulationState() == 1 ||
watchdogAlarm.GetLevel() != PoweredSubsystem::Ready ||
heatAlarm.GetLevel() == HeatSink::FailureHeat;
@@ -226,6 +231,23 @@ void
swayAngle = minAnimationNoise;
}
if (getenv("BT_SWAY_LOG"))
{
static Scalar s_lastSway = -1.0f;
if (swayAngle != s_lastSway)
{
s_lastSway = swayAngle;
DEBUG_STREAM << "[sway] angle=" << swayAngle
<< " bias=" << swayBias
<< " impaired=" << (int)impaired
<< " (state=" << GetSimulationState()
<< " watchdog=" << watchdogAlarm.GetLevel()
<< " heat=" << heatAlarm.GetLevel() << ")"
<< " band=[" << minAnimationNoise
<< "," << maxAnimationNoise << "]" << endl << flush;
}
}
IntegrateEyeJoint(time_slice);
IntegrateBody(time_slice);
+18 -8
View File
@@ -184,17 +184,27 @@
// powered/impaired percentages) and the placement scratch the
// damage fan-out rotates hit directions through.
//
//
// The mech's instability, republished here every master frame
// (binary gyro+0x3a8, written by the master perf tail). The gyro's
// own tip/sway machinery (@004b275c) reads it summed with +0x3b0
// against the +0x3bc threshold -- that consumer is a later brick;
// the publish lands now so the value is where the binary puts it.
//
public:
Scalar mechInstability;
//
// The mech's instability, republished here by the master perf every
// frame (binary gyro+0x3a8) straight into the sway bias below.
//
void
SetSwayBias(Scalar instability)
{ Check(this); swayBias = instability; }
protected:
//
// swayAngle the live cockpit noise amplitude (binary gyro+0x3bc),
// slewed toward its target at rotationPerSecond and clamped into
// the authored [minAnimationNoise, maxAnimationNoise] band.
// swayBias THE MECH'S INSTABILITY (binary gyro+0x3a8), added to
// the sway target: the closer the mech is to going over, the
// harder the cockpit shakes. 5.3.131 -- this member always
// existed and the slew always read it, but nothing wrote it; the
// separate 'mechInstability' member added in 5.3.127 was a
// duplicate of the same cell and is retired.
//
Scalar swayAngle, swayBias, swayVelocity;
int swayActive;
Vector3D placeRot, placePos;
+1 -1
View File
@@ -1333,7 +1333,7 @@ void
if (gyroSubsystem != NULL)
{
((Gyroscope *)gyroSubsystem)->mechInstability = unstablePercentage;
((Gyroscope *)gyroSubsystem)->SetSwayBias(unstablePercentage);
}
}
+44
View File
@@ -475,3 +475,47 @@ a bare `+ 0x354)` grep proves nothing. No Mech attribute row binds them
either. Landing them would be three loops feeding dead memory, so per the
project rule (record where the binary is silent, never invent a consumer)
this stays decoded and unlanded until a consumer turns up.
## 5.3.131 -- the instability loop CLOSES, and a family of watchers is found inert
**The loop is closed.** `@004b275c` -- the gyro's sway slew -- was ALREADY
reconstructed in our tree, and it already read the cell the master perf
writes: `swayBias`, which nothing had ever written. So the 5.3.127
`mechInstability` member I added on the gyro was a DUPLICATE of that same
cell (the second such mistake this arc, after `realMaxSpeed`/`runSpeedMax`
-- the lesson is the same: grep our own tree for an existing carve before
adding a member for a binary offset). Merged: the master perf now
publishes into `swayBias` through a setter, and the instability model
finally does something -- an unstable mech shakes its pilot harder,
through code that was already correct and waiting.
Also landed: the binary's impaired gate has THREE terms and ours had two.
A gyro DESTROYED outright (`gyro+0x40 == 1`) swings to the impaired sway
base, not only an unpowered or heat-failed one.
VERIFIED on the rig (BT_SWAY_LOG): bias arrives (0 -> 1.0 -> 0.90), the
angle slews toward base+bias and stays inside the authored [0, 1.2] band,
zero faults.
### NEW DEFECT FOUND, precisely located, NOT fixed here
The same log showed `impaired=1` for an entire clean run. Instrumenting
the three terms settled it: `state=0 heat=0` (so my new term is inert and
correct) but `watchdog=0` where Ready is 4 -- that term fires forever.
Chasing it up: `PowerWatcher::UpdateWatch` sets the watchdog from the
WATCHED subsystem's voltage state and falls back to 0 when
`watchedLink.Resolve()` is NULL. And `watchedLink` is
DEFAULT-CONSTRUCTED (HEAT.CPP:886) and **bound nowhere in the tree** --
not for PowerWatcher, not for HeatWatcher (HEAT.CPP:964 resolves the same
way).
So the whole WATCHER family is inert: every watcher resolves NULL, every
PowerWatcher reports its target unpowered, and the gyro has consequently
been sitting on its `percentageOnDestruction` sway base since the sway
model landed -- invisibly, until this log existed.
The fix needs the streamed watch index out of the watcher resource plus
the ctor bind, in the shape the crit-table binding already uses. That is
a proper brick, not a guess, and guessing a binding is exactly what this
project's rules forbid -- so it is recorded here for the next sitting.