foxygit / doom Log in
commit b4d57fb0d043e17263036f0a7972200ee6b8a916
Author:     jens <jens.se@icloud.com>
AuthorDate: Sat Aug 22 08:13:02 2026 +0200
Commit:     jens <jens.se@icloud.com>
CommitDate: Sat Aug 22 08:13:02 2026 +0200

    Scale the finale/cast-crawl screens for the larger buffer

    Same root cause as the earlier menu/intermission/status bar fix:
    f_finale.c's text screen, cast-call screen, and end-episode art were
    still laid out in native ORIGWIDTH x ORIGHEIGHT (320x200) terms and
    only filled a corner of the real (now larger) buffer. Applies the same
    scaled-draw approach (V_DrawPatchScaled/V_DrawPatchFlippedScaled) via
    a file-wide macro redirect, plus fixes the handful of SCREENWIDTH-as-
    native-space bugs in F_TextWrite's line wrap and F_CastPrint/
    F_CastDrawer's centering.

    F_BunnyScroll's scrolling mirror effect needed different treatment:
    it was already reading past its source patches' actual (native-width)
    column data once the real screen exceeded that width - not just a
    scaling bug but a latent out-of-bounds read. F_DrawPatchCol now takes
    an explicit scale and maps each real column back to its native source
    column instead of assuming they're the same width.

    Verified via a normal-gameplay smoke test (build is clean, no
    regressions in code paths outside the finale itself) - the finale
    screens themselves need a full DOOM2/Ultimate Doom IWAD to reach
    in-game (cast call needs MAP30, bunny-scroll needs episode 4), which
    isn't available in this environment; verified by careful code review
    instead, following the same pattern already proven correct for the
    menu/intermission/status bar fixes.

    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 TODO.md             |  3 --
 src/doom/f_finale.c | 97 ++++++++++++++++++++++++++++++++++++-----------------
 2 files changed, 67 insertions(+), 33 deletions(-)

diff --git a/TODO.md b/TODO.md
index 1e42f9ff..29c1dac3 100644
--- a/TODO.md
+++ b/TODO.md
@@ -28,9 +28,6 @@ and unstructured wish list of features and improvements. The bug tracker
   - Screensaver mode

 * Infinite splitscreen fork:
-  - Finale/cast-crawl screen (f_finale.c) is still native-resolution and
-    too small, same root cause as the now-fixed menu/intermission/status
-    bar (see m_menu.c, wi_stuff.c, st_stuff.c/st_lib.c, i_video.h).
   - A player dying and respawning resets the whole game for everybody
     instead of just that player.
   - Losing connection or reloading the phone controller page spawns a new
diff --git a/src/doom/f_finale.c b/src/doom/f_finale.c
index 0acd6e39..5ab1c542 100644
--- a/src/doom/f_finale.c
+++ b/src/doom/f_finale.c
@@ -25,6 +25,7 @@
 #include "deh_main.h"
 #include "i_system.h"
 #include "i_swap.h"
+#include "i_video.h"
 #include "z_zone.h"
 #include "v_video.h"
 #include "w_wad.h"
@@ -38,6 +39,23 @@
 #include "doomstat.h"
 #include "r_state.h"

+// The finale screens are laid out in ORIGWIDTH x ORIGHEIGHT (320x200)
+// terms, same as vanilla (see i_video.h for why SCREENWIDTH/SCREENHEIGHT
+// no longer match that). Redirect every V_DrawPatch/V_DrawPatchFlipped
+// call in this file to the scaled variant instead of touching each call
+// site individually. This file's own layout math also uses
+// SCREENWIDTH/SCREENHEIGHT in a few places standing in for
+// ORIGWIDTH/ORIGHEIGHT (F_TextWrite's line-wrap check, F_CastPrint/
+// F_CastDrawer's centering) - those are fixed at each use instead of
+// here. The tiled background fills (F_TextWrite, F_BunnyScroll's
+// scrolling column blit) are raw pixel loops, not patch draws, and
+// are handled separately since they're already resolution-independent
+// or need genuinely different (not just scaled) logic.
+#define V_DrawPatch(x, y, patch) \
+    V_DrawPatchScaled((x), (y), (patch), (SCREENWIDTH / ORIGWIDTH))
+#define V_DrawPatchFlipped(x, y, patch) \
+    V_DrawPatchFlippedScaled((x), (y), (patch), (SCREENWIDTH / ORIGWIDTH))
+
 typedef enum
 {
     F_STAGE_TEXT,
@@ -284,12 +302,12 @@ void F_TextWrite (void)
 	}

 	w = SHORT (hu_font[c]->width);
-	if (cx+w > SCREENWIDTH)
+	if (cx+w > ORIGWIDTH)
 	    break;
 	V_DrawPatch(cx, cy, hu_font[c]);
 	cx+=w;
     }
-
+
 }

 //
@@ -512,7 +530,7 @@ void F_CastPrint (const char *text)
     }

     // draw it
