gauges: all 6 cockpit MFD surfaces in a SEPARATE dev window (Milestone C)

Generalizes the single-'sec'-inset (Milestone B) into a full 6-surface
compositor presented in its own top-level window -- the default under
BT_DEV_GAUGES (BT_DEV_GAUGES_DOCK=1 docks it into the main window instead).
A 960x384 window tiles all six pod instrument screens: Heat (COOLANT/BALANCE/
RES + condenser gauges), Comm (KILLS/DEATHS/SELECT TARGET), Mfd1/2/3 (DISPLAY/
PROGRAM/NEAREST frames), and the color radar (SCALE grid + dials + ARMOR
DAMAGE). Main 800x600 3D view is un-occluded; default DEV un-regressed
(TARGET DESTROYED after 8 hits, 0 crashes).

Design (mapped by the mfd-multisurface-map workflow):
- All 6 surfaces are bit-plane MASKS over the ONE shared SVGA16/pixelBuffer
  (sec=palette low byte; Heat=0x4000 UL, Mfd2=0x0400 UC, Comm=0x8000 UR,
  Mfd1=0x0100 LL, Mfd3=0x1000 LR), so the compositor reaches the SVGA16 once
  and extracts each by mask. No port-name reconcile needed on the dev path --
  fetch the BT names directly; the RP aux* names only matter to the pod's own
  SVGA16::Update demux (a deferred pod-only fallback).
- SVGA16::DrawDevSurface: two kernels -- palette-LUT (sec) + mono bit-plane->
  tint ((word&mask)?tint:0). BTDrawGaugeSurfaces iterates a 6-entry table.
- Separate window = one CreateAdditionalSwapChain on the existing device (no
  2nd D3D device). BTGaugeWindowRenderAndPresent (after the main EndScene):
  SetRenderTarget -> SetDepthStencilSurface(NULL) -> Clear -> BeginScene ->
  6 tiles -> EndScene -> restore -> swap->Present.
- KEY BUG fixed: the main 800x600 depth surface stays bound when rendering to
  the 960x384 gauge backbuffer; a bound depth smaller than the RT is invalid
  -> all draws silently fail (clear color, no geometry). Unbind depth (Z is
  off), restore after.

