#147 range caret: NaN poisons a process-lifetime static -- and the caret's input was never logged

Oracle: "no range finder on this drop" + a screenshot -- tick marks present,
moving caret absent, one drop, only tester affected.

Not the host, not the chassis, not his destroyed HUD.  From the four field
logs: he WAS hosting (`[lobby] host:` appears only in his log) but range
computed fine on his node (1806 nonzero samples) and the reticle built on all
6 drops; a second tester flew a Thor the same night without hosting and saw
nothing, and the ladder is shared HudSimulation/BTReticleRenderable, not
per-chassis content; his HUD was destroyed twice but for 11s and 26s only, and
a destroyed HUD costs the LOCK (_DAT_004b7ec4 = 0.75), not the caret.

THE DEFECT.  sShownRange -- what the caret binds to -- is a function-level
static in mech4's targeting step: one cell for the whole process, shared by
every mech, carried across drops, never re-seeded.  NaN is ABSORBING in

    step = trueRange - sShownRange;
    if (step >  maxStep) step =  maxStep;      // false for NaN
    if (step < -maxStep) step = -maxStep;      // false for NaN
    sShownRange += step;

so one poisoned frame makes it NaN for the life of the process.  The consumer
repeats the mistake -- BTReticleRenderable::Draw clamps with the same two
comparisons -- so NaN reaches AddPoint/ConcatMatrix and the caret + its bar
become degenerate geometry that STOPS RENDERING, while every static reticle
element including the tick marks still draws.  That is the reported symptom
exactly, and it is sticky until relaunch.

