The map's darkening was a grid of grey boxes, and a 12 px string was emptying the glyph sheet
grom, after a night's play: "make the map look a lot nicer, it would be really nice if the darkened areas were not just massive squares on the map, and should fit into the actual edges of the terrian if that makes sense, also I shouldn't see this dark squares on the fully zoomed out world map, also make it so the world map can't move out of the frame." **The darkening.** OpenMapClassic washed a never-visited zone through its land mask, and those masks are Voronoi-dilated by construction — "so that the forty of them tile each continent with no gap" — so for any zone that is not on a coast the mask *is* the rectangle. Measured at zoom 2.5 over the Eastern Kingdoms: a grid of flat grey boxes. Nothing could have been softened into a fix; it was the wrong mask. It was also drawn at full strength from every zoom, and at the whole-world view the zones' own art is not drawn at all, so the only thing on the world map was the fog's rectangles. A land mask for every map now comes from the client's own data. `bake_zone_land` unions a map's `WorldMapOverlay` alpha — the pieces the game reveals as you explore, cut to the shape of the ground — into one 256×256 mask; `overlay-land-index.py` writes the list the addon reads, because Lua cannot test for a file and a *missing* mask is dropped by the engine so the whole rectangle draws, which is the defect itself. 53 maps have overlays (the 40 stock zones, Hyjal, Alterac Valley and 11 of the realm's own); 76 have none and keep a wide feather. `clip-zone-masks-to-coast.py` then takes the open sea off the dilated sheet masks — the same dilation was painting parchment out on the ocean — by closing the continent's own land by 500 yards (above the 466-yard inradius of the widest enclosed gap, so seams fill and the sea does not) and keeping a 300-yard coastal margin. Twelve landlocked zones lose 0.0%; Darkshore loses 53%, all of it water. `tga.rs` gains the writer that needed, `script::uimap` and `ui_world_map::build_catalog` become public so an example can reach the uiMapID graph, and `WorldMapUiData` grows the accessor so the VM push and an outside caller cannot build different lists. **The glyph sheet.** His four-minute session reset it 27 times, each dropping every cached glyph. The producer is Atlas-CFM's cursor coordinate readout: a 12 px FontString on `WorldMapDetailFrame`, rewritten every frame, and the canvas zooms by scaling that frame — 140 device px a digit at a canvas scale of 6.9. Nothing about the addon is wrong, so the fix is ours: `raster_ppem` rasterizes exactly up to 64 device px and snaps to a third-octave ladder above it, with `cell()` scaling the cached bitmap and metrics left keyed on the true ppem, so advances and `GetStringWidth` are bit-for-bit unchanged. Same 1,200-frame reproduction: 3 resets and 6,623 rasters before, **0 and 1,424** after, with occupancy climbing once to 30% and stopping. A reset no longer clears the metric cache either, and `note_loud_cell` names the string that asked for an outsized cell. **Four more from the same log.** `SetOwner`'s unknown-anchor line named a forwarding wrapper rather than the addon that chose the anchor (two `debugstack` frames now); the nine reference anchor names are written down with the binary evidence, because `ANCHOR_TOP` is not one of them. The `CreateFrame(kind, …, inherits=<other kind>)` warning fired in both directions and only one drops content — `kind_subsumes` makes it directional, and `type_chain` moves out of `install` so `IsObjectType` and the loader read one table. A string that is not UTF-8 now names the Lua line that cut it, and both glyph warnings quote the string the character came out of. The three world-map CVars stay unregistered, with the argument written down beside `REGISTERED` so it is not re-litigated from the warning. **`World.zmp`.** The realm's `WorldMapArea` carries a third `areaID == 0` row, `694 World`, with an all-zero rect; the reference ships no bitmap for it and neither the graph nor the realm's own `Overrides.lua` lets it reach a player. The miss is a `debug!` line now, and the `warn!` is kept for a continent that *has* a rectangle and is missing its bitmap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -3231,6 +3231,65 @@ With that in, `initializationErrors` is **0**, the window is `shown=0` at login,
|
||||
map, and a real `--drag` pans it. The settings window opens with its real tabs — Audio, General,
|
||||
Voice Packs, Profiles — and clicking them switches pages with no raise.
|
||||
|
||||
### The same trap again, one library down: `CallbackHandler-1.0`'s own `Fire`
|
||||
|
||||
A session logged `...ills\lib\CallbackHandler-1.0\CallbackHandler-1.0.lua:113: attempt to perform
|
||||
arithmetic on local 'argc' (a boolean value)`, and the shape is the one above with a different
|
||||
library. `WidgetBase.Fire` is the *widget* side; `registry:Fire` is the *callback registry* side,
|
||||
and only the first was shifted.
|
||||
|
||||
Four copies of `CallbackHandler-1.0` are installed. Three are Ace3v (MINOR 6:
|
||||
MasterTradeSkills, SoftSpoken, UnitXP_SP3_Addon) with `registry:Fire(eventname, argc, a1…a10)`;
|
||||
OpenMapClassic's is modern (MINOR 8) with `registry:Fire(eventname, ...)`. `LibStub.minors` reads
|
||||
2 for every library, so the first copy to load wins outright, and alphabetical load order makes
|
||||
that MasterTradeSkills'. OpenMapClassic's `API.lua:27` then fires modern-style through an Ace3v
|
||||
registry:
|
||||
|
||||
```lua
|
||||
local function Fire(...) if events then events:Fire(...) end end
|
||||
...
|
||||
mod.OnStitchedChanged = function(on)
|
||||
on = on and true or false
|
||||
if on ~= wasStitched then Fire("OpenMap_StitchedChanged", on) end -- API.lua:162
|
||||
end
|
||||
```
|
||||
|
||||
`on` lands in `argc`, and `Dispatchers[argc+1]` at `:113` raises on a boolean. **Not a verb of
|
||||
ours**: no engine call is anywhere in that chain, and the reference client raises here too, off the
|
||||
same realm `LibStub`. The blast radius is OpenMap's whole published callback surface, not one
|
||||
event: `OpenMap_ViewChanged` passes a 0..1 fraction where the count belongs, so `Dispatchers[u+1]`
|
||||
is nil and that one raises on the *call* instead of the add, and the two dungeon-map events pass a
|
||||
table. No addon can follow OpenMap's map at all — which is the feature `API.lua`'s header
|
||||
advertises to Questie, TomTom and the gatherers.
|
||||
|
||||
**Fixed caller-side, in `API.lua`, and the choice is the interesting part.** The callee-side edit
|
||||
is tempting — shift a non-numeric `argc` down and count what arrived, once, and all three Ace3v
|
||||
copies are covered. But those three copies live in *MasterTradeSkills*, *SoftSpoken* and
|
||||
*UnitXP_SP3_Addon*: third-party addons in the player's own folder, which he updates. A fix there is
|
||||
a fix that disappears, and patching a stranger's vendored library is the opposite of the thing this
|
||||
whole document is for. `OpenMapClassic` is ours.
|
||||
|
||||
The objection to caller-side was that a caller cannot know which registry it got. It can: ask.
|
||||
|
||||
```lua
|
||||
local probe = {}
|
||||
local reg = CBH:New(probe, "RegisterCallback", "UnregisterCallback", "UnregisterAllCallbacks")
|
||||
local seen
|
||||
probe.RegisterCallback(probe, "OpenMapAbiProbe", function(_, v) seen = v end)
|
||||
reg:Fire("OpenMapAbiProbe", 7)
|
||||
fireTakesArgc = seen ~= 7
|
||||
```
|
||||
|
||||
A *number* is fired so neither ABI can raise on the probe itself: Ace3v reads 7 as a count and
|
||||
hands the handler seven nils, the modern one hands it the 7. Probed rather than read off `MINOR`
|
||||
because the minor number is the thing that has already lied — `LibStub.minors` says 2 for
|
||||
everything. Then `Fire` adds `select("#", ...)` or does not.
|
||||
|
||||
Measured in a capture that registers a real callback and then moves the view: **5 `OpenMap_ViewChanged`
|
||||
callbacks delivered, `0.700, 0.460, 12.00`**, where before the fix the same probe got none and the
|
||||
log carried the raise. The one remaining `argc` line in that capture's log is the probe's own
|
||||
answer, `argcAbi=true`.
|
||||
|
||||
### Two more defects it was hiding
|
||||
|
||||
Opening all three registered options tables (`MasterTradeSkills`, `OpenMapClassic`,
|
||||
@@ -3774,3 +3833,261 @@ ruins — not the city. That is true of this install and not only of retail: `te
|
||||
the player's own chain holds `Azeroth` and `Kalimdor` and a folder per dungeon, and no folder for
|
||||
any city (measured across 112,411 files). There is no interior minimap to show, so the two
|
||||
underground cities keep their plan whatever the new setting says.
|
||||
|
||||
## "Darken unexplored areas" drew a grid of grey boxes, because the mask it used is dilated on purpose
|
||||
|
||||
> "it would be really nice if the darkened areas were not just massive squares on the map, and
|
||||
> should fit into the actual edges of the terrian if that makes sense, also I shouldn't see this
|
||||
> dark squares on the fully zoomed out world map"
|
||||
|
||||
Two separate faults with one symptom, and the second one is the kind that is only obvious once you
|
||||
stop looking at the code and look at the picture.
|
||||
|
||||
### The shade had two paths and the cheap one was a rectangle
|
||||
|
||||
`Stitch.lua`'s `Shade` darkened a zone two ways. A zone the player had been to got **one darkened
|
||||
copy of each unexplored piece's art**, which carries that piece's own alpha and therefore the shape
|
||||
of the ground. A zone the player had found *none* of got `Shade.Texture`: a single
|
||||
`SetColorTexture` over the whole 1002×668 art rectangle, cut down by the zone's land mask.
|
||||
|
||||
The land masks are `MapZoneDilate` — forty hand-made files whose own purpose, stated in their
|
||||
generator, is to be **Voronoi-dilated "so that the forty of them tile each continent with no
|
||||
gap"**. For a zone that is not on a coast, a mask that tiles the continent *is* the zone's
|
||||
rectangle. So the cheap path painted a grey box, by construction, for every inland zone.
|
||||
|
||||
Measured at zoom 2.5 over the Eastern Kingdoms with nothing explored: a grid of flat grey
|
||||
rectangles with two or three coastal zones the only honest shapes among them.
|
||||
|
||||
### …and the expensive path was covering something this fork does not have
|
||||
|
||||
The flat path is **gone**, and so is the copy layer behind it. The copies exist upstream for a
|
||||
stated reason: they are drawn over the **baked tiles** (`ns.MapZoneArt`, one pre-rendered texture
|
||||
per world tile standing in for the live layers at mid zoom), and a baked tile is many pieces in
|
||||
one, so it cannot be tinted piece by piece. This fork has no baked tiles — `Data/ZoneArt.lua` is
|
||||
three lines, *"Forever MapZones bake removed. Baked path disabled."*, and its `colorMatch = -1`
|
||||
means `Baked.use` can never become true. The copies were covering nothing.
|
||||
|
||||
So the darkening is now one line in `GradeEntry`: multiply an unexplored piece's **own** texture by
|
||||
`Shade.DIM`. That is the same picture — a texture multiplied by `DIM` is a texture multiplied by
|
||||
`DIM`, whether it is the art or a copy of the art over it — and it is a *better* one in a way the
|
||||
copies had to work for: a zone's base art carries no piece (`PlaceBase` passes `false`) and the
|
||||
overlays only ever cover land, so the sea a 1.12 sheet paints stays bright by construction, where
|
||||
the copies needed a land mask to achieve the same thing.
|
||||
|
||||
That removed `Shade.Texture`, `Shade.Zone`, `Shade.Pieces`, `Shade.Colour`, `Shade.Near`,
|
||||
`Shade.Show`, the per-zone shade cull and the `Shade.root` layer — about 120 lines down to four
|
||||
lines of state and one function. `shadeRoot` is kept, empty, only so every layer above it keeps the
|
||||
frame level it had.
|
||||
|
||||
Quads a frame at zoom 5 with half the overlays explored: `show` 4,729 · `dim` **4,675** · `hide`
|
||||
4,550. Darkening a map now costs *fewer* quads than not darkening it, because the colour-match twin
|
||||
over a darkened piece is hidden rather than drawn. Before the change `dim` was 5,124 against
|
||||
`show`'s 4,674 — 450 extra quads, every one of them masked.
|
||||
|
||||
Frame times, measured once the machine was actually idle — which is the point worth keeping:
|
||||
`show` **5.20 ms** · `dim` **5.14** · `hide` **4.88** (240 frames each, same scene, zoom 5).
|
||||
Darkening is free now. Every earlier reading that night shared the machine with a release build and
|
||||
was worthless: the same `dim` scene read 8.68 ms, then 8.94, then **26.05** under load average 8.
|
||||
Quad counts were load-independent and said the same thing throughout, which is why they are the
|
||||
numbers above. A frame-time A/B taken while something else is compiling is not a measurement.
|
||||
|
||||
*What is given up, stated plainly.* The flat shade sat on `shadeRoot`, **over** the close-up
|
||||
minimap terrain; the piece copies sat under it, and the tint is the art itself, so neither darkens
|
||||
the minimap. At a high zoom inside a zone you have explored none of, the minimap terrain therefore
|
||||
reads at full brightness with the darkened sheet showing around it. That was already true of
|
||||
upstream's behaviour for any zone you had partly explored — the flat path only ever ran for
|
||||
"none of it" — and buying it back means a rectangle, which is the thing this whole section is
|
||||
about. Left as it is, deliberately.
|
||||
|
||||
### A fog that was the only thing on the whole-world view
|
||||
|
||||
The darkening was drawn at full strength "from every zoom". At the whole-world view the *zones' own
|
||||
art is not drawn at all* — `DETAIL_FADE` starts at zoom 3 — so on the world map there was Blizzard's
|
||||
clean painted world with nothing on it but the fog's rectangles.
|
||||
|
||||
It now rides `detailNow`, the very ramp the art it darkens rides. Measured on the current build:
|
||||
the whole-world view with "Darken unexplored areas" **on** and with it **off** differ by **0
|
||||
pixels**. The same comparison before the change differed by 84,818.
|
||||
|
||||
Riding the art's own ramp is also what bounds the cost. `Shade.vis` is false out there, and while
|
||||
it is false nothing is *built*: at the far zoom the cull box is a whole continent, so the old code
|
||||
was free to lay out the piece copies for forty zones at once.
|
||||
|
||||
### The cities had to be switched off, not faded
|
||||
|
||||
A city's own sheet is darkened by **multiplying its art** (`cities.Dim`), not by a shade laid over
|
||||
it, so it cannot be faded out with everything else — a vertex colour has no alpha to ramp. Left
|
||||
alone, an unvisited city stayed a small hard black rectangle on the world view after the rest of
|
||||
the fog had gone. It now reads `Shade.vis` too, and `S.SetShadeZoom` re-runs `cities.Dim()` when
|
||||
that crosses. That is also the `black-rectangle dungeon insets in Darken mode` from an earlier
|
||||
report: half of the remaining pixels at the world view were cities, and the count went 2,894 → 1,385
|
||||
→ 0 as the two were fixed.
|
||||
|
||||
## A land mask for every map, generated from the exploration overlays
|
||||
|
||||
The hand-made mask set has **40** files. This realm's `WorldMapArea.dbc` has **175 rows**. Every
|
||||
map with no mask was drawn through a plain edge feather — `MapFeather` is opaque from 7.8% in, so
|
||||
over a zone-sized sheet that is a rectangle with a thin soft rim — and shaded the same way.
|
||||
|
||||
`cargo run -p benilla-app --example bake_zone_land` writes a mask per map from the client's own
|
||||
data: the union of that map's `WorldMapOverlay` alpha, composited into the 1002×668 sheet at each
|
||||
overlay's `offsetX/offsetY` and downscaled to 256×256. The overlays are the pieces the game reveals
|
||||
as you explore, cut to the shape of the ground, so their union is the map's land and nothing else —
|
||||
which is exactly what "should fit into the actual edges of the terrain" asks for, and it comes from
|
||||
the player's own install rather than from a table somebody traced.
|
||||
|
||||
**53 maps have overlays**: the 40 stock outdoor zones, Alterac Valley, Hyjal, and 11 of the realm's
|
||||
own — Gillijim's Isle, Lapidis Isle, Icepoint Rock, Tel'Abim, Gilneas, Thalassian Highlands,
|
||||
Blackstone Island, Balor, Northwind, Grim Reaches, Moonwhisper Coast. `scripts/overlay-land-index.py`
|
||||
turns the baked folder into `Data/OverlayLand.lua`, because Lua cannot ask whether a texture file
|
||||
exists — and a mask whose art is missing is *dropped* by the engine, so the whole rectangle draws,
|
||||
which is the very defect this set removes.
|
||||
|
||||
Two things the baker had to get right, both recorded in its own source: the reveal is alpha-over
|
||||
rather than `max`, because `WorldMapFrame.lua` puts one texture per piece on the detail frame at
|
||||
the default blend; and bare pixels the overlay art completely *encloses* are flood-filled, because
|
||||
the pieces are feathered into their own rects and where two abut their ramps cross below opaque —
|
||||
Westfall's raw union had 10,165 pinholes through the middle of the zone.
|
||||
|
||||
Where the two sets disagree they disagree in the expected direction. Against the hand-made masks,
|
||||
84–93% of the generated coverage falls inside theirs, while only 50–76% of theirs falls inside the
|
||||
generated — that gap *is* the dilation, filling sea to tile the continent. So the two are used for
|
||||
different jobs: a zone's **sheet** still prefers the dilated mask, because a sheet has to reach its
|
||||
neighbour; the **shade** prefers the generated one, because a shade should stop at the coast.
|
||||
|
||||
**76 maps have no `WorldMapOverlay` rows at all** — the six cities, two battlegrounds, every
|
||||
instance, and about twenty of the realm's own outdoor pockets, Alah'Thalas among them. No source in
|
||||
the client can give those a coastline, so they keep the feather fallback, now `FEATHER_WIDE` (13.7%)
|
||||
rather than `FEATHER` (7.8%): a sheet laid down rather than a box cut out. That is the half of
|
||||
"edges of maps are the same" that can be answered without new art.
|
||||
|
||||
## Zone sheets were painting parchment on the open sea
|
||||
|
||||
The same dilation, one layer up. A zone's **sheet** is cut by `MapZoneSoft`, and that mask is
|
||||
dilated so the forty of them tile a continent with no gap — which is right on land, where the
|
||||
dilation is what closes the seam to the neighbour, and wrong past the coast, where there is no
|
||||
neighbour and nothing underneath. So a zone painted its blank parchment on the ocean. Measured at
|
||||
zoom 6 west of the Eastern Kingdoms: pale rectangles floating in the Great Sea at world
|
||||
(0.725, 0.483) and (0.748, 0.407), both inside the rects of stock zones that *have* a mask.
|
||||
|
||||
`scripts/clip-zone-masks-to-coast.py` writes `MapZoneCoast`: `MapZoneSoft` intersected with the
|
||||
continent's own land — the union of every `MapOverlayLand` mask — **morphologically closed by 500
|
||||
yards**, then grown by a **300-yard coastal margin** with a 150-yard feather.
|
||||
|
||||
Three numbers, each measured rather than chosen:
|
||||
|
||||
- **500 yards** because the close has to fill the gaps *between* zones without filling the ocean,
|
||||
and a close by R removes exactly the background a disk of radius R cannot sit in — so R must
|
||||
exceed the widest enclosed gap's **inradius**, not its width. Those are 466 yards on Kalimdor
|
||||
and 267 on the Eastern Kingdoms; after the close, zero enclosed gap pixels remain on either.
|
||||
- **300 yards** because a 1.12 sheet deliberately paints the hatched shallows off its own coast
|
||||
and those should stay. 10.4% of everything the forty sheets paint sits more than 600 yards past
|
||||
any land; at a 600-yard margin a straight rect edge is still plainly visible off the Wetlands,
|
||||
at 300 the kept water follows the coastline everywhere.
|
||||
- The buffer is sized in **yards, not world units**, because the sheet is 44,533 yards across a
|
||||
world-u and 29,689 across a world-v — exactly 1.5:1, the detail frame's 1002:668. A circular
|
||||
dilate in world units would have been an ellipse on the map.
|
||||
|
||||
Twelve genuinely landlocked stock zones lose **0.0%** — that is the guard the script refuses to
|
||||
ship without. Darkshore loses 53%, Teldrassil 46%, Stranglethorn 39%, Durotar 27%; all of it is
|
||||
water.
|
||||
|
||||
## The map could be dragged out of its own window
|
||||
|
||||
> "make it so the world map can't move out of the frame like it is now, still pan and zoom when
|
||||
> zoomed, but zoomed out should fit and stay in the frame"
|
||||
|
||||
`WorldMap.lua`'s `Extents` gives the view `EDGE_SLACK = 0.2` of room past the world's edge, so the
|
||||
wheel can zoom about a point near the edge without the pan fighting it. The slack is a fifth of a
|
||||
**world** width, and it does not shrink with the zoom: at zoom 30 the view is a thirtieth of the
|
||||
world, so a fifth of a world is six screenfuls of empty sea. Measured before the change — the view
|
||||
driven to the world's top-left corner at zoom 6 sat there, and the whole window was open water with
|
||||
a strip of the world's top edge along the bottom.
|
||||
|
||||
`mod.db.clampView`, on by default, drops the slack to zero and makes `Range` return the centre
|
||||
outright on whichever axis the world is smaller than the view, at every zoom rather than
|
||||
interpolating out of it — that second half is what "zoomed out should fit and stay in the frame"
|
||||
asks for, because interpolating let the world slide about inside its own letterbox. Driving the
|
||||
view at the world's corner now answers:
|
||||
|
||||
```
|
||||
zoom 1 → u 0.5000 v 0.5000 (locked centred: the world fits)
|
||||
zoom 2 → u 0.2500 v 0.2555 (half a view from the edge)
|
||||
zoom 6 → u 0.0833 v 0.0852
|
||||
zoom 20 → u 0.0250 v 0.0255
|
||||
```
|
||||
|
||||
i.e. the map's edge meets the window's edge and stops, at every zoom. The cost is that the wheel can
|
||||
no longer zoom exactly about a point hard against the map's edge, which is what upstream bought with
|
||||
the slack; "Keep the map in the window" in the options turns it back off.
|
||||
|
||||
## The `World.zmp` warning was the data agreeing with itself
|
||||
|
||||
Every boot logged `world map: no zone bitmap for World: reading Interface\WorldMap\World.zmp: file
|
||||
not in patch chain`. The realm's `WorldMapArea.dbc` carries three `areaID == 0` rows — `13 Kalimdor`
|
||||
(map 1), `14 Azeroth` (map 0) and `694 World` (map 0, **all-zero rect**) — and the reference's own
|
||||
catalog walk selects continents by exactly that column, so the third reaches our catalog too.
|
||||
|
||||
It has no rectangle, so there is nothing to hover; and the reference ships no bitmap for it either —
|
||||
`Interface\WorldMap\` holds exactly `Azeroth.zmp`, `Kalimdor.zmp` and `Expansion01.zmp` across the
|
||||
whole 112,523-file chain. Nothing a player sees carries the row: `uimap::build_graph` drops it by
|
||||
art folder and the realm's own `Overrides.lua` pins `CONTINENTS_LENGTH = 2`. So the miss is now a
|
||||
`debug!` line with the reasoning in it, and the `warn!` is kept for a continent that *does* have a
|
||||
rectangle and *is* missing its bitmap — which is the case worth seeing.
|
||||
|
||||
## A 12 px FontString inside a zoomed canvas emptied the glyph sheet 27 times in four minutes
|
||||
|
||||
His session logged `ui_text: glyph sheet full (…/4194304 texels, N cells) — resetting at the frame
|
||||
boundary` **27 times in four minutes**, N running from 460 to 2,537. Every one of those drops the
|
||||
whole glyph cache and the next frame re-rasterizes every string on screen. That is a hitch, 27
|
||||
times, and it is a large part of "we need the whole thing running smoother".
|
||||
|
||||
**The producer is `AtlasCFMWorldMapCursorText`** — Atlas-CFM's world-map cursor coordinate
|
||||
readout, a Friz Quadrata **12 px** FontString created directly on `WorldMapDetailFrame`
|
||||
(`CFMAtlas/AtlasUI.lua:561`) and rewritten from an `OnUpdate` on that same frame every frame the
|
||||
cursor is over the map. It is on in his saved variables (`AtlasCursorCoords = true`).
|
||||
|
||||
Nothing about that is wrong. `MapCanvas.lua` reparents the detail frame into the canvas and the
|
||||
canvas zooms by **scaling that child**, so the FontString's effective scale is the zoom: 12 px ×
|
||||
his 1.6875 seam × a canvas scale of 6.9 is **140 device px per digit**, and the string is different
|
||||
every frame, so a glide minted a fresh set of ~13 display-size cells per frame. The evidence is the
|
||||
correlation: every one of the 27 resets opens within ~2 s of a `world map art: … scale N` line with
|
||||
N > 1, and there is not one while the canvas sits at its resting 0.90.
|
||||
|
||||
**The engine was what turned it into a hitch**, so that is where it is fixed. `raster_ppem()` now
|
||||
rasterizes at exactly the requested size up to **64 device px** and snaps to a third-octave ladder
|
||||
(`64, 81, 102, 128, 161, 203, 256`) above it, with `cell()` scaling the cached cell's extent and
|
||||
bearings by `ppem / rung` so the quad lands on the same device pixels. **Metrics are untouched** —
|
||||
`chars` stays keyed by the true ppem, so every advance, wrap and `GetStringWidth` is bit-for-bit
|
||||
what it was. 192 device sizes over one glide become 7 rasters. 64 is chosen so nothing the stock
|
||||
interface draws at 1:1 is quantized: the largest is `ZoneTextFont`/`WorldMapTextFont` at 32 units
|
||||
after the reference's own cap, 54 device px on his screen.
|
||||
|
||||
Same reproduction both sides — his saved variables, all his addons, 1,200 frames, the canvas glided
|
||||
between min and max zoom every 90:
|
||||
|
||||
| | `glyph sheet full` | cells rasterized | sheet occupancy |
|
||||
|---|---|---|---|
|
||||
| before | 3 | 6,623 | sawtooth 0 → 95% → reset |
|
||||
| after | **0** | **1,424** | climbs once to 30.2% and stops |
|
||||
|
||||
Saturation is the real result: the cache reaches a bounded steady state instead of sawtoothing.
|
||||
|
||||
Two things came with it. A sheet reset no longer clears the **metric** cache — `chars` holds pen
|
||||
metrics and shaper keys and nothing about the sheet, so clearing it made the frame after a reset
|
||||
re-*shape* every string as well as re-rasterize it, which is the expensive half. And a new
|
||||
`note_loud_cell` line names the offender once per `(face, rung, radius)`, with the string that
|
||||
asked for it:
|
||||
|
||||
```
|
||||
ui_text: a 128 px cell for '7' in 'Fonts\FRIZQT__.TTF' (outline radius 0) is 65×91 texels,
|
||||
1/709 of the glyph sheet, drawn at 115 px — asked for by "70.1, 11.5" (byte 0).
|
||||
```
|
||||
|
||||
`"70.1, 11.5"` is Atlas-CFM's readout and nothing else — two lines for a 1,200-frame run. The next
|
||||
session that does this will say so itself instead of needing to be correlated out of timestamps.
|
||||
|
||||
Quality was checked rather than assumed: four THICKOUTLINE rows at the ladder's worst points
|
||||
(0.889× minify, 1.118× magnify), same scene, both binaries. A control band with no rig text is
|
||||
**pixel-identical**, the tight bounding boxes of the 72 px and 90 px rows are identical to the
|
||||
pixel, and the text bands differ by 4.6–6.0% RMSE — edge antialiasing, invisible at 1:1.
|
||||
|
||||
Reference in New Issue
Block a user