This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
404 lines
14 KiB
Rust
404 lines
14 KiB
Rust
//! The frame backdrop as the 1.12 client builds it: the tiled background and 8-piece border that a
|
||
//! `<Backdrop>` element or `SetBackdrop` installs. This module owns the data ([`Backdrop`]) and the
|
||
//! geometry ([`pieces`], screen-space quads with per-corner UVs); the app renders them.
|
||
|
||
use crate::layout::Rect;
|
||
|
||
/// The default `edgeSize`, logical 32: the constructor writes device 0.025 (`DAT_0081c9b0`).
|
||
pub const DEFAULT_EDGE_SIZE: f32 = 32.0;
|
||
|
||
/// `BackgroundInsets`: the background's inset from the frame rect; the border is never inset.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||
pub struct Insets {
|
||
pub left: f32,
|
||
pub right: f32,
|
||
pub top: f32,
|
||
pub bottom: f32,
|
||
}
|
||
|
||
/// A frame's installed backdrop (the 0x68-byte struct at `frame+0x1ac`): exactly the keys the
|
||
/// reader accepts (`0x7776e0`) plus the two colours. The colours default to opaque white; the XML
|
||
/// parser defaults a present but partial `<Color>` or `<BorderColor>` to black.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Backdrop {
|
||
/// The background texture (`+0x24`); none or empty draws no background.
|
||
pub bg_file: Option<String>,
|
||
/// The 8-slice border texture (`+0x30`); none or empty draws no border.
|
||
pub edge_file: Option<String>,
|
||
/// Tile the background, else stretch it (`+0x40`).
|
||
pub tile: bool,
|
||
/// `tileSize` in logical px (`+0x4c`); 0 tiles at `edge_size` (`0x77f0c0`).
|
||
pub tile_size: f32,
|
||
/// `edgeSize` in logical px (`+0x48`): the border piece size and the edge tiling period.
|
||
pub edge_size: f32,
|
||
/// `BackgroundInsets` (`+0x50..0x5c`).
|
||
pub insets: Insets,
|
||
/// The background's vertex colour (`+0x60`, `SetBackdropColor`).
|
||
pub bg_color: [f32; 4],
|
||
/// All 8 border pieces' vertex colour (`+0x64`, `SetBackdropBorderColor`).
|
||
pub border_color: [f32; 4],
|
||
}
|
||
|
||
impl Default for Backdrop {
|
||
fn default() -> Self {
|
||
// The constructor (`0x77e5f0`).
|
||
Self {
|
||
bg_file: None,
|
||
edge_file: None,
|
||
tile: false,
|
||
tile_size: 0.0,
|
||
edge_size: DEFAULT_EDGE_SIZE,
|
||
insets: Insets::default(),
|
||
bg_color: [1.0, 1.0, 1.0, 1.0],
|
||
border_color: [1.0, 1.0, 1.0, 1.0],
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Backdrop {
|
||
fn has_bg(&self) -> bool {
|
||
self.bg_file.as_deref().is_some_and(|s| !s.is_empty())
|
||
}
|
||
fn has_border(&self) -> bool {
|
||
self.edge_file.as_deref().is_some_and(|s| !s.is_empty())
|
||
}
|
||
}
|
||
|
||
/// One drawable piece, the background or one of the 8 border pieces: explicit corners, not a rect,
|
||
/// so the bottom-right inset quirk can slant it, and per-corner UVs, since the top and bottom edges
|
||
/// are rotated 90° (`0x77f0c0`). Both arrays run `[TL, TR, BR, BL]`, y-up.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub struct BackdropPiece {
|
||
/// Corner positions in screen px.
|
||
pub corners: [[f32; 2]; 4],
|
||
/// Corner UVs in texture space.
|
||
pub uvs: [[f32; 2]; 4],
|
||
/// The texture needs repeat addressing. The corners stay in `[0,1]` but share the edge texture,
|
||
/// so they carry `true` too and the border batches as one texture (`0x77e8d0`).
|
||
pub tile: bool,
|
||
/// Drawn with `bg_file` and `bg_color`, else with `edge_file` and `border_color`.
|
||
pub is_bg: bool,
|
||
}
|
||
|
||
const SLICE: f32 = 0.125;
|
||
|
||
/// Pull a piece's UVs half a texel inward on every axis that does not tile, so bilinear
|
||
/// magnification cannot sample the neighbouring slice of the shared border atlas (in
|
||
/// `ChatBubble-Backdrop`, a white alpha-0 column: a pale line through the speech bubble). A tiling
|
||
/// axis, a strip's `v` past 1, is left exact, or each period would land short and walk the art.
|
||
#[must_use]
|
||
pub fn inset_atlas_bleed(uvs: [[f32; 2]; 4], tex_w: f32, tex_h: f32) -> [[f32; 2]; 4] {
|
||
let mut out = uvs;
|
||
for (axis, tex) in [(0usize, tex_w), (1usize, tex_h)] {
|
||
if tex < 2.0 {
|
||
continue; // a 1-texel axis has no interior
|
||
}
|
||
let (lo, hi) = uvs.iter().fold((f32::MAX, f32::MIN), |(lo, hi), c| {
|
||
(lo.min(c[axis]), hi.max(c[axis]))
|
||
});
|
||
// A range of exactly 1, the corners' `v`, is bounded and gets the inset.
|
||
if hi - lo > 1.0 + f32::EPSILON {
|
||
continue;
|
||
}
|
||
let half = 0.5 / tex;
|
||
if hi - lo <= 2.0 * half {
|
||
continue; // degenerate: the inset would invert the range
|
||
}
|
||
for c in &mut out {
|
||
c[axis] += if c[axis] <= lo { half } else { -half };
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Axis-aligned corners `[TL, TR, BR, BL]`, y-up.
|
||
fn aa_corners(x0: f32, x1: f32, y_bottom: f32, y_top: f32) -> [[f32; 2]; 4] {
|
||
[
|
||
[x0, y_top], // TL
|
||
[x1, y_top], // TR
|
||
[x1, y_bottom], // BR
|
||
[x0, y_bottom], // BL
|
||
]
|
||
}
|
||
|
||
/// The drawable pieces for a frame's resolved rect (y-up), in paint order: the background (layer
|
||
/// `BACKGROUND`), then the 8 border pieces (layer `BORDER`). Geometry `0x77e8d0`, UVs `0x77f0c0`.
|
||
pub fn pieces(frame: Rect, bd: &Backdrop) -> Vec<BackdropPiece> {
|
||
let mut out = Vec::with_capacity(9);
|
||
let e = bd.edge_size;
|
||
let (l, r, b, t) = (frame.left, frame.right, frame.bottom, frame.top);
|
||
|
||
// ── The background piece ──
|
||
if bd.has_bg() {
|
||
let (il, ir, it, ib) = (
|
||
bd.insets.left,
|
||
bd.insets.right,
|
||
bd.insets.top,
|
||
bd.insets.bottom,
|
||
);
|
||
// The four anchors, y-up. Bottom-right takes the top inset, as the reference does
|
||
// (`0x77e9ae` loads `+0x50`, top, not `+0x54`); invisible when the two are equal.
|
||
let corners = [
|
||
[l + il, t - it],
|
||
[r - ir, t - it],
|
||
[r - ir, b + it], // BR: the top inset
|
||
[l + il, b + ib],
|
||
];
|
||
// Untiled, no `SetTexCoord` runs and the background stretches over `[0,1]` (`0x77f0c0`).
|
||
let uvs = if bd.tile {
|
||
let period = if bd.tile_size != 0.0 { bd.tile_size } else { e };
|
||
let bg_w = (r - ir) - (l + il);
|
||
let bg_h = (t - it) - (b + ib);
|
||
let (wt, ht) = (bg_w / period, bg_h / period);
|
||
[[0.0, 0.0], [wt, 0.0], [wt, ht], [0.0, ht]]
|
||
} else {
|
||
[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
|
||
};
|
||
out.push(BackdropPiece {
|
||
corners,
|
||
uvs,
|
||
tile: bd.tile,
|
||
is_bg: true,
|
||
});
|
||
}
|
||
|
||
// ── The 8 border pieces ──
|
||
if bd.has_border() {
|
||
// Edge strips tile `run` periods between the corners, 0 below two edges (`0x77f0c0`).
|
||
let w_run = ((r - l) / e - 2.0).max(0.0);
|
||
let h_run = ((t - b) / e - 2.0).max(0.0);
|
||
|
||
let mut edge = |x0: f32, x1: f32, y0: f32, y1: f32, uvs: [[f32; 2]; 4]| {
|
||
out.push(BackdropPiece {
|
||
corners: aa_corners(x0, x1, y0, y1),
|
||
uvs,
|
||
tile: true,
|
||
is_bg: false,
|
||
});
|
||
};
|
||
|
||
// LEFT, slice 0, upright.
|
||
edge(
|
||
l,
|
||
l + e,
|
||
b + e,
|
||
t - e,
|
||
[[0.0, 0.0], [SLICE, 0.0], [SLICE, h_run], [0.0, h_run]],
|
||
);
|
||
// RIGHT, slice 1, upright.
|
||
edge(
|
||
r - e,
|
||
r,
|
||
b + e,
|
||
t - e,
|
||
[
|
||
[SLICE, 0.0],
|
||
[2.0 * SLICE, 0.0],
|
||
[2.0 * SLICE, h_run],
|
||
[SLICE, h_run],
|
||
],
|
||
);
|
||
// TOP, slice 2, rotated: atlas `u` runs down the screen, atlas `v` right to left.
|
||
edge(
|
||
l + e,
|
||
r - e,
|
||
t - e,
|
||
t,
|
||
[
|
||
[2.0 * SLICE, w_run],
|
||
[2.0 * SLICE, 0.0],
|
||
[3.0 * SLICE, 0.0],
|
||
[3.0 * SLICE, w_run],
|
||
],
|
||
);
|
||
// BOTTOM, slice 3, rotated as TOP.
|
||
edge(
|
||
l + e,
|
||
r - e,
|
||
b,
|
||
b + e,
|
||
[
|
||
[3.0 * SLICE, w_run],
|
||
[3.0 * SLICE, 0.0],
|
||
[4.0 * SLICE, 0.0],
|
||
[4.0 * SLICE, w_run],
|
||
],
|
||
);
|
||
// The corners, slices 4 to 7, upright.
|
||
let corner_uv = |i: f32| {
|
||
let u0 = i * SLICE;
|
||
let u1 = (i + 1.0) * SLICE;
|
||
[[u0, 0.0], [u1, 0.0], [u1, 1.0], [u0, 1.0]]
|
||
};
|
||
edge(l, l + e, t - e, t, corner_uv(4.0)); // TOPLEFT
|
||
edge(r - e, r, t - e, t, corner_uv(5.0)); // TOPRIGHT
|
||
edge(l, l + e, b, b + e, corner_uv(6.0)); // BOTTOMLEFT
|
||
edge(r - e, r, b, b + e, corner_uv(7.0)); // BOTTOMRIGHT
|
||
}
|
||
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// On the `ChatBubble-Backdrop` geometry: 256×32, eight 32-texel slices.
|
||
#[test]
|
||
fn the_bleed_inset_lands_on_texel_centres_and_spares_tiling() {
|
||
const W: f32 = 256.0;
|
||
const H: f32 = 32.0;
|
||
let half_u = 0.5 / W;
|
||
|
||
let corner = [[0.5, 0.0], [0.625, 0.0], [0.625, 1.0], [0.5, 1.0]];
|
||
let got = inset_atlas_bleed(corner, W, H);
|
||
// 128.5 and 159.5 are the centres of the slice's first and last texels.
|
||
let us: Vec<f32> = got.iter().map(|c| c[0] * W).collect();
|
||
assert!((us[0] - 128.5).abs() < 1e-3, "left u at {} texels", us[0]);
|
||
assert!((us[1] - 159.5).abs() < 1e-3, "right u at {} texels", us[1]);
|
||
let vs: Vec<f32> = got.iter().map(|c| c[1] * H).collect();
|
||
assert!((vs[0] - 0.5).abs() < 1e-3, "top v at {} texels", vs[0]);
|
||
assert!((vs[2] - 31.5).abs() < 1e-3, "bottom v at {} texels", vs[2]);
|
||
|
||
let strip = [[0.25, 8.0], [0.25, 0.0], [0.375, 0.0], [0.375, 8.0]];
|
||
let got = inset_atlas_bleed(strip, W, H);
|
||
for (g, s) in got.iter().zip(strip.iter()) {
|
||
assert_eq!(g[1], s[1], "the tiling axis must be bit-exact");
|
||
}
|
||
assert!((got[0][0] - (0.25 + half_u)).abs() < 1e-6);
|
||
assert!((got[2][0] - (0.375 - half_u)).abs() < 1e-6);
|
||
|
||
for i in 0u8..8 {
|
||
let (u0, u1) = (f32::from(i) * SLICE, f32::from(i + 1) * SLICE);
|
||
let piece = [[u0, 0.0], [u1, 0.0], [u1, 1.0], [u0, 1.0]];
|
||
for c in inset_atlas_bleed(piece, W, H) {
|
||
assert!(
|
||
c[0] > u0 && c[0] < u1,
|
||
"slice {i}: u {} escaped ({u0},{u1})",
|
||
c[0]
|
||
);
|
||
}
|
||
}
|
||
|
||
assert_eq!(inset_atlas_bleed(corner, 0.0, 0.0), corner);
|
||
let sliver = [[0.0, 0.0], [0.001, 0.0], [0.001, 1.0], [0.0, 1.0]];
|
||
assert_eq!(
|
||
inset_atlas_bleed(sliver, W, H)[0][0],
|
||
0.0,
|
||
"too thin to inset"
|
||
);
|
||
}
|
||
|
||
fn tooltip_backdrop(edge_size: f32, inset: f32) -> Backdrop {
|
||
Backdrop {
|
||
bg_file: Some("Interface\\Tooltips\\UI-Tooltip-Background".into()),
|
||
edge_file: Some("Interface\\Tooltips\\UI-Tooltip-Border".into()),
|
||
tile: true,
|
||
tile_size: edge_size,
|
||
edge_size,
|
||
insets: Insets {
|
||
left: inset,
|
||
right: inset,
|
||
top: inset,
|
||
bottom: inset,
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
// The tooltip's shape on a 200×100 frame (`Rect::new` takes bottom, left, top, right).
|
||
#[test]
|
||
fn piece_geometry_200x100_edge16_inset5() {
|
||
let frame = Rect::new(0.0, 0.0, 100.0, 200.0);
|
||
let bd = tooltip_backdrop(16.0, 5.0);
|
||
let ps = pieces(frame, &bd);
|
||
assert_eq!(ps.len(), 9);
|
||
|
||
// Symmetric insets hide the bottom-right quirk.
|
||
let bg = ps[0];
|
||
assert!(bg.is_bg);
|
||
assert_eq!(bg.corners[0], [5.0, 95.0]); // TL
|
||
assert_eq!(bg.corners[1], [195.0, 95.0]); // TR
|
||
assert_eq!(bg.corners[2], [195.0, 5.0]); // BR
|
||
assert_eq!(bg.corners[3], [5.0, 5.0]); // BL
|
||
assert_eq!(bg.uvs[0], [0.0, 0.0]);
|
||
assert!((bg.uvs[1][0] - 190.0 / 16.0).abs() < 1e-4);
|
||
assert!((bg.uvs[2][1] - 90.0 / 16.0).abs() < 1e-4);
|
||
|
||
// Border pieces 1 to 8: LEFT, RIGHT, TOP, BOTTOM, TL, TR, BL, BR, flush inside the frame.
|
||
let left = ps[1];
|
||
assert!(!left.is_bg && left.tile);
|
||
assert_eq!(
|
||
left.corners,
|
||
[[0.0, 84.0], [16.0, 84.0], [16.0, 16.0], [0.0, 16.0]]
|
||
);
|
||
// h_run = 100/16 - 2 = 4.25.
|
||
assert_eq!(left.uvs[0], [0.0, 0.0]);
|
||
assert_eq!(left.uvs[1], [0.125, 0.0]);
|
||
assert!((left.uvs[3][1] - 4.25).abs() < 1e-4);
|
||
|
||
// TOP, rotated; w_run = 200/16 - 2 = 10.5.
|
||
let top = ps[3];
|
||
assert_eq!(top.corners[0], [16.0, 100.0]); // TL
|
||
assert_eq!(top.corners[2], [184.0, 84.0]); // BR
|
||
assert_eq!(top.uvs[0][0], 0.25); // TL u
|
||
assert_eq!(top.uvs[3][0], 0.375); // BL u
|
||
assert!((top.uvs[0][1] - 10.5).abs() < 1e-4); // TL v = w_run
|
||
assert_eq!(top.uvs[1][1], 0.0); // TR v = 0
|
||
|
||
// BOTTOMRIGHT, slice 7, upright.
|
||
let br = ps[8];
|
||
assert_eq!(br.corners[0], [184.0, 16.0]); // TL
|
||
assert_eq!(br.uvs[0], [0.875, 0.0]);
|
||
assert_eq!(br.uvs[2], [1.0, 1.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn bg_bottomright_rides_top_inset_bug() {
|
||
let frame = Rect::new(0.0, 0.0, 100.0, 200.0);
|
||
let bd = Backdrop {
|
||
insets: Insets {
|
||
left: 3.0,
|
||
right: 4.0,
|
||
top: 7.0,
|
||
bottom: 9.0,
|
||
},
|
||
..tooltip_backdrop(16.0, 0.0)
|
||
};
|
||
let bg = pieces(frame, &bd)[0];
|
||
assert_eq!(bg.corners[3], [3.0, 9.0]);
|
||
// BR takes the top inset, 7, not the bottom's 9.
|
||
assert_eq!(bg.corners[2], [196.0, 7.0]);
|
||
assert_eq!(bg.corners[0], [3.0, 93.0]); // TL
|
||
assert_eq!(bg.corners[1], [196.0, 93.0]); // TR
|
||
}
|
||
|
||
#[test]
|
||
fn run_clamps_to_zero_below_two_edges() {
|
||
// 20/16 - 2 = -0.75.
|
||
let frame = Rect::new(0.0, 0.0, 20.0, 20.0);
|
||
let bd = tooltip_backdrop(16.0, 5.0);
|
||
let ps = pieces(frame, &bd);
|
||
let left = ps[1]; // LEFT edge
|
||
assert_eq!(left.uvs[3][1], 0.0, "h_run clamped to 0"); // BL v
|
||
let top = ps[3]; // TOP edge
|
||
assert_eq!(top.uvs[0][1], 0.0, "w_run clamped to 0"); // TL v
|
||
}
|
||
|
||
#[test]
|
||
fn presence_gates_pieces() {
|
||
let frame = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||
assert!(pieces(frame, &Backdrop::default()).is_empty());
|
||
let bg_only = Backdrop {
|
||
bg_file: Some("bg".into()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(pieces(frame, &bg_only).len(), 1);
|
||
let border_only = Backdrop {
|
||
edge_file: Some("edge".into()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(pieces(frame, &border_only).len(), 8);
|
||
}
|
||
}
|