diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index d6857f7..447730d 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -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)); diff --git a/game/reconstructed/dpl2d.cpp b/game/reconstructed/dpl2d.cpp index 7ed0359..881c074 100644 --- a/game/reconstructed/dpl2d.cpp +++ b/game/reconstructed/dpl2d.cpp @@ -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 // Scalar + engine prelude @@ -35,11 +45,6 @@ #include #include -//===========================================================================// -// 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 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 matrixStack; // saved translations (Push/Pop) std::vector commands; + // the currently-open vertex primitive (between AddOpenX .. AddCloseX) + int openKind; // -1 none, else Command::Kind + std::vector 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(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(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 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 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); diff --git a/phases/phase-02-dpl2d-reticle.md b/phases/phase-02-dpl2d-reticle.md new file mode 100644 index 0000000..99e0dfd --- /dev/null +++ b/phases/phase-02-dpl2d-reticle.md @@ -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).