diff --git a/emulator/render-bridge/gauge-ab/README.md b/emulator/render-bridge/gauge-ab/README.md new file mode 100644 index 00000000..833bfedb --- /dev/null +++ b/emulator/render-bridge/gauge-ab/README.md @@ -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. diff --git a/emulator/render-bridge/gauge-ab/ab.sh b/emulator/render-bridge/gauge-ab/ab.sh new file mode 100644 index 00000000..4e5b9a07 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/ab.sh @@ -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 $!" diff --git a/emulator/render-bridge/gauge-ab/grab.ps1 b/emulator/render-bridge/gauge-ab/grab.ps1 index d542b54f..6d9e995c 100644 --- a/emulator/render-bridge/gauge-ab/grab.ps1 +++ b/emulator/render-bridge/gauge-ab/grab.ps1 @@ -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++ +} diff --git a/emulator/render-bridge/gauge-ab/planes.py b/emulator/render-bridge/gauge-ab/planes.py new file mode 100644 index 00000000..45343af9 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/planes.py @@ -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]) diff --git a/emulator/render-bridge/gauge-ab/score.py b/emulator/render-bridge/gauge-ab/score.py new file mode 100644 index 00000000..aac37444 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/score.py @@ -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)) diff --git a/restoration/source410/BT/MECHMPPR.HPP b/restoration/source410/BT/MECHMPPR.HPP index dfe3ee61..e38b8cc9 100644 --- a/restoration/source410/BT/MECHMPPR.HPP +++ b/restoration/source410/BT/MECHMPPR.HPP @@ -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; diff --git a/restoration/source410/BT_L4/BTL4GAU2.CPP b/restoration/source410/BT_L4/BTL4GAU2.CPP index 887fa462..8ea37b0c 100644 --- a/restoration/source410/BT_L4/BTL4GAU2.CPP +++ b/restoration/source410/BT_L4/BTL4GAU2.CPP @@ -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; } //------------------------------------------------------------ diff --git a/restoration/source410/BT_L4/BTL4GAU3.CPP b/restoration/source410/BT_L4/BTL4GAU3.CPP index cdad1163..5e131897 100644 --- a/restoration/source410/BT_L4/BTL4GAU3.CPP +++ b/restoration/source410/BT_L4/BTL4GAU3.CPP @@ -37,6 +37,10 @@ #include #pragma hdrstop +#if !defined(MECHMPPR_HPP) +# include +#endif + #if !defined(MISSION_HPP) # include #endif @@ -53,6 +57,10 @@ # include #endif +#if !defined(MECHMPPR_HPP) +# include +#endif + #if !defined(NTTMGR_HPP) # include #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 diff --git a/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md b/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md index b00bf043..ad27453a 100644 --- a/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md +++ b/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md @@ -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). diff --git a/restoration/source410/MUNGA/GAUGREND.CPP b/restoration/source410/MUNGA/GAUGREND.CPP index 93fd499d..590f7816 100644 --- a/restoration/source410/MUNGA/GAUGREND.CPP +++ b/restoration/source410/MUNGA/GAUGREND.CPP @@ -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