BT410 5.3.84: the yellow bar and the MFD bleed are ONE bug -- BTSEC1.PCX index 254, and the offender is named
Second operator report, same day: "what is that yellow orange artifact on the
display, it too shows up here and in bt411 but not in the original" -- a
solid vertical bar beside the RANGE readout on the colour head.
It is the same defect as 5.3.83's MFD bleed. Not a similar one: the same
676 pixels.
HOW IT WAS CAUGHT. 5.3.83 shipped a fix that could not be verified, because
the fix makes something INVISIBLE and the A/B rig's boot cockpit already
showed zero MFD extras -- the unfixed build looked perfectly clean. So this
commit adds a POSITIVE CONTROL instead of another argument:
BT_TRANS_PROVOKE fills the colour head's uninitialised
translationTable[64..255] with 0xFF00 -- every high-byte head bit --
rather than the zero the fix installs. Any draw that indexes the tail
then lights ALL the mono heads at once.
On the boot cockpit that lights exactly 1030 pixels: Eng1/Eng2/Eng3 +1030
each, Mfd1 +684, Mfd2 +704, Mfd3 +1074, Comm +676, against 0 extras with the
fix. The bug was firing the whole time. Our heap simply happened to hold
zeros in that tail -- the same luck the shipped binary has been having, which
is exactly why the operator sees the artifact and the A/B rig does not.
THE OFFENDER, NAMED. oormask.py renders which pixels those are and prints
their horizontal run lengths. The mask is a glyph cluster plus 52 runs of
13px -- a SOLID 13x52 BAR at screen (526,228)-(538,279). Solid means a
rectangle in the source art, so decode the art:
BTSEC1.PCX, the colour head's own 480x640 background, contains exactly
ONE out-of-range value in the entire image: index 254, exactly 676
pixels, a solid 52x13 rectangle at (199,526)-(250,538).
The sec port is configured at ROTATION 270, mapping source (x,y) ->
screen (y, 479-x). That puts the rectangle at screen x 526..538,
y 229..280. Measured mask: x 526..538, y 228..279. Same rectangle, to
the pixel.
ONE READ, TWO SYMPTOMS. translationTable[254] is never written (
BuildSecondaryTranslation fills only 1<<numberOfBits = 64 entries), and
DrawPoint ORs the result in unmasked. Whatever the heap left there decides
which symptom the operator sees:
low 6 bits set -> a coloured block on the COLOUR head, beside the RANGE
readout. The yellow-orange bar.
high 8 bits set -> garbage in the MFD / ENG / COMM planes. The bleed.
Both reports, one uninitialised int. It also explains the "not in the
original" asymmetry without needing the original to differ in code: it does
not differ, it is just getting zeros there. And it explains BT411 showing it
too -- both reconstructions inherit the read from the archive.
5.3.83's zero-fill therefore cures both, and turns a heap-lottery into a
guarantee. Still not DIRECTLY observed cured, because no rig we have was
showing the artifact to begin with; that honesty is recorded in the file
header rather than smoothed over.
ALSO IN: oormask.py, barbox.py, vis_provoke.conf, and a README section on
positive controls -- when a fix replaces garbage with a benign value, build
the variant that replaces it with a maximally LOUD value, because that turns
"I see no difference" into a number and separates "the fix works" from "this
screen never exercised the path".
METHOD NOTE, recorded in the README because it nearly cost the fix: three
grabs of one running instance score IDENTICALLY, so the within-boot noise
floor is zero -- and that is the wrong floor. Judging a rebuild needs the
ACROSS-boot floor (~6px on the MFD heads). A single bad grab, caught
mid-draw, read as a 2700px regression and nearly got a correct change
reverted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,3 +80,50 @@ artifact; each was settled -- or killed -- by one instrumented run. So:
|
||||
3. Only then read code, and only that widget's.
|
||||
|
||||
And re-stage before believing any measurement.
|
||||
|
||||
## Positive controls: making an invisible bug measurable
|
||||
|
||||
A fix that makes something *disappear* cannot be verified on a screen where
|
||||
nothing was visible in the first place. The translation-table artifact
|
||||
(BT410 5.3.83) is the worked example, and the trick generalises.
|
||||
|
||||
`vis_provoke.conf` sets `BT_TRANS_PROVOKE=1`, which makes the
|
||||
`BTL4GraphicsPort` ctor (source410/MUNGA_L4/L4GREND.CPP) fill the colour
|
||||
head's uninitialised `translationTable[64..255]` with `0xFF00` -- every
|
||||
high-byte head bit -- instead of the zero the fix installs. Any draw that
|
||||
indexes the tail then lights *all* the mono heads at once, so:
|
||||
|
||||
```
|
||||
bash ab.sh rec # baseline, tail = 0
|
||||
... -conf vis_provoke.conf # same binary, tail = 0xFF00
|
||||
python3 planes.py shipped.png ours_provoke.png # Eng1/2/3 extras = the hit count
|
||||
```
|
||||
|
||||
On the boot cockpit that is **1030 pixels**. With the fix it is **0**. The
|
||||
bug was always firing; the heap simply happened to hold zeros.
|
||||
|
||||
`oormask.py shipped.png ours_provoke.png mask.png` then renders *where* those
|
||||
pixels are and prints their horizontal run lengths. Solid runs mean a
|
||||
rectangle in the source art, not scattered noise -- that is what identified
|
||||
BTSEC1.PCX's 52x13 block of index 254, which the port's 270-degree rotation
|
||||
lands at screen (526,228)-(538,279).
|
||||
|
||||
`barbox.py <pngs...>` reports what each build draws inside that box in the
|
||||
sec plane, which is how "all five captures are identical here" got
|
||||
established.
|
||||
|
||||
**The generalisable move:** when a fix replaces garbage with a benign value,
|
||||
build the variant that replaces it with a *maximally loud* value. That
|
||||
converts "I cannot see any difference" into a number, and it distinguishes
|
||||
"the fix works" from "this screen never exercised the path".
|
||||
|
||||
## Noise floors are not interchangeable
|
||||
|
||||
Three grabs of one running instance scored *identically* on the MFD heads, so
|
||||
the within-boot noise floor is zero. That floor is the wrong one: it says
|
||||
nothing about a fresh process. Re-booting the *same* binary moves the MFD
|
||||
heads by ~6px, and only that across-boot floor can judge a rebuild.
|
||||
|
||||
A first "fixed" capture appeared to regress the MFDs by ~2700px and nearly
|
||||
got a correct change reverted. It was a bad grab caught mid-draw. Two
|
||||
agreeing runs, compared against the across-boot floor, is the rule.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#
|
||||
# What does each build draw in the out-of-range BAR box?
|
||||
#
|
||||
# The provoke mask localised the offending block to a 13x52 rectangle. The
|
||||
# question that decides whether the fix cures the operator's yellow bar is
|
||||
# simply: in the SEC (colour) plane, does the shipped binary light those
|
||||
# pixels, and do we?
|
||||
#
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
BOX = (8, 52, 648, 531)
|
||||
BAR = (526, 228, 539, 280) # x0,y0,x1,y1 in cropped coords
|
||||
SEC = 0x003F
|
||||
|
||||
|
||||
def scan(path):
|
||||
im = Image.open(path).convert("RGB").crop(BOX)
|
||||
px = im.load()
|
||||
lit = 0
|
||||
vals = {}
|
||||
for y in range(BAR[1], BAR[3]):
|
||||
for x in range(BAR[0], BAR[2]):
|
||||
r, g, b = px[x, y]
|
||||
wd = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
|
||||
v = wd & SEC
|
||||
if v:
|
||||
lit += 1
|
||||
vals[v] = vals.get(v, 0) + 1
|
||||
top = sorted(vals.items(), key=lambda kv: -kv[1])[:3]
|
||||
return lit, top
|
||||
|
||||
|
||||
for p in sys.argv[1:]:
|
||||
lit, top = scan(p)
|
||||
print("%-20s sec-lit %4d/676 top sec values: %s" %
|
||||
(p, lit, ", ".join("0x%02X x%d" % t for t in top)))
|
||||
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# Extract the OUT-OF-RANGE pixel mask from the positive-control capture.
|
||||
#
|
||||
# With BT_TRANS_PROVOKE the sec port's translationTable[64..255] is 0xFF00,
|
||||
# so every pixel whose source index exceeded the 6-bit palette sets ALL the
|
||||
# high-byte head bits. Eng1 (0x0200) is sparse, so "Eng1 lit in provoke but
|
||||
# not in shipped" isolates the offenders exactly.
|
||||
#
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
BOX = (8, 52, 648, 531)
|
||||
ENG1 = 0x0200
|
||||
|
||||
|
||||
def words(path):
|
||||
im = Image.open(path).convert("RGB").crop(BOX)
|
||||
w, h = im.size
|
||||
px = im.load()
|
||||
return [[((px[x, y][0] >> 3) << 11) | ((px[x, y][1] >> 2) << 5) |
|
||||
(px[x, y][2] >> 3) for x in range(w)] for y in range(h)], w, h
|
||||
|
||||
|
||||
A, w, h = words(sys.argv[1]) # shipped
|
||||
B, _, _ = words(sys.argv[2]) # provoke
|
||||
|
||||
out = Image.new("RGB", (w, h), (0, 0, 0))
|
||||
op = out.load()
|
||||
runs = {}
|
||||
n = 0
|
||||
for y in range(h):
|
||||
run = 0
|
||||
for x in range(w):
|
||||
if (B[y][x] & ENG1) and not (A[y][x] & ENG1):
|
||||
op[x, y] = (255, 40, 0)
|
||||
n += 1
|
||||
run += 1
|
||||
else:
|
||||
if run:
|
||||
runs[run] = runs.get(run, 0) + 1
|
||||
run = 0
|
||||
if run:
|
||||
runs[run] = runs.get(run, 0) + 1
|
||||
|
||||
out.save(sys.argv[3])
|
||||
print("out-of-range pixels: %d" % n)
|
||||
print("horizontal run lengths (len: count), longest first:")
|
||||
for k in sorted(runs, reverse=True)[:12]:
|
||||
print(" %3d px run x%d" % (k, runs[k]))
|
||||
@@ -0,0 +1,32 @@
|
||||
[sdl]
|
||||
output=opengl
|
||||
[dosbox]
|
||||
memsize=32
|
||||
machine=svga_s3
|
||||
[cpu]
|
||||
core=normal
|
||||
cputype=pentium
|
||||
cycles=20000
|
||||
[serial]
|
||||
serial1=disabled
|
||||
serial2=disabled
|
||||
[autoexec]
|
||||
mount c "C:\VWE\TeslaRel410\restoration\build410\run\image"
|
||||
c:
|
||||
set L4CONTROLS=RIO,KEYBOARD
|
||||
set BT_MECH_LOG=1
|
||||
set BT_VIS_LOG=1
|
||||
set BT_TRANS_PROVOKE=1
|
||||
set HEAPSIZE=15000000
|
||||
set L4TIMER=
|
||||
set L4SOUND=OFF
|
||||
set L4GAUGE=640x480x16
|
||||
set L4PLASMA=
|
||||
set L4VIDEO=OFF
|
||||
set BLASTER=A220 I5 D1 H5 P330 T6
|
||||
set TEMP=c:\
|
||||
32rtm.exe -x
|
||||
BTL4REC.EXE -egg test.egg > OUTREC.TXT
|
||||
echo GAME-RC=%errorlevel% >> OUT.TXT
|
||||
32rtm.exe -u
|
||||
exit
|
||||
@@ -541,20 +541,50 @@ void
|
||||
// OR. That is the most conservative possible outcome and matches what the
|
||||
// shipped binary is observed to do.
|
||||
//
|
||||
// VERIFICATION STATUS (2026-07-29) -- read this before believing the fix:
|
||||
// THE OFFENDER IS NAMED (2026-07-29). BT_TRANS_PROVOKE (below) fills the
|
||||
// tail with 0xFF00 instead of 0, which makes every out-of-range draw light
|
||||
// all eight high-byte heads -- turning an invisible bug into a measurable
|
||||
// one. On the boot cockpit it lights exactly 1030 pixels, and the mask
|
||||
// splits into a glyph cluster plus ONE SOLID 13x52 BAR at screen
|
||||
// (526,228)-(538,279).
|
||||
//
|
||||
// * The MECHANISM is proven from the archive source (the two builders'
|
||||
// asymmetric loops, above). That part is solid.
|
||||
// * The FIX IS PROVEN NON-REGRESSIVE on the gauge A/B rig: three boots
|
||||
// (unfixed x2, fixed x1) score identically on every head, Mfd1 132/0,
|
||||
// Mfd2 37/7, Mfd3 23/0, within a ~6px across-boot noise floor.
|
||||
// * The FIX IS NOT YET PROVEN TO CURE THE REPORTED SYMPTOM. The A/B rig's
|
||||
// capture point is the boot/attract cockpit, where the MFD heads already
|
||||
// show ZERO extra pixels -- there is no bleed there to remove. The
|
||||
// operator's report is from an in-mission cockpit with the radar live.
|
||||
// Confirming the cure needs a mission-state capture; until then this is a
|
||||
// hardening change against a real uninitialised read, not a demonstrated
|
||||
// cure.
|
||||
// Decoding the art finds the source of that bar with no ambiguity left:
|
||||
//
|
||||
// BTSEC1.PCX, the colour head's own 480x640 background, contains exactly
|
||||
// ONE out-of-range value in the whole image -- index 254, exactly 676
|
||||
// pixels, a SOLID 52x13 RECTANGLE at (199,526)-(250,538).
|
||||
//
|
||||
// The sec port is configured at ROTATION 270 (L4GAUGE.CFG), which maps
|
||||
// source (x,y) -> screen (y, 479-x). That places the rectangle at screen
|
||||
// x 526..538, y 229..280. The measured mask is x 526..538, y 228..279.
|
||||
// Same rectangle, to the pixel.
|
||||
//
|
||||
// So one uninitialised read produces BOTH reported symptoms, depending only
|
||||
// on which bits the heap garbage happens to have set:
|
||||
//
|
||||
// low 6 bits set -> a coloured block on the COLOUR head. This is the
|
||||
// operator's second report the same day, "that yellow
|
||||
// orange artifact", a vertical bar beside the RANGE
|
||||
// readout -- which is precisely where the rotated
|
||||
// rectangle lands.
|
||||
// high 8 bits set -> garbage in the MFD / ENG / COMM planes. This is the
|
||||
// first report, "bleeding over ... into the MFDs".
|
||||
//
|
||||
// VERIFICATION STATUS:
|
||||
//
|
||||
// * MECHANISM: proven from source (asymmetric builders) AND live (the
|
||||
// provoke control lights 1030 pixels that the fix zeroes).
|
||||
// * OFFENDING ART: proven by decode (BTSEC1.PCX index 254, 676px, and the
|
||||
// rotation arithmetic lands it on the reported spot).
|
||||
// * NON-REGRESSIVE: gauge A/B, three boots (unfixed x2, fixed x1) score
|
||||
// identically on every head -- Mfd1 132/0, Mfd2 37/7, Mfd3 23/0 --
|
||||
// against a ~6px across-boot noise floor.
|
||||
// * NOT DIRECTLY OBSERVED CURED. In all our A/B boots the heap tail
|
||||
// happened to hold ZEROS, so the unfixed build already scored 0 extras
|
||||
// and there was no visible artifact to remove. That is the same luck
|
||||
// the shipped binary has been enjoying. The fix makes it guaranteed
|
||||
// rather than lucky; confirming the operator's bar is gone needs a
|
||||
// capture from a rig that was showing it.
|
||||
//
|
||||
// A first "fixed" capture appeared to REGRESS the MFDs by ~2700px. It was a
|
||||
// bad grab (caught mid-draw); the rebuild-and-rerun matched the unfixed
|
||||
@@ -590,9 +620,22 @@ void
|
||||
first = 1 << numberOfBits,
|
||||
i;
|
||||
|
||||
//
|
||||
// POSITIVE CONTROL. Zero is the fix; it is also invisible,
|
||||
// so on its own it cannot tell us whether the out-of-range
|
||||
// path is ever TAKEN on a given screen. BT_TRANS_PROVOKE
|
||||
// fills the same tail with every MFD bit instead: if the
|
||||
// path fires, the MFD heads flood with garbage and the A/B
|
||||
// scoreboard's "extra" columns explode. If they do not
|
||||
// move, the sec port never sees an out-of-range index on
|
||||
// that screen and the fix is inert there.
|
||||
//
|
||||
int
|
||||
fill = (getenv("BT_TRANS_PROVOKE") != NULL) ? 0xFF00 : 0;
|
||||
|
||||
for (i = first; i < 256; ++i)
|
||||
{
|
||||
translationTable[i] = 0;
|
||||
translationTable[i] = fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user