Files
TeslaRel410/restoration/source410/BT_L4/BTL4VID.CPP
T
CydandClaude Fable 5 e846717283 BT410 5.3.65: THE MECH STANDS -- wire matrix convention proven from engine source
pose_chase.png: the MadCat assembled from its 19 parts, standing in the arena
at its live articulated position.  Feet, legs, torso, weapon pods, canopy
plate.  Not a collapsed stack at the origin, which is what every prior frame
actually showed.

The convention is no longer inferred -- it is read from the engine.
RootRenderable's ctor writes an entity pose into a DCS with
*(Matrix4x4*)dpl_GetDCSMatrix(myDCS) = localToWorld, so
Matrix4x4::operator=(const AffineMatrix&) IS the wire layout: row-major,
rotation in rows 0-2, zeros in column 3, translation in ROW 3 (entries
12/13/14).

The old 3/7/11 choice rested on two errors, both corrected in the code
comments: AffineMatrix does keep translation at 3/7/11, but because it is
COLUMN-major -- citing it as precedent for a row-major layout read the storage
and ignored the indexer (AFFNMTRX.HPP:99).  And 'the last row made the maths
blow up' came from the contaminated bisect; those crashes were the RIO fault.

Rotation now composes through the engine's own Matrix4x4::operator=(const
EulerAngles&) from the page's pitch/yaw/roll, and the node write uses the
RootRenderable idiom (dpl_GetDCSMatrix + assign).

Also banked: the chase-frame fifobridge variant -- camera framed on the
articulated root instead of the arena bounds -- as the offline proof harness
for any future pose question.

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

596 lines
19 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
//
// Matrix4x4 -- the DCS wire layout (see RecurseSKLFile). l4video.hpp pulls
// rotation.hpp but not matrix.hpp; L4VIDRND.CPP includes it the same way.
//
#if !defined(MATRIX_HPP)
# include <matrix.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,
dpl_DCS *parent_dcs,
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;
//
// The ROOT page's DCS attaches under the caller's parent (the mech's
// RootRenderable DCS, so the model rides the entity's localToWorld) or,
// with no parent, straight to the scene -- the old bring-up behaviour,
// which parks the model at the world origin.
//
dpl_DCS
*root = RecurseSKLFile(
entity, parent_dcs, 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);
//
// The node's LOCAL TRANSFORM, in the engine's own terms -- the wire
// convention is no longer inferred from captures, it is READ from the
// engine: RootRenderable's ctor (L4VIDRND.CPP:853) writes an entity pose
// into a DCS with
//
// *(Matrix4x4*)dpl_GetDCSMatrix(myDCS) = myEntity->localToWorld;
//
// so whatever Matrix4x4::operator=(const AffineMatrix&) produces IS the
// DCS matrix layout. Reading it (MATRIX.CPP:130): Matrix4x4 is ROW-major
// (MATRIX.HPP:113, entries[(Row<<2)+Column]) with rotation in rows 0-2,
// ZEROS in column 3, and the TRANSLATION IN ROW 3 -- entries 12/13/14.
//
// (The previous revision put translation at entries 3/7/11, quoting
// AffineMatrix as precedent. AffineMatrix does keep translation at
// 3/7/11 -- but because it is COLUMN-major, entries[(column<<2)+row]
// (AFFNMTRX.HPP:99); its (3,c) translation row lands at 3/7/11 by
// storage, not by convention. Matrix4x4 transposes that on copy. The
// 'last row made the maths blow up' claim attached to the old choice
// came from the contaminated bisect -- the crashes were the RIO fault.)
//
// Rotation comes from the page's pitch/yaw/roll through the engine's own
// Matrix4x4::operator=(const EulerAngles&), so the rotation ORDER is the
// engine's by construction, not a guess. MAD.SKL base-pose angles are
// all 0 or ~1e-3, so this is nearly identity today -- but it is the
// correct compose for any skeleton authored with real angles.
//
Scalar
tran_x = 0.0f,
tran_y = 0.0f,
tran_z = 0.0f,
rot_pitch = 0.0f,
rot_yaw = 0.0f,
rot_roll = 0.0f;
skeleton->GetEntry(page_name, "tranx", &tran_x);
skeleton->GetEntry(page_name, "trany", &tran_y);
skeleton->GetEntry(page_name, "tranz", &tran_z);
skeleton->GetEntry(page_name, "pitch", &rot_pitch);
skeleton->GetEntry(page_name, "yaw", &rot_yaw);
skeleton->GetEntry(page_name, "roll", &rot_roll);
Matrix4x4
node_matrix;
node_matrix = EulerAngles(rot_pitch, rot_yaw, rot_roll);
node_matrix(3,0) = tran_x;
node_matrix(3,1) = tran_y;
node_matrix(3,2) = tran_z;
//
// Write in place and flush -- the same idiom as RootRenderable's ctor
// (dpl_GetDCSMatrix + assign), not dpl_SetDCSMatrix.
//
float32
*dcs_matrix = dpl_GetDCSMatrix(dcs);
Check_Pointer(dcs_matrix);
*(Matrix4x4 *)dcs_matrix = node_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();
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)
{
//
// THE ENGINE COMPOSITION, mirrored from the Mover branch
// of DPLRenderer::MakeEntityRenderables
// (L4VIDEO.CPP:4795-4860), which the base cannot apply
// here because it only accepts Object/Rubble resources --
// chaining it with a Skeleton just prints "wrong video
// resource type" and builds NOTHING (measured: zero
// vr_flush_dcs_artic records on the wire, camera frozen
// at the world origin, and the mech statue parked there
// with the eye inside it).
//
// 1. A DYNAMIC RootRenderable. Its ctor adds its DCS to
// the scene and seeds it from entity->localToWorld;
// its Execute re-flushes whenever the entity moves.
// That per-frame flush is the ONLY source of wire
// articulation (0x1f) for the vehicle -- without it
// nothing on the board ever moves. NULL graphical
// object exactly like the CameraShip inside-view case
// (L4VIDEO.CPP:4548): the skeleton supplies the
// geometry.
//
RootRenderable
*this_root = new RootRenderable(
entity,
RootRenderable::Dynamic,
NULL,
dplMainZone,
dpl_isect_mode_obj,
NULL);
Register_Object(this_root);
dpl_DCS
*root_DCS = this_root->GetDCS();
//
// 2. The skeleton hangs UNDER the root DCS -- so the whole
// model rides the entity's localToWorld instead of
// being parked at the world origin.
//
ReadSKLFile(entity, root_DCS,
video_object->GetObjectFilename(), view_type);
//
// 3. Inside view: the eyepoint, driven off the same root
// DCS plus the mech's published EyepointRotation --
// this is what the camera follows.
//
if (view_type == insideEntity)
{
EulerAngles
*eyepoint_rotation =
(EulerAngles *)entity->GetAttributePointer(
"EyepointRotation");
DPLEyeRenderable
*this_eye = new DPLEyeRenderable(
entity,
dplMainZone,
LinearMatrix::Identity,
root_DCS,
dplMainView,
eyepoint_rotation);
Register_Object(this_eye);
}
handled = True;
}
}
Entity_Being_Created = NULL;
//
// Only chain the base when NO skeleton was found -- for a
// skeleton it contributes nothing but the complaint, and the
// root/eye it would otherwise build were built above.
//
if (!handled)
{
if (getenv("BT_MER_LOG"))
{
DEBUG_STREAM << " [chain] no skeleton -> DPLRenderer"
<< 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);
}