From 24ea06574cc7df2c109e6bdb95f21d68b846e572 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 26 Jul 2026 22:29:01 -0500 Subject: [PATCH] BT410 Phase 5.3.28: the visual A/B pass -- 93% pixel-identical to the shipped cockpit Ran the reconstructed exe and the shipped ALPHA_1 binary against the SAME GAUGE content, same mount, gauges on, and diffed the 640x480x16 framebuffers. OUR EXE DRAWS THE COCKPIT: 90.1% pixel-identical on the first run, and every difference traced to a single root cause. The authored aux-screen block (screen number / background placement / label) lives in the subsystem resource -- which is freed with the Mech ctor's stream buffer (the 5.3.24 use-after-free landmine). Both this tree and the BT411 port had stubbed it, which is why BT411's displays show the same artifacting. PoweredSubsystem now caches the block at ctor time with accessors, and btl4gau2 uses the AUTHORED screen instead of a roster-order stand-in and builds the placement strip art. Panels snapped onto their authored screens -- weapon dials now land on the same panels as the shipped exe, mech icons appear on the sensor/myomer panels, generator letters correct -- taking the match to 93.2% identical / 85% coverage. Remaining differences are donor-inherited stubs: the panel title art (now proven to come from the q-strip statusImage path, not auxScreenLabel -- the label blit is wired and guarded regardless), the pilot name bitmap, and some lamp frames. The A/B rig is banked at emulator/render-bridge/gauge-ab/ (both confs + the window-grab script) so the comparison is repeatable. Co-Authored-By: Claude Fable 5 --- emulator/render-bridge/gauge-ab/grab.ps1 | 52 +++++++++++++++ emulator/render-bridge/gauge-ab/vis_rec.conf | 30 +++++++++ emulator/render-bridge/gauge-ab/vis_shp.conf | 30 +++++++++ restoration/source410/BT/POWERSUB.CPP | 11 ++++ restoration/source410/BT/POWERSUB.HPP | 21 +++++++ restoration/source410/BT_L4/BTL4GAU2.CPP | 63 +++++++++++++++++-- .../source410/BT_L4/GAUGE-BLOCK.NOTES.md | 40 ++++++++++++ 7 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 emulator/render-bridge/gauge-ab/grab.ps1 create mode 100644 emulator/render-bridge/gauge-ab/vis_rec.conf create mode 100644 emulator/render-bridge/gauge-ab/vis_shp.conf diff --git a/emulator/render-bridge/gauge-ab/grab.ps1 b/emulator/render-bridge/gauge-ab/grab.ps1 new file mode 100644 index 00000000..bb743e4f --- /dev/null +++ b/emulator/render-bridge/gauge-ab/grab.ps1 @@ -0,0 +1,52 @@ +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] + $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++ +} diff --git a/emulator/render-bridge/gauge-ab/vis_rec.conf b/emulator/render-bridge/gauge-ab/vis_rec.conf new file mode 100644 index 00000000..db622833 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/vis_rec.conf @@ -0,0 +1,30 @@ +[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 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/emulator/render-bridge/gauge-ab/vis_shp.conf b/emulator/render-bridge/gauge-ab/vis_shp.conf new file mode 100644 index 00000000..3b95f8a8 --- /dev/null +++ b/emulator/render-bridge/gauge-ab/vis_shp.conf @@ -0,0 +1,30 @@ +[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 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 +BTL4SHP.EXE -egg test.egg > OUTSHP.TXT +echo GAME-RC=%errorlevel% >> OUT.TXT +32rtm.exe -u +exit diff --git a/restoration/source410/BT/POWERSUB.CPP b/restoration/source410/BT/POWERSUB.CPP index 97757a28..0f660557 100644 --- a/restoration/source410/BT/POWERSUB.CPP +++ b/restoration/source410/BT/POWERSUB.CPP @@ -116,6 +116,17 @@ PoweredSubsystem::PoweredSubsystem( thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient; startTime = subsystem_resource->startTime; + + // + // Cache the authored aux-screen block NOW -- the resource memory dies + // with the Mech ctor's stream buffer. + // + auxScreenNumber = subsystem_resource->auxScreenNumber; + auxScreenPlacement = subsystem_resource->auxScreenPlacement; + Str_Copy(auxScreenLabel, subsystem_resource->auxScreenLabel, + sizeof(auxScreenLabel)); + Str_Copy(engScreenLabel, subsystem_resource->engScreenLabel, + sizeof(engScreenLabel)); startTimer = startTime; voltageScale = 1.0f; diff --git a/restoration/source410/BT/POWERSUB.HPP b/restoration/source410/BT/POWERSUB.HPP index d84ddb92..4200ad0c 100644 --- a/restoration/source410/BT/POWERSUB.HPP +++ b/restoration/source410/BT/POWERSUB.HPP @@ -154,6 +154,23 @@ virtual void DeathReset(Logical full_reset); + // + // The AUTHORED aux-screen assignment (streamed). CACHED at ctor + // time because MechSubsystem::resource points into the Mech ctor's + // padded stream copy, which is FREED before the ctor returns -- the + // 5.3.24 use-after-free landmine. The cockpit's engineering panels + // key their screen number, background placement and title label off + // these. + // + int + GetAuxScreenNumber() const { Check(this); return auxScreenNumber; } + int + GetAuxScreenPlacement() const { Check(this); return auxScreenPlacement; } + const char* + GetAuxScreenLabel() const { Check(this); return auxScreenLabel; } + const char* + GetEngScreenLabel() const { Check(this); return engScreenLabel; } + Subsystem* ResolveVoltageSource() { return voltageSource.Resolve(); } int @@ -233,6 +250,10 @@ SubsystemConnection voltageSource; AlarmIndicator electricalStateAlarm; AlarmIndicator modeAlarm; + int auxScreenNumber; + int auxScreenPlacement; + char auxScreenLabel[64]; + char engScreenLabel[64]; Scalar thermalResistivityCoefficient; Scalar startTime; Scalar startTimer; diff --git a/restoration/source410/BT_L4/BTL4GAU2.CPP b/restoration/source410/BT_L4/BTL4GAU2.CPP index b71d1bbc..0b380084 100644 --- a/restoration/source410/BT_L4/BTL4GAU2.CPP +++ b/restoration/source410/BT_L4/BTL4GAU2.CPP @@ -1163,6 +1163,13 @@ static Logical } } +// +// The AUTHORED aux screen (binary sub+0x1dc). PoweredSubsystem caches the +// streamed block at ctor time (its resource memory dies with the Mech +// ctor's stream buffer), so the panels land on the screens the artists +// assigned -- not in roster order. A non-powered panel subsystem (or a +// model that authored nothing) falls back to the sequential walk. +// static int AuxScreenAssignmentOf( Entity *mech, @@ -1174,6 +1181,16 @@ static int position = 0, count = mech->GetSubsystemCount(); + if (subsystem->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) + { + int authored = + ((PoweredSubsystem *)subsystem)->GetAuxScreenNumber(); + if (authored > 0) + { + return authored; + } + } + for (i = 1; i < count; i++) // slot 0 = the controls mapper { Subsystem @@ -1748,7 +1765,22 @@ void num.Draw(&localView, 0.0f); } - //---- subsystem label bitmap @(0x125,0x172): SKIPPED (no live label). + //---- subsystem label bitmap @(0x125,0x172): the AUTHORED aux-screen + // label, cached on PoweredSubsystem at ctor time (the streamed + // resource dies with the Mech ctor's buffer). This is the panel + // title art -- "SENSOR CLUSTER", "MYOMERS", the weapon name and + // range strip. + // + if (sub->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) + { + const char *label = + ((PoweredSubsystem *)sub)->GetAuxScreenLabel(); + if (label != NULL && *label != '') + { + localView.SetColor(0xff); + DrawPortBackground(&localView, warehouse, 0x125, 0x172, label); + } + } //---- type-specific label cells (the authentic ClassID dispatch) switch (sub->GetClassID()) @@ -1868,13 +1900,32 @@ SubsystemCluster::SubsystemCluster( } // - // Background strip art: the binary picked image_names[auxScreenPlacement] - // (sub+0x1e0). STAND-IN: no live placement source (see the aux-screen - // ledger) -> the backdrop child is not built. image_names stays in the - // signature for the day the placement feed lands. + // Background strip art: the binary picks image_names[auxScreenPlacement] + // (sub+0x1e0) -- LIVE now that PoweredSubsystem caches the authored + // placement (the resource itself is freed with the Mech ctor's stream + // buffer). A model that authored nothing keeps the bare frame. // - (void)image_names; background = NULL; + { + int placement = -1; + if (subsystem_in != NULL + && subsystem_in->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) + { + placement = + ((PoweredSubsystem *)subsystem_in)->GetAuxScreenPlacement(); + } + if (placement >= 0 && placement < 8 && image_names != NULL + && image_names[placement] != NULL) + { + background = new BackgroundBitmap( + mfd_mode, renderer_in, owner_ID, mfd_port, + x, y, image_names[placement], + 1, // opaque + 0xff, 0 // fg / bg + ); + Register_Object(background); + } + } //------------------------------------------------------------ // Paint the panel frame image into the MFD port. diff --git a/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md b/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md index 18ed62b7..e347bb21 100644 --- a/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md +++ b/restoration/source410/BT_L4/GAUGE-BLOCK.NOTES.md @@ -475,3 +475,43 @@ not state) and Searchlight/LightOn (Searchlight is a memberless PowerWatcher subclass) -- both fall to the zero cell and draw static, which is correct until those bricks exist. Gauge fight + smoke + novice all clean. + +## 5.3.28 -- THE VISUAL A/B PASS (first side-by-side vs the shipped exe) + +RIG (repeatable): both exes live in the same mount reading the SAME GAUGE +content -- run/image/BTL4SHP.EXE (the shipped ALPHA_1 binary) and +BTL4REC.EXE (ours); confs run/vis_shp.conf + run/vis_rec.conf (gauges on: +L4GAUGE=640x480x16, HEAPSIZE=15000000, machine=svga_s3, output=opengl). +Capture: scratchpad/grab.ps1 (Win32 window grab) at ~70s, then a PIL diff. +With L4VIDEO off the DOSBox window IS the 640x480x16 gauge framebuffer -- +the packed buffer the VDB splits into the six heads -- so no VPX needed. + +RESULT: **our reconstructed exe draws the cockpit.** Measured against the +shipped framebuffer (chrome cropped): + + first run 90.1% pixels identical, 82% of shipped's lit pixels lit + after fix 93.2% pixels identical, 85% coverage + +Every difference in the first run traced to ONE root cause: the authored +aux-screen block (number / placement / label) lives in the subsystem +resource, which is FREED with the Mech ctor's stream buffer (the 5.3.24 +use-after-free landmine). Both this tree AND the BT411 port had stubbed it +("PoweredSubsystem keeps NO live copy") -- which is why BT411's displays +show the same artifacting. FIX: PoweredSubsystem now CACHES the block at +ctor time (auxScreenNumber / auxScreenPlacement / auxScreenLabel / +engScreenLabel + accessors), and btl4gau2 uses the authored screen instead +of a roster-order stand-in and builds the placement strip art. Panels +immediately moved onto their authored screens (the weapon recharge dials +land on the same panels as the shipped exe; mech icons appear on the +sensor/myomer panels; the generator letters correct to their authored +screens). + +STILL DIFFERING (all donor-inherited stubs, unchanged by this pass): +the panel TITLE art ("SENSOR CLUSTER" / "MYOMERS" / "ERMED LASER RANGE +500M" / "TYPE II 65 TONS"), the pilot name ("Aeolus"), and some lamp +frames ("OFF x" vs the shipped state digits). The label blit is now wired +through GetAuxScreenLabel() but draws nothing on bhk1 -- so those titles +come from the q-strip statusImage path (the 8 qblh*.pcc typeStrings +VehicleSubSystems passes to DrawStatusCells), NOT from auxScreenLabel. +That is the next thread. Name bitmaps stay inert (CreateMutantPixelmap8 / +LookupPlayerNameBitmap bring-up stubs).