Files
BT411/engine/MUNGA_L4/L4VB16.h
T
CydandClaude Opus 4.8 2f9c1146a3 red radar: a FLASHING radar button goes red instead of yellow
Player request.  The radar's buttons are the yellow class, so an alerting lamp
pulsed yellow among yellows -- a brightness change only, which is the night-16
miss that already produced the #172 alert ring and the protrusion bonus.  The
bright colour of a FLASHING radar button now becomes the house red
(230,70,70 -- the MFD family), so an alert reads as a COLOUR change.

Only the bright phase moves: dim and off keep the yellow family, so a radar
button that is not alerting looks exactly as before.  Flashing is bits 0-1 of
the lamp word (0 = solid), i.e. the pod's own way of saying alert -- no new
state, just a different colour for the state it already had.

ONE decode, BTRadarLampBright(lampState) in L4VB16.h beside BTLampBrightnessOf,
consumed by all THREE desktop renderers -- CkLampColors (surround), PaintGlass
(exploded windows) and L4PADPANEL -- so they cannot drift apart the way the
button GEOMETRY did before L4RIOBANK.  BT_RADAR_FLASH_RED=0 restores the yellow
bright.

GLASS-ONLY DEVIATION (#154-class): the pod's buttons are single-colour backlit
hardware and cannot change colour.  This lives entirely in the three desktop
renderers; the serial RIO lamp path is untouched, so a real pod is unaffected
by construction.

Also adds the bench hook this needed: BT_LAMPTEST=<addr>[,<state>] forces one
lamp's state at PadRIO::GetLampState -- the single read point all three
renderers share -- so lamp rendering can be checked without inducing the fault
that would raise it.  Default 0x33 is the Panic lamp's alert shape (flashFast +
state1Off + state2Bright).