-    cx = SCREENWIDTH/2-width/2;
+    cx = ORIGWIDTH/2-width/2;
     ch = text;
     while (ch)
     {
@@ -559,41 +577,52 @@ void F_CastDrawer (void)

     patch = W_CacheLumpNum (lump+firstspritelump, PU_CACHE);
     if (flip)
-	V_DrawPatchFlipped(SCREENWIDTH/2, 170, patch);
+	V_DrawPatchFlipped(ORIGWIDTH/2, 170, patch);
     else
-	V_DrawPatch(SCREENWIDTH/2, 170, patch);
+	V_DrawPatch(ORIGWIDTH/2, 170, patch);
 }


 //
 // F_DrawPatchCol
+// Draws one column of a native-space (ORIGWIDTH-wide) patch, scaled -
+// each source pixel becomes a scale x scale block, matching
+// V_DrawPatchScaled's approach (see v_video.c) but for one column at a
+// specific real-screen x rather than a whole patch at a scaled
+// position. Used only by F_BunnyScroll's scrolling mirror effect,
+// which needs to pick a different source column per real x rather than
+// draw a whole patch at once.
 //
 void
 F_DrawPatchCol
 ( int		x,
   patch_t*	patch,
-  int		col )
+  int		col,
+  int		scale )
 {
     column_t*	column;
     byte*	source;
     pixel_t*	dest;
-    pixel_t*	desttop;
     int		count;
-
+    int		i;
+    int		dy;
+
+    if (col < 0 || col >= SHORT(patch->width))
+        return;
+
     column = (column_t *)((byte *)patch + LONG(patch->columnofs[col]));
-    desttop = I_VideoBuffer + x;

     // step through the posts in a column
     while (column->topdelta != 0xff )
     {
 	source = (byte *)column + 3;
-	dest = desttop + column->topdelta*SCREENWIDTH;
 	count = column->length;
-
-	while (count--)
+
+	for (i = 0 ; i < count ; i++)
 	{
-	    *dest = *source++;
-	    dest += SCREENWIDTH;
+	    dest = I_VideoBuffer + (column->topdelta + i) * scale * SCREENWIDTH + x;
+	    for (dy = 0 ; dy < scale ; dy++, dest += SCREENWIDTH)
+		*dest = source[i];
 	}
 	column = (column_t *)(  (byte *)column + column->length + 4 );
     }
@@ -607,42 +636,50 @@ void F_BunnyScroll (void)
 {
     signed int  scrolled;
     int		x;
+    int		nativecol;
+    int		scale;
     patch_t*	p1;
     patch_t*	p2;
     char	name[10];
     int		stage;
     static int	laststage;
-
+
+    scale = SCREENWIDTH / ORIGWIDTH;
+
     p1 = W_CacheLumpName (DEH_String("PFUB2"), PU_LEVEL);
     p2 = W_CacheLumpName (DEH_String("PFUB1"), PU_LEVEL);

     V_MarkRect (0, 0, SCREENWIDTH, SCREENHEIGHT);
-
-    scrolled = (SCREENWIDTH - ((signed int) finalecount-230)/2);
-    if (scrolled > SCREENWIDTH)
-	scrolled = SCREENWIDTH;
+
+    // In native (ORIGWIDTH) terms, matching vanilla's timing - p1/p2
+    // are native-width patches, and each real column below maps back
+    // down to a native source column (x / scale) to read from them.
+    scrolled = (ORIGWIDTH - ((signed int) finalecount-230)/2);
+    if (scrolled > ORIGWIDTH)
+	scrolled = ORIGWIDTH;
     if (scrolled < 0)
 	scrolled = 0;
-
+
     for ( x=0 ; x<SCREENWIDTH ; x++)
     {
-	if (x+scrolled < SCREENWIDTH)
-	    F_DrawPatchCol (x, p1, x+scrolled);
+	nativecol = x / scale;
+	if (nativecol+scrolled < ORIGWIDTH)
+	    F_DrawPatchCol (x, p1, nativecol+scrolled, scale);
 	else
-	    F_DrawPatchCol (x, p2, x+scrolled - SCREENWIDTH);
+	    F_DrawPatchCol (x, p2, nativecol+scrolled - ORIGWIDTH, scale);
     }
-
+
     if (finalecount < 1130)
 	return;
     if (finalecount < 1180)
     {
-        V_DrawPatch((SCREENWIDTH - 13 * 8) / 2,
-                    (SCREENHEIGHT - 8 * 8) / 2,
+        V_DrawPatch((ORIGWIDTH - 13 * 8) / 2,
+                    (ORIGHEIGHT - 8 * 8) / 2,
                     W_CacheLumpName(DEH_String("END0"), PU_CACHE));
 	laststage = 0;
 	return;
     }
-
+
     stage = (finalecount-1180) / 5;
     if (stage > 6)
 	stage = 6;
@@ -651,10 +688,10 @@ void F_BunnyScroll (void)
 	S_StartSound (NULL, sfx_pistol);
 	laststage = stage;
     }
-
+
     DEH_snprintf(name, 10, "END%i", stage);
-    V_DrawPatch((SCREENWIDTH - 13 * 8) / 2,
-                (SCREENHEIGHT - 8 * 8) / 2,
+    V_DrawPatch((ORIGWIDTH - 13 * 8) / 2,
+                (ORIGHEIGHT - 8 * 8) / 2,
                 W_CacheLumpName (name,PU_CACHE));
 }