docs: a hosted page draws under its host, and art that does not resolve is invisible

Two defects from one play report, both in the ports, both measured rather
than guessed:

`SetParent` re-levels the frame it is called on and nothing under it
(0x76ab65 pushes propagate = 0, and a script SetFrameLevel does the same),
so Cinematic's pages -- built parentless, widgets at levels 1..20 -- landed
25 levels under the window its toplevel raise had lifted to the top of
DIALOG. The page drew under the window's own backdrop and the backdrop took
every click. The screenshot named it before any code was read: the page's
white read (103,103,103) and its gold (103,84,0), the same 0.404 factor as a
nameplate behind the window, where a fade on top would have composited to
(110,116,121). Multiply, not blend, means underneath.

And the map's resize grips are shown, mouse-enabled at level 9002, and take
the cursor -- but wear a 2.0 texture this chain has not got, and a Lua caller
cannot find that out: SetTexture returns nothing and GetTexture echoes the
path it was handed either way. Twenty pixels of nothing in the map's corners.
The client's own `texture miss:` warning is the only instrument that names
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-02 18:42:54 -07:00
co-authored by Claude Opus 5
parent 204184afb2
commit 5308b5a200
+129 -9
View File
@@ -1877,7 +1877,7 @@ whole-corpus harness confirms `AceGUI.tooltip` is a real tooltip again and `SetO
anything — **load order does**. Before reasoning about a library's behaviour here, find the copy
that registered, not the copy with the highest version.
## `WOW_MAP_TRACE=1`: what the map's base art actually is
## `world map art:`: what the map's base art actually is, in every session's log
A player's report about how the map *looks* cannot be answered from this side. The offline addon
harness seats no renderer, the capture harness seats no UI or addons, and a login as anyone but the
@@ -1885,15 +1885,20 @@ player would kick them off their own account — so a full day of offline audit
correct, catalog right, canvas geometry right, overlay tiling arithmetic right, addon setup
complete) still could not say why grom's Dun Morogh map came up magnified, clipped and grey.
Every number that decides the picture *is* readable from the VM, so `WOW_MAP_TRACE=1` now prints
them once per map change (`ui_world_map.rs`): the detail frame's rect and effective scale, then per
tile its rect, the texture path it holds, and the **decoded size of that file** — measured through
a scratch texture with no size of its own, because the tile declares 256x256 in
`WorldMapFrame.xml` and would answer that whatever the file holds (this install's art is a 512²
upscale pack). A tile whose rect is not 256x256 at a 256 pitch, or whose file is not the size the
layout assumes, names the fault outright instead of being guessed at from a screenshot.
Every number that decides the picture *is* readable from the VM, so the client prints them once
per map change (`ui_world_map.rs`, `world map art:`): the detail frame's rect and effective scale,
the frame it is drawn into, then per tile its rect, the texture path it holds, and the **decoded
size of that file** — measured through a scratch texture with no size of its own, because the tile
declares 256x256 in `WorldMapFrame.xml` and would answer that whatever the file holds (this
install's art is a 512² upscale pack). A tile whose rect is not 256x256 at a 256 pitch, or whose
file is not the size the layout assumes, names the fault outright instead of being guessed at from
a screenshot.
Off unless the variable is set: it runs a Lua chunk per map change.
It was behind `WOW_MAP_TRACE=1` at first, and that gate cost a round trip: the next report about
the map arrived without the line, and the only way to ask for it again was to wait for the player
to play again. It is always on now, one line per map change. **A diagnostic that a play report
needs is worth more unconditional than it costs in log lines** — the same lesson as the subsystems
that write nothing but `debug!`.
## The map, answered by its own trace: the stock art does not follow a resized detail frame
@@ -1940,3 +1945,118 @@ Two smaller things fell out of it and are fixed with it:
`script.resolve`, and `resolve` is what drains the layout pass's queued `OnSizeChanged`. So
`OnSizeChanged` never fires under `--tick` alone and it is not evidence of an engine gap; a
`--mouse` or `--plates` step resolves, and under those it fires normally.
## `SetParent` re-levels the frame and nothing under it, so a hosted page draws under its host
grom could read Cinematic's settings pages but not click them: everything on a page was dimmed to
about 40% and every click went nowhere, while the **first** page worked. Three measurements from
the screenshot he sent, before any code was read:
- The window's own title and close button read `(255,210,0)` and `(255,226,24)` — full brightness.
- Everything on the page read exactly `0.404 ×` its colour: white `(103,103,103)`, the gold page
header `(103,84,0)` against a full gold of `(255,210,0)`, the checkbox tick `(103,103,13)`.
- A **nameplate** behind the window read `(103,103,103)`, and the same nameplate outside the
window read `(255,255,255)`.
The page was being dimmed by exactly as much as the world behind the window, which is what it
means to be *under* the window's own backdrop rather than faded on top of it (fading on top would
composite over the dark backdrop and come out `(110,116,121)`, not neutral grey).
The cause is one line of the reference. `SetParent` (`0x76ab10`) sets the frame's strata to its
new parent's and its level to the parent's **+ 1**, and at `0x76ab65` it pushes `0` as
`SetFrameLevel`'s propagate argument:
```
mov edx,[esi+0x9c] ; the new parent
mov eax,[edx+0xc4] ; parent->level
push 0x0 ; propagate = 0 <-- children keep their levels
inc eax
push eax
call 0x76a4f0 ; SetFrameLevel(parent->level + 1, 0)
```
A script `SetFrameLevel` does the same (`0x774560` → `0x76a4f0`, propagate `0`, which stock
`BonusActionButtonTemplate` relies on). So **nothing in Lua moves a subtree's levels** — a frame
reparented into a window takes its new place alone, and everything under it stays where it was
built.
Cinematic's pages are built parentless (`CreateFrame("Frame", "CinematicOptionsPage"..n)`), so
their widgets sit at levels 1..20. Measured in the corpus harness, with the window hosting a page:
```
page1 after raise: w=25 host=26 canvas=27 | subtree n=27 levels 25..40
Audio: w=25 host=26 canvas=27 | subtree n=24 levels 1..20
UI Manager: w=25 host=26 canvas=27 | subtree n=1352 levels 1..20
```
The window is at 25 because `SetToplevel` + the raise (`CSimpleTop::Raise`, triggered by Show and
by mouse-down) compacts the stratum and lands the window one above the top — and **DIALOG has 25
occupied levels on this install**, which is simply what a hundred addons come to. The first page
escaped because it was already attached when that raise ran, and a raise *does* carry a frame's
children with it (`set_frame_level(…, propagate = true)`). Every page attached afterwards is
attached to an already-raised window and lands 25 levels below it.
The hit test agrees with the draw order, which is the other half of the symptom:
```
fixed: focus = <26x26 CheckButton, level 30>
sunk 27: focus = CinematicOptionsWindow (900x620, level 25)
```
**The rule:** after reparenting a frame built elsewhere, shift its subtree by however far the
frame itself moved. Not to `parent + 1` per depth — that flattens offsets the page meant — but by
the delta, recursively, since `SetFrameLevel` will not do it:
```lua
local function ShiftLevels(frame, delta)
for _, child in ipairs({ frame:GetChildren() }) do
child:SetFrameLevel(child:GetFrameLevel() + delta)
ShiftLevels(child, delta)
end
end
```
and parent each page **once**, never `SetParent(nil)` between shows: `SetParent(nil)` resets the
frame to MEDIUM level 0 (`0x76aba3`/`0x76abac`) while its children keep the levels they now have,
so the next shift stacks on top of the last one.
This is not a benilla deviation — a reference client does the same thing — but it bites here and
not on Era for a measurable reason: **the number of occupied DIALOG levels.** On a stock install
the raise lands a window at 3 or 4 and a page built at 1..20 still covers it. At 25 it does not.
Any addon that hosts a foreign canvas is one crowded stratum away from this.
## A texture that does not resolve is invisible, and no Lua caller can tell
The same report carried "the map ... can't be resized right now". OpenMapClassic's map window has
two 20x20 resize grips in its bottom corners, and offline both are shown, mouse-enabled, at frame
level 9002, and take the cursor when it is put on them — they *work*. What they do not do is
appear:
```
texture miss: 'Interface\ChatFrame\UI-ChatIM-SizeGrabber-Up' does not resolve in the patch chain
texture miss: 'Interface\ChatFrame\UI-ChatIM-SizeGrabber-Highlight' …
texture miss: 'Interface\ChatFrame\UI-ChatIM-SizeGrabber-Down' …
```
The chat-IM size grabber arrived with the IM window in 2.0; `benilla-extract list` finds nothing
resembling it anywhere in this chain. So the grips were twenty pixels of nothing in the corners of
the map: reachable only by someone who already knew where to aim.
**And the addon cannot find this out at runtime.** `SetTexture` returns nothing on 1.12 and
`GetTexture` echoes back the path it was handed whether or not a file resolved — measured:
```
grabber: SetTexture→nil GetTexture→Interface\ChatFrame\UI-ChatIM-SizeGrabber-Up
tooltip: SetTexture→nil GetTexture→Interface\Tooltips\UI-Tooltip-Border
```
identical answers for a path that resolves and one that does not. (`SetTexture` returning `false`
on a miss is an Era-ism; two call sites in `OpenMapClassic/Map/Stitch.lua` already lean on it and
get `nil`, which is not `false`, so their fallbacks never run either.) A port cannot probe its way
out of this — it has to use art the chain actually holds, and the only instrument that names a
missing one is the client's own `texture miss` warning in the session log. **Read those warnings
after any port: each one is something a player is not seeing.**
The grips are drawn now the way 1.12 windows draw a corner grip — two nested squares of
`Interface\Tooltips\UI-Tooltip-Border` turned 45 degrees with the eight-argument `SetTexCoord`,
AceGUI's own sizer art, brightened on hover.