Files
BT411/game/reconstructed/dpl2d.cpp
T
arcattackandClaude Opus 5 35c750dc7c Gitea #44: the reticle TARGET NAME PLATE (PNAME1-8) -- the target's callsign under the crosshair
Reconstructed the last deferred piece of the 1995 reticle, decoded from the
Execute disassembly @004cdcf0 [T1]:
 - selection: target_mech+0x190 (owning BTPlayer) -> player+0x1e0
   (playerBitmapIndex), 1-based into the mesh table at this+0x2e8
 - placement: re-placed every frame the aim moves -- scale 0.12 uniform,
   translate (K*retPos.x, K*retPos.y - 0.08, -1.0), K = 1/2.8 (the x87 long
   double @0x4cee64) -- so the label TRACKS THE AIM POINT, not screen centre
 - visibility: the LOCK attribute (this+0x184, cached +0x188) AND an owning
   player; reticle-off / simple-X also hide it
 - art: the plate quad UV-addresses a shared 128x64 bitslice texmap whose
   texels the pod OVERWROTE each mission with the egg's callsign rasters
   (original source commented out at L4VIDEO.cpp:5682-5710) -- the baked
   'PLAYER n' art in BMAP.BSL is only the shipped fallback.  Material colour
   is a neutral (0.5,0.5,0.5): grey-white, not phosphor green.

Port: same geometry in reticle units (0.224 below the aim point, 0.336x0.084)
drawn as the target's egg callsign texture through the reticle's own MapX/MapY
mapping, so it follows the world view in every layout and BT_SHOT captures it.
New: dpl2d_DrawTexturedRect, BTReticleTargetPlate, BTGetPlayerNameTexture.

Also: kRetCaret corrected 0.02f -> 0.025f (_DAT_004cd7f4 read from the binary;
the old value was a T3 guess, 20% small).

KB: gauges-hud + open-questions corrected (the chain was filed as deferred and
mis-attributed to 'the 3D marker'); TWO PNAME consumers now distinguished (this
plate, and CameraShipHUDRenderable's ranking window which shipped 2026-07-18);
new bgf-format section on the plate atlas.

Rig-verified: 2-node relay, BT_GOTO=enemy -> '[hud] name plate: bmp=2 tex=1'
and 'Boreas' rendered under the crosshair on the locked target.  Solo clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-24 18:36:02 -05:00

680 lines
23 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);
}
//
//#############################################################################
// dpl2d_DrawTexturedRect -- a TEXTURED quad in RETICLE coordinates
//#############################################################################
//
// The reticle's target NAME PLATE (the 1995 PNAMEx.bgf plate) is a UV-mapped
// quad, so it cannot ride the line/polyline display lists above -- but it must
// land in the SAME space, because the binary places it from the same
// `Reticle::reticlePosition` that translates the aim group (reticle Execute
// @004cdede [T1 disasm]). So it reuses this TU's authentic mapping: MapX's
// aspect-corrected x unit + MapY's half-height y unit, in the CURRENT viewport
// (which is the world sub-rect under the cockpit-surround layout, not the whole
// backbuffer -- so the plate follows the view like the rest of the reticle).
//
// cx/cy = quad CENTRE, w/h = full extents, all in reticle units. The texture
// arrives as void* because the caller (game/reconstructed/btl4vid.cpp) carries
// no d3d9 types. State is saved/restored exactly as dpl2d_ExecuteList does
// (leaking a texture stage or the alpha state into the next frame's 3D pass is
// the "floating rocks" class of bug).
//
void dpl2d_DrawTexturedRect(
IDirect3DDevice9 *device,
void *texture,
float cx,
float cy,
float w,
float h,
unsigned long argb)
{
if (device == 0 || texture == 0)
return;
D3DVIEWPORT9 vp;
if (FAILED(device->GetViewport(&vp)))
return;
const float ox = (float)vp.X, oy = (float)vp.Y;
const float vw = (float)vp.Width, vh = (float)vp.Height;
const float xunit = XUnit(vw, vh); // aspect-corrected, as MapX
const float yunit = vh * 0.5f; // as MapY
const float px = MapX(cx, ox, vw, vh);
const float py = MapY(cy, oy, vh);
const float hw = (w * 0.5f) * xunit;
const float hh = (h * 0.5f) * yunit;
struct VertexTex
{
float x, y, z, rhw;
DWORD color;
float u, v;
};
const DWORD kVertexTexFVF =
(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1);
VertexTex quad[4];
memset(quad, 0, sizeof(quad));
for (int i = 0; i < 4; ++i)
{
quad[i].z = 0.0f;
quad[i].rhw = 1.0f;
quad[i].color = (DWORD)argb; // modulates the plate art (its material colour)
}
// tri-strip order: TR, TL, BR, BL
quad[0].x = px + hw; quad[0].y = py - hh; quad[0].u = 1.0f; quad[0].v = 0.0f;
quad[1].x = px - hw; quad[1].y = py - hh; quad[1].u = 0.0f; quad[1].v = 0.0f;
quad[2].x = px + hw; quad[2].y = py + hh; quad[2].u = 1.0f; quad[2].v = 1.0f;
quad[3].x = px - hw; quad[3].y = py + hh; quad[3].u = 0.0f; quad[3].v = 1.0f;
DWORD oldLighting = 0, oldZ = 0, oldCull = 0;
DWORD oldAlpha = 0, oldSrc = 0, oldDst = 0, oldFog = 0;
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->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(kVertexTexFVF);
device->SetTexture(0, (IDirect3DTexture9 *)texture);
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(VertexTex));
device->SetTexture(0, oldTex);
if (oldTex) oldTex->Release();
device->SetFVF(kVertex2DFVF);
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);
}