Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/fpsmaster_app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[package]
name = "fpsmaster_app"
# Product version shown in-game (title screen / window / F3), sourced at compile
# time via CARGO_PKG_VERSION — the single source of truth for the display version.
version = "1.0.0"
edition.workspace = true
rust-version.workspace = true
Expand Down
14 changes: 14 additions & 0 deletions crates/fpsmaster_app/src/frame_profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub struct FrameProfiler {
ema_ms: f32,
/// Last completed window summary, served to the F3 overlay.
latest: PhaseSummary,
/// The just-completed frame's per-phase durations, kept after `cur` is
/// cleared so a per-frame consumer (the F10 perf capture) can sample them.
last_frame_phases: BTreeMap<&'static str, Duration>,
}

impl FrameProfiler {
Expand All @@ -53,6 +56,7 @@ impl FrameProfiler {
last_flush: now,
ema_ms: 0.0,
latest: PhaseSummary::default(),
last_frame_phases: BTreeMap::new(),
}
}

Expand All @@ -62,6 +66,13 @@ impl FrameProfiler {
&self.latest
}

/// The per-phase CPU durations of the frame that just closed (via
/// [`Self::end_frame`]). Unlike [`Self::latest`] this is a single frame, not
/// a windowed average, so the F10 capture can build its own statistics.
pub fn last_frame_phases(&self) -> &BTreeMap<&'static str, Duration> {
&self.last_frame_phases
}

