Skip to content

fix(gamefont): Fix and improve memory allocation strategies for game fonts - #3288

Open
xezon wants to merge 6 commits into
TheSuperHackers:mainfrom
xezon:xezon/tweak-fontbuffer-scale
Open

fix(gamefont): Fix and improve memory allocation strategies for game fonts#3288
xezon wants to merge 6 commits into
TheSuperHackers:mainfrom
xezon:xezon/tweak-fontbuffer-scale

Conversation

@xezon

@xezon xezon commented Sep 13, 2026

Copy link
Copy Markdown

Merge with Rebase

This change applies further fixes and improvements for the memory allocations of game fonts. It solves inefficient allocations patterns with large font sizes and also reduces the memory footprint for the game runtime.

The changes were mainly implemented with Claude Opus 5 and went through human review and local AI review.

Summary

FontCharsClass caches the rasterized GDI glyphs for all 2D text, and Render2DSentenceClass packs those glyphs into textures. The sizes of the glyph buffers, the GDI scratch bitmap and the sentence textures were all fixed for the 8-20 pt fonts of an 800x600 game.

Font sizes scale with the resolution. With the default ClassicNoCeiling method and scaler 0.7, fonts are scaled by 1.98 at 1920x1080 and by 3.66 at 3840x2160, and FontLibrary::getFont allows up to 512 pt. At those sizes the fixed sizes fail in several ways:

  • Above roughly 120 pt two glyphs no longer fit into one 64 KB glyph block, so every glyph gets its own heap allocation and the rest of the previous block is abandoned.
  • The glyph copy in Store_GDI_Char is bounded by the extent GDI reports, not by the scratch bitmap it reads from, so a tall or wide glyph reads past the bitmap.
  • Once the character height reaches 256 px (around 170 pt for Arial) no sentence texture size can hold a row of glyphs, and the glyph is blitted past the end of the locked surface.
  • Glyph caches are never freed before shutdown, so every resolution change adds a complete new set of font sizes.

This PR makes the font code correct and bounded across the whole 1-512 pt range. Rendered output stays the same, and the fonts used at 800x600 keep their existing texture sizes, apart from also counting the first glyph of a texture opened partway through a string. It is split into six commits that can be reviewed independently.

Changes

1. Size the GDI scratch bitmap from the font metrics

Problem. Create_GDI_Font created the scratch DIB as a PointSize * 2 square before it knew anything about the font. Store_GDI_Char then copied as many rows and columns as GetTextExtentPoint32W reported. Nothing guaranteed that this extent fit inside the bitmap, so a font whose tmHeight exceeds PointSize * 2, or a glyph wider than that, read past GDIBitmapBits.

Change.

  • The font is selected and its metrics are read before the bitmap is created. The bitmap is then sized from them: tmHeight rows, and tmMaxCharWidth + tmOverhang + PixelOverlap + 1 columns, which covers the overlap column and the one-pixel shift applied to 'W'.
  • The glyph extent GDI reports is clamped to the bitmap, so the copy can never read beyond it.
  • Both extents are sanity-clamped to 4 * PointSize + 8, so a font with absurd metrics renders clipped instead of allocating an absurd bitmap.
  • Rows that GDI did not report are zeroed. Blit_Char always reads CharHeight rows, and the glyph blocks are not zero-initialized.
  • CurrPixelOffset now advances by cx * CharHeight. That is exactly what Update_Current_Buffer reserves and what Blit_Char reads back; the old code added PixelOverlap a second time.

For well-formed fonts the clamps never engage, so glyph widths and text layout are unchanged.

Arial reports a tmMaxCharWidth of about 3.6 times the point size, so the scratch bitmap is roughly 40% larger in area than the old square (46x18 instead of 24x24 at 12 pt). There is one such bitmap per font object; at 3840x2160 it costs a few tens of KB per font, which is the price of a copy that is safe for any glyph.

2. Cache one byte of coverage per glyph pixel

Problem. Each cached pixel was a 16-bit A4R4G4B4 word, composed as (v ? 0x0FFF : 0) | ((v >> 4) << 12) from the 8-bit GDI coverage v. Only the alpha nibble and whether v is zero carried any information.

