11# Backporting inline ` |T…|t ` texture escape sequences to 1.12
22
3- Design + reverse-engineering map for teaching the 1.12 text engine to render
4- inline texture markup (` |Tpath:height:width:…|t ` ) in FontStrings / chat /
5- tooltips — the way 4.3.4+ does. ** The feature is complete and shipped**
6- ([ src/text/InlineTexture.cpp] ( ../src/text/InlineTexture.cpp ) ): icons render
7- inline in chat/display text (BLP + uncompressed TGA), multi-icon and multi-line,
8- font-centred, wrapping correctly, cropped from sprite sheets via texcoords,
9- tinted by an optional ` r:g:b ` vertex colour, and excluded from editable input
10- fields. The one deliberate gap is measure width (an icon counts as ~ zero width
11- for ` GetStringWidth ` /wrap/hit-test — see the hover-accuracy section for why it's
12- deferred). This file keeps the RE map that got us there: the rendering primitive
13- first, then the positioning / measure / editbox / texcoord / colour findings.
14-
15- ## SOLVED — the working rendering primitive (verified in-game: a coin icon renders in full colour)
3+ The 1.12 text engine shows ` |Tpath:height:width:…|t ` markup as literal text.
4+ This project makes it draw the icon inline in FontStrings, chat, and tooltips —
5+ the way 4.3.4 and later clients do. The feature is complete and shipped
6+ ([ src/text/InlineTexture.cpp] ( ../src/text/InlineTexture.cpp ) ,
7+ [ src/text/InlineTexturePool.cpp] ( ../src/text/InlineTexturePool.cpp ) ).
8+
9+ This file is the reverse-engineering map. ** Current design** below states what
10+ ships today. Everything from ** Goal & spec** down is the historical RE trail —
11+ the 4.3.4 study, the 1.12 injection map, and the bring-up plan. It is kept for
12+ the addresses and the reasoning, not as a description of the current code.
13+
14+ ## Current design (shipped)
15+
16+ Each icon renders as an ** engine-managed region** (a pooled ` CSimpleTexture ` )
17+ anchored to its owning FontString. The engine draws the region every frame and
18+ moves it with its line, so texture residency and scroll tracking are correct by
19+ construction. (A raw-GxU-quad renderer existed during bring-up and was removed
20+ in ` 56c2670 ` ; its RE is the "REMOVED" section below.)
21+
22+ The DLL hooks the engine's own text pipeline — no companion addon:
23+
24+ - ** Positioning** — a co-hook on the glyph emitter ` FUN_005ccbe0 ` records each
25+ icon at its pen position and delegates the plain text runs to the engine
26+ (segmented delegation, below). The pen read-back needed a fix: the engine's
27+ pen is lazy and omits the last glyph's own advance, so a coin sat on its
28+ digits; the emitter now adds the true terminal advance from the engine's own
29+ glyph-advance helpers (` 550cf5e ` ).
30+ - ** Rendering** — the paint-pass co-hook ` FUN_005c8fe0 ` walks the icon records
31+ and queues each to ` Text::InlineTexturePool ` , which places the region on the
32+ next frame tick (never mid-render).
33+ - ** Measure** — three cold FontString-level co-hooks make measure match the
34+ drawn result: width (` FUN_00772890 ` ), height (` FUN_007729B0 ` ), and wrap
35+ (` FUN_005C7260 ` ). ` GetStringWidth ` , ` fontstring:GetStringHeight ` , wrap
36+ breaks, and line height all count the icon.
37+ - ** Tall icons** — an icon taller than the font grows its line height, so it
38+ does not overlap its neighbors, and it centers in the grown line. A hyperlink
39+ that contains a tall icon is hoverable across the whole icon
40+ (` 49934b0 ` , ` b9d0705 ` ).
41+ - ** Chat fade** — icons fade in lockstep with their chat line (` 3ca3b7e ` ).
42+ - ** Editboxes** — an editbox keeps the raw markup, so the caret stays aligned
43+ (two complementary signals, below).
44+ - ** Payload** — the full
45+ ` |Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t ` :
46+ size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
47+ TGA.
48+ - ** Anti-spoof** — chat strips player-injected ` |T ` icons.
49+
50+ The one measure path still icon-blind is the substring width (` FUN_00772AE0 ` ),
51+ which nothing consumes yet. The Lua control surface is one kill switch,
52+ ` _classicapi_InlineTexEnable ` (the SEH latch trips it on a flush fault); the
53+ bring-up tune / stat / probe functions were removed (` 5e5677b ` , ` 6800a9a ` ).
54+
55+ ## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
56+
57+ This was the FIRST renderer: each icon drawn as a raw textured quad through the
58+ text VB path. It shipped, then was removed in ` 56c2670 ` for the engine-region
59+ model (regions are resident by construction; the quad path needed the texture-
60+ residency machinery below and still flickered on VRAM pressure — see
61+ ` docs/InlineTextureResidency.md ` ). This section is the address record for the
62+ removed code, not a description of the current renderer. ` LoadTextureByPath ` ,
63+ ` DrawTexturedQuad ` , and the GxU device offsets it names are gone from
64+ ` Offsets.h ` ; the values live here and in git history.
1665
1766An arbitrary texture draws through the text VB path as a coloured, alpha-blended
18- quad. The end-to-end recipe (all offsets in [ Offsets.h] ( ../src/Offsets.h ) under
19- the "Inline texture escape" block):
67+ quad. The end-to-end recipe:
2068
21691 . ** Load by path** — ` LoadTextureByPath ` replicates ` FUN_00770200 ` 's load: build
2270 flags via ` FUN_0058a980(&flags, DAT_00878cf0, 0,0, 0,0,0, 1, 0) ` , then
@@ -110,11 +158,12 @@ We do NOT reimplement the emitter's intricate glyph vertex math. Instead, in the
110158 int-cast yields garbage (this bug hid the icon off-screen and dropped the
111159 trailing text).
1121603 . Record an ` IconRecord ` at the pen (node-local ` x ` ,` y ` ) into ` g_nodeIcons[line] `
113- and advance the pen by the icon width. ` DrawTexturedQuad ` never runs during
114- the build — icons are recorded and ** flushed** in the ` FUN_005c8fe0 ` paint
115- co-hook, where each line's origin ` [line+0x70]/[+0x74] ` maps node-local →
116- screen (the same translate the glyph copy ` FUN_005c8710 ` applies; confirmed
117- pure translation, no extra scale, DAT_00c2b9dc==0 path).
161+ and advance the pen by the icon width. No draw runs during the build — icons
162+ are recorded and ** flushed** in the ` FUN_005c8fe0 ` paint co-hook, which
163+ computes each icon's screen rect (line origin ` [line+0x70]/[+0x74] ` maps
164+ node-local → screen, the same translate the glyph copy ` FUN_005c8710 `
165+ applies) and queues it to ` Text::InlineTexturePool ` as a placement RELATIVE
166+ TO THE OWNING FONTSTRING. The pool applies placements on the next frame tick.
1181674 . Restore ` penXYZ[0] ` on exit (the original never writes it; the draw builder
119168 does NOT reset it between left-justified lines, so leaving it mutated
120169 cascade-shifts every following line).
@@ -148,9 +197,12 @@ fall out of the segmentation loop naturally.
148197### Vertical centring — font-relative
149198
150199` penXYZ[1] ` sits near the text TOP, so the icon centre = `penY + fontHeight *
151- 0.5` . Font height comes from the engine's own ` FUN_005c6fa0(flag, [ node+0x1c] )`
152- (the emitter's glyph-sizing call), so centring holds across font sizes without a
153- fixed pixel nudge.
200+ g_centerFrac` (0.6, calibrated in-game across chat + pfUI's bubble). Font height
201+ comes from the engine's own ` FUN_005c6fa0(flag, [node+0x1c]) ` (the emitter's
202+ glyph-sizing call), so centring holds across font sizes without a fixed pixel
203+ nudge. A TALL icon (height > font) centres in its GROWN line (see the tall-icon
204+ sections in Current design) — the string-height co-hook adds the overflow, so
205+ the extra room sits half above and half below the text.
154206
155207### Measure / wrap — co-hook the tokenizer
156208
@@ -257,8 +309,9 @@ the break↔icon-count coupling discontinuous — no fixed point exists).
257309` text ` always points at the REMAINING text, so a continuation line whose
258310icons are behind it gets sum = 0. Because all four consumers share the hook,
259311render breaks, ` GetStringHeight ` , truncation, and break arrays shift
260- together. Diagnostics: ` _classicapi_InlineTexWrap(n) ` dumps the last 8
261- icon-bearing calls with per-probe raw out-params.
312+ together. (A ` _classicapi_InlineTexWrap(n) ` ring dumped the last 8
313+ icon-bearing calls during bring-up; removed with the other diagnostics in
314+ ` 5e5677b ` .)
262315
263316Unit trap #2 (hit on first flight, like the width hook's): each caller passes
264317fontH/wrapWidth in its OWN space — the draw builder passes node text units
@@ -272,12 +325,27 @@ scale), so px → caller units is exactly `fontH / fontHPx`. The hook computes
272325the icon sum in true pixels (the same value the emitter reserves) and scales
273326by that ratio.
274327
328+ ** Chat hyperlink hover over inline icons works (verified).** An emote wrapped in a
329+ hyperlink — ` |Htel:name|h|T…|t|h ` , the TwitchEmotes pattern — pops its tooltip when
330+ you hover the * icon* , not just adjacent text. The hover region is NOT re-measured
331+ on hover: the engine records each hyperlink's screen rect at layout time into a
332+ ` GXUFONTHYPERLINKINFO ` array (32-byte entries: rect + link data) via ` FUN_005cd310 ` ,
333+ called from the draw builder ` FUN_005cdc20 ` — the same layout pass that runs the
334+ line through our co-hooked tokenizer (` FUN_005c2810 ` ) and wrap-stepper
335+ (` FUN_005c7260 ` ). So the recorded rect rides the same icon-aware position
336+ accounting the wrap fix installs; the icon lands inside the hover region for free,
337+ and hovering it fires the frame's ` OnHyperlinkEnter ` . This is a consequence of the
338+ wrap fix, not a separate feature. (Ghidra caveat: the decompile of the
339+ ` FUN_005cd310 ` call args is mangled — a pointer-as-float + an uninitialized temp —
340+ so this is confirmed by the call path plus in-game test, not the byte-level
341+ formula.)
342+
275343** Still icon-blind (accepted residuals):**
276344
277- - The substring measure (` FUN_00772AE0 ` ) and hyperlink hit-testing still see
278- icons as ~ 0. Hover hit-testing on an icon needs a hyperlink-region feature
279- we have not built. Revisit ` FUN_00772AE0 ` (also cold) if a consumer needs
280- icon-aware substring widths.
345+ - The substring measure (` FUN_00772AE0 ` ) still sees icons as ~ 0. It is a
346+ * separate * function from the hyperlink hover rect above (which works) — nothing
347+ consumes it yet; revisit it (also cold) if a consumer needs icon-aware substring
348+ widths.
281349- A ~ ≤1px artifact when an icon is the last token: the gxu width loop ends on
282350 the last * glyph's* ink width rather than its advance (` FUN_005c6b70 ` gets the
283351 remaining-text pointer), and a trailing icon shifts the previous glyph's
@@ -368,11 +436,14 @@ preceding fields present; the `TintIcon` helper passes `0`s for width..bottom
368436(texW/texH ` 0 ` disables the texcoord crop → full texture) so the colour lands in
369437the trailing slots.
370438
371- ** Vertical flip:** the UI device backend is OpenGL (bottom-left texture origin,
372- v=0 at the bottom), so ` DrawTexturedQuad ` maps the TOP screen corners to ** v1**
373- and the bottom to ** v0** . Without this the texture renders upside-down —
374- invisible on symmetric icons (a coin), obvious on directional ones (raid
375- markers, swords).
439+ ** Texcoord order:** the region's 4-float texcoord setter
440+ (` FUN_SIMPLETEXTURE_SET_TEXCOORD ` ) takes the rect ** Y-FIRST INTERLEAVED**
441+ (` {v0, u0, v1, u1} ` ), matching the engine's ` {yA, left, yB, right} ` rect
442+ convention — diagnosed in-game when ` {u0,v0,u1,v1} ` rendered a raid-mark crop as
443+ the wrong cell. v is top-down, same as the ` |T ` payload's top/bottom fields.
444+ (The removed quad path had the inverse concern: the OpenGL backend put v=0 at
445+ the bottom, so the quad mapped the TOP screen corners to v1 — kept here as the
446+ backend note.)
376447
377448### Build / deploy / test cadence
378449
@@ -383,15 +454,23 @@ markers, swords).
383454- ** The embedded ` !!!ClassicAPI ` addon is symlinked** , so Lua-only changes (the
384455 ` TextureTest() ` harness in ` Util/AddOnCompat.lua ` ) need only ` /reload ` .
385456
386- ### Still scaffolding / optional
387-
388- Diagnostic Lua (` _classicapi_InlineTexEnable/Suppress/Tune/Stats ` ) and the
389- capture globals remain for now. Optional slices left: exact icon width in the
390- measure path (deferred — see "Deliberate gap: measure width undercounts an icon"
391- above for the full RE verdict) and animation-strip frame cycling. Vertex-colour
392- tint (the ` :r:g:b ` fields) is ** done** . RLE-compressed TGAs don't decode in 1.12
393- (only uncompressed) — convert with `magick in.tga -compress none -orient
394- bottom-left`.
457+ ### Remaining / optional
458+
459+ - ** Measure width, height, wrap** — all DONE (the three fs-level co-hooks in
460+ Current design). Vertex-colour tint (` :r:g:b ` ) — DONE. The bring-up
461+ diagnostics (` _classicapi_InlineTexSuppress/Tune/Stats/ProbeFS/RegionCal/Wrap `
462+ and the capture globals) were REMOVED (` 5e5677b ` ); only
463+ ` _classicapi_InlineTexEnable ` remains.
464+ - ** Animated emotes** — no DLL work is needed: a TwitchEmotes-style animator
465+ re-` SetText ` s each line ~ 30fps and rewrites the ` |T ` texcoords to crop the
466+ current film-strip frame. That is addon Lua. RLE-compressed TGAs do not
467+ decode in 1.12 (uncompressed only) — convert with
468+ ` magick in.tga -compress none in.tga ` (no ` -orient ` /` -flip ` : the region
469+ renderer's texcoord order shows the frame right-side-up).
470+ - ** Still icon-blind** (accepted): the substring measure ` FUN_00772AE0 ` ; the
471+ hyperlink hit-test past a tall icon OUTSIDE the link on the same line; and a
472+ ≤1px trailing-icon residual in ` GetStringWidth ` (the measure loop ends on the
473+ last glyph's ink width, not its advance).
395474
396475## Goal & spec
397476
@@ -602,10 +681,22 @@ Two implementation strategies once the primitive is pinned:
602681 that gate color/escape handling, so the reimplemented loops pass them
603682 through unchanged.
604683
605- ## Reality check on scope
606-
607- This is the largest feature in the project by a wide margin: faithfully
608- reimplementing 1.12's glyph emitter ` FUN_005ccbe0 ` (a long, intricate function)
609- + the measure loop, plus standing up a separate inline-texture batch + flush,
610- all on the per-frame text path, verifiable only by in-game iteration. Land it
611- in the slices above, smallest first, and expect multiple build/test rounds.
684+ ## Reality check on scope — how it actually landed
685+
686+ The plan above feared reimplementing 1.12's glyph emitter ` FUN_005ccbe0 ` (a
687+ long, intricate function) and the measure loop. It did not come to that. The
688+ shipped design avoids both:
689+
690+ - ** Positioning** does NOT reimplement the emitter — it SEGMENTS the line at
691+ icon boundaries and delegates each plain run to the ORIGINAL emitter, so the
692+ engine still lays out every glyph. We only track where the icons go.
693+ - ** Rendering** does NOT stand up a separate quad batch — icons are
694+ engine-managed regions anchored to their FontString (the removed quad batch
695+ was strategy B; regions replaced it).
696+ - ** Measure** does NOT reimplement the loops — three cold co-hooks on the
697+ fs-level width / height / wrap functions add the icons' contribution and call
698+ the originals.
699+
700+ The result is the largest feature in the project, but built from co-hooks that
701+ delegate to the engine rather than from reimplemented layout code. It was
702+ verified by in-game iteration over many build/test rounds, as expected.
0 commit comments