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>
534 lines
30 KiB
Markdown
534 lines
30 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.
|