Change. The glyph buffers store v itself, and Blit_Char rebuilds the texel with the same formula. The rebuild is exact, including the transparent white texels produced by coverage below one alpha step, so the textures contain the same bytes as before. Blit_Char runs when a sentence is rebuilt, not every frame. Every glyph now takes half the memory.

3. Size the glyph cache blocks from the font's own glyph cell

Problem. Blocks had a fixed size of 32768 uint16. Small fonts filled them well, but above roughly 120 pt two glyphs no longer fit into one block, so each glyph gets a block of its own and the rest of the previous block is abandoned. From about 126 pt the widest glyphs exceed a block entirely and, since #3268, get an exactly sized block. Large fonts therefore make one heap allocation per glyph.

Change. A block is sized from the worst-case glyph cell of its font, GlyphBitmapWidth * CharHeight:

  • The final block size is 16 cells, clamped to 32 KB..512 KB and never smaller than one cell.
  • The first block of a font is a quarter of that size, the second half, and every later block full size.

The block size balances three costs:

Cost Wants Bounded by
Allocation overhead: blocks bypass the DMA pools and cost a MemoryPoolSingleBlock header plus a GlobalAlloc each Large blocks 16-cell target
Tail waste: the space abandoned when the next glyph does not fit Large blocks At most one cell per block, at any size
Over-allocation: the newest block is allocated in full even if the font never fills it Small blocks Ramp; a font that uses few glyphs only pays for a quarter block
  • The 32 KB floor holds the same number of glyphs as the old 64 KB block did at two bytes per pixel, so small fonts keep their density. For Arial it applies up to about 19 pt.
  • The 512 KB cap bounds the size of a single allocation for very large fonts. For Arial it applies from about 80 pt.

4. Let the sentence texture grow beyond 256 pixels for large fonts

Problem. Allocate_New_Surface only considered 64, 128 and 256 px textures. Once the character height reaches 256 px (around 170 pt for Arial), none of them holds a row of glyphs. CurrTextureSize then kept its initial value of 256 and the assert that the text fits the texture failed. In a release build the glyph was blitted past the end of the locked surface.

Change.

  • The smallest usable texture is derived from the character height and the widest glyph of the text still to be placed. It deliberately does not use the widest glyph the font can produce: Arial reports a tmMaxCharWidth of about 3.6 times the point size, which would move most fonts at every resolution into a larger texture.
  • When a string runs off the bottom of a texture, the new texture is sized from the text starting at the character that is placed first on it. The character loops consume that character before they allocate, so previously neither its width nor its spacing was counted, and the new texture could be too narrow for it.
  • Fonts that fit are searched over 64-256 px as before and pick the same size, except that a texture opened partway through a string now also counts the glyph that starts it.
  • Larger fonts use the smallest texture that fits, up to 2048 px and never beyond what the device reports in MaxTextureWidth/MaxTextureHeight.
  • A glyph that still does not fit is skipped instead of being blitted out of bounds; the assert remains for debug builds. This can also happen when a sentence is rebuilt onto a texture that was sized for an earlier text before being rendered, as W3DGameWindow::winSetText can do, if the new text has a wider glyph.

Textures stay square, because Build_Textures and Draw_Sentence rely on that.

5. Discard cached glyphs on map load and on resolution change

Problem. WW3DAssetManager keeps a permanent reference to every FontCharsClass, one per font name, point size and bold flag, and releases them only 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 whole session. Each UI font can also create up to four font objects: regular, bold for hotkeys, and the same-size Unicode alternate of each.

Change.

  • FontCharsClass::Free_Glyph_Cache frees the glyph blocks and character arrays but keeps the object, its GDI font and its metrics. Every GameFont and fontData pointer held by display strings stays valid, and a glyph that is needed again is simply rasterized again.
  • WW3DAssetManager::Free_All_FontChars_Glyph_Caches applies this to every font.
  • It is called from W3DDisplay::reset, which runs on map load and on the return to the shell, and after a successful mode change in W3DDisplay::setDisplayMode, the point where the previously scaled sizes become dead.

