Skip to main content

vk_graph_window/
lib.rs

1//! `winit` window, event loop, and swapchain helpers for `vk-graph`.
2
3#![deny(missing_docs)]
4#![deny(rustdoc::broken_intra_doc_links)]
5
6mod frame;
7pub mod swapchain;
8
9pub use {self::frame::FrameContext, winit};
10
11use {
12    self::swapchain::{Swapchain, SwapchainError, SwapchainInfo},
13    log::{error, info, trace, warn},
14    std::{error, fmt, ops::Deref},
15    vk_graph::{
16        Graph,
17        driver::{
18            DriverError,
19            ash::vk,
20            device::{Device, DeviceInfo},
21            surface::Surface,
22        },
23        pool::hash::HashPool,
24    },
25    winit::raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle},
26    winit::{
27        application::ApplicationHandler,
28        error::EventLoopError,
29        event::{DeviceEvent, DeviceId, Event, WindowEvent},
30        event_loop::{ActiveEventLoop, EventLoop},
31        monitor::MonitorHandle,
32        window::{WindowAttributes, WindowId},
33    },
34};
35
36/// A closure type for picking surface formats.
37pub type SurfaceFormatFn = dyn Fn(&[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR;
38
39/// Describes a screen mode for display.
40#[derive(Clone, Copy, Debug)]
41pub enum FullscreenMode {
42    /// A display mode which retains other operating system windows behind the current window.
43    Borderless,
44
45    /// Seems to be the only way for stutter-free rendering on Nvidia + Win10.
46    Exclusive,
47}
48
49/// A convenience wrapper that owns a `winit` event loop and a compatible `vk-graph` device.
50#[read_only::embed]
51pub struct Window {
52    data: WindowData,
53
54    /// A device which is compatible with this window.
55    ///
56    /// _Note:_ This field is read-only.
57    #[readonly]
58    pub device: Device,
59
60    #[readonly]
61    pub(self) event_loop: EventLoop<()>,
62}
63
64impl Deref for ReadOnlyWindow {
65    type Target = EventLoop<()>;
66
67    fn deref(&self) -> &Self::Target {
68        &self.event_loop
69    }
70}
71
72impl Window {
73    /// Creates a window using the default [`WindowBuilder`] configuration.
74    pub fn new() -> Result<Self, WindowError> {
75        Self::builder().build()
76    }
77
78    /// Creates a builder for configuring a [`Window`] before construction.
79    pub fn builder() -> WindowBuilder {
80        Default::default()
81    }
82
83    /// Runs the application event loop and invokes `draw_fn` for each rendered frame.
84    pub fn run<F>(self, draw_fn: F) -> Result<(), WindowError>
85    where
86        F: FnMut(FrameContext),
87    {
88        struct Application<F> {
89            active_window: Option<ActiveWindow>,
90            data: WindowData,
91            device: Device,
92            draw_fn: F,
93            error: Option<WindowError>,
94            primary_monitor: Option<MonitorHandle>,
95        }
96
97        impl<F> Application<F> {
98            fn create_swapchain(
99                &mut self,
100                window: &winit::window::Window,
101            ) -> Result<Swapchain, DriverError> {
102                let surface = Surface::create(&self.device, window, window)?;
103                let surface_formats = Surface::formats(&surface)?;
104                let surface_format = self
105                    .data
106                    .surface_format_fn
107                    .as_ref()
108                    .map(|f| f(&surface_formats))
109                    .unwrap_or_else(|| Surface::linear_or_default(&surface_formats));
110                let window_size = window.inner_size();
111
112                let mut swapchain_info =
113                    SwapchainInfo::new(window_size.width, window_size.height, surface_format)
114                        .into_builder()
115                        .command_buffer_count(self.data.cmd_buf_count);
116
117                if let Some(min_image_count) = self.data.min_image_count {
118                    swapchain_info = swapchain_info.min_image_count(min_image_count);
119                }
120
121                let v_sync = self.data.v_sync.unwrap_or_default();
122                let present_modes = Surface::present_modes(&surface)?;
123                if !present_modes.is_empty() {
124                    let best_modes = if v_sync {
125                        [vk::PresentModeKHR::FIFO_RELAXED, vk::PresentModeKHR::FIFO].as_slice()
126                    } else {
127                        [vk::PresentModeKHR::MAILBOX, vk::PresentModeKHR::IMMEDIATE].as_slice()
128                    };
129
130                    swapchain_info = swapchain_info.present_mode(
131                        best_modes
132                            .iter()
133                            .copied()
134                            .find(|best| present_modes.contains(best))
135                            .or_else(|| {
136                                warn!("requested present modes unsupported: {best_modes:?}");
137
138                                present_modes.first().copied()
139                            })
140                            .ok_or_else(|| {
141                                error!("display does not support presentation");
142
143                                DriverError::Unsupported
144                            })?,
145                    );
146                }
147
148                let swapchain = Swapchain::new(surface, swapchain_info)?;
149
150                trace!("created swapchain");
151
152                Ok(swapchain)
153            }
154
155            fn window_mode_attributes(
156                &self,
157                attributes: WindowAttributes,
158                window_mode_override: Option<Option<FullscreenMode>>,
159            ) -> WindowAttributes {
160                match window_mode_override {
161                    Some(Some(mode)) => {
162                        let inner_size;
163                        let attributes = attributes
164                            .with_decorations(false)
165                            .with_maximized(true)
166                            .with_fullscreen(Some(match mode {
167                                FullscreenMode::Borderless => {
168                                    info!("Using borderless fullscreen");
169
170                                    inner_size = None;
171
172                                    winit::window::Fullscreen::Borderless(None)
173                                }
174                                FullscreenMode::Exclusive => {
175                                    if let Some(video_mode) =
176                                        self.primary_monitor.as_ref().and_then(|monitor| {
177                                            let monitor_size = monitor.size();
178                                            monitor.video_modes().find(|mode| {
179                                                let mode_size = mode.size();
180
181                                                // Don't pick a mode with greater resolution
182                                                // than the monitor.
183                                                // It can panic on x11 in winit.
184                                                mode_size.height <= monitor_size.height
185                                                    && mode_size.width <= monitor_size.width
186                                            })
187                                        })
188                                    {
189                                        info!(
190                                            "Using {}x{} {}bpp @ {}hz exclusive fullscreen",
191                                            video_mode.size().width,
192                                            video_mode.size().height,
193                                            video_mode.bit_depth(),
194                                            video_mode.refresh_rate_millihertz() / 1_000
195                                        );
196
197                                        inner_size = Some(video_mode.size());
198
199                                        winit::window::Fullscreen::Exclusive(video_mode)
200                                    } else {
201                                        warn!("unsupported exclusive fullscreen mode");
202
203                                        inner_size = None;
204
205                                        winit::window::Fullscreen::Borderless(None)
206                                    }
207                                }
208                            }));
209
210                        if let Some(inner_size) = inner_size
211                            .or_else(|| self.primary_monitor.as_ref().map(|monitor| monitor.size()))
212                        {
213                            attributes.with_inner_size(inner_size)
214                        } else {
215                            attributes
216                        }
217                    }
218                    Some(None) => attributes.with_fullscreen(None),
219                    _ => attributes,
220                }
221            }
222        }
223
224        impl<F> ApplicationHandler for Application<F>
225        where
226            F: FnMut(FrameContext),
227        {
228            fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
229                if event_loop.exiting() {
230                    return;
231                }
232
233                if let Some(ActiveWindow { window, .. }) = self.active_window.as_ref() {
234                    window.request_redraw();
235                }
236            }
237
238            fn device_event(
239                &mut self,
240                _event_loop: &ActiveEventLoop,
241                device_id: DeviceId,
242                event: DeviceEvent,
243            ) {
244                if let Some(ActiveWindow { events, .. }) = self.active_window.as_mut() {
245                    events.push(Event::DeviceEvent { device_id, event });
246                }
247            }
248
249            fn resumed(&mut self, event_loop: &ActiveEventLoop) {
250                info!("Resumed");
251
252                self.data.attributes = self.window_mode_attributes(
253                    self.data.attributes.clone(),
254                    self.data.window_mode_override,
255                );
256
257                let window = match event_loop.create_window(self.data.attributes.clone()) {
258                    Err(err) => {
259                        warn!("unable to create window: {err}");
260
261                        self.error = Some(EventLoopError::Os(err).into());
262                        event_loop.exit();
263
264                        return;
265                    }
266                    Ok(res) => res,
267                };
268                let swapchain = match self.create_swapchain(&window) {
269                    Err(err) => {
270                        warn!("unable to create swapchain: {err}");
271
272                        self.error = Some(err.into());
273                        event_loop.exit();
274
275                        return;
276                    }
277                    Ok(res) => res,
278                };
279                let swapchain_pool = HashPool::new(&self.device);
280
281                self.active_window = Some(ActiveWindow {
282                    swapchain,
283                    swapchain_pool,
284                    swapchain_resize: None,
285                    events: vec![],
286                    window,
287                });
288            }
289
290            fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) {
291                if let Some(ActiveWindow { events, .. }) = self.active_window.as_mut() {
292                    events.push(Event::UserEvent(event));
293                }
294            }
295
296            fn window_event(
297                &mut self,
298                event_loop: &ActiveEventLoop,
299                window_id: WindowId,
300                event: WindowEvent,
301            ) {
302                if let Some(active_window) = self.active_window.as_mut() {
303                    match &event {
304                        WindowEvent::CloseRequested => {
305                            if event_loop.exiting() {
306                                return;
307                            }
308
309                            info!("close requested");
310
311                            event_loop.exit();
312                        }
313                        WindowEvent::RedrawRequested => {
314                            if event_loop.exiting() {
315                                return;
316                            }
317
318                            match active_window.draw(&self.device, &mut self.draw_fn) {
319                                Ok(true) => {}
320                                res => {
321                                    if let Err(err) = res {
322                                        self.error = Some(WindowError::Swapchain(err));
323                                    }
324
325                                    event_loop.exit();
326                                }
327                            }
328
329                            profiling::finish_frame!();
330                        }
331                        WindowEvent::Resized(size) => {
332                            active_window.swapchain_resize = Some((size.width, size.height));
333                        }
334                        _ => (),
335                    }
336
337                    active_window
338                        .events
339                        .push(Event::WindowEvent { window_id, event });
340                }
341            }
342        }
343
344        struct ActiveWindow {
345            swapchain: Swapchain,
346            swapchain_pool: HashPool,
347            swapchain_resize: Option<(u32, u32)>,
348            events: Vec<Event<()>>,
349            window: winit::window::Window,
350        }
351
352        impl ActiveWindow {
353            fn draw(
354                &mut self,
355                device: &Device,
356                mut f: impl FnMut(FrameContext),
357            ) -> Result<bool, SwapchainError> {
358                if let Some((width, height)) = self.swapchain_resize.take() {
359                    let mut swapchain_info = self.swapchain.info;
360                    swapchain_info.width = width;
361                    swapchain_info.height = height;
362                    self.swapchain.set_info(swapchain_info);
363                }
364
365                if let Some(swapchain_image) = self.swapchain.acquire_next_image()? {
366                    let mut graph = Graph::default();
367                    let swapchain_image = graph.bind_resource(swapchain_image);
368                    let swapchain_info = self.swapchain.info;
369
370                    let mut will_exit = false;
371
372                    trace!("drawing");
373
374                    f(FrameContext {
375                        device,
376                        events: &self.events,
377                        height: swapchain_info.height,
378                        graph: &mut graph,
379                        swapchain_image,
380                        width: swapchain_info.width,
381                        will_exit: &mut will_exit,
382                        window: &self.window,
383                    });
384
385                    self.events.clear();
386
387                    if will_exit {
388                        info!("exit requested");
389
390                        return Ok(false);
391                    }
392
393                    self.window.pre_present_notify();
394                    self.swapchain
395                        .present_image(&mut self.swapchain_pool, graph, swapchain_image, 0)
396                        .inspect_err(|err| {
397                            warn!("unable to present swapchain image: {err}");
398                        })?;
399                } else {
400                    warn!("unable to acquire swapchain image");
401                }
402
403                self.window.request_redraw();
404
405                Ok(true)
406            }
407        }
408
409        let mut app = Application {
410            active_window: None,
411            data: self.data,
412            device: self.read_only.device,
413            draw_fn,
414            error: None,
415            primary_monitor: None,
416        };
417
418        self.read_only.event_loop.run_app(&mut app)?;
419
420        if let Some(ActiveWindow {
421            swapchain, window, ..
422        }) = app.active_window.take()
423        {
424            drop(swapchain);
425            drop(window);
426        }
427
428        info!("Window closed");
429
430        if let Some(err) = app.error {
431            Err(err)
432        } else {
433            Ok(())
434        }
435    }
436}
437
438impl HasDisplayHandle for Window {
439    fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
440        self.event_loop.display_handle()
441    }
442}
443
444/// Builder for configuring and constructing a [`Window`].
445pub struct WindowBuilder {
446    attributes: WindowAttributes,
447    cmd_buf_count: usize,
448    device_info: DeviceInfo,
449    min_image_count: Option<u32>,
450    surface_format_fn: Option<Box<SurfaceFormatFn>>,
451    v_sync: Option<bool>,
452    window_mode_override: Option<Option<FullscreenMode>>,
453}
454
455impl WindowBuilder {
456    /// Builds the window, event loop, and compatible `vk-graph` device.
457    pub fn build(self) -> Result<Window, WindowError> {
458        let event_loop = EventLoop::new()?;
459        let device = Device::try_from_display(&event_loop, self.device_info)?;
460
461        Ok(Window {
462            data: WindowData {
463                attributes: self.attributes,
464                cmd_buf_count: self.cmd_buf_count,
465                min_image_count: self.min_image_count,
466                surface_format_fn: self.surface_format_fn,
467                v_sync: self.v_sync,
468                window_mode_override: self.window_mode_override,
469            },
470            read_only: ReadOnlyWindow { device, event_loop },
471        })
472    }
473
474    /// Specifies the number of in-flight command buffers, which should be greater
475    /// than or equal to the desired swapchain image count.
476    ///
477    /// More command buffers mean less time waiting for previously submitted frames to complete, but
478    /// more memory in use.
479    ///
480    /// Generally a value of one or two greater than desired image count produces the smoothest
481    /// animation.
482    pub fn command_buffer_count(mut self, count: usize) -> Self {
483        self.cmd_buf_count = count;
484        self
485    }
486
487    /// Enables Vulkan graphics debugging layers.
488    ///
489    /// _NOTE:_ Any validation warnings or errors will cause the current thread to park itself after
490    /// describing the error using the `log` crate. This makes it easy to attach a debugger and see
491    /// what is causing the issue directly.
492    ///
493    /// ## Platform-specific
494    ///
495    /// **macOS:** Has no effect.
496    pub fn debug(mut self, enabled: bool) -> Self {
497        self.device_info.debug = enabled;
498        self
499    }
500
501    /// A function to select the desired swapchain surface image format.
502    ///
503    /// By default linear color space will be selected unless it is not available.
504    pub fn desired_surface_format<F>(mut self, f: F) -> Self
505    where
506        F: 'static + Fn(&[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR,
507    {
508        self.surface_format_fn = Some(Box::new(f));
509        self
510    }
511
512    /// The minimum number of presentable images that the application needs. The implementation will
513    /// either create the swapchain with at least that many images, or it will fail to create the
514    /// swapchain.
515    ///
516    /// More images introduce more display lag, but smoother animation.
517    pub fn min_image_count(mut self, count: u32) -> Self {
518        self.min_image_count = Some(count);
519        self
520    }
521
522    /// Sets up fullscreen mode. In addition, decorations are set to `false` and maximized is set to
523    /// `true`.
524    ///
525    /// # Note
526    ///
527    /// There are additional options offered by `winit` which can be accessed using the `window`
528    /// function.
529    pub fn fullscreen_mode(mut self, mode: FullscreenMode) -> Self {
530        self.window_mode_override = Some(Some(mode));
531        self
532    }
533
534    /// When `true` specifies that the presentation engine does not wait for a vertical blanking
535    /// period to update the current image, meaning this mode may result in visible tearing.
536    ///
537    /// # Note
538    ///
539    /// Applies only to exlcusive fullscreen mode.
540    pub fn v_sync(mut self, enabled: bool) -> Self {
541        self.v_sync = Some(enabled);
542        self
543    }
544
545    /// Allows deeper customization of the window, if needed.
546    pub fn window<WindowFn>(mut self, f: WindowFn) -> Self
547    where
548        WindowFn: FnOnce(WindowAttributes) -> WindowAttributes,
549    {
550        self.attributes = f(self.attributes);
551        self
552    }
553
554    /// Sets up "windowed" mode, which is the opposite of fullscreen.
555    ///
556    /// # Note
557    ///
558    /// There are additional options offered by `winit` which can be accessed using the `window`
559    /// function.
560    pub fn window_mode(mut self) -> Self {
561        self.window_mode_override = Some(None);
562        self
563    }
564}
565
566impl fmt::Debug for WindowBuilder {
567    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568        f.debug_struct("WindowBuilder")
569            .field("attributes", &self.attributes)
570            .field("cmd_buffer_count", &self.cmd_buf_count)
571            .field("device_info", &self.device_info)
572            .field("min_image_count", &self.min_image_count)
573            .field(
574                "surface_format_fn",
575                &self.surface_format_fn.as_ref().map(|_| ()),
576            )
577            .field("v_sync", &self.v_sync)
578            .field("window_mode_override", &self.window_mode_override)
579            .finish()
580    }
581}
582
583impl Default for WindowBuilder {
584    fn default() -> Self {
585        Self {
586            attributes: Default::default(),
587            cmd_buf_count: 5,
588            device_info: Default::default(),
589            min_image_count: None,
590            surface_format_fn: None,
591            v_sync: None,
592            window_mode_override: None,
593        }
594    }
595}
596
597struct WindowData {
598    attributes: WindowAttributes,
599    cmd_buf_count: usize,
600    min_image_count: Option<u32>,
601    surface_format_fn: Option<Box<SurfaceFormatFn>>,
602    v_sync: Option<bool>,
603    window_mode_override: Option<Option<FullscreenMode>>,
604}
605
606/// Errors produced while creating or running a [`Window`].
607#[derive(Debug)]
608pub enum WindowError {
609    /// A Vulkan or `vk-graph` driver error occurred.
610    Driver(DriverError),
611    /// `winit` failed to create or run the event loop.
612    EventLoop(EventLoopError),
613    /// An window system integration or swapchain presentation error occurred.
614    Swapchain(SwapchainError),
615}
616
617impl error::Error for WindowError {
618    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
619        Some(match self {
620            Self::Driver(err) => err,
621            Self::EventLoop(err) => err,
622            Self::Swapchain(err) => err,
623        })
624    }
625}
626
627impl fmt::Display for WindowError {
628    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629        match self {
630            Self::Driver(err) => err.fmt(f),
631            Self::EventLoop(err) => err.fmt(f),
632            Self::Swapchain(err) => err.fmt(f),
633        }
634    }
635}
636
637impl From<DriverError> for WindowError {
638    fn from(err: DriverError) -> Self {
639        Self::Driver(err)
640    }
641}
642
643impl From<EventLoopError> for WindowError {
644    fn from(err: EventLoopError) -> Self {
645        Self::EventLoop(err)
646    }
647}