From 3072035417577c492174d386d369d6a2f645ee4e Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:28:29 -0400 Subject: [PATCH] =?UTF-8?q?D226:=20HUD=20scale=20option=20(Game.HudScale?= =?UTF-8?q?=2075-200%)=20=E2=80=94=20fast3d=20G=5FHUDSCALE=5FEXT=20rect=20?= =?UTF-8?q?scaling=20about=20per-element=20anchors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ammo (per-hand corner), bottom messages/title card (bottom-left) and top dialogue (text origin) scale with crisp glyphs; dialogue and bottom messages are width-capped because their line breaks are authored. 100% emits nothing (golden 3/3). Captures verified at 150/200%; Win + Linux. --- port/fast3d/gfx_pc.cpp | 38 ++++++++++++++++++ port/include/hudaspect.h | 11 ++++++ port/src/optionsoverlay.c | 3 ++ port/src/video.c | 8 ++++ src/game/bondview2.c | 81 +++++++++++++++++++++++++++++++++++++++ src/game/gunfire.c | 4 ++ 6 files changed, 145 insertions(+) diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index dd002809..35deefc2 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -3384,8 +3384,36 @@ static void gfx_draw_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lr } } +/* D226: HUD span scale (G_HUDSCALE_EXT, emitted by port/include/hudaspect.h + * PORT_HUD_SCALE around specific HUD draws). While active, every rectangle + * (text glyphs, message boxes, ammo icons are all rects) is scaled about the + * anchor in logical 10.2 screen space, and texrect steps are divided by the + * scale so the same texels cover the larger/smaller rect. 1.0 = inactive. */ +static float s_hud_scale = 1.0f; +static int32_t s_hud_ax = 0, s_hud_ay = 0; /* anchor, 10.2 fixed (px*4) */ + +static inline void gfx_hud_scale_rect(int32_t& ulx, int32_t& uly, int32_t& lrx, int32_t& lry) { + if (s_hud_scale == 1.0f) { + return; + } + ulx = s_hud_ax + (int32_t)lroundf((float)(ulx - s_hud_ax) * s_hud_scale); + lrx = s_hud_ax + (int32_t)lroundf((float)(lrx - s_hud_ax) * s_hud_scale); + uly = s_hud_ay + (int32_t)lroundf((float)(uly - s_hud_ay) * s_hud_scale); + lry = s_hud_ay + (int32_t)lroundf((float)(lry - s_hud_ay) * s_hud_scale); + /* A full-width box (e.g. the dialogue backdrop, 0..W) scaled about a + * centre anchor goes negative; negative rect coords are not drawn, so + * clip at the canvas origin (the right/bottom overhang is harmless). */ + if (ulx < 0) ulx = 0; + if (uly < 0) uly = 0; +} + static void gfx_dp_texture_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lry, uint8_t tile, int16_t uls, int16_t ult, int16_t dsdx, int16_t dtdy, bool flip) { + if (s_hud_scale != 1.0f) { /* D226 */ + gfx_hud_scale_rect(ulx, uly, lrx, lry); + dsdx = (int16_t)lroundf((float)dsdx / s_hud_scale); + dtdy = (int16_t)lroundf((float)dtdy / s_hud_scale); + } uint64_t saved_combine_mode = rdp.combine_mode; if ((rdp.other_mode_h & (3U << G_MDSFT_CYCLETYPE)) == G_CYC_COPY) { // Per RDP Command Summary Set Tile's shift s and this dsdx should be set to 4 texels @@ -3509,6 +3537,7 @@ static void gfx_dp_image_rectangle(int32_t tile, int32_t w, int32_t h, } static void gfx_dp_fill_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lry) { + gfx_hud_scale_rect(ulx, uly, lrx, lry); /* D226 */ if (rdp.color_image_address == rdp.z_buf_address) { // Don't clear Z buffer here since we already did it with glClear return; @@ -3698,6 +3727,14 @@ static void gfx_run_dl(Gfx* cmd) { case (uint8_t)G_CLEARGEOMETRYMODE: gfx_sp_geometry_mode(cmd->words.w1, 0); break; + case 0x46: /* G_HUDSCALE_EXT (D226): w0 low16 = scale*256 (0/256 = off), w1 = ax4<<16 | ay4 */ + { + const uint32_t sc = C0(0, 16); + s_hud_scale = (sc == 0 || sc == 256) ? 1.0f : (float)sc / 256.0f; + s_hud_ax = (int32_t)C1(16, 16); + s_hud_ay = (int32_t)C1(0, 16); + break; + } case G_EXTRAGEOMETRYMODE_EXT: gfx_sp_extra_geometry_mode(~C0(0, 24), cmd->words.w1); break; @@ -4091,6 +4128,7 @@ static double s_perf_pre = 0, s_perf_post = 0, s_perf_swap = 0; static uint64_t s_perf_frames = 0, s_perf_last_start = 0; extern "C" void gfx_run(Gfx* commands) { + s_hud_scale = 1.0f; /* D226: never carry a HUD scale across frames */ const uint64_t perf_t0 = gfx_perfstat_on() ? gfx_perf_now_ns() : 0; ++num_dls; gfx_sp_reset(); diff --git a/port/include/hudaspect.h b/port/include/hudaspect.h index 0d5ad4e0..7dc84e48 100644 --- a/port/include/hudaspect.h +++ b/port/include/hudaspect.h @@ -35,4 +35,15 @@ extern float portNativeAspect(void); /* port/src/video.c */ #define PORT_HUD_ASPECT(gdl, mode) \ do { if (portNativeAspect() > 1.3334f) { gSPHudAspectEXT((gdl)++, (mode)); } } while (0) +/* D226: HUD span scale. PORT_HUD_SCALE(gdl, ax, ay) scales every rectangle + * drawn after it (glyphs, message boxes, ammo icons) about the anchor + * (logical screen px) by Game.HudScale %, until PORT_HUD_SCALE_END. At 100% + * nothing is emitted (byte-identical). Decoded as G_HUDSCALE_EXT in fast3d. */ +#define GE_HUDSCALE_EXT 0x46 +extern s32 portHudScalePercent(void); /* port/src/video.c */ +#define gSPHudScaleEXT(pkt, scale256, ax4, ay4) gDma0p((pkt), GE_HUDSCALE_EXT, ((u32)(ax4) << 16) | ((u32)(ay4) & 0xFFFFu), ((u32)(scale256) & 0xFFFFu)) +#define PORT_HUD_SCALE(gdl, ax, ay) do { s32 _hp = portHudScalePercent(); if (_hp != 100) { gSPHudScaleEXT((gdl)++, _hp * 256 / 100, (ax) * 4, (ay) * 4); } } while (0) +#define PORT_HUD_SCALE_PCT(gdl, pct, ax, ay) do { s32 _hp = (pct); if (_hp != 100) { gSPHudScaleEXT((gdl)++, _hp * 256 / 100, (ax) * 4, (ay) * 4); } } while (0) +#define PORT_HUD_SCALE_END(gdl) do { if (portHudScalePercent() != 100) { gSPHudScaleEXT((gdl)++, 256, 0, 0); } } while (0) + #endif diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 5c5c829f..63e78854 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -203,6 +203,9 @@ static struct Row rows[] = { /* D232: the community "no damage flash" toggle (suppresses the red/green * hit-flash overlay in bondview2). */ { "__HdrGame", "GAME", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, + /* D226: scales the ammo counter, pickup / status messages and dialogue + * about their screen anchors. 100% = original, nothing emitted. */ + { "Game.HudScale", "HUD scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Video.DisplayFPS", "Show FPS", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, diff --git a/port/src/video.c b/port/src/video.c index cc299759..088a82c4 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -63,6 +63,7 @@ static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-Po static int cfgFovScale = 100; /* D211: percent of the original vertical FOV; 100 = unchanged (byte-identical) */ static int cfgWidescreenAuto = 1; /* WIDESCREEN-FOV-PLAN Phase 4: auto-scale vertical FOV by window aspect ratio; on by default, no-op at 4:3 */ static int cfgNativeWidescreen = 1; /* D334 (WIDESCREEN-FOV-PLAN Phase 2): project the world at the real window aspect (Hor+); no-op at 4:3 */ +static int cfgHudScale = 100; /* D226: HUD text/ammo scale %, 100 = original (no emission) */ static int cfgDrawDistance = 150; /* D218: percent of the level's authored far-clip/fog distance. Default raised 100->150 for v0.2.0: at the authored N64 distance, props visibly fade in just before they become visible on modern displays (Dam alarms / wall switches); 150 is the value the Steam Deck preset playtest-validated. 100 = unchanged N64. */ static int cfgDrawDistanceAutoFov = 1; /* D218: couple draw distance to Video.FovScale unless DrawDistance is set explicitly */ static int cfgLodDistance = 150; /* D249: percent scale on the geometry/model LOD-swap distance. Default raised 100->150 for v0.2.0 (same pop-in family as DrawDistance: LOD-swapped props like Dam's alarms/wall switches faded in at range); 150 matches the Steam Deck preset. 100 = unchanged N64. */ @@ -156,6 +157,12 @@ f32 portFovScale = 1.0f; * see the same aspect, so there is no second "view width" to reconcile * (WIDESCREEN-FOV-PLAN s6). Clamped to [0.5, 4.0]. At exactly 4:3 the * formula is identity, so 4:3 renders are unchanged. */ +/* D226: Game.HudScale percent (75..200); 100 = original size, nothing emitted. */ +s32 portHudScalePercent(void) +{ + return cfgHudScale; +} + f32 portNativeAspect(void) { f32 a = gfx_current_dimensions.aspect_ratio; @@ -348,6 +355,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Video.FovScale", &cfgFovScale, 50, 150); configRegisterInt("Video.WidescreenAuto", &cfgWidescreenAuto, 0, 1); configRegisterInt("Video.NativeWidescreen", &cfgNativeWidescreen, 0, 1); /* D334 */ + configRegisterInt("Game.HudScale", &cfgHudScale, 75, 200); /* D226 */ configRegisterInt("Video.DrawDistance", &cfgDrawDistance, 100, 400); configRegisterInt("Video.DrawDistanceAutoFov", &cfgDrawDistanceAutoFov, 0, 1); configRegisterInt("Video.LodDistance", &cfgLodDistance, 25, 400); diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 9e0ef7e6..f565b948 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -9260,6 +9260,63 @@ Gfx *bondviewRenderCredits(Gfx *gdl) } +#ifdef PORT +/* D226: the top dialogue's line breaks are authored into the text (no runtime + * wrap), so an unconditional HUD scale pushes long lines off-screen. Scale it + * as far as the widest line still fits between the text origin (view left + + * 0x1e, msg.x) and the same margin on the right. */ +static s32 portDialogueHudScalePercent(void) +{ + s32 hp = portHudScalePercent(); + s32 th = 0, tw = 0; + s32 avail; + char *text; + + if (hp <= 100) { + return hp; + } +#if defined(LEFTOVERDEBUG) + text = stringbuffer_top[upper_text_buffer_index]; +#else + text = dword_CODE_bss_80079DC8[upper_text_buffer_index]; +#endif + if (text == NULL || text[0] == '\0') { + return hp; + } + textMeasure(&th, &tw, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0); + avail = viGetViewWidth() - 2 * 0x1e; + if (tw > 0 && tw * hp > avail * 100) { + hp = (avail * 100) / tw; + if (hp < 100) hp = 100; + } + return hp; +} +#endif + +#ifdef PORT +/* D226: same width cap for the lower-left pickup / status message (and the + * level title card), measured with the fonts the bottom renderer uses. */ +static s32 portBottomHudScalePercent(void) +{ + s32 hp = portHudScalePercent(); + s32 th = 0, tw = 0; + s32 avail; + char *text = stringbuffer_lowerleft[status_bar_text_buffer_index]; + + if (hp <= 100 || text[0] == 0) { + return hp; + } + textMeasure(&th, &tw, text, BONDVIEW_2ND_FONTTABLE(status_bar_text_buffer_index), + BONDVIEW_1ST_FONTTABLE(status_bar_text_buffer_index), 0); + avail = viGetViewWidth() - 2 * 0x1e; + if (tw > 0 && tw * hp > avail * 100) { + hp = (avail * 100) / tw; + if (hp < 100) hp = 100; + } + return hp; +} +#endif + Gfx *maybe_mp_interface(Gfx *gdl) { s32 ulx; @@ -9278,16 +9335,28 @@ Gfx *maybe_mp_interface(Gfx *gdl) bondviewIntroCameraTextTick(); #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER); +#endif +#ifdef PORT + PORT_HUD_SCALE_PCT(gdl, portBottomHudScalePercent(), viGetViewLeft(), viGetViewTop() + viGetViewHeight()); /* D226: bottom messages, width-capped */ #endif gdl = hudmsgBottomRender(gdl); +#ifdef PORT + PORT_HUD_SCALE_END(gdl); +#endif #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE); #endif bondviewUpperTextWindowTimerTick(); #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER); +#endif +#ifdef PORT + PORT_HUD_SCALE_PCT(gdl, portDialogueHudScalePercent(), viGetViewLeft() + 0x1e, viGetViewTop()); /* D226: dialogue, width-capped */ #endif gdl = sub_GAME_7F08AAE8(gdl); +#ifdef PORT + PORT_HUD_SCALE_END(gdl); +#endif #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE); #endif @@ -9461,16 +9530,28 @@ Gfx *maybe_mp_interface(Gfx *gdl) bondviewIntroCameraTextTick(); #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER); +#endif +#ifdef PORT + PORT_HUD_SCALE_PCT(gdl, portBottomHudScalePercent(), viGetViewLeft(), viGetViewTop() + viGetViewHeight()); /* D226: bottom messages, width-capped */ #endif gdl = hudmsgBottomRender(gdl); +#ifdef PORT + PORT_HUD_SCALE_END(gdl); +#endif #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE); #endif bondviewUpperTextWindowTimerTick(); #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER); +#endif +#ifdef PORT + PORT_HUD_SCALE_PCT(gdl, portDialogueHudScalePercent(), viGetViewLeft() + 0x1e, viGetViewTop()); /* D226: dialogue, width-capped */ #endif gdl = sub_GAME_7F08AAE8(gdl); +#ifdef PORT + PORT_HUD_SCALE_END(gdl); +#endif #ifdef PORT PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE); #endif diff --git a/src/game/gunfire.c b/src/game/gunfire.c index 0a82ed39..53293f63 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -6222,6 +6222,7 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl) #ifdef PORT /* D335: right-hand ammo anchors to the right edge (native widescreen). */ PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_RIGHT); + PORT_HUD_SCALE(gdl, viGetViewLeft() + viGetViewWidth(), viGetViewTop() + viGetViewHeight()); /* D226 */ #endif if (weapon_right != ITEM_UNARMED) { @@ -6291,7 +6292,9 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl) #ifdef PORT /* D335: left-hand (dual-wield) ammo anchors to the left edge. */ + PORT_HUD_SCALE_END(gdl); PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_LEFT); + PORT_HUD_SCALE(gdl, viGetViewLeft(), viGetViewTop() + viGetViewHeight()); /* D226 */ #endif if (weapon_left != ITEM_UNARMED) { @@ -6362,6 +6365,7 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl) } #ifdef PORT + PORT_HUD_SCALE_END(gdl); PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE); #endif return gdl;