Part of #41: the last of five `benilla-app` pieces. This one is everything the others did not cover: the addon harness, the settings registry, the glue screens (login, realm list, character select and create), key bindings, the minimap, nameplates, the loading screen, transports, the performance instruments, the smaller modules, the integration tests, and the UI asset files (`OptionsFrame.xml`, `benilla.toc` and the rest). Their comments are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 27,351 comment lines become 11,010. Fixes #33. Fixes #103. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - The weapon trail's emissive is 0 (`0x70ca30`): the trail is a type-5 render record, so its ambient-only light term is exact. - The camera-style dropdown writes "1", "2", "0" (`UIOptionsFrame.lua:525,536,547`); several register-site addresses in the settings table are corrected. - The graphics settings are held until `RestartGx`, as in 1.12; `Fonts.xml`'s early load no longer carries a false reason. - vmangos sends a player-name invalidation after a rename (`CharacterHandler.cpp:839`), and the cinematic camera's announce packet has an empty handler there (`MiscHandler.cpp:919`). - Stock FrameXML and GlueXML line citations are checked against the stock 1.12 files, and vmangos line citations against the pinned revision. - **Deviation labels.** "Deviation:" now marks only a choice with its recorded reason. Declining to copy a reference defect is stated plainly, and so is an unbuilt behaviour or an untraced one. - **Verification.** Every file strips to identical code, apart from test literals in `login/`, `death/` and `ui_script/death_tests.rs` that now use placeholder account and character names, and `cargo fmt --check` is clean. Six second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference. A second commit corrects one comment in `benilla-ui` (`unit_is_grey`: the reference's exclusion is players only). A third lowers `ui_script_wall`'s ceiling from 45 to 40: five gameplay files named `UiScript` only in comments the rewrite removed, and the ratchet asks for the number to follow. Behaviour differences the rewrite surfaced are filed as their own issues (#339 to #353 among them). String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
162 lines
5.0 KiB
Rust
162 lines
5.0 KiB
Rust
//! The frame-cost meters the pill reads: a rolling window each of wall frame time and process
|
|
//! CPU per frame.
|
|
//! While synced, wall frame time measures the display's present grant, not our cost, so the CPU
|
|
//! series is the pill's headline and `wall` only feeds the dim fps and the hitch log.
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
use bevy::prelude::*;
|
|
use bevy::time::Real;
|
|
|
|
use super::clock::{main_thread_cpu_secs, process_cpu_secs};
|
|
|
|
/// Recent frames kept for the pill's windowed means (~5 s at 60 fps, ~2.5 s at 120).
|
|
pub(super) const SAMPLE_WINDOW: usize = 300;
|
|
|
|
/// Frame duration above which a frame is logged as a hitch, far above any present interval.
|
|
const HITCH_LOG_MS: f32 = 250.0;
|
|
|
|
/// A rolling window of one cost series, in milliseconds, capped at its own length.
|
|
#[derive(Clone)]
|
|
pub(super) struct Series {
|
|
samples: VecDeque<f32>,
|
|
cap: usize,
|
|
}
|
|
|
|
impl Series {
|
|
fn new(cap: usize) -> Self {
|
|
Self {
|
|
samples: VecDeque::with_capacity(cap),
|
|
cap,
|
|
}
|
|
}
|
|
|
|
fn push(&mut self, ms: f32) {
|
|
if self.samples.len() == self.cap {
|
|
self.samples.pop_front();
|
|
}
|
|
self.samples.push_back(ms);
|
|
}
|
|
|
|
pub(super) fn len(&self) -> usize {
|
|
self.samples.len()
|
|
}
|
|
|
|
/// Windowed mean; `None` on an empty window tells "no data" from "zero cost".
|
|
pub(super) fn mean(&self) -> Option<f32> {
|
|
(!self.samples.is_empty())
|
|
.then(|| self.samples.iter().sum::<f32>() / self.samples.len() as f32)
|
|
}
|
|
}
|
|
|
|
/// The per-frame cost meters; `Clone` is for the HUD's 4 Hz snapshot.
|
|
#[derive(Resource, Clone)]
|
|
pub(super) struct FrameStats {
|
|
/// Wall frame interval: the present grant while synced, our cost only when uncapped.
|
|
pub(super) wall: Series,
|
|
/// Process CPU per frame, user+system across every thread (`getrusage`).
|
|
pub(super) cpu: Series,
|
|
prev_cpu_secs: Option<f64>,
|
|
/// The main thread's CPU per frame, sampled pinned to that thread.
|
|
pub(super) main: Series,
|
|
prev_main_secs: Option<f64>,
|
|
}
|
|
|
|
impl Default for FrameStats {
|
|
fn default() -> Self {
|
|
Self {
|
|
wall: Series::new(SAMPLE_WINDOW),
|
|
cpu: Series::new(SAMPLE_WINDOW),
|
|
main: Series::new(SAMPLE_WINDOW),
|
|
prev_cpu_secs: None,
|
|
prev_main_secs: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FrameStats {
|
|
/// Windowed mean frames per second; it cannot see cost, so the pill draws it dim.
|
|
pub(super) fn fps(&self) -> f32 {
|
|
match self.wall.mean() {
|
|
Some(mean) if mean > 0.0 => 1000.0 / mean,
|
|
_ => 0.0,
|
|
}
|
|
}
|
|
|
|
/// Feed `(wall_ms, cpu_ms)` frames without the clocks, for tests; returns the clock it left
|
|
/// off at.
|
|
#[cfg(test)]
|
|
pub(super) fn feed_frames(&mut self, frames: &[(f32, f32)], start_t: f32, dt: f32) -> f32 {
|
|
let mut t = start_t;
|
|
for &(wall, cpu) in frames {
|
|
self.wall.push(wall);
|
|
self.cpu.push(cpu);
|
|
// A fixed main-thread share, so a test can tell the two series apart.
|
|
self.main.push(cpu * 0.5);
|
|
t += dt;
|
|
}
|
|
t
|
|
}
|
|
}
|
|
|
|
/// Sample both meters for this frame.
|
|
pub(super) fn sample_frame_time(
|
|
// Pinned to the main thread: `main_thread_cpu_secs` reads the calling thread's clock.
|
|
_pin: bevy::ecs::system::NonSendMarker,
|
|
time: Res<Time<Real>>,
|
|
mut stats: ResMut<FrameStats>,
|
|
) {
|
|
let wall_ms = time.delta_secs() * 1000.0;
|
|
stats.wall.push(wall_ms);
|
|
|
|
let cpu = process_cpu_secs();
|
|
if let (Some(prev), Some(n)) = (stats.prev_cpu_secs, cpu) {
|
|
stats.cpu.push(((n - prev) * 1000.0) as f32);
|
|
}
|
|
stats.prev_cpu_secs = cpu;
|
|
let main = main_thread_cpu_secs();
|
|
if let (Some(prev), Some(n)) = (stats.prev_main_secs, main) {
|
|
stats.main.push(((n - prev) * 1000.0) as f32);
|
|
}
|
|
stats.prev_main_secs = main;
|
|
|
|
// The first frame's delta is the startup gap, not a hitch.
|
|
if wall_ms > HITCH_LOG_MS && stats.wall.len() > 1 {
|
|
warn!("frame hitch: {wall_ms:.0} ms (main thread blocked this long)");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_windows_roll_and_the_means_are_windowed() {
|
|
let mut s = FrameStats::default();
|
|
let loud: Vec<_> = (0..100).map(|_| (33.2, 30.0)).collect();
|
|
let calm: Vec<_> = (0..SAMPLE_WINDOW).map(|_| (16.6, 8.0)).collect();
|
|
let mut t = s.feed_frames(&loud, 0.0, 1.0 / 30.0);
|
|
t = s.feed_frames(&calm, t, 1.0 / 60.0);
|
|
let _ = t;
|
|
|
|
assert_eq!(s.wall.len(), SAMPLE_WINDOW, "the window is capped");
|
|
let cpu = s.cpu.mean().expect("cpu has samples");
|
|
assert!(
|
|
(cpu - 8.0).abs() < 0.01,
|
|
"the loud prefix must have rolled out, got {cpu}"
|
|
);
|
|
assert!(
|
|
(s.fps() - 1000.0 / 16.6).abs() < 0.5,
|
|
"fps is the windowed wall mean's reciprocal, got {}",
|
|
s.fps()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_data_is_not_zero_cost() {
|
|
let s = FrameStats::default();
|
|
assert!(s.cpu.mean().is_none());
|
|
assert_eq!(s.fps(), 0.0);
|
|
}
|
|
}
|