Skip to content

Commit df88896

Browse files
committed
docs(text): truth-pass on the inline-texture RE docs
InlineTextureEscapes.md and InlineTextureResidency.md described the removed raw-quad renderer and a bring-up plan as if current. Bring them in line with the shipped engine-region design. InlineTextureEscapes.md: - New "Current design (shipped)" section up front: region renderer, the three fs-level measure hooks (width/height/wrap), the lazy-pen fix, chat fade, tall-icon line growth + full-height hover, editbox exclusion, the payload, and anti-spoof — each with its commit. - Banner the quad "rendering primitive" section as REMOVED (bring-up record; the address log for code deleted in 56c2670). - Fix the DrawTexturedQuad / quad-flush references to region-queue language; vertical centring is g_centerFrac (0.6) with the tall-icon grown-rect note; the flip note becomes the region SetTexCoord Y-first order. - "Remaining/optional": diagnostics removed (only InlineTexEnable left), measure/height/wrap DONE, animation is addon-side, the TGA convert command is -compress none only. - "Reality check": correct the prediction that we would reimplement the emitter — the shipped design segment-delegates and co-hooks instead. InlineTextureResidency.md: rewritten from a "not-yet-implemented plan" into an accurate reference. States the region design, records the CSimpleTexture entry points / layout the pool uses (VAs verified against Offsets.h), and corrects the root cause honestly — the flicker was the LAA pointer-bound truncation (dc61f77), NOT texture residency; regions are kept for 4.3.4 parity and are resident by construction as a bonus. Keeps the D3D "ruled out" notes as a do-not-re-investigate record.
1 parent b9d0705 commit df88896

2 files changed

Lines changed: 256 additions & 50 deletions

File tree

docs/InlineTextureEscapes.md

Lines changed: 141 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,70 @@
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

1766
An 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

2169
1. **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).
112160
3. 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.
118167
4. 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
258310
icons are behind it gets sum = 0. Because all four consumers share the hook,
259311
render 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

263316
Unit trap #2 (hit on first flight, like the width hook's): each caller passes
264317
fontH/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
272325
the icon sum in true pixels (the same value the emitter reserves) and scales
273326
by 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
369437
the 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

Comments
 (0)