diff --git a/context/decomp-reference.md b/context/decomp-reference.md index f3e8447..cd36098 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -419,7 +419,7 @@ default-ON (`'0'` disables). | `BT_GAUGE_SKIP_LOG` | `[gskip] ` per unregistered gauge widget the parse skips | | `BT_VALVE_LOG` | condenser valve flow distribution | | `BT_BAYTEST=` | #46 rig: send message 1 (crit-induced cook-off arm) to the first AmmoBin at the given sim frame — the 10 s fuse then runs live (`scratchpad/baytest.py` / `baypurge.py`) | -| `BT_PLANE_AUDIT` | #48 forensics: log every gauge draw whose color carries bits outside its port's plane mask (the cross-display leak trap; `[plane] LEAK/PORT` lines) | +| `BT_PLANE_AUDIT` | #48 forensics -- **DEFAULT-ON** (`=0` opts out): logs any gauge draw whose color carries bits outside its port's plane mask (`[plane] LEAK/PORT`) AND any out-of-bounds draw start (`[plane] OOB`, clamped). First detection of each also writes a `GLITCH` **matchlog** record, so playtest evidence arrives with the round logs even unattended | | `BT_LOOP_AUDIT` | `[loop-audit]` per-sound `sample-flag × source-render-type -> AL_LOOPING` (Gitea #51; rig `scratchpad/loopaudit.py`) | | `BT_LOOP_LEGACY=1` | restore the OLD always-loop rule (`AL_LOOPING = sample != ForceStatic`) for a field A/B — see the loop-flag note in [[wintesla-port]] | | `BT_AUDIO_DUMP` | once-a-second `[playing]` dump of every playing AL source + gain/pitch/**loop** — catches a stuck looping source red-handed | diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 1a809db..d8fdd8c 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -664,6 +664,16 @@ index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a preservable behaviour, so the cycle-fill is a guarded PORT deviation. Post-fix: **0 leaks over 60s** on the same probe; sim3 3-pod regression clean. +**The tripwires are DEFAULT-ON in every build** (BT_PLANE_AUDIT=0 opts out): the +plane-leak trap, plus an out-of-bounds draw-start trap in `buildDestPointer` +(release `Verify` is a no-op, so a wild start previously wrote silently -- now +logged `[plane] OOB` and clamped). First detection of either class writes a +`GLITCH` matchlog record, so if the artifact has a third cause we have not +seen, the evidence lands in the round logs the operator already collects -- +no env setup needed on playtest machines. (Coverage note: an in-buffer +EXTENT overrun from a valid start -- a blit walking past the right/bottom edge +into later rows of the same plane -- is NOT trapped; per-pixel walk checks are +too hot. The plane trap still catches it whenever the color leaks planes.) SECOND real defect fixed en route: sessions configure the CAMERA seat first (`cameraInit` -- which builds the MISSION-REVIEW context: `configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc)`, a DirectColor context whose table holds diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index f81fc83..2baf373 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -1768,6 +1768,43 @@ void Verify(screenY >= 0); Verify(screenY < height); + // #48 tripwire (DEFAULT-ON; BT_PLANE_AUDIT=0 opts out): release builds + // compile Verify() to NOTHING, so an out-of-bounds draw start silently + // computes a wild pointer into (or past) the shared buffer -- the OTHER + // artifact class this funnel can catch, since every primitive builds its + // start pointer here. Log + clamp; first hit also lands in the matchlog. + { + static int s_oobOn = -1; + if (s_oobOn < 0) + { + const char *e = getenv("BT_PLANE_AUDIT"); + s_oobOn = (e != NULL && *e == '0') ? 0 : 1; + } + if (s_oobOn && + (screenX < 0 || screenX >= width || screenY < 0 || screenY >= height)) + { + static int s_oobCount = 0; + ++s_oobCount; + if (s_oobCount == 1) + { + extern int BTMatchLogActive(); + extern void BTMatchLog(const char *tag, const char *format, ...); + if (BTMatchLogActive()) + BTMatchLog("GLITCH", "kind=oob x=%d y=%d w=%d h=%d", + screenX, screenY, width, height); + } + if (s_oobCount <= 100 || (s_oobCount % 500) == 0) + DEBUG_STREAM << "[plane] OOB #" << s_oobCount + << " draw start at(" << screenX << "," << screenY + << ") outside " << width << "x" << height + << " -- clamped" << "\n" << std::flush; + if (screenX < 0) screenX = 0; + if (screenX >= width) screenX = width - 1; + if (screenY < 0) screenY = 0; + if (screenY >= height) screenY = height - 1; + } + } + *pixel_pointer = pixelBuffer.Data.MapPointer + screenX + (screenY * width); @@ -1861,8 +1898,15 @@ void static void BTPlaneAudit(const char *prim, int color, int bitmask, Enumeration operation, int x, int y) { + // DEFAULT-ON (#48 tripwire): playtest machines set no env vars, so the + // trap must arm itself. BT_PLANE_AUDIT=0 opts out. Cost when armed is a + // couple of integer ops per DRAW CALL (not per pixel). static int s_on = -1; - if (s_on < 0) s_on = (getenv("BT_PLANE_AUDIT") != NULL) ? 1 : 0; + if (s_on < 0) + { + const char *e = getenv("BT_PLANE_AUDIT"); + s_on = (e != NULL && *e == '0') ? 0 : 1; + } if (!s_on) return; int leak; @@ -1879,6 +1923,17 @@ static void BTPlaneAudit(const char *prim, int color, int bitmask, static int s_count = 0; ++s_count; + // Surface the FIRST detection through the matchlog pipeline -- those files + // are collected after every playtest round, so the evidence arrives even + // if nobody was watching btl4.log. + if (s_count == 1) + { + extern int BTMatchLogActive(); + extern void BTMatchLog(const char *tag, const char *format, ...); + if (BTMatchLogActive()) + BTMatchLog("GLITCH", "kind=plane prim=%s x=%d y=%d color=0x%x mask=0x%x leak=0x%x op=%d", + prim, x, y, color, bitmask, leak, (int)operation); + } if (s_count <= 200 || (s_count % 500) == 0) DEBUG_STREAM << "[plane] LEAK #" << s_count << " " << prim << " at(" << x << "," << y << ")" @@ -3921,7 +3976,7 @@ void { { static int s_paOn = -1; - if (s_paOn < 0) s_paOn = (getenv("BT_PLANE_AUDIT") != NULL) ? 1 : 0; + if (s_paOn < 0) { const char *e = getenv("BT_PLANE_AUDIT"); s_paOn = (e != NULL && *e == '0') ? 0 : 1; } if (s_paOn && translation_table != NULL) for (int _pa = 0; _pa < 256; ++_pa) if ((translation_table[_pa] & ~bitmask & 0xFFFF) != 0) @@ -6447,7 +6502,7 @@ void // #48 forensics: name the PORT + pixmap for any leaking table entries. { static int s_paOn = -1; - if (s_paOn < 0) s_paOn = (getenv("BT_PLANE_AUDIT") != NULL) ? 1 : 0; + if (s_paOn < 0) { const char *e = getenv("BT_PLANE_AUDIT"); s_paOn = (e != NULL && *e == '0') ? 0 : 1; } if (s_paOn) for (int _pa = 0; _pa < 256; ++_pa) if ((translationTable[_pa] & ~bitMask & 0xFFFF) != 0)