Files
BT411/game/reconstructed/dpl2d.cpp
arcattackandClaude Fable 5 fa88f74c68 HUD aspect correction: square reticle units at any window shape (task #44)
User: "it doesn't scale with our screen resolution?" -- correct. The
HUD draws into the fixed 800x600 backbuffer and the present stretches
it into the client area: on non-4:3 windows everything distorted
(circles -> ellipses, the bottom tape widened vs the ladder, positions
drifted outward).

- BTGetPresentAspect (L4VIDEO, from gWindowAspect; 4:3 fallback).
- dpl2d x-unit = (bbW/2)/presentAspect (== bbH/2 at 4:3 -- unchanged
  there), circles pre-squished in bb space so the stretch restores
  round.
- The reticle<->NDC conversions (BTGetAimRay / BTProjectToReticle /
  BTProjectHotBox / BTTwistToReticleX) use the PRESENT aspect; the
  mouse map (BTClientToReticle) is now pure client-relative.
- Placement itself is authentic and stays put: the binary clusters the
  instruments around the boresight (ladder x=0.35 half-heights, spans
  +-0.25) -- NOT at the display edges.
- Crash en route: the BTGetPresentAspect extern declared INSIDE dpl2d's
  anonymous namespace = a different, unresolved symbol -> /FORCE
  garbage call (landed in MissileLauncher::DefaultData). Moved to
  global scope; gotcha noted in the comment.

Verified 4:3 regression: aimed lock + zone hits + no crash; hud-geom
MapX/MapY unchanged at 4:3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:14:36 -05:00

575 lines
19 KiB
C++

//===========================================================================//
// File: dpl2d.cpp //
// Project: BattleTech port (WinTesla / btl4) //
//---------------------------------------------------------------------------//
// libDPL 2D vector display-list layer, re-hosted over Direct3D 9. //
// //
// OPCODE-FAITHFUL rework (task #35 / phase-02): every dpl2d_ recorder in the //
// binary self-identifies via its debug-name string (part_010.c 0x487f34.. //
// 0x4888c0) -- the full API + opcode map is recorded in //
// phases/phase-02-dpl2d-reticle.md. This TU implements that model: //
// //
// OpenDisplayList(mode) .. CloseDisplayList .. FlushDisplayList //
// AddOpenPolypoint(2)/AddClosePolypoint(3) -- a POINT set //
// AddOpenPolyline(4)/AddClosePolyline(5) -- a CLOSED loop //
// AddOpenLines(6)/AddCloseLines(7) -- SEPARATE SEGMENT PAIRS //
// AddPoint(8, x, y) -- vertex of the open prim //
// AddCircle(9, x, y, r, fill) //
// AddSetColor(0xF, r, g, b) //
// AddSetMatrix(0x10, 2x3) / AddConcatMatrix(0x11, 2x3) //
// AddPushState(0x12) / AddPopState(0x13) //
// AddSetLineWidth(0x15, w) //
// AddCallDisplayList(list) -- nested glyph reuse //
// AddFullScreenClipRegion -- clip reset (no-op here) //
// //
// Coordinates are normalised view space (the reticle builder's constants //
// are fractions like 0.04/0.1/0.35); dpl2d_ExecuteList maps them to the //
// current viewport and draws XYZRHW primitives with state save/restore. //
// //
// NOTE the OLD trio dpl2d_PushMatrix/MoveTo/PopMatrix was a misreading of //
// AddOpenPolypoint/AddPoint/AddClosePolypoint ("draw a point"); the aliases //
// below keep old callers compiling while mapping to the true semantics. //
//===========================================================================//
#include <bt.hpp> // Scalar + engine prelude
#pragma hdrstop
#if !defined(BTL4VID_HPP)
# include <btl4vid.hpp> // dpl2d_DISPLAY (opaque) + dpl2d_* prototypes
#endif
#if !defined(DPL2D_HPP)
# include <dpl2d.hpp>
#endif
#include <d3d9.h>
#include <vector>
#include <math.h>
// the presented (client) aspect -- L4VIDEO.cpp (gWindowAspect); GLOBAL scope
// on purpose (an extern inside the anonymous namespace = a different symbol
// -> /FORCE garbage call; task #44)
extern float BTGetPresentAspect();
namespace
{
struct Vertex2D
{
float x, y, z, rhw;
DWORD color;
};
const DWORD kVertex2DFVF = (D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
const int kCircleSegments = 32; // tessellation of AddCircle
const float kPi = 3.14159265358979323846f;
inline float ClampUnit(float v)
{
return (v < 0.0f) ? 0.0f : (v > 1.0f ? 1.0f : v);
}
struct Vec2 { float x, y; };
// 2x3 affine (row vectors): [x y 1] * | m00 m01 |
// | m10 m11 |
// | m20 m21 |
struct Mat2x3
{
float m[6]; // m00 m01 m10 m11 m20 m21
void Identity() { m[0]=1; m[1]=0; m[2]=0; m[3]=1; m[4]=0; m[5]=0; }
Vec2 Apply(float x, float y) const
{
Vec2 r;
r.x = x * m[0] + y * m[2] + m[4];
r.y = x * m[1] + y * m[3] + m[5];
return r;
}
static Mat2x3 Mul(const Mat2x3 &a, const Mat2x3 &b) // a then b
{
Mat2x3 r;
r.m[0] = a.m[0]*b.m[0] + a.m[1]*b.m[2];
r.m[1] = a.m[0]*b.m[1] + a.m[1]*b.m[3];
r.m[2] = a.m[2]*b.m[0] + a.m[3]*b.m[2];
r.m[3] = a.m[2]*b.m[1] + a.m[3]*b.m[3];
r.m[4] = a.m[4]*b.m[0] + a.m[5]*b.m[2] + b.m[4];
r.m[5] = a.m[4]*b.m[1] + a.m[5]*b.m[3] + b.m[5];
return r;
}
};
//
// One recorded command (the opcode stream, structurally).
//
struct Command
{
enum Kind
{
kPoints, // vertex run: a point per vertex
kLineStrip, // vertex run: SEGMENT PAIRS (LINELIST; task #44)
kPolyLoop, // vertex run: closed loop
kCircleFill,
kCircleOutline,
kSetColor,
kSetWidth,
kPushState,
kPopState,
kSetMatrix,
kConcatMatrix,
kCallList
} kind;
DWORD color; // kSetColor (prebaked ARGB)
float a, b, c, d; // circle x,y,r,fill / width in a
Mat2x3 mat; // kSetMatrix/kConcatMatrix
void *callee; // kCallList (Dpl2dList*)
std::vector<Vec2> verts; // vertex runs
};
}
struct Dpl2dList
{
bool recording;
bool compiled;
int mode;
std::vector<Command> commands;
// the currently-open vertex primitive (between AddOpenX .. AddCloseX)
int openKind; // -1 none, else Command::Kind
std::vector<Vec2> openVerts;
Dpl2dList() : recording(false), compiled(false), mode(0), openKind(-1) {}
};
static inline Dpl2dList *AsList(dpl2d_DISPLAY *handle)
{
return reinterpret_cast<Dpl2dList *>(handle);
}
static void FlushOpenPrimitive(Dpl2dList *self)
{
if (self->openKind < 0)
return;
Command cmd;
cmd.kind = (Command::Kind)self->openKind;
cmd.verts = self->openVerts;
self->commands.push_back(cmd);
self->openKind = -1;
self->openVerts.clear();
}
//===========================================================================//
// Recorder -- the dpl2d_ entry points (prototypes in btl4vid.hpp)
//===========================================================================//
dpl2d_DISPLAY *dpl2d_NewDisplayList()
{
return reinterpret_cast<dpl2d_DISPLAY *>(new Dpl2dList);
}
void dpl2d_Begin(dpl2d_DISPLAY *list, int mode) // OpenDisplayList
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
self->recording = true;
self->compiled = false;
self->mode = mode;
self->openKind = -1;
self->openVerts.clear();
self->commands.clear();
}
void dpl2d_End(dpl2d_DISPLAY *list) // CloseDisplayList
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
self->recording = false;
}
void dpl2d_Compile(dpl2d_DISPLAY *list) // FlushDisplayList
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
self->compiled = true;
}
void dpl2d_SetColor(dpl2d_DISPLAY *list, Scalar red, Scalar green, Scalar blue)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
Command cmd;
cmd.kind = Command::kSetColor;
cmd.color = D3DCOLOR_COLORVALUE(
ClampUnit((float)red), ClampUnit((float)green), ClampUnit((float)blue), 1.0f);
self->commands.push_back(cmd);
}
void dpl2d_SetLineWidth(dpl2d_DISPLAY *list, Scalar width)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
Command cmd;
cmd.kind = Command::kSetWidth;
cmd.a = (float)width;
self->commands.push_back(cmd);
}
void dpl2d_PushState(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
Command cmd; cmd.kind = Command::kPushState;
self->commands.push_back(cmd);
}
void dpl2d_PopState(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
Command cmd; cmd.kind = Command::kPopState;
self->commands.push_back(cmd);
}
void dpl2d_SetMatrix(dpl2d_DISPLAY *list, const Scalar *six)
{
Dpl2dList *self = AsList(list);
if (self == 0 || six == 0) return;
Command cmd; cmd.kind = Command::kSetMatrix;
for (int i = 0; i < 6; ++i) cmd.mat.m[i] = (float)six[i];
self->commands.push_back(cmd);
}
void dpl2d_ConcatMatrix(dpl2d_DISPLAY *list, const Scalar *six)
{
Dpl2dList *self = AsList(list);
if (self == 0 || six == 0) return;
Command cmd; cmd.kind = Command::kConcatMatrix;
for (int i = 0; i < 6; ++i) cmd.mat.m[i] = (float)six[i];
self->commands.push_back(cmd);
}
void dpl2d_CallList(dpl2d_DISPLAY *list, dpl2d_DISPLAY *callee)
{
Dpl2dList *self = AsList(list);
if (self == 0 || callee == 0) return;
Command cmd; cmd.kind = Command::kCallList;
cmd.callee = AsList(callee);
self->commands.push_back(cmd);
}
void dpl2d_OpenPolypoint(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
self->openKind = Command::kPoints;
}
void dpl2d_ClosePolypoint(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
}
void dpl2d_OpenLines(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
self->openKind = Command::kLineStrip;
}
void dpl2d_CloseLines(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
}
void dpl2d_OpenPolyline(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
self->openKind = Command::kPolyLoop;
}
void dpl2d_ClosePolyline(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
}
void dpl2d_AddPoint(dpl2d_DISPLAY *list, Scalar x, Scalar y)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
if (self->openKind < 0)
self->openKind = Command::kPoints; // tolerate a stray vertex
Vec2 v = { (float)x, (float)y };
self->openVerts.push_back(v);
}
void dpl2d_Circle(dpl2d_DISPLAY *list, Scalar x, Scalar y, Scalar radius, int fill)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
FlushOpenPrimitive(self);
Command cmd;
cmd.kind = fill ? Command::kCircleFill : Command::kCircleOutline;
cmd.a = (float)x; cmd.b = (float)y; cmd.c = (float)radius;
self->commands.push_back(cmd);
}
void dpl2d_FullScreenClip(dpl2d_DISPLAY * /*list*/)
{
// clip reset -- the executor always draws to the full viewport
}
//
// LEGACY ALIASES (the earlier AddWeapon transcription used these names for
// what the binary's opcodes reveal to be the point primitive).
//
void dpl2d_PushMatrix(dpl2d_DISPLAY *list) { dpl2d_OpenPolypoint(list); }
void dpl2d_MoveTo(dpl2d_DISPLAY *list, Scalar x, Scalar y) { dpl2d_AddPoint(list, x, y); }
void dpl2d_PopMatrix(dpl2d_DISPLAY *list) { dpl2d_ClosePolypoint(list); }
//===========================================================================//
// Executor -- rasterise a compiled list on the supplied device.
//===========================================================================//
namespace
{
struct ExecState
{
DWORD color;
float width;
Mat2x3 mat;
};
//
// COORDINATE MODEL (from the reticle's authored constants): centred origin,
// +x right, +y down, unit = half the PRESENTED height on both axes (keeps
// circles round ON SCREEN; the reticle content spans roughly +-0.5).
//
// ASPECT PRE-COMPENSATION (task #44): the list draws into the fixed
// backbuffer, which the present STRETCHES into the client area. For the
// on-screen result to be square at any window shape, the backbuffer x-unit
// is (bbW/2)/presentAspect -- equal to bbH/2 on a 4:3 window (unchanged),
// pre-squished on wider windows so the stretch restores square.
//
inline float XUnit(float vw, float vh)
{
// NB the extern lives at GLOBAL scope (below the namespace ends /
// above in the TU) -- an extern declared INSIDE an anonymous
// namespace mangles as `anonymous-namespace'::... = a DIFFERENT,
// unresolved symbol, which /FORCE turns into a garbage call target
// (crashed exactly so; the /FORCE gotcha).
float a = BTGetPresentAspect();
return (a > 0.1f) ? (vw * 0.5f) / a : (vh * 0.5f);
}
inline float MapX(float x, float ox, float vw, float vh)
{ return ox + vw * 0.5f + x * XUnit(vw, vh); }
inline float MapY(float y, float oy, float vh)
{ return oy + vh * 0.5f + y * (vh * 0.5f); }
void DrawVertexRun(IDirect3DDevice9 *device, const Command &cmd,
const ExecState &st, float ox, float oy, float vw, float vh)
{
if (cmd.verts.empty())
return;
static std::vector<Vertex2D> verts;
verts.clear();
for (size_t i = 0; i < cmd.verts.size(); ++i)
{
Vec2 p = st.mat.Apply(cmd.verts[i].x, cmd.verts[i].y);
Vertex2D v;
v.x = MapX(p.x, ox, vw, vh); v.y = MapY(p.y, oy, vh);
v.z = 0.0f; v.rhw = 1.0f; v.color = st.color;
verts.push_back(v);
}
if (cmd.kind == Command::kPoints)
{
// a point renders as a small filled quad (D3D9 point size is
// unreliable on the fixed function path); ~width pixels square.
float half = (st.width > 1.0f) ? st.width : 1.0f;
for (size_t i = 0; i < verts.size(); ++i)
{
Vertex2D q[4] = { verts[i], verts[i], verts[i], verts[i] };
q[0].x -= half; q[0].y -= half;
q[1].x += half; q[1].y -= half;
q[2].x -= half; q[2].y += half;
q[3].x += half; q[3].y += half;
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, q, sizeof(Vertex2D));
}
}
else if (cmd.kind == Command::kLineStrip)
{
// SEPARATE SEGMENTS (task #44): OpenLines/CloseLines points come in
// PAIRS -- every ctor use is segment pairs (the 4 cross arms, one
// pair per ladder tick, the compass stem, the lock ring's 4 ticks,
// each threat mark). Drawing them as a connected STRIP joined the
// ticks into zigzags, hung diagonals off the crosshair arms, and
// drew chords across the lock ring (user screenshot). LINELIST =
// one line per point pair, the binary's semantics.
if (verts.size() >= 2)
device->DrawPrimitiveUP(D3DPT_LINELIST,
(UINT)(verts.size() / 2), &verts[0], sizeof(Vertex2D));
}
else // kPolyLoop -- closed
{
if (verts.size() >= 2)
{
verts.push_back(verts[0]);
device->DrawPrimitiveUP(D3DPT_LINESTRIP,
(UINT)verts.size() - 1, &verts[0], sizeof(Vertex2D));
}
}
}
void ExecuteListInner(Dpl2dList *self, IDirect3DDevice9 *device,
ExecState &st, std::vector<ExecState> &stack,
float ox, float oy, float vw, float vh, int depth)
{
if (self == 0 || depth > 8)
return;
Vertex2D ring[kCircleSegments + 2];
for (size_t i = 0; i < self->commands.size(); ++i)
{
const Command &cmd = self->commands[i];
switch (cmd.kind)
{
case Command::kSetColor: st.color = cmd.color; break;
case Command::kSetWidth: st.width = cmd.a; break;
case Command::kPushState: stack.push_back(st); break;
case Command::kPopState:
if (!stack.empty()) { st = stack.back(); stack.pop_back(); }
break;
case Command::kSetMatrix: st.mat = cmd.mat; break;
case Command::kConcatMatrix:
st.mat = Mat2x3::Mul(cmd.mat, st.mat);
break;
case Command::kCallList:
// INLINE include: a called list's state changes (matrices,
// colours) PERSIST into the caller -- the reticle's range
// caret is a called list holding just a translate that shifts
// the master's subsequent geometry.
ExecuteListInner((Dpl2dList *)cmd.callee, device,
st, stack, ox, oy, vw, vh, depth + 1);
break;
case Command::kPoints:
case Command::kLineStrip:
case Command::kPolyLoop:
DrawVertexRun(device, cmd, st, ox, oy, vw, vh);
break;
case Command::kCircleFill:
case Command::kCircleOutline:
{
Vec2 p = st.mat.Apply(cmd.a, cmd.b);
const float cx = MapX(p.x, ox, vw, vh);
const float cy = MapY(p.y, oy, vh);
const float rx = cmd.c * XUnit(vw, vh); // pre-squished in bb space,
const float ry = cmd.c * (vh * 0.5f); // round after the present stretch
if (cmd.kind == Command::kCircleFill)
{
ring[0].x = cx; ring[0].y = cy; ring[0].z = 0.0f;
ring[0].rhw = 1.0f; ring[0].color = st.color;
for (int s = 0; s <= kCircleSegments; ++s)
{
float a = (2.0f * kPi * s) / kCircleSegments;
ring[s + 1].x = cx + cosf(a) * rx;
ring[s + 1].y = cy + sinf(a) * ry;
ring[s + 1].z = 0.0f; ring[s + 1].rhw = 1.0f;
ring[s + 1].color = st.color;
}
device->DrawPrimitiveUP(D3DPT_TRIANGLEFAN,
kCircleSegments, ring, sizeof(Vertex2D));
}
else
{
for (int s = 0; s <= kCircleSegments; ++s)
{
float a = (2.0f * kPi * s) / kCircleSegments;
ring[s].x = cx + cosf(a) * rx;
ring[s].y = cy + sinf(a) * ry;
ring[s].z = 0.0f; ring[s].rhw = 1.0f;
ring[s].color = st.color;
}
device->DrawPrimitiveUP(D3DPT_LINESTRIP,
kCircleSegments, ring, sizeof(Vertex2D));
}
break;
}
}
}
}
}
void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device)
{
Dpl2dList *self = AsList(list);
if (self == 0 || device == 0 || self->commands.empty())
return;
D3DVIEWPORT9 vp;
if (FAILED(device->GetViewport(&vp)))
return;
// BT_HUD_LOG: one-shot geometry ground truth -- the viewport the HUD maps
// into + where key reticle x/y land (diagnosing scale/placement reports).
{
static int s_logged = -1;
if (s_logged < 0) s_logged = getenv("BT_HUD_LOG") ? 1 : 0;
if (s_logged == 1)
{
s_logged = 2;
DEBUG_STREAM << "[hud-geom] viewport x=" << vp.X << " y=" << vp.Y
<< " w=" << vp.Width << " h=" << vp.Height
<< " MapX(0.35)=" << (vp.X + vp.Width*0.5f + 0.35f*(vp.Height*0.5f))
<< " MapX(-0.25)=" << (vp.X + vp.Width*0.5f - 0.25f*(vp.Height*0.5f))
<< " MapY(0.25)=" << (vp.Y + vp.Height*0.5f + 0.25f*(vp.Height*0.5f))
<< " MapY(0.35)=" << (vp.Y + vp.Height*0.5f + 0.35f*(vp.Height*0.5f))
<< "\n" << std::flush;
}
}
DWORD oldLighting, oldZ, oldCull, oldAlpha, oldSrc, oldDst, oldFog;
device->GetRenderState(D3DRS_LIGHTING, &oldLighting);
device->GetRenderState(D3DRS_ZENABLE, &oldZ);
device->GetRenderState(D3DRS_CULLMODE, &oldCull);
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &oldAlpha);
device->GetRenderState(D3DRS_SRCBLEND, &oldSrc);
device->GetRenderState(D3DRS_DESTBLEND, &oldDst);
device->GetRenderState(D3DRS_FOGENABLE, &oldFog);
IDirect3DBaseTexture9 *oldTex = 0;
device->GetTexture(0, &oldTex);
device->SetTexture(0, 0);
device->SetRenderState(D3DRS_LIGHTING, FALSE);
device->SetRenderState(D3DRS_ZENABLE, FALSE);
device->SetRenderState(D3DRS_FOGENABLE, FALSE);
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
device->SetFVF(kVertex2DFVF);
ExecState st;
st.color = 0xFFFFFFFF;
st.width = 1.0f;
st.mat.Identity();
std::vector<ExecState> stack;
ExecuteListInner(self, device,
st, stack, (float)vp.X, (float)vp.Y, (float)vp.Width, (float)vp.Height, 0);
device->SetTexture(0, oldTex);
if (oldTex) oldTex->Release();
device->SetRenderState(D3DRS_LIGHTING, oldLighting);
device->SetRenderState(D3DRS_ZENABLE, oldZ);
device->SetRenderState(D3DRS_FOGENABLE, oldFog);
device->SetRenderState(D3DRS_CULLMODE, oldCull);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, oldAlpha);
device->SetRenderState(D3DRS_SRCBLEND, oldSrc);
device->SetRenderState(D3DRS_DESTBLEND, oldDst);
}