Rebuilding costs one glyph rasterization per glyph that is used again, during transitions that already take seconds.

6. Remove the stale FontCharsBuffer memory pool entry

FontCharsBuffer stopped being a memory pool object in #3268, so no pool by that name is ever created and its entry in the pool size tables can never match. Removing it changes nothing at runtime.

Memory

The numbers below replay the old and the new allocation logic, as written, against real GDI measurements: Arial at 96 DPI, measured with GetTextMetrics and GetTextExtentPoint32W. Point sizes use the default ClassicNoCeiling scaling with scaler 0.7.

The absolute amounts are modest; the main value of this PR is correctness and bounded behaviour at large sizes. The glyph cache still shrinks at every resolution.

A typical set of UI fonts

This is an illustrative working set, not a capture from the game. Glyphs are cached on demand, so only the glyphs a font actually draws count.

Font Base size Glyphs used
UI text 12 95 (printable ASCII)
UI text bold (hotkeys) 12 bold 26
Tooltips 10 80
Headers 16 bold 40
Counters (money, timers) 14 12
Captions 24 bold 30

Each font also creates a same-size Unicode alternate that draws no glyphs for English text but still owns a scratch bitmap; those bitmaps are included.

Resolution Scale Glyph blocks old → new Scratch bitmaps old → new Total old → new
800x600 1.00 384 KB → 127 KB (−67%) 33 KB → 48 KB 417 KB → 175 KB (−58%)
1920x1080 1.98 832 KB → 400 KB (−52%) 126 KB → 177 KB 958 KB → 577 KB (−40%)
2560x1440 2.54 1280 KB → 660 KB (−48%) 208 KB → 291 KB 1488 KB → 950 KB (−36%)
3840x2160 3.66 2432 KB → 1254 KB (−48%) 436 KB → 599 KB 2868 KB → 1853 KB (−35%)

At 3840x2160 the glyph blocks also drop from 38 allocations to 13. The game uses more fonts and sizes than this set, so real totals scale up accordingly.

Large fonts

A 30-glyph caption in bold, glyph blocks only:

Size Old New
64 pt 448 KB in 7 blocks 259 KB in 2 blocks
96 pt 896 KB in 14 blocks 896 KB in 3 blocks
128 pt 1792 KB in 28 blocks 896 KB in 3 blocks
160 pt 2156 KB in 30 blocks 1414 KB in 4 blocks
256 pt 5133 KB in 30 blocks 3231 KB in 7 blocks

From about 120 pt the old code allocates one block per glyph.

Changing resolution

Switching 1920x1080 → 2560x1440 → 3840x2160 in the options menu without loading a map:

  • Old: all three glyph sets stay cached, 5.2 MB in total.
  • New: only the glyphs of the current resolution remain, 2.3 MB in total. The scratch bitmaps of the font objects from earlier resolutions remain in both cases.

Testing

  • Builds with the vc6 presets and a modern MSVC preset for Generals and Zero Hour
  • 800x600: shell, in-game UI, tooltips and credits look identical to main
  • 1920x1080 and 3840x2160: text renders correctly, no asserts
  • Very large sizes (a high ResolutionFontAdjustment in options.ini, or the script action that sets a font size): glyphs above 170 pt render, or are skipped, without memory corruption
  • Load a map, return to the shell and change resolution several times: text keeps drawing correctly and memory does not keep growing

xezon and others added 6 commits September 13, 2026 15:20
…s 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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…rge 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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@xezon xezon added Major Severity: Minor < Major < Critical < Blocker Performance Is a performance concern Gen Relates to Generals ZH Relates to Zero Hour Memory Is memory related Fix Is fixing something, but is not user facing labels Sep 13, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix and bound game font memory allocation

🐞 Bug fix ✨ Enhancement ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Bounds glyph rasterization and sentence texture writes for the full supported font range.
• Halves glyph pixel storage and dynamically sizes cache blocks to reduce allocation waste.
• Clears stale glyph caches after display resets and successful resolution changes.
Diagram

graph TD
  Mode["Mode Change"] --> Display["W3D Display"] --> Manager["Asset Manager"] --> Cache["Glyph Cache"] --> Raster["GDI Rasterizer"] --> Texture["Sentence Texture"]
  Reset["Display Reset"] --> Display
