Files
benilla-octo/crates/benilla-ui/src/layout.rs
T
gromandClaude Opus 5 0fe0a99d8a layout: a size the author set is not a question for the solver
`GetWidth`/`GetHeight` answer against current layout, so they resolve first — and
a resolve that follows any `CreateFrame` re-derives the whole layout graph, because
minting a frame id drops the cached one (`Model::frame_id` → `touch_layout`). A UI
load is nothing but creation interleaved with reads, so the player's interface paid
**839 whole-roster derivations in one load, 2.84 ms each: 2.4 s**, against 650
`GetWidth` calls. `WOW_LAYOUT_DERIVE_TRACE` named the giver-up: every sampled one
was `create_frame`.

Most of those reads cannot learn anything from a resolve. An edge takes its anchor
scan first and only then `combineEdge(center, opposite, size · scale)`
(`resolve_left`, `resolve_right`), so an authored size IS the span unless the
anchors pin **both** of that axis's edges. `LayoutInput::authored_span` is that
rule, written beside the solver whose precedence it mirrors, and it refuses the two
cases where only the solver knows: both edges anchored, and a clamped frame, whose
rect the screen can squeeze.

Measured on the player's own 91 addons through the harness's `--together` load,
three passes each:

```
derives per load      839  →  235
preamble per load   2,414  →  560 ms
frame:GetWidth      686 µs →  60 µs per call  (1,241 ms → 118 ms over three loads)
```

**1.85 seconds off every login and every `/reload`**, and the steady state gains
the same way: an addon reading a size it set in an `OnUpdate` no longer walks the
roster for it.

Proven as well as measured: six UI captures (character, bag, merchant, quest, quest
log, tooltip) are **pixel-identical** before and after, MAE 0.000, max 0; the
layout suite passes under `WOW_LAYOUT_VERIFY`, which re-derives every frame from
scratch and demands the same rects; and `resolve_bench` now asserts both halves —
an authored, singly-anchored span answers with no gate walk, while one whose
anchors pin both edges still forces the resolve and reports the anchors' distance,
not the authored number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-18 18:51:45 -06:00

1377 lines
46 KiB
Rust

