HUD aspect correction: square reticle units at any window shape (task #44)

User: "it doesn't scale with our screen resolution?" -- correct. The
HUD draws into the fixed 800x600 backbuffer and the present stretches
it into the client area: on non-4:3 windows everything distorted
(circles -> ellipses, the bottom tape widened vs the ladder, positions
drifted outward).

- BTGetPresentAspect (L4VIDEO, from gWindowAspect; 4:3 fallback).
- dpl2d x-unit = (bbW/2)/presentAspect (== bbH/2 at 4:3 -- unchanged
  there), circles pre-squished in bb space so the stretch restores
  round.
- The reticle<->NDC conversions (BTGetAimRay / BTProjectToReticle /
  BTProjectHotBox / BTTwistToReticleX) use the PRESENT aspect; the
  mouse map (BTClientToReticle) is now pure client-relative.
- Placement itself is authentic and stays put: the binary clusters the
  instruments around the boresight (ladder x=0.35 half-heights, spans
  +-0.25) -- NOT at the display edges.
- Crash en route: the BTGetPresentAspect extern declared INSIDE dpl2d's
  anonymous namespace = a different, unresolved symbol -> /FORCE
  garbage call (landed in MissileLauncher::DefaultData). Moved to
  global scope; gotcha noted in the comment.

Verified 4:3 regression: aimed lock + zone hits + no crash; hud-geom
MapX/MapY unchanged at 4:3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 09:14:36 -05:00
co-authored by Claude Fable 5
parent e13e8af44b
commit fa88f74c68
2 changed files with 71 additions and 21 deletions
+48 -8
View File
@@ -45,6 +45,11 @@
#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
@@ -343,11 +348,27 @@ namespace
//
// COORDINATE MODEL (from the reticle's authored constants): centred origin,
// +x right, +y down, unit = half the viewport HEIGHT on both axes (keeps
// circles round; the reticle content spans roughly +-0.5).
// +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 * (vh * 0.5f); }
{ 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); }
@@ -447,7 +468,8 @@ namespace
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 r = cmd.c * (vh * 0.5f); // height-unit -> stays round
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;
@@ -455,8 +477,8 @@ namespace
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].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;
}
@@ -468,8 +490,8 @@ namespace
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].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;
}
@@ -493,6 +515,24 @@ void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device)
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);