SeekVoltageGraph: full reconstruction -- the eng-page POWER graph + top-box eraser (Gitea #11)

The #10 audit's one WRONG: the port Execute was a bring-up no-op, so the
emitter/myomer engineering pages never erased their top data box -> stale
sibling-page ghosts on the shared Eng bit-plane (SYSTEM 10 PPC showing the
Streak ammo box, etc).  Full faithful widget landed:

- btl4gau2: ctor/BecameActive/Execute/clear/ticks/cursor recovered from the
  capstone disasm of @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/
  @004c6c6c (Ghidra dropped every x87 arg).  Plot: v=0..12000 step 1200,
  x=Round(response(v)*230), y=Round(v*(1/12000)*187) (ld80 @004c6bd0/@004c6d74
  = exactly 1/12000); change-test samples the response at 12000V vs the 9999
  activation sentinel; XOR op for tick/cursor move-by-redraw; destroyed branch
  centres edestryd.pcc and revives via own vtbl+0xC (BecameActive, slot 3 of
  PTR_0051a1fc -- vtable-dump verified).
- The vtbl+0x3C sampler identified from the binary vtables: Emitter slot 15
  @004bb42c = sqrt(P(v)/2.0e8), P @004bb3f4 = damageFraction*v^2*0.5*
  energyCoefficient; Myomers slot 15 @004b8f94 = sqrt(AvailableOutput(v)*3.6/
  350).  FUN_004dd138 == sqrt (part_015.c:4026): myomers' fabs reading
  corrected, GetSpeedReading renamed SeekVoltageResponse.  Port dispatch via
  complete-type bridges BTSeekVoltageSample/BTMyomersSeekSample +
  BTSubsystemDestroyed (databinding rule).
- Emitter's AUTHENTIC attribute table recovered (binary @0x511dd4, ids
  0x1D-0x25) and published: Laser*/Seek*/OutputVoltage@0x414 (currentLevel,
  RAW volts -- the live-cursor feed).  Field renames per the table:
  0x3F8 minSeekVoltageIndex / 0x3FC maxSeekVoltageIndex (static_assert-locked).
  The MechWeapon 0x1D OutputVoltage PORT ALIAS retired (binary table ends
  0x1C; Find walks lowest-id-first, the alias shadowed the authentic row).
  PPC::DefaultData -> Emitter::GetAttributeIndex().
- mech4: BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses) for
  steady-state pixel verification; BT_SEEK_LOG diag.

