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