Verified [T2] by pixel tally of the radar window with BT_LAMPTEST=0x1b
(GeneratorB's lamp, the night-16 one): 620 house-red px with the feature on, 0
with BT_RADAR_FLASH_RED=0, and the radar-yellow count falling by exactly that
many (2820 -> 2196).  Screenshot: the one flashing lamp is red, every other
radar button still yellow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 14:36:31 -05:00

706 lines
18 KiB
C++

#pragma once
#include "..\munga\graph2d.h"
#include "..\munga\time.h"
#include <D3DX9.h>
#include <D3DX9math.h>
class L4graphicsPort;
class GaugeRenderer;
// GLASS: resolve the gauge renderer / main window. DEFINED in game/btl4main.cpp
// off the REAL app + window pointers it holds as locals -- NOT off the
// `application`/`ghWnd` globals, which are duplicate-defined and bind
// non-deterministically per link under /FORCE (the "duplicate-symbol race" --
// CMakeLists.txt:186), so a newly-added engine TU (L4GLASSWIN) can read NULL.
GaugeRenderer *BTResolveGaugeRenderer();
void *BTResolveMainWindow(); // HWND (kept void* -- no windows.h dependency)
//########################################################################
//#################### COCKPIT SURROUND (BT_COCKPIT) #####################
//########################################################################
//
// The pod-faithful single-window layout: the 3D world view CENTERED, with the
// six live gauge surfaces composited AROUND it at 1/2 native scale (the pod
// monitors physically clip the eyeport, so the corner MFDs / top-center / etc.
// overlap the view edges) and clickable RIO button lamps. DEFAULT under
// BT_DEV_GAUGES (BT_COCKPIT=0 falls back to the dock-bottom strip).
//
// The layout is the SINGLE SOURCE OF TRUTH -- computed from the backbuffer
// (canvas) size and consumed by the window sizing, the world viewport, the
// panel/button draw, the aspect calc, and the mouse hit-test. See L4VB16.cpp.
//
struct BTCockpitBtn
{
int address; // RIO buttonGroup address (0x00-0x47)
int x, y, w, h; // backbuffer-space rect (full hit target; only the
// protruding lamp edge shows after the surface draws over)
int colorClass; // 0 = red (MFD), 1 = yellow (radar), 2 = blue (flight)
int inert; // 1 = no authored .CTL mapping -> drawn greyed
const char *label; // non-NULL (blue flight block) -> face is drawn + labeled
};
struct BTCockpitLayout
{
int canvasW, canvasH;
int viewX, viewY, viewW, viewH; // centered world view
int surfX[6], surfY[6], surfW[6], surfH[6]; // Heat,Mfd2,Mfd1,Mfd3,sec,Comm dest rects
BTCockpitBtn buttons[80]; // 5*8 red + 16 yellow + 16 blue = 72
int buttonCount;
};
extern int gBTGaugeCockpit; // mode select; set once by btl4main
extern int gBTCockpitCanvasW, gBTCockpitCanvasH; // backbuffer canvas dims (for the aspect calc)
//
// LETTERBOX FIT (2026-07-26). The cockpit canvas is a FIXED size -- D3D9
// stretches it into whatever the client area is, so a window dragged to a
// different shape used to squash the instruments (the projection was
// aspect-corrected, but the panels, lamps and MFD glass were not).
//
// Now the canvas is fitted at ONE uniform scale, centred, with the leftover
// left black: the cockpit scales both ways and never distorts, whatever the
// window (drag, maximise, -fit borderless). D3D9 does the scaling at Present
// time via a destination rect, which D3DSWAPEFFECT_DISCARD forbids -- so the
// WINDOWED swap effect becomes D3DSWAPEFFECT_COPY when the surround is up and
// multisampling is off (COPY cannot multisample). gBTCockpitLetterbox says
// whether that happened; when it is 0 the old full-client stretch stands, so
// an MSAA run still works, just without the letterbox.
//
extern int gBTCockpitLetterbox; // 1 = the swap chain can take a dest rect
// The uniform-scale centred rect for a client area. Returns 1 when it differs
// from the full client (letterboxing needed), 0 when it fills it exactly.
int BTCockpitFitRect(int clientW, int clientH, RECT *out);
// Present destination for the main device window, or NULL for the full client.
const RECT *BTCockpitPresentDest(void *hwnd, RECT *storage);
void BTCockpitCanvasFor(int viewW, int viewH, int *canvasW, int *canvasH);
void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out);
void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device);
int BTCockpitMouseDown(int cx, int cy, int clientW, int clientH, int rightButton); // 1 = consumed
void BTCockpitMouseUp(void);
// Lamp brightness decode (RIOBase::LampState) -- shared by the cockpit draw and
// the glass per-display windows. Returns 0 (off) / 1 (dim) / 3 (bright).
//
// The wire word is TWO brightness fields plus a flash mode (L4RIO.h [T0]):
// bits 0-1 solid=0 / flashSlow=1 / flashMed=2 / flashFast=3
// bits 2-3 state 1 brightness (state1Off=0, state1Dim=1, state1Bright=3)
// bits 4-5 state 2 brightness (state2Off=0, state2Dim=1, state2Bright=3)
// Solid shows state 1; flashing ALTERNATES state 1 and state 2 at a
// 500/250/125 ms half-period.
//
// ⚠ FIXED 2026-07-26: this used to return max(state1,state2) and blank to 0 on
// the alternate phase. That agrees with the enum only when one state is Off --
// true for the Panic lamp (L4CTRL.cpp flashFast+state1Off+state2Bright) and for
// L4LAMP's off/dim pulse, which is why it went unnoticed -- but L4LAMP.cpp:252
// commands flashFast + state1Dim + state2Bright, a DIM-to-BRIGHT pulse that
// rendered as a hard BRIGHT-to-OFF blink. RP412's L4MFDVIEW LampLevel has the
// faithful formula; this is it.
inline int BTLampBrightnessOf(int state, unsigned long tick)
{
int level1 = (state >> 2) & 0x3;
int level2 = (state >> 4) & 0x3;
int flash = state & 0x3;
if (flash == 0) return level1;
unsigned long half = (flash == 1) ? 500 : (flash == 2) ? 250 : 125;
return ((tick / half) & 1) ? level2 : level1;
}
//
// Is this lamp FLASHING rather than solid? Bits 0-1 of the wire word are the
// flash mode (0 = solid, 1/2/3 = slow/med/fast), so any non-zero value means
// the lamp is pulsing -- which is the pod's own way of saying ALERT.
//
// RADAR FLASH = RED (2026-08-14, player request; GLASS-ONLY DEVIATION). The
// radar's buttons are the yellow class, and a yellow lamp pulsing among other
// yellow lamps is easy to miss -- the night-16 miss that also produced the
// #172 ring and the protrusion bonus. The desktop renderers therefore swap
// the BRIGHT colour of a FLASHING radar button to the red family, so an alert
// reads as a colour change and not just a brightness change. The pod's own
// buttons are physical bicolour-less lamps and cannot do this, so it is a
// desktop affordance only -- and it is confined to the three desktop
// renderers (surround, exploded windows, pad panel); the serial RIO lamp
// path is untouched. BT_RADAR_FLASH_RED=0 restores the yellow bright.
//
inline int BTLampIsFlashing(int state)
{
return (state & 0x3) != 0;
}
//
// The bright colour a radar (yellow-class) button should use right now: the
// red family while the lamp is flashing, its own yellow otherwise. ONE
// definition, consumed by all three desktop renderers so they cannot drift
// (the lesson of the L4RIOBANK geometry split). Returns 0x00RRGGBB.
//
inline unsigned long BTRadarLampBright(int lampState)
{
static int enabled = -1;
if (enabled < 0)
{
const char *e = getenv("BT_RADAR_FLASH_RED");
enabled = (e != 0 && e[0] == '0') ? 0 : 1; // default ON
}
return (enabled && BTLampIsFlashing(lampState))
? 0x00E64646ul // 230,70,70 -- the house red (MFD family)
: 0x00F5D23Cul; // 245,210,60 -- the radar's own yellow
}
//########################################################################
//######################### Video16BitBuffered ###########################
//########################################################################
//
// X/Y values passed to these routines are assumed to be in
// the VIDEO DISPLAY COORDINATE SYSTEM, with (0,0) at the top left.
//
class Video16BitBuffered :
public GraphicsDisplay
{
public:
Video16BitBuffered(int x, int y);
~Video16BitBuffered();
Logical
TestInstance() const;
void
ShowInstance(char *indent);
void
DrawPoint(
int color,
int bitmask,
Enumeration operation,
int x, int y
);
void
DrawLine(
int color,
int bitmask,
Enumeration operation,
int x1, int y1,
int x2, int y2,
Logical include_last_pixel
);
void
DrawFilledRectangle(
int color,
int bitmask,
Enumeration operation,
int x1, int y1,
int x2, int y2
);
void
DrawText(
int color,
int bitmask,
Enumeration operation,
Logical opaque,
int rotation,
Enumeration fontNumber,
Logical vertical,
GraphicsDisplay::Justification justification,
Rectangle2D *clippingRectanglePtr,
char *stringPointer
);
void
DrawBitMap(
int color,
int bitmask,
Enumeration operation,
int rotation,
int x, int y,
BitMap *bitmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
DrawBitMapOpaque(
int color,
int background,
int bitmask,
Enumeration operation,
int rotation,
int x, int y,
BitMap *bitmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
DrawPixelMap8(
int *translation_table,
int bitmask,
Enumeration operation,
Logical opaque,
int rotation,
int x, int y,
PixelMap8 *pixelmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
DrawPixelMap8SingleColor(
int color,
int bitmask,
Enumeration operation,
int rotation,
int x, int y,
PixelMap8 *pixelmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
protected:
void
MarkChangedLines(int screenTop, int screenBottom);
void
buildDestPointer(
int x, int y,
Word **pixelPointer,
LWord **changed_bit_pointer,
LWord *changed_bit
);
Logical
valid;
int
width,
changedBitWidth,
height,
maximumX,
maximumY;
PixelMap16
pixelBuffer;
Byte
*changedLine;
LWord
*changedBit;
};
//########################################################################
//################################ SVGA16 ################################
//########################################################################
struct SVGA16Palette
{
Word
hardwarePort;
Logical
modified;
Palette8
paletteData;
Scalar
flashAccumulator,
flashRate;
enum PaletteID
{
maskStates=4
};
unsigned char
mask[maskStates];
int
previousMaskState;
};
class SVGA16 :
public Video16BitBuffered
{
friend class L4GraphicsPort; // for palette access
public:
enum PaletteID {
NativePalette=0,
SecondaryPalette,
AuxiliaryPalette1,
AuxiliaryPalette2,
PaletteCount
};
SVGA16(
int mode,
int width,
int height,
int page_size,
int mem_granularity,
int bytes_per_line,
int page_function_pointer,
int special_interface,
bool windowed,
int *secondaryIndex,
int *aux1Index,
int *aux2Index
);
~SVGA16();
Logical
TestInstance() const;
void
ShowInstance(char *indent);
Logical
Update(Logical forceall);
void
Refresh();
void
UpdatePalette();
void
FadeToPalettes(Scalar fade_time);
void
FadeToWhite(Scalar fade_time);
void
FlashPalette(
int palette_number,
Scalar rate,
unsigned char *stateList
);
void
UnflashPalette(int palette_number);
void
FunkyVideo(Logical on_off);
protected:
void BuildWindows(unsigned int width, unsigned int height, bool windowed, int *secondaryIndex, int *aux1Gauge, int *aux2Gauge);
void
ResetUpdatePosition();
int
pageSize,
pageDelta,
widthInBytes,
pageFcnPtr,
specialInterface;
int
currentPageNumber,
nextPageNumber,
lineNumber;
long
destOffset;
Byte
*changedLinePointer;
Word
*sourcePointer;
LWord
*changedBitPointer;
enum PaletteFadeState
{
staticPalette,
fadeToWhite,
whitePalette,
fadeToColor
};
PaletteFadeState
paletteFadeState;
Scalar
fadeAlpha,
fadeUnitsPerSecond;
Time
previousFadeTime;
// This array is closely linked to the PaletteID enumeration, above
SVGA16Palette
palette[PaletteCount];
// GLASS dirty-skip (2026-08-10): bumped by the palette writers
// (L4GraphicsPort::BuildSecondaryColor on a REAL entry change; the full
// palette rebuilds unconditionally) so the glass repaint token can see
// palette-only animation -- the ColorMapper family (armor rosette tints,
// damage flash) changes COLORS with zero pixel writes, which the pixel
// checksum alone can never catch.
unsigned long
paletteGeneration;
private:
int NUMGAUGEWINDOWS;
HWND *gaugeWindows;
LPDIRECT3DDEVICE9 *mDevice;
D3DPRESENT_PARAMETERS *mPresentParams;
LPDIRECT3DVERTEXBUFFER9 *mVertBuffers;
LPDIRECT3DTEXTURE9 *mSurfaces;
DWORD *mLockFlags;
RECT *mSurfaceRects;
int mDisplayToUpdate;
LPDIRECT3DTEXTURE9 mDevSurfaceTex[10]; // DEV-COMPOSITE: one texture per gauge surface
// (sec/radar + the 5 MFD bit-planes + the Eng1-3
// preset-page planes, Gitea #9), on the MAIN device.
public:
// DEV-COMPOSITE: extract ONE gauge surface from the shared pixelBuffer into
// mDevSurfaceTex[slot] on the given (main) device + draw it as a quad at (dstX,dstY,
// dstW,dstH). monoTint < 0 -> palette-expand (sec/radar, case-0 LUT); monoTint >= 0
// -> mono bit-plane expand ((word & mask) ? tint : 0) for an MFD plane. See
// BTDrawGaugeSurfaces (L4VB16.cpp).
// rotateCCW: 0 = as-is; 1/3 = display the texture turned 90 degrees (the
// pod's portrait-mounted secondary CRT; the dev panel unrotates it).
void DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
int monoTint, float dstX, float dstY, float dstW, float dstH,
int rotateCCW = 0);
// GLASS (per-display windows, L4GLASSWIN): CPU-expand ONE gauge surface (a
// bit-plane of the shared pixelBuffer) into a 32-bit BGRA image for GDI
// StretchDIBits -- no D3D. Same pixel rule as DrawDevSurface: monoTint < 0 ->
// palette-expand via palette[paletteID]; else fill (word & mask) ? monoTint : 0
// (monoTint is a 0x00RRGGBB value here, NOT R5G6B5). rotateQuadrant: 0 = native
// (outW=bufW, outH=bufH); 1 = 90 CCW / 3 = 90 CW -> transposed (outW=bufH,
// outH=bufW), for the portrait secondary CRT. `dst` must hold >= bufW*bufH
// dwords; the image is written TOP-DOWN. *outW/*outH receive the produced size.
void ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotateQuadrant,
unsigned long *dst, int *outW, int *outH);
// GLASS dirty-skip (L4GLASSWIN, 2026-08-09): FNV-1a checksum of the shared
// gauge pixelBuffer, masked to the bits a given window can show. Lets the
// glass repaint pump re-blit ONLY the windows whose plane actually changed
// (idle MFDs / static panels skip; the sweeping radar keeps updating).
unsigned long PlaneChecksum(int mask) const;
// GLASS dirty-skip: the palette write generation (see paletteGeneration).
// Folded into the repaint token of palette-expanding windows so ColorMapper
// palette animation (armor tints / damage flash) repaints without pixel churn.
unsigned long PaletteGeneration() const { return paletteGeneration; }
};
//########################################################################
//########################## L4GraphicsPort ##############################
//########################################################################
//
// X/Y values passed to these routines are assumed to be in
// the GRAPHICS COORDINATE SYSTEM, with (0,0) at the bottom left.
//
class L4GraphicsPort :
public GraphicsPort
{
public:
enum ChannelEnableID {
RedChannel,
GreenChannel,
BlueChannel,
AllChannels,
DirectColor,
BlankColor,
RedChannelTransparentZero=0x80,
GreenChannelTransparentZero,
BlueChannelTransparentZero,
AllChannelsTransparentZero
};
L4GraphicsPort(
Video16BitBuffered *graphics_display,
const char *name,
int rotation,
int bit_mask,
SVGA16::PaletteID palette_ID,
L4GraphicsPort::ChannelEnableID channel_enable
);
~L4GraphicsPort();
Logical
TestInstance() const;
void
ShowInstance(char *indent);
void
SetSecondaryPalette(
Palette8 *source_palette
);
void
SetAuxiliaryPalette();
void
SetColor(
PaletteTriplet *source_color,
int color_index
);
void
DrawPoint(
int color,
Enumeration operation,
int x, int y
);
void
DrawLine(
int color,
Enumeration operation,
int x1, int y1,
int x2, int y2,
Logical include_last_pixel
);
void
DrawFilledRectangle(
int color,
Enumeration operation,
int x1, int y1,
int x2, int y2
);
void
DrawText(
int color,
Enumeration operation,
Logical opaque,
int rotation,
Enumeration fontNumber,
Logical vertical,
GraphicsDisplay::Justification justification,
Rectangle2D *clippingRectanglePtr,
char *stringPointer
);
void
DrawBitMap(
int color,
Enumeration operation,
int rotation,
int x, int y,
BitMap *bitmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
DrawBitMapOpaque(
int color,
int background,
Enumeration operation,
int rotation,
int x, int y,
BitMap *bitmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
DrawPixelMap8(
Enumeration operation,
Logical opaque,
int rotation,
int x, int y,
PixelMap8 *pixelmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
DrawPixelMap8SingleColor(
int color,
Enumeration operation,
int rotation,
int x, int y,
PixelMap8 *pixelmap,
int sx1, int sy1, int sx2, int sy2 // these are SOURCE coordinates
);
void
SetPaletteID(SVGA16::PaletteID palette_ID)
{
Check(this);
paletteID = palette_ID;
}
void
SetEnableID(L4GraphicsPort::ChannelEnableID channel_enable)
{
Check(this);
channelEnable = channel_enable;
}
int
GetBitMask()
{
Check(this);
return bitMask;
}
// DEV-COMPOSITE (Gitea #9): the dock honors the live reconfigure state --
// a plane whose channel is currently BlankColor contributes nothing on the
// pod's shared-word palette LUT, so the dev panel skips drawing it.
L4GraphicsPort::ChannelEnableID
GetEnableID()
{
Check(this);
return channelEnable;
}
SVGA16::PaletteID
paletteID;
protected:
void
convertPortPair(
int *xp,
int *yp,
int x,
int y
);
void
BuildDirectTranslation(
Palette8 *source_palette
);
void
BuildSecondaryPalette(
Palette8 *source_palette
);
void
BuildAuxiliaryPalette();
void
BuildSecondaryTranslation();
void
BuildAuxiliaryTranslation();
void
BlankPalette();
void
BuildSecondaryColor(
PaletteTriplet *source_triplet,
int color_number
);
//------------------------------------------------------------
// Protected data
//------------------------------------------------------------
int
translationTable[256];
unsigned char
myColor[256];
int
baseRotation,
bitMask,
numberOfBits,
maximumX,
maximumY;
L4GraphicsPort::ChannelEnableID
channelEnable;
};