Loading
High-Level Assessment

The proposed approach is appropriate: it preserves stable FontCharsClass pointers while releasing only rebuildable glyph data, derives allocations from actual font metrics, and enforces device texture limits. Recreating font objects would risk invalidating retained pointers, while globally oversized fixed buffers would preserve the allocation waste this change is intended to remove.

Files changed (10) +252 / -60

Enhancement (5) +52 / -4
render2dsentence.hDefine compact, dynamically sized glyph cache structures +20/-4

Define compact, dynamically sized glyph cache structures

• Changes cached glyph storage from 16-bit texels to 8-bit coverage values. Declares cache sizing limits, metric-derived allocation fields, and the glyph-cache cleanup API.

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h

assetmgr.cppImplement global Generals glyph cache cleanup +11/-0

Implement global Generals glyph cache cleanup

• Adds an asset-manager operation that clears cached glyph data from every retained font object without destroying those objects.

Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp

assetmgr.hExpose Generals glyph cache cleanup API +5/-0

Expose Generals glyph cache cleanup API

• Declares the asset-manager method used to discard all font glyph caches while preserving font instances.

Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h

assetmgr.cppImplement global Zero Hour glyph cache cleanup +11/-0

Implement global Zero Hour glyph cache cleanup

• Adds the GeneralsMD asset-manager implementation for clearing every retained font object's glyph cache.

GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp

assetmgr.hExpose Zero Hour glyph cache cleanup API +5/-0

Expose Zero Hour glyph cache cleanup API

• Declares the GeneralsMD asset-manager method for releasing cached glyph data without invalidating font pointers.

GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.h

Bug fix (3) +200 / -54
render2dsentence.cppBound font rasterization, caching, and texture allocation +180/-54

Bound font rasterization, caching, and texture allocation

• Sizes and clamps the GDI scratch bitmap from font metrics, stores one-byte glyph coverage, and allocates ramped cache blocks based on glyph cells. Sentence textures can grow within device limits, include the first remaining glyph when resized, and skip glyphs that cannot safely fit. Adds reusable glyph-cache cleanup while retaining font resources and metrics.

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp

W3DDisplay.cppClear Generals glyph caches during display transitions +10/-0

Clear Generals glyph caches during display transitions

• Discards cached glyphs after successful display mode changes and during display resets, preventing resolution-specific caches from accumulating.

Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

W3DDisplay.cppClear Zero Hour glyph caches during display transitions +10/-0

Clear Zero Hour glyph caches during display transitions

• Discards cached glyphs after successful resolution changes and during display resets to bound cache growth across transitions.

GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

Other (2) +0 / -2
GameMemoryInitPools_Generals.inlRemove obsolete Generals font buffer pool entry +0/-1

Remove obsolete Generals font buffer pool entry

• Removes the stale FontCharsBuffer pool sizing record because glyph buffers no longer use the memory-pool object.

Core/GameEngine/Source/Common/System/GameMemoryInitPools_Generals.inl

GameMemoryInitPools_GeneralsMD.inlRemove obsolete Zero Hour font buffer pool entry +0/-1

Remove obsolete Zero Hour font buffer pool entry

• Removes the unused FontCharsBuffer pool sizing record from the GeneralsMD memory configuration.

Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Right-edge glyphs corrupt textures 🐞 Bug ≡ Correctness
Description
fits_texture checks char_spacing, while FontCharsClass::Blit_Char writes the larger
data->Width column count. This occurs when packing places a glyph within the final overlap or
overhang columns of a row, allowing the guarded blit to write beyond the locked surface in both
sentence builders.
Code

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[R1147-1148]

+			const bool fits_texture = ((TextureOffset.I + char_spacing) < CurrTextureSize) && ((TextureOffset.J + char_height) < CurrTextureSize);
+			WWASSERT (fits_texture);
Evidence
Both builders' new guards compare the destination position with char_spacing, but
Get_Char_Spacing subtracts PixelOverlap and CharOverhang from the cached width. Blit_Char
nevertheless writes every one of data->Width columns, so the guard can succeed when the actual
write extends past the row boundary.

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[894-960]
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1049-1156]
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1324-1339]
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1349-1376]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The sentence builders determine whether a glyph fits using character spacing, but the blitter writes the glyph's full width. A glyph near the right texture edge can therefore pass the new safety check and write beyond the locked surface.