//! The frame anchor resolver: the 1.12 client's `CLayoutFrame` rect resolution, transcribed
//! function by function, with the reference's operations in the reference's order.
//!
//! The client computes on the x87 at 53-bit precision, which `f64` matches, and narrows to `f32`
//! only at its stores (each sub-edge before a `combine_*`, each final edge, the `width * scale`
//! span), so the arithmetic here runs in `f64` with an explicit `as f32` at exactly those sites.
//!
//! Rects are `[bottom, left, top, right]` (frame `+0x64..+0x70`, `GetRect` `0x768320`) in screen
//! pixels, y-up.
use std::collections::VecDeque;
// ── Constants ────────────────────────────────────────────────────────────────────────────────
/// The unset-coordinate sentinel, `+Inf` (`0xcf550c`, set at runtime from `0x81d56c`).
const UNSET: f64 = f64::INFINITY;
/// The least width or height change that fires `OnSizeChanged`: 2^-22, bits `0x34800000` at
/// `0x8029d4`, tested in `ApplyRect` (`0x76b580`).
pub const SIZE_EPS: f64 = 2.384_185_791_015_625e-7;
/// The least edge move against the cached rect for the client to re-store it and `ApplyRect`, 1e-5
/// (`0x80c5c8`, in `0x768d20`). Recorded, not applied: the caller's own dirty gate decides.
pub const RESOLVE_EPS: f64 = f32::from_bits(0x3727_c5ac) as f64;
/// Point id to X column: left 0, center 1, right 2.
const X_COL: [u8; 9] = [0, 1, 2, 0, 1, 2, 0, 1, 2];
/// Point id to Y row: top 0, center 1, bottom 2.
const Y_ROW: [u8; 9] = [0, 0, 0, 1, 1, 1, 2, 2, 2];
// ── Public types ─────────────────────────────────────────────────────────────────────────────
/// The nine anchor points; the discriminant is the client's point id, the slot in its
/// `anchorPoints[9]` (tables at `0x81c3b8..0x81c3fc`).
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Point {
TopLeft = 0,
Top = 1,
TopRight = 2,
Left = 3,
Center = 4,
Right = 5,
BottomLeft = 6,
Bottom = 7,
BottomRight = 8,
}
impl Point {
#[inline]
pub const fn id(self) -> u8 {
self as u8
}
pub const fn from_id(id: u8) -> Option<Point> {
Some(match id {
0 => Point::TopLeft,
1 => Point::Top,
2 => Point::TopRight,
3 => Point::Left,
4 => Point::Center,
5 => Point::Right,
6 => Point::BottomLeft,
7 => Point::Bottom,
8 => Point::BottomRight,
_ => return None,
})
}
}
/// An anchor target, which the caller maps to a frame or an external rect (`CAnchor+0xc`).
pub type Handle = u32;
/// A resolved rect, `[bottom, left, top, right]` in screen pixels, y-up.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rect {
pub bottom: f32,
pub left: f32,
pub top: f32,
pub right: f32,
}
impl Rect {
pub const fn new(bottom: f32, left: f32, top: f32, right: f32) -> Rect {
Rect {
bottom,
left,
top,
right,
}
}
#[inline]
pub const fn to_array(self) -> [f32; 4] {
[self.bottom, self.left, self.top, self.right]
}
#[inline]
pub const fn from_array(a: [f32; 4]) -> Rect {
Rect::new(a[0], a[1], a[2], a[3])
}
#[inline]
pub fn width(self) -> f32 {
self.right - self.left
}
#[inline]
pub fn height(self) -> f32 {
self.top - self.bottom
}
}
/// One anchor, a client `CAnchor` (`xOff +4`, `yOff +8`, `relativeTo +0xc`, `relativePoint +0x10`):
/// `point` on this frame pins to `relative_point` on the target.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Anchor {
pub point: Point,
pub relative_to: Handle,
pub relative_point: Point,
pub x_off: f32,
pub y_off: f32,
}
impl Anchor {
pub fn new(
point: Point,
relative_to: Handle,
relative_point: Point,
x_off: f32,
y_off: f32,
) -> Anchor {
Anchor {
point,
relative_to,
relative_point,
x_off,
y_off,
}
}
}
/// A frame's layout input, the geometry fields the client's resolver reads: `anchorPoints[9]`
/// (`G+0x4`), `width` (`G+0x50`), `height` (`G+0x54`), `layoutScale` (`G+0x58`) and the clamp flag
/// (bit 4 of `G+0x3c`).
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutInput {
/// Slotted by `point`, the later of two on one point winning (`SetPoint`, `0x767c70`).
pub anchors: Vec<Anchor>,
/// `0.0` is the no-size sentinel (`0x7ffd74`): the axis derives from the anchors.
pub width: f32,
pub height: f32,
/// The effective scale, parent times own (`0x76ac90`), applied only to the anchor offsets and
/// the size span.
pub scale: f32,
/// Clamp the rect into `[0, extent]` on each axis.
pub clamp: bool,
/// The clamp's X extent; the client's default is `0.8` (`0x41ae60`).
pub extent_x: f32,
/// The clamp's Y extent; the client's default is `0.6` (`0x41ae70`).
pub extent_y: f32,
}
impl Default for LayoutInput {
fn default() -> LayoutInput {
LayoutInput {
anchors: Vec::new(),
width: 0.0,
height: 0.0,
scale: 1.0,
clamp: false,
extent_x: 0.8,
extent_y: 0.6,
}
}
}
impl LayoutInput {
/// The span this axis reports **without resolving anything**, or `None` when only the solver
/// can say.
///
/// `GetWidth`/`GetHeight` answer against current layout, so they resolve first, and during a
/// UI load that is the dominant cost in the whole client: every frame an addon creates drops
/// the cached layout graph, so the next geometry read re-derives it over every frame and
/// region there is. The player's own interface measured **839 of those derives in one load,
/// 2.84 ms each — 2.4 s**, against 650 `GetWidth` calls.
///
/// Most of those reads cannot learn anything from a resolve. An axis is authored (`width`
/// non-zero) and unclamped, so the solver would compute the same number back: an edge takes
/// its anchor scan first and only then `combineEdge(center, opposite, size · scale)`
/// ([`resolve_left`], [`resolve_right`]), so the authored size IS the span unless anchors pin
/// **both** of the axis's edges — the one case this refuses, along with a clamped frame, whose
/// rect the screen can squeeze.
///
/// The value is in the frame's own units, as the getters report: the solver multiplies the
/// span by the scale and the getter divides it back out.
pub fn authored_span(&self, width: bool) -> Option<f32> {
let size = if width { self.width } else { self.height };
if size == 0.0 || self.clamp {
return None;
}
let (lo, hi) = if width {
(IDS_LEFT, IDS_RIGHT)
} else {
(IDS_BOTTOM, IDS_TOP)
};
let pinned = |ids: [usize; 3]| {
self.anchors
.iter()
.any(|a| ids.contains(&(a.point.id() as usize)))
};
(!(pinned(lo) && pinned(hi))).then_some(size)
}
/// A frame with the given anchors and explicit size, scale `1.0`, no clamp.
pub fn sized(anchors: Vec<Anchor>, width: f32, height: f32) -> LayoutInput {
LayoutInput {
anchors,
width,
height,
..LayoutInput::default()
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ResolveOutcome {
Resolved(Rect),
/// An edge stayed `+Inf`: the client's `assemble` returns 0 and its caller sets flags bit 3.
Unresolvable,
}
// ── Leaf kernels: each returns the un-narrowed `f64` ─────────────────────────────────────────
/// `combineEdge` (`0x767440`): an edge from the opposite edge and the signed `size * scale` span,
/// else from the axis center. The sentinels compare by IEEE equality, as the client's `fcomp`
/// does, so a NaN never matches one.
#[allow(clippy::float_cmp)]
pub fn combine_edge(center: f32, opp: f32, span: f32) -> f64 {
let (c, o, s) = (f64::from(center), f64::from(opp), f64::from(span));
if o != UNSET && s != 0.0 {
return o + s;
}
if c != UNSET {
if s != 0.0 {
return s * 0.5 + c;
}
if o != UNSET {
return (c - o) + c;
}
}
UNSET
}
/// `combineCenter` (`0x7672d0`): an axis center from the low and high edges and the signed span.
#[allow(clippy::float_cmp)]
pub fn combine_center(lo: f32, hi: f32, span: f32) -> f64 {
let (l, h, s) = (f64::from(lo), f64::from(hi), f64::from(span));
if l != UNSET {
if h != UNSET {
return (l + h) * 0.5;
}
if s != 0.0 {
return s * 0.5 + l;
}
}
if h != UNSET && s != 0.0 {
return h - s * 0.5;
}
UNSET
}
/// `CAnchor::ResolveX` (`0x7a2f90`): the anchor's X against its target's cached `rect`, which is
/// `None` when `GetRect` fails. `mirror` is the target's normalize predicate (vtable `+0x24`, 0 for
/// an ordinary frame at `0x46ff60`), which shifts the edges to start at 0, each stored as `f32`.
pub fn anchor_resolve_x(
scale: f32,
x_off: f32,
rel_point: u32,
rect: Option<[f32; 4]>,
mirror: bool,
) -> f64 {
let Some([_bottom, mut left, _top, mut right]) = rect else {
return UNSET;
};
if mirror {
right = (-f64::from(left) + f64::from(right)) as f32;
left = 0.0;
}
if rel_point > 8 {
return UNSET;
}
let (s, xo) = (f64::from(scale), f64::from(x_off));
let (l, r) = (f64::from(left), f64::from(right));
match X_COL[rel_point as usize] {
0 => s * xo + l, // relLeft
1 => (r + l) * 0.5 + s * xo, // center
_ => s * xo + r, // relRight
}
}
/// `CAnchor::ResolveY` (`0x7a3070`), the Y counterpart of [`anchor_resolve_x`].
pub fn anchor_resolve_y(
scale: f32,
y_off: f32,
rel_point: u32,
rect: Option<[f32; 4]>,
mirror: bool,
) -> f64 {
let Some([mut bottom, _left, mut top, _right]) = rect else {
return UNSET;
};
if mirror {
top = (-f64::from(bottom) + f64::from(top)) as f32;
bottom = 0.0;
}
if rel_point > 8 {
return UNSET;
}
let (s, yo) = (f64::from(scale), f64::from(y_off));
let (b, t) = (f64::from(bottom), f64::from(top));
match Y_ROW[rel_point as usize] {
0 => s * yo + t, // relTop
1 => (t + b) * 0.5 + s * yo, // center
_ => s * yo + b, // relBottom
}
}
/// [`assemble_rect`]'s result: `ok` when every edge resolved.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AssembleRect {
pub ok: bool,
/// `[bottom, left, top, right]`, meaningful only when `ok`.
pub rect: [f32; 4],
}
/// `assemble` (`0x767a20`): fails if any edge is `+Inf`, else with `clamp` (flags bit 4) shifts
/// the rect into `[0, extent]` on each axis, low edge first, keeping its size.
#[allow(clippy::float_cmp)]
pub fn assemble_rect(edges: [f32; 4], clamp: bool, extent_x: f32, extent_y: f32) -> AssembleRect {
let [mut bottom, mut left, mut top, mut right] = edges;
let inf = f32::INFINITY;
if bottom == inf || left == inf || top == inf || right == inf {
return AssembleRect {
ok: false,
rect: edges,
};
}
if clamp {
if (left as f64) < 0.0 {
right = (f64::from(right) - f64::from(left)) as f32;
left = 0.0;
}
if (bottom as f64) < 0.0 {
top = (f64::from(top) - f64::from(bottom)) as f32;
bottom = 0.0;
}
if (right as f64) > (extent_x as f64) {
let over = f64::from(right) - f64::from(extent_x);
left = (f64::from(left) - over) as f32;
right = extent_x;
}
if (top as f64) > (extent_y as f64) {
let over = f64::from(top) - f64::from(extent_y);
bottom = (f64::from(bottom) - over) as f32;
top = extent_y;
}
}
AssembleRect {
ok: true,
rect: [bottom, left, top, right],
}
}
/// `ApplyRect`'s `OnSizeChanged` test (`0x76b580`): width or height moved by at least
/// [`SIZE_EPS`], the deltas taken in `f64` from the `f32` edges.
pub fn size_changed(old: Rect, new: Rect) -> bool {
let dw =
(f64::from(new.right) - f64::from(new.left)) - (f64::from(old.right) - f64::from(old.left));
let dh =
(f64::from(new.top) - f64::from(new.bottom)) - (f64::from(old.top) - f64::from(old.bottom));
dw.abs() >= SIZE_EPS || dh.abs() >= SIZE_EPS
}
// ── The composed resolver: `assemble` over six recursion-guarded edge resolvers ──────────────
/// An [`Anchor`] with its target's rect looked up, `None` where `GetRect` would fail.
#[derive(Clone, Copy)]
struct AnchorRel {
x_off: f32,
y_off: f32,
rel_point: u32,
rel_rect: Option<[f32; 4]>,
mirror: bool,
}
struct EdgeInput {
anchors: [Option<AnchorRel>; 9],
width: f32,
height: f32,
scale: f32,
clamp: bool,
extent_x: f32,
extent_y: f32,
}
// The recursion-guard bits of the client's `G+0x28`.
const G_LEFT: u32 = 0x01;
const G_TOP: u32 = 0x02;
const G_RIGHT: u32 = 0x04;
const G_BOTTOM: u32 = 0x08;
const G_XC: u32 = 0x10;
const G_YC: u32 = 0x20;
// The point ids each edge scans (`.rdata` 0x81c3b8..0x81c3f4).
const IDS_LEFT: [usize; 3] = [0, 3, 6];
const IDS_RIGHT: [usize; 3] = [2, 5, 8];
const IDS_TOP: [usize; 3] = [0, 1, 2];
const IDS_BOTTOM: [usize; 3] = [6, 7, 8];
const IDS_XC: [usize; 3] = [1, 4, 7];
const IDS_YC: [usize; 3] = [3, 4, 5];
/// `anchorScanX` (`0x7671a0`): the first of the edge's anchors whose X resolves, else `+Inf`.
#[allow(clippy::float_cmp)]
fn anchor_scan_x(ids: &[usize; 3], f: &EdgeInput) -> f64 {
for &id in ids {
if let Some(a) = f.anchors[id] {
let v = anchor_resolve_x(f.scale, a.x_off, a.rel_point, a.rel_rect, a.mirror);
if v != UNSET {
return v;
}
}
}
UNSET
}
/// `anchorScanY` (`0x7671f0`).
#[allow(clippy::float_cmp)]
fn anchor_scan_y(ids: &[usize; 3], f: &EdgeInput) -> f64 {
for &id in ids {
if let Some(a) = f.anchors[id] {
let v = anchor_resolve_y(f.scale, a.y_off, a.rel_point, a.rel_rect, a.mirror);
if v != UNSET {
return v;
}
}
}
UNSET
}
/// The edge resolvers' span, `size * scale` narrowed to `f32`, negated for LEFT and BOTTOM. The
/// client reads `size` through a virtual getter: a frame's is the authored field (`0x768420`), but
/// a texture (`0x770720`) and a font string (`0x772930`) replace an authored `0.0` with their
/// content's extent, so a region's [`LayoutInput`] must already carry that extent.
fn size_span(size: f32, scale: f32, negate: bool) -> f32 {
let span = (f64::from(size) * f64::from(scale)) as f32;
if negate {
-span
} else {
span
}
}
/// LEFT (`0x7673d0`): its anchor scan, else `combineEdge(Xcenter, RIGHT, -width * scale)`.
#[allow(clippy::float_cmp)]
fn resolve_left(f: &EdgeInput, guard: &mut u32) -> f64 {
if *guard & G_LEFT != 0 {
return UNSET;
}
*guard |= G_LEFT;
let v = anchor_scan_x(&IDS_LEFT, f);
let r = if v != UNSET {
v
} else {
let span = size_span(f.width, f.scale, true);
let opp = resolve_right(f, guard) as f32;
let center = resolve_xcenter(f, guard) as f32;
combine_edge(center, opp, span)
};
*guard &= !G_LEFT;
r
}
/// RIGHT (`0x767540`): its anchor scan, else `combineEdge(Xcenter, LEFT, width * scale)`.
#[allow(clippy::float_cmp)]
fn resolve_right(f: &EdgeInput, guard: &mut u32) -> f64 {
if *guard & G_RIGHT != 0 {
return UNSET;
}
*guard |= G_RIGHT;
let v = anchor_scan_x(&IDS_RIGHT, f);
let r = if v != UNSET {
v
} else {
let span = size_span(f.width, f.scale, false);
let opp = resolve_left(f, guard) as f32;
let center = resolve_xcenter(f, guard) as f32;
combine_edge(center, opp, span)
};
*guard &= !G_RIGHT;
r
}
/// TOP (`0x7674d0`): its anchor scan, else `combineEdge(Ycenter, BOTTOM, height * scale)`.
#[allow(clippy::float_cmp)]
fn resolve_top(f: &EdgeInput, guard: &mut u32) -> f64 {
if *guard & G_TOP != 0 {
return UNSET;
}
*guard |= G_TOP;
let v = anchor_scan_y(&IDS_TOP, f);
let r = if v != UNSET {
v
} else {
let span = size_span(f.height, f.scale, false);
let opp = resolve_bottom(f, guard) as f32;
let center = resolve_ycenter(f, guard) as f32;
combine_edge(center, opp, span)
};
*guard &= !G_TOP;
r
}
/// BOTTOM (`0x7675b0`): its anchor scan, else `combineEdge(Ycenter, TOP, -height * scale)`.
#[allow(clippy::float_cmp)]
fn resolve_bottom(f: &EdgeInput, guard: &mut u32) -> f64 {
if *guard & G_BOTTOM != 0 {
return UNSET;
}
*guard |= G_BOTTOM;
let v = anchor_scan_y(&IDS_BOTTOM, f);
let r = if v != UNSET {
v
} else {
let span = size_span(f.height, f.scale, true);
let opp = resolve_top(f, guard) as f32;
let center = resolve_ycenter(f, guard) as f32;
combine_edge(center, opp, span)
};
*guard &= !G_BOTTOM;
r
}
/// Xcenter (`0x767260`): its anchor scan, else `combineCenter(LEFT, RIGHT, width * scale)`.
#[allow(clippy::float_cmp)]
fn resolve_xcenter(f: &EdgeInput, guard: &mut u32) -> f64 {
if *guard & G_XC != 0 {
return UNSET;
}
*guard |= G_XC;
let v = anchor_scan_x(&IDS_XC, f);
let r = if v != UNSET {
v
} else {
let span = size_span(f.width, f.scale, false);
let lo = resolve_left(f, guard) as f32;
let hi = resolve_right(f, guard) as f32;
combine_center(lo, hi, span)
};
*guard &= !G_XC;
r
}
/// Ycenter (`0x767360`): its anchor scan, else `combineCenter(BOTTOM, TOP, height * scale)`.
#[allow(clippy::float_cmp)]
fn resolve_ycenter(f: &EdgeInput, guard: &mut u32) -> f64 {
if *guard & G_YC != 0 {
return UNSET;
}
*guard |= G_YC;
let v = anchor_scan_y(&IDS_YC, f);
let r = if v != UNSET {
v
} else {
let span = size_span(f.height, f.scale, false);
let lo = resolve_bottom(f, guard) as f32;
let hi = resolve_top(f, guard) as f32;
combine_center(lo, hi, span)
};
*guard &= !G_YC;
r
}
/// `assemble` (`0x767a20`) composed, resolving LEFT, BOTTOM, RIGHT, TOP in the client's order; the
/// `+Inf` test reads the un-narrowed `f64` edge, as the client's `fcomp` precedes its `fst`.
#[allow(clippy::float_cmp)]
fn resolve_edges(f: &EdgeInput) -> ResolveOutcome {
let mut guard = 0u32;
let left = resolve_left(f, &mut guard);
let bottom = resolve_bottom(f, &mut guard);
let right = resolve_right(f, &mut guard);
let top = resolve_top(f, &mut guard);
if left == UNSET || bottom == UNSET || right == UNSET || top == UNSET {
return ResolveOutcome::Unresolvable;
}
let edges = [bottom as f32, left as f32, top as f32, right as f32];
let a = assemble_rect(edges, f.clamp, f.extent_x, f.extent_y);
ResolveOutcome::Resolved(Rect::from_array(a.rect))
}
/// Resolve one frame's rect; `target_rect` gives each anchor target's resolved rect, and `None`
/// makes that anchor `+Inf`, as a failed `GetRect` does.
pub fn resolve_rect(
frame: &LayoutInput,
target_rect: impl FnMut(Handle) -> Option<Rect>,
) -> ResolveOutcome {
resolve_edges(&build_edge_input(frame, target_rect))
}
fn build_edge_input(
frame: &LayoutInput,
mut target_rect: impl FnMut(Handle) -> Option<Rect>,
) -> EdgeInput {
let mut slots: [Option<AnchorRel>; 9] = [None; 9];
for a in &frame.anchors {
slots[a.point.id() as usize] = Some(AnchorRel {
x_off: a.x_off,
y_off: a.y_off,
rel_point: a.relative_point.id() as u32,
rel_rect: target_rect(a.relative_to).map(Rect::to_array),
// An ordinary frame's normalize predicate returns 0 (`0x46ff60`).
mirror: false,
});
}
EdgeInput {
anchors: slots,
width: frame.width,
height: frame.height,
scale: frame.scale,
clamp: frame.clamp,
extent_x: frame.extent_x,
extent_y: frame.extent_y,
}
}
/// Resolve one frame's edges, `[bottom, left, top, right]`, each `None` where it stayed unset;
/// `assemble` and its clamp are not applied. The region pass reads it axis by axis.
#[allow(clippy::float_cmp)]
pub fn resolve_rect_edges(
frame: &LayoutInput,
target_rect: impl FnMut(Handle) -> Option<Rect>,
) -> [Option<f32>; 4] {
let input = build_edge_input(frame, target_rect);
let mut guard = 0u32;
let left = resolve_left(&input, &mut guard);
let bottom = resolve_bottom(&input, &mut guard);
let right = resolve_right(&input, &mut guard);
let top = resolve_top(&input, &mut guard);
let cvt = |v: f64| if v == UNSET { None } else { Some(v as f32) };
[cvt(bottom), cvt(left), cvt(top), cvt(right)]
}
// ── Graph driver ─────────────────────────────────────────────────────────────────────────────
/// Resolves anchored frames in dependency order, a driver of benilla's own: the client resolves
/// lazily through a dependency-ordered deferred queue (`0x7680e0`, ordered by `0x7681b0`). Handles
/// index its arrays directly, so they must be dense. A round: [`begin`](Self::begin),
/// [`set_external`](Self::set_external) and [`set_frame`](Self::set_frame), [`solve`](Self::solve).
#[derive(Clone, Debug, Default)]
pub struct LayoutSolver {
/// Overwritten in place, so each anchors `Vec` keeps its allocation.
input: Vec<LayoutInput>,
is_frame: Vec<bool>,
/// Externals as seeded, frames as they resolve.
rect: Vec<Option<Rect>>,
/// Kahn in-degree and reverse edges; each inner `Vec` is cleared, never freed.
indeg: Vec<u32>,
dependents: Vec<Vec<Handle>>,
/// The handles written this round, the ones `begin` resets.
touched: Vec<Handle>,
marked: Vec<bool>,
/// Ascending: cycle members resolve in this order, and the result depends on it.
live: Vec<Handle>,
queue: VecDeque<Handle>,
emitted: Vec<bool>,
order: Vec<Handle>,
cycle: Vec<Handle>,
unresolvable: Vec<Handle>,
}
impl LayoutSolver {
pub fn new() -> LayoutSolver {
LayoutSolver::default()
}
fn ensure(&mut self, h: Handle) {
let need = h as usize + 1;
if self.input.len() < need {
self.input.resize_with(need, LayoutInput::default);
self.is_frame.resize(need, false);
self.rect.resize(need, None);
self.indeg.resize(need, 0);
self.dependents.resize_with(need, Vec::new);
self.marked.resize(need, false);
self.emitted.resize(need, false);
}
}
/// Record `h` for [`begin`](Self::begin) to reset.
fn touch(&mut self, h: Handle) {
let i = h as usize;
if !self.marked[i] {
self.marked[i] = true;
self.touched.push(h);
}
}
/// Start a round, resetting only the slots the last round touched.
pub fn begin(&mut self) {
for &h in &self.touched {
let i = h as usize;
self.is_frame[i] = false;
self.rect[i] = None;
self.indeg[i] = 0;
self.dependents[i].clear();
self.marked[i] = false;
self.emitted[i] = false;
}
self.touched.clear();
self.live.clear();
self.queue.clear();
self.order.clear();
self.cycle.clear();
self.unresolvable.clear();
}
/// Seed a rect this solve does not compute, such as the screen root (`CSimpleTop`); also
/// publishes one mid-sweep for later nodes of the same pass.
pub fn set_external(&mut self, h: Handle, rect: Rect) {
self.ensure(h);
self.touch(h);
self.rect[h as usize] = Some(rect);
}
/// Register a frame to solve, copying `src` into its slot; a steady round allocates nothing.
pub fn set_frame(&mut self, h: Handle, src: &LayoutInput) {
self.ensure(h);
self.touch(h);
let dst = &mut self.input[h as usize];
dst.anchors.clear();
dst.anchors.extend_from_slice(&src.anchors);
Self::copy_fields(dst, src);
self.is_frame[h as usize] = true;
self.live.push(h);
}
fn copy_fields(dst: &mut LayoutInput, src: &LayoutInput) {
dst.width = src.width;
dst.height = src.height;
dst.scale = src.scale;
dst.clamp = src.clamp;
dst.extent_x = src.extent_x;
dst.extent_y = src.extent_y;
}
/// Resolve every registered frame, anchor targets first; cycle members then resolve
/// best-effort, ascending by handle, against the rects known by then.
pub fn solve(&mut self) {
self.live.sort_unstable();
// Only frame-to-frame anchors order the pass. A self-anchor counts, so a self-anchored
// frame lands in `cycle`; a target named twice counts once.
for idx in 0..self.live.len() {
let h = self.live[idx];
let mut seen: [Handle; 9] = [0; 9];
let mut n_seen = 0usize;
for a_idx in 0..self.input[h as usize].anchors.len() {
let d = self.input[h as usize].anchors[a_idx].relative_to;
if !self.is_frame.get(d as usize).copied().unwrap_or(false) {
continue;
}
if seen[..n_seen].contains(&d) {
continue;
}
if n_seen < seen.len() {
seen[n_seen] = d;
n_seen += 1;
}
self.indeg[h as usize] += 1;
self.dependents[d as usize].push(h);
}
}
for &h in &self.live {
if self.indeg[h as usize] == 0 {
self.queue.push_back(h);
}
}
while let Some(h) = self.queue.pop_front() {
self.order.push(h);
self.emitted[h as usize] = true;
for i in 0..self.dependents[h as usize].len() {
let dep = self.dependents[h as usize][i];
let e = &mut self.indeg[dep as usize];
*e -= 1;
if *e == 0 {
self.queue.push_back(dep);
}
}
}
for i in 0..self.order.len() {
let h = self.order[i];
self.resolve_one(h);
}
// A frame never emitted is on or behind a cycle.
for idx in 0..self.live.len() {
let h = self.live[idx];
if !self.emitted[h as usize] {
self.cycle.push(h);
self.resolve_one(h);
}
}
}
fn resolve_one(&mut self, h: Handle) {
let outcome = {
let input = &self.input[h as usize];
let rects = &self.rect;
resolve_rect(input, |t| rects.get(t as usize).copied().flatten())
};
match outcome {
ResolveOutcome::Resolved(r) => self.rect[h as usize] = Some(r),
ResolveOutcome::Unresolvable => self.unresolvable.push(h),
}
}
/// The rect at `h`: an external as seeded, a frame as resolved.
#[inline]
pub fn rect(&self, h: Handle) -> Option<Rect> {
self.rect.get(h as usize).copied().flatten()
}
/// Frames on or behind an anchor cycle, ascending.
pub fn cycle(&self) -> &[Handle] {
&self.cycle
}
/// Frames that did not resolve; may overlap [`cycle`](Self::cycle).
pub fn unresolvable(&self) -> &[Handle] {
&self.unresolvable
}
}
#[cfg(test)]
mod tests {
use super::*;
const SCREEN: Handle = 0;
fn screen_rect() -> Rect {
Rect::new(0.0, 0.0, 600.0, 800.0)
}
fn bits(r: Rect) -> [u32; 4] {
[
r.bottom.to_bits(),
r.left.to_bits(),
r.top.to_bits(),
r.right.to_bits(),
]
}
fn assert_rect(got: ResolveOutcome, want: Rect) {
match got {
ResolveOutcome::Resolved(r) => {
assert_eq!(bits(r), bits(want), "got {r:?} want {want:?}")
}
ResolveOutcome::Unresolvable => panic!("unresolvable, wanted {want:?}"),
}
}
// ── Leaf kernels ─────────────────────────────────────────────────────────────────────────
// Expected values are hand-computed from the formulas, not taken from the client.
#[test]
fn combine_edge_legs() {
assert_eq!(combine_edge(10.0, 50.0, 7.0), 57.0); // opp+span
assert_eq!(combine_edge(10.0, f32::INFINITY, 7.0), 7.0 * 0.5 + 10.0); // span/2+center
assert_eq!(combine_edge(10.0, 50.0, 0.0), (10.0 - 50.0) + 10.0); // 2c-opp = -30
assert_eq!(
combine_edge(f32::INFINITY, f32::INFINITY, 0.0),
f64::INFINITY
);
assert_eq!(combine_edge(f32::INFINITY, 50.0, 0.0), f64::INFINITY);
assert_eq!(combine_edge(10.0, f32::INFINITY, 0.0), f64::INFINITY);
// -0.0 span == 0.0 (IEEE) -> the 2c-opp leg
assert_eq!(combine_edge(10.0, 50.0, -0.0), (10.0 - 50.0) + 10.0);
}
#[test]
fn combine_center_legs() {
assert_eq!(combine_center(10.0, 50.0, 7.0), (10.0 + 50.0) * 0.5); // both -> midpoint
assert_eq!(combine_center(10.0, f32::INFINITY, 7.0), 7.0 * 0.5 + 10.0); // span/2+lo
assert_eq!(combine_center(f32::INFINITY, 50.0, 7.0), 50.0 - 7.0 * 0.5); // hi-span/2
assert_eq!(
combine_center(f32::INFINITY, f32::INFINITY, 7.0),
f64::INFINITY
);
assert_eq!(combine_center(10.0, f32::INFINITY, 0.0), f64::INFINITY);
// both finite -> midpoint regardless of span
assert_eq!(combine_center(10.0, 50.0, -0.0), 30.0);
}
#[test]
fn anchor_resolve_x_columns() {
let r = [3.0f32, 100.0, 480.0, 260.0]; // [bottom, left, top, right]
// point ids 0, 1, 2 (left, center, right), xOff 5
assert_eq!(anchor_resolve_x(1.0, 5.0, 0, Some(r), false), 5.0 + 100.0);
assert_eq!(
anchor_resolve_x(1.0, 5.0, 1, Some(r), false),
(260.0 + 100.0) * 0.5 + 5.0
);
assert_eq!(anchor_resolve_x(1.0, 5.0, 2, Some(r), false), 5.0 + 260.0);
assert_eq!(
anchor_resolve_x(2.0, 5.0, 0, Some(r), false),
2.0 * 5.0 + 100.0
);
// GetRect-fail (None) -> +Inf; out-of-range point -> +Inf
assert_eq!(anchor_resolve_x(1.0, 5.0, 0, None, false), f64::INFINITY);
assert_eq!(anchor_resolve_x(1.0, 5.0, 9, Some(r), false), f64::INFINITY);
// mirror: left->0, right->right-left; center = (r'+0)/2 + xOff
let rp = (260.0f32 - 100.0f32) as f64;
assert_eq!(
anchor_resolve_x(1.0, 0.0, 1, Some(r), true),
(rp + 0.0) * 0.5
);
}
#[test]
fn anchor_resolve_y_rows() {
let r = [3.0f32, 100.0, 480.0, 260.0];
// relTop, center, relBottom (rows: pt 0->top, 3->center, 6->bottom)
assert_eq!(anchor_resolve_y(1.0, 5.0, 0, Some(r), false), 5.0 + 480.0);
assert_eq!(
anchor_resolve_y(1.0, 5.0, 3, Some(r), false),
(480.0 + 3.0) * 0.5 + 5.0
);
assert_eq!(anchor_resolve_y(1.0, 5.0, 6, Some(r), false), 5.0 + 3.0);
}
// ── assemble / clamp ────────────────────────────────────────────────────────────────────
#[test]
fn assemble_passthrough_and_fail() {
let a = assemble_rect([3.0, 100.0, 480.0, 260.0], false, 0.8, 0.6);
assert!(a.ok);
assert_eq!(a.rect, [3.0, 100.0, 480.0, 260.0]);
let bad = assemble_rect([3.0, f32::INFINITY, 480.0, 260.0], false, 0.8, 0.6);
assert!(!bad.ok);
}
#[test]
fn assemble_clamp_low_edge() {
// left < 0 -> shift to 0, carry span to right
let a = assemble_rect([0.1, -0.2, 0.5, 0.3], true, 0.8, 0.6);
assert!(a.ok);
assert_eq!(a.rect[1], 0.0); // left
// right = 0.3 - (-0.2) computed via f64 then narrowed
let want_right = (f64::from(0.3f32) - f64::from(-0.2f32)) as f32;
assert_eq!(a.rect[3].to_bits(), want_right.to_bits());
}
#[test]
fn assemble_clamp_high_edge() {
// right > extent_x -> shift back, right = extent
let a = assemble_rect([0.1, 0.1, 0.5, 1.5], true, 0.8, 0.6);
assert!(a.ok);
assert_eq!(a.rect[3], 0.8);
let over = f64::from(1.5f32) - f64::from(0.8f32);
let want_left = (f64::from(0.1f32) - over) as f32;
assert_eq!(a.rect[1].to_bits(), want_left.to_bits());
}
// ── Composed resolver ─────────────────────────────────────────────────────────────────────
/// The rect the running reference client's `assemble` (`0x767a20`) produced.
#[test]
fn oracle_single_topleft_explicit_size() {
let f = LayoutInput::sized(
vec![Anchor::new(
Point::TopLeft,
SCREEN,
Point::TopLeft,
10.0,
-5.0,
)],
200.0,
50.0,
);
// The reference run's screen root: top 600, right 800.
let got = resolve_rect(&f, |_| Some(Rect::new(0.0, 0.0, 600.0, 800.0)));
assert_rect(got, Rect::new(545.0, 10.0, 595.0, 210.0));
}
#[test]
fn single_anchor_all_nine_points_explicit_size() {
// Each point pinned to the same point on the screen: every leg must keep the 100x40 size.
let screen = screen_rect();
for &p in &[
Point::TopLeft,
Point::Top,
Point::TopRight,
Point::Left,
Point::Center,
Point::Right,
Point::BottomLeft,
Point::Bottom,
Point::BottomRight,
] {
let f = LayoutInput::sized(vec![Anchor::new(p, SCREEN, p, 0.0, 0.0)], 100.0, 40.0);
let out = resolve_rect(&f, |_| Some(screen));
let r = match out {
ResolveOutcome::Resolved(r) => r,
ResolveOutcome::Unresolvable => panic!("{p:?} unresolvable"),
};
assert_eq!(r.width().to_bits(), 100.0f32.to_bits(), "{p:?} width");
assert_eq!(r.height().to_bits(), 40.0f32.to_bits(), "{p:?} height");
}
}
#[test]
fn two_edge_derivation_no_size() {
// TOPLEFT + BOTTOMRIGHT to screen corners -> all edges direct, w/h ignored.
let f = LayoutInput::sized(
vec![
Anchor::new(Point::TopLeft, SCREEN, Point::TopLeft, 10.0, -10.0),
Anchor::new(Point::BottomRight, SCREEN, Point::BottomRight, -20.0, 30.0),
],
0.0, // no explicit width
0.0, // no explicit height
);
let got = resolve_rect(&f, |_| Some(screen_rect()));
// left=0+10, top=600-10, right=800-20, bottom=0+30
assert_rect(got, Rect::new(30.0, 10.0, 590.0, 780.0));
}
/// The getters' shortcut must refuse exactly what the solver would decide for itself: an edge
/// takes its anchor scan before the authored span, so a span is the author's only while the
/// anchors leave one of the axis's edges free.
#[test]
fn an_authored_span_is_the_answer_only_when_the_anchors_leave_an_edge_free() {
let at = |p: Point| Anchor {
point: p,
relative_to: 0,
relative_point: p,
x_off: 0.0,
y_off: 0.0,
};
let input = |anchors: Vec<Anchor>| LayoutInput {
anchors,
width: 120.0,
height: 40.0,
..LayoutInput::default()
};
// One edge pinned: the other comes from the authored span.
assert_eq!(
input(vec![at(Point::Left)]).authored_span(true),
Some(120.0)
);
assert_eq!(
input(vec![at(Point::TopLeft)]).authored_span(true),
Some(120.0)
);
assert_eq!(
input(vec![at(Point::Center)]).authored_span(true),
Some(120.0)
);
assert_eq!(input(vec![at(Point::Top)]).authored_span(false), Some(40.0));
// Both edges pinned: the span is the anchors' distance, whatever was authored.
assert_eq!(
input(vec![at(Point::Left), at(Point::Right)]).authored_span(true),
None
);
assert_eq!(
input(vec![at(Point::TopLeft), at(Point::BottomRight)]).authored_span(true),
None,
"corner points pin an edge on both axes"
);
assert_eq!(
input(vec![at(Point::TopLeft), at(Point::BottomRight)]).authored_span(false),
None
);
// The other axis of the same pair is still free.
assert_eq!(
input(vec![at(Point::Left), at(Point::Right)]).authored_span(false),
Some(40.0)
);
// No authored size: the axis derives.
let derived = LayoutInput {
anchors: vec![at(Point::Left)],
..LayoutInput::default()
};
assert_eq!(derived.authored_span(true), None);
// Clamped: the screen can squeeze the rect, so only the solver knows.
let clamped = LayoutInput {
clamp: true,
..input(vec![at(Point::Left)])
};
assert_eq!(clamped.authored_span(true), None);
}
#[test]
fn center_anchor_derives_edges() {
let f = LayoutInput::sized(
vec![Anchor::new(Point::Center, SCREEN, Point::Center, 0.0, 0.0)],
100.0,
40.0,
);
let got = resolve_rect(&f, |_| Some(screen_rect()));
// centered at (400, 300): [bottom 280, left 350, top 320, right 450]
assert_rect(got, Rect::new(280.0, 350.0, 320.0, 450.0));
}
#[test]
fn scale_interaction() {
let f = LayoutInput {
anchors: vec![Anchor::new(
Point::TopLeft,
SCREEN,
Point::TopLeft,
10.0,
-5.0,
)],
width: 200.0,
height: 50.0,
scale: 2.0,
..LayoutInput::default()
};
let got = resolve_rect(&f, |_| Some(screen_rect()));
// left=2*10=20, top=600+2*(-5)=590, right=20+200*2=420, bottom=590-50*2=490
assert_rect(got, Rect::new(490.0, 20.0, 590.0, 420.0));
}
#[test]
fn chained_frames_via_solver() {
const A: Handle = 1;
const B: Handle = 2;
let mut s = LayoutSolver::new();
s.begin();
s.set_external(SCREEN, screen_rect());
// Register B before A to prove topological ordering, not registration ordering.
s.set_frame(
B,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::BottomRight, 0.0, 0.0)],
20.0,
20.0,
),
);
s.set_frame(
A,
&LayoutInput::sized(
vec![Anchor::new(
Point::TopLeft,
SCREEN,
Point::TopLeft,
100.0,
-100.0,
)],
50.0,
50.0,
),
);
s.solve();
assert!(s.cycle().is_empty());
assert!(s.unresolvable().is_empty());
// A: left 100, top 500, right 150, bottom 450
assert_eq!(
bits(s.rect(A).expect("A resolved")),
bits(Rect::new(450.0, 100.0, 500.0, 150.0))
);
// B hangs off A's bottom-right corner (150, 450), size 20
assert_eq!(
bits(s.rect(B).expect("B resolved")),
bits(Rect::new(430.0, 150.0, 450.0, 170.0))
);
}
#[test]
fn no_anchor_is_unresolvable() {
// A frame with no SetPoint cannot resolve (every edge derives circularly to +Inf).
let f = LayoutInput::sized(vec![], 100.0, 40.0);
assert_eq!(
resolve_rect(&f, |_| Some(screen_rect())),
ResolveOutcome::Unresolvable
);
}
#[test]
fn unset_sentinel_propagates() {
// The one anchor's target has no rect, so its edges are +Inf and nothing else pins them.
let f = LayoutInput::sized(
vec![Anchor::new(
Point::TopLeft,
SCREEN,
Point::TopLeft,
0.0,
0.0,
)],
100.0,
40.0,
);
assert_eq!(resolve_rect(&f, |_| None), ResolveOutcome::Unresolvable);
}
#[test]
fn dependent_of_unresolvable_is_unresolvable() {
const A: Handle = 1; // no anchors -> unresolvable
const B: Handle = 2; // anchored to A
let mut s = LayoutSolver::new();
s.begin();
s.set_external(SCREEN, screen_rect());
s.set_frame(A, &LayoutInput::sized(vec![], 100.0, 40.0));
s.set_frame(
B,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::TopLeft, 0.0, 0.0)],
20.0,
20.0,
),
);
s.solve();
assert!(s.unresolvable().contains(&A));
assert!(s.unresolvable().contains(&B));
assert!(s.rect(A).is_none());
assert!(s.rect(B).is_none());
}
#[test]
fn cycle_detected() {
const A: Handle = 1;
const B: Handle = 2;
let mut s = LayoutSolver::new();
s.begin();
s.set_frame(
A,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, B, Point::TopLeft, 0.0, 0.0)],
20.0,
20.0,
),
);
s.set_frame(
B,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::TopLeft, 0.0, 0.0)],
20.0,
20.0,
),
);
s.solve();
assert!(s.cycle().contains(&A));
assert!(s.cycle().contains(&B));
assert!(s.unresolvable().contains(&A));
assert!(s.unresolvable().contains(&B));
}
#[test]
fn self_anchor_is_a_cycle() {
const A: Handle = 1;
let mut s = LayoutSolver::new();
s.begin();
s.set_frame(
A,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::BottomRight, 0.0, 0.0)],
20.0,
20.0,
),
);
s.solve();
assert!(s.cycle().contains(&A));
}
#[test]
fn size_changed_epsilon() {
let r = Rect::new(0.0, 0.0, 40.0, 100.0);
// 1e-7 is under SIZE_EPS.
let tiny = Rect::new(0.0, 0.0, 40.0, 100.0 + 1e-7);
assert!(!size_changed(r, tiny));
let big = Rect::new(0.0, 0.0, 40.0, 100.001);
assert!(size_changed(r, big));
}
/// The buffers live across rounds, so `begin` must leave nothing behind: round 2 anchors to a
/// handle only round 1 registered and must find it unknown.
#[test]
fn begin_leaves_no_state_from_the_previous_round() {
const A: Handle = 1;
const B: Handle = 2;
let mut s = LayoutSolver::new();
// Round 1: A anchored to the screen, B hanging off A.
s.begin();
s.set_external(SCREEN, screen_rect());
s.set_frame(
A,
&LayoutInput::sized(
vec![Anchor::new(
Point::TopLeft,
SCREEN,
Point::TopLeft,
0.0,
0.0,
)],
100.0,
40.0,
),
);
s.set_frame(
B,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::BottomRight, 0.0, 0.0)],
20.0,
20.0,
),
);
s.solve();
let a_first = s.rect(A).expect("A resolved in round 1");
assert!(s.rect(B).is_some());
// Round 2: only B, whose target A is no longer registered.
s.begin();
s.set_external(SCREEN, screen_rect());
s.set_frame(
B,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::BottomRight, 0.0, 0.0)],
20.0,
20.0,
),
);
s.solve();
assert!(
s.rect(A).is_none(),
"round 1's A rect must not survive begin"
);
assert!(
s.rect(B).is_none(),
"B must not resolve against a stale target"
);
assert!(s.unresolvable().contains(&B));
assert!(s.cycle().is_empty(), "cycle list must be cleared too");
// Round 3: the original pair again, which must match round 1.
s.begin();
s.set_external(SCREEN, screen_rect());
s.set_frame(
A,
&LayoutInput::sized(
vec![Anchor::new(
Point::TopLeft,
SCREEN,
Point::TopLeft,
0.0,
0.0,
)],
100.0,
40.0,
),
);
s.set_frame(
B,
&LayoutInput::sized(
vec![Anchor::new(Point::TopLeft, A, Point::BottomRight, 0.0, 0.0)],
20.0,
20.0,
),
);
s.solve();
assert_eq!(
bits(s.rect(A).expect("A resolved in round 3")),
bits(a_first)
);
assert!(s.unresolvable().is_empty());
}
}