BT410 Phase 5.3.32: the per-head A/B -- three engineering heads now exact

The gauge framebuffer is PLANE-PACKED: L4GAUGE.CFG gives each port a bit
mask, not a rectangle, so all six heads share the same x/y and are separated
only by bit -- the packing the VDB splits into the pod's physical screens.
Comparing RGB was therefore measuring nothing useful; the "magenta artifact"
just meant the wrong heads were lit at that pixel.

New instruments (emulator/render-bridge/gauge-ab/, with a README):
  planes.py  per-head scoreboard -- shipped vs ours, missing vs extra, per
             port mask, recovering the 16-bit word from DOSBox's RGB565
  BT_VIS_LOG a complete cockpit widget map from the single dispatch every
             gauge widget is built through (MethodDescription::Execute),
             printing name + port + authored position
  ab.sh      stages the fresh build over BTL4REC.EXE and kills any running
             DOSBox before launching -- see below

The fix: the Comm page's pilot roster is the POD roster (the viewpoint mech's
ControlsMapper pilot array, mapper attributes 15/16), not the mission's
player list.  It is zero in a solo mission, so the shipped page draws empty;
ours resolved the local player and painted its icon in colour 0xff.  The gate
belongs in the row source, not PilotList::Execute -- Execute's empty branch
still has to run, because erasing is what the shipped page draws.

  head    shipped    ours   missing   extra          (was)
  Eng1      17981   17981         0       0     (0 / 1030)
  Eng2      17981   17981         0       0
  Eng3      17981   17981         0       0
  Comm      15656   13557      2099       0     (2099 / 1253)
  Heat      21374   20913       566     105     (566 / 990)

Title-band magenta 404 -> 0.  The 2099 Comm pixels we are missing were
missing before the gate, so it is a strict improvement.

