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>
This commit is contained in:
Cyd
2026-07-28 18:46:37 -05:00
co-authored by Claude Fable 5
parent fac559bc31
commit d66c2b0812
3 changed files with 391 additions and 0 deletions
+314
View File
@@ -15,6 +15,15 @@
# 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,
@@ -68,6 +77,262 @@ BTL4VideoRenderer::~BTL4VideoRenderer()
// 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.
@@ -98,6 +363,55 @@ void
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.
+27
View File
@@ -19,6 +19,9 @@
# include <l4video.hpp>
# endif
# include <dpl.h>
# include <dplutils.h>
//###########################################################################
//####################### BTL4VideoRenderer #############################
//###########################################################################
@@ -53,6 +56,30 @@
ResourceDescription *model_resource,
ViewFrom view_type);
//
// The mech's model resource is a SKELETON, not an object, and the
// engine rejects it ("wrong video resource type") because building
// one is the GAME renderer's job. Shape taken from the surviving
// sibling header CODE/RP/RP_L4/RPL4VID.HPP.
//
dpl_DCS *
ReadSKLFile(
Entity *entity,
const char *skeleton_filename,
ViewFrom view_type);
dpl_DCS *
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);
protected:
int reserved[16];
};
@@ -726,3 +726,53 @@ and rejects our mech with "wrong video resource type for object mad.skl"
(L4VIDEO.CPP:4265) because it only accepts L4VideoObject::Object / Rubble. So
the skeleton arrives as a different L4VideoObject::ResourceType -- find that
enumerator and branch on it in our override rather than re-deriving the name.
================================================================================
THE MECH SKELETON IS BUILT. And the intermittent crash is now THE blocker.
================================================================================
BTL4VideoRenderer now answers MechClassID: it 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 the geometry hung off it.
VERIFIED, repeatedly, on the live pod:
[skl] video\mad.skl -> 26 nodes, 19 objects
no "wrong video resource type" complaint, no geometry load failures
and those counts are exactly what the file declares -- MAD.SKL has 25 "joint="
entries (+1 root = 26) and 19 "Object=" entries. Every joint page became a
DCS, every Object= became a loaded instance.
CORRECTION TO THE EARLIER SUCCESS CRITERION: this file's note previously said
the walk should produce 22 instances, reading the dpl3-revive capture's
"instance x22" against MAD.SKL's DZoneCount=22. That was wrong -- damage
zones are not geometry. There are 28 dzone= tags spread across 19 objects
(jointshakey alone carries 8 on one .bgf). The right criterion is the file's
own joint=/Object= counts, which we hit exactly.
TRANSLATIONS ARE IN, and the slot question is NOT yet settled: translation is
currently written to matrix[3]/[7]/[11] (MUNGA's AffineMatrix layout -- 3x4
row-major, translation in the 4th column, AFFNMTRX.CPP:50). It builds and
walks cleanly, but no frame has yet been seen WITH the mech, so the choice is
unconfirmed. Rotation is still identity on purpose (every base-pose
pitch/yaw/roll in MAD.SKL is 0 or ~1e-3, so translation alone assembles the
model and isolates one convention at a time).
!! A BISECT THAT LIED, worth reading before trusting any pod result !!
I concluded from single runs that non-identity translations crashed the pod,
"isolated" it by suppressing the values, and then "confirmed" the other slot
choice also crashed. All of that was NOISE. Re-running the SAME binary gave
walk-completes / crash / walk-completes / crash. The intermittent
plane-write defect ("Reference to a page you don't own", PF at 66D9 -- the
one with seven hypotheses already killed) is now firing on roughly HALF of
pod runs, and it lands at different points each time, which is exactly what
made it look like a deterministic matrix bug.
RULE FOR THIS RIG: never accept a single pod run as evidence. Run the
same binary at least twice and require agreement, or the intermittent
defect will invent a story for you.
That defect has therefore gone from background annoyance to the top of the
list: it is what stops a mech frame from ever being seen, because a run has
to survive BOTH the skeleton build AND the launch to render anything, and at
~50% per run that is a coin flip on a four-minute cycle.