Conversation
PR Summary by QodoFix and bound game font memory allocation
AI Description
Diagram
High-Level Assessment
Files changed (10)
|
Code Review by Qodo
1. Right-edge glyphs corrupt textures
|
|
| Filename | Overview |
|---|---|
| Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp | Adds bounded GDI glyph rasterization, adaptive cache allocation, exact texture-fit checks, and safe large-font sentence textures. |
| Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h | Changes cached glyph pixels to byte coverage and defines metric-derived block sizing and cache-reset state. |
| Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp | Clears reusable glyph caches after successful mode changes and display resets. |
| GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp | Mirrors glyph-cache reclamation for the Zero Hour display implementation. |
| Generals/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp | Adds asset-manager-wide glyph-cache clearing without destroying retained font objects. |
| GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/assetmgr.cpp | Mirrors the font-cache clearing operation 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 Zero Hour pool-size entry. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
GDI[GDI font metrics] --> Scratch[Bounded scratch bitmap]
Scratch --> Raster[Rasterize and clamp glyph]
Raster --> Cache[Byte coverage glyph cache]
Cache --> Build[Sentence builder]
Build --> Fit{Glyph fits texture?}
Fit -->|Yes| Blit[Rebuild A4R4G4B4 texels]
Fit -->|No| Skip[Skip unsafe blit]
Blit --> Surface[Sentence texture]
Reset[Map/display reset] --> Clear[Clear glyph caches]
Clear --> Raster
Reviews (2): Last reviewed commit: "chore(gamememory): Remove the stale Font..." | Re-trigger Greptile
…s instead of the point size (#3288) 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 (#3288) 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 (#3288) 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 (#3288) 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 (#3288) 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>
… (#3288) 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>
aaa806c to
5574f3b
Compare
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
FontCharsClasscaches the rasterized GDI glyphs for all 2D text, andRender2DSentenceClasspacks 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
ClassicNoCeilingmethod and scaler 0.7, fonts are scaled by 1.98 at 1920x1080 and by 3.66 at 3840x2160, andFontLibrary::getFontallows up to 512 pt. At those sizes the fixed sizes fail in several ways:Store_GDI_Charis bounded by the extent GDI reports, not by the scratch bitmap it reads from, so a tall or wide glyph reads past the bitmap.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_Fontcreated the scratch DIB as aPointSize * 2square before it knew anything about the font.Store_GDI_Charthen copied as many rows and columns asGetTextExtentPoint32Wreported. Nothing guaranteed that this extent fit inside the bitmap, so a font whosetmHeightexceedsPointSize * 2, or a glyph wider than that, read pastGDIBitmapBits.Change.
tmHeightrows, andtmMaxCharWidth + tmOverhang + PixelOverlap + 1columns, which covers the overlap column and the one-pixel shift applied to'W'.4 * PointSize + 8, so a font with absurd metrics renders clipped instead of allocating an absurd bitmap.Blit_Charalways readsCharHeightrows, and the glyph blocks are not zero-initialized.CurrPixelOffsetnow advances bycx * CharHeight. That is exactly whatUpdate_Current_Bufferreserves and whatBlit_Charreads back; the old code addedPixelOverlapa second time.For well-formed fonts the clamps never engage, so glyph widths and text layout are unchanged.
Arial reports a
tmMaxCharWidthof 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 coveragev. Only the alpha nibble and whethervis zero carried any information.Change. The glyph buffers store
vitself, andBlit_Charrebuilds 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_Charruns 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 block size balances three costs:
MemoryPoolSingleBlockheader plus aGlobalAlloceach4. Let the sentence texture grow beyond 256 pixels for large fonts
Problem.
Allocate_New_Surfaceonly 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.CurrTextureSizethen 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.
tmMaxCharWidthof about 3.6 times the point size, which would move most fonts at every resolution into a larger texture.MaxTextureWidth/MaxTextureHeight.W3DGameWindow::winSetTextcan do, if the new text has a wider glyph.Textures stay square, because
Build_TexturesandDraw_Sentencerely on that.5. Discard cached glyphs on map load and on resolution change
Problem.
WW3DAssetManagerkeeps a permanent reference to everyFontCharsClass, one per font name, point size and bold flag, and releases them only inFree_Assetswhen 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_Cachefrees the glyph blocks and character arrays but keeps the object, its GDI font and its metrics. EveryGameFontandfontDatapointer held by display strings stays valid, and a glyph that is needed again is simply rasterized again.WW3DAssetManager::Free_All_FontChars_Glyph_Cachesapplies this to every font.W3DDisplay::reset, which runs on map load and on the return to the shell, and after a successful mode change inW3DDisplay::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
FontCharsBuffermemory pool entryFontCharsBufferstopped 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
GetTextMetricsandGetTextExtentPoint32W. Point sizes use the defaultClassicNoCeilingscaling 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.
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.
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:
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:
Testing
vc6presets and a modern MSVC preset for Generals and Zero HourmainResolutionFontAdjustmentinoptions.ini, or the script action that sets a font size): glyphs above 170 pt render, or are skipped, without memory corruption