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