grass fades on a plane, model panes render, and the FPS journal ships
- the grass fade is a plane across the view, not a ring around the player: `0x6b2b80` texgens the camera-space position through `T(0,0,−52.5)·Ry(π/2)·S(1/17.5)`, so the fade coordinate is view-space depth, `u = (z_eye − 52.5)/17.5` — we measured `distance(eye, fragment)`, a sphere, which meets the ground as a circle centred on the player that slides as they walk and bites a piece out of every tuft it crosses, and that was the hard border; the ramp is `0x6b2320`'s quantised 64×8 table, `alpha = 4·(63 − col)`, read bilinearly as `(254 − 256·u)/255` and capped at `252/255` — plateau to 52.64 yd, zero at 69.86 yd, near grass 98.8 % opaque and never 100 — where we had a clean 1 → 0 line; the product `tex0.a × ramp` is what the 128/255 `detailDoodadAlpha` cutout tests (both stages MODULATE, `[0xc7b2fc]`, confirmed verbatim in all four apitrace captures), so the tuft eroding out at 61.11 yd is the reference's own behaviour and the "fix" that would have removed it never landed; the pass owns its render state — blend mode 2, ALPHAREF 128, cull off, depth-write on, the per-slot draw `0x6b2d60` binding nothing but the texture — where we passed the batch's authored blend and cull through, and the `clutter_state` probe counted the cost: of 322 detail batches, 2 author Opaque (no cutout, their margin drawn as a solid card) and 5 single-sided; the build gate measures the chunk's box against the frustum corner ray derived from the live projection (≈1.31× at 16:9, ≈1.45× at 21:9), not its centre plus a 24 yd margin that popped whole chunks in on relief (2004) - a `<Model>` widget's animation state is Lua-observable and lives in the engine: a private scene clock its own `OnUpdate` advances by `trunc(elapsed·1000)` ms on visible frames only (`0x76d7f0`), an arm with an anchor the sampler re-reads every frame (`0x7121a0`), `OnUpdateModel` at the top of every paint (`0x76d1a0`) and `OnAnimFinished` from the completion callback (`0x76cdc0`); the file's sequences and bounds are facts the host hands over, `SetModel` seeds Stand now or when they land (`0x70ebd0`) with a queued arm replaying after the seed, and the XML `scale=` on a model pane is the model's scale (`CSimpleModel::LoadXML` `0x76cac0`), which the loader had never read (2007); and the pane renders its M2 — every visible pane with a file gets a cell of one render-target atlas at its device size through one orthographic camera, the root at `1280·modelScale·layoutScale` FrameXML units per model unit, the tile's `AnimationPlayer` paused and seeked to the engine's play head, the light black, particles in their own unit (`768·√(a²+1)` per model unit, neither scale), composited as one premultiplied quad at the callback rank; two corrections off the bytes: `OnUpdateModel` fires only with a model installed (`[widget+0x318] ≠ 0`) and `OnAnimFinished` fires once per arm for a looping sequence too (`0x719370`); the loader now applies a `<Model>`'s `file=` at all, which every XML-declared pane had lacked; retired: the minimap ping's byte-measured sprite, since `MiniMapPing` and `WorldMapPing` draw their own file on the pane's clock (2013) - the stance feed fires `UPDATE_SHAPESHIFT_FORMS` only for a list change: the reference fires it at `0x4b28ff`/`0x4b2e43` from learn, unlearn and rank-replace alone, while ours fired on any change of the pushed view, and every fire runs the stock `ShapeshiftBar_Update`, which shows `ShapeshiftBarMiddle` unconditionally past two forms and `Show()`s an already-shown frame — no `OnShow`, no manage pass — so a stance switch or the category-47 cooldown arming and expiring left the middle strip drawn alone over a raised bottom-left bar; castability now rides `SPELL_UPDATE_USABLE` (`0x4b31c0`), active/texture/cooldown are pushed silently and repainted on `PLAYER_AURAS_CHANGED` and `SPELL_UPDATE_COOLDOWN`, the feed ordered before both handler sets so they read the fresh list (2009); and the loot-roll frame waits for its item template (B371): `GroupLootFrame_OnShow` paints once and nothing repaints it, and we fired `START_LOOT_ROLL` on the frame the packet arrived, against a snapshot whose name, texture and quality were still `None` — the reference's `0x61b310` ends in the item-template cache lookup `0x55ba30`, a hit firing the event there and a miss firing it from the arrival callback `0x61b460`, so the feed holds a fresh roll's event until the template answers, with two releases that are ours: a negative answer (a frame that never opens cannot even be passed on) and a 2 s deadline paid out of the roll's minute; `arg2` stays the wire's countdown, as `0x61b430` pushes it (2010) - the FPS journal ships to players: it had registered inside the dev-only `DevToolsPlugin` and been armed by an environment variable, so nothing we shipped could have written a row on the one GPU-bound frame in the corpus, a Steam Deck's at 94 % busy; `/console fpsJournal 1` — a CVar of our own, persisted like every CVar — opens `benilla-config/Diagnostics/fps-journal.csv` and appends one row a second, `0` closes it mid-run, and `WOW_FPS_JOURNAL=<path>` stays as the harness lever; eight GPU columns appended, `gpu_ms` then `gpu_opaque, gpu_static, gpu_transp, gpu_glow, gpu_post, gpu_ui, gpu_other`, read off bevy's `RenderDiagnosticsPlugin`, which times passes where the device offers `TIMESTAMP_QUERY_INSIDE_PASSES` (Vulkan and DX12) and leaves the cells empty on an Apple GPU, empty being the honest value; `static_gx`, the four `ffx_glow_*` passes and `ui_gamma_decode` open diagnostic spans so a city's own draw is named in the split; a fresh file opens with the adapter, backend, driver and `gpu_spans yes|no` line; and a probe's `/console` line, which skips the stock `ChatEdit_ParseText` and had landed in a parser arm that knew only `reloadUI`, now forwards to `ConsoleExec` and writes a CVar the way the typed line does (2008) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -357,6 +357,9 @@ fn splice_simple(eq: &benilla_ui::script::ExtractedQuad) -> bool {
|
||||
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.
|
||||
| QuadContent::ModelPane { .. }
|
||||
// Both colour-picker arms write one quad and nothing else — and they change on every
|
||||
// step of a drag, which is exactly the traffic the splice exists for.
|
||||
| QuadContent::ColorWheel
|
||||
@@ -615,6 +618,8 @@ pub(super) fn drive_script(
|
||||
// the atlas's bake generation, which followed the same two terms; with nothing to re-bake, the
|
||||
// honest thing to watch is the term itself.)
|
||||
let dpi = window.scale_factor();
|
||||
// The tile renderer sizes its cells in device pixels off this (decision 2008).
|
||||
booths.tiles.dpi = dpi;
|
||||
let generation = font_atlas.as_deref().map(|a| a.generation);
|
||||
let seam_moved = *last_seam != s || *last_dpi != dpi;
|
||||
if seam_moved {
|
||||
@@ -1346,10 +1351,14 @@ fn convert_entry(
|
||||
// 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.
|
||||
QuadContent::ModelPane {
|
||||
handle,
|
||||
name,
|
||||
model,
|
||||
facing,
|
||||
model_scale: _,
|
||||
model_scale,
|
||||
position,
|
||||
own_alpha,
|
||||
icon,
|
||||
} => {
|
||||
use crate::portrait::PortraitSource;
|
||||
// The world-map arrow (decision 1980): the stock `WorldMapFrame.lua` /
|
||||
@@ -1390,6 +1399,58 @@ fn convert_entry(
|
||||
});
|
||||
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
|
||||
// conversion may re-publish it freely; which panes draw is the engine's paint list.
|
||||
if let Some(path) = model.as_deref() {
|
||||
let tiles = &mut booths.tiles;
|
||||
let dpi = tiles.dpi.max(0.01);
|
||||
let aspect = if h > 0.0 { w / h } else { 4.0 / 3.0 };
|
||||
let diag = (aspect * aspect + 1.0).sqrt();
|
||||
let layout = eq.scale;
|
||||
let size_px = UVec2::new(
|
||||
(rect.width() * dpi).round().max(1.0) as u32,
|
||||
(rect.height() * dpi).round().max(1.0) as u32,
|
||||
);
|
||||
tiles.requests.insert(
|
||||
handle,
|
||||
crate::ui_models::TileRequest {
|
||||
path: path.to_string(),
|
||||
size_px,
|
||||
// `1 model unit = 1280 · modelScale · layoutScale` FrameXML units (§3).
|
||||
px_per_unit: 1280.0 * model_scale * layout * s * dpi,
|
||||
// `SetPosition` is in layout units: `768 · √(a²+1)` FrameXML per unit.
|
||||
pos_px_per_unit: 768.0 * diag * layout * s * dpi,
|
||||
// A particle's half-extent: eye space, neither scale (§6).
|
||||
star_px_per_unit: 768.0 * diag * s * dpi,
|
||||
facing,
|
||||
position: Vec3::new(position.0, position.1, position.2),
|
||||
icon: icon.clone(),
|
||||
},
|
||||
);
|
||||
if let (Some(cell), Some(atlas)) = (tiles.cells.get(&handle), tiles.atlas.clone()) {
|
||||
let a = tiles.atlas_size.as_vec2();
|
||||
let (u0, v0) = (cell.origin.x as f32 / a.x, cell.origin.y as f32 / a.y);
|
||||
let (u1, v1) = (
|
||||
(cell.origin.x + cell.size.x) as f32 / a.x,
|
||||
(cell.origin.y + cell.size.y) as f32 / a.y,
|
||||
);
|
||||
out.push(UiQuad {
|
||||
rect,
|
||||
z_key: eq.z,
|
||||
texture: Some(atlas),
|
||||
uv: UvRect::from_tex_coords([u0, u1, v0, v1]),
|
||||
// The instance draws at the widget's OWN alpha (render law §4.4).
|
||||
color: [1.0, 1.0, 1.0, own_alpha],
|
||||
// A render target: premultiplied by construction (`UiQuad` doc).
|
||||
premultiplied: true,
|
||||
clip,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(slot) = name.as_deref().and_then(crate::portrait::model_pane_booth) else {
|
||||
return;
|
||||
};
|
||||
@@ -1998,6 +2059,7 @@ mod clip_plumb_tests {
|
||||
app.init_resource::<Assets<Image>>();
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::autocast_shine::ShineSites>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
@@ -2085,6 +2147,7 @@ mod clip_plumb_tests {
|
||||
app.init_resource::<Assets<Image>>();
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::autocast_shine::ShineSites>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
@@ -2292,6 +2355,7 @@ mod extract_gate_tests {
|
||||
app.init_resource::<Assets<Image>>();
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::autocast_shine::ShineSites>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
|
||||
Reference in New Issue
Block a user