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))
+13
View File
@@ -152,6 +152,19 @@
// Local Data -- the published control inputs (the streamed mappings write
// these; InterpretControls, phase 5.3, reads them to drive the mech).
//
public:
//
// The POD ROSTER (binary mapper attributes PilotArrayPage/PilotArray,
// ids 15/16): the comm scoreboard's row source. Zero = no roster
// authored (a solo mission), which is why the shipped cockpit leaves
// that page blank.
//
int
GetPilotArray() const { Check(this); return pilotArray; }
int
GetPilotArrayPage() const { Check(this); return pilotArrayPage; }
protected:
ControlsJoystick stickPosition;
Scalar throttlePosition;
+14
View File
@@ -1427,6 +1427,15 @@ GeneratorCluster::GeneratorCluster(
generatorOn, "TwoState");
Register_Object(lampB);
if (getenv("BT_VIS_LOG"))
DEBUG_STREAM << "[gen] port=" << graphics_port_number
<< " x=" << x << " y=" << y
<< " lampA='" << lampA_image << "' at " << (x + 0xC) << "," << (y + 2)
<< " lampB='" << lampB_image << "' at " << (x + 2) << "," << (y + 2)
<< " on=" << *generatorOn
<< " col0=" << lamp_on_color << " col1=" << lamp_off_color
<< "\n" << flush;
subsystem = subsystem_in; // @0xAC
_reserved0xB0 = 0; // @0xB0 (binary leaves uninit; zeroed)
@@ -1953,6 +1962,11 @@ SubsystemCluster::SubsystemCluster(
);
Register_Object(titleBanner);
}
if (getenv("BT_VIS_LOG"))
DEBUG_STREAM << "[cluster] port=" << mfd_port
<< " x=" << x << " y=" << y
<< " title='" << (title ? title : "(null)")
<< "' banner=" << (titleBanner ? 1 : 0) << "\n" << flush;
}
//------------------------------------------------------------
+59 -1
View File
@@ -37,6 +37,10 @@
#include <btl4.hpp>
#pragma hdrstop
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(MISSION_HPP)
# include <mission.hpp>
#endif
@@ -53,6 +57,10 @@
# include <btplayer.hpp>
#endif
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(NTTMGR_HPP)
# include <nttmgr.hpp>
#endif
@@ -104,6 +112,56 @@ static Player *
return NULL;
}
//
//#############################################################################
// COMM-PAGE roster gate -- the cockpit's pilot list is NOT the mission's
// player list. The binary fed it from the viewpoint mech's ControlsMapper
// pilot array (mapper attributes PilotArrayPage / PilotArray, ids 15/16),
// which the pod network authors; a mission with no pod roster leaves it
// zero and the shipped cockpit draws the page EMPTY.
//
// A/B EVIDENCE (5.3.32): with the ungated walk our slot 0 resolved the solo
// mission player and painted its icon in colour 0xff -- 404 magenta pixels
// in the Comm title band where the shipped binary paints the black erase
// rect. Gate the ROW SOURCE here, not PilotList::Execute: Execute's empty
// branch still has to run, because erasing IS what the shipped page draws.
//
// PlayerStatus (the observer/camera scoreboard) keeps the ungated walk -- it
// is a mission-wide standings page, not the pod roster.
//#############################################################################
//
static Player *
ResolveCommRosterPlayer(int slot)
{
if (application == NULL)
{
return NULL;
}
Entity
*viewpoint = application->GetViewpointEntity();
if (viewpoint == NULL || viewpoint->GetSubsystemCount() <= 0)
{
return NULL;
}
Subsystem
*controls = viewpoint->GetSubsystem(0);
if (controls == NULL
|| !controls->IsDerivedFrom(MechControlsMapper::ClassDerivations))
{
return NULL;
}
if (((MechControlsMapper *) controls)->GetPilotArray() == 0)
{
return NULL; // no pod roster authored -> the page stays empty
}
return ResolveRosterPlayer(slot);
}
//
//#############################################################################
// KILLS access (the gauge scoring wave): the KILLS column must read the SAME
@@ -916,7 +974,7 @@ void
*pilot;
if (built == False)
{
pilot = ResolveRosterPlayer(currentSlot);
pilot = ResolveCommRosterPlayer(currentSlot);
if (pilot == NULL)
{
built = True; // past the first empty slot -> rest empty this cycle
@@ -622,3 +622,103 @@ use, switch the grab to PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT), which
asks the window to render itself and works while occluded (verify it against
the OpenGL output mode first -- if PrintWindow comes back blank on GL, set
[sdl] output=surface in the vis_*.conf for capture runs).
================================================================================
5.3.32 -- THE PER-HEAD A/B: the packed framebuffer, read one head at a time
================================================================================
The stray magenta blocks are GONE (404 px -> 0), and the three engineering
heads are now PIXEL-IDENTICAL to the shipped binary.
Per-head, shipped vs reconstruction (planes.py):
head shipped ours missing extra
Eng1 17981 17981 0 0 <- exact
Eng2 17981 17981 0 0 <- exact
Eng3 17981 17981 0 0 <- exact
Comm 15656 13557 2099 0 <- nothing wrong drawn
Heat 21374 20913 566 105
overlay 3283 3325 55 97
sec 35423 36096 357 1030
Mfd1 41784 37040 8807 4063 <- the remaining climb
Mfd2 40799 39197 6882 5280
Mfd3 32146 27870 6935 2659
TWO THINGS CAME OUT OF THIS PASS. The fix is the smaller one.
--------------------------------------------------------------------------------
1. THE INSTRUMENT: the framebuffer is PLANE-PACKED, so RGB diffing is a lie
--------------------------------------------------------------------------------
L4GAUGE.CFG gives every port a BIT MASK, not a rectangle:
configure(8,Heat, 0,0x4000,clut2,blue, NULL); # UL
configure(9,Comm, 0,0x8000,clut2,red, NULL); # UR
All six heads live at the SAME x/y and are separated only by bit -- that is
exactly the packing the VDB splits into the pod's physical screens. So the
"magenta artifact" I had been chasing for two rounds was never a colour bug:
magenta 0xD2BB simply means Comm+Heat+Mfd3+Eng1 lit at a pixel where the
shipped buffer has Eng1+Eng2+Eng3. Decode the word (R>>3,G>>2,B>>3 inverts
DOSBox's RGB565 bit replication), mask per port, and each head can be scored
on its own. That is emulator/render-bridge/gauge-ab/planes.py, and it turned
a vague 89.7%-identical number into "Eng1 has 1030 extra pixels at (67,3)".
Second instrument, same session: BT_VIS_LOG. MethodDescription::Execute
(MUNGA/GAUGREND.CPP) is the ONE dispatch every gauge widget is constructed
through, so a single line there prints the entire cockpit as a map --
[w] pilotList port=9 at 0,0
[w] rankAndScore port=9 at 33,0
-- name, port, authored position. That is how "the Eng1 head has a spurious
block at (67,3)" became "port 3 has no drawing widget at all, so stop reading
Eng1 code". Both instruments are banked with a README in gauge-ab/.
--------------------------------------------------------------------------------
2. THE FIX: the Comm page's roster is the POD roster, not the mission's
--------------------------------------------------------------------------------
PilotList::Execute drew slot 0 from the EntityManager "Players" group, which
in a solo mission resolves the local player and paints its icon in colour
0xff. The shipped binary feeds that page from the viewpoint mech's
ControlsMapper pilot array (mapper attributes PilotArrayPage / PilotArray,
ids 15/16) -- authored by the pod network, zero in a solo mission, so the
shipped page draws EMPTY.
The gate belongs in the ROW SOURCE (ResolveCommRosterPlayer), NOT in
PilotList::Execute: Execute's empty branch still has to run, because erasing
is what the shipped page actually draws there. Gating Execute instead
removed the erase too and cost a point of coverage. PlayerStatus keeps the
ungated walk -- it is the observer/camera standings page, not the pod roster.
MECHMPPR.HPP grew GetPilotArray()/GetPilotArrayPage() for the gate.
Result: Comm extra 1253 -> 0, Eng1 extra 1030 -> 0, and the 2099 Comm pixels
we are MISSING were missing before the change too, so the gate is a strict
improvement. (Those 2099 are unbuilt Comm content -- a separate thread.)
--------------------------------------------------------------------------------
THE METHOD LESSON, WHICH COST MORE THAN THE BUG
--------------------------------------------------------------------------------
The rig's confs run BTL4REC.EXE out of the shared mount; `build410.sh link`
writes build410/btl4opt.exe. Nothing connected the two. Three consecutive
measurements ran a binary from 11:02 while I believed I was testing 11:05,
11:13 and 11:32 builds -- so a change that "did nothing" three times was in
fact never tested, and I discarded the CORRECT first hypothesis (the roster)
on that evidence and went hunting through generator lamps, blit primitives
and port tables instead.
Now enforced: emulator/render-bridge/gauge-ab/ab.sh re-stages the exe on
every launch and kills any running DOSBox first (two instances share OUT.TXT
and interleave the trace). Launch the rig no other way.
The earlier note in this file still stands and now has a fifth data point:
for anything cockpit-visual, measure before reading code. Five suspects came
from source inspection -- PilotList slot coords, PlayerStatus off-by-one, the
generator TwoState lamp polarity, the shared DrawBitMap primitive, and the
Plasma port table -- and every one of them was killed by an instrumented run
that took four minutes. The generator lamps in particular looked airtight
until the widget map showed they draw on port 0, nowhere near the pixels.
NEXT ON THIS HEAD-BY-HEAD LADDER: the three MFD quadrant heads (Mfd1/2/3),
which now hold essentially all of the remaining difference -- ~22.6K missing
and ~12K extra between them, against ~0 on the engineering heads. Those are
the SubsystemCluster quadrant panels ([cluster] port=2/4/6 in the map).
+8
View File
@@ -2734,6 +2734,14 @@ void
//-------------------------------------------------------------
// Execute the call
//-------------------------------------------------------------
// COCKPIT WIDGET MAP (env BT_VIS_LOG): the single dispatch every
// gauge widget is built through -- name, port and authored position.
// This is the map that turns an A/B pixel difference into a widget.
if (getenv("BT_VIS_LOG"))
DEBUG_STREAM << "[w] " << name
<< " port=" << display_port_index
<< " at " << position.x << "," << position.y
<< "\n" << flush;
Check_Pointer(execute);
# if DEBUG_LEVEL > 0
Logical