HUD Media Lifetime
Contract
HUD FDF trees may stay cached for a level. Texture and font configstring indices must not. Names live for the process; indices live for the level.
SV_Map does memset(&sv, 0, sizeof(sv)), which empties CS_IMAGES and CS_FONTS. The game module is not unloaded. All in-game HUD bindings live in one BSS object, hud. UI_ResetHud is memset(&hud, 0, sizeof(hud)) plus UI_ClearTemplates() for the FDF frames[] pool. Scene files bind into hud.console, hud.simple, and so on; a NULL root means "not bound yet".
Quake 2 does not parse UI files every frame. G_SetStats calls gi.imageindex(item->icon) from a name every frame, and DrawPic looks up that name. The one cached pic (level.pic_health) is assigned again in SpawnEntities after memset(&sv). Same rule here.
Data Flow
G_LoadMap
-> memset(&hud, 0, sizeof(hud))
-> UI_ClearTemplates()
-> UI_LoadHud() // every panel, once per level
-> UI_Write* / UI_BuildFrameForWrite
UI_LiveImage / UI_LiveFont re-ImageIndex from hud.image_key[]
hud.image_key[] is write-once per slot until the next memset. After a wipe, ImageIndex reuses slot 1; overwriting that slot's remembered chrome name with the new command-button occupant is the same collision.
Do not parse ConsoleUI.fdf on every resource-bar write. Isolated scene files stay; they share one accumulator.
Glue UI (games/warcraft-3/menu/) is a separate stb_fdf instance with its own frames[] and ui_textures[]. It is not this contract. Keep the frames[] definition hidden per shared library: on ELF/Linux an exported duplicate symbol can be interposed between libgame and libmenu, causing this server-side UI_ClearTemplates() to erase the client's glue/loading frame registry during SV_Map.
Client
Same-map load never changes CS_WORLD, so the client cannot wait for CL_ClearState. CL_RestartRefresh zeros cl.pics / cl.fonts / cl.models (renderer caches still own the GPU objects). CL_PrepRefresh always binds from the current configstring, and empty slots stay NULL so leftover art cannot draw.
Runtime Registration Order And Capacity
Command/build/status art is still server-authored through gi.ImageIndex() and CS_IMAGES; it is not part of the client's
MAX_DYNAMIC_IMAGES text-path cache. Two constraints follow from that ownership:
- a newly allocated image configstring must be serialized before the
svc_layoutthat first references its numeric slot;PF_Unicast()therefore callsSV_QueuePendingConfigStrings()before appending the authored layout to the client's message; - occupied
CS_IMAGESslots cannot be evicted/reused during the level because retained layers may still hold their numbers.MAX_IMAGESis 2048 (2,047 usable nonzero indices), replacing the legacy 256-entry table that could make later command icons resolve to index 0 after enough distinct UI art had been visited.
A renderer texture-cache miss and a network image-index exhaustion are different failures. Renderer-local textures remain in the
unbounded session hash described in docs/fs-loading-architecture.md; increasing or recycling MAX_DYNAMIC_IMAGES does not repair a
full CS_IMAGES table.
Verification is covered by server_net.image_registry_exceeds_legacy_255_slot_limit and
server_net.pending_image_configstring_precedes_dependent_payload. A visual run is still useful for final artwork correctness, but is
not required to establish these transport/index invariants.
Diagnostic Workflow
After load quick, tabs must show resolved KEY_QUESTS strings and tab art, not the FDF ids themselves. Command-card icons must match the selected unit, not shuffled chrome.
build/bin/openwarcraft3 -data "data/Warcraft III" -roc \
+map "Maps/Campaign/Human02.w3m" +wait +save quick +wait +load quick \
+screenshot 20 +com_frame_limit 80
Repeat with -tft. Engine coverage: make test-wc3-engine WC3_PATTERN='wc3_game.hud_image*'
Known Pitfalls
- Copying
FRAMEDEF.Texture.Imageonto the wire withoutUI_LiveImageafter a configstring wipe. - Leaving
hudbindings live acrossG_LoadMapwhileframes[]was cleared. - Treating
GetConfigstring(CS_IMAGES + old_index)as the name of a cached frame after the slot has been reused. - Preserving
CS_IMAGESacrossSV_Mapas a workaround. The table is per-level.
Widescreen Console Extension Tiles
Retail 1.30+ ConsoleUI.fdf uses symbolic ConsoleTexture05 and ConsoleTexture06 for the left/right widescreen console extensions, anchored 0.256 outside the 4:3 root on each side; classic archives author no such frames. The server resolves symbolic keys through UI_ThemeImagePath and the recipient's war3skins.txt race category before registering concrete CS_IMAGES paths. Some classic/pre-widescreen skin tables contain the extension art in the archives but omit one or both symbolic fields. In that case the skin lookup lacks a value and the renderer otherwise searches for a literal file named ConsoleTexture06, producing the 16x16 placeholder.
When one console extension key remains unresolved, UI_ThemeImagePath() derives its path from the resolved sibling (...05[.blp] ↔ ...06[.blp]). This keeps the active race/custom skin authoritative instead of hard-coding a race path. Runtime confirmation on a 2880x1620 Human campaign showed the left extension resolving to HumanUITile05 while the missing right extension was laid out correctly but retained literal ConsoleTexture06; this is a media-resolution failure, not an anchor failure.
Class-gated, deferred registration. These two keys are the exception to parse-time registration. UI_LoadTexture gives them a handle at or above HUD_DEFERRED_IMAGE_BASE (hud.deferred_key[]) instead of calling gi.ImageIndex, UI_LoadHudConsole collects the frames into hud.console_wide[], and UI_WriteConsoleBackdrop hides them for recipients whose client_s.canvas is not UI_CANVAS_WIDE. UI_LiveImage resolves a deferred handle for the recipient's race and registers it the first time a wide client's console is written, so sessions on 4:3 windows never carry the tiles in CS_IMAGES. UI_ImageKey returns the symbolic key for either handle range. Once registered the slots follow the normal per-level lifetime above. See ui-canvas.md for the client side (window class, settle timer, ui_canvas command).