diff --git a/restoration/source410/BT_L4/BTL4VID.CPP b/restoration/source410/BT_L4/BTL4VID.CPP index cc9b3aa9..58c2acaf 100644 --- a/restoration/source410/BT_L4/BTL4VID.CPP +++ b/restoration/source410/BT_L4/BTL4VID.CPP @@ -15,6 +15,15 @@ # include #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 + video_chain(NULL); + L4VideoObjectWrapper::BuildVideoObjectChainFromResource( + &video_chain, model_resource); + + ChainIteratorOf + 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. diff --git a/restoration/source410/BT_L4/BTL4VID.HPP b/restoration/source410/BT_L4/BTL4VID.HPP index 93c7ffb6..74faf486 100644 --- a/restoration/source410/BT_L4/BTL4VID.HPP +++ b/restoration/source410/BT_L4/BTL4VID.HPP @@ -19,6 +19,9 @@ # include # endif +# include +# include + //########################################################################### //####################### 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]; }; diff --git a/restoration/source410/RENDER-ROADMAP.NOTES.md b/restoration/source410/RENDER-ROADMAP.NOTES.md index 5161b61b..d8ca4dd7 100644 --- a/restoration/source410/RENDER-ROADMAP.NOTES.md +++ b/restoration/source410/RENDER-ROADMAP.NOTES.md @@ -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.