From 943219345a91c9932370dcf0ffc9e3921d553078 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 29 Jul 2026 22:36:59 -0500 Subject: [PATCH] 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< 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 --- emulator/render-bridge/gauge-ab/README.md | 47 ++++++++++++ emulator/render-bridge/gauge-ab/barbox.py | 37 ++++++++++ emulator/render-bridge/gauge-ab/oormask.py | 49 +++++++++++++ .../render-bridge/gauge-ab/vis_provoke.conf | 32 +++++++++ restoration/source410/MUNGA_L4/L4GREND.CPP | 71 +++++++++++++++---- 5 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 emulator/render-bridge/gauge-ab/barbox.py create mode 100644 emulator/render-bridge/gauge-ab/oormask.py create mode 100644 emulator/render-bridge/gauge-ab/vis_provoke.conf diff --git a/emulator/render-bridge/gauge-ab/README.md b/emulator/render-bridge/gauge-ab/README.md index 833bfedb..4171801b 100644 --- a/emulator/render-bridge/gauge-ab/README.md +++ b/emulator/render-bridge/gauge-ab/README.md @@ -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 ` 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. diff --git a/emulator/render-bridge/gauge-ab/barbox.py b/emulator/render-bridge/gauge-ab/barbox.py new file mode 100644 index 00000000..4b6345c7 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/barbox.py @@ -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))) diff --git a/emulator/render-bridge/gauge-ab/oormask.py b/emulator/render-bridge/gauge-ab/oormask.py new file mode 100644 index 00000000..0ee3d814 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/oormask.py @@ -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])) diff --git a/emulator/render-bridge/gauge-ab/vis_provoke.conf b/emulator/render-bridge/gauge-ab/vis_provoke.conf new file mode 100644 index 00000000..b9f85bf2 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/vis_provoke.conf @@ -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 diff --git a/restoration/source410/MUNGA_L4/L4GREND.CPP b/restoration/source410/MUNGA_L4/L4GREND.CPP index f6c4e732..e33ae5ee 100644 --- a/restoration/source410/MUNGA_L4/L4GREND.CPP +++ b/restoration/source410/MUNGA_L4/L4GREND.CPP @@ -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; } } }