//===========================================================================// // 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 // Scalar + engine prelude #pragma hdrstop #if !defined(BTL4VID_HPP) # include // dpl2d_DISPLAY (opaque) + dpl2d_* prototypes #endif #if !defined(DPL2D_HPP) # include #endif #include #include #include // 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 verts; // vertex runs }; } struct Dpl2dList { bool recording; bool compiled; int mode; 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), openKind(-1) {} }; 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 (prototypes in btl4vid.hpp) //===========================================================================// dpl2d_DISPLAY *dpl2d_NewDisplayList() { return reinterpret_cast(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 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 &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 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); }