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>
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>
Confirmed every call the .SKL walk needs -- the NotationFile accessors and the
MakeEntryList/GetFirstEntry/GetNextEntry idiom (copy DPLReadINIPage's
objectpath loop), and the dPL side (NewDCS/SetDCSMatrix/AddDCSToDCS/
AddDCSToScene/SetDCSZone/FlushDCS, LoadObject/NewInstance/SetInstanceObject/
AddInstanceToDCS/FlushInstance).
On the matrix: a DCS flush body is [remote][type_check][node][64 bytes] = a
4x4 of float32. Decoding a real BT capture shows a near-identity with a
single 10.0 term in the last row, suggesting row-major with translation in
row 3 -- but the rows print shifted by one word against a true identity, so
the decoder offset is suspect and the convention is recorded as NOT yet
proven. Flagged rather than guessed.
Staged the work so each half is verifiable without looking at pixels: build
the tree with identity matrices first and prove the structure by wire counts
(26 DCS + 22 instance flushes, which MAD.SKL's JointCount=25/DZoneCount=22 and
the dpl3-revive reference capture agree on), then add real transforms and fix
the convention by watching which slot moves.
Also noted the one API still to check first: the skeleton reaches us as a
non-Object L4VideoObject::ResourceType, so branch on that enumerator rather
than re-deriving the filename.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything needed to write the .SKL -> dpl_DCS path is now known and written
down: the file format (each page = one node with a local transform, an
optional .bgf, damage-zone tags and its child joint pages), the dPL call set
(NewDCS / SetDCSMatrix / AddDCSToDCS / LoadObject / NewInstance /
AddInstanceToDCS / FlushDCS), and the algorithm matching RPL4VID.HPP's
ReadSKLFile + RecurseSKLFile, including where DPLJointToDCSTranslator picks
up the joint->DCS mapping afterwards.
One unknown is flagged honestly: no surviving source calls dpl_SetDCSMatrix
(the RP game file that did is missing, like BT's), so the matrix convention
must be derived from the dpl3-revive protocol spec and DPLTYPES.H.
The success criterion is exact and needs no pixels: the dpl3-revive reference
capture of a real pod decodes as 26 DCS flushes and 22 instance flushes, and
MAD.SKL declares JointCount=25 (+root = 26) with DZoneCount=22. A correct
walk of that one file should reproduce those counts on the wire.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pod updates every few seconds. Measured by sampling a head window every
2s (emulator/render-bridge/headrate.py): OURS changed in 5 of 12 samples,
SHIPPED in 1 of 12. So the reconstruction is not slower than the shipped
binary -- the emulated board is just expensive. Caveat recorded with it:
ours was being driven by the throttle hooks while the shipped exe ignores
them and sat parked, so treat those as same-order, not a win.
What did cost us 3x was mine: BT_MECH_LOG/BT_LAUNCH_LOG write per-frame lines
and DEBUG_STREAM=cout is redirected to COM3, so every one goes through an
emulated serial port. Turning them off took the wire from 480 to ~1480
bytes/sec. pod_render_quiet.conf is the conf to use for timing work.
And a correction to my own earlier framing: wire bytes/sec is NOT a frame
rate. Shipped pushes ~8900 B/s against our ~1480 and the difference is
CONTENT, not speed -- shipped submits the mech and we do not:
OURS: L4VIDEO.cpp wrong video resource type for object mad.skl
SHIPPED: (no such line)
The mech's model resource is a SKELETON. The engine's default
MakeEntityRenderables accepts only Object/Rubble and rejects anything else
with exactly that message, because skeletons are the GAME renderer's job. So
our arena renders but the MECH IS ABSENT, and with it the cockpit interior --
one unimplemented path explaining both the missing model and the lower wire
volume.
Next brick is now precisely scoped: answer MechClassID by reading the .SKL
notation pages into a dpl_DCS hierarchy and hanging the per-node .BGF
geometry off it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
emulator/render-bridge/first-3d-frame.png: the arena from the pod -- sky,
horizon, ground, the arena structures along the skyline -- at ~31fps on the
VelociRender bridge.
The whole chain now works in our build: MakeVideoRenderer ->
BTL4VideoRenderer over DPLRenderer -> board boot -> Renderer::LoadMission ->
DPLReadEnvironment for the art paths -> 40/40 arena objects loaded -> mission
launch -> per-frame submission over the wire.
The mistake worth remembering: the run was never stalled after
InitializePlayerLink. I called that from a 150-second sample and it was just
too short. CheckLoadMessageHandler reposts every second and will not advance
until the min-priority event queue drains, and with the video renderer in the
mission that takes minutes. A BT_LAUNCH_LOG trace showed the queue draining
and then both RunMissionMessageHandler calls, the plasma display, and the
first sensor tick -- matching the shipped binary line for line. The renderer
deliberately holds a blank screen until RunningMission (L4VIDEO.CPP:5100), so
'black' was the app still loading, not a rendering bug.
Zero unbuildable entities, zero geometry failures. The frame is static only
because the mech is parked with no input, and the camera in the bridge title
is the bridge's own viewer, not the game's eyepoint.
Also banked the launch-gate trace (BT_LAUNCH_LOG) behind an env var.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DPLRenderer::LoadMissionImplementation (L4VIDEO.CPP:6007) is not empty: it
calls DPLReadEnvironment (which opens L4DPLCFG/btdpl.ini and sets the dPL
object/material/texmap paths from the objectpath= entries) and then
LoadNameBitmaps. The 'bring-up no-op' installed earlier therefore did not do
nothing -- it REPLACED that, so the loader never learned where the art lives
and every dpl_LoadObject returned NULL.
I justified that no-op by pointing at VideoRenderer's bare Tell and
GaugeRenderer's identical one; neither is our base. Check the ACTUAL base
before overriding in this engine and chain it unless there is a reason not
to. DPLReadEnvironment is private to DPLRenderer, so chaining is the only
way a game renderer can reach it at all.
before: 40x 'couldn't load object', 'NULL instance', run ends,
wire ~24 bytes/sec
after: 0 failures, run continues, wire ~12 KB/s (1.17MB climbing),
bridge live at 88fps
Proved it was ours and not the rig by running the SHIPPED exe under the same
conf: it loaded every object. That A/B is cheap and is the right first move
whenever the pod misbehaves.
Still black: the bridge camera sits at its default (0,10,0), so the wire
carries state and object loads rather than a populated scene. The mech and
arena entities still need MakeEntityRenderables bodies.
Rig improvements, both from the user: nosound is fine on the SLOW clock (it
is the FAST SOS clock that needs the AWE32), which saves ~4 min and ~300MB of
audio taps per run; and serial3=file + '> COM3' gives a live unbuffered log,
so a run that does NOT crash is finally readable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BTL4VideoRenderer now overrides MakeEntityRenderables. Class 3035 resolves
to BTPlayerClassID (the BT enum block starts at 3000 in VDATA.HPP), and a
player carries no graphics -- exactly what the engine already does for its
own PlayerClassID with an empty case. Everything else chains to DPLRenderer,
so the override can only add answers.
On the pod: the 'couldn't figure out how to MakeEntityRenderables' complaint
is gone and the run no longer exits, it keeps running. It still renders
nothing -- the wire fifodump grows at ~24 bytes/sec, keep-alive rather than a
frame stream -- which is expected, since only the player has been answered
and the mech and arena still have no renderables.
Banked a practical problem worth fixing before the next session: a pod run
that does NOT crash produces no readable log, because the conf redirects the
game's stdout and DOS buffers it. Every readable log this session came from
a run that crashed and had its buffer flushed by the fault handler. The
RC.TXT marker (a separate command) survives that, and the com3/serial3=file
trick from the emulator notes would give live unbuffered output -- worth
wiring in, otherwise progress is invisible precisely when things are going
well.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With every authored watcher resolving, the pod run reaches the mission, draws
the FULL COCKPIT with the board booted and audio running, and exits GAME-RC=0
with no Fail. It builds no 3-D scene, and the engine says why:
Entity 1:1 class3035 couldn't figure out how to MakeEntityRenderables
L4VIDEO.cpp couldn't load object sky.bgf (then the whole arena)
NULL instance
VideoRenderer::MakeEntityRenderables (VIDREND.CPP:231) is the BOTTOM of a
virtual chain -- its comment says so outright -- and BTL4VideoRenderer does
not override it, so every entity falls through, no scene graph is built, and
the geometry that would hang off it never loads.
CORRECTS an earlier note in the roadmap: I recorded the first full-rig run as
proving '.BGF loading was never a reconstruction problem, only a missing
bridge', because the complaint count was zero. It was zero because the run
Failed at a watcher long before reaching geometry. Now that it gets there,
the loads are attempted and fail -- for want of renderables, not the bridge.
Next brick is the real btl4vid body, shape pinned by the surviving sibling
header RPL4VID.HPP: override MakeEntityRenderables, plus ReadSKLFile /
RecurseSKLFile to walk the .SKL skeleton pages into a dpl_DCS hierarchy and
load the .BGF geometry per node. Renderable content from the BT411 donor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The linker map named the faulting function: the crash address minus the CODE
base (0x410000) looked up in btl4opt.map's Publics-by-Value landed inside
AudioStateWatcher::AudioStateWatcher +0x2D.
AudioStateWatcher is AudioWatcherOf<StateIndicator> and its ctor immediately
runs Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this).
So every authored *State name must be published as a StateIndicator --
AlarmIndicator counts, it derives from one -- and pointing one at a plain int
sends that member call through garbage.
That explains both earlier failures: the AlarmIndicator attempt was the right
type but was tested with other bugs still in the batch, and the plain-int
attempt was simply the wrong type and crashed further along.
Published as state objects: Reservoir/ReservoirState -> reservoirAlarm,
Generator/GeneratorState -> stateAlarm, plus new StateIndicator members for
Condenser/CondenserState and Torso/MotionState. StateIndicator has no
Initialize(); the default ctor suffices because the watcher only needs the
object to exist.
Verified on the pod rig: no crash, ladder advanced to ReportLeak.
Technique worth keeping: on an extender fault, subtract 0x410000 from the
dumped address and look it up in btl4opt.map -- it names the engine function,
and for this family of work that names the watcher class and hence the
required member type.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Published and verified green on the pod rig: Condenser/CondenserState,
Generator/GeneratorState, Emitter/LaserOn, ControlsMapper/TargetRangeExponent,
plus the Torso and Myomers tables from the previous pass.
Reservoir/ReservoirState CRASHES the pod on the DOS extender, and a bisect
pins it to exactly that change: revert it and the run returns to a clean
Fail, re-apply it alone and the crash returns. It is reverted; the tree is
green and the ladder is blocked there.
The important lesson is general: AttributeWatcherOf<T> does
currentValue = *(T*)attributePointer AT CONSTRUCTION, so a published name is
read the moment its watcher is built. My earlier staging note claimed the
provisional types could not matter because nothing drives the values yet --
that is wrong, and this is the counterexample.
Ruled out by measurement and recorded so they are not retried: the
AlarmIndicator-vs-int type (a plain int got further, 232 -> 749 bytes of log,
but still crashed), static-init order (reservr.obj sorts last, after heat),
an id gap (contiguous at HeatSink::NextAttributeID), and the gauge rig (same
binary runs clean there -- only the pod/arena context faults).
Crash signature for whoever picks it up: 0044B4AD, mov eax,[edx+0x18] then
call [eax+4], EAX=0x15, fault at 0x19 -- a small integer called through as an
object, i.e. the AttributeIndexSet::Build uninitialised-slot pattern.
Condenser is the control: same base class, plain int, no crash.
Also fixed on the way: staged members must be appended at the END of a class
(offset-sensitive readers exist -- condenserNumber is reached as
master+0x1d4) and never added to a resource struct, which is a wire format.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BTL4.RES stores each watcher as a (SUBSYSTEM, ATTRIBUTE) string pair, so the
complete list can be extracted directly instead of discovering one name per
6-minute run. Banked the extraction method and the full normalised table.
Sixth rung climbed: Torso/MotionState (staged member, appended at the end of
Torso's own range so no downstream id moves). LocalVelocity and
LocalAcceleration turn out to need nothing -- the engine's Mover already
publishes them.
Also banked, and deliberately NOT acted on: ReportLeak and
ConfigureActivePress are base-class attributes inside the pinned range, and
chasing them turned up what looks like the authentic attribute layout. The
shipped pool gives MechSubsystem 1, HeatableSubsystem 3, HeatSink 6,
PoweredSubsystem 5 -- which counted from Subsystem::NextAttributeID=2 lands
MechWeapon's real ids on the binary-pinned 0x12 with EXACTLY ONE pad, where
our tree needs three. That arithmetic is strong evidence for the original
layout, and reconciling to it would replace three guessed pads with the real
table. But it moves ids underneath a cockpit currently verified at 98.9%
pixel-identical, so it wants a deliberate session with the A/B rig open
rather than a bulk edit at the end of a long one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under the documented rig (launch_pod.ps1 with the GL bridge up) the 'couldn't
load object' count went to ZERO. .BGF loading was never a reconstruction
problem, only a missing bridge.
What blocks now is a series of authored AttributeWatchers: BTL4.RES binds
them BY NAME and the engine Fails outright on any that does not resolve, so
each is a name our subsystems must publish. Five rungs climbed, each
run-verified: UnstablePercentage, SpeedEffect (Myomers table), AnimationState
+ CollisionState/CollisionNormal + ReduceButton (Mech), and the full Torso
table (all six authentic names mapped onto existing members).
The method that makes this cheap is banked: the shipped binary's string pool
carries each class's attribute names CONTIGUOUSLY in ID ORDER after the class
name, and in every case so far our member declarations sit in the same order
-- which confirms the layout and lets ids be pinned rather than guessed.
Intersecting those names with BTL4.RES gives the exact work list.
A THIRD miswired SharedData found on the way: Myomers carried Subsystem's
tables where it derives from PoweredSubsystem, the same defect as
MissileLauncher (5.3.33). Worth a sweep across every subsystem.
Staged member TYPES are provisional and documented as such: AttributeWatcherOf
reads *(T*)attributePointer and the instantiation comes from the resource,
which the string pool does not reveal. Nothing drives them yet, so settle the
types with the models that write them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RUNG 1: BTL4VideoRenderer::LoadMissionImplementation, Fail -> bring-up no-op.
Legal, not a cheat: the engine's own VideoRenderer version (VIDREND.CPP:259)
is a bare Tell and our working GaugeRenderer ships the same, so the renderer
comes up and runs its frame loop with an empty scene -- exactly what we want
to measure before writing content. The authentic shape for the real body is
pinned by the surviving sibling header CODE/RP/RP_L4/RPL4VID.HPP (walk the
mission entities -> MakeEntityRenderables -> ReadSKLFile/RecurseSKLFile).
RUNG 2: Mech::EyepointRotation published. The engine then Failed at
SetupCull, which fetches that attribute BY NAME off the viewpoint entity and
composes it with the siteeyepoint segment to build worldToEyeMatrix. The
mech already HAD EulerAngles eyepointRotation with a getter -- only the
publication was missing. One enum id, one table row.
The run now reaches the full mech build, constructs the renderer, boots the
board (~907K VPX wire transactions), loads the mission, runs the per-frame
cull and enters geometry loading with ZERO Fails and zero exceptions. It
stops at 'couldn't load object buttee.bgf' / 'NULL instance' -- and that is
not a defect: the models are present and BTDPL.INI points at them, but .BGF
loading goes through the board to the external GL render bridge that the VPX
HLE tees over VPX_FIFOSOCK, and that bridge was not running. 'NULL instance'
comes from the prebuilt LIBDPL.LIB refusing to instance an object that never
loaded.
Next rung is the documented full rig (render-bridge/launch_pod.ps1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run against the emulated Division card, our build reaches the mission scene
load and stops at the one method we stubbed:
btl4vid.cpp(42): BTL4VideoRenderer::LoadMissionImplementation
-- btl4vid.cpp not yet reconstructed
That proves MakeVideoRenderer, the BTL4VideoRenderer ctor over the engine's
DPLRenderer, the board boot (transputer + i860 firmware, ~858K wire
transactions through the VPX HLE) and Renderer::LoadMission all work in our
build. btl4vid is not a from-scratch climb; the engine half was always
linked and what is missing is the mission-load hook and its renderables.
Banked the reproduction rig (emulator/vidtest.conf + the host VPX env --
VPX_RESPOND alone is not enough, the iserver handshake dies without
VPX_RENDER), the authentic contract from CODE/RP/MUNGA/RENDERER.CPP:263, and
a note that the BT411 donor restructured this hook so the contract comes from
the 1995 engine and only the renderables come from the donor.
Also corrected today's earlier note: the transient 3-D frame seen during a
gauge run was NOT the dPL path. MakeVideoRenderer returns NULL without
DPLARG and no gauge conf sets it, so no video renderer exists in those runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-dossier workflow synthesis: the engine half of BOTH render paths
(DPLRenderer/libDPL for the division-card 3D view; the L4 gauge stack for
the cockpit instruments) ALREADY compiles and links into btl4opt.exe from
the CODE originals, and the emulator's VPX board-side device is
EXE-agnostic (pure wire-level). What's missing is game-TU work with
complete BT411 donors: the 6 gauge TUs (~302 fns, the first rendered
milestone -- diffable against the shipped exe on the same GAUGE content)
and then btl4vid (the 3D world renderable factory). Full protocol map,
env gates, and the staged plan in RENDER-ROADMAP.NOTES.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>