Files
TeslaRel410/restoration/source410/BT_L4/BTL4VID.CPP
T
CydandClaude Fable 5 475ae3106e BT410 5.3.59: A MISSION RUNS -- the RIO serial port was blocking every load
pod_render_norio.conf is pod_render_rec.conf with one line changed,
serial1=disabled instead of the vrio named pipe.  Same binary, same egg.  With
it, our build launches and the mech walks:

  [launch] state=2 minPriorityEmpty=1 ticks=85316
  [queues] p0=- p1=- p2=BUSY p3=- p4=-
  BTL4Application::RunMissionMessageHandler
  Turning Plasma Score Display On
  [sim] pos=(180.466,10,-358.703) yaw=-0.275605 spd=14.4
  [sim] pos=(251.959,10,-250.498) yaw=-0.89103  spd=14.4

No fault, 290 log lines and counting.  This is the first time the
reconstruction has reached a running mission on the pod.

The hang was never starvation, a deadlock, or a refill loop -- it was VOLUME,
the first candidate I listed and then talked myself out of.  530 renderer
events queue at priority 0 during load; BackgroundTasks::Execute runs exactly
one task per call round-robin over seven tasks, and the 1ms frame budget is
always blown here so no extra background passes happen.  That is ~9 events per
real second, so a load legitimately takes ~60s.  The fault arrived at ~40s,
before the backlog could clear.  Every 'the queue never drains' reading was
really 'the process dies before it can'.

The disproof was already in hand: the post tally climbed to 530 and went FLAT,
which means nothing was refilling.  p0=BUSY with nextReady=1 is equally
consistent with a large finite backlog still draining, and I read it as refill.

Why the RIO port: the pipe has no server attached, which should behave as an
unplugged cable.  The emulator log shows steady serial1 RX overruns, our build
prints 'RIO never came back from test mode!', and the SHIPPED binary prints
'lost RIO analog request' and launches anyway on this identical rig.  So an
unplugged RIO is survivable and our handling of it is not -- a real defect, not
a rig artifact, since a pod with an unplugged RIO cable should still boot.

The wild reference at EulerAngles::operator=(const LinearMatrix&)+0x19 is still
unexplained, but it is now reproducible on demand by enabling serial1 rather
than being a coin flip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 23:23:29 -05:00

528 lines
17 KiB
C++

