Files
TeslaRel410/restoration/source410/BT_L4/BTL4VID.CPP
T
CydandClaude Fable 5 d66c2b0812 BT410 5.3.55: the mech skeleton is built -- .SKL walked into a dpl_DCS tree
BTL4VideoRenderer answers MechClassID: walks the video-object chain the way
the engine does, hands every L4VideoObject::Skeleton entry to ReadSKLFile, and
recurses the .SKL into a dpl_DCS tree with geometry instanced onto it.

Verified repeatedly on the live pod: '[skl] video\mad.skl -> 26 nodes, 19
objects', with no 'wrong video resource type' complaint and no load failures.
Those counts are exactly what the file declares (25 joint= entries + root, 19
Object= entries).

Corrects the earlier success criterion in this file, which said 22 instances
by reading the reference capture's 'instance x22' against DZoneCount=22.
Damage zones are not geometry -- 28 dzone= tags spread across 19 objects.

Translations are written to matrix[3]/[7]/[11], MUNGA's own AffineMatrix
layout.  It walks cleanly but no frame has been seen WITH the mech yet, so
the slot choice is recorded as unconfirmed.  Rotation stays identity by
design: every base-pose angle in MAD.SKL is 0 or ~1e-3, so translation alone
assembles the model and isolates one convention at a time.

AND A CORRECTION I have to flag loudly: I earlier concluded from single runs
that non-identity translations crashed the pod, 'isolated' it, and 'confirmed'
the alternative also crashed.  That was all noise.  The same binary re-run
gives walk / crash / walk / crash -- the known intermittent plane-write defect
is now firing on ~half of pod runs and lands at different points each time,
which is precisely what made it look deterministic.  Never accept a single
pod run as evidence on this rig; require two agreeing runs.

That defect is now the top of the list: a run must survive both the skeleton
build and the launch to render anything, which at ~50% is a coin flip on a
four-minute cycle.

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

452 lines
14 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;
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);
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;
Entity_Being_Created = entity;
video_iterator.First();
while ((video_wrapper = video_iterator.ReadAndNext()) != NULL)
{
const L4VideoObject
*video_object = video_wrapper->GetVideoObject();
if (video_object->GetResourceType()
== L4VideoObject::Skeleton)
{
ReadSKLFile(entity,
video_object->GetObjectFilename(), view_type);
handled = True;
}
}
Entity_Being_Created = NULL;
if (!handled)
{
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);
}