From fa88f74c688c9865421cf675912a50b9c6b556e6 Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 09:14:36 -0500 Subject: [PATCH] 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 --- engine/MUNGA_L4/L4VIDEO.cpp | 36 ++++++++++++++--------- game/reconstructed/dpl2d.cpp | 56 ++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 192b9ec..7ebab85 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -139,24 +139,24 @@ void BTSetAimProjection(float p11, float p22, float vpW, float vpH) } } +float BTGetPresentAspect(); // defined below with gWindowAspect (task #44) + // -// Client-area mouse position -> reticle coords: undo the present stretch -// (client px -> viewport px), then centre/scale by the dpl2d unit. Clamped -// to the visible frame. +// Client-area mouse position -> reticle coords. The reticle frame is SQUARE +// on the PRESENTED image (task #44): unit = half the CLIENT height on both +// axes -- no backbuffer round-trip. Clamped to the visible frame. // void BTClientToReticle(float mx, float my, float cw, float ch, float *rx, float *ry) { - if (cw <= 0.0f || ch <= 0.0f || gBTAimVpW <= 0.0f || gBTAimVpH <= 0.0f) + if (cw <= 0.0f || ch <= 0.0f) { *rx = 0.0f; *ry = 0.0f; return; } - const float vx = mx * (gBTAimVpW / cw); - const float vy = my * (gBTAimVpH / ch); - float x = (vx - gBTAimVpW * 0.5f) / (gBTAimVpH * 0.5f); - float y = (vy - gBTAimVpH * 0.5f) / (gBTAimVpH * 0.5f); - const float xmax = gBTAimVpW / gBTAimVpH; + float x = (mx - cw * 0.5f) / (ch * 0.5f); + float y = (my - ch * 0.5f) / (ch * 0.5f); + const float xmax = cw / ch; if (x < -xmax) x = -xmax; if (x > xmax) x = xmax; if (y < -1.0f) y = -1.0f; @@ -176,7 +176,7 @@ float BTTwistToReticleX(float twist_rad) { if (!gBTAimCamValid || gBTAimP11 <= 0.0f || gBTAimVpH <= 0.0f) return 0.0f; - return (float)tan((double)twist_rad) * gBTAimP11 * (gBTAimVpW / gBTAimVpH); + return (float)tan((double)twist_rad) * gBTAimP11 * BTGetPresentAspect(); } // @@ -190,7 +190,7 @@ int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]) if (!gBTAimCamValid || gBTAimP11 <= 0.0f || gBTAimP22 <= 0.0f || gBTAimVpW <= 0.0f) return 0; - const float ndcX = rx * (gBTAimVpH / gBTAimVpW); + const float ndcX = rx / BTGetPresentAspect(); // square reticle frame (task #44) const float ndcY = -ry; const float cx = ndcX / gBTAimP11; const float cy = ndcY / gBTAimP22; @@ -240,7 +240,7 @@ int BTProjectToReticle(const float world[3], float *rx, float *ry) } const float ndcX = (xc * gBTAimP11) / depth; const float ndcY = (yc * gBTAimP22) / depth; - *rx = ndcX * (gBTAimVpW / gBTAimVpH); + *rx = ndcX * BTGetPresentAspect(); // square reticle frame (task #44) *ry = -ndcY; return 1; } @@ -273,7 +273,7 @@ int BTProjectHotBox(const float top[3], float *xl, float *xr, *side = (xc >= 0.0f) ? 1 : -1; return 0; } - const float xs = (gBTAimP11 / depth) * (gBTAimVpW / gBTAimVpH); + const float xs = (gBTAimP11 / depth) * BTGetPresentAspect(); // (task #44) const float ys = gBTAimP22 / depth; *xl = (xc - 4.0f) * xs; *xr = (xc + 4.0f) * xs; @@ -1112,6 +1112,16 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) // the scene fat/skinny. float gWindowAspect = 0.0f; +// The PRESENTED aspect (task #45): the fixed 800x600 backbuffer stretches into +// the client area, so anything drawn in backbuffer pixels must pre-compensate +// by the CLIENT aspect to appear square on screen. The HUD + the aim-ray +// reticle<->NDC conversions use this. +float BTGetPresentAspect() +{ + extern float gWindowAspect; + return (gWindowAspect > 0.0f) ? gWindowAspect : (800.0f / 600.0f); +} + #define PILL_COUNT 20 #define PILL_SIZE (y_size*0.03125) // 32 @ 1280x1024 #define PILL_SPACING (PILL_SIZE*0.625) // 20 @ 1280x1024 diff --git a/game/reconstructed/dpl2d.cpp b/game/reconstructed/dpl2d.cpp index 2a05372..1ab9352 100644 --- a/game/reconstructed/dpl2d.cpp +++ b/game/reconstructed/dpl2d.cpp @@ -45,6 +45,11 @@ #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 @@ -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);