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                if 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
75        match event {
76            winit::event::Event::NewEvents(_) => {
77                event_span.take();
78                prof_span!(span, "Process Events");
79                event_span = Some(span);
80            },
81            winit::event::Event::AboutToWait => {
82                event_span.take();
83                poll_span.take();
84                if polled_twice {
85                    handle_main_events_cleared(&mut states, event_loop, &mut global_state);
86                }
87                prof_span!(span, "Poll Winit");
88                poll_span = Some(span);
89                polled_twice = !polled_twice;
90            },
91            winit::event::Event::WindowEvent { event, .. } => {
92                span!(_guard, "Handle WindowEvent");
93
94                if let winit::event::WindowEvent::Focused(focused) = event {
95                    global_state.audio.set_master_volume(if focused {
96                        global_state.settings.audio.master_volume.get_checked()
97                    } else {
98                        global_state
99                            .settings
100                            .audio
101                            .inactive_master_volume_perc
102                            .get_checked()
103                            * global_state.settings.audio.master_volume.get_checked()
104                    });
105                }
106
107                global_state
108                    .window
109                    .handle_window_event(event, &mut global_state.settings)
110            },
111            winit::event::Event::DeviceEvent { event, .. } => {
112                span!(_guard, "Handle DeviceEvent");
113                global_state.window.handle_device_event(event)
114            },
115            winit::event::Event::LoopExiting => {
116                // Save any unsaved changes to settings and profile
117                global_state
118                    .settings
119                    .save_to_file_warn(&global_state.config_dir);
120                global_state
121                    .profile
122                    .save_to_file_warn(&global_state.config_dir);
123            },
124            _ => {},
125        }
126    })
127}
128
129fn handle_main_events_cleared(
130    states: &mut Vec<Box<dyn PlayState>>,
131    event_loop: &ActiveEventLoop,
132    global_state: &mut GlobalState,
133) {
134    span!(guard, "Handle MainEventsCleared");
135    // Screenshot / Fullscreen toggle
136    global_state
137        .window
138        .resolve_deduplicated_events(&mut global_state.settings, &global_state.config_dir);
139    // Run tick here
140
141    // What's going on here?
142    // ---------------------
143    // The state system used by Voxygen allows for the easy development of
144    // stack-based menus. For example, you may want a "title" state
145    // that can push a "main menu" state on top of it, which can in
146    // turn push a "settings" state or a "game session" state on top of it.
147    // The code below manages the state transfer logic automatically so that we
148    // don't have to re-engineer it for each menu we decide to add
149    // to the game.
150    let mut exit = true;
151    while let Some(state_result) = states.last_mut().map(|last| {
152        let events = global_state.window.fetch_events(&mut global_state.settings);
153        last.tick(global_state, events)
154    }) {
155        // Implement state transfer logic.
156        match state_result {
157            PlayStateResult::Continue => {
158                exit = false;
159                break;
160            },
161            PlayStateResult::Shutdown => {
162                // Clear the Discord activity before shutting down
163                #[cfg(feature = "discord")]
164                global_state.discord.clear_activity();
165
166                debug!("Shutting down all states...");
167                while states.last().is_some() {
168                    states.pop().map(|old_state| {
169                        debug!("Popped state '{}'.", old_state.name());
170                        global_state.on_play_state_changed();
171                    });
172                }
173            },
174            PlayStateResult::Pop => {
175                states.pop().map(|old_state| {
176                    debug!("Popped state '{}'.", old_state.name());
177                    global_state.on_play_state_changed();
178                });
179                states.last_mut().map(|new_state| {
180                    new_state.enter(global_state, Direction::Backwards);
181                });
182            },
183            PlayStateResult::Push(mut new_state) => {
184                new_state.enter(global_state, Direction::Forwards);
185                debug!("Pushed state '{}'.", new_state.name());
186                states.push(new_state);
187                global_state.on_play_state_changed();
188            },
189            PlayStateResult::Switch(mut new_state) => {
190                new_state.enter(global_state, Direction::Forwards);
191                states.last_mut().map(|old_state| {
192                    debug!(
193                        "Switching to state '{}' from state '{}'.",
194                        new_state.name(),
195                        old_state.name()
196                    );
197                    mem::swap(old_state, &mut new_state);
198                    global_state.on_play_state_changed();
199                });
200            },
201        }
202    }
203
204    if exit {
205        event_loop.exit();
206    }
207
208    let mut capped_fps = false;
209
210    drop(guard);
211
212    #[cfg(feature = "egui-ui")]
213    let scale_factor = global_state.window.scale_factor() as f32;
214
215    if let Some(last) = states.last_mut() {
216        capped_fps = last.capped_fps();
217
218        span!(guard, "Render");
219
220        // Render the screen using the global renderer
221        if let Some(mut drawer) = global_state
222            .window
223            .renderer_mut()
224            .start_recording_frame(last.globals_bind_group())
225            .expect("Unrecoverable render error when starting a new frame!")
226        {
227            if global_state.clear_shadows_next_frame {
228                drawer.clear_shadows();
229            }
230
231            last.render(&mut drawer, &global_state.settings);
232
233            #[cfg(feature = "egui-ui")]
234            if last.egui_enabled() && global_state.settings.interface.egui_enabled() {
235                drawer.draw_egui(&mut global_state.egui_state.platform, scale_factor);
236            }
237        };
238        if global_state.clear_shadows_next_frame {
239            global_state.clear_shadows_next_frame = false;
240        }
241
242        drop(guard);
243    }
244
245    if !exit {
246        // Wait for the next tick.
247        span!(guard, "Main thread sleep");
248
249        // Enforce an FPS cap for the non-game session play states to prevent them
250        // running at hundreds/thousands of FPS resulting in high GPU usage for
251        // effectively doing nothing.
252        let max_fps = get_fps(global_state.settings.graphics.max_fps);
253        let max_background_fps = u32::min(
254            max_fps,
255            get_fps(global_state.settings.graphics.max_background_fps),
256        );
257        let max_fps_focus_adjusted = if global_state.window.focused {
258            max_fps
259        } else {
260            max_background_fps
261        };
262
263        const TITLE_SCREEN_FPS_CAP: u32 = 60;
264
265        let target_fps = if capped_fps {
266            u32::min(TITLE_SCREEN_FPS_CAP, max_fps_focus_adjusted)
267        } else {
268            max_fps_focus_adjusted
269        };
270
271        global_state
272            .clock
273            .set_target_dt(Duration::from_secs_f64(1.0 / target_fps as f64));
274        global_state.clock.tick();
275        drop(guard);
276        #[cfg(feature = "tracy")]
277        common_base::tracy_client::frame_mark();
278
279        // Maintain global state.
280        global_state.maintain();
281    }
282}