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                let response = global_state
46                    .egui_state
47                    .winit_state
48                    .on_window_event(global_state.window.window(), event);
49                if response.consumed {
50                    return;
51                }
52            }
53        }
54
55        // Don't pass resize events to the ui, `Window` is responsible for:
56        // - deduplicating them
57        // - generating resize events for the ui
58        // - ensuring consistent sizes are passed to the ui and to the renderer
59        if !matches!(&event, winit::event::Event::WindowEvent {
60            event: winit::event::WindowEvent::Resized(_),
61            ..
62        }) {
63            let window = &mut global_state.window;
64            // Get events for the ui.
65            if let Some(event) = ui::Event::try_from(&event, window.window(), window.modifiers()) {
66                window.send_event(Event::Ui(event));
67            }
68            // iced ui events
69            if let winit::event::Event::WindowEvent { event, .. } = &event
70                && let Some(event) =
71                    ui::ice::window_event(event, window.scale_factor(), window.modifiers())
72            {
73                window.send_event(Event::IcedUi(event));
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::AboutToWait => {
84                event_span.take();
85                poll_span.take();
86                if polled_twice {
87                    handle_main_events_cleared(&mut states, event_loop, &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::LoopExiting => {
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    event_loop: &ActiveEventLoop,
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(&mut global_state.settings);
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        event_loop.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.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        #[cfg(feature = "egui-ui")]
223        let mut platform_output = None;
224
225        // Render the screen using the global renderer
226        if let Some(mut drawer) = global_state
227            .window
228            .renderer_mut()
229            .start_recording_frame(last.globals_bind_group())
230            .expect("Unrecoverable render error when starting a new frame!")
231        {
232            if global_state.clear_shadows_next_frame {
233                drawer.clear_shadows();
234            }
235
236            last.render(&mut drawer, &global_state.settings);
237
238            #[cfg(feature = "egui-ui")]
239            if last.egui_enabled() && global_state.settings.interface.egui_enabled() {
240                platform_output =
241                    Some(drawer.draw_egui(&mut global_state.egui_state.winit_state, scale_factor));
242            }
243        };
244
245        #[cfg(feature = "egui-ui")]
246        if let Some(output) = platform_output {
247            global_state
248                .egui_state
249                .winit_state
250                .handle_platform_output(global_state.window.window(), output);
251        }
252
253        if global_state.clear_shadows_next_frame {
254            global_state.clear_shadows_next_frame = false;
255        }
256
257        drop(guard);
258    }
259
260    if !exit {
261        // Wait for the next tick.
262        span!(guard, "Main thread sleep");
263
264        // Enforce an FPS cap for the non-game session play states to prevent them
265        // running at hundreds/thousands of FPS resulting in high GPU usage for
266        // effectively doing nothing.
267        let max_fps = get_fps(global_state.settings.graphics.max_fps);
268        let max_background_fps = u32::min(
269            max_fps,
270            get_fps(global_state.settings.graphics.max_background_fps),
271        );
272        let max_fps_focus_adjusted = if global_state.window.focused {
273            max_fps
274        } else {
275            max_background_fps
276        };
277
278        const TITLE_SCREEN_FPS_CAP: u32 = 60;
279
280        let target_fps = if capped_fps {
281            u32::min(TITLE_SCREEN_FPS_CAP, max_fps_focus_adjusted)
282        } else {
283            max_fps_focus_adjusted
284        };
285
286        global_state
287            .clock
288            .set_target_dt(Duration::from_secs_f64(1.0 / target_fps as f64));
289        global_state.clock.tick();
290        drop(guard);
291        #[cfg(feature = "tracy")]
292        common_base::tracy_client::frame_mark();
293
294        // Maintain global state.
295        global_state.maintain();
296    }
297}