veloren_voxygen/
run.rs

1use crate::{
2    Direction, GlobalState, PlayState, PlayStateResult,
3    menu::main::MainMenuState,
4    settings::get_fps,
5    ui,
6    window::{Event, EventLoop},
7};
8use common_base::{prof_span, span};
9use std::{mem, time::Duration};
10use tracing::debug;
11
12pub fn run(mut global_state: GlobalState, event_loop: EventLoop) {
13    // Set up the initial play state.
14    let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(MainMenuState::new(&mut global_state))];
15    states.last_mut().map(|current_state| {
16        current_state.enter(&mut global_state, Direction::Forwards);
17        let current_state = current_state.name();
18        debug!(?current_state, "Started game with state");
19    });
20
21    // Used to ignore every other `MainEventsCleared`
22    // This is a workaround for a bug on macos in which mouse motion events are only
23    // reported every other cycle of the event loop
24    // See: https://github.com/rust-windowing/winit/issues/1418
25    let mut polled_twice = false;
26
27    let mut poll_span = None;
28    let mut event_span = None;
29
30    event_loop.run(move |event, _, control_flow| {
31        // Continuously run loop since we handle sleeping
32        *control_flow = winit::event_loop::ControlFlow::Poll;
33
34        #[cfg(feature = "egui-ui")]
35        {
36            let enabled_for_current_state = states.last().is_some_and(|state| state.egui_enabled());
37
38            // Only send events to the egui UI when it is being displayed.
39            if enabled_for_current_state && global_state.settings.interface.egui_enabled() {
40                global_state.egui_state.platform.handle_event(&event);
41                if global_state.egui_state.platform.captures_event(&event) {
42                    return;
43                }
44            }
45        }
46
47        // Don't pass resize events to the ui, `Window` is responsible for:
48        // - deduplicating them
49        // - generating resize events for the ui
50        // - ensuring consistent sizes are passed to the ui and to the renderer
51        if !matches!(&event, winit::event::Event::WindowEvent {
52            event: winit::event::WindowEvent::Resized(_),
53            ..
54        }) {
55            // Save new window state in settings
56            let scale = global_state.window.window().scale_factor();
57            let size = global_state.window.window().inner_size().to_logical(scale);
58            global_state.settings.graphics.window.size = [size.width, size.height];
59            global_state.settings.graphics.window.maximised =
60                global_state.window.window().is_maximized();
61            // Get events for the ui.
62            if let Some(event) = ui::Event::try_from(&event, global_state.window.window()) {
63                global_state.window.send_event(Event::Ui(event));
64            }
65            // iced ui events
66            // TODO: no clone
67            if let winit::event::Event::WindowEvent { event, .. } = &event {
68                let window = &mut global_state.window;
69                if let Some(event) =
70                    ui::ice::window_event(event, window.scale_factor(), window.modifiers())
71                {
72                    window.send_event(Event::IcedUi(event));
73                }
74            }
75        }
76
77        match event {
78            winit::event::Event::NewEvents(_) => {
79                event_span.take();
80                prof_span!(span, "Process Events");
81                event_span = Some(span);
82            },
83            winit::event::Event::MainEventsCleared => {
84                event_span.take();
85                poll_span.take();
86                if polled_twice {
87                    handle_main_events_cleared(&mut states, control_flow, &mut global_state);
88                }
89                prof_span!(span, "Poll Winit");
90                poll_span = Some(span);
91                polled_twice = !polled_twice;
92            },
93            winit::event::Event::WindowEvent { event, .. } => {
94                span!(_guard, "Handle WindowEvent");
95
96                if let winit::event::WindowEvent::Focused(focused) = event {
97                    global_state.audio.set_master_volume(if focused {
98                        global_state.settings.audio.master_volume.get_checked()
99                    } else {
100                        global_state
101                            .settings
102                            .audio
103                            .inactive_master_volume_perc
104                            .get_checked()
105                            * global_state.settings.audio.master_volume.get_checked()
106                    });
107                }
108
109                global_state
110                    .window
111                    .handle_window_event(event, &mut global_state.settings)
112            },
113            winit::event::Event::DeviceEvent { event, .. } => {
114                span!(_guard, "Handle DeviceEvent");
115                global_state.window.handle_device_event(event)
116            },
117            winit::event::Event::LoopDestroyed => {
118                // Save any unsaved changes to settings and profile
119                global_state
120                    .settings
121                    .save_to_file_warn(&global_state.config_dir);
122                global_state
123                    .profile
124                    .save_to_file_warn(&global_state.config_dir);
125            },
126            _ => {},
127        }
128    });
129}
130
131fn handle_main_events_cleared(
132    states: &mut Vec<Box<dyn PlayState>>,
133    control_flow: &mut winit::event_loop::ControlFlow,
134    global_state: &mut GlobalState,
135) {
136    span!(guard, "Handle MainEventsCleared");
137    // Screenshot / Fullscreen toggle
138    global_state
139        .window
140        .resolve_deduplicated_events(&mut global_state.settings, &global_state.config_dir);
141    // Run tick here
142
143    // What's going on here?
144    // ---------------------
145    // The state system used by Voxygen allows for the easy development of
146    // stack-based menus. For example, you may want a "title" state
147    // that can push a "main menu" state on top of it, which can in
148    // turn push a "settings" state or a "game session" state on top of it.
149    // The code below manages the state transfer logic automatically so that we
150    // don't have to re-engineer it for each menu we decide to add
151    // to the game.
152    let mut exit = true;
153    while let Some(state_result) = states.last_mut().map(|last| {
154        let events = global_state.window.fetch_events();
155        last.tick(global_state, events)
156    }) {
157        // Implement state transfer logic.
158        match state_result {
159            PlayStateResult::Continue => {
160                exit = false;
161                break;
162            },
163            PlayStateResult::Shutdown => {
164                // Clear the Discord activity before shutting down
165                #[cfg(feature = "discord")]
166                global_state.discord.clear_activity();
167
168                debug!("Shutting down all states...");
169                while states.last().is_some() {
170                    states.pop().map(|old_state| {
171                        debug!("Popped state '{}'.", old_state.name());
172                        global_state.on_play_state_changed();
173                    });
174                }
175            },
176            PlayStateResult::Pop => {
177                states.pop().map(|old_state| {
178                    debug!("Popped state '{}'.", old_state.name());
179                    global_state.on_play_state_changed();
180                });
181                states.last_mut().map(|new_state| {
182                    new_state.enter(global_state, Direction::Backwards);
183                });
184            },
185            PlayStateResult::Push(mut new_state) => {
186                new_state.enter(global_state, Direction::Forwards);
187                debug!("Pushed state '{}'.", new_state.name());
188                states.push(new_state);
189                global_state.on_play_state_changed();
190            },
191            PlayStateResult::Switch(mut new_state) => {
192                new_state.enter(global_state, Direction::Forwards);
193                states.last_mut().map(|old_state| {
194                    debug!(
195                        "Switching to state '{}' from state '{}'.",
196                        new_state.name(),
197                        old_state.name()
198                    );
199                    mem::swap(old_state, &mut new_state);
200                    global_state.on_play_state_changed();
201                });
202            },
203        }
204    }
205
206    if exit {
207        *control_flow = winit::event_loop::ControlFlow::Exit;
208    }
209
210    let mut capped_fps = false;
211
212    drop(guard);
213
214    #[cfg(feature = "egui-ui")]
215    let scale_factor = global_state.window.window().scale_factor() as f32;
216
217    if let Some(last) = states.last_mut() {
218        capped_fps = last.capped_fps();
219
220        span!(guard, "Render");
221
222        // Render the screen using the global renderer
223        if let Some(mut drawer) = global_state
224            .window
225            .renderer_mut()
226            .start_recording_frame(last.globals_bind_group())
227            .expect("Unrecoverable render error when starting a new frame!")
228        {
229            if global_state.clear_shadows_next_frame {
230                drawer.clear_shadows();
231            }
232
233            last.render(&mut drawer, &global_state.settings);
234
235            #[cfg(feature = "egui-ui")]
236            if last.egui_enabled() && global_state.settings.interface.egui_enabled() {
237                drawer.draw_egui(&mut global_state.egui_state.platform, scale_factor);
238            }
239        };
240        if global_state.clear_shadows_next_frame {
241            global_state.clear_shadows_next_frame = false;
242        }
243
244        drop(guard);
245    }
246
247    if !exit {
248        // Wait for the next tick.
249        span!(guard, "Main thread sleep");
250
251        // Enforce an FPS cap for the non-game session play states to prevent them
252        // running at hundreds/thousands of FPS resulting in high GPU usage for
253        // effectively doing nothing.
254        let max_fps = get_fps(global_state.settings.graphics.max_fps);
255        let max_background_fps = u32::min(
256            max_fps,
257            get_fps(global_state.settings.graphics.max_background_fps),
258        );
259        let max_fps_focus_adjusted = if global_state.window.focused {
260            max_fps
261        } else {
262            max_background_fps
263        };
264
265        const TITLE_SCREEN_FPS_CAP: u32 = 60;
266
267        let target_fps = if capped_fps {
268            u32::min(TITLE_SCREEN_FPS_CAP, max_fps_focus_adjusted)
269        } else {
270            max_fps_focus_adjusted
271        };
272
273        global_state
274            .clock
275            .set_target_dt(Duration::from_secs_f64(1.0 / target_fps as f64));
276        global_state.clock.tick();
277        drop(guard);
278        #[cfg(feature = "tracy")]
279        common_base::tracy_client::frame_mark();
280
281        // Maintain global state.
282        global_state.maintain();
283    }
284}