Skip to main content

ferroui_vulkan/
vulkan_platform_graphics.rs

1use crate::interop::{VulkanDevice, VulkanInstance};
2use crate::vulkan_context::VulkanContext;
3use crate::{
4    IVulkanDevice, IVulkanInstance, VulkanDeviceCreationOptions, VulkanException, VulkanOptions,
5    VulkanPlatformSpecificOptions,
6};
7use ferroui_base::logging::{LogEventLevel, Logger};
8use ferroui_base::platform::{IPlatformGraphics, IPlatformGraphicsContext};
9use std::rc::Rc;
10use std::sync::Arc;
11
12/// The platform graphics of Vulkan: creates the graphics contexts of the
13/// threads that render, each over a device of its own or all over the device
14/// of the application.
15pub struct VulkanPlatformGraphics {
16    factory: Arc<dyn IVulkanDeviceFactory>,
17    platform_options: VulkanPlatformSpecificOptions,
18}
19
20/// Where the devices of the contexts come from.
21pub trait IVulkanDeviceFactory: Send + Sync {
22    /// Whether all contexts share one device.
23    fn uses_shaded_device(&self) -> bool;
24
25    /// The device all contexts share.
26    ///
27    /// # Panics
28    /// Panics when the factory creates a device per context.
29    fn get_shared_device(&self, platform_options: &VulkanPlatformSpecificOptions) -> Arc<dyn IVulkanDevice>;
30
31    /// Creates the device of a context.
32    ///
33    /// # Panics
34    /// Panics when the contexts share one device.
35    fn create_device(
36        &self,
37        platform_options: &VulkanPlatformSpecificOptions,
38    ) -> Result<Arc<dyn IVulkanDevice>, VulkanException>;
39}
40
41struct DefaultDeviceFactory {
42    instance: Arc<dyn IVulkanInstance>,
43    device_options: VulkanDeviceCreationOptions,
44}
45
46impl IVulkanDeviceFactory for DefaultDeviceFactory {
47    fn uses_shaded_device(&self) -> bool {
48        false
49    }
50
51    fn get_shared_device(&self, _platform_options: &VulkanPlatformSpecificOptions) -> Arc<dyn IVulkanDevice> {
52        panic!("Specified method is not supported.");
53    }
54
55    fn create_device(
56        &self,
57        platform_options: &VulkanPlatformSpecificOptions,
58    ) -> Result<Arc<dyn IVulkanDevice>, VulkanException> {
59        VulkanDevice::create(self.instance.clone(), &self.device_options, platform_options)
60    }
61}
62
63struct CustomSharedDeviceFactory {
64    device: Arc<dyn IVulkanDevice>,
65}
66
67impl IVulkanDeviceFactory for CustomSharedDeviceFactory {
68    fn uses_shaded_device(&self) -> bool {
69        true
70    }
71
72    fn get_shared_device(&self, _platform_options: &VulkanPlatformSpecificOptions) -> Arc<dyn IVulkanDevice> {
73        self.device.clone()
74    }
75
76    fn create_device(
77        &self,
78        _platform_options: &VulkanPlatformSpecificOptions,
79    ) -> Result<Arc<dyn IVulkanDevice>, VulkanException> {
80        panic!("Specified method is not supported.");
81    }
82}
83
84impl VulkanPlatformGraphics {
85    /// Platform graphics over a device factory.
86    pub fn new(factory: Arc<dyn IVulkanDeviceFactory>, platform_options: VulkanPlatformSpecificOptions) -> Self {
87        Self { factory, platform_options }
88    }
89
90    /// The platform graphics for the options of the application and of the
91    /// platform: over the device of the application when it gave one, else
92    /// over a new instance, on which a device is created once to see that
93    /// one can be. A failure is logged in the area `Vulkan`, and there are
94    /// no platform graphics then.
95    pub fn try_create(options: &VulkanOptions, platform_options: VulkanPlatformSpecificOptions) -> Option<Arc<Self>> {
96        if let Some(custom_shared_device) = &options.custom_shared_device {
97            return Some(Arc::new(Self::new(
98                Arc::new(CustomSharedDeviceFactory { device: custom_shared_device.clone() }),
99                platform_options,
100            )));
101        }
102
103        let create = || -> Result<Arc<Self>, VulkanException> {
104            let instance = VulkanInstance::create(&options.vulkan_instance_creation_options, &platform_options)?;
105
106            let dev_opts = options.vulkan_device_creation_options.clone();
107            VulkanDevice::create(instance.clone(), &dev_opts, &platform_options)?.dispose();
108
109            Ok(Arc::new(Self::new(
110                Arc::new(DefaultDeviceFactory { instance, device_options: dev_opts }),
111                platform_options.clone(),
112            )))
113        };
114        match create() {
115            Ok(graphics) => Some(graphics),
116            Err(e) => {
117                //instance?.Dispose();
118                if let Some(logger) = Logger::try_get(LogEventLevel::Error, "Vulkan") {
119                    logger.log_with_values(None, "Unable to initialize Vulkan rendering: {0}", &[&e]);
120                }
121                None
122            }
123        }
124    }
125}
126
127impl IPlatformGraphics for VulkanPlatformGraphics {
128    fn create_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
129        let context = self
130            .factory
131            .create_device(&self.platform_options)
132            .and_then(|device| VulkanContext::new(device, &self.platform_options.platform_features));
133        match context {
134            Ok(context) => context,
135            Err(error) => panic!("{error}"),
136        }
137    }
138
139    fn get_shared_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
140        // The reference keeps the context it made last and makes a new one
141        // at every call all the same: nothing reads the one it kept.
142        let device = self.factory.get_shared_device(&self.platform_options);
143        match VulkanContext::new(device, &self.platform_options.platform_features) {
144            Ok(context) => context,
145            Err(error) => panic!("{error}"),
146        }
147    }
148
149    fn uses_shared_context(&self) -> bool {
150        self.factory.uses_shaded_device()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::IVulkanPlatformGraphicsContext;
158    use ferroui_base::platform::IOptionalFeatureProvider;
159    use ferroui_base::reactive::{Disposable, IDisposable};
160    use std::any::{Any, TypeId};
161    use std::sync::atomic::{AtomicUsize, Ordering};
162
163    struct Instance;
164
165    impl IVulkanInstance for Instance {
166        fn handle(&self) -> isize {
167            0x1000
168        }
169
170        fn get_instance_proc_address(&self, _instance: isize, _name: &str) -> isize {
171            0
172        }
173
174        fn get_device_proc_address(&self, _device: isize, _name: &str) -> isize {
175            0
176        }
177
178        fn enabled_extensions(&self) -> Vec<String> {
179            Vec::new()
180        }
181    }
182
183    #[derive(Default)]
184    struct Device {
185        locks: AtomicUsize,
186    }
187
188    impl IOptionalFeatureProvider for Device {
189        fn try_get_feature(&self, _feature_type: TypeId) -> Option<Rc<dyn Any>> {
190            None
191        }
192    }
193
194    impl IVulkanDevice for Device {
195        fn handle(&self) -> isize {
196            0x2000
197        }
198
199        fn physical_device_handle(&self) -> isize {
200            0x3000
201        }
202
203        fn main_queue_handle(&self) -> isize {
204            0x4000
205        }
206
207        fn graphics_queue_family_index(&self) -> u32 {
208            7
209        }
210
211        fn instance(&self) -> Arc<dyn IVulkanInstance> {
212            Arc::new(Instance)
213        }
214
215        fn is_lost(&self) -> bool {
216            false
217        }
218
219        fn lock(&self) -> Rc<dyn IDisposable> {
220            self.locks.fetch_add(1, Ordering::SeqCst);
221            Disposable::empty()
222        }
223
224        fn enabled_extensions(&self) -> Vec<String> {
225            Vec::new()
226        }
227
228        fn dispose(&self) {}
229    }
230
231    #[test]
232    fn a_custom_device_is_shared_by_the_contexts() {
233        let device = Arc::new(Device::default());
234        let options = VulkanOptions { custom_shared_device: Some(device.clone()), ..Default::default() };
235        let graphics = VulkanPlatformGraphics::try_create(&options, Default::default()).expect("no instance is needed");
236        assert!(graphics.uses_shared_context());
237
238        // No entry point is called to make a context: the tables are filled with what the
239        // instance answers, which is nothing here, and the device has no extensions.
240        let context = graphics.get_shared_context();
241        assert!(device.locks.load(Ordering::SeqCst) >= 1);
242        assert!(!context.is_lost());
243
244        let features: &dyn IOptionalFeatureProvider = &*context;
245        let vulkan = features.try_get::<dyn IVulkanPlatformGraphicsContext>().expect("the context is a Vulkan context");
246        assert_eq!(vulkan.device().handle(), 0x2000);
247        assert_eq!(vulkan.instance().handle(), 0x1000);
248        assert_eq!(ash::vk::Handle::as_raw(vulkan.device_handle()), 0x2000);
249        assert_eq!(ash::vk::Handle::as_raw(vulkan.physical_device_handle()), 0x3000);
250        assert_eq!(ash::vk::Handle::as_raw(vulkan.instance_handle()), 0x1000);
251        assert_eq!(ash::vk::Handle::as_raw(vulkan.main_queue_handle()), 0x4000);
252        assert_eq!(vulkan.graphics_queue_family_index(), 7);
253        // Without the extensions there is no external objects feature, and no platform
254        // registered a surface factory.
255        assert!(features.try_get::<dyn crate::IVulkanContextExternalObjectsFeature>().is_none());
256        assert!(features.try_get_shared::<dyn crate::IVulkanKhrSurfacePlatformSurfaceFactory>().is_none());
257        context.dispose();
258    }
259
260    #[test]
261    #[should_panic(expected = "Specified method is not supported.")]
262    fn a_custom_device_factory_creates_no_devices() {
263        let options = VulkanOptions { custom_shared_device: Some(Arc::new(Device::default())), ..Default::default() };
264        let graphics = VulkanPlatformGraphics::try_create(&options, Default::default()).unwrap();
265        let _ = graphics.create_context();
266    }
267
268    #[test]
269    fn without_a_way_to_the_entry_points_there_are_no_platform_graphics() {
270        assert!(VulkanPlatformGraphics::try_create(&VulkanOptions::default(), Default::default()).is_none());
271    }
272
273    #[test]
274    #[should_panic(expected = "Unable to find a suitable platform surface")]
275    fn a_context_without_a_suitable_surface_creates_no_render_target() {
276        let options = VulkanOptions { custom_shared_device: Some(Arc::new(Device::default())), ..Default::default() };
277        let graphics = VulkanPlatformGraphics::try_create(&options, Default::default()).unwrap();
278        let context = graphics.get_shared_context();
279        let features: &dyn IOptionalFeatureProvider = &*context;
280        let vulkan = features.try_get::<dyn IVulkanPlatformGraphicsContext>().unwrap();
281        let _ = vulkan.create_render_target(&[]);
282    }
283
284    #[test]
285    fn the_platform_graphics_are_shared_by_threads() {
286        fn assert_send_sync<T: Send + Sync>() {}
287        assert_send_sync::<VulkanPlatformGraphics>();
288        assert_send_sync::<Arc<dyn IVulkanDevice>>();
289        assert_send_sync::<Arc<dyn IVulkanInstance>>();
290    }
291}