Verified live (BLH, autofire): both #10 repro pairs held ghost-free for
minutes (SYS09->SYS10 PPC, SYS02->SYS04 ERMed, SYS05->SYS06 Myomers); curves
draw with live cursors; one replot per activation; quad panels/J-K-L/sec
panel un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 20:27:39 -05:00
co-authored by Claude Fable 5
parent f1a6ccbaaf
commit 63b168cb92
15 changed files with 594 additions and 109 deletions
+216 -41
View File
@@ -57,7 +57,9 @@
// 0x463b8000=12000.0f generator max voltage
// 0x42c80000=100.0f HeatSinkCluster numeric scale base
// _DAT_004c8df0 heat numeric scale _DAT_004c92e0 warning threshold
// _DAT_004c6bdc/_DAT_004c6be0 seek-curve plot step/limit
// _DAT_004c6bdc=1200.0f / _DAT_004c6be0=12000.0f seek-curve plot step/limit
// ld80 @004c6bd0 / @004c6d74 = 8.3333333333333e-05 (exactly 1/12000) --
// the voltage->y normaliser (y = v * (1/12000) * yScale)
// DAT_0051936c PrepEngr engineer-screen number (1..12)
//
@@ -84,6 +86,10 @@ static const Scalar SeekVoltageYScale = 187.0f; // 0x433b0000
static const Scalar RedrawSentinel = 9999.0f; // 0x461c3c00
static const Scalar GeneratorMaxVoltage = 12000.0f; // 0x463b8000
static const Scalar HeatNumericBase = 100.0f; // 0x42c80000
// SeekVoltageGraph plot constants (capstone disasm of @004c6934, task/issue #11):
static const Scalar SeekCurveStep = 1200.0f; // _DAT_004c6bdc (10 segments)
static const Scalar SeekCurveLimit = 12000.0f; // _DAT_004c6be0 (y-axis full scale)
static const Scalar SeekCurveYNorm = (Scalar)(1.0 / 12000.0); // ld80 @004c6bd0/@004c6d74
// (DebugStream warning channel, DAT_00524e20, is provided by the shared
// reconstruction headers -- heat.hpp / mechrecon.hpp.)
@@ -97,15 +103,6 @@ static char DAT_00518f24[] = "GeneratorCluster"; // subsystem name to resolve
static int DAT_00518ee0 = 0; // GaugeRate
static char DAT_00518f68[] = ""; // background image name
//
// Original FUN_0041bfc0 = Entity::FindAttributeIndex(name) -> ordinal. The
// WinTesla engine exposes attribute lookup via Simulation::AttributeIndexSet
// (Find/FindEntry by name) rather than a name->ordinal call, so this best-effort
// shim stands in for it; the stored indices are consumed by the (summarised)
// SeekVoltageGraph::Execute plot.
//
static int FindAttributeIndex(Entity *, const char *) { return -1; }
//
// FUN_0041bfc0 == Entity::FindAttributeIndex(name) returns a POINTER into the
// subsystem (subsystem + attributeMemberOffset), or 0 on miss. The composite
@@ -250,14 +247,31 @@ protected:
//###########################################################################
//###########################################################################
// SeekVoltageGraph @004c6798
// (issue #11: fully reconstructed from the capstone disasm of
// @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/@004c6c6c --
// Ghidra had dropped every x87 argument; tools/disas2.py recovered them.)
//###########################################################################
//###########################################################################
//
// The subsystem's voltage-response sampler == the binary subsystem vtbl+0x3C
// (slot 15): Emitter @004bb42c = sqrt(seekPower(v)/2.0e8), Myomers @004b8f94 =
// sqrt(speed(v)*3.6/350). Reached through a complete-type dispatch bridge
// (emitter.cpp -> myomers.cpp) per the databinding rule (gotcha 8).
//
extern Scalar BTSeekVoltageSample(void *subsystem, Scalar voltage);
//
// The destroyed test == *(subsystem+0x40) == 1 (MechSubsystem::simulationState,
// 1 = Destroyed). Named-member bridge in powersub.cpp (databinding rule).
//
extern int BTSubsystemDestroyed(Subsystem *subsystem);
//
// @004c6798 -- ctor (vtable PTR_FUN_0051a1fc). GraphicGauge base, interns and
// ref-counts the "destroyed" bitmap, resolves the four SeekVoltage attribute
// indices off the weapon subsystem, sets the port extent (0x97,0x80,0x17d,0x13b)
// and stashes the 230x187 plot scale.
// POINTERS off the weapon subsystem (FUN_0041bfc0 returns subsystem+offset or
// 0), sets the view XOR draw op + the port extent (0x97,0x80,0x17d,0x13b) and
// stashes the 230x187 plot scale.
//
SeekVoltageGraph::SeekVoltageGraph(
GaugeRate rate,
@@ -278,22 +292,34 @@ SeekVoltageGraph::SeekVoltageGraph(
destroyedImage = (char *)destroyed_image;
renderer->warehousePointer->bitMapBin.Get(destroyedImage); // FUN_00442aec (AddRef)
currentSeekIndex = FindAttributeIndex(subsystem, "CurrentSeekVoltageIndex"); // FUN_0041bfc0
minSeekIndex = FindAttributeIndex(subsystem, "MinSeekVoltageIndex");
maxSeekIndex = FindAttributeIndex(subsystem, "MaxSeekVoltageIndex");
seekVoltageTable = FindAttributeIndex(subsystem, "SeekVoltage");
// FUN_0041bfc0(subsystem, name) == the engine's name-keyed attribute-pointer
// resolve (subsystem+memberOffset, or 0 on a miss); the composite ctors gate
// the tick/cursor draws on a 0 result.
currentSeekIndexPtr = (int *)AttributePointerOf(subsystem, "CurrentSeekVoltageIndex"); // this[0x25]
minSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MinSeekVoltageIndex"); // this[0x26]
maxSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MaxSeekVoltageIndex"); // this[0x27]
seekVoltageTablePtr = (Scalar *)AttributePointerOf(subsystem, "SeekVoltage"); // this[0x28]
voltageSubsystem = (Entity *)seek_voltage_pointer; // this[0x29]
liveVoltage = (Scalar *)seek_voltage_pointer; // this[0x29]
this->subsystem = (Subsystem *)subsystem; // this[0x2A]
// graphicsView -> GraphicGauge::localView. SetExtent == SetPositionWithinPort;
// the original SetClip() (vtbl+0x0C) constrained drawing to that extent.
// @004c6798: (vtbl+0x0C)(3) then (vtbl+0x08)(0x97,0x80,0x17d,0x13b) --
// SetOperation(Xor) + SetPositionWithinPort (GRAPH2D.h vtable order). The
// XOR op is what lets DrawTicks/DrawCursorBox erase by redrawing.
localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C Xor
localView.SetPositionWithinPort(0x97, 0x80, 0x17d, 0x13b); // vtbl+0x08
xScale = SeekVoltageXScale; // this[0x2C]
yScale = SeekVoltageYScale; // this[0x2D]
drawCursor = draw_cursor; // this[0x2F]
destroyedShown = 0; // this[0x2E]
// (binary leaves this[0x2B]/[0x30..0x32] uninitialised -- activation's
// BecameActive sentinel forces the first replot, which seeds them before
// any read. Seeded here for determinism.)
previousVoltage = RedrawSentinel; // this[0x2B]
cursorX = -1; // this[0x30]
cursorY = -1; // this[0x31]
tickIndexShown = -1; // this[0x32]
}
//
@@ -311,38 +337,187 @@ Logical SeekVoltageGraph::TestInstance() const
}
//
// @004c6920 -- BecameActive: previousVoltage = 9999.0f (force first redraw).
// @004c6920 -- BecameActive: previousVoltage = 9999.0f (poison the cached
// response so the next Execute clears + replots the whole box -- THE top-box
// eraser on a page switch; the binary body writes ONLY this[0x2B]).
//
void SeekVoltageGraph::BecameActive()
{
previousVoltage = RedrawSentinel; // @0xAC force first redraw
destroyedShown = 0;
previousVoltage = RedrawSentinel; // @0xAC force full redraw
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] BecameActive sub="
<< (subsystem ? subsystem->GetName() : "?") << "\n" << std::flush;
}
//
// @004c6934 -- Execute (the seek-voltage curve plot). BRING-UP: the full plot
// (SetupClip/DrawTicks/DrawCursor over a 0..1 voltage polyline) needs the live
// seek-voltage sampler (subsys vtbl+0x3c) + the four attribute indices; the
// attribute resolve is summarised (FindAttributeIndex returns -1), so the plot is
// deferred and Execute is inert until the sampler + attribute reads land. Kept as
// a defined no-op so the gauge's vtable is complete (no /FORCE crash when the
// Myomer/Energy cluster ticks its SeekVoltageGraph child).
// @004c6be4 -- the full-box erase ("SetupClip" in the old notes; it is really
// a clear): SetOperation(Replace), SetColor(0), MoveToAbsolute(0,0),
// DrawFilledRectangleToAbsolute(1000,1000) -- clipped to the view = the whole
// (0x97,0x80)-(0x17d,0x13b) top data box painted black. NOTE: leaves the view
// in Replace mode (the polyline plot relies on that; Execute restores Xor).
//
void SeekVoltageGraph::ClearGraph()
{
localView.SetOperation((GraphicsDisplay::Operation)0); // vtbl+0x0C Replace
localView.SetColor(0); // vtbl+0x18
localView.MoveToAbsolute(0, 0); // vtbl+0x24
localView.DrawFilledRectangleToAbsolute(1000, 1000); // vtbl+0x48
}
//
// @004c6c30 -- the live cursor: an 8x8 filled square centred on
// (cursorX,cursorY), drawn in the view's current (Xor) op so a second call
// erases it. MoveToAbsolute(x-4,y-4) + DrawFilledRectangleToRelative(8,8).
//
void SeekVoltageGraph::DrawCursorBox()
{
localView.MoveToAbsolute(cursorX - 4, cursorY - 4); // vtbl+0x24
localView.DrawFilledRectangleToRelative(8, 8); // vtbl+0x4C
}
//
// @004c6c6c -- the per-gear tick marks (XOR). For each seek index i in
// [*min..*max]: V = seekVoltage[i]; x = Round(sample(V) * xScale);
// y = Round(V * (1/12000) * yScale) [ld80 @004c6d74]. The gear the highlight
// was last drawn at (this[0x32]) gets the full L (axis->point->axis); the rest
// get 10-pixel axis stubs. Called in pairs to move the highlight (XOR erase +
// redraw).
//
void SeekVoltageGraph::DrawTicks()
{
// (port guard: the binary derefs min/max unguarded -- its call sites only
// guarantee current+table non-null; all four always resolve on the
// emitter/myomer subsystems that own a graph.)
if (minSeekIndexPtr == 0 || maxSeekIndexPtr == 0)
return;
int last = *maxSeekIndexPtr; // this[0x27] deref
for (int i = *minSeekIndexPtr; i <= last; i++) // this[0x26] deref
{
Scalar gearVoltage = seekVoltageTablePtr[i]; // this[0x28][i]
int x = Round(BTSeekVoltageSample(subsystem, gearVoltage) * xScale); // (vtbl+0x3C)(V)*230
int y = Round(gearVoltage * SeekCurveYNorm * yScale); // V/12000*187
if (i == tickIndexShown) // this+0xC8
{
localView.MoveToAbsolute(0, y); // vtbl+0x24
localView.DrawLineToAbsolute(x, y); // vtbl+0x30
localView.DrawLineToAbsolute(x, 0);
}
else
{
localView.MoveToAbsolute(0, y);
localView.DrawLineToAbsolute(10, y);
localView.MoveToAbsolute(x, 0);
localView.DrawLineToAbsolute(x, 10);
}
}
}
//
// @004c6934 -- Execute (the seek-voltage response plot; full x87 recovery).
//
// DESTROYED branch (subsys simulationState==1): once, clear the box and draw
// the centred "destroyed" bitmap (edestryd.pcc) at
// ((Round(xScale)-w)/2, (Round(yScale)-h)/2), color 0xFF.
//
// LIVE branch:
// * on the destroyed->alive edge, call BecameActive (own vtbl+0xC == slot 3
// of PTR_0051a1fc = @004c6920) to poison the cache;
// * change-test: sample the response at 12000 V (0x463b8000) and compare with
// the cached this[0x2B]; on change (or the 9999 activation sentinel):
// CLEAR (@004c6be4 -- the ghost eraser), SetColor(0xFF), MoveTo(0,0), then
// the polyline v = 0,1200,...,10800 (step _DAT_004c6bdc=1200 while
// v < _DAT_004c6be0=12000): LineTo(Round(sample(v)*230),
// Round(v * [1/12000 * 187])), reset the cursor slots to -1, restore
// SetOperation(Xor), and draw the gear ticks at the current index;
// * tick maintenance: when the current seek index moved, XOR-erase the old
// highlight + redraw at the new (two DrawTicks calls);
// * cursor (draw_cursor pages only): XOR-erase the old box (if any), sample
// at the LIVE voltage (*this[0x29]) and draw the new 8x8 box at
// (Round(sample(V)*230), Round(V * [1/12000 * 187])).
//
void SeekVoltageGraph::Execute()
{
// (seek-voltage plot deferred -- see note above.)
}
if (BTSubsystemDestroyed(subsystem)) // *(subsys+0x40) == 1
{
if (destroyedShown == 0)
{
destroyedShown = 1; // this[0x2E]
ClearGraph(); // @004c6be4
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
BitMap *image = warehouse->bitMapBin.Get(destroyedImage); // FUN_00442aec
if (image != NULL) // (port guard; binary derefs unchecked)
{
localView.MoveToAbsolute( // vtbl+0x24 centre the bitmap
(Round(xScale) - image->Data.Size.x) >> 1,
(Round(yScale) - image->Data.Size.y) >> 1);
localView.SetColor(0xff); // vtbl+0x18
localView.DrawBitMap(0, image, // vtbl+0x54
0, 0, image->Data.Size.x - 1, image->Data.Size.y - 1);
}
warehouse->bitMapBin.Release(destroyedImage); // FUN_00442c12
}
return;
}
//
// @004c6934 (original notes preserved) -- Execute. If the subsystem is destroyed
// (subsys+0x40 == 1) draw
// the centred "destroyed" bitmap once. Otherwise, when the live seek voltage
// (subsys vtbl+0x3c sampler) changes: clear (SetupClip @004c6be4), re-plot the
// 0..1 voltage curve as a polyline (step _DAT_004c6bdc up to _DAT_004c6be0,
// Round each point), then draw the min/max tick marks (DrawTicks @004c6c6c)
// and, if drawCursor, the live cursor at the current seek index
// (DrawCursor @004c6c30).
//
if (destroyedShown != 0)
{
destroyedShown = 0;
BecameActive(); // own vtbl+0xC (@004c6920)
}
Scalar yStep = SeekCurveYNorm * yScale; // fld tbyte @004c6bd0 * this[0x2D]
// change-test: the response curve's value at max voltage.
Scalar response = BTSeekVoltageSample(subsystem, GeneratorMaxVoltage); // (vtbl+0x3C)(12000.0f)
if (previousVoltage != response) // this[0x2B]
{
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] replot sub="
<< (subsystem ? subsystem->GetName() : "?")
<< " response12k=" << response << "\n" << std::flush;
previousVoltage = response;
ClearGraph(); // @004c6be4 -- erases the WHOLE box
localView.SetColor(0xff); // vtbl+0x18 (Replace mode from the clear)
localView.MoveToAbsolute(0, 0); // vtbl+0x24
for (Scalar v = 0.0f; v < SeekCurveLimit; v += SeekCurveStep)
{
int x = Round(BTSeekVoltageSample(subsystem, v) * xScale);
int y = Round(v * yStep);
localView.DrawLineToAbsolute(x, y); // vtbl+0x30
}
cursorX = -1; // this[0x30]
cursorY = -1; // this[0x31]
localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C back to Xor
if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0)
{
tickIndexShown = *currentSeekIndexPtr; // this[0x32]
DrawTicks(); // @004c6c6c
}
}
// gear-change tick maintenance (XOR pair: erase old highlight, draw new).
if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0)
{
int currentIndex = *currentSeekIndexPtr;
if (currentIndex != tickIndexShown)
{
DrawTicks(); // erase at the old index
tickIndexShown = currentIndex;
DrawTicks(); // draw at the new
}
}
// live-voltage cursor (drawCursor pages: the emitter/PPC graphs).
if (drawCursor != 0) // this[0x2F]
{
if (cursorX >= 0)
DrawCursorBox(); // XOR-erase the old box
Scalar live = *liveVoltage; // *this[0x29]
cursorX = Round(BTSeekVoltageSample(subsystem, live) * xScale);
cursorY = Round(live * yStep);
DrawCursorBox(); // @004c6c30
}
}
//###########################################################################