Pod SVGA16::Update/BuildWindows path byte-unchanged; all gated BT_DEV_GAUGES.
Content is still the authored cockpit frames + base-table gauges -- the live
MFD widgets need the BTL4MethodDescription reconstruction (this window is now
the live viewer for that work). Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-06 11:43:31 -05:00
co-authored by Claude Opus 4.8
parent a256813395
commit b526e92cad
5 changed files with 333 additions and 49 deletions
+248 -36
View File
@@ -109,6 +109,15 @@ static bool DevGaugeComposite()
return v != 0;
}
// DEV-COMPOSITE: BT_DEV_GAUGES_DOCK=1 docks the 6-surface panel INTO the main 800x600
// window (occludes the 3D view); default (unset) presents them in a SEPARATE window.
static bool DevGaugeDocked()
{
static int v = -1;
if (v < 0) v = (getenv("BT_DEV_GAUGES_DOCK") != NULL) ? 1 : 0;
return v != 0;
}
//===========================================================================//
// DEV-COMPOSITE (option B) -- draw the woken gauge renderer's SECONDARY/radar
// surface as an inset in the MAIN window. The gauge widgets have already
@@ -117,50 +126,70 @@ static bool DevGaugeComposite()
// a quad. Called by the main renderer as the LAST draw before EndScene, so no
// render-state save/restore is needed (the next frame resets device state).
//===========================================================================//
void SVGA16::DrawDevInset(LPDIRECT3DDEVICE9 device, int secMask, int paletteID)
void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
int monoTint, float dstX, float dstY, float dstW, float dstH)
{
int w = pixelBuffer.Data.Size.x; // 640
int h = pixelBuffer.Data.Size.y; // 480
if (device == NULL || w <= 0 || h <= 0) return;
if (device == NULL || slot < 0 || slot >= 8 || w <= 0 || h <= 0) return;
// Lazily create the inset texture on the MAIN device (MANAGED -> lockable +
// Lazily create this surface's texture on the MAIN device (MANAGED -> lockable +
// survives device reset).
if (mDevInsetTex == NULL)
device->CreateTexture(w, h, 1, 0, D3DFMT_R5G6B5, D3DPOOL_MANAGED, &mDevInsetTex, NULL);
if (mDevInsetTex == NULL) return;
if (mDevSurfaceTex[slot] == NULL)
device->CreateTexture(w, h, 1, 0, D3DFMT_R5G6B5, D3DPOOL_MANAGED, &mDevSurfaceTex[slot], NULL);
LPDIRECT3DTEXTURE9 tex = mDevSurfaceTex[slot];
if (tex == NULL) return;
// Palette-expand the secondary plane into the texture (== SVGA16::Update case 0).
SVGA16Palette *pal = &palette[paletteID];
// Extract this surface's bit-plane from the ONE shared pixelBuffer into the texture.
D3DLOCKED_RECT rect;
if (SUCCEEDED(mDevInsetTex->LockRect(0, &rect, NULL, 0)))
if (SUCCEEDED(tex->LockRect(0, &rect, NULL, 0)))
{
Word *source = pixelBuffer.Data.MapPointer;
Word *dest = (Word*)rect.pBits;
int postRowIncrement = (rect.Pitch / 2) - w;
for (int y = 0; y < h; y++)
if (monoTint < 0)
{
for (int x = 0; x < w; x++)
// PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0).
SVGA16Palette *pal = &palette[paletteID];
for (int y = 0; y < h; y++)
{
PaletteTriplet *pe = &(pal->paletteData.Color[*source & secMask]);
*dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
dest++; source++;
for (int x = 0; x < w; x++)
{
PaletteTriplet *pe = &(pal->paletteData.Color[*source & mask]);
*dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
dest++; source++;
}
dest += postRowIncrement;
}
dest += postRowIncrement;
}
mDevInsetTex->UnlockRect(0);
else
{
// MONO MFD surface -- single-bit-plane expand to a tint (the reduced core of
// SVGA16::Update cases 1/2; MFD masks are single bits, so no DWORD SIMD/pack).
Word tint = (Word) monoTint;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
*dest = (*source & mask) ? tint : 0;
dest++; source++;
}
dest += postRowIncrement;
}
}
tex->UnlockRect(0);
}
// Draw the inset: a 320x240 panel in the bottom-left of the (800x600) backbuffer.
const float iw = 320.0f, ih = 240.0f, ix = 8.0f, iy = 600.0f - ih - 8.0f;
// Draw the surface quad at (dstX,dstY,dstW,dstH) in the current render target.
struct InsetVert { float x, y, z, rhw, u, v; };
InsetVert quad[4] =
{
{ ix, iy, 0.0f, 1.0f, 0.0f, 0.0f },
{ ix + iw, iy, 0.0f, 1.0f, 1.0f, 0.0f },
{ ix + iw, iy + ih, 0.0f, 1.0f, 1.0f, 1.0f },
{ ix, iy + ih, 0.0f, 1.0f, 0.0f, 1.0f },
{ dstX, dstY, 0.0f, 1.0f, 0.0f, 0.0f },
{ dstX + dstW, dstY, 0.0f, 1.0f, 1.0f, 0.0f },
{ dstX + dstW, dstY + dstH, 0.0f, 1.0f, 1.0f, 1.0f },
{ dstX, dstY + dstH, 0.0f, 1.0f, 0.0f, 1.0f },
};
device->SetTexture(0, mDevInsetTex);
device->SetTexture(0, tex);
device->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
@@ -173,23 +202,205 @@ void SVGA16::DrawDevInset(LPDIRECT3DDEVICE9 device, int secMask, int paletteID)
device->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, quad, sizeof(InsetVert));
}
//
// The pod's SIX instrument surfaces, all bit-plane views of the ONE shared 640x480
// gauge buffer (masks/positions from content/GAUGE/L4GAUGE.CFG MechInit @4395):
// sec = radar/secondary (palette) Heat = UL Mfd2 = UC Comm = UR
// Mfd1 = LL Mfd3 = LR
// monoTint < 0 -> palette-expand; else the mono tint for that MFD plane. The MFD
// content today is mostly the authored cockpit frame art (btquad/btcomm/btheat) --
// the live widgets are BT-specific gauge classes not yet reconstructed (parse-skipped).
//
struct BTGaugeSurfaceDesc
{
const char *portName;
int monoTint; // -1 = palette (sec); else R5G6B5 tint
float cellX, cellY; // normalized cell origin within the panel (y down)
float cellW, cellH; // normalized cell size
};
static const BTGaugeSurfaceDesc kBTGaugeSurfaces[6] =
{
// top row: Heat (UL) | Mfd2 (UC) | Comm (UR)
{ "Heat", 0xFFFF, 0.0f / 3, 0.0f, 1.0f / 3, 0.5f },
{ "Mfd2", 0xFFFF, 1.0f / 3, 0.0f, 1.0f / 3, 0.5f },
{ "Comm", 0xFFFF, 2.0f / 3, 0.0f, 1.0f / 3, 0.5f },
// bottom row: Mfd1 (LL) | sec/radar (center) | Mfd3 (LR)
{ "Mfd1", 0xFFFF, 0.0f / 3, 0.5f, 1.0f / 3, 0.5f },
{ "sec", -1, 1.0f / 3, 0.5f, 1.0f / 3, 0.5f },
{ "Mfd3", 0xFFFF, 2.0f / 3, 0.5f, 1.0f / 3, 0.5f },
};
//
// Composite ALL SIX gauge surfaces into the CURRENT render target, tiled within the
// panel rect (px,py,pw,ph). Reaches the ONE shared SVGA16 via any resolvable port and
// extracts each surface by its own bit-plane mask. Ports that don't resolve are skipped.
//
void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw, float ph)
{
if (device == NULL) return;
GaugeRenderer *gr = application ? application->GetGaugeRenderer() : NULL;
if (gr == NULL) return;
// Reach the shared display once (any present port shares the same SVGA16/pixelBuffer).
SVGA16 *svga = NULL;
for (int i = 0; i < 6 && svga == NULL; i++)
{
L4GraphicsPort *p = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(kBTGaugeSurfaces[i].portName));
if (p != NULL) svga = (SVGA16*) p->graphicsDisplay;
}
if (svga == NULL) return;
for (int i = 0; i < 6; i++)
{
const BTGaugeSurfaceDesc &d = kBTGaugeSurfaces[i];
L4GraphicsPort *port = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(d.portName));
if (port == NULL) continue; // this surface's port isn't configured -> skip
svga->DrawDevSurface(
device, i, port->GetBitMask(), port->paletteID, d.monoTint,
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph);
}
}
//
// Free entry point the MAIN renderer calls (L4VIDEO DPLRenderer::ExecuteImplementation,
// just before EndScene). No-op unless BT_DEV_GAUGES is set + the gauge renderer + its
// 'sec' port exist. Reaches the gauge renderer's SVGA16 and blits the secondary inset.
// just before EndScene). No-op unless BT_DEV_GAUGES is set. Docks the 6-surface panel
// in the bottom-left of the 800x600 main window (a second dedicated window is the goal;
// see BTGaugeWindow* below).
//
void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
{
if (!DevGaugeComposite() || device == NULL) return;
GaugeRenderer *gr = application ? application->GetGaugeRenderer() : NULL;
if (gr == NULL) return;
// The secondary/radar surface ('sec') is the one port that resolves without
// the RP<->BT MFD port-name reconciliation (Step 2), so it is the beachhead.
L4GraphicsPort *secPort = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort("sec"));
if (secPort == NULL) return;
SVGA16 *svga = (SVGA16*)secPort->graphicsDisplay;
if (svga == NULL) return;
svga->DrawDevInset(device, secPort->GetBitMask(), secPort->paletteID);
if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return;
// DOCKED mode only: draw into the main backbuffer (this runs before EndScene).
BTDrawGaugeSurfaces(device, 2.0f, 300.0f, 540.0f, 296.0f);
}
//===========================================================================//
// DEV-COMPOSITE -- the SEPARATE cockpit-MFD window (the default under BT_DEV_GAUGES).
// A second top-level window fed by an ADDITIONAL SWAP CHAIN on the existing main
// device (no second D3D device needed -- textures/vertex data are shared). The pod's
// own per-surface fullscreen-device path (SVGA16::BuildWindows) is untouched; this is
// the dev-box analog of the pod's separate instrument monitors, collapsed into one
// tiled window. BTGaugeWindowRenderAndPresent is called AFTER the main EndScene.
//===========================================================================//
static HWND s_gaugeHwnd = NULL;
static IDirect3DSwapChain9 *s_gaugeSwap = NULL;
static const int s_gaugeW = 960;
static const int s_gaugeH = 384;
static LRESULT CALLBACK BTGaugeWndProc(HWND h, UINT m, WPARAM w, LPARAM l)
{
if (m == WM_CLOSE) { ShowWindow(h, SW_HIDE); return 0; } // hide, never quit the app
return DefWindowProc(h, m, w, l);
}
// Lazily create the window + its additional swap chain (on the MAIN device).
static bool BTGaugeWindowEnsure(LPDIRECT3DDEVICE9 device)
{
if (s_gaugeSwap != NULL) return true;
if (device == NULL) return false;
HINSTANCE hInst = GetModuleHandle(NULL);
static bool s_classReg = false;
if (!s_classReg)
{
WNDCLASSA wc;
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = BTGaugeWndProc;
wc.hInstance = hInst;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
wc.lpszClassName = "BTGaugeWnd";
RegisterClassA(&wc);
s_classReg = true;
}
if (s_gaugeHwnd == NULL)
{
RECT r = { 0, 0, s_gaugeW, s_gaugeH };
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
s_gaugeHwnd = CreateWindowExA(
0, "BTGaugeWnd", "BattleTech - Cockpit MFDs", WS_OVERLAPPEDWINDOW,
40, 40, r.right - r.left, r.bottom - r.top, NULL, NULL, hInst, NULL);
if (s_gaugeHwnd == NULL) return false;
ShowWindow(s_gaugeHwnd, SW_SHOWNOACTIVATE);
}
// Match the main swap chain's backbuffer format so the additional chain is compatible.
D3DFORMAT bbFormat = D3DFMT_X8R8G8B8;
IDirect3DSwapChain9 *mainSwap = NULL;
if (SUCCEEDED(device->GetSwapChain(0, &mainSwap)) && mainSwap != NULL)
{
D3DPRESENT_PARAMETERS mpp;
if (SUCCEEDED(mainSwap->GetPresentParameters(&mpp)))
bbFormat = mpp.BackBufferFormat;
mainSwap->Release();
}
D3DPRESENT_PARAMETERS pp;
memset(&pp, 0, sizeof(pp));
pp.Windowed = TRUE;
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
pp.hDeviceWindow = s_gaugeHwnd;
pp.BackBufferFormat = bbFormat;
pp.BackBufferWidth = s_gaugeW;
pp.BackBufferHeight = s_gaugeH;
pp.BackBufferCount = 1;
if (FAILED(device->CreateAdditionalSwapChain(&pp, &s_gaugeSwap)))
{
s_gaugeSwap = NULL;
return false;
}
return true;
}
// Render the 6 surfaces into the gauge window's swap chain and present it. Called AFTER
// the main EndScene, BEFORE the main Present (so it can BeginScene/EndScene on its own).
void BTGaugeWindowRenderAndPresent(LPDIRECT3DDEVICE9 device)
{
if (!DevGaugeComposite() || DevGaugeDocked() || device == NULL) return;
if (!BTGaugeWindowEnsure(device)) return;
IDirect3DSurface9 *gaugeBB = NULL;
if (FAILED(s_gaugeSwap->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &gaugeBB)) || gaugeBB == NULL)
{
// device likely lost -> drop the chain so it recreates next frame
s_gaugeSwap->Release();
s_gaugeSwap = NULL;
return;
}
IDirect3DSurface9 *mainRT = NULL;
device->GetRenderTarget(0, &mainRT);
IDirect3DSurface9 *mainDS = NULL;
device->GetDepthStencilSurface(&mainDS); // may be NULL
device->SetRenderTarget(0, gaugeBB);
// Unbind the main depth-stencil: it is 800x600 but the gauge backbuffer is 960x384
// (wider) -- a bound depth surface smaller than the render target is INVALID and makes
// every draw silently fail. We don't need depth here (Z is disabled per surface quad).
device->SetDepthStencilSurface(NULL);
device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(12, 12, 16), 1.0f, 0);
if (SUCCEEDED(device->BeginScene()))
{
BTDrawGaugeSurfaces(device, 0.0f, 0.0f, (float) s_gaugeW, (float) s_gaugeH);
device->EndScene();
}
if (mainRT != NULL) // restore the main render target + depth for the next frame
{
device->SetRenderTarget(0, mainRT);
mainRT->Release();
}
device->SetDepthStencilSurface(mainDS); // NULL is valid (restores "no depth")
if (mainDS != NULL)
mainDS->Release();
gaugeBB->Release();
if (s_gaugeSwap->Present(NULL, NULL, s_gaugeHwnd, NULL, 0) == D3DERR_DEVICELOST)
{
s_gaugeSwap->Release();
s_gaugeSwap = NULL;
}
}
void SVGA16::BuildWindows(unsigned int width, unsigned int height, bool windowed, int *secondaryIndex, int *aux1Index, int *aux2Index)
@@ -3927,7 +4138,8 @@ SVGA16::SVGA16(
):Video16BitBuffered(init_width, init_height)
{
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
mDevInsetTex = NULL; // DEV-COMPOSITE (option B): lazily created on first inset draw
for (int _i = 0; _i < 8; _i++) // DEV-COMPOSITE: lazily created on first surface draw
mDevSurfaceTex[_i] = NULL;
//STUBBED: VIDEO RB 1/15/07
# if defined(DEBUG)
Tell("SVGA16::SVGA16()\n");
+9 -5
View File
@@ -292,12 +292,16 @@ private:
int mDisplayToUpdate;
LPDIRECT3DTEXTURE9 mDevInsetTex; // DEV-COMPOSITE (option B): the secondary/radar
// plane uploaded to a texture on the MAIN device.
LPDIRECT3DTEXTURE9 mDevSurfaceTex[8]; // DEV-COMPOSITE: one texture per gauge surface
// (sec/radar + the 5 MFD bit-planes), on the MAIN device.
public:
// DEV-COMPOSITE (option B): palette-expand the secondary plane into mDevInsetTex on
// the given (main) device + draw it as an inset quad. See BTDrawGaugeInset (L4VB16.cpp).
void DrawDevInset(LPDIRECT3DDEVICE9 device, int secMask, int paletteID);
// 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).
void DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
int monoTint, float dstX, float dstY, float dstW, float dstH);
};
//########################################################################
+8 -2
View File
@@ -6963,13 +6963,19 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
if (mCamShipHUD)
mCamShipHUD->Render(0, &viewTransform);
// DEV-COMPOSITE (option B): blit the woken gauge renderer's secondary/radar surface
// as an inset in this window (LAST draw before EndScene; no-op unless BT_DEV_GAUGES).
// DEV-COMPOSITE: in DOCKED mode (BT_DEV_GAUGES_DOCK) blit the 6-surface gauge panel
// into this window as the LAST draw before EndScene (no-op otherwise / off pod).
extern void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device);
BTDrawGaugeInset(mDevice);
hr = mDevice->EndScene();
// DEV-COMPOSITE: default mode -- render the 6 cockpit surfaces into a SEPARATE window
// (its own additional swap chain on this device) + present it, between the main
// EndScene and the main Present. No-op unless BT_DEV_GAUGES (and not docked mode).
extern void BTGaugeWindowRenderAndPresent(LPDIRECT3DDEVICE9 device);
BTGaugeWindowRenderAndPresent(mDevice);
// DIAG (turn-hitch hunt): draw CPU is _rt0..here; Present blocks on the GPU.
LARGE_INTEGER _rt1; QueryPerformanceCounter(&_rt1);
hr = mDevice->Present(NULL, NULL, NULL, NULL);