From f0f0e3d70e35e046fa6ba9457ab19a77553ce111 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 30 Jul 2026 09:25:12 -0500 Subject: [PATCH] BT410 5.3.88: hinges flush as MATRICES -- the axis was never on the wire, and a DCS remembers how it was last set The mech rendered with a live root and frozen limbs. Cause, and it is not in the renderer: L4VIDRND.CPP:1028 sets SINGLE_AXIS_HINGE True, so HingeRenderable pushes a joint with dpl_SetDCSXAxis / YAxis / ZAxis -- three separate libDPL entry points, each handed only (sine, cosine). The AXIS is carried by WHICH FUNCTION WAS CALLED, and libDPL never puts it on the wire. Confirmed against a live capture rather than argued: every articulation record in joints_check.fifodump is 12 bytes, [handle][sin][cos], 26133 of them, no axis field anywhere. And the renderer cannot recover it from context either -- that capture contains ZERO action-0x22 name records, so a DCS handle cannot be matched back to a .SKL node. vrboard was parsing those records correctly and dropping them on purpose, with a comment saying so. 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 -- the axis ends up IN the matrix, and it flushes as a 12-float pose the renderer already applies. That is precisely what BallJointRenderable::Execute does unconditionally, with no #if at all, which is why ball joints were never affected. So this is the branch the original authors wrote and did not take, applied to hinges so they behave like the ball joints beside them. A DCS REMEMBERS HOW IT WAS LAST SET -- the part that cost a build. The first attempt subclassed HingeRenderable and overrode only Execute. It compiled, 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 diagnosis in one number. The base ctor calls dpl_SetDCS?Axis before the subclass gets control, which puts the DCS in single-axis mode, and the flush then serialises two floats out of it no matter what goes in through dpl_GetDCSMatrix. So the axis setter must never touch this DCS. BTL4HingeRenderable therefore derives from ChildOffsetRenderable, whose ctor builds the offset DCS and calls no axis setter -- making it the exact hinge analogue of BallJointRenderable: same base, holds its own attribute pointer plus a previous-value copy, writes a full matrix in Execute. CODE/ untouched; Component::Execute is virtual (CMPNNT.HPP:40) so the override dispatches normally, and shadowing 1900 lines of L4VIDRND.CPP to flip one #define was not needed. VERIFIED on the rig, torso-sweep conf, clean run: before 26133 records, ALL 2-float, 1 handle, renderer applied NOTHING after 7626 records, ALL 12-float, ZERO 2-float messages remaining, 22 handles emitted, renderer applied all 22 (anim_abs) and the swept joint moves: handle 0x684, m00 across 1532 distinct values from 0.1582 to 1.0000 -- cos of a 0..80 degree sweep, which is exactly what BT_FORCE_TORSO drives. LEG GAIT: NOT FIXED, and the earlier note claiming this would fix it was wrong. The transport was necessary but not sufficient. On a walking run (new pod_render_joints.conf = the arena mission with BT_JOINTS) all 22 joints flush as matrices and the renderer applies them, but only handle 0x672 -- the vehicle ROOT -- animates. The limbs emit their initial pose and never change. Because nothing drives them. Joint::SetHinge / SetRotation exist, and the ONLY caller anywhere in the reconstruction is TorsoSimulation (BT/TORSO.CPP:382, horizontalJointNode->SetRotation). That is why the torso is the one thing that moves. MAD.SKL declares JointCount=25 -- jointhip, jointlthigh, jointrthigh, jointtorso, jointshakey, jointeye and the rest -- and the gait that should walk them is simply not reconstructed yet. Separate piece of work, now unblocked rather than done. HARNESS CAVEAT: pose_probe frames on "the last-articulated root" in anim_abs. With 21 joint DCSs now landing there, that heuristic picks a joint instead of the vehicle root and frames empty arena. The change invalidated the assumption; the render is not evidence either way until it takes the root handle explicitly. The counts above are the evidence. FAULTS SEEN: two crashes during this work, both cr2=7000FA64 at host 66D9 -- the known parked load-window fault, address unmoved across a relink (which is the standing test for "not ours"). They died at DIFFERENT points ([mer] 217 and [mer] 116), where a defect in new construction code would die consistently. Third and fourth runs clean. Co-Authored-By: Claude Fable 5 --- emulator/render-bridge/pod_render_joints.conf | 86 +++++++++ restoration/source410/BT_L4/BTL4VID.CPP | 166 +++++++++++++++++- 2 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 emulator/render-bridge/pod_render_joints.conf diff --git a/emulator/render-bridge/pod_render_joints.conf b/emulator/render-bridge/pod_render_joints.conf new file mode 100644 index 00000000..e366695b --- /dev/null +++ b/emulator/render-bridge/pod_render_joints.conf @@ -0,0 +1,86 @@ +# pod_render_norio.conf -- pod_render_rec with the RIO serial port OFF. +# +# The emulator log shows a steady stream of serial1 RX OVERRUN errors on +# the RIO pipe, and controls post their events at HighEventPriority +# UNCONDITIONALLY (CONTROLS.HPP:250) -- so a chattering RIO port would +# flood a priority the background pump always serves first, starving the +# priority-0 renderer events the load gate waits on. This conf tests that +# by removing the port entirely. Everything else is identical to the rec +# conf, so a launch here and a hang there isolates the RIO. +# +[sdl] +output=opengl +# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop; +# with the retry patches a rare dropout self-recovers (see gauge_rio.conf). +priority=higher,higher +[dosbox] +memsize=32 +machine=svga_s3 +[cpu] +core=dynamic +cputype=pentium +cycles=max +[sblaster] +sbtype=sb16 +sbbase=220 +irq=5 +dma=1 +hdma=5 +[mixer] +# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief +# emulation-thread stalls (RIO retry recovery) don't audibly chop +rate=44100 +blocksize=1024 +prebuffer=60 +[serial] +# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's +# few-ms ACK deadline is met; plasma display on COM2 (real pod has both). +# VWE fork namedpipe backend (com0com/realport retired -- COM1/COM2 gone): +# DOSBox = pipe client (retry), vRIO/vPLASMA apps = servers; an unconnected +# pipe behaves as an unplugged cable so the mission still runs. serialnamedpipe.h +serial1=disabled +serial2=namedpipe pipe:vplasma +# live UNBUFFERED game output: DOS char devices are not buffered, so +# redirecting stdout to COM3 lands every line immediately. A normal +# '> file' redirect stays 0 bytes until the process exits, which hides +# all progress on a run that does NOT crash. +serial3=file file:C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt +[autoexec] +mount c "C:\VWE\TeslaRel410\ALPHA_1" +c: +cd \REL410\BT +set VIDEOFORMAT=svga +rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per +rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS +rem driver relies on; skipping it left the cards uninitialized (silent). +rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the +rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is +rem dumped from a real card. diagnose /s kept (passes, sets mixer config). +set BLASTER=A220 I5 D1 H5 P330 T6 +c:\sb16\diagnose /s +set BLASTER=A240 I7 D3 H6 P300 T6 +c:\sb16\diagnose /s +set BLASTER=A220 I5 D1 H5 P330 T6 +set TEMP=c:\ +rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO +rem attached; stdout redirected so mission-load progress survives kills. +set BT_JOINTS=1 +set BT_MECH_LOG=1 +set BT_MAP_LOG=1 +set BT_MER_LOG=1 +set BT_VID_LOG=1 +set BT_LAUNCH_LOG=1 +set BT_STACK_LOG=1 +set BT_FORCE_THROTTLE=0.6 +set BT_FORCE_TURN=0.25 +set HEAPSIZE=15000000 +set L4GAUGE=640x480x16 +call setenv.bat r s n p +32rtm.exe -x +BTL4REC.EXE -egg testarn.egg > COM3 + +echo GAME-RC=%errorlevel% >> RC.TXT +32rtm.exe -u +echo ALPHA1-RUN-DONE +pause + diff --git a/restoration/source410/BT_L4/BTL4VID.CPP b/restoration/source410/BT_L4/BTL4VID.CPP index 7a0f02e2..a971e0cc 100644 --- a/restoration/source410/BT_L4/BTL4VID.CPP +++ b/restoration/source410/BT_L4/BTL4VID.CPP @@ -40,6 +40,164 @@ 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, @@ -366,7 +524,13 @@ dpl_DCS * case Joint::HingeXJointType: case Joint::HingeYJointType: case Joint::HingeZJointType: - joint_renderable = new HingeRenderable( + // + // 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());