Files
BT411/game/reconstructed/dpl2d.cpp
T
arcattackandClaude Opus 5 5174c638ce K/D scoreboard ROOT-CAUSED (#45) + respawn latch fix (#57) + plate stage ops (#58)
FIXES (compile clean; exe link pending -- a game client still holds btl4.exe):

#57 respawn latch: deathPending was released in ONE place (btplayer.cpp:343) and
only if a LATER re-post happened to find the mech alive -- so if the +5s re-post
landed before the drop-zone reply, or never arrived, the flag stayed set while
the pilot respawned and fought on, and their NEXT death hit the dedup at :391
and was swallowed whole (no warp, no tally, no hunt) for the rest of the
process.  Tonight 3 of 3 deaths in the final round were swallowed, each by a
player who had respawned successfully earlier in the same process.  Clear moved
to the actual completion point (after Mech::Reset at the drop zone) + the four
abandon paths that also stranded it.  Cycle logging promoted to always-on
(START / RESET / SWALLOWED warning) + a RESPAWN matchlog record.

#58 reticle callsign plate drew as a SOLID RECTANGLE on some maps: my
dpl2d_DrawTexturedRect bound a texture but never set COLOROP/ALPHAOP, and
L4D3D.cpp:1085 sets both to SELECTARG1 from TFACTOR (ignores the texture, fills
with a constant).  Which state was live depended on what the 3D pass drew last
-- hence 'depends on the level'.  Now sets MODULATE texture x diffuse for both
channels and restores.  Latent sibling noted (CameraHUDDrawQuad, same omission).

ROOT-CAUSE DOC (no code): docs/KD_SCOREBOARD_PLAN.md.  Headlines:
 - the port reads DEATHS from the WRONG FIELD: the binary's PilotList draws
   +0x27c/+0x280; we labelled +0x280 'pad_0x280', never write it, and
   substituted the engine's Player::deathCount (+0x200, the respawn identity
   number seeded to -2) -> remote rows read a clamped fake 0 by construction;
 - BTPlayer::VehicleDeadMessageHandler @0x4c05c4 is MISSING from our decomp
   export (functions_index.tsv jumps 4c052c -> 4c083c, verified) -- that silence
   is what produced the 'pad' belief.  An absent function is not evidence of an
   absent behaviour;
 - neither counter rides an update record, so divergence never self-heals and
   bystanders show 0/0 all mission;  BTPlayerCountObservedDeath is unreachable
   dead code;
 - 'a kill I didn't earn' = last-hitter-takes-all via lastInflictingID, and
   COLLISIONS count (5 of 47 deaths had a type=0 killing blow);
 - the phantom kill is authentic 1995 (the kill handler bumps the victim's
   killCount too);
 - btplayer.cpp:453 does simulationFlags |= 0x1 where the binary does
   ForceUpdate() -- bit 0 is DelayWatchersFlag and nothing clears it, so the
   first death of any pilot permanently disables ExecuteWatchers on that
   player's simulation.  Filing separately.

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

716 lines
25 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);
//
// TEXTURE STAGE OPS -- explicitly, every time (Gitea #58). Binding a
// texture is NOT enough: the fixed-function stage still has whatever
// COLOROP/ALPHAOP the last drawer left, and this TU's own list path runs
// UNtextured (`SetTexture(0, 0)`), so nothing here establishes them. In
// particular `L4D3D.cpp:1085-1088` sets
// COLOROP = ALPHAOP = D3DTOP_SELECTARG1 with ARG1 = D3DTA_TFACTOR
// which makes the pipeline ignore the texture completely and fill with a
// constant colour -- i.e. the callsign plate renders as a SOLID RECTANGLE.
// Whether that state happens to be live depends on what the 3D scene drew
// last, which is why the field report was "sometimes, depends on the level
// and the background". MODULATE texture x diffuse for BOTH channels makes
// the draw deterministic: colour = texel * diffuse, alpha = texel * diffuse,
// so a 0x0000 texel (the transparent background of the 1bpp raster upload,
// L4VIDEO.cpp LoadBitSliceTexture) stays invisible.
//
DWORD oCOp = 0, oCA1 = 0, oCA2 = 0, oAOp = 0, oAA1 = 0, oAA2 = 0;
device->GetTextureStageState(0, D3DTSS_COLOROP, &oCOp);
device->GetTextureStageState(0, D3DTSS_COLORARG1, &oCA1);
device->GetTextureStageState(0, D3DTSS_COLORARG2, &oCA2);
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &oAOp);
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &oAA1);
device->GetTextureStageState(0, D3DTSS_ALPHAARG2, &oAA2);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(VertexTex));
device->SetTextureStageState(0, D3DTSS_COLOROP, oCOp);
device->SetTextureStageState(0, D3DTSS_COLORARG1, oCA1);
device->SetTextureStageState(0, D3DTSS_COLORARG2, oCA2);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, oAOp);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, oAA1);
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, oAA2);
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);
}