the cooldown goes stock, early-Z comes back, and lists reach the end

- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-06 19:35:31 +07:00
co-authored by Claude Fable 5.1
parent c92ca1b8ec
commit c3555b4eea
93 changed files with 2423 additions and 1643 deletions
@@ -16,9 +16,7 @@ use crate::ui_text::UiFontAtlas;
use benilla_assets::WorldAssets;
mod colorselect;
mod cooldown;
mod text;
use cooldown::cooldown_quads;
/// `WOW_UI_COST=1` — the untraced per-frame cost meter for this system's phases (the premise
/// instrument for the UI epoch-gate lane, 0730's warm slice): one `[ui-cost]` line per frame with
@@ -94,7 +92,6 @@ fn report_ui_pick(eq: &benilla_ui::script::ExtractedQuad, rect: Rect, at: Vec2,
let what = match &eq.content {
QuadContent::Frame => "frame-slot".to_string(),
QuadContent::Minimap { .. } => "minimap".to_string(),
QuadContent::Cooldown { .. } => "cooldown".to_string(),
QuadContent::Texture {
path,
color,
@@ -118,7 +115,6 @@ fn content_kind(c: &QuadContent) -> &'static str {
match c {
QuadContent::Frame => "frame",
QuadContent::Minimap { .. } => "minimap",
QuadContent::Cooldown { .. } => "cooldown",
QuadContent::ModelPane { .. } => "modelpane",
QuadContent::Texture { .. } => "texture",
QuadContent::ColorWheel => "colorwheel",
@@ -354,7 +350,6 @@ fn span_bounds(spans: &[u32], i: usize) -> (usize, usize) {
fn splice_simple(eq: &benilla_ui::script::ExtractedQuad) -> bool {
match &eq.content {
QuadContent::Frame
| QuadContent::Cooldown { .. }
| QuadContent::Backdrop { .. }
// A model pane writes at most one quad and one idempotent tile request (decision 2008)
// — and the map's arrow changes facing on every turn while the map is open.
@@ -1310,14 +1305,6 @@ fn convert_entry(
alpha: eq.alpha,
});
}
// The Cooldown widget's pie wipe + finish flash (decision 0137 phase 4) — the
// byte-pinned look of `UI-Cooldown-Indicator.m2`, rebuilt natively (see
// [`cooldown_quads`]).
QuadContent::Cooldown { fraction, flash } => {
cooldown_quads(
rect, eq.z, eq.alpha, fraction, flash, clip, assets, images, out,
);
}
// A `<Model>`/`<PlayerModel>` pane's content: the off-screen body bake its window keeps,
// sampled square edge to edge. The pane→booth join is
// [`crate::portrait::model_pane_booth`], whose doc says why it is a table of frame names.
@@ -1328,8 +1315,9 @@ fn convert_entry(
// own file, which declares a bare `<PlayerModel>` and no Texture at all — so the widget
// itself has to draw, or the character sheet's paper doll is an empty rectangle.
//
// A pane with no name, or one no window claims, draws nothing (`SetModel` panes included:
// this engine holds their scene and renders no M2 into it). Nothing is stubbed white.
// A FILE pane is a tile (2013; the map arrow among them since 2015); a UNIT pane joins
// the booth its window keeps. A pane with no name, or one no window claims, draws
// nothing. Nothing is stubbed white.
QuadContent::ModelPane {
handle,
name,
@@ -1341,44 +1329,6 @@ fn convert_entry(
icon,
} => {
use crate::portrait::PortraitSource;
// The world-map arrow (decision 1980): the stock `WorldMapFrame.lua` /
// `Blizzard_BattlefieldMinimap.lua` create an anonymous `Model` child, `SetModel` it to
// the minimap arrow, and steer it with `SetFacing`/`SetPosition` every update. benilla
// draws that one file as the minimap's own arrow sprite — the same art the minimap
// ring draws (`crate::minimap::blips`), turned the same way (`-facing`: the M2's yaw
// is CCW-positive, the screen's is CW), filling the pane the engine sized to the
// reference's 33.6 px footprint times the arrow's `SetModelScale`. No 3D pane needs
// to exist for a two-triangle arrow.
if model.as_deref() == Some(benilla_ui::script::ARROW_MODEL) {
use crate::minimap::blips::{
PLAYER_ARROW_OFFSET_PX, PLAYER_ARROW_QUAD_PX, PLAYER_ARROW_TEXTURE,
};
let Some(handle) = assets
.as_mut()
.and_then(|a| a.sprite_texture(PLAYER_ARROW_TEXTURE, images))
else {
return;
};
// The M2's single quad sits off its origin by an authored offset that turns with
// the facing — the minimap's arrow applies the same one at its 33.6 px basis, and
// the pane the engine sized (`worldmap_arrow.rs`: the footprint times the
// `SetModelScale`) scales it here. `model_scale` is already in the rect.
let rotation = -facing;
let (sin, cos) = rotation.sin_cos();
let off = PLAYER_ARROW_OFFSET_PX * (rect.width() / PLAYER_ARROW_QUAD_PX);
let off = Vec2::new(off.x * cos - off.y * sin, off.x * sin + off.y * cos);
let rect = Rect::from_center_size(rect.center() + off, rect.size());
out.push(UiQuad {
rect,
z_key: eq.z,
texture: Some(handle),
color: [1.0, 1.0, 1.0, eq.alpha],
rotation,
clip,
..default()
});
return;
}
// A FILE pane (decision 2008): publish what the tile renderer needs — the pane's
// device-pixel size and the render law's unit ladder off it — and draw the tile's
// atlas cell when the renderer has one. The request is idempotent, so the memoized