## Fix Focus Areas
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[894-960]
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1049-1156]

## Recommended Fix
Fetch the current glyph width and use `TextureOffset.I + char_width` for row-overflow detection and final fit validation in both centered and non-centered builders. Continue advancing `TextureOffset.I` and layout coordinates by `char_spacing` so overlap and text layout remain unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Large-font text vanishes on older cards 🐞 Bug ≡ Correctness
Description
Allocate_New_Surface lowers min_pow2 to the device limit even when that size is smaller than the
measured glyph extent. The subsequent fits_texture check then fails and suppresses Blit_Char
while still advancing the layout, so large characters disappear on devices whose maximum texture is
below the required glyph size.
Code

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[R655-660]

+		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);
Evidence
The new sizing path explicitly reduces the selected texture exponent to the device's smallest
maximum dimension. The changed packing code then only blits when the measured spacing and height
fit, but always advances TextureOffset.I; SurfaceClass passes the requested size directly to
CreateImageSurface, so there is no later resize that could make the oversized glyph fit.

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[643-660]
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1147-1158]
Core/Libraries/Source/WWVegas/WW3D2/surfaceclass.cpp[163-170]
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp[2848-2860]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
`Allocate_New_Surface` can select a texture smaller than the widest glyph after clamping to `MaxTextureWidth`/`MaxTextureHeight`; later packing recognizes that it does not fit and skips the glyph.

Fix Focus Areas
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[643-660]
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1147-1158]

Recommended Fix
Do not treat a device-limited texture as sufficient for a glyph that exceeds it. Add an explicit oversized-glyph path that preserves visible output and consistent layout, such as choosing a supported fallback font/size before building the sentence or implementing clipped/tiled glyph rendering; do not silently skip the blit after advancing the cursor.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a substantial memory-management and rendering change spanning multiple independent code paths, with platform-specific duplication and several subtle bounds, cache-lifetime, texture-sizing, and allocation invariants that merit redundant review.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1147 to +1148
const bool fits_texture = ((TextureOffset.I + char_spacing) < CurrTextureSize) && ((TextureOffset.J + char_height) < CurrTextureSize);
WWASSERT (fits_texture);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Right-edge glyphs corrupt textures 🐞 Bug ≡ Correctness

fits_texture checks char_spacing, while FontCharsClass::Blit_Char writes the larger
data->Width column count. This occurs when packing places a glyph within the final overlap or
overhang columns of a row, allowing the guarded blit to write beyond the locked surface in both
sentence builders.
Agent Prompt
## Issue description
The sentence builders determine whether a glyph fits using character spacing, but the blitter writes the glyph's full width. A glyph near the right texture edge can therefore pass the new safety check and write beyond the locked surface.

## Fix Focus Areas
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[894-960]
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1049-1156]

## Recommended Fix
Fetch the current glyph width and use `TextureOffset.I + char_width` for row-overflow detection and final fit validation in both centered and non-centered builders. Continue advancing `TextureOffset.I` and layout coordinates by `char_spacing` so overlap and text layout remain unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +655 to +660
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Large-font text vanishes on older cards 🐞 Bug ≡ Correctness

Allocate_New_Surface lowers min_pow2 to the device limit even when that size is smaller than the
measured glyph extent. The subsequent fits_texture check then fails and suppresses Blit_Char
while still advancing the layout, so large characters disappear on devices whose maximum texture is
below the required glyph size.
Agent Prompt
Issue description
`Allocate_New_Surface` can select a texture smaller than the widest glyph after clamping to `MaxTextureWidth`/`MaxTextureHeight`; later packing recognizes that it does not fit and skips the glyph.

Fix Focus Areas
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[643-660]
- Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp[1147-1158]

