HUD phase 1: full dpl2d API recovered + opcode-faithful 2D layer rework

Every dpl2d_ recorder in the binary self-identifies via its debug-name string
(part_010.c 0x487f34..0x4888c0) -- the complete API + opcode map is recorded
in phases/phase-02-dpl2d-reticle.md: point sets (2/3), closed polylines (4/5),
open line strips (6/7), AddPoint (8), AddCircle (9), SetColor (0xF),
Set/ConcatMatrix (0x10/0x11, 2x3 affine), Push/PopState (0x12/0x13),
SetLineWidth (0x15), CallDisplayList (nested glyphs), FullScreenClipRegion.

dpl2d.cpp reworked to that model: command-stream recorder (open-primitive
vertex runs, state commands, nested list calls) + a recursive ExecuteList
rasteriser (XYZRHW points/strips/loops/circles, 2x3 transform + state stacks,
save/restore).  CORRECTION: the old "PushMatrix/MoveTo/PopMatrix" trio was a
misreading of OpenPolypoint/AddPoint/ClosePolypoint ("draw a point") -- kept
as aliases mapping to the true semantics.

Reticle findings recorded: the ctor (@004cc40c) builds ~15 display lists (the
dotted-cross reticle, tick ladders via FUN_004cd938, side arrows, arcs) plus a
3D marker chain and the PNAME1-8.bgf pip meshes (all ship).  Next: transcribe
ctor+Execute, wire the 1996 caller's AddWeapon arguments, hook the draw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 19:32:36 -05:00
co-authored by Claude Fable 5
parent 56f02b2176
commit 1cd8ca1a80
3 changed files with 467 additions and 161 deletions
+19
View File
@@ -112,6 +112,25 @@ void dpl2d_PopMatrix(dpl2d_DISPLAY *list);
void dpl2d_MoveTo(dpl2d_DISPLAY *list, Scalar x, Scalar y);
void dpl2d_End(dpl2d_DISPLAY *list);
void dpl2d_Compile(dpl2d_DISPLAY *list);
//
// The FULL recovered API (phase-02: every recorder named by its debug string;
// opcode map in phases/phase-02-dpl2d-reticle.md). The Push/Move/Pop trio
// above is a legacy alias of OpenPolypoint/AddPoint/ClosePolypoint.
//
void dpl2d_OpenPolypoint(dpl2d_DISPLAY *list); // opcode 2
void dpl2d_ClosePolypoint(dpl2d_DISPLAY *list); // opcode 3
void dpl2d_OpenPolyline(dpl2d_DISPLAY *list); // opcode 4 (closed loop)
void dpl2d_ClosePolyline(dpl2d_DISPLAY *list); // opcode 5
void dpl2d_OpenLines(dpl2d_DISPLAY *list); // opcode 6 (open strip)
void dpl2d_CloseLines(dpl2d_DISPLAY *list); // opcode 7
void dpl2d_AddPoint(dpl2d_DISPLAY *list, Scalar x, Scalar y); // opcode 8
void dpl2d_SetLineWidth(dpl2d_DISPLAY *list, Scalar width); // opcode 0x15
void dpl2d_PushState(dpl2d_DISPLAY *list); // opcode 0x12
void dpl2d_PopState(dpl2d_DISPLAY *list); // opcode 0x13
void dpl2d_SetMatrix(dpl2d_DISPLAY *list, const Scalar *six); // opcode 0x10 (2x3)
void dpl2d_ConcatMatrix(dpl2d_DISPLAY *list, const Scalar *six); // opcode 0x11
void dpl2d_CallList(dpl2d_DISPLAY *list, dpl2d_DISPLAY *callee); // nested glyphs
void dpl2d_FullScreenClip(dpl2d_DISPLAY *list);
void dpl_SetMaterialNameCallback(char *(*callback)(char *source));
+378 -161
View File
@@ -4,21 +4,31 @@
//---------------------------------------------------------------------------//
// libDPL 2D vector display-list layer, re-hosted over Direct3D 9. //
// //
// Replaces the inert dpl2d_* stubs that used to live in btstubs.cpp. The //
// recorder calls (dpl2d_NewDisplayList / Begin / SetColor / Circle / //
// PushMatrix / PopMatrix / MoveTo / End / Compile -- prototypes in //
// btl4vid.hpp) build a small command list; dpl2d_ExecuteList (dpl2d.hpp) //
// draws a compiled list with screen-space 2D primitives. //
// 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: //
// //
// The recorded calls match what the reconstructed reticle/PIP builder in //
// btl4vid.cpp actually issues, e.g. //
// dpl2d_Begin(list, 1); //
// dpl2d_SetColor(list, r, g, b); //
// dpl2d_Circle(list, x, y, 0.012f, 1); // filled blip //
// dpl2d_SetColor(list, 0, 0, 0); //
// dpl2d_Circle(list, x, y, 0.014f, 0); // dark outline ring //
// dpl2d_PushMatrix(list); dpl2d_MoveTo(list, x, y); dpl2d_PopMatrix(); //
// dpl2d_End(list); dpl2d_Compile(list); //
// OpenDisplayList(mode) .. CloseDisplayList .. FlushDisplayList //
// AddOpenPolypoint(2)/AddClosePolypoint(3) -- a POINT set //
// AddOpenPolyline(4)/AddClosePolyline(5) -- a CLOSED loop //
// AddOpenLines(6)/AddCloseLines(7) -- an OPEN line strip //
// 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
@@ -35,11 +45,6 @@
#include <vector>
#include <math.h>
//===========================================================================//
// Pre-transformed 2D vertex -- mirrors MUNGA_L4/L4D3D.h L4VERTEX_2D and its
// L4VERTEX_2D_FVF, redeclared locally so this TU does not need the MUNGA_L4
// source dir on the include path (the engine lib uses the identical layout).
//===========================================================================//
namespace
{
struct Vertex2D
@@ -49,7 +54,7 @@ namespace
};
const DWORD kVertex2DFVF = (D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
const int kCircleSegments = 32; // tessellation of dpl2d_Circle
const int kCircleSegments = 32; // tessellation of AddCircle
const float kPi = 3.14159265358979323846f;
inline float ClampUnit(float v)
@@ -57,44 +62,74 @@ namespace
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 drawing command. The dpl2d_ API the port uses only ever
// emits coloured circles (filled disc or outline ring); the matrix stack
// and pen move are tracked as a translation so any future MoveTo-relative
// geometry lands in the right place.
// One recorded command (the opcode stream, structurally).
//
struct Command
{
enum Kind { kCircleFill, kCircleOutline } kind;
DWORD color;
float x, y; // normalised view coords (already translated)
float radius; // normalised (fraction of viewport width)
enum Kind
{
kPoints, // vertex run: a point per vertex
kLineStrip, // vertex run: open strip
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 Vec2 { float x, y; };
}
//
// The concrete display list. dpl2d_DISPLAY is an opaque empty class
// (DPLSTUB.h), so the recorder allocates one of these and hands the caller a
// reinterpret_cast handle; every dpl2d_ call casts it straight back.
//
struct Dpl2dList
{
bool recording;
bool compiled;
int mode;
DWORD currentColor;
float penX, penY; // current pen (MoveTo)
float transX, transY; // current translation
std::vector<Vec2> matrixStack; // saved translations (Push/Pop)
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),
currentColor(0xFFFFFFFF),
penX(0.0f), penY(0.0f), transX(0.0f), transY(0.0f)
{}
Dpl2dList() : recording(false), compiled(false), mode(0), openKind(-1) {}
};
static inline Dpl2dList *AsList(dpl2d_DISPLAY *handle)
@@ -102,8 +137,20 @@ 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 the game calls (declared in btl4vid.hpp)
// Recorder -- the dpl2d_ entry points (prototypes in btl4vid.hpp)
//===========================================================================//
dpl2d_DISPLAY *dpl2d_NewDisplayList()
@@ -111,92 +158,310 @@ dpl2d_DISPLAY *dpl2d_NewDisplayList()
return reinterpret_cast<dpl2d_DISPLAY *>(new Dpl2dList);
}
void dpl2d_Begin(dpl2d_DISPLAY *list, int mode)
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->currentColor= 0xFFFFFFFF;
self->transX = self->transY = 0.0f;
self->penX = self->penY = 0.0f;
self->matrixStack.clear();
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;
self->currentColor = D3DCOLOR_COLORVALUE(
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.color = self->currentColor;
cmd.x = (float)x + self->transX;
cmd.y = (float)y + self->transY;
cmd.radius = (float)radius;
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_PushMatrix(dpl2d_DISPLAY *list)
void dpl2d_FullScreenClip(dpl2d_DISPLAY * /*list*/)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
Vec2 saved = { self->transX, self->transY };
self->matrixStack.push_back(saved);
}
void dpl2d_PopMatrix(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0 || self->matrixStack.empty()) return;
Vec2 saved = self->matrixStack.back();
self->matrixStack.pop_back();
self->transX = saved.x;
self->transY = saved.y;
// clip reset -- the executor always draws to the full viewport
}
//
// MoveTo: position the pen. In the recovered usage it is bracketed by
// Push/Pop with no draw in between (a no-op marker stamp), so it only updates
// the pen + the current translation; nothing is emitted.
// LEGACY ALIASES (the earlier AddWeapon transcription used these names for
// what the binary's opcodes reveal to be the point primitive).
//
void dpl2d_MoveTo(dpl2d_DISPLAY *list, Scalar x, Scalar y)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
self->penX = (float)x;
self->penY = (float)y;
self->transX = (float)x;
self->transY = (float)y;
}
void dpl2d_End(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
self->recording = false;
}
void dpl2d_Compile(dpl2d_DISPLAY *list)
{
Dpl2dList *self = AsList(list);
if (self == 0) return;
self->compiled = true; // geometry is tessellated lazily at execute time
}
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;
};
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 = ox + p.x * vw; v.y = oy + p.y * 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)
{
if (verts.size() >= 2)
device->DrawPrimitiveUP(D3DPT_LINESTRIP,
(UINT)verts.size() - 1, &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, float ox, float oy, float vw, float vh, int depth)
{
if (self == 0 || depth > 8)
return;
std::vector<ExecState> stack;
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:
ExecuteListInner((Dpl2dList *)cmd.callee, device,
st, 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 = ox + p.x * vw;
const float cy = oy + p.y * vh;
const float r = cmd.c * vw; // width-relative -> stays round
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) * r;
ring[s + 1].y = cy + sinf(a) * r;
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) * r;
ring[s].y = cy + sinf(a) * r;
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);
@@ -206,20 +471,15 @@ void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device)
D3DVIEWPORT9 vp;
if (FAILED(device->GetViewport(&vp)))
return;
const float ox = (float)vp.X;
const float oy = (float)vp.Y;
const float vw = (float)vp.Width;
const float vh = (float)vp.Height;
// --- save the render states we touch ---------------------------------
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);
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);
@@ -233,56 +493,13 @@ void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device)
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
device->SetFVF(kVertex2DFVF);
Vertex2D ring[kCircleSegments + 2];
ExecState st;
st.color = 0xFFFFFFFF;
st.width = 1.0f;
st.mat.Identity();
ExecuteListInner(self, device,
st, (float)vp.X, (float)vp.Y, (float)vp.Width, (float)vp.Height, 0);
for (size_t i = 0; i < self->commands.size(); ++i)
{
const Command &cmd = self->commands[i];
// normalised view coords -> pixels. radius scales with viewport width
// so it stays round on a non-square viewport.
const float cx = ox + cmd.x * vw;
const float cy = oy + cmd.y * vh;
const float rx = cmd.radius * vw;
const float ry = cmd.radius * vw;
if (cmd.kind == Command::kCircleFill)
{
// triangle fan: centre + rim, closed back to the first rim vertex.
ring[0].x = cx; ring[0].y = cy; ring[0].z = 0.0f; ring[0].rhw = 1.0f;
ring[0].color = cmd.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 = cmd.color;
}
device->DrawPrimitiveUP(
D3DPT_TRIANGLEFAN, kCircleSegments,
ring, sizeof(Vertex2D));
}
else // kCircleOutline
{
// line strip around the rim, closed.
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 = cmd.color;
}
device->DrawPrimitiveUP(
D3DPT_LINESTRIP, kCircleSegments,
ring, sizeof(Vertex2D));
}
}
// --- restore ----------------------------------------------------------
device->SetTexture(0, oldTex);
if (oldTex) oldTex->Release();
device->SetRenderState(D3DRS_LIGHTING, oldLighting);
+70
View File
@@ -0,0 +1,70 @@
# Phase 02 — dpl2d API recovery + BTReticleRenderable reconstruction (task #35)
**Goal:** the main-view HUD (targeting reticle + up to 10 per-weapon pips).
**Status:** API fully recovered [T1]; ctor/Execute transcription + wiring IN PROGRESS.
## The recovered dpl2d recorder API (libDPL 2D display lists) [T1]
Every recorder self-identifies via its `FUN_0049a300(1, s_dpl2d_...)` debug-name string
(part_010.c). Opcodes are the first word written into the list's command buffer.
| FUN @ | dpl2d name | opcode | args |
|---|---|---|---|
| 00487f34 | NewDisplayList | — | → list handle |
| 00487fbc | OpenDisplayList (Begin) | — | (list, mode) |
| 00488030 | CloseDisplayList (End) | — | (list) |
| 00488054 | FlushDisplayList (Compile) | — | (list) |
| 004880e8 | AddOpenPolyline | 4 | closed-loop polyline begin |
| 0048814c | AddClosePolyline (loop end) | 5 | |
| 00488278 | AddOpenLines | 6 | open line-strip begin |
| 004882dc | AddClosePolyline (strip end) | 7 | |
| 00488340 | AddOpenPolypoint | 2 | point-set begin |
| 004883a4 | AddClosePolypoint | 3 | |
| 00488408 | AddPoint | 8 | (x, y) — vertex of the open primitive |
| 0048847c | AddCircle | 9 | (x, y, r, fill) |
| 00488500 | AddSetLineWidth | 0x15 | (w) — 1.0/2.0/3.0 used |
| 0048856c | AddPushState | 0x12 | |
| 004885cc | AddPopState | 0x13 | |
| 00488630 | AddSetColor | 0xf | (r, g, b) floats |
| 004886ac | AddSetColor (packed) | 0x16 | (argb?) |
| 00488718 | AddSetMatrix | 0x10 | 6 floats (2x3 affine) |
| 004887b0 | AddConcatMatrix | 0x11 | 6 floats + flag |
| 00488850 | AddCallDisplayList | (nested) | (list) — glyph reuse |
| 004888c0 | AddFullScreenClipRegion | | |
| 00488b80 / 00488bac | 2x3 matrix build: identity / set-translate(x,y) | — | local helpers feeding AddSetMatrix |
⚠ CORRECTION to the earlier AddWeapon recon comments: the "PushMatrix/MoveTo/PopMatrix"
triple is actually **AddOpenPolypoint / AddPoint / AddClosePolypoint** = "draw a POINT" —
`game/reconstructed/dpl2d.cpp`'s current Push/Move/Pop interpretation must be reworked to
this opcode model (points, open/closed polylines, circles, width, color, push/pop state,
set/concat matrix, nested list calls) and its `dpl2d_ExecuteList` renderer extended to draw
them (XYZRHW screen-space lines/points/fans; state save/restore already present).
## BTReticleRenderable — what the ctor builds (@004cc40c, part_014.c:4362) [T1 read]
- Calibration from ctor params/templates: pip origin/bias/scale (+0x1fc..0x234 in class
terms), min/max range from templates, `rangeScale = (clamped - lo) / (hi - lo)`.
- Loads **PNAME1-8.bgf** (all ship in VIDEO/GEO) — pip/number meshes — plus a 3D
renderable chain (FUN_00489708/489724/48ab0c/491cc4... — the dpl *3D* overlay object at
0.12 scale) — the reticle has BOTH a 3D marker component and the 2D display lists.
- Builds ~15 display lists: the master list (param_1[0x98]) composed of nested glyph lists
via AddCallDisplayList: crosshair dot cluster (AddPoint xy at ±0.04/±0.1 = the dotted
cross), heavier ±0.1..0.16 tick cross at width 3, two tick-LADDERS via FUN_004cd938
(count 0xd=13 / 0x15=21 ticks, spacing/geometry from the calibration constants at
[0x7f..0x88]: 0.35/0.25/0.5/±0.008/0.016...), side arrow polylines (the 4-vertex
closed arrows at the ladder ends), an arc segment (AddCircle partial at 0.03?), and the
heading/aim sub-lists ([0x9a..0xa1, 0xb6..0xba]) positioned by AddSetMatrix translates.
- FUN_004cd938 = the tick-ladder builder (count, flag, scale, x, y, step, minor/major
tick half-lengths) — transcribe next.
## Remaining steps
1. Rework dpl2d.cpp to the opcode model + extend ExecuteList (lines/points/polys/matrix/call).
2. Transcribe the ctor's glyph program into BTReticleRenderable::BTReticleRenderable
(+ FUN_004cd938 helper) with the authentic constants.
3. Read + transcribe Execute (vtable PTR_LAB_0051db44 — find the Execute slot; per-frame:
withinRange attr reads → pip list visibility/colors, aim/heading matrix updates).
4. Read the 1996 caller wiring (FUN_004cef28's inside path: the ctor's 15 args + the
AddWeapon per-weapon argument sourcing) and reproduce in MakeMechRenderables.
5. Draw hook: the engine already calls mReticle->Execute() per frame (L4VIDEO ~6562);
or execute from the BT renderer after the 3D scene; dpl2d_ExecuteList(device).
6. The cockpit canopy shell (blx_cop, punch cutout) black-enclosure fix (BT_INSIDE_COCKPIT=1).