From 07371cc893657c487ff85d1a508798216f3b2db3 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 5 Nov 2026 07:21:37 +0100 Subject: [PATCH] Debug FPS display with rendering backend info --- VERSION | 2 +- lang/de.lang | 1 + lang/en.lang | 1 + src/main.rs | 37 +++++++++++++++++++++++++++++++++++-- src/menu.rs | 6 +++++- src/platform.rs | 28 ++++++++++++++++++++++++++++ src/settings.rs | 18 ++++++++++++++++++ www/dc_plugin.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 133 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 6b29bfb..fae70f8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v202611-00014 +v202611-00015 diff --git a/lang/de.lang b/lang/de.lang index 3778c6b..1387489 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -339,6 +339,7 @@ dir.nw = Nordwesten val.auto = Auto ({0}x) set.render_scale = 3D-Auflösung +set.debug_fps = FPS-Anzeige hint.pickup_prisoner_where = Gefangene können nur auf eigenem Gebiet aufgehoben werden. hint.punch_where = Die Hand kann Helden nur auf eigenem Gebiet schlagen. diff --git a/lang/en.lang b/lang/en.lang index 33e251a..2332e72 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -340,6 +340,7 @@ dir.nw = north-west val.auto = Auto ({0}x) set.render_scale = 3D resolution +set.debug_fps = FPS debug hint.pickup_prisoner_where = Prisoners can only be picked up on your own territory. hint.punch_where = The hand can only punch heroes on your own territory. diff --git a/src/main.rs b/src/main.rs index b4b1464..7c9565b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -394,6 +394,8 @@ struct App { ws_id: i32, match_mode: bool, coop: bool, + fps: f32, + renderer: String, } impl App { @@ -600,7 +602,7 @@ async fn main() { let seed = (miniquad::date::now() * 1000.0) as u32 | 1; // behind the title menu: the endless bridge battle let (game, cam) = Game::new_demo(seed); - let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false }; + let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, fps: 0.0, renderer: platform::renderer() }; menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(); app.game.hints = app.settings.hints; let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None }; @@ -614,8 +616,11 @@ async fn main() { let mut cache_t = 0.0f32; loop { - let dt = get_frame_time().min(0.1); + let raw_dt = get_frame_time(); + let dt = raw_dt.min(0.1); let now = get_time(); + let fps_now = 1.0 / raw_dt.max(0.001); + app.fps = if app.fps <= 0.0 { fps_now } else { app.fps + (fps_now - app.fps) * 0.08 }; let mut settings_changed = false; // ---------------- touch or mouse: which interface is active @@ -1423,4 +1428,32 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M if !touch { ui.draw_cursor(gfx, &app.game, now, blocked); } + if app.settings.debug_fps { + draw_debug_fps(app, gfx); + } +} + +fn draw_debug_fps(app: &App, gfx: &Gfx) { + ui::begin_ui(); + let scale = ui::SMALL().max(1.0); + let fps = app.fps.max(0.0).round() as i32; + let renderer = { + let r = app.renderer.trim(); + if r.is_empty() { + if cfg!(target_arch = "wasm32") { + "WebGL" + } else { + "OpenGL" + } + } else { + r + } + }; + let max_cols = (((ui::sw() - 6.0) / ui::CW()).floor() as i32).max(12) as usize; + let line: String = format!("{} FPS {}", fps, renderer).chars().take(max_cols).collect(); + let w = gfx.measure(&line, scale); + let x = (ui::sw() - w - 2.0).max(2.0); + let y = 2.0; + draw_rectangle(x - 2.0, y - 2.0, w + 4.0, scale * 10.0 + 4.0, Color::from_rgba(0, 0, 0, 150)); + gfx.text(&line, x, y, scale, GOLD); } diff --git a/src/menu.rs b/src/menu.rs index 1407b32..0344b8c 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -50,6 +50,7 @@ pub enum Item { Lights, UiScale, RenderScale, + DebugFps, GameSpeed, Hints, TouchUi, @@ -62,7 +63,7 @@ fn tab_items(tab: usize) -> &'static [Item] { 0 => &[Language, Volume, MusicVolume, Mute, Voice], 1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse], 2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale], - 3 => &[GameSpeed, Hints, TouchUi], + 3 => &[GameSpeed, Hints, TouchUi, DebugFps], _ => &[], } } @@ -83,6 +84,7 @@ fn item_label(it: Item) -> String { Item::Lights => "set.lights", Item::UiScale => "set.ui_scale", Item::RenderScale => "set.render_scale", + Item::DebugFps => "set.debug_fps", Item::GameSpeed => "set.game_speed", Item::Hints => "set.hints", Item::TouchUi => "set.touch_ui", @@ -124,6 +126,7 @@ fn item_value(it: Item, s: &Settings) -> String { format!("{}x", s.ui_factor) } } + Item::DebugFps => onoff(s.debug_fps), Item::RenderScale => { // the setting is for a 1080 pixel high window; show what applies to this one let now = crate::render3d::effective_div(s.render_scale.max(1) as u32, crate::ui::sh()); @@ -174,6 +177,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) { Item::Lights => s.max_lights = cycle(&[8u8, 16, 24], s.max_lights, d), Item::UiScale => s.ui_factor = cycle(&[0u8, 1, 2, 3, 4], s.ui_factor, d), Item::RenderScale => s.render_scale = cycle(&(1u8..=16).collect::>(), s.render_scale.clamp(1, 16), d), + Item::DebugFps => s.debug_fps = !s.debug_fps, Item::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d), Item::Hints => s.hints = !s.hints, Item::TouchUi => s.touch_ui = cycle(&[0u8, 1, 2], s.touch_ui, d), diff --git a/src/platform.rs b/src/platform.rs index 167da92..53a1df8 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -35,6 +35,7 @@ mod imp { fn dc_ws_send(id: i32, p: *const u8, pl: u32) -> i32; fn dc_ws_take(id: i32) -> i32; fn dc_ws_close(id: i32); + fn dc_renderer() -> i32; } // starts a request; poll the returned id with http_poll() @@ -171,6 +172,18 @@ mod imp { unsafe { dc_ws_close(id) } } } + + pub fn renderer() -> String { + unsafe { + let n = dc_renderer(); + if n < 0 { + return String::new(); + } + let mut buf = vec![0u8; n as usize]; + dc_storage_take(buf.as_mut_ptr()); + String::from_utf8(buf).unwrap_or_default() + } + } } #[cfg(not(target_arch = "wasm32"))] @@ -233,6 +246,21 @@ mod imp { None } pub fn ws_close(_id: i32) {} + + pub fn renderer() -> String { + #[cfg(target_vendor = "apple")] + { + use macroquad::miniquad::conf::AppleGfxApi; + return match macroquad::miniquad::window::apple_gfx_api() { + AppleGfxApi::Metal => "Metal".into(), + AppleGfxApi::OpenGl => "OpenGL".into(), + }; + } + #[cfg(not(target_vendor = "apple"))] + { + return "OpenGL".into(); + } + } } pub use imp::*; diff --git a/src/settings.rs b/src/settings.rs index a91dec7..13188c7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -150,6 +150,7 @@ pub struct Settings { pub ui_factor: u8, // interface size: 0 = automatic, 1..4 = fixed integer factor pub render_scale: u8, // 3D world resolution divisor: 1 = full, 2..16 = pixel look pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off + pub debug_fps: bool, // show FPS and rendering backend // game pub game_speed: f32, // 1 or 2 pub hints: bool, @@ -179,6 +180,7 @@ impl Default for Settings { ui_factor: 0, render_scale: 1, touch_ui: 0, + debug_fps: false, game_speed: 1.0, hints: true, difficulty: 1, @@ -225,3 +227,19 @@ impl Settings { serde_json::from_str(s).ok() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn debug_fps_round_trips() { + let mut s = Settings::default(); + assert!(!s.debug_fps); + s.debug_fps = true; + let l = Settings::from_json(&s.to_json()).expect("settings serialize back"); + assert!(l.debug_fps); + let old = Settings::from_json(r#"{"language":"en","map_w":64,"map_h":64}"#).expect("old settings remain readable"); + assert!(!old.debug_fps); + } +} diff --git a/www/dc_plugin.js b/www/dc_plugin.js index 5f750ae..1fc0c1a 100644 --- a/www/dc_plugin.js +++ b/www/dc_plugin.js @@ -261,5 +261,49 @@ window.addEventListener(ev, startAudio, true); }); + function gfxInfo() { + try { + var canvas = document.getElementById("glcanvas"); + if (!canvas) { + var cs = document.getElementsByTagName("canvas"); + canvas = cs.length ? cs[0] : null; + } + if (!canvas) return ""; + var gl = canvas.getContext("webgl2"); + var version = "WebGL 2"; + if (!gl) { + gl = canvas.getContext("webgl"); + version = "WebGL 1"; + } + if (!gl) return ""; + var versionName = gl.getParameter(gl.VERSION) || version; + var dbg = gl.getExtension("WEBGL_debug_renderer_info"); + var gpu = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) || "" : ""; + var vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) || "" : ""; + var api = "WebGL"; + var all = versionName + " " + gpu + " " + vendor; + if (all.indexOf("Vulkan") >= 0) api = "Vulkan"; + else if (all.indexOf("Metal") >= 0) api = "Metal"; + else if (all.indexOf("Direct3D") >= 0 || all.indexOf("D3D") >= 0) api = "D3D"; + else if (all.indexOf("OpenGL") >= 0) api = "OpenGL"; + return versionName + " | " + api + " | " + (gpu || vendor || api); + } catch (e) { + return ""; + } + } + + function registerGfx(importObject) { + importObject.env.dc_renderer = function () { + pending = new TextEncoder().encode(gfxInfo()); + return pending.length; + }; + } + + var oldRegister = register; + register = function (importObject) { + oldRegister(importObject); + registerGfx(importObject); + }; + miniquad_add_plugin({ register_plugin: register, version: 1, name: "dungeoncraft" }); })();