diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 4d268f5..a58f94e 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -560,6 +560,15 @@ and passed straight into the ApplicationManager ctor by the DOS main whenever the decomp shows an unexplained multiply/divide by `_DAT_0052140c`, it is per-second <-> per-frame conversion at the pod's 28 Hz. +**The AUDIO/EFFECT FRAME CLOCK is this same rate.** `AudioTime:: +Seconds_To_Frames` in the image is `fmul [0x52140c]; fadd 0.5; round` (e.g. +@0x42c611, @0x42dd86 — the 0x42xxxx audio-layer cluster of the ~90 consumers), +so every authored audio duration/delay counts frames at 28. WinTesla's +`DefaultRendererRate` said **30** — all audio timing ran ~7% fast until +corrected (2026-08-02). Oracle's coupled report ("prolonged smoke and +explosion including sound") was the lead: effect visuals and audio share this +clock family. + **So the pod's nominal simulation cadence was 28 fps, not the i860 board's 30 Hz — and it was UNLOCKED.** Corroborated testimony (2026-08-02): Oracle — the pod rate was "flipbook" territory, heavy fights "a slide show"; Lynx diff --git a/engine/MUNGA/RENDERER.cpp b/engine/MUNGA/RENDERER.cpp index 25b7493..b560754 100644 --- a/engine/MUNGA/RENDERER.cpp +++ b/engine/MUNGA/RENDERER.cpp @@ -13,7 +13,12 @@ //############################ Renderer ################################# //############################################################################# -const RendererRate DefaultRendererRate = 30.0f; +// 2026-08-02 (#96 clock trace): the DOS binary's audio/effect frame clock runs +// at the ENGINE FRAME RATE GLOBAL DAT_0052140c = 28.0f (0x401ace), not 30 -- +// AudioTime::Seconds_To_Frames in the image is `fmul [0x52140c]; fadd 0.5` +// (e.g. @0x42c611), and every authored frame-count duration assumes 28. +// 30 here ran ALL audio timing ~7% fast. [T1] +const RendererRate DefaultRendererRate = 28.0f; const RendererComplexity MaxRendererComplexity = 3.0f; const RendererComplexity MinRendererComplexity = 0.0f; const RendererFrameBudget DefaultRendererFrameBudget = 0.14f; //0.12f; // 0.16f; diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index c0acade..8461311 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -745,6 +745,7 @@ struct BTPfxEmitter struct BTPfxParticle { D3DXVECTOR3 pos, vel, accel; + int defIndex; // census attribution (#114 / wreck-smoke tail) float age, life; float rad, exp, dexp; float colorWarp, alphaWarp; // def-level warps, carried per particle @@ -875,6 +876,39 @@ void BTStartPfxFrame(int effect_number, float x, float y, float z, gBTPfxEmitters.push_back(e); } +// #114/wreck-smoke census (BT_PFX_LOG): every emitter start + a 2s population +// line, so "what is still smoking 40s after the kill" is a log fact. +static void BTPfxCensus() +{ + if (!getenv("BT_PFX_LOG")) return; + static unsigned long s_at = 0; + unsigned long now = GetTickCount(); + if (now - s_at < 2000) return; + s_at = now; + int perDef[BT_PFX_SLOTS]; memset(perDef, 0, sizeof(perDef)); + int emitters = 0; + for (size_t i = 0; i < gBTPfxEmitters.size(); ++i) + if (gBTPfxEmitters[i].active) + { + ++emitters; + int di = (int)(gBTPfxEmitters[i].def - gBTPfxDefs); + if (di >= 0 && di < BT_PFX_SLOTS) ++perDef[di]; + } + int partDef[BT_PFX_SLOTS]; memset(partDef, 0, sizeof(partDef)); + for (size_t i = 0; i < gBTPfxParticles.size(); ++i) + { + int di = gBTPfxParticles[i].defIndex; + if (di >= 0 && di < BT_PFX_SLOTS) ++partDef[di]; + } + DEBUG_STREAM << "[pfx] t=" << (now / 1000) << "s census: emitters=" << emitters + << " particles=" << (int)gBTPfxParticles.size(); + for (int d = 0; d < BT_PFX_SLOTS; ++d) + if (perDef[d] > 0) DEBUG_STREAM << " {E" << d << ":" << perDef[d] << "}"; + for (int d = 0; d < BT_PFX_SLOTS; ++d) + if (partDef[d] > 0) DEBUG_STREAM << " {p" << d << ":" << partDef[d] << "}"; + DEBUG_STREAM << std::endl; +} + void BTStartPfx(int effect_number, float x, float y, float z) { BTStartPfxFrame(effect_number, x, y, z, 0, 0, 0); @@ -964,6 +998,7 @@ static void BTPfxSpawn(const BTPfxEmitter &e) // laser bursts during missile volleys) return; BTPfxParticle p; + p.defIndex = (int)(e.def - gBTPfxDefs); // census attribution // Sample in the effect's LOCAL frame, then orient into the world through // the emitter's basis (the victim's localToWorld rows). The position // jitter pv is an isotropic scatter about the base offset -> symmetric @@ -1181,6 +1216,7 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) } } } + BTPfxCensus(); for (size_t ei = 0; ei < gBTPfxEmitters.size(); ++ei) { BTPfxEmitter &e = gBTPfxEmitters[ei]; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 4e0127e..9555312 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2204,6 +2204,18 @@ void // the empty spot). if (!wreck_visible && collisionVolumeCount != 0) { + // #114-family (user report: smoke continues 30-60s after the mech + // disappears): the re-armed plume was a WORLD effect (ownerTag=0), + // so the respawn/burial cleanups could not kill it -- the last 10s + // emission window played out over an empty spot, plus 6+-2s + // particle lives. The plume is entity-tagged now (below), so end + // the emitters WITH the burial; spawned particles fade in <=8s, + // which is the authored envelope (DDTHSMK once: window 10s, + // life 6+-2 -- the binary's smoke died WITH the sink). + { + extern void BTStopEntityPfx(void *owner); + BTStopEntityPfx((void *)this); + } // Zeroing the count stops MoveCollisionVolume from re-placing the // box -- but the mech-vs-mech gather (Mover::GetCurrentCollisions) // tests mover->collisionVolume DIRECTLY, never the count, so the @@ -2225,11 +2237,14 @@ void if (wreck_visible && wreckSmokeTimer <= 0.0f) { wreckSmokeTimer = 10.0f; // DDTHSMK's release window - extern void BTStartPfx(int effect_number, float x, float y, float z); - BTStartPfx(1, + // ATTACHED (not world): tagged to this entity so the burial / + // respawn cleanups can end it, and it rides the sinking hulk. + extern void BTStartPfxAttached(int effect_number, void *owner, + int seg_index, float x, float y, float z, const float *rows9); + BTStartPfxAttached(1, (void *)this, 0, (float)localOrigin.linearPosition.x, (float)(localOrigin.linearPosition.y + kMuzzleHeight), - (float)localOrigin.linearPosition.z); + (float)localOrigin.linearPosition.z, 0); if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[death] wreck smoke plume re-armed (psfx 1)\n" << std::flush; } diff --git a/scratchpad/night9/wrecksmoke.sh b/scratchpad/night9/wrecksmoke.sh new file mode 100644 index 0000000..bbddd2d --- /dev/null +++ b/scratchpad/night9/wrecksmoke.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Wreck-smoke tail: kill the enemy, then WATCH 90s after the kill. +# Reads: [pfx] census every 2s (emitters by psfx slot + particle count), +# [death] wreck re-arm lines, BT_SHOT frames for the visual tail. +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 2 +sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > WS.EGG +rm -f wrecksmoke.log smoke_*.png +export BT_SPAWN_ENEMY=1 BT_GOTO=enemy BT_GOTO_STOP=60 +export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=3 +export BT_PFX_LOG=1 BT_DEATH_LOG=1 BT_DMG_LOG=1 +export BT_SHOT_EVERY=60 BT_SHOT_PREFIX=smoke +bt_launch wrecksmoke.log WS.EGG 0x03 +sleep 190 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2