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