/// Record one phase's duration for the current frame. Overwrites, so each
/// phase is recorded at most once per frame. Always accumulated (cheap) so the
/// F3 readout works even without the log flag.
Expand Down Expand Up @@ -103,6 +114,9 @@ impl FrameProfiler {
self.frame_sum += cpu;
self.frame_max = self.frame_max.max(cpu);
self.frames += 1;
// Hand the closed frame's phases to any per-frame consumer before we
// reset `cur` for the next frame.
std::mem::swap(&mut self.last_frame_phases, &mut self.cur);
self.cur.clear();

if now.duration_since(self.last_flush) >= Duration::from_secs(1) {
Expand Down
2 changes: 1 addition & 1 deletion crates/fpsmaster_app/src/gui/ingame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ fn draw_debug_overlay(

let grounded = if info.on_ground { "yes" } else { "no" };
let left = [
format!("FPSMaster Extreme {fps:.0} fps"),
format!("{} {fps:.0} fps", crate::version::title()),
format!("XYZ: {fx:.3} / {fy:.3} / {fz:.3}"),
format!("Block: {bx} {by} {bz} (grounded: {grounded})"),
format!("Chunk: {rx} {ry} {rz} in {cx} {cz}"),
Expand Down
78 changes: 63 additions & 15 deletions crates/fpsmaster_app/src/gui/main_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ use super::{draw_default_background, DrawCtx, GuiAction, GuiScreen, ScreenCtx};
#[derive(Default)]
pub struct GuiMainMenu {
buttons: Vec<GuiButton>,
/// A random line from the vanilla `texts/splashes.txt`, chosen once when the
/// title screen is (re)opened — vanilla re-rolls it in `GuiMainMenu.initGui`.
splash: Option<String>,
}

impl GuiMainMenu {
pub fn new() -> Self {
Self::default()
Self {
splash: pick_splash(),
..Self::default()
}
}

/// Vanilla `initGui` + `addSingleplayerMultiplayerButtons`.
Expand Down Expand Up @@ -71,30 +77,42 @@ impl GuiScreen for GuiMainMenu {
}

let s = ctx.scale;
let cx = ctx.width / 2;

// Custom MINECRAFT logo (single image, 1364×214). Drawn 256 GUI px wide,
// aspect-preserved (~40 GUI px tall), centered horizontally near the top.
const LOGO_SRC_W: u32 = 1364;
const LOGO_SRC_H: u32 = 214;
let logo_w = 256 * s;
let logo_h = logo_w * LOGO_SRC_H as i32 / LOGO_SRC_W as i32;
let logo_x = (ctx.width - logo_w) / 2;
// Vanilla MINECRAFT logo from gui/title/minecraft.png: two 155×44 halves
// (rows at v=0 and v=45 of the 256×256 sheet) blitted side by side, exactly
// like vanilla `GuiMainMenu` (`j = width/2 - 137`, logo at y=30).
let logo_y = 30 * s;
let j = cx - 137 * s;
ui.image(UiRect::new(j, logo_y, 155 * s, 44 * s), GuiTexture::Title, 0, 0, 155, 44);
ui.image(
UiRect::new(logo_x, logo_y, logo_w, logo_h),
UiRect::new(j + 155 * s, logo_y, 155 * s, 44 * s),
GuiTexture::Title,
0,
0,
LOGO_SRC_W,
LOGO_SRC_H,
45,
155,
44,
);

// Splash text from the vanilla texts/splashes.txt, yellow with a shadow near
// the logo's lower-right (vanilla anchors it at width/2+90, tilted ~20° and
// pulsing — the tilt/pulse need rotated, fractionally-scaled glyphs the UI
// layer has no primitive for, so it renders upright at the GUI scale).
if let Some(splash) = &self.splash {
let splash_w = fpsmaster_render::text_width(splash, s);
let splash_x = (cx + 88 * s - splash_w / 2)
.min(ctx.width - splash_w - 2 * s)
.max(2 * s);
let splash_y = logo_y + 34 * s;
ui.text_shadowed(splash_x, splash_y, s, super::TEXT_YELLOW, splash);
}

// Brand subtitle under the logo: "FPSMaster Extreme", centered.
{
let brand = "FPSMaster Extreme";
let brand = crate::version::PRODUCT_NAME;
let brand_w = fpsmaster_render::text_width(brand, s);
let brand_x = (ctx.width - brand_w) / 2;
let brand_y = logo_y + logo_h + 2 * s;
let brand_y = logo_y + 46 * s;
ui.text_shadowed(brand_x, brand_y, s, super::TEXT_YELLOW, brand);
}

Expand All @@ -104,7 +122,13 @@ impl GuiScreen for GuiMainMenu {
}

// Version string: bottom-left, white with shadow.
ui.text_shadowed(2 * s, ctx.height - 10 * s, s, super::TEXT_WHITE, "FPSMaster Extreme 1.8.9");
ui.text_shadowed(
2 * s,
ctx.height - 10 * s,
s,
super::TEXT_WHITE,
crate::version::title(),
);
}

fn wants_panorama(&self) -> bool {
Expand Down Expand Up @@ -133,3 +157,27 @@ impl GuiScreen for GuiMainMenu {
Vec::new()
}
}

/// The vanilla splash file, one splash per line, under the active assets.
const SPLASHES_ASSET: &str = "assets/minecraft/texts/splashes.txt";

/// Pick a random splash line from the vanilla [`SPLASHES_ASSET`], or `None` when
/// the asset is unavailable (no vanilla assets extracted). Blank lines are
/// dropped; the choice is seeded from the wall clock so it varies per visit,
/// matching vanilla's per-`initGui` re-roll.
fn pick_splash() -> Option<String> {
let text = fpsmaster_render::read_asset_string(SPLASHES_ASSET)?;
let lines: Vec<&str> = text
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.collect();
if lines.is_empty() {
return None;
}
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
Some(lines[(seed % lines.len() as u128) as usize].to_owned())
}
17 changes: 16 additions & 1 deletion crates/fpsmaster_app/src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,25 @@ pub struct ScreenCtx<'a> {
pub mouse: (f64, f64),
}

/// The active GUI-scale preference (vanilla `guiScale`: `0` = Auto, `1..` = a
/// fixed cap), published once per frame from [`Settings`] by the render loop.
/// Every menu and HUD reads the scale through the ambient [`gui_scale`] helper —
/// which is a pure function of the window size — so the preference lives here as
/// frame-global state rather than being threaded through dozens of draw calls.
static GUI_SCALE_PREF: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);

/// Publish the user's GUI-scale preference for this frame. Call before any
/// screen or HUD draw so [`gui_scale`] resolves against the current setting.
pub fn set_gui_scale_pref(pref: u32) {
GUI_SCALE_PREF.store(pref, std::sync::atomic::Ordering::Relaxed);
}

/// GUI pixel scale shared by every screen and the HUD (same formula the
/// renderer rasterizes the UI buffer at, so all layout snaps to GUI pixels).
/// Honors the GUI-scale preference set via [`set_gui_scale_pref`].
pub fn gui_scale(width: i32, height: i32) -> i32 {
fpsmaster_render::gui_pixel_scale(width.max(1) as u32, height.max(1) as u32) as i32
let pref = GUI_SCALE_PREF.load(std::sync::atomic::Ordering::Relaxed);
fpsmaster_render::gui_pixel_scale_capped(width.max(1) as u32, height.max(1) as u32, pref) as i32
}

// ─── Shared drawing helpers ──────────────────────────────────────────────────
Expand Down
19 changes: 18 additions & 1 deletion crates/fpsmaster_app/src/gui/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub struct GuiOptions {
controls_btn: Option<GuiButton>,
resource_packs_btn: Option<GuiButton>,
language_btn: Option<GuiButton>,
gui_scale_btn: Option<GuiButton>,
sound_btn: Option<GuiButton>,
done: Option<GuiButton>,
dragging: Option<Slider>,
Expand Down Expand Up @@ -134,8 +135,11 @@ impl GuiOptions {
Some(GuiButton::at_px(x, top + 72 * s, 98 * s, s, tr("options.resourcepack")));
self.sound_btn =
Some(GuiButton::at_px(x + 102 * s, top + 72 * s, 98 * s, s, tr("options.sounds")));
// Language + GUI Scale share a row (left/right halves). GUI Scale is a
// cycle button (Auto/1/2/…), so its label is set at draw time.
self.language_btn =
Some(GuiButton::at_px(x, top + 96 * s, 200 * s, s, tr("options.language")));
Some(GuiButton::at_px(x, top + 96 * s, 98 * s, s, tr("options.language")));
self.gui_scale_btn = Some(GuiButton::at_px(x + 102 * s, top + 96 * s, 98 * s, s, ""));
// A gap above Done, echoing vanilla's separated bottom button.
self.done = Some(GuiButton::at_px(x, top + 132 * s, 200 * s, s, tr("gui.done")));
}
Expand All @@ -161,6 +165,7 @@ impl GuiScreen for GuiOptions {
self.resource_packs_btn.as_ref(),
self.sound_btn.as_ref(),
self.language_btn.as_ref(),
self.gui_scale_btn.as_ref(),
self.done.as_ref(),
]
.into_iter()
Expand Down Expand Up @@ -203,6 +208,11 @@ impl GuiScreen for GuiOptions {
if let Some(b) = &self.language_btn {
b.draw(ui, s, ctx.mouse, ctx.mouse_down);
}
if let Some(gui_scale) = &mut self.gui_scale_btn {
gui_scale.label =
format!("{}: {}", tr("options.guiScale"), ctx.settings.clone().gui_scale_label());
gui_scale.draw(ui, s, ctx.mouse, ctx.mouse_down);
}
if let Some(done) = &self.done {
done.draw(ui, s, ctx.mouse, ctx.mouse_down);
}
Expand Down Expand Up @@ -257,6 +267,13 @@ impl GuiScreen for GuiOptions {
))),
];
}
if self.gui_scale_btn.as_ref().is_some_and(|b| b.clicked(x, y)) {
// Cycle Auto → 1 → … → Auto. The new scale is picked up next frame
// when the render loop re-publishes the preference, so every menu and
// the HUD rescale together.
ctx.settings.cycle_gui_scale();
return vec![GuiAction::SaveSettings];
}
if self.done.as_ref().is_some_and(|b| b.clicked(x, y)) {
return vec![
GuiAction::SaveSettings,
Expand Down
Loading
Loading