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>
50 lines
1.4 KiB
Python
50 lines
1.4 KiB
Python
#
|
|
# 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]))
|