//===========================================================================// // 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 #pragma hdrstop #if !defined(BTL4VID_HPP) # include #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 #endif // // Mech::ResolveJoint + the Joint types the articulation path switches on. // #if !defined(MECH_HPP) # include #endif #if !defined(JOINT_HPP) # 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; //########################################################################### //###################### BTL4HingeRenderable ############################ //########################################################################### // // A HingeRenderable that flushes the joint as a FULL MATRIX instead of a // single-axis sin/cos pair. // // THE PROBLEM. L4VIDRND.CPP:1028 sets SINGLE_AXIS_HINGE True, so both the // ctor and Execute push the hinge with dpl_SetDCSXAxis / YAxis / ZAxis -- // three distinct libDPL entry points, each handed only (sine, cosine). The // AXIS is therefore carried by WHICH function was called, and libDPL does not // put it on the wire. Confirmed against a live capture // (emulator/render-bridge/joints_check.fifodump): every articulation record // is 12 bytes, [dcs handle][sin][cos], 26133 of them, and there is no axis // field anywhere in the message. Nor can the renderer recover it from // context -- the wire names nothing (zero action-0x22 name records in that // capture), so a handle cannot be matched back to a .SKL node. // // The consequence is visible: vrboard parses those records, cannot apply // them, and leaves the flushed matrix standing. The mech renders with a // live root but frozen limbs. // // THE FIX is the archive's own alternative. The #else half of that same // #if (L4VIDRND.CPP:1153) builds a Quaternion from the Hinge and assigns it // over the DCS matrix, which encodes the axis in the matrix itself and // flushes as a 12-float pose. The renderer already applies those -- it is // exactly what BallJointRenderable::Execute does unconditionally, with no // #if at all, which is why ball joints were never affected by this. // // So this is not a new mechanism. It is the branch the original authors // wrote and did not take, applied to hinges so they behave like the ball // joints beside them. // // Cost: 48 wire bytes per changed hinge per frame instead of 8. With ~22 // articulated nodes that is under 1KB/frame, against captures that already // run to megabytes. // // Why a subclass and not a shadow of L4VIDRND.CPP: this needs one method, // and Component::Execute is virtual (CMPNNT.HPP:40), so an override // dispatches normally. Shadowing the engine file to flip one #define would // fork 1900 lines of it to change two. // // WHY THIS DERIVES FROM ChildOffsetRenderable AND NOT FROM HingeRenderable. // The first attempt did subclass HingeRenderable and override only Execute. // It built, ran, and DID NOT WORK -- the wire still carried 12-byte records. // What it did change was their contents: the pair went from (sin, cos) to // (0.9990, 0.0000), which is m00 and m01 of the matrix being written. // // That is the whole answer. The DCS remembers HOW IT WAS LAST SET. The base // HingeRenderable ctor calls dpl_SetDCSXAxis/YAxis/ZAxis before we get // control, which puts the DCS in single-axis mode, and the flush then // serialises two floats out of it no matter what we write through // dpl_GetDCSMatrix. Writing a full matrix into an axis-mode DCS just means // the first two cells of the matrix get sent. // // So the axis-mode setter must never run on this DCS. Deriving from // ChildOffsetRenderable -- whose ctor builds the offset DCS and touches no // axis setter -- is what avoids it. That makes this class the exact hinge // analogue of BallJointRenderable: same base, same shape, holds its own // attribute pointer plus a previous-value copy, writes a full matrix in // Execute. Which is presumably how it would have been written had the // single-axis optimisation not been there. // // NOTE on the archive's stale comment: the ctor there says the quaternion // hop exists "because the math library doesn't support direct assignment of // hinge to matrix yet". Still true, and worth recording because the header // suggests otherwise -- Matrix4x4::operator=(const Hinge&) is DECLARED at // MATRIX.HPP:94 but implemented nowhere, so taking the direct route is a // link error. Quaternion::operator=(const Hinge&) is real // (MUNGA/ROTATION.CPP:707, correct half-angle about axisNumber) and // Matrix4x4::operator=(const Quaternion&) is real (MATRIX.CPP:260). // class BTL4HingeRenderable : public ChildOffsetRenderable { public: BTL4HingeRenderable( Entity *entity, ExecutionType execution_type, dpl_OBJECT *graphical_object, dpl_ZONE *this_zone, dpl_ISECT_MODE intersect_mode, uint32 intersect_mask, dpl_DCS *parent_DCS, LinearMatrix *offset_matrix, const Hinge *my_hinge ): ChildOffsetRenderable( entity, execution_type, graphical_object, this_zone, intersect_mode, intersect_mask, parent_DCS, offset_matrix ) { Check(my_hinge); myHinge = my_hinge; oldHinge = *my_hinge; // // Push the initial pose, exactly as both stock joint renderables // do in their ctors -- otherwise the DCS holds the identity the // base installed until the joint first moves. // FlushAsMatrix(); } void Execute(); protected: void FlushAsMatrix(); const Hinge *myHinge; // the joint attribute we watch Hinge oldHinge; // last value seen, so we only flush on change }; void BTL4HingeRenderable::FlushAsMatrix() { Quaternion temp_quaternion; float32 *temp_matrix; temp_matrix = dpl_GetDCSMatrix(myDCS); Check_Pointer(temp_matrix); temp_quaternion = oldHinge; *(Matrix4x4 *)temp_matrix = temp_quaternion; // // This is what DPL_FLUSH_DCS expands to (L4VIDRND.CPP:73). The macro is // file-private to L4VIDRND.CPP, so spell it out rather than redefine it // here and risk the two drifting. It matters that this is the DELAYED // flush and not a bare dpl_FlushDCS: the batch is what the renderer // coalesces per frame, and going around it would put every joint on the // wire immediately. // myRenderer->DPLDelayDCSFlush(myDCS); } void BTL4HingeRenderable::Execute() { Check(this); if (oldHinge != *myHinge) { oldHinge = *myHinge; FlushAsMatrix(); } // // Chain the GRANDPARENT, not HingeRenderable::Execute -- that would run // the single-axis write we are replacing and re-flush the same DCS. // ChildOffsetRenderable::Execute(); } 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, int *eye_count, int *joint_count) { Check(this); Check_Pointer(skeleton_filename); Check_Pointer(eye_count); Check_Pointer(joint_count); 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, eye_count, joint_count); DEBUG_STREAM << "[skl] " << path << " -> " << node_count << " nodes, " << object_count << " objects, " << *eye_count << " eye, " << *joint_count << " articulated\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, int *eye_count, int *joint_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; } // // 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; // // This node's geometry, if it has any. Loaded BEFORE the node is built, // because a joint renderable takes its object as a ctor argument and // builds/flushes the instance itself (DCSObjectRenderable, L4VIDRND.CPP: // 649). 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). // dpl_OBJECT *object = NULL; const char *object_name; if (skeleton->GetEntry(page_name, "Object", &object_name) && object_name) { object = dpl_LoadObject((char *)object_name, dpl_load_normal); if (object == NULL) { DEBUG_STREAM << "[skl] couldn't load object " << object_name << " for '" << page_name << "'\n" << flush; } } dpl_DCS *dcs = NULL; // // ARTICULATION. A node whose page name resolves to a live skeleton Joint // gets a JOINT RENDERABLE instead of a static DCS, so the board hears // about the joint MOVING. // // Why this is the whole game: a static node's matrix is written and // flushed once at build time. Torso::TorsoSimulation faithfully calls // SetRotation() on its Joint every frame, but nothing carried that back // to the DCS -- measured 5.3.79, twist sweeping the full authored range // with anim_abs=1 joints=0 on the wire and a cockpit view that did not // move by more than 74 pixels. The engine's answer is this family // (L4VIDRND.CPP:1026+): each renderable holds the joint's rest offset in // one DCS and the live rotation in a child DCS, and its Execute compares // the watched Hinge/EulerAngles against a cached copy, writes the axis // and calls DPL_FLUSH_DCS -- which is what puts more than one node into // vr_flush_dcs_artic (0x1f). // // The joint's live value is read straight out of the mech's own // JointSubsystem (Mech::ResolveJoint by segment name -- the .SKL page // names ARE the segment names, which is how Torso already resolves // 'jointtorso'), so the simulation and the render read ONE source. // // Env-gated while it proves out: the static path is a working cockpit // render and this replaces the node construction wholesale. // Joint *joint = NULL; if ( getenv("BT_JOINTS") != NULL && parent_dcs != NULL && entity->GetClassID() == RegisteredClass::MechClassID ) { joint = ((Mech *)entity)->ResolveJoint(page_name); } if (joint != NULL) { LinearMatrix offset; offset = EulerAngles(rot_pitch, rot_yaw, rot_roll); offset(3,0) = tran_x; offset(3,1) = tran_y; offset(3,2) = tran_z; ChildOffsetRenderable *joint_renderable = NULL; switch (joint->GetJointType()) { case Joint::HingeXJointType: case Joint::HingeYJointType: case Joint::HingeZJointType: // // BTL4HingeRenderable, not HingeRenderable: the stock one // flushes (sin,cos) with the axis implied by which libDPL // entry point it called, and the axis never reaches the wire. // See the class comment at the top of this file. // joint_renderable = new BTL4HingeRenderable( entity, VideoRenderable::Dynamic, object, zone, dpl_isect_mode_obj, 0, parent_dcs, &offset, &joint->GetHinge()); break; case Joint::BallJointType: joint_renderable = new BallJointRenderable( entity, VideoRenderable::Dynamic, object, zone, dpl_isect_mode_obj, 0, parent_dcs, &offset, &joint->GetEulerAngles()); break; default: // // StaticJointType and BallTranslation: the static path below. // (BallTranslate has its own renderable, but no MAD.SKL node // animates a translation -- only jointeye is balltranslate, and // it carries the camera, not geometry.) // break; } if (joint_renderable != NULL) { Register_Object(joint_renderable); dcs = joint_renderable->GetDCS(); if (object != NULL) { ++(*object_count); } ++(*joint_count); } } if (dcs == NULL) { // // STATIC node: baked matrix, flushed once. Correct for anything the // simulation never moves. // dcs = dpl_NewDCS(); Check_Pointer(dcs); dpl_SetDCSZone(dcs, zone); // // 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); } if (object != NULL) { dpl_INSTANCE *instance = dpl_NewInstance(); Check_Pointer(instance); dpl_SetInstanceObject(instance, object); dpl_AddInstanceToDCS(dcs, instance); dpl_FlushInstance(instance); ++(*object_count); } dpl_FlushDCS(dcs); } ++(*node_count); // // 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, eye_count, joint_count); } } Unregister_Object(children); delete children; } // // "site=" children. Sites get NO draw component of their own -- the // real pod's capture decodes exactly 26 DCS bodies for this skeleton, // which is joints+root only, and this walk matched that count precisely // BECAUSE it ignored sites. One site spawns hardware instead of // geometry: siteeyepoint is the COCKPIT CAMERA. // // The authentic construction comes from the donor's decompile // (bt411 btl4vid.cpp:462, decomp FUN_004579a8): the eye's offset matrix // is the SITE'S OWN local rest transform, and the eye is parented on the // site's PARENT segment's draw component -- this page's DCS -- NOT the // hull root. World orientation and all live motion (torso twist, gait) // then come from the parent-chain composition for free. For MAD.SKL the // chain is jointtorso -> jointeye (trany +1.687, tranz -1.318) -> // siteeyepoint (identity), which puts the eye in the canopy where the // cockpit sits, instead of at the hull origin staring through the torso // panels. // if (view_type == insideEntity) { NameList *sites = skeleton->MakeEntryList(page_name, "site"); if (sites != NULL) { Register_Object(sites); NameList::Entry *site_entry; for (site_entry = sites->GetFirstEntry(); site_entry != NULL; site_entry = site_entry->GetNextEntry()) { const char *site_page = (const char *)site_entry->dataReference; if ( site_page != NULL && strcmp(site_page, "siteeyepoint") == 0 && skeleton->PageExists(site_page) ) { Scalar site_tx = 0.0f, site_ty = 0.0f, site_tz = 0.0f, site_pitch = 0.0f, site_yaw = 0.0f, site_roll = 0.0f; skeleton->GetEntry(site_page, "tranx", &site_tx); skeleton->GetEntry(site_page, "trany", &site_ty); skeleton->GetEntry(site_page, "tranz", &site_tz); skeleton->GetEntry(site_page, "pitch", &site_pitch); skeleton->GetEntry(site_page, "yaw", &site_yaw); skeleton->GetEntry(site_page, "roll", &site_roll); // // Default ctor identities; the EulerAngles assignment // writes ONLY the 3x3 rotation (AFFNMTRX.CPP:179), so // the identity's zero translation survives it and the // site translation goes in after. // LinearMatrix site_offset; site_offset = EulerAngles(site_pitch, site_yaw, site_roll); site_offset(3,0) = site_tx; site_offset(3,1) = site_ty; site_offset(3,2) = site_tz; EulerAngles *eyepoint_rotation = (EulerAngles *)entity->GetAttributePointer( "EyepointRotation"); DPLEyeRenderable *cockpit_eye = new DPLEyeRenderable( entity, dplMainZone, site_offset, dcs, dplMainView, eyepoint_rotation); Register_Object(cockpit_eye); ++(*eye_count); if (getenv("BT_MER_LOG")) { DEBUG_STREAM << " [eye] cockpit eye on '" << page_name << "' offset=(" << site_tx << "," << site_ty << "," << site_tz << ")\n" << flush; } } } Unregister_Object(sites); delete sites; } } 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 class 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 video_chain(NULL); L4VideoObjectWrapper::BuildVideoObjectChainFromResource( &video_chain, model_resource); ChainIteratorOf 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. The walk also // builds the COCKPIT EYE when it meets the // siteeyepoint site (donor construction: offset = // the site's local rest, parent = the site's parent // joint DCS -- so the eye sits in the canopy and // rides torso twist through chain composition). // // THE INSIDE VIEW LOADS THE X-VARIANT SKELETON. The // resource names one skeleton (mad.skl) for both views; // the cockpit build is derived from it by the fleet-wide // naming convention -- third letter X: MAD->MAX, // AVA->AVX, BAT->BAX, BLH->BLX, FIR->FIX, JAK->JAX, // LOK->LOX, with the numbered chassis following // (MAD1->MAX1, LOK1->LOX1; all present in VIDEO/). The // X skeleton carries the SAME 25-joint chain -- so the // canopy eye and torso twist compose identically -- but // its only geometry is the cockpit shell (MAX.SKL -> // max_cop.bgf, the MAX_COP canopy with PUNCH-texel // windows from the real-pod capture forensics). The // donor names the same mechanism from the decomp side: // inside = SkeletonType_A with '_cop' selection // (btl4vid.hpp:678). Without this, the pilot sits // inside the OUTSIDE model staring at torso panels. // int eye_count = 0, joint_count = 0; const char *skeleton_name = video_object->GetObjectFilename(); char inside_name[64]; dpl_DCS *skl_result = NULL; if ( view_type == insideEntity && strlen(skeleton_name) >= 3 && strlen(skeleton_name) < sizeof(inside_name) ) { strcpy(inside_name, skeleton_name); inside_name[2] = (inside_name[2] >= 'a' && inside_name[2] <= 'z') ? 'x' : 'X'; skl_result = ReadSKLFile(entity, root_DCS, inside_name, view_type, &eye_count, &joint_count); if (skl_result == NULL) { DEBUG_STREAM << "[skl] no cockpit variant '" << inside_name << "' -- falling back to the body skeleton\n" << flush; } } if (skl_result == NULL) { ReadSKLFile(entity, root_DCS, skeleton_name, view_type, &eye_count, &joint_count); } // // 3. FALLBACK eyepoint only. A skeleton without a // siteeyepoint still needs a camera for the inside // view, and the hull root with a zero offset is the // engine's own zero-construction default // (L4VIDEO.CPP:4849). For MAD.SKL this no longer // runs -- the walk builds the real cockpit eye. // if (view_type == insideEntity && eye_count == 0) { 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); }