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>
729 lines
41 KiB
Markdown
729 lines
41 KiB
Markdown
# The render roadmap — from headless to visible (researched 2026-07-24)
|
|
|
|
Four-dossier workflow synthesis (card protocol / video-TU delta / emulator
|
|
side / gauge path). The headline: **the render seam is far closer than
|
|
expected** — the engine half of BOTH render paths already compiles and links
|
|
into our btl4opt.exe from the CODE originals, and the emulator's board-side
|
|
device is EXE-agnostic.
|
|
|
|
## The two independent render paths (either can run without the other)
|
|
|
|
1. **The 3D main view (division card)**: the game speaks dPL — a
|
|
RETAINED-mode scene graph API (never pixels, never transformed polys) —
|
|
through the statically linked libDPL, which frames every call into
|
|
vr_action messages on the INMOS C012 link adapter at port 0x150
|
|
(FIFO fast path: body as REP OUTSW words to 0x154, trailing length word
|
|
to 0x151 commits). Gate: `DPLARG` env (set by SETENV.BAT when
|
|
L4VIDEO != OFF); NULL DPLARG = the headless shape we run today
|
|
(`MakeVideoRenderer()` returns NULL, tolerated).
|
|
- Per-frame submission: **engine code** — `DPLRenderer::
|
|
ExecuteImplementation` (CODE/RP/MUNGA_L4/L4VIDEO.CPP:5065) walks the
|
|
dplRenderableSocket; renderables (L4VIDRND.CPP, ~30 classes) set DCS
|
|
matrices; `dpl_FlushDCSArticulations` = wire action 0x1f (batched
|
|
poses, absolute 3x3+T or [sin,cos] joint records); `vr_draw_scene`
|
|
(9) kicks the frame; `velocirender_sync` (0x2d, exe @0x48D220) is the
|
|
ack-less bulk-op fence.
|
|
- **Our build ALREADY COMPILES this whole engine layer** (mungal4.lib:
|
|
l4video/l4vidrnd/l4dplmem + prebuilt 1995 STARTDPL.obj + LIBDPL.LIB;
|
|
see build410 obj logs). The missing piece is GAME code: **btl4vid**
|
|
(BTL4VideoRenderer — MakeEntityRenderables/MakeMechRenderables/
|
|
effects/wreck swap/material substitution; BT411 donor 3039/848 lines
|
|
with per-method @ADDR evidence, binary extent @4cc40c..@4d2bbc; our
|
|
stub Fails at LoadMissionImplementation).
|
|
2. **The gauge path (cockpit instruments)**: completely separate hardware —
|
|
the PC's own SVGA (VESA 0x111, 640x480x16, L4SVGA16.ASM banked blits),
|
|
split across VGA heads by the VDB board (port 0x300, palette groups; the
|
|
octopus-cable 5-mono-MFDs + color-radar decode). Gate: `L4GAUGE` env.
|
|
The plasma readout is a gauge-renderer graphics port (PlasmaDisplay on
|
|
COM2), not a separate renderer.
|
|
- Engine half (GaugeRenderer/L4GaugeRenderer/the 5171-line L4GAUGE
|
|
widget interpreter/GAUGMAP/L4VB16) already compiles + links in
|
|
build410; one documented deviation (interpreter table 46864 -> 262144
|
|
for BT's larger CFG).
|
|
- Missing: the 6 BT gauge TUs — **btl4gaug (130 fn) / btl4gau2 (77) /
|
|
btl4gau3 (46) / btl4rdr (36) / btl4grnd (13, ours is a 4-fn stub) /
|
|
btl4galm** — ~302 functions/~43KB, binary extent 0x4c19fc-0x4cc40c,
|
|
the largest function-count block left in btl4.lib. BT411 donors are
|
|
COMPLETE (semantic .cpp/.hpp with @ADDR evidence, ~5900 donor lines);
|
|
the work is RE-HOSTING onto 1995 engine APIs, not fresh decompilation.
|
|
Class inventory per TU in the dossier (widget primitives / composite
|
|
clusters / MP-HUD / radar MapDisplay / the renderer / alarm manager).
|
|
|
|
## The emulator side is READY and EXE-agnostic
|
|
|
|
The DOSBox-X fork's VPX device (emulator/vpx-device/vpxlog.cpp, ports
|
|
0x150-0x161 + VDB 0x300-0x31A) implements the BOARD side purely at the wire
|
|
level: blind firmware absorb, iserver handshake (VPX_RESPOND=1), generic
|
|
acks, the 0x2d sync echo, draw_scene frame-acks with a REAL raycast reticle
|
|
pick against its own decoded scene, and the GL renderer (VPX_RENDER=1) that
|
|
draws the main view + cockpit windows. It never reads game memory and keys
|
|
only on wire contents — **our reconstructed exe drives it unmodified once it
|
|
emits the same traffic**. The AWE32 pair is mandatory for any timed run
|
|
(SOS FAST clock); gauges also render live on the emulated S3 with the
|
|
shipped exe (GAUGE-NOTES.md: HEAPSIZE=15000000 + granularityInKB=64).
|
|
|
|
## The plan (order of value)
|
|
|
|
1. **Gauges-only first** (decisively cheaper): set `L4GAUGE=640x480x16` (+
|
|
HEAPSIZE), reconstruct the 6 gauge TUs from the BT411 donors. Built-in
|
|
oracle: the shipped exe renders reference output from the SAME GAUGE
|
|
content — per-head diffable. Live instruments need the attribute
|
|
sources, which the 5.3.x waves have largely built (weapon states, heat,
|
|
power, damage, radar).
|
|
2. **Then btl4vid** (the 3D world renderer) + DPLARG on: the engine
|
|
DPLRenderer + libDPL do the wire; btl4vid builds the renderables per
|
|
entity; verify against the VPX device's GL window and the dpl3-revive
|
|
bridge.
|
|
3. The review/playback pair (btl4pb + the BTL4.CPP review branch) closes
|
|
the last absent TUs.
|
|
|
|
Message-id / protocol quick refs: vr_create=1 (host-assigned handles),
|
|
vr_flush=3 (node structs; VIEW proves 832x512), geometry 0x17/0x19/0x1a
|
|
(texel word [pad,B,G,R]), artics 0x1f, draw 9, sync 0x2d, fire 0x23,
|
|
reticle 0x26. DPLARG reference value in emulator/baseline.conf:21.
|
|
|
|
================================================================================
|
|
2026-07-27 -- THE 3-D PATH RUNS TO MISSION LOAD (measured, not estimated)
|
|
================================================================================
|
|
Our reconstruction, run against the emulated Division card, gets ALL THE WAY to
|
|
the mission scene load before it stops -- at the one method we stubbed.
|
|
|
|
c:/.../bt_l4/btl4vid.cpp(42): BTL4VideoRenderer::LoadMissionImplementation
|
|
-- btl4vid.cpp not yet reconstructed
|
|
|
|
What that single line proves is working end to end in OUR build:
|
|
* BTL4Application::MakeVideoRenderer builds a BTL4VideoRenderer
|
|
* the BTL4VideoRenderer ctor chains the engine's DPLRenderer
|
|
* the board boots: transputer + i860 firmware upload, ~858K wire
|
|
transactions logged by the VPX device HLE
|
|
* Renderer::LoadMission runs (RENDERER.CPP:273) and calls the hook
|
|
|
|
So btl4vid is NOT a from-scratch climb. The engine half was always linked;
|
|
what is missing is the game's mission-load hook and the renderables it builds.
|
|
|
|
HOW TO REPRODUCE (this is the rig, and it is cheap):
|
|
emulator/vidtest.conf -- baseline.conf with the mount made absolute and
|
|
BTL4REC.EXE (our build, staged beside the shipped BTL4OPT.EXE which is NOT
|
|
touched) instead of the shipped exe. Host env before launching dosbox-x:
|
|
|
|
VPX_RESPOND=1 VPX_RENDER=1 VPX_NOMAIN=1 VPXLOG=<file> VPX_DUMPDIR=<dir>
|
|
|
|
VPX_RESPOND alone is NOT enough -- with only that, the board boot dies at
|
|
"Protocol error : length 65535 too big / rcv_protocol fail during iserver
|
|
handling", which is the iserver handshake reading a floating bus.
|
|
VPX_NOMAIN=1 keeps it from opening windows; add VPX_EXPLODE=1 only when you
|
|
want the 7-display pentapus. The full documented pod launch is
|
|
render-bridge/launch_pod.ps1 (LAUNCH.md) when the GL bridge is wanted.
|
|
|
|
THE CONTRACT for the next brick (authentic, CODE/RP/MUNGA/RENDERER.CPP:263):
|
|
Renderer::LoadMission verifies InactiveRendererStatus, sets
|
|
LoadingRendererStatus, stamps nextRenderTime, calls
|
|
GetRendererManager()->StartRenderer(this), THEN LoadMissionImplementation.
|
|
The base implementation Fails ("should never reach here") -- every concrete
|
|
renderer must override it.
|
|
|
|
DONOR: BT411 game/reconstructed/btl4vid.cpp, 3188 lines / 27 methods, carrying
|
|
the renderable family (BTReticleRenderable, BTTranslocationRenderable, the
|
|
pending-wrecks map). NOTE it does not contain a LoadMissionImplementation by
|
|
that name -- the WinTesla port restructured the hook -- so take the CONTRACT
|
|
from the 1995 engine above and the CONTENT from the donor's renderables.
|
|
|
|
CORRECTION to the note earlier today: a transient 3-D cockpit frame captured
|
|
during a gauge-rig run was NOT evidence of the dPL path emitting anything.
|
|
MakeVideoRenderer returns NULL unless DPLARG is set (BTL4APP.CPP:152), and no
|
|
gauge conf sets it -- so no video renderer exists in those runs at all. That
|
|
frame was almost certainly attract/loading art out of BTL4.RES. The evidence
|
|
above is the real thing, and it came from setting DPLARG deliberately.
|
|
|
|
--------------------------------------------------------------------------------
|
|
TWO RUNGS CLIMBED -- the 3-D pipeline now runs end to end to the geometry load
|
|
--------------------------------------------------------------------------------
|
|
RUNG 1: BTL4VideoRenderer::LoadMissionImplementation, Fail -> bring-up no-op.
|
|
A no-op is a LEGAL body, not a cheat: the engine's own
|
|
VideoRenderer::LoadMissionImplementation (CODE/RP/MUNGA/VIDREND.CPP:259) is a
|
|
bare Tell, and our working GaugeRenderer (GAUGREND.CPP:3275) ships the same.
|
|
The renderer comes up and runs its frame loop with an EMPTY scene, which is
|
|
what we wanted to measure before writing content.
|
|
|
|
RUNG 2: Mech::EyepointRotation PUBLISHED. With the renderer live the engine
|
|
immediately Failed at
|
|
|
|
l4video.cpp(5004): DPLRenderer::SetupCull jointed mover had no
|
|
EyepointRotation attribute
|
|
|
|
SetupCull (CODE/RP/MUNGA_L4/L4VIDEO.CPP:4990) fetches "EyepointRotation" BY
|
|
NAME off the viewpoint entity and, for a JointedMover, composes it with the
|
|
"siteeyepoint" segment to build worldToEyeMatrix. Our mech already HAD
|
|
EulerAngles eyepointRotation with a getter (MECH.HPP) -- only the attribute
|
|
publication was missing. One enum id + one table row. (The siteeyepoint
|
|
check passed first time, so that segment was already streaming correctly.)
|
|
|
|
WHERE IT STOPS NOW, and why that is not a defect:
|
|
|
|
L4VIDEO.cpp couldn't load object buttee.bgf (x123, also dg100/mslr)
|
|
NULL instance
|
|
|
|
ZERO Fails, zero exceptions -- the run reaches the full mech build (all 7
|
|
weapons), constructs the renderer, boots the board (~907K VPX wire
|
|
transactions), loads the mission, runs the per-frame cull and gets all the way
|
|
into geometry loading. The models ARE present (VIDEO/GEO/BUTTEE.BGF etc, 879
|
|
BGFs) and BTDPL.INI points at them (objectpath=.ideo\geo), but loading a
|
|
.BGF 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, i.e. the library refusing to instance an
|
|
object that never loaded.
|
|
|
|
NEXT RUNG: run under the documented full rig (render-bridge/launch_pod.ps1,
|
|
see LAUNCH.md) so the bridge is up and geometry actually loads. Only then is
|
|
it worth writing real MakeEntityRenderables content -- until the bridge is in
|
|
the loop there is nothing to see even if the scene graph is correct.
|
|
|
|
--------------------------------------------------------------------------------
|
|
THE FULL POD RIG: geometry loads, and the ladder is now the AUDIO WATCHER SET
|
|
--------------------------------------------------------------------------------
|
|
Run under the documented rig (render-bridge/launch_pod.ps1 with the GL bridge
|
|
up) the geometry complaint COUNT WENT TO ZERO -- .BGF loading was never a
|
|
reconstruction problem, only a missing bridge, exactly as predicted.
|
|
|
|
emulator/render-bridge/gauge_arena_rec.conf = gauge_arena_sound.conf with
|
|
the exe swapped to BTL4REC.EXE (our build, staged into ALPHA_1/REL410/BT
|
|
beside the shipped BTL4OPT.EXE, which is NOT touched).
|
|
Launch: .\launch_pod.ps1 -Conf ...\gauge_arena_rec.conf
|
|
|
|
What blocks now is a SERIES of authored AttributeWatchers. BTL4.RES binds
|
|
watchers BY NAME and the engine Fails outright on any that does not resolve
|
|
(WATCHER.CPP:141) -- these are the sound triggers, so every one of them is a
|
|
name our subsystems must publish.
|
|
|
|
RUNGS CLIMBED THIS PASS (each one run-verified, ~6 min per cycle):
|
|
UnstablePercentage Mech, staged member (no instability model yet)
|
|
SpeedEffect Myomers -- member existed, table published
|
|
AnimationState Mech -- member existed (bound 307x in the RES!)
|
|
CollisionState/Normal Mech -- state existed, normal staged
|
|
ReduceButton Mech, staged
|
|
SpeedOfTorsoHorizontal Torso -- all six names mapped to real members
|
|
|
|
THE METHOD THAT MAKES THIS CHEAP (use it instead of one rung per run):
|
|
The shipped binary's string pool carries each class's attribute names
|
|
CONTIGUOUSLY, in ID ORDER, right after the class name and its message names.
|
|
So: strings -a BTL4OPT.EXE | grep -n -A14 '^Torso$' gives the authentic
|
|
table for that class, and in every case so far our member declarations sit in
|
|
the SAME ORDER -- which both confirms the reconstruction's layout and lets the
|
|
ids be pinned rather than guessed. Cross-reference against the resource with
|
|
|
|
comm -12 <(strings BTL4.RES|grep -x '[A-Z][A-Za-z0-9]\{3,\}'|sort -u) <(strings BTL4OPT.EXE|...|sort -u)
|
|
|
|
which returns 59 candidate names; the attribute-like ones are the work list.
|
|
|
|
STILL TO PUBLISH (from that intersection, owner in brackets where known):
|
|
MotionState [Torso? -- sits between StickPosition and
|
|
TorsoHorizontalEnabled in the pool, but that region MIXES attribute
|
|
names with resource-field names, so confirm before publishing]
|
|
LocalVelocity, LocalAcceleration [Mover/Entity]
|
|
AmmoState [AmmoBin] CondenserState [Condenser]
|
|
ReservoirState [Reservoir] GeneratorState [Generator]
|
|
SimulationState [Subsystem] DisplayMode [ControlsMapper]
|
|
LookForward/Left/Right/Down/Behind [MechControlsMapper]
|
|
LaserOn, ReportLeak, FireCountdownStarted, TargetRangeExponent,
|
|
ConfigureActivePress, StickPosition
|
|
|
|
A THIRD MISWIRED SharedData FOUND ON THE WAY: Myomers carried
|
|
Subsystem::MessageHandlers + Subsystem::AttributeIndex where it derives from
|
|
PoweredSubsystem -- the same defect as MissileLauncher (5.3.33), dropping
|
|
every powered attribute AND the powered message handlers. Torso is wired to
|
|
Subsystem's too, but there the chain (PowerWatcher -> HeatWatcher ->
|
|
MechSubsystem) genuinely publishes nothing, so only its AttributeIndex moved.
|
|
WORTH A SWEEP: check every subsystem's DefaultData against its actual parent.
|
|
|
|
TYPES ARE PROVISIONAL on the staged members. AttributeWatcherOf<T> reads
|
|
*(T*)attributePointer (WATCHER.HPP:288) and the instantiation comes from the
|
|
resource, which the string pool does not reveal. Nothing drives these yet so
|
|
no watcher can fire -- settle the types WITH the models that write them.
|
|
|
|
--------------------------------------------------------------------------------
|
|
THE WATCHER LIST IS NOW FULLY KNOWN -- stop discovering it one run at a time
|
|
--------------------------------------------------------------------------------
|
|
BTL4.RES stores each watcher as a (SUBSYSTEM, ATTRIBUTE) string pair, so the
|
|
whole list can be read straight out of the resource instead of one 6-minute
|
|
run per name:
|
|
|
|
for each candidate name, find its NUL-terminated occurrences in BTL4.RES
|
|
and take the printable string immediately before it -- that is the owning
|
|
subsystem. Candidates = strings present in BOTH BTL4.RES and BTL4OPT.EXE
|
|
matching ^[A-Z][A-Za-z0-9]{3,}$ (59 of them).
|
|
|
|
Normalised across instance names (ERMLaser_1/2/3 -> the weapon class,
|
|
AmmoBinSRM6_1 -> AmmoBin, Condenser1..6 -> Condenser, GeneratorA..D ->
|
|
Generator), the COMPLETE set the engine will demand is:
|
|
|
|
Entity AnimationState(306) LocalVelocity(324) SimulationState(74)
|
|
CollisionSpeed(72) CollisionState(54) FootStep(36)
|
|
IncomingLock(36) UnstablePercentage(36) LocalAcceleration(18)
|
|
DistanceToMissile(18) ReduceButton(18) CollisionNormal(18)
|
|
weapons WeaponState PercentDone LaserOn ReportLeak
|
|
ConfigureActivePress SimulationState
|
|
AmmoBin AmmoState FireCountdownStarted SimulationState
|
|
Condenser CondenserState ReportLeak
|
|
Generator GeneratorOn GeneratorState ReportLeak
|
|
Reservoir ReservoirState
|
|
Myomers SpeedEffect ConfigureActivePress ReportLeak SimulationState
|
|
Avionics(Sensor) ConfigureActivePress ReportLeak SimulationState
|
|
ControlsMapper DisplayMode Look{Forward,Left,Right,Down,Behind}
|
|
TargetRangeExponent
|
|
Torso SpeedOfTorsoHorizontal MotionState
|
|
HeatSink CurrentTemperature
|
|
|
|
DONE so far (6 rungs, each run-verified): UnstablePercentage, SpeedEffect,
|
|
AnimationState, CollisionState/Normal, ReduceButton, SpeedOfTorsoHorizontal
|
|
(+ the whole Torso table), MotionState. LocalVelocity / LocalAcceleration
|
|
need nothing -- the ENGINE's Mover already publishes them (MOVER.CPP:104).
|
|
|
|
LEAF-SAFE APPENDS (do these next; each goes at the END of a class that
|
|
nothing chains off, so no id moves): ReservoirState, CondenserState,
|
|
GeneratorState (maps to the EXISTING stateAlarm), AmmoState +
|
|
FireCountdownStarted (AmmoBin has no table at all yet), LaserOn (Emitter),
|
|
TargetRangeExponent (ControlsMapper).
|
|
|
|
--------------------------------------------------------------------------------
|
|
!! ReportLeak AND ConfigureActivePress ARE NOT LEAF-SAFE -- and they reveal
|
|
the authentic attribute layout. DO NOT bulk-edit these without deciding.
|
|
--------------------------------------------------------------------------------
|
|
Both belong to BASE classes in the pinned range. The shipped string pool
|
|
gives each class's attribute names contiguously in id order:
|
|
|
|
MechSubsystem ConfigureActivePress (1)
|
|
HeatableSubsystem CurrentTemperature DegradationTemperature
|
|
FailureTemperature (3)
|
|
HeatSink NormalizedPressure DegradationPressure
|
|
CoolantCapacity CoolantMass
|
|
CoolantMassLeakRate ReportLeak (6)
|
|
PoweredSubsystem InputVoltage AuxScreenNumber AuxScreenPlacement
|
|
AuxScreenLabel EngScreenLabel (5)
|
|
|
|
Count them from Subsystem::NextAttributeID = 2: 2 | 3..5 | 6..0x0B |
|
|
0x0C..0x10 -> NextAttributeID = 0x11 -> MechWeapon needs EXACTLY ONE pad
|
|
to land its real ids on the binary-pinned 0x12. Our tree needs THREE pads,
|
|
because our tables differ: we publish HeatLoad (authentic does not),
|
|
CoolantAvailable (authentic does not), and a different PoweredSubsystem five
|
|
(OutputVoltage/RatedVoltage/VoltageState/ConnectMode instead of the four
|
|
AuxScreen* names). The one-pad arithmetic landing exactly on 0x12 is strong
|
|
evidence the four rows above ARE the original layout.
|
|
|
|
Reconciling to it is a real improvement -- it would replace three guessed
|
|
pads with the authentic table -- but it moves ids underneath the gauge
|
|
cockpit that is currently verified at 98.9% pixel-identical, so it wants a
|
|
deliberate session with the A/B rig open, not a bulk edit at the end of a
|
|
long one. Neither HeatLoad nor CoolantAvailable is bound by any gauge (0
|
|
hits in L4GAUGE.CFG), so the drop side looks cheap; the risk is the id shift,
|
|
not the names.
|
|
|
|
--------------------------------------------------------------------------------
|
|
A TRAP IN THE WATCHER WORK: publishing a name can CRASH the pod
|
|
--------------------------------------------------------------------------------
|
|
Publishing Reservoir/ReservoirState crashes the pod on the DOS extender
|
|
("Reference to a page you don't own" -> PF), and BISECTING proved it is that
|
|
one change: revert it and the run returns to a clean Fail; re-apply it alone
|
|
and the crash returns. Everything else in the same batch (Condenser,
|
|
Generator, Emitter, ControlsMapper, Torso, Myomers) is innocent and is IN.
|
|
|
|
WHY IT MATTERS BEYOND ONE ATTRIBUTE: AttributeWatcherOf<T> does
|
|
|
|
currentValue = *(T*)attributePointer; (WATCHER.HPP:306)
|
|
|
|
AT CONSTRUCTION. So a published name is READ the moment its watcher is
|
|
built -- "nothing drives the value yet, so the type cannot matter" (which is
|
|
what the earlier staging note claimed) is WRONG. A wrong-typed target is a
|
|
live crash, not a deferred cosmetic issue.
|
|
|
|
Things ruled out by measurement, so nobody repeats them:
|
|
* NOT the AlarmIndicator-vs-int type. Publishing a plain int
|
|
reservoirState instead got FURTHER (log 232 -> 749 bytes) but still
|
|
crashed, so the object copy is not the whole story.
|
|
* NOT static-init order. reservr.obj sorts LAST in the bt lib (build410.sh
|
|
puts TUs missing from the 1995 order list at the end), so
|
|
HeatSink::AttributeIndex is constructed before Reservoir's.
|
|
* NOT an id gap. ReservoirState sits at HeatSink::NextAttributeID with the
|
|
parent chain contiguous behind it.
|
|
* NOT visible on the gauge rig. The same binary runs clean under
|
|
vis_rec.conf -- only the pod/arena context faults, so reproduce it there.
|
|
|
|
The crash itself: 0044B4AD, `mov eax,[edx+0x18]` then `call [eax+4]`, with
|
|
EAX = 0x15 and the fault at 0x19. A small integer is being called through as
|
|
an object -- an index slot holding an ID where a pointer belongs. That is
|
|
the AttributeIndexSet::Build uninitialised-slot signature, so the next step
|
|
is to dump Reservoir's built index set and compare it against a class whose
|
|
publication works (Condenser is the control: same base, plain int, no crash).
|
|
|
|
SOLVED. The linker map named the faulting function outright:
|
|
|
|
tlink32 -m writes build410/btl4opt.map; the crash address minus the CODE
|
|
base (0x410000) is the map offset, and "Publics by Value" gives the
|
|
enclosing symbol. 0x3B4AD landed inside
|
|
AudioStateWatcher::AudioStateWatcher(PlugStream*,Entity*) +0x2D.
|
|
|
|
AudioStateWatcher is AudioWatcherOf<StateIndicator> and its ctor runs
|
|
|
|
Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this)
|
|
(AUDWTHR.CPP:891)
|
|
|
|
immediately. So every authored *State name MUST be published as a
|
|
StateIndicator (AlarmIndicator counts -- it derives from one). Point it at a
|
|
plain int and that member call goes through garbage and the pod dies on the
|
|
extender. That is why BOTH earlier attempts failed: the AlarmIndicator try
|
|
was right in type but was tested while other bugs were still in the batch,
|
|
and the "plain int" try was wrong in type and merely crashed further along.
|
|
|
|
Fixed by publishing state objects: Reservoir/ReservoirState -> reservoirAlarm,
|
|
Generator/GeneratorState -> stateAlarm, and NEW StateIndicator members for
|
|
Condenser/CondenserState and Torso/MotionState. Note StateIndicator has no
|
|
Initialize() -- it takes its count in the ctor, and the default ctor is
|
|
enough here because the watcher only needs the object to exist (the socket it
|
|
attaches to is a member). Verified on the pod: no crash, ladder advanced to
|
|
ReportLeak.
|
|
|
|
THE REUSABLE TECHNIQUE: when the pod dies with an extender fault, take the
|
|
address out of the dump, subtract 0x410000, and look it up in
|
|
btl4opt.map. It names the exact engine function -- which, for this family of
|
|
work, names the watcher class and therefore the required member type.
|
|
|
|
--------------------------------------------------------------------------------
|
|
THE WATCHER LADDER IS DONE -- and the next brick is named by the engine itself
|
|
--------------------------------------------------------------------------------
|
|
Every authored AttributeWatcher now resolves. The pod run reaches the mission,
|
|
DRAWS THE FULL COCKPIT (sensor cluster, myomers, cooling, the weapon panels,
|
|
kills/deaths) with the board booted and audio running, and exits GAME-RC=0
|
|
without a Fail. What it does NOT do is build a 3-D scene, and the engine says
|
|
why in one line:
|
|
|
|
Entity 1:1 class3035 couldn't figure out how to MakeEntityRenderables
|
|
L4VIDEO.cpp couldn't load object sky.bgf (then the whole arena:
|
|
aw02/aw03/aw04/afloor/bcor1/bdet1/bdet2/bpip1 ...)
|
|
NULL instance
|
|
|
|
VideoRenderer::MakeEntityRenderables (CODE/RP/MUNGA/VIDREND.CPP:231) is the
|
|
BOTTOM of a virtual chain -- its own comment says "when a higher level of this
|
|
virtual can't figure out how to make something it calls down to the level
|
|
below it. If we reach here then nobody could figure out what to do." Our
|
|
BTL4VideoRenderer does not override it, so every entity falls through to the
|
|
complaint, no scene graph is built, and the arena geometry that would hang off
|
|
it never loads. class3035 = 0xBDB.
|
|
|
|
CORRECTION to the earlier note in this file: the first full-rig run was
|
|
recorded as "geometry complaint count went to ZERO -- .BGF loading was never a
|
|
reconstruction problem, only a missing bridge". That reading was wrong. The
|
|
count was zero because the run Failed at an attribute watcher long BEFORE
|
|
reaching geometry. Now that the watchers all resolve, the loads are attempted
|
|
and they fail -- because nothing built the renderables to hang them on, not
|
|
because of the bridge.
|
|
|
|
SO THE NEXT BRICK IS THE REAL btl4vid BODY, and its shape is pinned by the
|
|
surviving sibling header CODE/RP/RP_L4/RPL4VID.HPP:
|
|
|
|
void MakeEntityRenderables(Entity*, ResourceDescription *model_resource,
|
|
ViewFrom type); <- override this
|
|
dpl_DCS* ReadSKLFile(Entity*, ResourceDescription*, ViewFrom,
|
|
char skeleton_type);
|
|
void RecurseSKLFile(Entity*, dpl_DCS *parentDCS, NotationFile *skeleton,
|
|
const char *page_name, int recursion_depth, ViewFrom,
|
|
int *joint_counter, dpl_LOAD_MODE cache_mode);
|
|
char SetupMaterialSubstitutionList(Entity*, ViewFrom);
|
|
void TearDownMaterialSubstitutionList();
|
|
|
|
i.e. walk the mission's entities, read each one's .SKL skeleton notation pages
|
|
(VIDEO/*.SKL are present in the mount), recurse them into a dpl_DCS hierarchy,
|
|
and load the .BGF geometry per node. Content for the BT-specific renderables
|
|
comes from the BT411 donor (game/reconstructed/btl4vid.cpp, 3188 lines:
|
|
BTReticleRenderable, BTTranslocationRenderable, the pending-wrecks map).
|
|
|
|
--------------------------------------------------------------------------------
|
|
FIRST RENDERABLE ANSWER: BTPlayer -- and how to read a NON-crashing pod run
|
|
--------------------------------------------------------------------------------
|
|
BTL4VideoRenderer now overrides MakeEntityRenderables. First answer:
|
|
class 3035 = BTPlayerClassID (VDATA.HPP: the BT block starts at
|
|
BTL4ApplicationClassID = 3000, so counting down the enum gives 3035) carries
|
|
NO GRAPHICS -- exactly what the engine already does for its own
|
|
PlayerClassID with an empty case. Everything else still chains to
|
|
DPLRenderer, so this override can only ADD answers, never remove one the
|
|
engine already gives.
|
|
|
|
Result on the pod: the "couldn't figure out how to MakeEntityRenderables"
|
|
complaint is GONE, and the run no longer exits -- it keeps running. But it
|
|
renders nothing: the wire fifodump grows at ~24 bytes/sec (1.4KB total),
|
|
which is keep-alive traffic, not a frame stream. Expected -- the mech and
|
|
the arena still have no renderables, only the player has been answered.
|
|
|
|
A PRACTICAL PROBLEM TO SOLVE FIRST NEXT TIME: a pod run that does NOT crash
|
|
produces NO readable log. The conf redirects the game's stdout
|
|
(> OUTREC.TXT) and DOS buffers it, so the file stays 0 bytes until the
|
|
process exits normally; every log this session that was readable came from a
|
|
run that CRASHED, because the fault handler flushed. Killing the pod loses
|
|
the buffer entirely.
|
|
|
|
Two ways out, both already in the toolbox:
|
|
* `echo GAME-RC=%errorlevel% >> RC.TXT` after the game line -- a separate
|
|
command, so it lands even when the game's own buffer is lost. Already
|
|
added to gauge_arena_rec.conf and it is how the clean GAME-RC=0 exit was
|
|
confirmed.
|
|
* the com3/serial3=file trick from the emulator notes -- live unbuffered
|
|
game output. Worth wiring into the pod conf before the next renderables
|
|
session, otherwise progress is invisible while things are going WELL.
|
|
|
|
NEXT: the mech (MechClassID 3001) and the arena/terrain entities are what
|
|
actually need renderables -- MakeEntityRenderables -> ReadSKLFile /
|
|
RecurseSKLFile over the .SKL skeleton pages (VIDEO/*.SKL are in the mount),
|
|
building a dpl_DCS hierarchy and loading the .BGF geometry per node.
|
|
|
|
--------------------------------------------------------------------------------
|
|
THE NO-OP WAS THE BUG: an override REPLACES the base, it does not extend it
|
|
--------------------------------------------------------------------------------
|
|
DPLRenderer::LoadMissionImplementation (L4VIDEO.CPP:6007) is NOT empty:
|
|
|
|
DPLReadEnvironment(mission); // opens L4DPLCFG (btdpl.ini), walks the
|
|
// compare/branch pages for this
|
|
// location/time and calls
|
|
// dpl_SetObjectFilePath / material /
|
|
// texmap from the objectpath= entries
|
|
LoadNameBitmaps();
|
|
|
|
The "bring-up no-op" installed earlier therefore did not do nothing -- it
|
|
SUPPRESSED the engine's own mission load, so the dPL library never learned
|
|
where the art lives and every dpl_LoadObject returned NULL. That is the whole
|
|
explanation for the 40 failed arena objects and the NULL instance.
|
|
|
|
Justifying the no-op by pointing at VideoRenderer::LoadMissionImplementation
|
|
(a bare Tell) and GaugeRenderer's (the same) was the error: neither is OUR
|
|
base. Check the ACTUAL base before overriding anything 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 -- more
|
|
evidence the authentic btl4vid chained too.)
|
|
|
|
BEFORE AFTER
|
|
40x "couldn't load object" 0
|
|
"NULL instance", run ends runs on
|
|
wire ~24 bytes/sec (keep-alive) ~12 KB/s (1.17MB and climbing)
|
|
bridge live at 88fps
|
|
|
|
PROVED IT WAS OURS, NOT THE RIG: the SHIPPED binary was run under the same
|
|
conf on the same rig and loaded every object (0 failures). That A/B is cheap
|
|
now and is the right first move whenever the pod misbehaves.
|
|
|
|
STILL BLACK. The bridge renders nothing yet and its camera sits at the
|
|
default (0,10,0) rather than the mech's eyepoint, so what is going over the
|
|
wire is state plus object loads, not a populated scene. The mech
|
|
(MechClassID 3001) and the arena entities still need MakeEntityRenderables
|
|
bodies -- ReadSKLFile / RecurseSKLFile over the .SKL pages into a dpl_DCS
|
|
hierarchy -- and the view/eyepoint flush wants checking once something is
|
|
actually in the zone.
|
|
|
|
--------------------------------------------------------------------------------
|
|
RIG: fast, observable pod runs (use this from now on)
|
|
--------------------------------------------------------------------------------
|
|
emulator/render-bridge/pod_render_rec.conf + `launch_pod.ps1 -NoSound`
|
|
|
|
* NOSOUND IS FINE ON THE SLOW CLOCK. The old note "--no-sound FREEZES
|
|
bt/rp" is half the story: it is the FAST SOS clock that needs the AWE32.
|
|
setenv.bat args are `r s n p` (RIO, SLOW clock, NOSOUND, plasma+gauges).
|
|
Saves the ~4 minute SoundFont upload AND the ~300MB of awe_*.wav taps
|
|
each run.
|
|
* LIVE UNBUFFERED LOG: serial3=file file:<path> plus `> COM3` instead of
|
|
`> OUTREC.TXT`. DOS char devices are not buffered, so the log is
|
|
readable WHILE the game runs. A plain file redirect stays 0 bytes until
|
|
the process exits normally -- which is why every readable log before this
|
|
came from a run that crashed.
|
|
* pod_render_shp.conf is the same conf running the SHIPPED exe, for the A/B.
|
|
|
|
================================================================================
|
|
THE 3-D WORLD RENDERS. Reconstruction -> emulated Division card -> a picture.
|
|
================================================================================
|
|
emulator/render-bridge/first-3d-frame.png -- the arena from the pod: sky,
|
|
horizon, ground, the arena structures along the skyline with their markings,
|
|
running at ~31fps on the VelociRender bridge.
|
|
|
|
The full chain now works end to end in OUR build: MakeVideoRenderer ->
|
|
BTL4VideoRenderer over DPLRenderer -> board boot (transputer + i860 firmware)
|
|
-> Renderer::LoadMission -> DPLReadEnvironment (art paths) -> 40/40 arena
|
|
objects loaded -> mission launch -> per-frame submission over the wire.
|
|
|
|
WHAT ACTUALLY UNBLOCKED IT, and the mistake worth remembering:
|
|
the run was NOT stalled after InitializePlayerLink. I concluded that from a
|
|
150-second sample and it was simply too short. Application::
|
|
CheckLoadMessageHandler (APP.CPP) reposts itself every second and refuses to
|
|
advance until the MIN-priority event queue drains; with the video renderer in
|
|
the mission that load takes minutes, not seconds. Traced with BT_LAUNCH_LOG:
|
|
|
|
[launch] state=11 minPriorityEmpty=0 <- loading, queue busy
|
|
... 60+ ticks ...
|
|
[launch] state=2 minPriorityEmpty=0
|
|
[launch] state=2 minPriorityEmpty=1 <- drained
|
|
BTL4Application::RunMissionMessageHandler (x2 -- the two messages that
|
|
Turning Plasma Score Display On carry WaitingForLaunch ->
|
|
BTL4Application::RunMissionMessageHandler LaunchingMission ->
|
|
Turning Plasma Score Display On RunningMission)
|
|
[tick] roster live (first Sensor frame), radarPercent=1 voltState=4
|
|
|
|
and that matches the SHIPPED binary's sequence line for line. The renderer
|
|
holds a deliberately blank screen until RunningMission
|
|
(L4VIDEO.CPP:5100, "Simple HACK to hold us on a blank screen till the mission
|
|
is actually started") -- so "black" was never a rendering bug, it was the app
|
|
still loading. WAIT FOR THE LAUNCH MARKERS before diagnosing a black pod.
|
|
|
|
STATE OF THE SCENE: no entity is unbuildable any more (zero "couldn't figure
|
|
out how to MakeEntityRenderables"), zero geometry failures, and the frame is
|
|
static only because the mech is parked with no pilot input. The camera in
|
|
the bridge title is the BRIDGE's own viewer (launch_pod: "arrows in the RENDER
|
|
window tune eye height", FP_UPOFF), not the game's eyepoint -- do not read it
|
|
as the pilot view without checking.
|
|
|
|
NEXT: drive it. With controls fed (vRIO is already running in this rig) the
|
|
mech should move and the scene should animate; then the mech's own model and
|
|
the cockpit-interior renderables are what to verify against the shipped
|
|
binary, which is a cheap A/B now (pod_render_shp.conf).
|
|
|
|
--------------------------------------------------------------------------------
|
|
FRAME RATE: measured properly, and what is actually missing from the scene
|
|
--------------------------------------------------------------------------------
|
|
The pod updates roughly once every several seconds. Measured by capturing a
|
|
head window every 2s and counting changed frames (scratchpad/headrate.py):
|
|
|
|
OURS 5 of 12 samples changed (~5s between updates)
|
|
SHIPPED 1 of 12 samples changed (~24s between updates)
|
|
|
|
So the reconstruction is NOT slower than the shipped binary here -- the
|
|
emulated Division board is simply expensive, and both run the pod at this
|
|
rate. CAVEAT on that comparison: ours was being driven (BT_FORCE_THROTTLE,
|
|
speed 14.4, gauges genuinely changing) while the shipped exe ignores those
|
|
env hooks and sat parked, so it had less to redraw. Treat the two numbers as
|
|
"same order", not as a win.
|
|
|
|
WHAT DID COST US 3x, AND IT WAS MINE: BT_MECH_LOG / BT_LAUNCH_LOG write
|
|
per-frame [sim]/[mppr] lines, and with DEBUG_STREAM=cout redirected to COM3
|
|
every one of them goes out through an emulated serial port. Turning them off
|
|
took the wire from 480 to ~1480 bytes/sec. Keep per-frame logging OFF for
|
|
anything timing-related -- pod_render_quiet.conf is that conf.
|
|
|
|
DO NOT READ WIRE BYTES/SEC AS A FRAME RATE. Shipped pushes ~8900 B/s against
|
|
our ~1480, and the difference is not speed -- it is CONTENT. Shipped is
|
|
submitting the mech; we are not:
|
|
|
|
OURS: L4VIDEO.cpp wrong video resource type for object mad.skl
|
|
SHIPPED: (no such line)
|
|
|
|
The mech's model resource is a SKELETON (mad.skl). The engine's default
|
|
MakeEntityRenderables only accepts Object/Rubble resource types and rejects
|
|
anything else with exactly that message (L4VIDEO.CPP:4265), because handling
|
|
skeletons is the GAME renderer's job -- RPL4VID.HPP's ReadSKLFile /
|
|
RecurseSKLFile. So our arena renders but the MECH IS ABSENT, and with it the
|
|
cockpit interior. That single unimplemented path explains both the missing
|
|
model and the lower wire volume.
|
|
|
|
NEXT BRICK, now precisely scoped: in BTL4VideoRenderer::MakeEntityRenderables
|
|
answer MechClassID (3001) by reading the .SKL notation pages (VIDEO/*.SKL are
|
|
in the mount) into a dpl_DCS hierarchy and hanging the per-node .BGF geometry
|
|
off it -- ReadSKLFile + RecurseSKLFile per the sibling header, content from
|
|
the BT411 donor.
|
|
|
|
================================================================================
|
|
SPEC FOR THE NEXT BRICK: the mech skeleton (.SKL -> dpl_DCS tree)
|
|
================================================================================
|
|
Everything needed to write it is now known. Written down so the next session
|
|
is mechanical rather than exploratory.
|
|
|
|
THE FILE FORMAT (VIDEO/MAD.SKL, a plain NotationFile):
|
|
|
|
[DamageZones] dz_dtorso=0 ... dz_utorso=21 (22 of them)
|
|
[ROOT] JointCount=25 DZoneCount=22
|
|
tranx/trany/tranz pitch/yaw/roll
|
|
joint=jointlocal joint=jointshadow
|
|
[jointlocal] parent=ROOT Type=balltranslate
|
|
tranx.. pitch.. joint=jointhip joint=jointlthigh ...
|
|
[jointhip] parent=jointlocal Type=hingex
|
|
Object=mad_hip.bgf <- the geometry
|
|
dzone=dz_hip <- damage-zone tags
|
|
tranx.. pitch.. joint=jointtorso site=siteedz_hip
|
|
|
|
So each page is one node: a local transform, an OPTIONAL .bgf object, zero or
|
|
more damage-zone tags, and its child joint page names. Type= is the joint
|
|
kind (ball / balltranslate / hingex / hingey) and matters for animation, not
|
|
for the initial build.
|
|
|
|
THE dPL CALLS (CODE/RP/MUNGA_L4/libDPL/dpl/DPL.H):
|
|
|
|
dpl_NewDCS() create a transform node
|
|
dpl_SetDCSMatrix(d, float32 *m) its 4x4 matrix
|
|
dpl_AddDCSToDCS(parent, child) parent it
|
|
dpl_AddDCSToScene(d) root it
|
|
dpl_SetDCSZone(d, zone) assign the zone (MakeNewZone gives one)
|
|
dpl_LoadObject(name, dpl_load_normal)
|
|
dpl_NewInstance() / dpl_SetInstanceObject(i,o) / dpl_AddInstanceToDCS(d,i)
|
|
dpl_FlushDCS(d) push it to the card
|
|
|
|
THE ALGORITHM (matches RPL4VID.HPP's ReadSKLFile + RecurseSKLFile):
|
|
ReadSKLFile: open the .SKL as a NotationFile, make the zone, recurse "ROOT".
|
|
RecurseSKLFile(page, parentDCS):
|
|
1. read tranx/y/z + pitch/yaw/roll -> a matrix -> dpl_NewDCS + SetDCSMatrix
|
|
2. parent it (AddDCSToDCS, or AddDCSToScene at the root)
|
|
3. if Object= -> dpl_LoadObject, dpl_NewInstance, SetInstanceObject,
|
|
AddInstanceToDCS (Entity_Being_Created is already set by the caller so
|
|
the C callback can tag geometry with damage zones -- see L4VIDEO.CPP:4176)
|
|
4. record dcs_array[segment index] so DPLJointToDCSTranslator (L4VIDEO.CPP:
|
|
6326) can map joint -> DCS afterwards
|
|
5. for each joint= child, recurse
|
|
|
|
THE ONE UNKNOWN: the exact dpl_MATRIX layout/convention. No surviving source
|
|
calls dpl_SetDCSMatrix -- the RP game file that did is missing, like BT's.
|
|
Derive it from dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md, which decodes DCS
|
|
bodies as "type 5 (dcs), 4x4 dpl_MATRIX each", and from DPLTYPES.H.
|
|
|
|
SUCCESS CRITERION, and it is exact. The dpl3-revive reference capture of a
|
|
real pod decodes as "Transforms: flush bodies type 5 (dcs) x26" and
|
|
"Placements: type 4 (instance) x22". MAD.SKL declares JointCount=25 (+1 root
|
|
= 26) and DZoneCount=22. So a correct walk of this one file should put 26 DCS
|
|
flushes and 22 instance flushes on the wire -- countable in the fifodump
|
|
without looking at a single pixel.
|
|
|
|
--------------------------------------------------------------------------------
|
|
API INVENTORY for the skeleton walk -- all confirmed, nothing left to discover
|
|
--------------------------------------------------------------------------------
|
|
NotationFile (CODE/RP/MUNGA/NOTATION.HPP) reads every field the .SKL needs:
|
|
GetEntry(page, entry, Scalar*) tranx/trany/tranz, pitch/yaw/roll
|
|
GetEntry(page, entry, const char**) Object= (the .bgf)
|
|
MakeEntryList(page, "joint") the child pages
|
|
MakeEntryList(page, "dzone") the damage-zone tags
|
|
NameList iteration is entry = list->GetFirstEntry(); entry;
|
|
entry = entry->GetNextEntry() -- exactly how DPLReadINIPage walks
|
|
objectpath= at L4VIDEO.CPP:1858, so copy that idiom.
|
|
|
|
dPL side, all in libDPL/dpl/DPL.H:
|
|
dpl_NewDCS / dpl_SetDCSMatrix / dpl_AddDCSToDCS / dpl_AddDCSToScene
|
|
dpl_SetDCSZone / dpl_FlushDCS
|
|
dpl_LoadObject(name, dpl_load_normal) / dpl_NewInstance /
|
|
dpl_SetInstanceObject / dpl_AddInstanceToDCS / dpl_FlushInstance /
|
|
dpl_FlushObject
|
|
|
|
THE MATRIX, as far as the evidence goes. A DCS flush body decodes as
|
|
[remote:4][type_check:4][node:4][matrix:64] -- 16 float32, a 4x4 (confirmed
|
|
by dpl3-revive/patha/analyze_scene.py, which reads rest[4:68] as 16 floats).
|
|
Decoding a real BT capture (patha/bt1.raw.bin) prints a near-identity DCS with
|
|
a single 10.0 term in the LAST ROW, which points at row-major with the
|
|
translation in row 3 -- but the printed rows look shifted by one word against
|
|
a true identity, so that decoder's offset is suspect by exactly 4 bytes.
|
|
DO NOT trust this without re-deriving it; the cheap way is to build the tree
|
|
with IDENTITY matrices first (below), then set one known node's translation
|
|
and watch which slot moves in the fifodump.
|
|
|
|
RECOMMENDED ORDER OF WORK -- two stages, each independently verifiable:
|
|
STAGE 1: walk the .SKL and build the tree with IDENTITY matrices. Every
|
|
node collapses onto the origin so it looks wrong, but the STRUCTURE is
|
|
provable without pixels: the wire must carry 26 DCS flushes and 22 instance
|
|
flushes (MAD.SKL: JointCount=25 +root, DZoneCount=22 -- and the dpl3-revive
|
|
reference capture of a real pod shows exactly 26 and 22). Getting those
|
|
counts proves the recursion, the object loads and the parenting.
|
|
STAGE 2: put the real transforms in and fix the convention by observation.
|
|
|
|
ONE API STILL TO CHECK BEFORE STAGE 1: how the .SKL filename reaches us. The
|
|
engine builds a chain via L4VideoObjectWrapper::BuildVideoObjectChainFromResource
|
|
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.
|