Method note, which cost more than the bug: the rig's confs run BTL4REC.EXE
out of the mount while the build writes build410/btl4opt.exe, and nothing
connected the two.  Three measurements ran an hour-old binary, so a change
that "did nothing" three times had never been tested -- and the correct first
hypothesis was discarded on that evidence.  ab.sh now re-stages every launch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-27 12:04:43 -05:00
co-authored by Claude Fable 5
parent c6cafadb28
commit 740ef2b0a7
10 changed files with 514 additions and 55 deletions
+82
View File
@@ -0,0 +1,82 @@
# gauge-ab -- the cockpit A/B rig
Runs the reconstruction and the shipped 4.10 binary against the **same mount**
(same `GAUGE/L4GAUGE.CFG`, same egg, same mech) and compares what each one
paints into the gauge framebuffer.
With `L4VIDEO=OFF` and `L4GAUGE=640x480x16`, the DOSBox window *is* the packed
gauge framebuffer -- the exact buffer the pod's VDB splits into its physical
heads. So a screen grab is a lossless read of the cockpit.
## Running
```bash
bash ab.sh rec # stage the fresh build + run the reconstruction
bash ab.sh shp # run the shipped binary
powershell -File grab.ps1 -Out ours.png
python3 planes.py shipped.png ours.png
```
**Always launch through `ab.sh`.** The confs run `BTL4REC.EXE` out of the
mount, *not* `build410/btl4opt.exe`. Forgetting to re-stage silently measures
the previous binary -- on 2026-07-27 that cost a full round of wrong
conclusions (a fix "did nothing" three times because the tested exe never
changed, and the correct first hypothesis was discarded on that evidence).
`ab.sh` re-stages every time and kills any running DOSBox first (two
instances share `OUT.TXT` and corrupt the trace).
## The instruments
### `planes.py` -- the per-head scoreboard (use this first)
The framebuffer is **plane-packed**: `L4GAUGE.CFG` gives each of the ten
logical ports a *bit mask*, not a rectangle --
```
configure(8,Heat, 0,0x4000,clut2,blue, NULL); # UL
configure(9,Comm, 0,0x8000,clut2,red, NULL); # UR
```
-- so all six heads occupy the same x/y and are separated only by bit. RGB
comparison is therefore meaningless: a "magenta artifact" is really *the wrong
heads lit at that pixel*. DOSBox presents the buffer as RGB565 with bit
replication, so the original 16-bit word is recovered exactly by
`R>>3, G>>2, B>>3`.
`planes.py` reports, per head, how many pixels the shipped binary lights, how
many we light, how many we're **missing**, how many are **extra**, and the
bounding box of the extras. Missing vs extra is the useful signal: extras are
*our* bugs, missing is unbuilt work.
### `score.py` -- one whole-frame number
Coarse (the big MFD heads dominate it). Good for "did this regress", useless
for locating anything. Prefer `planes.py`.
### `BT_VIS_LOG` -- the widget map
`set BT_VIS_LOG=1` in the conf makes the build print every gauge widget the
interpreter constructs, with its port and authored position:
```
[w] pilotList port=9 at 0,0
[w] rankAndScore port=9 at 33,0
[gen] port=0 x=443 y=322 lampA='sgena.pcc' ... on=1 col0=3 col1=9
[cluster] port=4 x=0 y=240 title='qsensors.pcc' banner=1
```
The `[w]` line comes from the single dispatch every widget is built through
(`MethodDescription::Execute`, MUNGA/GAUGREND.CPP), so the map is complete.
This is what turns "a diff at framebuffer (67,3)" into "that head has no
drawing widget there, so look elsewhere".
## Method
Source inspection has now produced **five** wrong suspects on this one
artifact; each was settled -- or killed -- by one instrumented run. So:
1. `planes.py` first: *which head*, and is it missing or extra?
2. `BT_VIS_LOG`: which widgets exist on that head, at what position?
3. Only then read code, and only that widget's.
And re-stage before believing any measurement.
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
#
# gauge-ab: stage + run one side of the cockpit A/B rig.
#
# ab.sh rec -- stage the freshly built reconstruction and run it
# ab.sh shp -- run the shipped 4.10 binary
#
# STAGING IS THE WHOLE POINT. The rig's confs run BTL4REC.EXE / BTL4SHP.EXE
# out of the shared mount, NOT build410/btl4opt.exe. Forgetting to copy the
# new build over BTL4REC.EXE silently measures the PREVIOUS binary -- which
# on 2026-07-27 cost a full round of wrong conclusions (a change "did
# nothing" three times because the tested exe never changed). So: always
# launch through this script, and it always re-stages first.
#
set -e
R=/c/VWE/TeslaRel410/restoration
IMG=$R/build410/run/image
SIDE=${1:-rec}
# one instance at a time -- two DOSBoxes share OUT.TXT and corrupt the trace
if tasklist //FI "IMAGENAME eq dosbox-x.exe" 2>/dev/null | grep -qi dosbox-x; then
echo "killing running dosbox-x"
taskkill //F //IM dosbox-x.exe > /dev/null 2>&1 || true
sleep 2
fi
if [ "$SIDE" = "rec" ]; then
SRC=$R/build410/btl4opt.exe
DST=$IMG/BTL4REC.EXE
[ -f "$SRC" ] || { echo "no build at $SRC -- run build410.sh link"; exit 1; }
cp -f "$SRC" "$DST"
echo "staged $(ls -la "$DST" | awk '{print $5, $6, $7, $8}') <- $(basename $SRC)"
CONF=$R/build410/run/vis_rec.conf
else
echo "shipped side (no staging)"
CONF=$R/build410/run/vis_shp.conf
fi
rm -f "$IMG/OUT.TXT"
cd /c/VWE/TeslaRel410/emulator/src/src
./dosbox-x.exe -conf "$CONF" > /dev/null 2>&1 &
echo "launched $SIDE ($(basename $CONF)) -- pid $!"
+81 -54
View File
@@ -1,54 +1,81 @@
param(
[Parameter(Mandatory=$true)][string]$Out,
[string]$Match = "DOSBox-X"
)
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class W {
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
public delegate bool EnumProc(IntPtr h, IntPtr p);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
}
"@
$found = @()
$cb = [W+EnumProc]{
param($h, $p)
if ([W]::IsWindowVisible($h)) {
$sb = New-Object System.Text.StringBuilder 512
[void][W]::GetWindowText($h, $sb, 512)
$t = $sb.ToString()
if ($t -like "*$Match*") { $script:found += ,@($h, $t) }
}
return $true
}
[void][W]::EnumWindows($cb, [IntPtr]::Zero)
if ($found.Count -eq 0) { Write-Output "NO-WINDOW"; exit 1 }
Add-Type -AssemblyName System.Drawing
$i = 0
foreach ($f in $found) {
$h = $f[0]; $title = $f[1]
[void][W]::SetForegroundWindow($h)
Start-Sleep -Milliseconds 1200
$r = New-Object W+RECT
[void][W]::GetWindowRect($h, [ref]$r)
$w = $r.R - $r.L; $ht = $r.B - $r.T
if ($w -le 0 -or $ht -le 0) { continue }
$bmp = New-Object System.Drawing.Bitmap $w, $ht
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($r.L, $r.T, 0, 0, (New-Object System.Drawing.Size $w, $ht))
$path = if ($found.Count -eq 1) { $Out } else { $Out -replace '\.png$', "_$i.png" }
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
Write-Output "SAVED $path ${w}x${ht} [$title]"
$i++
}
param(
[Parameter(Mandatory=$true)][string]$Out,
[string]$Match = "DOSBox-X"
)
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class W {
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
[DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
public delegate bool EnumProc(IntPtr h, IntPtr p);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
}
"@
$found = @()
$cb = [W+EnumProc]{
param($h, $p)
if ([W]::IsWindowVisible($h)) {
$sb = New-Object System.Text.StringBuilder 512
[void][W]::GetWindowText($h, $sb, 512)
$t = $sb.ToString()
if ($t -like "*$Match*") { $script:found += ,@($h, $t) }
}
return $true
}
[void][W]::EnumWindows($cb, [IntPtr]::Zero)
if ($found.Count -eq 0) { Write-Output "NO-WINDOW"; exit 1 }
Add-Type -AssemblyName System.Drawing
function Test-Blank($bmp) {
# sample a grid; blank/uniform => PrintWindow failed on this surface
$seen = @{}
for ($y = 10; $y -lt $bmp.Height - 10; $y += 23) {
for ($x = 10; $x -lt $bmp.Width - 10; $x += 29) {
$seen[$bmp.GetPixel($x, $y).ToArgb()] = $true
if ($seen.Count -gt 6) { return $false }
}
}
return $true
}
$i = 0
foreach ($f in $found) {
$h = $f[0]; $title = $f[1]
[void][W]::SetForegroundWindow($h)
Start-Sleep -Milliseconds 600
$r = New-Object W+RECT
[void][W]::GetWindowRect($h, [ref]$r)
$w = $r.R - $r.L; $ht = $r.B - $r.T
if ($w -le 0 -or $ht -le 0) { continue }
# PRIMARY: PrintWindow(PW_RENDERFULLCONTENT) -- immune to occlusion
$bmp = New-Object System.Drawing.Bitmap $w, $ht
$g = [System.Drawing.Graphics]::FromImage($bmp)
$hdc = $g.GetHdc()
$ok = [W]::PrintWindow($h, $hdc, 2)
$g.ReleaseHdc($hdc)
$method = "PrintWindow"
if (-not $ok -or (Test-Blank $bmp)) {
# FALLBACK: screen copy (only valid if nothing is in front)
$g.Dispose(); $bmp.Dispose()
$bmp = New-Object System.Drawing.Bitmap $w, $ht
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($r.L, $r.T, 0, 0, (New-Object System.Drawing.Size $w, $ht))
$method = "CopyFromScreen(FALLBACK -- occlusion-sensitive)"
}
$path = if ($found.Count -eq 1) { $Out } else { $Out -replace '\.png$', "_$i.png" }
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
Write-Output "SAVED $path ${w}x${ht} via $method [$title]"
$i++
}
+74
View File
@@ -0,0 +1,74 @@
#
# PER-HEAD A/B scoreboard for the packed gauge framebuffer.
#
# L4GAUGE.CFG configures ten logical ports onto ONE 640x480x16 buffer, each
# port owning a BIT MASK, not a rectangle -- the VDB later splits the planes
# into the pod's separate physical heads. Comparing RGB is therefore
# meaningless; the only useful question is "which HEAD differs, and where".
#
# DOSBox presents the buffer as RGB565 with bit replication, so the original
# 16-bit word is recovered exactly by R>>3, G>>2, B>>3.
#
import sys
from PIL import Image
BOX = (8, 52, 648, 531)
PORTS = [
("sec", 0x003F), # the 3-D scene (6-bit colour)
("overlay", 0x00C0),
("Mfd1", 0x0100), ("Eng1", 0x0200),
("Mfd2", 0x0400), ("Eng2", 0x0800),
("Mfd3", 0x1000), ("Eng3", 0x2000),
("Heat", 0x4000), ("Comm", 0x8000),
]
def words(path):
im = Image.open(path).convert("RGB").crop(BOX)
w, h = im.size
px = im.load()
out = []
for y in range(h):
row = []
for x in range(w):
r, g, b = px[x, y]
row.append(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3))
out.append(row)
return out, w, h
def main(ship_path, ours_path):
A, w, h = words(ship_path)
B, w2, h2 = words(ours_path)
if (w, h) != (w2, h2):
print("SIZE MISMATCH")
return
print("%-8s %8s %8s %8s %8s %s" %
("head", "shipped", "ours", "missing", "extra", "extra-bbox"))
for name, mask in PORTS:
sa = so = both = 0
xs = []
ys = []
for y in range(h):
ra, rb = A[y], B[y]
for x in range(w):
a = (ra[x] & mask) != 0
b = (rb[x] & mask) != 0
if a:
sa += 1
if b:
so += 1
if a and b:
both += 1
if b and not a:
xs.append(x)
ys.append(y)
bbox = ("(%d,%d)-(%d,%d)" % (min(xs), min(ys), max(xs), max(ys))
if xs else "-")
print("%-8s %8d %8d %8d %8d %s" %
(name, sa, so, sa - both, so - both, bbox))
main(sys.argv[1], sys.argv[2])
+40
View File
@@ -0,0 +1,40 @@
import sys
from PIL import Image
BOX = (8, 52, 648, 531) # the DOSBox client area = the gauge framebuffer
SHIPPED = "shipped.png"
def load(p):
return Image.open(p).convert("RGB").crop(BOX)
def magenta(p):
return p[0] > 150 and p[2] > 150 and p[1] < 140
def score(path):
a, b = load(SHIPPED), load(path)
if a.size != b.size:
return "%-10s SIZE MISMATCH %s vs %s" % (path, a.size, b.size)
pa, pb = a.load(), b.load()
w, h = a.size
same = lit_ship = lit_both = mag = 0
for y in range(h):
for x in range(w):
s, o = pa[x, y], pb[x, y]
if s == o:
same += 1
if sum(s) > 60:
lit_ship += 1
if sum(o) > 60:
lit_both += 1
if 0 <= x < 200 and 0 <= y < 60 and magenta(o):
mag += 1
n = w * h
return "%-10s identical %5.1f%% coverage %3d%% title-band magenta %4d" % (
path, 100.0 * same / n, round(100.0 * lit_both / max(lit_ship, 1)), mag)
for p in sys.argv[1:]:
print(score(p))