The desktop plasma marquee showed heap garbage from boot until the gauge
renderer first drew the callsigns. Root cause [T0]: PixelMap8(w,h) allocates
its pixel bytes UNCLEARED (GRAPH2D.cpp `new Byte[w*h]`, no memset) and
Video8BitBuffered never clears them either -- only changedLine[] is zeroed.
The real pod never showed it: the serial PlasmaDisplay transfers only CHANGED
lines, so the garbage never left the buffer and the hardware marquee sat at
its power-on blank. PlasmaWindow::Update however blits the WHOLE buffer
every frame, exposing the junk for the window-up-to-first-draw gap (seconds
on slower rigs).
Fix: PlasmaWindow ctor memsets the pixelBuffer to index 0 (the near-black
background) -- the window comes up blank like the hardware did. Glass-layer
only; the serial pod path is untouched. Verified: glass boot shows a clean
marquee from the first visible frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playtester report: (1) the CONTROL MODE stack showed filled boxes around the
inactive MID/ADV entries, (2) a phantom block between the HEADING dial and the
ARMOR rosette. Root-caused [T1]:
The #48 translation-table cycle-fill (6bb03ae, 2026-07-25) mapped
out-of-range art indices in-plane as (index mod 2^bits) -- so art index 254
landed on plane slot 62 = the LIVE colorMapperMultiArmor right-armor damage
slot. And idx-254 art EXISTS: every SMODE.PCC frame fills the INACTIVE
mode-box interiors with 254, and BTSEC1.PCX carries a stray 52x13 idx-254
bar at port (199-250,101-113) between the heading dial and the rosette (a
scratch duplicate of the rosette quadrant bars). Both lit up in the
current right-armor color (adpal ramp green/orange/red) on EVERY render
path. On the shipped machine those regions rendered BLACK (the garbage
entry's low plane bits were 0), which is why the 2026-07-19 smode audit --
run before the cycle-fill landed -- verified CORRECT.
FIX (BuildSecondaryTranslation): map [2^bits..255] to translationTable[0]
(plane BACKGROUND) -- the authentic on-screen result, same no-leak
guarantee. Verified on both desktop paths: MID/ADV back to authored
borders+text (idx 5/9), no interior fills; the phantom bar gone; the BAS
badge and the live armor rosette (in-range slots 60-63) untouched.
KB: gauges-hud #48 REFINED addendum; GAUGE_COMPOSITE audit row 33 corrected
(binding is ControlsMapper/ControlMode, not DisplayMode) + re-verification
note.
(The companion glass-token palette-generation fix lives on glass-panel-perf --
it depends on the dirty-skip code there. The two branches touch disjoint
hunks and merge in either order.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Latent flaw in the c9e25e5 dirty-skip (now in master): the token hashes plane
PIXELS + lamp state only, but the ColorMapper gauge family (armor rosette
tints, the adpal/adpal2 damage flash) animates by writing CLUT entries with
ZERO pixel churn -- so a glass window whose plane is pixel-static held stale
colors while the D3D surround (which re-expands every frame) tracked live.
New SVGA16::paletteGeneration, bumped by the palette writers:
- BuildSecondaryColor: only on a REAL entry change (the flash alternates
palettes every Execute writing identical RGB at zero damage -- an
unconditional bump would repaint every palette-expanding window every
pump for nothing);
- BuildSecondaryPalette / BuildAuxiliaryPalette / BlankPalette (full
rebuilds): unconditionally.
GlassWindowToken folds it in for palette-expanding windows (monoTint < 0, or
all windows under BT_GLASS_MFD_PAL); mono-tint windows don't read the palette
and keep their pixel-only token.
KB: glass-cockpit.md gains the repaint-pump section (records f3d27f5 +
c9e25e5 + this). Companion branch sec-phantom-fix carries the idx-254
translation-table fix -- disjoint hunks, merges in either order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#150 -- one kill credited twice (Rajel on Oracle, Oracle on Conn Man).
CAUGHT: one death -> two type-2 arms 1ms apart (awards 501.96/505.88 -- the
tally GREW between them), victim inst=R. Mechanism: this port deliberately
applies damage on replicants for local visual response; the report tail ran
there too, and a replicant's movement mode cannot flip to wreck until the
victim's death record round-trips -- so each round of a killing VOLLEY inside
that window re-posted the kill. Why missiles, why intermittent.
The binary forbids the whole situation: its handler OPENS with the assertion
if ((flags & 0xc) == 4) Fail("Replicant Mech recieving takedamage",
MECH.CPP, 0x3da) [part_012.c:14613]
-- TakeDamage on a replicant was a BUG in 1995; the tail only ever ran on the
victim's master. Fix: (1) gate the REPORT TAIL master-only (local damage
application stays); (2) adopt the binary's once-latch -- the 0->1 edge of
IsMechDestroyed() across the handler call (local_14), replacing the
deathBlastArmed form for the report gate.
VERIFIED (scratchpad/night14/scorepair.sh): 3 victim deaths, 5 shooter
respawns -> exactly one type-2 per credited kill, kills 0->1->2, zero doubles
(was: kills 0->2 on one death).
#151 -- panic eject "-499 instead of 0" is AUTHENTIC 1995 SCORING, decoded
from EjectPilot @0049f854: the punch-out dispatches a SELF TakeDamage of
damageAmount = role->killBonus (+0x1c, 500 in dfltrole), type Explosive,
senderMechID = SELF, player->suppressConsole = 1, graphicAlarm -> 10
which flows through the report tail as a SUICIDE kill: award = -CalcKillScore
(your own death), plus the type-1 death cost. 1000 - ~999 - 500 = -499:
the binary predicts Oracle's exact number. Our port already used
role->killBonus for the blast (mech.cpp:652) -- faithful end to end; bench
shows each eject charged exactly once (-900/-1050 across three ejects).
Panic eject = scored as suicide. Whether that is fun is a design question;
it is not a porting defect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
The dirty-skip commit staged the header via a lowercase path (l4vb16.h) while git
tracks it as L4VB16.h on this case-insensitive FS, so the PlaneChecksum declaration
was left out -- c9e25e5 as committed would not build from clean (L4VB16.cpp defines
SVGA16::PlaneChecksum, L4GLASSWIN.cpp calls it, but the header never declared it).
This adds the one-line decl. HEAD now builds standalone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-on to the HALFTONE->COLORONCOLOR fix. BTGlassPanels_Tick repainted all 7
glass windows every ~16 Hz pump unconditionally. Now each window carries a change
token and the pump re-blits ONLY the windows whose token differs:
token = FNV-1a checksum of the shared gauge pixelBuffer masked to the bits this
window can show (SVGA16::PlaneChecksum over primary + Eng-twin + RGB-group
ports) combined with each button's RENDERED lamp brightness + held/latch.
Folding the flash BRIGHTNESS (not the raw lamp state) into the token means a flashing
lamp repaints exactly when it toggles; a static panel or an idle cockpit skips its
expand + StretchDIBits entirely. The masked checksum reads the RENDERED RESULT of the
gauges' values, so it catches everything -- discrete value gauges, the continuous
radar sweep, any imagery -- with no gauge->port->window plumbing and no risk of a
frozen display (full pass, no stride; collision-free in practice).
Measured (dev box, solo mission, BT_GLASS_DIRTY): ~31 pumps/2s -> 4-15 window-repaints
vs 217-224 always-on (~15-40x fewer); avg frame work 3.3 -> 0.93ms. BT_GLASS_DIRTY=1
logs the repaint tally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playtester (Dave, SCREECH-PC, 4K) reported ~20 fps in the exploded per-display
glass panels vs ~130 fps in the cockpit surround -- same scene, same machine
(solo_20260809.log; maxDraw 57-92ms in panels vs 6-31ms surround).
Root cause: BlitSurface used SetStretchBltMode(HALFTONE) -- GDI's slowest,
per-output-pixel resample filter -- and BTGlassPanels_Tick repaints all 7 glass
windows SYNCHRONOUSLY on the main render thread every ~16 Hz. 7x HALFTONE
StretchDIBits of a 640x480 surface per pump stalls the frame.
Default to COLORONCOLOR (nearest); BT_GLASS_SMOOTH=1 restores HALFTONE. The
MFDs are low-res pixel content, so nearest reads crisp -- arguably closer to the
pod CRT than the blur. A/B on the dev box (fast, understates Dave's gain):
avg work 3.3->1.4ms, maxDraw stall 12-27->7ms, ~2.2x more frames per window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9e78d1c said 'smoke-tested: lines live' -- that run never entered the render
loop and printed nothing. This run did: 54 [glassperf] lines, e.g.
ticks=16 tickMs=256.9 | Heat MFD 24.8 | Engineering 20.6 | Comm 18.7 |
LeftW 20.6 | RightW 22.8 | Secondary/Radar 67.9 | ...
First finding, already: the synchronous panel sweep costs ~257ms/second even
on the DEV box (25% tax, hidden in headroom), and Secondary/Radar -- the
ROTATED portrait blit -- is the most expensive window. Oracle's machine
paying 4-8x that through his driver is now a quantified hypothesis, not a
guess. Optimization targets regardless of his verdict: dirty-gated repaints,
non-HALFTONE unscaled blits, cheaper rotation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
Oracle confirmed the separate-window glass mode WAS fast and tanked ~2 builds
back, so the regression is real and expresses only on his machine. This names
the cost from inside his own session log:
[glassperf] ticks=N tickMs=T | <window> n=paints ms=cost | ...
tickMs is the synchronous UpdateWindow sweep measured INSIDE the render frame
(L4VIDEO calls the tick), so on a machine where GDI serialises against D3D
present it IS the per-frame tax, and the per-window split names the panel.
Each WM_PAINT is QPC-timed at the dispatch chokepoint (the HALFTONE
StretchDIBits whose cost is driver-dependent). Default ON like [segperf];
BT_PERF_LOG=0 opts out. Smoke-tested: lines live in a 60s panel run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
The reported "torso no longer recentres on its own" decomposed into two parts:
the auto-recentre in Mid/Adv was never authentic (it was the stuck-centerCommand
bug acting as a phantom feature since ~674 -- analysis on the ticket, awaiting
Oracle's 1995-memory verdict), but underneath it sat a REAL defect: with the
phantom gone, a glass/RIO player had no way to recentre the torso at all.
WHY. The only centerCommand writer lived inside the desktop key-bridge block,
which is OFF whenever a RIO/PadRIO is present -- glass and the pod both. The
pod's dedicated CENTER button (0x42, "the shipped .RES name", UP arrow via
bindings.txt) reached nothing: benched two scripted 0x42 holds on the RIO path,
ctrCmd=0 throughout, twist parked forever. (Keyboard X/NumPad5 recentred only
as a side effect of ALL-STOP -- you could not recentre without stopping.)
FIX, two pieces, both existing patterns:
* L4PADRIO::EmitButton -- the documented single chokepoint every button
source funnels through -- publishes the 0x42 HOLD state
(gBTTorsoCenterHeld), exactly the 0x3F ReverseThrust precedent.
* mechmppr gains ONE unified centerCommand writer, deliberately OUTSIDE the
key-bridge gate (the same placement lesson as the mode-cycle hook):
hold = torsoCenter(@0x154 databound) OR gBTTorsoCenterHeld(0x42) OR the
one-frame X pulse; asserts while held, clears on release. Single writer
== the sources can never stomp each other's clear.
BENCHED (scratchpad/night14/center42.sh, RIO path, bridge off):
before: ctrCmd=0 in all samples across two 0x42 holds; twist parked at 1.478
after: hold -> ctrCmd=1 recen=1, twist slews 2.22->0.44 at the authored
0.87 rad/s; twist input DURING the hold cancels sim-side (authentic);
release clears; no button -> aim holds (authentic Std/Vet).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
Operator call: an opt-in flag on the one machine we most need data from is
the silent-diagnostic mistake this project's doctrine exists to prevent.
Every session log now carries [segperf] beside every [rstat] window, so the
next playtest gives the 817->857 stall hunt cross-machine baselines for free
-- Oracle's stalling rig and Sauron's clean one, same night, same build.
Cost: ~2 QPC reads per segment query, microseconds per second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
A/B on identical 2-node beam-heavy runs (scratchpad/night14/segperf.sh),
BT_BEAM_SEGFRESH=1 (swept behaviour) vs =0 (pre-sweep compose at the beam
site, the only per-frame swept call):
fresh(swept) legacy(pre-sweep)
rstat blocks>50ms 0 0
maxDraw worst 730ms 646ms (mission-load spike, BOTH)
segperf dirty-passes 38/s 4/s <- the sweep DOES multiply
segperf accessor ms 0.97/s 0.57/s <- ...by 0.4 ms/s. Noise.
So the invalidation-storm hypothesis I filed on #149 is wrong by three orders
of magnitude, and Oracle's sustained 50-104ms stall window does NOT reproduce
on this rig at all. Refusing to guess a third time: the build now carries the
telemetry to answer it on the machine that actually regresses --
[segperf] calls= dirty= ms= printed beside every [rstat] window under
BT_PERF_LOG (JMOVER counters; two integer increments when unset), and
BT_BEAM_SEGFRESH=0 remains as a one-env A/B for the beam site.
Default stays FRESH (the swept accessor): its measured cost is trivial and it
is the correctness-cautious side while the peer-beam-staleness question is
unmeasured.
Next for #149: Oracle runs one session with BT_PERF_LOG=1. If [segperf] ms is
large inside his stalled windows, segment work is implicated on HIS
configuration and BT_BEAM_SEGFRESH=0 gives the immediate A/B; if it is small
(as here), the stall is elsewhere in the 817->857 delta and we hunt with his
numbers instead of my theories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
teleport-poisoned finite difference, not heat, not the reset, not velocity
The one-line fix is the binary's own SECOND Reset instruction, dropped in
transcription: FUN_00408440(mech+0x58c, param_2) ==
accelPrevPos = origin.linearPosition;
+0x58c is the previous-position memory of the AccelerationLastFrame ring feed
(+0x81c/0x824/0x828/0x82c). The port reconstructed the ring faithfully (ctor
part_012.c:9836, derivative :15169) but Reset never re-seeded its cache, so the
first post-respawn sample computed |newPos - prevPos|/dt = TELEPORT DISTANCE/dt
(~1e5) into the velocity ring; the ring-mean derivative spiked
AccelerationLastFrame (pure forward, with an opposite-sign echo ~15 frames later
as the sample rotated out of the 15-ring); and the myomer integrator @004b8d18
turned it into a one-tick pendingHeat deposit of ~3e9:
termAccel = (1-accEff) * |v| * |a| * m * dt
= 0.2 * 40 * 1.04e5 * 75000 * 0.044 = 2.75e9
snapping the freshly-reset myomers from T=77 to T~9000 against failT=2000 ->
speedEffect 0 -> speedDemand *= 0 -> "respawned unable to move until it cools".
WHY ~8% IN THE FIELD: the deposit needs |v| in the same 1-2 frames, so only
pilots whose throttle was still forward at the respawn -- physical lever /
HOTAS, exactly who reported it -- had gait-republished speed in the spike
frames. Idle-throttle respawns deposit ~nothing. (And frozen-subset-of-
died-hot: died hot == was running hard == lever still forward.)
MEASURED, same abusive bench (0.95 throttle + continuous autofire):
before: deposits up to 3.35e9, every one 3-4 lines after a Mech::Reset;
4-6 of 7 respawns frozen; post-reset myomers T 7700-12100
after: deposits >1e7: ZERO across 7 respawns; frozen: ZERO;
post-reset myomers T 77-178 (vs degradeT=1000)
THEORIES KILLED ON THE WAY, each by operand data, in order:
* stale pendingHeat carryover (bounded to 1 frame, +1.2K -- gates agent)
* slow accumulation during the death window (consumers tick the wreck)
* conduction from a hot neighbour (roster-wide snapshot: ALL partners 77;
flow trap: zero e6 flows into the myomers, ever)
* drag/impulse writers (both trapped: never fired)
* my own earlier "velocity-driven, players accelerate to top speed" close of
the ticket -- arithmetically impossible (input ceiling ~6.5e5/frame ~=
60 deg/s; the observed snap was 1,100-21,000 deg/s) and corrected in
context/subsystems.md, which carried the wrong paragraph.
* the localAcceleration zero-fill (+0x1dc) restored along the way is KEPT --
the binary does it -- but it was NOT the cause; the snapshot is rebuilt
from the position difference one frame later.
Probes kept (all BT_HEAT_LOG-gated): roster-wide [myofreeze] at-death/at-reset/
post-reset (T + heatEnergy + pendingHeat per subsystem), the [heatflow]
conduction trap with full operands, the [myodep] deposit trap with mech
identity + acceleration components. Engine MOVER.cpp traps reverted -- they
proved their negative (drag/impulse innocent) and do not belong in engine
source.
Gotcha #30 records the class: when the binary's Reset writes a cell you don't
recognize, that write IS the spec -- transcribe the whole zero/seed list; any
prev-value cell backing a finite difference must be re-seeded at every
teleport; and derived state (T) sampled at the reset proves nothing about the
backing producers.
The operator called the shape of this two days ago: "maybe the math gets screwy
in respawning while some systems are ticking while values are being reset."
That is precisely what it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC