#156 the mission start/end fade RECONSTRUCTED: POVStartEndRenderable was an inert stub (rounds hard-cut) -- the binary's 5-phase fog machine (ctor @00454394, Execute @0045447c; constants byte-read @0x454740-50: flash 0.1s, fades 1.0s, plane floors 0.01/0.05) now runs at the renderer frame head keyed on the local player's SimulationState (3 MissionStarting -> white flash + 1.0s fade-in from white; 4 MissionEnding -> 1.0s fade-to-black, planes closing to the floors, re-arm). Drives the live fog pipeline (currentFogNear/Far + FOGCOLOR, which the frame Clear also samples -- the fade owns sky/void too) and re-reads the STYLE members per frame exactly as the binary re-reads live view fog (mid-fade searchlight swaps track). Armed by the now-real ctor in btl4vid.cpp (house pattern: inert object, module machine). Bench fade_bench.sh: 40s relay-clocked round, both nodes PASS armed + phases 1/2/3/4 in order with the end edge on simState=4. Rider: the [rio] STALL detector now keys on REPLY drought instead of request age (which the 50ms poll refreshed forever -- the detector could never fire; #160 telemetry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-11 18:54:12 -05:00
co-authored by Claude Fable 5
parent 2938d4918c
commit fc71f9c0bb
5 changed files with 187 additions and 6 deletions
+10 -2
View File
@@ -1377,11 +1377,19 @@ Logical
if (operational)
{
DWORD now = GetTickCount();
// #160 telemetry fix: the detector used to key on REQUEST age -- but the
// poll loop sends a fresh request every ~50ms regardless of replies, so
// request-age never reached 500ms and the STALL line could never fire
// (the field logs' age= field carried the signal instead). Key on the
// REPLY drought: requests outstanding AND no AnalogReply for 500ms
// since the last good one. Requires one good reply first (boot-mute
// shows in the req/rep counters, not here).
if (!gRioStallActive && gRioReqPending
&& now - gRioLastReqTick > 500)
&& gRioLastReplyTick != 0
&& now - gRioLastReplyTick > 500)
{
gRioStallActive = 1;
gRioStallStart = gRioLastReqTick;
gRioStallStart = gRioLastReplyTick;
gRioStallBtnBase = gRioBtnCount;
}
gRioPresent = 1;
+101
View File
@@ -8441,6 +8441,107 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
lastFrameTime = mTargetRenderTime;
currentFrameTime = Now();
// #156 -- the MISSION START/END FADE (POVStartEndRenderable, ctor @00454394,
// Execute @0045447c; the port class is armed by MakeEntityRenderables'
// BTPlayer master case via BTMissionFadeArm). The 1995 renderable drives
// the POV zone fog through a 5-phase machine keyed on the local player's
// SimulationState; constants byte-read from BTL4OPT.EXE @0x454740-0x454750:
// flash hold 0.1s, fades 1.0s, plane floors near 0.01 / far 0.05. Phases:
// 0 armed -> (MissionStarting) 1 WHITE FLASH (fog 1,1,1 @ 0.01/0.05) ->
// 2 FADE-IN (white -> live style; planes floor -> authored) -> 3 running ->
// (MissionEnding) 4 FADE-OUT (color x local -> BLACK, planes close) ->
// re-arm. The binary re-reads the LIVE fog each frame mid-fade (style
// swaps track); we read the STYLE members (fogRed/G/B, fogNear/Far), never
// currentFogNear/Far which we are overriding. Placed at the frame head:
// the Clear below samples FOGCOLOR, so the fade owns the WHOLE frame
// including sky/void. Receipts: [fade] phase lines under BT_FADE_LOG or
// BT_FOG_LOG.
{
extern void *gBTFadeIndicator; // BTMissionFadeArm (btl4vid.cpp)
extern int gBTFadeStart, gBTFadeEnd;
static int s_phase = 0;
static float s_T = 0.0f;
if (gBTFadeIndicator != 0)
{
const float kFlash = 0.1f, kFade = 1.0f; // @0x454740 / @0x454744
const float kNearFloor = 0.01f, kFarFloor = 0.05f; // @0x454750 / @0x45474c
int st = ((StateIndicator *)gBTFadeIndicator)->GetState();
float fdt = (float)dT;
if (fdt < 0.0f) fdt = 0.0f; else if (fdt > 0.1f) fdt = 0.1f;
float r = -1.0f, g = 0.0f, b = 0.0f, fn = 0.0f, ff = 0.0f;
switch (s_phase)
{
case 0:
if (st == gBTFadeStart) { s_phase = 1; s_T = 0.0f; }
break;
case 1:
r = 1.0f; g = 1.0f; b = 1.0f; fn = kNearFloor; ff = kFarFloor;
s_T += fdt;
if (s_T >= kFlash) { s_phase = 2; s_T = 0.0f; }
break;
case 2:
{
float local = kFade - s_T; if (local < 0.0f) local = 0.0f;
float el = kFade - local;
r = (1.0f - fogRed) * local + fogRed;
g = (1.0f - fogGreen) * local + fogGreen;
b = (1.0f - fogBlue) * local + fogBlue;
fn = fogNear * el + kNearFloor;
ff = fogFar * el + kFarFloor;
s_T += fdt;
if (local <= 0.0f) { s_phase = 3; r = -1.0f; } // fog back to the style
break;
}
case 3:
if (st == gBTFadeEnd) { s_phase = 4; s_T = 0.0f; }
break;
case 4:
{
float local = kFade - s_T; if (local < 0.0f) local = 0.0f;
r = fogRed * local;
g = fogGreen * local;
b = fogBlue * local;
fn = fogNear * local + kNearFloor;
ff = fogFar * local + kFarFloor;
s_T += fdt;
if (local <= 0.0f) { s_phase = 0; } // re-armed; final frame stays black
break;
}
}
if (r >= 0.0f)
{
currentFogNear = fn;
currentFogFar = ff;
mDevice->SetRenderState(D3DRS_FOGCOLOR,
D3DCOLOR_XRGB((int)(255 * r), (int)(255 * g), (int)(255 * b)));
mDevice->SetRenderState(D3DRS_FOGENABLE, TRUE);
}
else if (s_phase == 3 || s_phase == 0)
{
// Out of the fade: restore the style planes once so a fade that
// ended mid-style leaves the world exactly on its authored fog.
static int s_prev = -1;
if (s_prev != s_phase)
{
currentFogNear = fogNear;
currentFogFar = fogFar;
mDevice->SetRenderState(D3DRS_FOGCOLOR,
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
(int)(255 * fogBlue)));
}
s_prev = s_phase;
}
static int s_lastLogged = -1;
if (s_phase != s_lastLogged)
{
s_lastLogged = s_phase;
if (getenv("BT_FADE_LOG") || getenv("BT_FOG_LOG"))
DEBUG_STREAM << "[fade] phase " << s_phase
<< " (simState=" << st << ")\n" << std::flush;
}
}
}
// DIAG (turn-hitch hunt): time the render phases -- draw CPU vs Present
// (GPU-queue block). Logged on slow frames + 1 Hz stats.
LARGE_INTEGER _rt0; QueryPerformanceCounter(&_rt0);
+29
View File
@@ -3687,6 +3687,35 @@ namespace {
extern void BTSetWorldDead(int dead); // L4VIDRND.cpp bridge -> l4_application->SetIsDead
//===========================================================================//
// #156 -- the mission start/end fade arming seam. The phase machine itself
// runs at the renderer frame head (L4VIDEO.cpp, beside the FOGCOLOR clear);
// these globals are what MakeEntityRenderables' BTPlayer master case arms
// through the now-real BTPOVStartEndRenderable ctor below.
//===========================================================================//
void *gBTFadeIndicator = 0;
int gBTFadeStart = 3, gBTFadeEnd = 4;
// The real ctor (was a btstubs.cpp inert stub -- the #156 root cause: rounds
// hard-cut instead of fading). Binary ctor @00454394 stores the indicator +
// the authored fog params and registers on the view's watcher list; the port
// keeps the object inert (house pattern, like the translocation renderable)
// and arms the frame-head machine instead. The fog params ride the renderer
// members live, exactly as the binary re-reads them per fade frame.
BTPOVStartEndRenderable::BTPOVStartEndRenderable(
Entity *entity, int, dpl_VIEW *, dpl_ZONE *, dpl_ZONE *,
StateIndicator *sim_state,
float, float, float, float, float, int start_state, int end_state)
: BTRenderableBase(entity)
{
gBTFadeIndicator = (void *)sim_state;
gBTFadeStart = start_state;
gBTFadeEnd = end_state;
if (getenv("BT_FADE_LOG") || getenv("BT_FOG_LOG"))
DEBUG_STREAM << "[fade] armed (states " << start_state << "/"
<< end_state << ")\n" << std::flush;
}
// The renderable objects the entity tree builds for the translocation wiring are
// inert now (the warp is the self-contained one-shot below); the ctor/dtor just
// satisfy MakeEntityRenderables.
+3 -4
View File
@@ -336,10 +336,9 @@ BTMarkerWatcherRenderable::BTMarkerWatcherRenderable(
// BTTranslocationRenderable (the "blue warp" translocation sphere) is now a real
// reconstruction in btl4vid.cpp (task #52) -- no longer a stub here.
BTPOVStartEndRenderable::BTPOVStartEndRenderable(
Entity *entity, int, dpl_VIEW *, dpl_ZONE *, dpl_ZONE *, StateIndicator *,
float, float, float, float, float, int, int)
: BTRenderableBase(entity) {}
// BTPOVStartEndRenderable (the mission start/end fade) is now a real
// reconstruction: the arming ctor lives in btl4vid.cpp and the 5-phase fog
// machine at the renderer frame head (L4VIDEO.cpp) -- #156.
BTTracerEffectRenderable::BTTracerEffectRenderable(
Entity *entity, int, void *, int, dpl_ZONE *, dpl_DCS *, LinearMatrix *)
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# #156: the mission start/end fade. 2-node, 40s round -- the relay owns the
# clock and sends StopMission at length end (btconsole.py), so both fade edges
# fire: MissionStarting (white flash 0.1s + 1.0s fade-in from white) and
# MissionEnding (1.0s fade-out to black; the app stops ~2s later inside the
# player's 3.0s ladder). Receipts: [fade] armed + phase 1/2/3/4 transitions
# (BT_FADE_LOG). PASS: node A's log shows armed, then phases 1,2,3,4 in
# order, and the phase-4 line's simState is the ending state (4).
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
rm -f fade_a.log fade_b.log fade_r.log
bt_expert_egg MP.EGG FADE.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/; s/^length=.*/length=40/" FADE.EGG
grep -aq "^length=40" FADE.EGG || { echo "FAIL: egg length not set"; exit 1; }
( export BT_FADE_LOG=1 BT_MATCHLOG=1
bt_launch fade_b.log FADE.EGG 0x0C -net 1601 )
sleep 2
( export BT_FADE_LOG=1 BT_MATCHLOG=1
bt_launch fade_a.log FADE.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py FADE.EGG 127.0.0.1:1501 127.0.0.1:1601 > fade_r.log 2>&1 &
R=$!; sleep 110; kill $R 2>/dev/null; sleep 2
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
python - <<'PY'
import io, re, sys
ok = True
for node in ("fade_a.log", "fade_b.log"):
txt = io.open(node, encoding="latin-1", errors="replace").read()
armed = "[fade] armed" in txt
phases = [int(m.group(1)) for m in re.finditer(r"\[fade\] phase (\d)", txt)]
end4 = re.search(r"\[fade\] phase 4 \(simState=4\)", txt) is not None
print("%s: armed=%d phases=%s end-edge-ok=%d" % (node, armed, phases, end4))
want = [1, 2, 3, 4]
seq_ok = [p for p in phases if p in want] [:4] == want
if not (armed and seq_ok and end4):
ok = False
print("VERDICT:", "PASS -- flash + fade-in + fade-out ran on both nodes"
if ok else "FAIL -- phase sequence incomplete (see above)")
sys.exit(0 if ok else 1)
PY