From 2e29af27e6f56a600bf873449150bbbef52a3d38 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:21:12 +0200 Subject: [PATCH 1/6] fix(gamefont): Size the GDI glyph scratch bitmap from the font metrics instead of the point size Create_GDI_Font sized its scratch DIB as a PointSize*2 square, a guess that happens to clear Arial's tmHeight by about a third but is not enforced anywhere. The copy loop in Store_GDI_Char is bounded by the extent GetTextExtentPoint32W reports, not by the bitmap, so a font whose tmHeight exceeds PointSize*2 or a glyph wider than PointSize*2 reads past GDIBitmapBits. Select the font and read its metrics before creating the bitmap, then size the bitmap from tmHeight and tmMaxCharWidth, and clamp the reported glyph extent to it. Both extents are sanity clamped so a malformed font renders clipped instead of allocating an absurd bitmap. For well formed fonts the clamp never engages, so glyph widths and text layout are unchanged. Sizing for the widest glyph the font reports costs some memory: Arial reports a tmMaxCharWidth of about 3.6 times the point size, so the bitmap is roughly 40% larger in area than the old square. There is one such bitmap per font. Also advance CurrPixelOffset by exactly what Update_Current_Buffer reserved and what Blit_Char reads back, and zero any rows GDI did not report, since the glyph blocks are not zero initialized. Co-Authored-By: Claude Opus 5 --- .../Source/WWVegas/WW3D2/render2dsentence.cpp | 98 +++++++++++++------ .../Source/WWVegas/WW3D2/render2dsentence.h | 2 + 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 29d1a22a82d..6f004bcc14e 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -1170,6 +1170,8 @@ FontCharsClass::FontCharsClass () : CurrPixelOffset( 0 ), PointSize( 0 ), CharHeight( 0 ), + GlyphBitmapWidth( 0 ), + GlyphBitmapHeight( 0 ), UnicodeCharArray( nullptr ), FirstUnicodeChar( 0xFFFF ), LastUnicodeChar( 0 ), @@ -1313,8 +1315,8 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i const FontCharsClassCharDataStruct * FontCharsClass::Store_GDI_Char (WCHAR ch) { - int width = PointSize * 2; - int height = PointSize * 2; + const int width = GlyphBitmapWidth; + const int height = GlyphBitmapHeight; // // Draw the character into the memory DC @@ -1332,12 +1334,21 @@ FontCharsClass::Store_GDI_Char (WCHAR ch) SIZE char_size = { 0 }; ::GetTextExtentPoint32W( MemDC, &ch, 1, &char_size ); char_size.cx += PixelOverlap + xOrigin; + + // + // TheSuperHackers @fix ExtTextOutW clipped the glyph to the scratch bitmap, so the copy + // below must not read beyond it either, whatever extent GDI reports. A malformed font can + // report nothing at all, which leaves a character of zero width that everything skips. + // + char_size.cx = min (max ((int)char_size.cx, 0), width); + char_size.cy = min (max ((int)char_size.cy, 0), height); + // // Get a pointer to the surface that this character should use // Update_Current_Buffer( char_size.cx ); - uint16* curr_buffer_p = BufferList[BufferList.Count () - 1].Buffer; - curr_buffer_p += CurrPixelOffset; + uint16* glyph_buffer_p = BufferList[BufferList.Count () - 1].Buffer + CurrPixelOffset; + uint16* curr_buffer_p = glyph_buffer_p; // // Copy the BMP contents to the buffer @@ -1404,13 +1415,21 @@ FontCharsClass::Store_GDI_Char (WCHAR ch) } } + // + // TheSuperHackers @fix Blit_Char always reads CharHeight rows, so any row GDI did not + // report must not be left at whatever the freshly allocated block happened to contain. + // + if (char_size.cy < CharHeight) { + ::memset (curr_buffer_p, 0, (CharHeight - char_size.cy) * char_size.cx * sizeof (uint16)); + } + // // Save information about this character in our list // FontCharsClassCharDataStruct *char_data = W3DNEW FontCharsClassCharDataStruct; char_data->Value = ch; char_data->Width = char_size.cx; - char_data->Buffer = BufferList[BufferList.Count () - 1].Buffer + CurrPixelOffset; + char_data->Buffer = glyph_buffer_p; // // Insert this character into our array @@ -1422,9 +1441,10 @@ FontCharsClass::Store_GDI_Char (WCHAR ch) } // - // Advance the character position + // Advance the character position. This matches both what Update_Current_Buffer reserved and + // what Blit_Char reads back; char_size.cx already includes PixelOverlap. // - CurrPixelOffset += ((char_size.cx+PixelOverlap) * CharHeight); + CurrPixelOffset += (char_size.cx * CharHeight); // // Return the index of the entry we just added @@ -1513,18 +1533,56 @@ FontCharsClass::Create_GDI_Font (const char *font_name) CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, VARIABLE_PITCH, font_name); + // + // Create a device context we can select the font and bitmap into + // + MemDC = ::CreateCompatibleDC (screen_dc); + + // + // TheSuperHackers @fix Select the font and read its metrics before creating the scratch + // bitmap below, because that bitmap is sized from them. The point size alone cannot give a + // safe size: a font is free to report a tmHeight or a tmMaxCharWidth larger than any guess + // made from it, and Store_GDI_Char copies as many rows and columns as GDI reports. + // + OldGDIFont = (HFONT)::SelectObject (MemDC, GDIFont); + + // + // Lookup the pixel height of the font + // + TEXTMETRIC text_metric = { 0 }; + ::GetTextMetrics (MemDC, &text_metric); + CharHeight = text_metric.tmHeight; + CharAscent = text_metric.tmAscent; + CharOverhang = text_metric.tmOverhang; + if (doingGenerals) { + CharOverhang = 0; + } + + // + // The scratch bitmap must hold the widest glyph, the overlap column that Store_GDI_Char + // appends to it and the one pixel it shifts 'W' by. + // + GlyphBitmapWidth = (int)text_metric.tmMaxCharWidth + max ((int)text_metric.tmOverhang, 0) + PixelOverlap + 1; + + // Sanity check. A font reporting absurd metrics renders clipped + // rather than allocating an absurd bitmap and absurd glyph buffers. + const int max_glyph_extent = PointSize * 4 + 8; + GlyphBitmapWidth = min (max (GlyphBitmapWidth, 1), max_glyph_extent); + CharHeight = min (max (CharHeight, 1), max_glyph_extent); + GlyphBitmapHeight = CharHeight; + // // Set-up the fields of the BITMAPINFOHEADER // Note: Top-down DIBs use negative height in Win32. // BITMAPINFOHEADER bitmap_info = { 0 }; bitmap_info.biSize = sizeof (BITMAPINFOHEADER); - bitmap_info.biWidth = PointSize * 2; - bitmap_info.biHeight = -(PointSize * 2); + bitmap_info.biWidth = GlyphBitmapWidth; + bitmap_info.biHeight = -GlyphBitmapHeight; bitmap_info.biPlanes = 1; bitmap_info.biBitCount = 24; bitmap_info.biCompression = BI_RGB; - bitmap_info.biSizeImage = ((PointSize * PointSize * 4) * 3); + bitmap_info.biSizeImage = (((GlyphBitmapWidth * 3) + 3) & ~3) * GlyphBitmapHeight; bitmap_info.biXPelsPerMeter = 0; bitmap_info.biYPelsPerMeter = 0; bitmap_info.biClrUsed = 0; @@ -1540,36 +1598,18 @@ FontCharsClass::Create_GDI_Font (const char *font_name) nullptr, 0L); - // - // Create a device context we can select the font and bitmap into - // - MemDC = ::CreateCompatibleDC (screen_dc); - // // Release our temporary screen DC // ::ReleaseDC ((HWND)WW3D::Get_Window(), screen_dc); // - // Now select the BMP and font into the DC + // Now select the BMP into the DC // OldGDIBitmap = (HBITMAP)::SelectObject (MemDC, GDIBitmap); - OldGDIFont = (HFONT)::SelectObject (MemDC, GDIFont); ::SetBkColor (MemDC, RGB (0, 0, 0)); ::SetTextColor (MemDC, RGB (255, 255, 255)); - // - // Lookup the pixel height of the font - // - TEXTMETRIC text_metric = { 0 }; - ::GetTextMetrics (MemDC, &text_metric); - CharHeight = text_metric.tmHeight; - CharAscent = text_metric.tmAscent; - CharOverhang = text_metric.tmOverhang; - if (doingGenerals) { - CharOverhang = 0; - } - return GDIFont != nullptr && GDIBitmap != nullptr; } diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index a6c19625cfb..50f86e637c3 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -124,6 +124,8 @@ class FontCharsClass : public RefCountClass int CharAscent; int CharOverhang; int PixelOverlap; + int GlyphBitmapWidth; // extents of the GDI scratch bitmap, derived from the font metrics + int GlyphBitmapHeight; int PointSize; StringClass GDIFontName; HFONT OldGDIFont; From 356858712cb5e2378ee34d6eceef14cfd5e3027e Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:22:18 +0200 Subject: [PATCH 2/6] perf(gamefont): Cache one byte of coverage per glyph pixel instead of a full texel Store_GDI_Char composed each cached pixel as (v ? 0x0FFF : 0) | ((v >> 4) << 12) from the 8 bit GDI coverage v, so of the 16 stored bits only the alpha nibble and whether the coverage was non zero carried any information. Store v itself and let Blit_Char rebuild the texel. The reconstruction is exact, including the transparent white pixels that a coverage below one alpha step produces, so rendering is unchanged bit for bit. Blit_Char runs when a sentence is rebuilt rather than per frame, so the added work per pixel does not matter. A glyph block now holds the same number of glyphs in half the bytes, which is what keeps large point sizes affordable. Co-Authored-By: Claude Opus 5 --- .../Source/WWVegas/WW3D2/render2dsentence.cpp | 30 +++++++++---------- .../Source/WWVegas/WW3D2/render2dsentence.h | 6 ++-- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 6f004bcc14e..44b622f16be 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -1286,15 +1286,17 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i // Setup the src and destination pointers // int dest_inc = (dest_stride >> 1); - uint16 *src_ptr = data->Buffer; + const uint8 *src_ptr = data->Buffer; dest_ptr += (dest_inc * y) + x; // - // Simply copy the data from the src buffer to the destination + // Copy the data from the src buffer to the destination, rebuilding the A4R4G4B4 texel + // from the stored coverage value the same way Store_GDI_Char used to compose it. // for ( int row = 0; row < CharHeight; row ++ ) { for ( int col = 0; col < data->Width; col ++ ) { - uint16 curData = *src_ptr; + const uint8 coverage = *src_ptr; + uint16 curData = (coverage != 0 ? 0x0FFF : 0) | ((uint16)(coverage >> 4) << 12); if (col> 4) & 0xF); - *curr_buffer_p++ = pixel_color | (alpha_value << 12); + *curr_buffer_p++ = pixel_value; } } @@ -1420,7 +1418,7 @@ FontCharsClass::Store_GDI_Char (WCHAR ch) // report must not be left at whatever the freshly allocated block happened to contain. // if (char_size.cy < CharHeight) { - ::memset (curr_buffer_p, 0, (CharHeight - char_size.cy) * char_size.cx * sizeof (uint16)); + ::memset (curr_buffer_p, 0, (CharHeight - char_size.cy) * char_size.cx); } // @@ -1484,7 +1482,7 @@ FontCharsClass::Update_Current_Buffer (int char_width) { // TheSuperHackers @fix arcticdolphin 07/09/2026 Length may exceed CHAR_BUFFER_LEN to fit this glyph. const int length = max( (int)CHAR_BUFFER_LEN, char_len ); - BufferList.Add( FontCharsBuffer( length, W3DNEWARRAY uint16[length] ) ); + BufferList.Add( FontCharsBuffer( length, W3DNEWARRAY uint8[length] ) ); CurrPixelOffset = 0; } } diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index 50f86e637c3..40e7697d82d 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -57,7 +57,7 @@ class FontCharsClassCharDataStruct public: WCHAR Value; short Width; - uint16 * Buffer; + uint8 * Buffer; }; enum { CHAR_BUFFER_LEN = 32768 }; @@ -66,13 +66,13 @@ class FontCharsBuffer { public: FontCharsBuffer() : Length( 0 ), Buffer( nullptr ) {} - FontCharsBuffer( int length, uint16 *buffer ) : Length( length ), Buffer( buffer ) {} + FontCharsBuffer( int length, uint8 *buffer ) : Length( length ), Buffer( buffer ) {} bool operator== (const FontCharsBuffer &src) const { return Length == src.Length && Buffer == src.Buffer; } bool operator!= (const FontCharsBuffer &src) const { return !(*this == src); } int Length; - uint16 * Buffer; + uint8 * Buffer; }; From 41261c8c5c3b811af6d7c5dfc3c824ee51e3fbcb Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:23:21 +0200 Subject: [PATCH 3/6] perf(gamefont): Size the glyph cache blocks from the font's own glyph cell The block length was a fixed CHAR_BUFFER_LEN. Above roughly 120 point two glyphs no longer fit into one block, so every glyph got a block of its own, and the partly filled previous block was abandoned each time. Derive the block length from the widest glyph the font can produce: sixteen glyph cells, floored at the byte count that holds as many glyphs as the original block did, and capped so that a very large font does not allocate megabyte blocks. The space abandoned when a glyph does not fit is then at most one glyph at any point size, instead of growing with the font. The first blocks ramp up to a quarter and a half of that length, because a font whose working set is a handful of glyphs would otherwise pay for a whole block of them. For Arial the floor sets the block length up to roughly 19 point, and the cap from roughly 80 point. Co-Authored-By: Claude Opus 5 --- .../Source/WWVegas/WW3D2/render2dsentence.cpp | 25 +++++++++++++++++-- .../Source/WWVegas/WW3D2/render2dsentence.h | 14 ++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 44b622f16be..7cae904320f 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -1172,6 +1172,8 @@ FontCharsClass::FontCharsClass () : CharHeight( 0 ), GlyphBitmapWidth( 0 ), GlyphBitmapHeight( 0 ), + GlyphCellBytes( 0 ), + GlyphBlockBytes( 0 ), UnicodeCharArray( nullptr ), FirstUnicodeChar( 0xFFFF ), LastUnicodeChar( 0 ), @@ -1480,8 +1482,17 @@ FontCharsClass::Update_Current_Buffer (int char_width) // if (needs_new_buffer) { - // TheSuperHackers @fix arcticdolphin 07/09/2026 Length may exceed CHAR_BUFFER_LEN to fit this glyph. - const int length = max( (int)CHAR_BUFFER_LEN, char_len ); + // + // TheSuperHackers @fix Ceil the block size to the char size to make it fit. + // TheSuperHackers @tweak Ramp the first blocks up to the full size, because a font whose + // working set is a handful of glyphs would otherwise pay for a whole block of them. + // Halving rather than one small first block is what keeps such a font from being pushed + // into a full sized second block. + // + const int shift = 2 - min( 2, BufferList.Count() ); + const int length = max( GlyphBlockBytes >> shift, GlyphCellBytes ); + WWASSERT( char_len <= length ); + BufferList.Add( FontCharsBuffer( length, W3DNEWARRAY uint8[length] ) ); CurrPixelOffset = 0; } @@ -1569,6 +1580,16 @@ FontCharsClass::Create_GDI_Font (const char *font_name) CharHeight = min (max (CharHeight, 1), max_glyph_extent); GlyphBitmapHeight = CharHeight; + // + // TheSuperHackers @tweak Size the glyph cache blocks from the widest glyph this font can produce, + // so that a block always holds a whole number of glyphs and the space abandoned when one does not + // fit is at most one glyph. A block always fits at least one glyph, however large the font is. + // + GlyphCellBytes = GlyphBitmapWidth * GlyphBitmapHeight; + GlyphBlockBytes = GlyphCellBytes * GLYPH_BLOCK_TARGET_CELLS; + GlyphBlockBytes = min (max (GlyphBlockBytes, (int)GLYPH_BLOCK_MIN_BYTES), (int)GLYPH_BLOCK_MAX_BYTES); + GlyphBlockBytes = max (GlyphBlockBytes, GlyphCellBytes); + // // Set-up the fields of the BITMAPINFOHEADER // Note: Top-down DIBs use negative height in Win32. diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index 40e7697d82d..d29fdbc5e73 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -60,7 +60,17 @@ class FontCharsClassCharDataStruct uint8 * Buffer; }; -enum { CHAR_BUFFER_LEN = 32768 }; +// TheSuperHackers @tweak Glyph blocks are sized from the font's own glyph cell so that both the +// relative waste and the allocation count stay bounded at any point size. GLYPH_BLOCK_MIN_BYTES +// holds as many glyphs as the original fixed 64KB block did at one texel (2 bytes) per pixel, +// which keeps Arial up to roughly 19 point at the original density. GLYPH_BLOCK_MAX_BYTES +// bounds the allocation count for very large fonts, and only engages for Arial above roughly 80 point. +enum +{ + GLYPH_BLOCK_MIN_BYTES = 32768, + GLYPH_BLOCK_MAX_BYTES = 524288, + GLYPH_BLOCK_TARGET_CELLS = 16 +}; class FontCharsBuffer { @@ -126,6 +136,8 @@ class FontCharsClass : public RefCountClass int PixelOverlap; int GlyphBitmapWidth; // extents of the GDI scratch bitmap, derived from the font metrics int GlyphBitmapHeight; + int GlyphCellBytes; // worst case bytes for one glyph of this font + int GlyphBlockBytes; // size the glyph blocks ramp up to int PointSize; StringClass GDIFontName; HFONT OldGDIFont; From c0a2ad0a3dd7f1628ebe271a405d4f6e2dacad53 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:24:40 +0200 Subject: [PATCH 4/6] fix(gamefont): Let the sentence texture grow beyond 256 pixels for large fonts The texture size search only considered 64, 128 and 256 pixels. Once the character height reaches 256, around 170 point for Arial, no candidate can hold even one row of glyphs, so CurrTextureSize kept its initial value and the assert that the text fits the texture failed. Without the assert the character was blitted past the end of the locked surface. Derive the smallest usable size from the character height and from the widest glyph of the text still to be placed, and search up from there, bounded by the largest texture the device reports. The widest glyph the font can produce is not usable for this: Arial reports a tmMaxCharWidth of about 3.6 times the point size, which would push most fonts at every resolution into a larger texture. Reaching further up is a fix rather than an optimization, since the memory metric in the search rightly prefers small textures: every display string owns its own. When a string runs off the bottom of a texture, pass the text starting at the character that is placed first on the new texture. The character loops have already consumed that character when they allocate, so its width was not considered and the new texture could be too narrow for it. Its spacing is now counted in the search as well, which is the correct count. A font whose text already fit 256 pixels therefore picks the size it picked before, except that a texture opened partway through a string also counts the glyph that starts it. Skip a glyph that still does not fit instead of blitting it out of bounds, keeping the assert for debug builds. Co-Authored-By: Claude Opus 5 --- .../Source/WWVegas/WW3D2/render2dsentence.cpp | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 7cae904320f..022960b2494 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -617,21 +617,55 @@ Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExt } // - // Calculate the width of the text + // Calculate the width of the text and of its widest glyph // int text_width = 0; + int max_char_width = 0; for (int index = 0; text[index] != 0; index ++) { text_width += Font->Get_Char_Spacing (text[index]); + const int char_width = Font->Get_Char_Width (text[index]); + max_char_width = max (max_char_width, char_width); } int char_height = Font->Get_Char_Height (); + // + // TheSuperHackers @fix The texture must hold at least one row of glyphs and the widest glyph + // of this text, otherwise the blit runs off the surface. The widest glyph the font can produce + // is no use here, because fonts such as Arial report one several times wider than their text. + // Fonts that fit search 64 to 256 px, which bounds the texture that every display string + // allocates for itself. Larger fonts use the smallest texture that fits. + // + constexpr const int TextureSizeMinPow2 = 6; // 64 px, smallest texture + constexpr const int TextureSizeSearchMaxPow2 = 8; // 256 px, largest texture searched for fonts that fit + constexpr const int TextureSizeMaxPow2 = 11; // 2048 px, largest texture for any font + + const int min_extent = max (char_height + 1, max_char_width + TEXTURE_OFFSET + 1); + int min_pow2 = TextureSizeMinPow2; + while (min_pow2 < TextureSizeMaxPow2 && (1 << min_pow2) < min_extent) { + min_pow2 ++; + } + + int max_pow2 = max (TextureSizeSearchMaxPow2, min_pow2); + if (max_pow2 > TextureSizeSearchMaxPow2) { + // + // The font does not fit the search range, so use the smallest texture that fits, limited to + // the largest texture the device supports. + // + const D3DCAPS8 &dx8caps = DX8Wrapper::Get_Current_Caps ()->Get_DX8_Caps (); + const int max_device_size = (int)min (dx8caps.MaxTextureWidth, dx8caps.MaxTextureHeight); + while (max_pow2 > TextureSizeMinPow2 && (1 << max_pow2) > max_device_size) { + max_pow2 --; + } + min_pow2 = min (min_pow2, max_pow2); + } + // // Find the best texture size for the remaining text // - CurrTextureSize = 256; + CurrTextureSize = 1 << min_pow2; int best_tex_mem_usage = 999999999; - for (int pow2 = 6; pow2 <= 8; pow2 ++) { + for (int pow2 = min_pow2; pow2 <= max_pow2; pow2 ++) { int size = 1 << pow2; int row_count = (text_width / size) + 1; @@ -844,6 +878,8 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX } for(int i = 0; i <= charCount; i++) { + // TheSuperHackers @fix The text still to be placed, starting at ch, so a new surface is sized for ch as well. + const WCHAR *remaining_text = text; WCHAR ch = *text++; dontBlit = false; // @@ -851,6 +887,7 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX // if(ParseHotKey && (ch == L'&') && (*text != 0) && (*text > L' ') && (*text != L'\n')) { + remaining_text = text; ch = *text++; dontBlit = true; } @@ -892,7 +929,7 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX // Did the text extent completely off the texture? // if ((TextureOffset.J + char_height) >= CurrTextureSize) { - Allocate_New_Surface (text); + Allocate_New_Surface (remaining_text); } } } @@ -912,12 +949,14 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX // // Check to ensure the text will fit on this texture // - WWASSERT (((TextureOffset.I + char_spacing) < CurrTextureSize) && ((TextureOffset.J + char_height) < CurrTextureSize)); + const bool fits_texture = ((TextureOffset.I + char_spacing) < CurrTextureSize) && ((TextureOffset.J + char_height) < CurrTextureSize); + WWASSERT (fits_texture); // // Blit the character to the surface + // TheSuperHackers @fix Skip a glyph that still does not fit. // - if(!dontBlit) + if(!dontBlit && fits_texture) Font->Blit_Char (ch, LockedPtr, LockedStride, TextureOffset.I, TextureOffset.J); if (dontBlit) { @@ -988,6 +1027,8 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i // Loop over all the characters in the string // while (text != nullptr) { + // TheSuperHackers @fix The text still to be placed, starting at ch, so a new surface is sized for ch as well. + const WCHAR *remaining_text = text; WCHAR ch = *text++; dontBlit = false; // @@ -1001,6 +1042,7 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i else hotKeyPosX = Cursor.X + TextureOffset.I -TextureStartX;//TextureOffset.I; + remaining_text = text; ch = *text++; dontBlit = true; } @@ -1081,7 +1123,7 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i // Did the text extent completely off the texture? // if ((TextureOffset.J + char_height) >= CurrTextureSize) { - Allocate_New_Surface (text, justCalcExtents); + Allocate_New_Surface (remaining_text, justCalcExtents); } } } @@ -1102,12 +1144,14 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i // // Check to ensure the text will fit on this texture // - WWASSERT (((TextureOffset.I + char_spacing) < CurrTextureSize) && ((TextureOffset.J + char_height) < CurrTextureSize)); + const bool fits_texture = ((TextureOffset.I + char_spacing) < CurrTextureSize) && ((TextureOffset.J + char_height) < CurrTextureSize); + WWASSERT (fits_texture); // // Blit the character to the surface + // TheSuperHackers @fix Skip a glyph that still does not fit. // - if (!justCalcExtents && !dontBlit ) + if (!justCalcExtents && !dontBlit && fits_texture) { Font->Blit_Char (ch, LockedPtr, LockedStride, TextureOffset.I, TextureOffset.J); } From 59dadae9cc92ac1ddea5792788c9567d9b3b25f0 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:28:56 +0200 Subject: [PATCH 5/6] tweak(gamefont): Discard cached glyphs on map load and on resolution change The asset manager keeps a permanent reference to every FontCharsClass it creates, one per font name, point size and bold flag, and only releases them in Free_Assets when the display shuts down. A resolution or font scale change requests a whole new set of point sizes without retiring the old ones, so glyph caches accumulate for the lifetime of the session. Add FontCharsClass::Free_Glyph_Cache, which drops the glyph blocks and the character arrays but keeps the object, its GDI font and its derived metrics. Every GameFont and fontData pointer held by a display string therefore stays valid, and a glyph that is wanted again is simply rasterized again. Call it for every font from W3DDisplay::reset, which runs on map load and on the way back to the shell, and after a successful mode change in W3DDisplay::setDisplayMode, which is where the previously scaled sizes become dead. Rebuilding is lazy and costs one glyph rasterization each, hidden inside transitions that already take seconds. Co-Authored-By: Claude Opus 5 --- .../Source/WWVegas/WW3D2/render2dsentence.cpp | 25 ++++++++++++++++++- .../Source/WWVegas/WW3D2/render2dsentence.h | 2 ++ .../W3DDevice/GameClient/W3DDisplay.cpp | 10 ++++++++ .../Source/WWVegas/WW3D2/assetmgr.cpp | 11 ++++++++ .../Libraries/Source/WWVegas/WW3D2/assetmgr.h | 5 ++++ .../W3DDevice/GameClient/W3DDisplay.cpp | 10 ++++++++ .../Source/WWVegas/WW3D2/assetmgr.cpp | 11 ++++++++ .../Libraries/Source/WWVegas/WW3D2/assetmgr.h | 5 ++++ 8 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 022960b2494..6e68c6fe341 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -1234,14 +1234,37 @@ FontCharsClass::FontCharsClass () : // //////////////////////////////////////////////////////////////////////////////////// FontCharsClass::~FontCharsClass () +{ + Free_Glyph_Cache(); + Free_GDI_Font(); +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Free_Glyph_Cache +// Discards the cached glyphs but keeps the font itself, so that the pointers other +// objects hold to this font stay valid and glyphs are rebuilt on demand. +// +//////////////////////////////////////////////////////////////////////////////////// +void +FontCharsClass::Free_Glyph_Cache () { while ( BufferList.Count() ) { delete [] BufferList[0].Buffer; BufferList.Delete(0); } - Free_GDI_Font(); Free_Character_Arrays(); + + // + // The character arrays are gone, so the unicode range has to start over as well. + // The GDI font and the derived metrics are deliberately kept, so Store_GDI_Char + // can rebuild any glyph that is asked for again without recreating this object. + // + CurrPixelOffset = 0; + FirstUnicodeChar = 0xFFFF; + LastUnicodeChar = 0; } diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index d29fdbc5e73..28c28050cb7 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -110,6 +110,8 @@ class FontCharsClass : public RefCountClass void Blit_Char( WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, int y ); + void Free_Glyph_Cache(); + private: // diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 03d3bfe5bff..984ea234e6e 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -514,6 +514,12 @@ Bool W3DDisplay::setDisplayMode( UnsignedInt xres, UnsignedInt yres, UnsignedInt { Render2DClass::Set_Screen_Resolution(RectClass(0, 0, xres, yres)); Display::setDisplayMode(xres, yres, bitdepth, windowed); + + // TheSuperHackers @tweak Font point sizes are scaled from the resolution, so every glyph + // cached for the old resolution is now dead weight. Discard the current glyphs and start + // with a clean slate. + WW3DAssetManager::Get_Instance()->Free_All_FontChars_Glyph_Caches(); + return TRUE; } @@ -932,6 +938,10 @@ void W3DDisplay::reset() Display::reset(); + // TheSuperHackers @tweak Discard the current glyphs and start with a clean slate. + // This can reduce the memory overhead from glyphs that are no longer needed from here on. + WW3DAssetManager::Get_Instance()->Free_All_FontChars_Glyph_Caches(); + // Remove all render objects. if (m_3DScene != nullptr) diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp index a9dac652860..968a4385943 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp @@ -1491,6 +1491,17 @@ void WW3DAssetManager::Release_All_FontChars() } } + +/*********************************************************************************************** + * WW3DAssetManager::Free_All_FontChars_Glyph_Caches -- Discards all cached glyphs * + *=============================================================================================*/ +void WW3DAssetManager::Free_All_FontChars_Glyph_Caches() +{ + for ( int i = 0; i < FontCharsList.Count(); i++ ) { + FontCharsList[i]->Free_Glyph_Cache(); + } +} + /*********************************************************************************************** * WW3DAssetManager::Register_Prototype_Loader -- add a new loader to the system * * * diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h b/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h index 68ea6722627..6728d4e1c30 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h @@ -287,6 +287,11 @@ class WW3DAssetManager */ virtual FontCharsClass * Get_FontChars( const char * name, int point_size, bool is_bold = false ); + /* + ** Discard the cached glyphs of every font without destroying the fonts. + */ + virtual void Free_All_FontChars_Glyph_Caches(); + /* ** Access to HTrees, Used by Animatable3DObj's */ diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 155e8ac43ec..ec0fd111d01 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -564,6 +564,12 @@ Bool W3DDisplay::setDisplayMode( UnsignedInt xres, UnsignedInt yres, UnsignedInt { Render2DClass::Set_Screen_Resolution(RectClass(0, 0, xres, yres)); Display::setDisplayMode(xres, yres, bitdepth, windowed); + + // TheSuperHackers @tweak Font point sizes are scaled from the resolution, so every glyph + // cached for the old resolution is now dead weight. Discard the current glyphs and start + // with a clean slate. + WW3DAssetManager::Get_Instance()->Free_All_FontChars_Glyph_Caches(); + return TRUE; } @@ -982,6 +988,10 @@ void W3DDisplay::reset() Display::reset(); + // TheSuperHackers @tweak Discard the current glyphs and start with a clean slate. + // This can reduce the memory overhead from glyphs that are no longer needed from here on. + WW3DAssetManager::Get_Instance()->Free_All_FontChars_Glyph_Caches(); + // Remove all render objects. if (m_3DScene != nullptr) diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp index ce6f67a0324..72197ef8b6a 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp @@ -1496,6 +1496,17 @@ void WW3DAssetManager::Release_All_FontChars() } } + +/*********************************************************************************************** + * WW3DAssetManager::Free_All_FontChars_Glyph_Caches -- Discards all cached glyphs * + *=============================================================================================*/ +void WW3DAssetManager::Free_All_FontChars_Glyph_Caches() +{ + for ( int i = 0; i < FontCharsList.Count(); i++ ) { + FontCharsList[i]->Free_Glyph_Cache(); + } +} + /*********************************************************************************************** * WW3DAssetManager::Register_Prototype_Loader -- add a new loader to the system * * * diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h index 757afefbd31..5b37664f3bc 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h @@ -287,6 +287,11 @@ class WW3DAssetManager */ virtual FontCharsClass * Get_FontChars( const char * name, int point_size, bool is_bold = false ); + /* + ** Discard the cached glyphs of every font without destroying the fonts. + */ + virtual void Free_All_FontChars_Glyph_Caches(); + /* ** Access to HTrees, Used by Animatable3DObj's */ From aaa806cc497ff949de44495bd4c19863f18bc691 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:29:38 +0200 Subject: [PATCH 6/6] chore(gamememory): Remove the stale FontCharsBuffer memory pool entry FontCharsBuffer stopped being a memory pool object in #3268, which dropped its W3DMPO_CODE, so no pool by that name is ever created and the entry in the pool size table can never be matched. Removing it changes nothing at runtime. Co-Authored-By: Claude Opus 5 --- .../Source/Common/System/GameMemoryInitPools_Generals.inl | 1 - .../Source/Common/System/GameMemoryInitPools_GeneralsMD.inl | 1 - 2 files changed, 2 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_Generals.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_Generals.inl index 36080f71985..b80863fe9a1 100644 --- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_Generals.inl +++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_Generals.inl @@ -640,7 +640,6 @@ static PoolSizeRec PoolSizes[] = { "Render2DClass", 64, 32 }, { "SurfaceClass", 128, 32 }, { "FontCharsClassCharDataStruct", 1024, 32 }, - { "FontCharsBuffer", 16, 4 }, { "FVFInfoClass", 128, 32 }, { "TerrainTracksRenderObjClass", 128, 32 }, { "DynamicIBAccessClass", 32, 32 }, diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl index f33873f3fa1..f9e446fc6aa 100644 --- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl +++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl @@ -636,7 +636,6 @@ static PoolSizeRec PoolSizes[] = { "Render2DClass", 64, 32 }, { "SurfaceClass", 128, 32 }, { "FontCharsClassCharDataStruct", 1024, 32 }, - { "FontCharsBuffer", 16, 4 }, { "FVFInfoClass", 152, 64 }, { "TerrainTracksRenderObjClass", 128, 32 }, { "DynamicIBAccessClass", 32, 32 },