//===========================================================================//
// File: btl4vid.cpp //
// Project: BattleTech //
// Contents: Implementation details for the BT video renderer //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <btl4.hpp>
#pragma hdrstop
#if !defined(BTL4VID_HPP)
# include <btl4vid.hpp>
#endif
//
// The engine's damage-zone tagging callback reads this while geometry is
// loading (L4VIDEO.CPP:527). It is a bare global there -- defined at
// L4VIDEO.CPP:351 with no header declaration -- so declare it here to set
// it around our own loads, exactly as DPLRenderer does around its.
//
extern Entity
*Entity_Being_Created;
BTL4VideoRenderer::BTL4VideoRenderer(
RendererRate calibration_rate,
RendererComplexity calibration_complexity,
RendererPriority calibration_priority,
InterestType interest_type,
InterestDepth depth_calibration
):
DPLRenderer(
calibration_rate,
calibration_complexity,
calibration_priority,
interest_type,
depth_calibration
)
{
}
BTL4VideoRenderer::~BTL4VideoRenderer()
{
}
//
//#############################################################################
// LoadMissionImplementation -- called by Renderer::LoadMission (the authentic
// engine, CODE/RP/MUNGA/RENDERER.CPP:263) after it has set
// LoadingRendererStatus, stamped nextRenderTime and started the renderer with
// the RendererManager. This is where a GAME renderer builds the mission's
// scene content on the Division board.
//
// BRING-UP NO-OP (phase 1 of the btl4vid ladder). A no-op is a LEGAL body
// here, not a cheat: the engine's own VideoRenderer::LoadMissionImplementation
// (CODE/RP/MUNGA/VIDREND.CPP:259) is a bare Tell, and our GaugeRenderer
// (GAUGREND.CPP:3275) ships the same. The renderer therefore comes up and
// runs its frame loop with an EMPTY scene, which is exactly what we want to
// measure before writing any content.
//
// THE AUTHENTIC SHAPE, pinned by the surviving sibling header for Red
// Planet's renderer (CODE/RP/RP_L4/RPL4VID.HPP -- same engine, same board,
// same year; only the .CPP is missing there too):
//
// LoadMissionImplementation walks the mission's entities and calls
// MakeEntityRenderables(entity, model_resource, view_type) for each,
// which builds a dpl_DCS hierarchy through ReadSKLFile /
// RecurseSKLFile (the skeleton notation pages), with a material
// substitution list set up around it.
//
// The BT-specific renderables (BTReticleRenderable, BTTranslocationRenderable,
// the pending-wrecks map) come from the BT411 donor game/reconstructed/
// btl4vid.cpp -- 3188 lines -- but NOTE that port restructured this hook and
// has no method under this name, so the CONTRACT above comes from the 1995
// engine and only the CONTENT comes from the donor.
//#############################################################################
//
//
//#############################################################################
// ReadSKLFile -- build one entity's render skeleton from its .SKL file.
//
// The .SKL is a plain NotationFile. Each page is one node of the skeleton:
//
// [jointhip]
// parent=jointlocal
// Type=hingex <- joint kind (animation; unused at build)
// Object=mad_hip.bgf <- optional geometry for this node
// dzone=dz_hip <- damage-zone tags (zero or more)
// tranx=.. trany=.. tranz=..
// pitch=.. yaw=.. roll=..
// joint=jointtorso <- child pages (zero or more)
//
// so the whole model is a recursive walk from [ROOT].
//
// Files live under video\ -- the engine uses the same bare "video\\" prefix
// for its .pfx loads (L4VIDEO.CPP:1513).
//
// STAGE 1 (this version): the tree, the geometry and the parenting are real;
// every node is given an IDENTITY matrix. The model therefore collapses onto
// the entity origin and looks wrong, which is deliberate -- the STRUCTURE is
// what is being proved here, and it is provable without looking at a pixel:
// MAD.SKL declares JointCount=25 (+1 root) and DZoneCount=22, and the
// dpl3-revive capture of a REAL pod decodes as 26 DCS bodies and 22 instance
// bodies. The counts this walk reports must match.
//
// STAGE 2 is the transforms. The dpl_MATRIX convention is NOT yet proven
// (see RENDER-ROADMAP.NOTES.md) -- a DCS flush body carries 16 float32, and a
// decoded capture suggests row-major with the translation in the last row,
// but the decoder's offset is suspect by one word. Rather than guess it and
// ship a mech that renders confidently in the wrong orientation, this stage
// leaves identity in place.
//#############################################################################
//
dpl_DCS *
BTL4VideoRenderer::ReadSKLFile(
Entity *entity,
const char *skeleton_filename,
ViewFrom view_type)
{
Check(this);
Check_Pointer(skeleton_filename);
char
path[256];
strcpy(path, "video\\");
strcat(path, skeleton_filename);
NotationFile
*skeleton = new NotationFile(path);
Register_Object(skeleton);
if (skeleton->PageCount() == 0)
{
DEBUG_STREAM << "[skl] could not read " << path << "\n" << flush;
Unregister_Object(skeleton);
delete skeleton;
return NULL;
}
//
// Every node of this model shares one zone, switched on and flushed by
// the engine helper (L4VIDEO.CPP:1338).
//
dpl_ZONE
*zone = MakeNewZone();
int
node_count = 0,
object_count = 0;
dpl_DCS
*root = RecurseSKLFile(
entity, NULL, skeleton, "ROOT", 0, view_type,
zone, &node_count, &object_count);
DEBUG_STREAM << "[skl] " << path
<< " -> " << node_count << " nodes, "
<< object_count << " objects\n" << flush;
//
// The engine guards this the same way (L4VIDEO.CPP, DPLReadEnvironment):
// ~NotationFile REWRITES the file when dirtyFlag is set, and these are
// the game's shipped .SKL data files.
//
Verify(!skeleton->IsDirty());
Unregister_Object(skeleton);
delete skeleton;
return root;
}
//
//#############################################################################
// RecurseSKLFile -- one page of the skeleton, then its children.
//#############################################################################
//
dpl_DCS *
BTL4VideoRenderer::RecurseSKLFile(
Entity *entity,
dpl_DCS *parent_dcs,
NotationFile *skeleton,
const char *page_name,
int recursion_depth,
ViewFrom view_type,
dpl_ZONE *zone,
int *node_count,
int *object_count)
{
Check(this);
Check(skeleton);
Check_Pointer(page_name);
if (!skeleton->PageExists(page_name))
{
DEBUG_STREAM << "[skl] missing page '" << page_name << "'\n" << flush;
return NULL;
}
//
// A guard, not a limit: the file is authored data and a bad parent= chain
// could otherwise recurse forever. The deepest real chain in MAD.SKL is
// nowhere near this.
//
if (recursion_depth > 32)
{
DEBUG_STREAM << "[skl] recursion too deep at '" << page_name
<< "'\n" << flush;
return NULL;
}
dpl_DCS
*dcs = dpl_NewDCS();
Check_Pointer(dcs);
dpl_SetDCSZone(dcs, zone);
//
// STAGE 2a -- the node's LOCAL TRANSLATION.
//
// Slot convention, derived rather than guessed. A DCS flush body is
// [remote][type_check][node][pad][16 x float32]: our own identity
// matrices were found on the wire at body offset 16, which also
// showed analyze_scene.py's rest[4:68] read to be one word early.
// Re-reading a real BT capture with that correction gives a clean
// identity with the translation in the LAST ROW -- a row-vector
// convention, the transpose of MUNGA's AffineMatrix (which is 3x4
// row-major with translation in the 4th COLUMN, entries[3/7/11], per
// AFFNMTRX.CPP:50).
//
// Rotation is deliberately left identity for now: every base-pose
// pitch/yaw/roll in MAD.SKL is 0 or ~1e-3, so TRANSLATION alone
// assembles the model -- which isolates this one convention and
// makes a wrong guess about rotation order impossible to mistake for
// a wrong guess about translation.
//
Scalar
tran_x = 0.0f,
tran_y = 0.0f,
tran_z = 0.0f;
skeleton->GetEntry(page_name, "tranx", &tran_x);
skeleton->GetEntry(page_name, "trany", &tran_y);
skeleton->GetEntry(page_name, "tranz", &tran_z);
float32
matrix[16];
int
i;
for (i = 0; i < 16; ++i)
{
matrix[i] = 0.0f;
}
matrix[0] = matrix[5] = matrix[10] = matrix[15] = 1.0f;
//
// Translation goes in the 4th COLUMN (3/7/11), the same layout as
// MUNGA's own AffineMatrix (3x4 row-major, entries[3/7/11] --
// AFFNMTRX.CPP:50). Putting it in the last ROW instead made the
// matrix PROJECTIVE under a column-vector convention and the
// transform maths blew up: the walk died on the first node that
// carries geometry, reproducibly, and bisecting with the reads kept
// but the values suppressed ran clean. Same library house as
// MUNGA, same convention.
//
matrix[3] = (float32)tran_x;
matrix[7] = (float32)tran_y;
matrix[11] = (float32)tran_z;
dpl_SetDCSMatrix(dcs, matrix);
if (parent_dcs != NULL)
{
dpl_AddDCSToDCS(parent_dcs, dcs);
}
else
{
dpl_AddDCSToScene(dcs);
}
++(*node_count);
//
// This node's geometry, if it has any. Entity_Being_Created is already
// set by our caller so the library's C callback can tag the geometry with
// damage zones (L4VIDEO.CPP:4176).
//
const char
*object_name;
if (skeleton->GetEntry(page_name, "Object", &object_name) && object_name)
{
dpl_OBJECT
*object = dpl_LoadObject((char *)object_name, dpl_load_normal);
if (object != NULL)
{
dpl_INSTANCE
*instance = dpl_NewInstance();
Check_Pointer(instance);
dpl_SetInstanceObject(instance, object);
dpl_AddInstanceToDCS(dcs, instance);
dpl_FlushInstance(instance);
++(*object_count);
}
else
{
DEBUG_STREAM << "[skl] couldn't load object " << object_name
<< " for '" << page_name << "'\n" << flush;
}
}
dpl_FlushDCS(dcs);
//
// Children. Repeated "joint=" entries: the entry NAME is "joint" and the
// VALUE (dataReference) is the child page -- the same shape as the
// engine's objectpath= walk at L4VIDEO.CPP:1858.
//
NameList
*children = skeleton->MakeEntryList(page_name, "joint");
if (children != NULL)
{
Register_Object(children);
NameList::Entry
*entry;
for (entry = children->GetFirstEntry();
entry != NULL;
entry = entry->GetNextEntry())
{
const char
*child_page = (const char *)entry->dataReference;
if (child_page != NULL && *child_page != '\0')
{
RecurseSKLFile(
entity, dcs, skeleton, child_page, recursion_depth + 1,
view_type, zone, node_count, object_count);
}
}
Unregister_Object(children);
delete children;
}
return dcs;
}
//
//#############################################################################
// MakeEntityRenderables -- the game level of the renderable factory.
//
// The engine's DPLRenderer::MakeEntityRenderables (L4VIDEO.CPP:4151) knows
// the ENGINE entity classes and calls DOWN to
// VideoRenderer::MakeEntityRenderables for anything else, which only prints
// Entity <id> class<n> couldn't figure out how to MakeEntityRenderables
// So every BT class has to be answered here.
//
// FIRST ANSWER: BTPlayer (class 3035) carries no graphics. The engine
// already does exactly this for its own PlayerClassID -- an empty case --
// and BT's player is simply a different id it cannot know about. This is
// the class the live pod run complained about.
//
// Everything else still chains to the engine, so this override can only
// ADD answers, never remove the ones DPLRenderer already gives.
//#############################################################################
//
void
BTL4VideoRenderer::MakeEntityRenderables(
Entity *entity,
ResourceDescription *model_resource,
ViewFrom view_type)
{
Check(this);
Check(entity);
//
// RENDERABLE-BUILD TRACE (env BT_MER_LOG).
//
// The mission never launches because 530 renderer events queue at
// priority 0 during load and the background pump drains only one per
// seven frames, and the page fault lands on one of the LAST ~35 of them.
// So the fault belongs to a specific entity, not to elapsed time. The
// oldest log named it -- class 42, UnscalableTerrain -- but that was a
// different build, so this prints the class of every entity as its
// renderables are built and the last line before the fault names the
// culprit outright. One line per entity, not per frame: ~530 lines for a
// whole run, which the COM3 log carries without the 3x cost that per-frame
// logging brought.
//
if (getenv("BT_MER_LOG"))
{
static int
mer_count = 0;
mer_count++;
DEBUG_STREAM << "[mer] " << mer_count
<< " class=" << (int)entity->GetClassID()
<< " view=" << (int)view_type
<< " res=" << (model_resource ? 1 : 0)
<< endl << flush;
}
switch (entity->GetClassID())
{
case RegisteredClass::MechClassID:
//
// The mech's video resource is a SKELETON. Walk the chain the
// same way the engine does (L4VIDEO.CPP:4250) and hand every
// Skeleton entry to ReadSKLFile; anything else falls through to
// the engine, which knows what to do with plain objects.
//
{
if (model_resource == NULL)
{
break;
}
ChainOf<L4VideoObjectWrapper*>
video_chain(NULL);
L4VideoObjectWrapper::BuildVideoObjectChainFromResource(
&video_chain, model_resource);
ChainIteratorOf<L4VideoObjectWrapper*>
video_iterator(video_chain);
L4VideoObjectWrapper
*video_wrapper;
Logical
handled = False;
//
// BT_NO_SKL skips the skeleton build so the SAME binary can be
// run with and without it -- the only way to attribute the
// intermittent pod crash without a rebuild between samples.
//
if (getenv("BT_NO_SKL") != NULL)
{
break;
}
Entity_Being_Created = entity;
video_iterator.First();
while ((video_wrapper = video_iterator.ReadAndNext()) != NULL)
{
const L4VideoObject
*video_object = video_wrapper->GetVideoObject();
//
// The inside view produced NO [skl] line, so this chain
// evidently holds no Skeleton entry and we fall through to
// the engine -- straight into the eyepoint construction that
// faults. Print what is actually in the chain rather than
// assuming.
//
if (getenv("BT_MER_LOG"))
{
DEBUG_STREAM << " [vid] type="
<< (int)video_object->GetResourceType()
<< " file='" << video_object->GetObjectFilename()
<< "'" << endl << flush;
}
if (video_object->GetResourceType()
== L4VideoObject::Skeleton)
{
ReadSKLFile(entity,
video_object->GetObjectFilename(), view_type);
handled = True;
}
}
Entity_Being_Created = NULL;
if (!handled)
{
//
// Where is the stack HERE? The fault reads through a frame
// pointer at 0x7000FA74, while the main loop's frame sits at
// 0x12DEE7 -- 1.79 GB apart. Either something between this
// point and the fault switches stacks, or the frame pointer is
// being corrupted on the way in. One printout distinguishes
// them, and it is the last place we control before the engine
// takes over.
//
if (getenv("BT_MER_LOG"))
{
char
here;
DEBUG_STREAM << " [chain] into DPLRenderer, stack at 0x"
<< hex << (int)&here << dec << endl << flush;
}
DPLRenderer::MakeEntityRenderables(
entity, model_resource, view_type);
}
}
break;
case RegisteredClass::BTPlayerClassID:
//
// No graphics -- the player is a control/scoring entity.
//
break;
default:
DPLRenderer::MakeEntityRenderables(entity, model_resource, view_type);
break;
}
}
void
BTL4VideoRenderer::LoadMissionImplementation(Mission *mission)
{
Check(this);
//
// CHAIN THE BASE. DPLRenderer::LoadMissionImplementation
// (L4VIDEO.CPP:6007) is NOT empty -- it reads the renderer environment
// and loads the name bitmaps. DPLReadEnvironment opens the
// notation file named by L4DPLCFG (SETENV.BAT defaults it to
// btdpl.ini) and hands its "main" page to DPLReadINIPage, which walks
// the compare/branch pages for this location/time and calls
// dpl_SetObjectFilePath / material / texmap from the objectpath=
// entries (L4VIDEO.CPP:1852). It is PRIVATE to DPLRenderer, so the
// game renderer reaches it only by chaining -- which is the whole
// point: an override here REPLACES the base, it does not extend it.
//
// WITHOUT IT every dpl_LoadObject returns NULL. The live pod run
// failed all 40 arena objects (sky / aw01..aw04 / afloor / bcor1 /
// bdet1 / bdet2 / bpip1) and ended in "NULL instance", while the
// SHIPPED binary on the SAME rig loaded every one of them. The
// models were never missing -- the loader simply had no paths, because
// the bring-up no-op that used to live here SUPPRESSED the base.
//
DPLRenderer::LoadMissionImplementation(mission);
}