WHY NO LOG COULD SETTLE IT.  The caret's actual input had NO diagnostic
anywhere: BT_RANGE_LOG instruments the PICK (#4), and [target]'s `range=` is a
SEPARATE locally-recomputed Sqrt in the weapon-range check -- neither is
sShownRange or gBTHudRangeStorage.  Grepping the field logs for NaN returns
nothing because the poisoned variable was never printed.  Absence of the
signal was not evidence of absence.

FIX (4 parts):
  1. re-seed sShownRange when the viewpoint mech CHANGES, so a new drop starts
     at the binary's 1200 default.  Deliberately NOT on respawn -- that reuses
     the entity, and the binary does not reset the readout on respawn either.
  2. producer NaN trap -> re-seed to 1200 instead of propagating.
  3. NaN-safe consumer clamp (test x == x first) -> fall back to the authentic
     no-target peg rather than rendering nothing.
  4. BT_RANGE_LOG now prints the caret's real input at 1 Hz plus a
     "[range] NaN TRAPPED" receipt, so the next field log CAN settle it.

STATUS [T3 on the field link].  The defect and the symptom match exactly and
the fix stands on its own merits -- a process-lifetime static feeding unguarded
float geometry is a bug regardless.  But the causal link to Oracle's report is
INFERENCE: the NaN source is unidentified and this has not been reproduced.
Field-verify with BT_RANGE_LOG=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
Joe DiPrima
2026-08-08 07:16:32 -05:00
co-authored by Claude Opus 5
parent 832bec0966
commit 1ae57398f1
4 changed files with 162 additions and 1 deletions
+15
View File
@@ -568,6 +568,21 @@ and every instrument is now live [T2]:**
threshold for HUD page visibility" was neither heat nor page visibility) — corrected in place,
with the live lock/slide implementation staying in mech4.cpp's targeting step, which had both
thresholds right all along.
**THE CARET CAN DIE FOR THE WHOLE SESSION — #147, fixed 2026-08-08 [T2 code, T3 field link]:**
`sShownRange` (mech4's targeting step) is a **function-level static** — one cell per process,
shared by every mech, carried across drops. **NaN is absorbing** in `step = trueRange
sShownRange; sShownRange += step`, and neither the producer's clamps nor
`BTReticleRenderable::Draw`'s (`range < minRange` / `range > maxRange`) catch it — both
comparisons are **false for NaN**. A poisoned value therefore reaches `AddPoint`/`ConcatMatrix`,
and the caret + its bar become **degenerate geometry that stops rendering while the static tick
marks keep drawing** — the exact reported symptom ("no range finder on this drop", ticks there,
caret gone), sticky until relaunch. Fixed four ways: re-seed on mech change (not on respawn —
that reuses the entity, and the binary doesn't reset the readout either), a producer NaN trap, a
NaN-safe consumer clamp falling back to the 1200 peg, and — the reason no log could settle it —
**`BT_RANGE_LOG` now prints the caret's actual input** (`[range] caret input shown=`). It never
did before: `BT_RANGE_LOG` instrumented the PICK (#4) and `[target]`'s `range=` is a *separate*
locally-recomputed Sqrt in the weapon-range check, so grepping field logs for NaN found nothing
because the poisoned variable was never printed. Causal link to the field report is INFERENCE.
**GAP — the 100 m RANGE BIAS is NOT reconstructed [T1 read, unimplemented]:** HudSimulation
subtracts `_DAT_004b7ecc` (100.0f) from `RangeToTarget@0x1EC` **every frame while the timed
flag @0x22C is set**, accumulating @0x21C by `time_slice` until it reaches @0x1D8, then clearing
+8
View File
@@ -2622,6 +2622,14 @@ void
{
// the range caret translate, from the live target range
Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f;
// #147: NaN-SAFE clamp. `range < minRange` and `range > maxRange` are BOTH
// false for NaN, so the old pair let a poisoned value straight through into
// AddPoint/ConcatMatrix below -- degenerate geometry, and the caret + its
// bar silently STOP DRAWING while the static ticks remain. Test for NaN
// first (x == x is false only for NaN) and fall back to the binary's
// no-target default rather than rendering nothing.
if (!(range == range))
range = maxRange; // 1200: the authentic no-target peg
if (range < minRange) range = minRange;
if (range > maxRange) range = maxRange;
Scalar frac = (range - minRange) / (maxRange - minRange);
+51 -1
View File
@@ -6286,7 +6286,21 @@ void
// clamp(true - shown, +-dt*500) -- so the caret sweeps smoothly as
// the boresight crosses near/far ground instead of teleporting.
// (Applies to the no-target 1200 default too.)
static float sShownRange = 1200.0f;
// #147: sShownRange is DISPLAY state, but it is a function-level
// static -- one cell for the whole process, shared by every mech and
// carried across drops. Re-seed it whenever the viewpoint mech
// CHANGES (a new drop hands us a new entity) so a fresh drop starts
// at the binary's 1200 default instead of inheriting the last
// mission's slid value. A respawn REUSES the entity (Mech::Reset
// heals in place), so this deliberately does not fire there -- the
// binary does not reset the readout on respawn either.
static float sShownRange = 1200.0f;
static const void *sShownOwner = 0;
if (sShownOwner != (const void *)this)
{
sShownOwner = (const void *)this;
sShownRange = 1200.0f;
}
float trueRange = 1200.0f; // no target: the binary default
Entity *des = MECH_TARGET_ENTITY(this);
if (des != 0 && des != hotTarget)
@@ -6343,6 +6357,26 @@ void
// the 500 m/s slide toward trueRange (see the banner above)
{
// #147 NaN TRAP. NaN is ABSORBING here and the clamps below
// cannot catch it: `step > maxStep` and `step < -maxStep` are
// BOTH false for NaN, so a single poisoned frame makes
// sShownRange NaN and it stays NaN for the life of the process
// (the static is never re-seeded except on a mech change).
// Downstream, BTReticleRenderable::Draw clamps the same way, so
// the NaN reaches AddPoint/ConcatMatrix and the caret + its bar
// render as degenerate geometry -- i.e. they VANISH while every
// static reticle element (the tick marks) still draws. That is
// exactly the reported "no range finder on this drop: ticks
// there, moving caret gone". Re-seed instead of propagating.
if (!(trueRange == trueRange) || !(sShownRange == sShownRange))
{
if (getenv("BT_RANGE_LOG"))
DEBUG_STREAM << "[range] NaN TRAPPED (true=" << trueRange
<< " shown=" << sShownRange << ") -- re-seeded to 1200\n"
<< std::flush;
trueRange = 1200.0f;
sShownRange = 1200.0f;
}
float maxStep = (float)dt * 500.0f;
if (maxStep < 0.0f) maxStep = -maxStep;
float step = trueRange - sShownRange;
@@ -6350,6 +6384,22 @@ void
if (step < -maxStep) step = -maxStep;
sShownRange += step;
BTSetHudTargetRange((Scalar)sShownRange);
// The caret's ACTUAL input had NO diagnostic anywhere: BT_RANGE_LOG
// instruments the PICK (#4), and [target]'s `range=` is a separate
// locally-recomputed Sqrt in the weapon-range check -- so a field
// log could neither confirm nor refute a dead caret. Fixed.
if (getenv("BT_RANGE_LOG"))
{
static float sRlog = 0.0f;
sRlog += (float)dt;
if (sRlog >= 1.0f)
{
sRlog = 0.0f;
DEBUG_STREAM << "[range] caret input shown=" << sShownRange
<< " true=" << trueRange << " lock=" << gBTHudLockState
<< "\n" << std::flush;
}
}
}
// BT_RANGE_LOG (Gitea #4 VERDICT instrumentation -- uncommitted diag):
+88
View File
@@ -0,0 +1,88 @@
"""Night 13: rewrite #147's body (the inline attempt was mangled by the shell). ASCII only."""
import sys
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
import gitea
BODY = """**Reported by Oracle (2026-08-06 session):** "no range finder on this drop" + a screenshot.
Symptom confirmed by eye: **the ladder tick marks were present, the moving caret was not.**
Intermittent -- one drop, not every drop. He was the only tester who hit it.
## What it is NOT
Ruled out from the four field logs:
* **Not a Steam-host bug.** Oracle *was* hosting (`[lobby] host: lobby up` / `GO with N member(s)`
appears in his log and no other), but the host path is uninvolved: the range **computed** fine on
his node (1806 nonzero `[target] range=` samples) and the reticle **built** fine on every drop
(`[hud] reticle built: 7 weapon pip(s) registered` -- 6 drops, 6 builds).
* **Not the Thor chassis.** He flew a Thor, which is why the report reads as chassis-specific -- but
a second tester flew a Thor the same night *without* hosting and reported nothing, and the range
ladder is drawn by the shared `HudSimulation` / `BTReticleRenderable`, not per-chassis cockpit
content. No Thor asset-missing warnings in any log (only the known wreck-model fallbacks).
* **Not his destroyed HUD.** His HUD subsystem *was* destroyed twice -- the only tester to reach
`condition 0` all night -- but only for ~11 s and ~26 s, repaired by respawn each time. And a
destroyed HUD costs you the fire-control LOCK (own host zone >= 0.75 damage, `_DAT_004b7ec4`),
not the caret.
## The defect
`sShownRange` -- the value the caret ultimately binds to -- is a **function-level static** in
mech4.cpp's targeting step. One cell for the whole process, shared by every mech, carried across
drops, never re-seeded. Its update:
```c
float step = trueRange - sShownRange;
if (step > maxStep) step = maxStep;
if (step < -maxStep) step = -maxStep;
sShownRange += step;
```
**NaN is absorbing here, and the clamps cannot catch it** -- `step > maxStep` and `step < -maxStep`
are BOTH false for NaN. So a single poisoned frame makes `sShownRange` NaN and it stays NaN *for
the life of the process*.
The consumer repeats the same mistake -- `BTReticleRenderable::Draw`:
```c
Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f;
if (range < minRange) range = minRange; // false for NaN
if (range > maxRange) range = maxRange; // false for NaN
Scalar frac = (range - minRange) / (maxRange - minRange);
```
NaN flows into `AddPoint` / `ConcatMatrix`, so the caret and its bar become **degenerate geometry
and stop rendering** -- while every static reticle element, tick marks included, still draws.
That is exactly the reported symptom: ticks present, caret gone, sticky until relaunch.
## Why no log could confirm it
**The caret's actual input had no diagnostic anywhere.** `BT_RANGE_LOG` instruments the *pick*
(#4), and the `range=` field in `[target]` is a separate, locally recomputed
`Sqrt(ddx*ddx + ddy*ddy + ddz*ddz)` inside the weapon-range check -- it is neither `sShownRange`
nor `gBTHudRangeStorage`. Grepping the field logs for NaN returns nothing because **the poisoned
variable was never printed.** Absence of the signal was not evidence of absence.
## Fixed (unreleased)
1. **Re-seed on mech change** -- a new drop starts at the binary's 1200 default instead of
inheriting the previous mission's slid value. Deliberately does NOT fire on respawn: that
reuses the entity, and the binary does not reset the readout on respawn either.
2. **NaN trap at the producer** -- re-seed to 1200 rather than propagate.
3. **NaN-safe clamp at the consumer** -- test `x == x` first, and fall back to the authentic
no-target peg (1200) instead of rendering nothing.
4. **`BT_RANGE_LOG` now prints the caret's real input** -- `[range] caret input shown=... true=...
lock=...` at 1 Hz, plus a `[range] NaN TRAPPED` receipt.
## Status [T3 -- honest]
The defect and the symptom match exactly, and the fix is correct on its own merits: a
process-lifetime static feeding unguarded float geometry is a bug regardless of who reported what.
But **the causal link to Oracle's report is INFERENCE, not proof.** The NaN source is unidentified
and the failure has not been reproduced. What would settle it: fly with `BT_RANGE_LOG=1` -- if the
caret dies again, the log now names the frame it happened on.
"""
gitea.call("/issues/147", method="PATCH", payload={"body": BODY})
print("rewrote #147 body")