Recommended Fix
Do not treat a device-limited texture as sufficient for a glyph that exceeds it. Add an explicit oversized-glyph path that preserves visible output and consistent layout, such as choosing a supported fallback font/size before building the sentence or implementing clipped/tiled glyph rendering; do not silently skip the blit after advancing the cursor.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@greptile-apps

greptile-apps Bot commented Sep 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR reworks font glyph caching and sentence-texture allocation to reduce memory use and prevent large-font rendering overruns, and clears cached glyphs during display lifecycle transitions.

  • Stores one byte of GDI coverage per cached glyph pixel and sizes cache blocks from font metrics.
  • Sizes GDI scratch bitmaps and sentence textures dynamically, with device-capability bounds.
  • Adds reusable glyph-cache cleanup through both game variants' asset managers and display reset paths.
  • Removes obsolete memory-pool configuration from both variants.
  • One remaining width-versus-spacing mismatch leaves the intended texture-overrun protection incomplete.

Confidence Score: 4/5

The PR is not yet safe to merge because glyphs near a texture's right edge can still overrun the row despite the newly added fit guard.

The fit checks use character advance while the blitter writes the larger full glyph width, leaving the central large-font bounds fix incomplete in both sentence-building paths.

Files Needing Attention: Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp

Important Files Changed

Filename Overview
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp Implements dynamic scratch/cache/texture sizing and cache cleanup, but the new fit guard validates spacing rather than the full glyph blit width.
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Changes glyph storage to byte coverage and adds metric-derived cache sizing state.
Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Clears font glyph caches after successful mode changes and during display reset.
GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Mirrors the display-lifecycle cache cleanup for Zero Hour.
Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp Adds iteration over retained font objects to discard their glyph caches.
GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp Mirrors the asset-manager glyph-cache cleanup for Zero Hour.
Core/GameEngine/Source/Common/System/GameMemoryInitPools_Generals.inl Removes the obsolete FontCharsBuffer pool-size entry.
Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl Removes the corresponding obsolete pool-size entry for Zero Hour.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Metrics[GDI font metrics] --> Scratch[Bounded scratch bitmap]
  Scratch --> Rasterize[Rasterize glyph coverage]
  Rasterize --> Cache[Byte-based glyph cache blocks]
  Cache --> Sentence[Build sentence texture]
  Device[Device texture limits] --> Sentence
  Sentence --> Blit[Rebuild A4R4G4B4 texels and blit]
  Reset[Map reset or display-mode change] --> Clear[Clear glyph caches]
  Clear --> Rasterize
Loading
Prompt To Fix All With AI
### Issue 1
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp:952
**Glyph fit check is narrow**

When a glyph is near the texture's right edge, this check can pass even though the glyph does not fit. It checks `char_spacing`, which excludes `PixelOverlap` and `CharOverhang`, while `Blit_Char` writes the full glyph width. The glyph can therefore write one or more columns beyond the texture row instead of being skipped. Validate the full glyph width here and in the equivalent non-centered path at line 1147.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "chore(gamememory): Remove the stale Font..." | Re-trigger Greptile

// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Glyph fit check is narrow

When a glyph is near the texture's right edge, this check can pass even though the glyph does not fit. It checks char_spacing, which excludes PixelOverlap and CharOverhang, while Blit_Char writes the full glyph width. The glyph can therefore write one or more columns beyond the texture row instead of being skipped. Validate the full glyph width here and in the equivalent non-centered path at line 1147.

Knowledge Base Used: WWVegas services

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
Line: 952

Comment:
**Glyph fit check is narrow**

When a glyph is near the texture's right edge, this check can pass even though the glyph does not fit. It checks `char_spacing`, which excludes `PixelOverlap` and `CharOverhang`, while `Blit_Char` writes the full glyph width. The glyph can therefore write one or more columns beyond the texture row instead of being skipped. Validate the full glyph width here and in the equivalent non-centered path at line 1147.

**Knowledge Base Used:** [WWVegas services](https://app.greptile.com/thesuperhackers/-/custom-context/knowledge-base/thesuperhackers/generalsgamecode/-/docs/wwvegas-services.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix Is fixing something, but is not user facing Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker Memory Is memory related Performance Is a performance concern ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant