Added BT_NO_SKL, which skips the skeleton build so ONE executable can be run with and without it. That is the only clean way to test an intermittent fault, and it settles attribution: walk enabled ~50% of runs die (page fault at 66D9) at DIFFERENT points walk disabled 0 crashes in 2 runs shipped exe 0 crashes in 2 runs, same rig and conf So it is our new code, not the rig, and the varying fault point points at memory corruption surfacing later rather than a bad instruction in the walk. Hypotheses killed by reading the private library headers: the matrix array is the right size (dpl_MATRIX is float32[4][4]); and neither s_dplobject nor the common dpl_node header retains an object name, weakening the dangling-string theory (a private name cache inside the library is still possible and is the best surviving suspect). Also checked something that would have been much worse than a crash: ~NotationFile REWRITES its file when dirty, and these are the game's shipped .SKL data files. MAD.SKL is untouched, and ReadSKLFile now carries the engine's own Verify(!IsDirty()) guard. Next tests are listed cheapest-first in the roadmap: keep the NotationFile alive, copy the Object= names, count board allocations, and re-check whether MakeEntityRenderables runs twice per mech. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
468 lines
15 KiB
C++
468 lines
15 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);
|
|
|
|
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 (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);
|
|
}
|