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                                    profiling::finish_frame!();
346
347                                    return;
348                                }
349                                Err(err) => {
350                                    self.error = Some(WindowError::Graphchain(err));
351                                    event_loop.exit();
352                                }
353                                Ok(false) => {
354                                    event_loop.exit();
355                                }
356                                Ok(true) => {}
357                            }
358
359                            profiling::finish_frame!();
360                            self.active_window = Some(active_window);
361                        }
362                        WindowEvent::Resized(size) if size.width * size.height > 0 => {
363                            active_window.swapchain_resize = Some((size.width, size.height));
364                            self.active_window = Some(active_window);
365                        }
366                        _ => self.active_window = Some(active_window),
367                    }
368
369                    if let Some(active_window) = self.active_window.as_mut() {
370                        active_window
371                            .events
372                            .push(Event::WindowEvent { window_id, event });
373                    }
374                }
375            }
376        }
377
378        struct ActiveWindow {
379            graphchain: Option<Graphchain>,
380            swapchain_pool: HashPool,
381            swapchain_resize: Option<(u32, u32)>,
382            events: Vec<Event<()>>,
383            window: winit::window::Window,
384        }
385
386        impl ActiveWindow {
387            fn draw(
388                &mut self,
389                device: &Device,
390                data: &WindowData,
391                mut f: impl FnMut(FrameContext),
392            ) -> Result<bool, GraphchainError> {
393                if self.graphchain.is_none() {
394                    self.graphchain = Some(create_graphchain(device, data, &self.window)?);
395                }
396
397                let graphchain = self.graphchain.as_mut().expect("missing graphchain");
398
399                if let Some((width, height)) = self.swapchain_resize.take() {
400                    if width == 0 || height == 0 {
401                        self.swapchain_resize = Some((width, height));
402                        self.events.clear();
403
404                        return Ok(true);
405                    }
406
407                    let mut graphchain_info = graphchain.info;
408                    graphchain_info.width = width;
409                    graphchain_info.height = height;
410                    graphchain.set_info(graphchain_info);
411                }
412
413                if let Some(swapchain_image) = graphchain.acquire_next_image()? {
414                    let mut graph = Graph::default();
415                    let swapchain_image = graph.bind_resource(swapchain_image);
416                    let graphchain_info = graphchain.info;
417
418                    let mut will_exit = false;
419
420                    trace!("drawing");
421
422                    f(FrameContext {
423                        device,
424                        events: &self.events,
425                        height: graphchain_info.height,
426                        graph: &mut graph,
427                        swapchain_image,
428                        width: graphchain_info.width,
429                        will_exit: &mut will_exit,
430                        window: &self.window,
431                    });
432
433                    self.events.clear();
434
435                    if will_exit {
436                        info!("exit requested");
437
438                        return Ok(false);
439                    }
440
441                    self.window.pre_present_notify();
442                    graphchain
443                        .present_image(&mut self.swapchain_pool, graph, swapchain_image, 0)
444                        .inspect_err(|err| {
445                            warn!("unable to present graphchain image: {err}");
446                        })?;
447                } else {
448                    warn!("unable to acquire graphchain image");
449                }
450
451                self.window.request_redraw();
452
453                Ok(true)
454            }
455        }
456
457        let mut app = Application {
458            active_window: None,
459            data: self.data,
460            device: self.read_only.device,
461            draw_fn,
462            error: None,
463            primary_monitor: None,
464        };
465
466        let proxy = self.read_only.event_loop.create_proxy();
467        if let Err(e) = ctrlc::set_handler(move || {
468            trace!("received SIGINT/SIGTERM");
469
470            let _ = proxy.send_event(());
471        }) {
472            warn!("failed to set Ctrl-C handler: {e}");
473        }
474
475        self.read_only.event_loop.run_app(&mut app)?;
476
477        if let Some(ActiveWindow {
478            graphchain, window, ..
479        }) = app.active_window.take()
480        {
481            drop(graphchain);
482            drop(window);
483        }
484
485        info!("Window closed");
486
487        if let Some(err) = app.error {
488            Err(err)
489        } else {
490            Ok(())
491        }
492    }
493}
494
495impl HasDisplayHandle for Window {
496    fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
497        self.event_loop.display_handle()
498    }
499}
500
501/// Builder for configuring and constructing a [`Window`].
502pub struct WindowBuilder {
503    attributes: WindowAttributes,
504    cmd_buf_count: usize,
505    device_info: DeviceInfo,
506    min_image_count: Option<u32>,
507    surface_format_fn: Option<Box<SurfaceFormatFn>>,
508    v_sync: Option<bool>,
509    window_mode_override: Option<Option<FullscreenMode>>,
510}
511
512impl WindowBuilder {
513    /// Builds the window, event loop, and compatible `vk-graph` device.
514    pub fn build(self) -> Result<Window, WindowError> {
515        let event_loop = EventLoop::new()?;
516        let device = Device::try_from_display(&event_loop, self.device_info)?;
517
518        Ok(Window {
519            data: WindowData {
520                attributes: self.attributes,
521                cmd_buf_count: self.cmd_buf_count,
522                min_image_count: self.min_image_count,
523                surface_format_fn: self.surface_format_fn,
524                v_sync: self.v_sync,
525                window_mode_override: self.window_mode_override,
526            },
527            read_only: ReadOnlyWindow { device, event_loop },
528        })
529    }
530
531    /// Specifies the number of in-flight command buffers, which should be greater
532    /// than or equal to the desired swapchain image count.
533    ///
534    /// More command buffers mean less time waiting for previously submitted frames to complete, but
535    /// more memory in use.
536    ///
537    /// Generally a value of one or two greater than desired image count produces the smoothest
538    /// animation.
539    pub fn command_buffer_count(mut self, count: usize) -> Self {
540        self.cmd_buf_count = count;
541        self
542    }
543
544    /// Enables Vulkan graphics debugging layers.
545    ///
546    /// _NOTE:_ Validation errors will only park the current thread for debugger attach when the
547    /// process is attached to an interactive terminal. Otherwise they continue after logging.
548    ///
549    /// ## Platform-specific
550    ///
551    /// **macOS:** Has no effect.
552    pub fn debug(mut self, enabled: bool) -> Self {
553        self.device_info.debug = enabled;
554        self
555    }
556
557    /// A function to select the desired swapchain surface image format.
558    ///
559    /// By default linear color space will be selected unless it is not available.
560    pub fn desired_surface_format<F>(mut self, f: F) -> Self
561    where
562        F: 'static + Fn(&[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR,
563    {
564        self.surface_format_fn = Some(Box::new(f));
565        self
566    }
567
568    /// The minimum number of presentable images that the application needs. The implementation will
569    /// either create the swapchain with at least that many images, or it will fail to create the
570    /// swapchain.
571    ///
572    /// More images introduce more display lag, but smoother animation.
573    pub fn min_image_count(mut self, count: u32) -> Self {
574        self.min_image_count = Some(count);
575        self
576    }
577
578    /// Sets up fullscreen mode. In addition, decorations are set to `false` and maximized is set to
579    /// `true`.
580    ///
581    /// # Note
582    ///
583    /// There are additional options offered by `winit` which can be accessed using the `window`
584    /// function.
585    pub fn fullscreen_mode(mut self, mode: FullscreenMode) -> Self {
586        self.window_mode_override = Some(Some(mode));
587        self
588    }
589
590    /// When `true`, requests presentation synchronized to the display refresh.
591    ///
592    /// # Note
593    ///
594    /// Applies only to exclusive fullscreen mode.
595    pub fn v_sync(mut self, enabled: bool) -> Self {
596        self.v_sync = Some(enabled);
597        self
598    }
599
600    /// Allows deeper customization of the window, if needed.
601    pub fn window<WindowFn>(mut self, f: WindowFn) -> Self
602    where
603        WindowFn: FnOnce(WindowAttributes) -> WindowAttributes,
604    {
605        self.attributes = f(self.attributes);
606        self
607    }
608
609    /// Sets up "windowed" mode, which is the opposite of fullscreen.
610    ///
611    /// # Note
612    ///
613    /// There are additional options offered by `winit` which can be accessed using the `window`
614    /// function.
615    pub fn window_mode(mut self) -> Self {
616        self.window_mode_override = Some(None);
617        self
618    }
619}
620
621impl fmt::Debug for WindowBuilder {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        f.debug_struct("WindowBuilder")
624            .field("attributes", &self.attributes)
625            .field("cmd_buffer_count", &self.cmd_buf_count)
626            .field("device_info", &self.device_info)
627            .field("min_image_count", &self.min_image_count)
628            .field(
629                "surface_format_fn",
630                &self.surface_format_fn.as_ref().map(|_| ()),
631            )
632            .field("v_sync", &self.v_sync)
633            .field("window_mode_override", &self.window_mode_override)
634            .finish()
635    }
636}
637
638impl Default for WindowBuilder {
639    fn default() -> Self {
640        Self {
641            attributes: Default::default(),
642            cmd_buf_count: 5,
643            device_info: Default::default(),
644            min_image_count: None,
645            surface_format_fn: None,
646            v_sync: None,
647            window_mode_override: None,
648        }
649    }
650}
651
652struct WindowData {
653    attributes: WindowAttributes,
654    cmd_buf_count: usize,
655    min_image_count: Option<u32>,
656    surface_format_fn: Option<Box<SurfaceFormatFn>>,
657    v_sync: Option<bool>,
658    window_mode_override: Option<Option<FullscreenMode>>,
659}
660
661/// Errors produced while creating or running a [`Window`].
662#[derive(Debug)]
663pub enum WindowError {
664    /// A Vulkan or `vk-graph` driver error occurred.
665    Driver(DriverError),
666    /// `winit` failed to create or run the event loop.
667    EventLoop(EventLoopError),
668    /// A window system integration or swapchain presentation error occurred.
669    Graphchain(GraphchainError),
670}
671
672impl error::Error for WindowError {
673    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
674        Some(match self {
675            Self::Driver(err) => err,
676            Self::EventLoop(err) => err,
677            Self::Graphchain(err) => err,
678        })
679    }
680}
681
682impl fmt::Display for WindowError {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        match self {
685            Self::Driver(err) => err.fmt(f),
686            Self::EventLoop(err) => err.fmt(f),
687            Self::Graphchain(err) => err.fmt(f),
688        }
689    }
690}
691
692impl From<DriverError> for WindowError {
693    fn from(err: DriverError) -> Self {
694        Self::Driver(err)
695    }
696}
697
698impl From<EventLoopError> for WindowError {
699    fn from(err: EventLoopError) -> Self {
700        Self::EventLoop(err)
701    }
702}