Skip to main content

vulkan_rust/
device.rs

1use std::sync::Arc;
2
3use crate::loader::Loader;
4use crate::vk;
5
6/// Wrapper around a `VkDevice` handle and its loaded command table.
7///
8/// Owns a `Box<DeviceCommands>` containing all device-level function
9/// pointers, loaded at construction via `vkGetDeviceProcAddr`. Using the
10/// real device handle gives the ICD's direct function pointers, bypassing
11/// the loader trampoline, this is the fastest dispatch path in Vulkan.
12///
13/// Holds an optional reference to the Vulkan shared library so that
14/// function pointers remain valid even if the originating `Entry` is
15/// dropped. When created via `from_raw_parts`, the caller manages the
16/// library lifetime and this field is `None`.
17///
18/// Does **not** implement `Drop`, the caller must explicitly call
19/// `destroy_device` when done. This avoids double-destroy bugs when
20/// wrapping externally managed handles via `from_raw_parts`.
21///
22/// **Guide:** Most device-level operations are covered across
23/// [Memory Management](https://hiddentale.github.io/vulkan_rust/concepts/memory.html),
24/// [Command Buffers](https://hiddentale.github.io/vulkan_rust/concepts/command-buffers.html),
25/// and [Synchronization](https://hiddentale.github.io/vulkan_rust/concepts/synchronization.html).
26///
27/// # Examples
28///
29/// ```no_run
30/// use vulkan_rust::vk::structs::*;
31///
32/// # let (entry, instance, device) = vulkan_rust::test_helpers::create_test_device().unwrap();
33/// // Use the device to create Vulkan objects.
34/// let fence_info = FenceCreateInfo::builder();
35/// let fence = unsafe { device.create_fence(&fence_info, None) }
36///     .expect("create_fence failed");
37///
38/// // Clean up (reverse creation order).
39/// unsafe {
40///     device.destroy_fence(fence, None);
41///     device.destroy_device(None);
42///     instance.destroy_instance(None);
43/// };
44/// ```
45pub struct Device {
46    handle: vk::handles::Device,
47    commands: Box<vk::commands::DeviceCommands>,
48    _loader: Option<Arc<dyn Loader>>,
49}
50
51impl Device {
52    /// Internal construction path. Called by `Instance::create_device`.
53    ///
54    /// # Safety
55    ///
56    /// - `handle` must be a valid `VkDevice`.
57    /// - `get_device_proc_addr` must resolve device-level commands for
58    ///   this handle.
59    pub(crate) unsafe fn load(
60        handle: vk::handles::Device,
61        get_device_proc_addr: vk::commands::PFN_vkGetDeviceProcAddr,
62        loader: Option<Arc<dyn Loader>>,
63    ) -> Self {
64        let get_device_proc_addr_fn = get_device_proc_addr.expect("vkGetDeviceProcAddr not loaded");
65        // SAFETY: handle is valid per caller contract; transmute converts raw fn ptrs.
66        let commands = Box::new(unsafe {
67            vk::commands::DeviceCommands::load(|name| {
68                std::mem::transmute(get_device_proc_addr_fn(handle, name.as_ptr()))
69            })
70        });
71        Self {
72            handle,
73            commands,
74            _loader: loader,
75        }
76    }
77
78    /// Wrap a raw handle created externally (OpenXR, middleware, testing).
79    ///
80    /// # Safety
81    ///
82    /// - `handle` must be a valid `VkDevice`.
83    /// - `get_device_proc_addr` must resolve commands for this device.
84    /// - The caller owns the device lifetime.
85    ///
86    /// # Examples
87    ///
88    /// ```no_run
89    /// use vulkan_rust::Device;
90    /// # use vulkan_rust::vk::handles::Handle;
91    /// # let entry = vulkan_rust::test_helpers::create_test_entry().unwrap();
92    ///
93    /// // Given a raw device handle and proc addr from an external source:
94    /// # let raw_device = vulkan_rust::vk::handles::Device::null();
95    /// let device = unsafe {
96    ///     Device::from_raw_parts(raw_device, entry.get_device_proc_addr())
97    /// };
98    /// ```
99    pub unsafe fn from_raw_parts(
100        handle: vk::handles::Device,
101        get_device_proc_addr: vk::commands::PFN_vkGetDeviceProcAddr,
102    ) -> Self {
103        // SAFETY: forwards caller's safety guarantees to `load`.
104        unsafe { Self::load(handle, get_device_proc_addr, None) }
105    }
106
107    /// Returns the raw `VkDevice` handle.
108    pub fn handle(&self) -> vk::handles::Device {
109        self.handle
110    }
111
112    /// Returns the loaded device-level command table.
113    ///
114    /// Use this to call any of the ~200 device-level commands directly,
115    /// including those without hand-written ergonomic wrappers.
116    pub fn commands(&self) -> &vk::commands::DeviceCommands {
117        &self.commands
118    }
119
120    /// Create a single graphics pipeline.
121    ///
122    /// Convenience wrapper around [`create_graphics_pipelines`](Self::create_graphics_pipelines)
123    /// for the common single-pipeline case.
124    ///
125    /// # Safety
126    ///
127    /// Same requirements as `create_graphics_pipelines`.
128    pub unsafe fn create_graphics_pipeline(
129        &self,
130        pipeline_cache: vk::handles::PipelineCache,
131        create_info: &vk::structs::GraphicsPipelineCreateInfo,
132        allocator: Option<&vk::structs::AllocationCallbacks>,
133    ) -> crate::VkResult<vk::handles::Pipeline> {
134        unsafe { self.create_graphics_pipelines(pipeline_cache, &[*create_info], allocator) }
135            .map(|v| v[0])
136    }
137
138    /// Create a single compute pipeline.
139    ///
140    /// Convenience wrapper around [`create_compute_pipelines`](Self::create_compute_pipelines)
141    /// for the common single-pipeline case.
142    ///
143    /// # Safety
144    ///
145    /// Same requirements as `create_compute_pipelines`.
146    pub unsafe fn create_compute_pipeline(
147        &self,
148        pipeline_cache: vk::handles::PipelineCache,
149        create_info: &vk::structs::ComputePipelineCreateInfo,
150        allocator: Option<&vk::structs::AllocationCallbacks>,
151    ) -> crate::VkResult<vk::handles::Pipeline> {
152        unsafe { self.create_compute_pipelines(pipeline_cache, &[*create_info], allocator) }
153            .map(|v| v[0])
154    }
155
156    /// Map a device memory object into host address space.
157    ///
158    /// Returns a pointer to the mapped region. Wraps
159    /// [`vkMapMemory`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkMapMemory.html).
160    ///
161    /// # Safety
162    ///
163    /// - `memory` must be a valid, non-mapped `DeviceMemory`.
164    /// - `offset + size` must not exceed the allocation size (`WHOLE_SIZE` is valid for `size`).
165    /// - The returned pointer is only valid until `unmap_memory` is called.
166    pub unsafe fn map_memory(
167        &self,
168        memory: vk::handles::DeviceMemory,
169        offset: u64,
170        size: u64,
171        flags: vk::structs::MemoryMapFlags,
172    ) -> crate::VkResult<*mut core::ffi::c_void> {
173        let fp = self.commands().map_memory.expect("vkMapMemory not loaded");
174        let mut data: *mut core::ffi::c_void = core::ptr::null_mut();
175        crate::error::check(unsafe { fp(self.handle(), memory, offset, size, flags, &mut data) })?;
176        Ok(data)
177    }
178
179    /// Map a device memory object into host address space (Vulkan 1.4+).
180    ///
181    /// Returns a pointer to the mapped region. Wraps
182    /// [`vkMapMemory2`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkMapMemory2.html).
183    ///
184    /// # Safety
185    ///
186    /// - `p_memory_map_info` must describe a valid, non-mapped memory range.
187    /// - The returned pointer is only valid until the memory is unmapped.
188    pub unsafe fn map_memory2(
189        &self,
190        p_memory_map_info: &vk::structs::MemoryMapInfo,
191    ) -> crate::VkResult<*mut core::ffi::c_void> {
192        let fp = self
193            .commands()
194            .map_memory2
195            .expect("vkMapMemory2 not loaded");
196        let mut data: *mut core::ffi::c_void = core::ptr::null_mut();
197        crate::error::check(unsafe { fp(self.handle(), p_memory_map_info, &mut data) })?;
198        Ok(data)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::ffi::c_char;
206    use vk::handles::Handle;
207
208    fn fake_handle() -> vk::handles::Device {
209        vk::handles::Device::from_raw(0xBEEF)
210    }
211
212    /// Stub `vkGetDeviceProcAddr` that returns null for all lookups.
213    unsafe extern "system" fn mock_get_device_proc_addr(
214        _device: vk::handles::Device,
215        _name: *const c_char,
216    ) -> vk::structs::PFN_vkVoidFunction {
217        None
218    }
219
220    #[test]
221    fn from_raw_parts_stores_handle() {
222        let device =
223            unsafe { Device::from_raw_parts(fake_handle(), Some(mock_get_device_proc_addr)) };
224        assert_eq!(device.handle().as_raw(), fake_handle().as_raw());
225    }
226
227    #[test]
228    fn handle_returns_value_from_construction() {
229        let device = unsafe { Device::load(fake_handle(), Some(mock_get_device_proc_addr), None) };
230        assert_eq!(device.handle().as_raw(), fake_handle().as_raw());
231    }
232
233    #[test]
234    fn commands_returns_reference() {
235        let device = unsafe { Device::load(fake_handle(), Some(mock_get_device_proc_addr), None) };
236        // Commands were loaded with a null-returning proc addr, so all
237        // function pointers are None,but the reference is valid.
238        let _ = device.commands();
239    }
240
241    #[test]
242    fn load_with_loader_reference() {
243        use std::ffi::{CStr, c_void};
244        struct DummyLoader;
245        unsafe impl Loader for DummyLoader {
246            unsafe fn load(&self, _name: &CStr) -> *const c_void {
247                std::ptr::null()
248            }
249        }
250        let loader: Arc<dyn Loader> = Arc::new(DummyLoader);
251        let device = unsafe {
252            Device::load(
253                fake_handle(),
254                Some(mock_get_device_proc_addr),
255                Some(loader.clone()),
256            )
257        };
258        assert_eq!(Arc::strong_count(&loader), 2);
259        assert_eq!(device.handle().as_raw(), fake_handle().as_raw());
260    }
261
262    #[test]
263    fn load_without_loader() {
264        let device = unsafe { Device::load(fake_handle(), Some(mock_get_device_proc_addr), None) };
265        assert_eq!(device.handle().as_raw(), fake_handle().as_raw());
266        // All commands should be None since mock returns null.
267        assert!(device.commands().device_wait_idle.is_none());
268    }
269
270    #[test]
271    fn commands_all_none_from_null_mock() {
272        let device =
273            unsafe { Device::from_raw_parts(fake_handle(), Some(mock_get_device_proc_addr)) };
274        assert!(device.commands().create_buffer.is_none());
275        assert!(device.commands().destroy_device.is_none());
276        assert!(device.commands().get_device_queue.is_none());
277    }
278
279    // -- Rich mock that provides some real function pointers ------------------
280
281    unsafe extern "system" fn mock_device_wait_idle(
282        _device: vk::handles::Device,
283    ) -> vk::enums::Result {
284        vk::enums::Result::SUCCESS
285    }
286
287    unsafe extern "system" fn mock_destroy_device(
288        _device: vk::handles::Device,
289        _p_allocator: *const vk::structs::AllocationCallbacks,
290    ) {
291    }
292
293    /// `vkGetDeviceProcAddr` that resolves a few commands for richer testing.
294    unsafe extern "system" fn rich_get_device_proc_addr(
295        _device: vk::handles::Device,
296        name: *const c_char,
297    ) -> vk::structs::PFN_vkVoidFunction {
298        let name = unsafe { std::ffi::CStr::from_ptr(name) };
299        match name.to_bytes() {
300            b"vkDeviceWaitIdle" => Some(unsafe {
301                std::mem::transmute::<
302                    unsafe extern "system" fn(vk::handles::Device) -> vk::enums::Result,
303                    unsafe extern "system" fn(),
304                >(mock_device_wait_idle)
305            }),
306            b"vkDestroyDevice" => Some(unsafe {
307                std::mem::transmute::<
308                    unsafe extern "system" fn(
309                        vk::handles::Device,
310                        *const vk::structs::AllocationCallbacks,
311                    ),
312                    unsafe extern "system" fn(),
313                >(mock_destroy_device)
314            }),
315            _ => None,
316        }
317    }
318
319    #[test]
320    fn load_with_rich_mock_populates_some_commands() {
321        let device = unsafe { Device::load(fake_handle(), Some(rich_get_device_proc_addr), None) };
322        assert!(
323            device.commands().device_wait_idle.is_some(),
324            "device_wait_idle should be loaded"
325        );
326        assert!(
327            device.commands().destroy_device.is_some(),
328            "destroy_device should be loaded"
329        );
330        // Commands not returned by the mock should still be None.
331        assert!(device.commands().create_buffer.is_none());
332    }
333
334    #[test]
335    fn from_raw_parts_with_rich_mock_populates_commands() {
336        let device =
337            unsafe { Device::from_raw_parts(fake_handle(), Some(rich_get_device_proc_addr)) };
338        assert!(device.commands().device_wait_idle.is_some());
339        assert!(device.commands().destroy_device.is_some());
340    }
341
342    #[test]
343    fn device_wait_idle_succeeds_with_mock() {
344        let device =
345            unsafe { Device::from_raw_parts(fake_handle(), Some(rich_get_device_proc_addr)) };
346        let result = unsafe { device.device_wait_idle() };
347        assert!(result.is_ok());
348    }
349
350    #[test]
351    fn destroy_device_succeeds_with_mock() {
352        let device =
353            unsafe { Device::from_raw_parts(fake_handle(), Some(rich_get_device_proc_addr)) };
354        // Should not panic; the mock destroy is a no-op.
355        unsafe { device.destroy_device(None) };
356    }
357
358    #[test]
359    fn from_raw_parts_stores_no_loader() {
360        let device =
361            unsafe { Device::from_raw_parts(fake_handle(), Some(mock_get_device_proc_addr)) };
362        // from_raw_parts passes None for loader, verify handle is correct.
363        assert_eq!(device.handle().as_raw(), fake_handle().as_raw());
364    }
365
366    #[test]
367    fn load_with_loader_keeps_arc_alive() {
368        use std::ffi::{CStr, c_void};
369        struct DummyLoader;
370        unsafe impl Loader for DummyLoader {
371            unsafe fn load(&self, _name: &CStr) -> *const c_void {
372                std::ptr::null()
373            }
374        }
375        let loader: Arc<dyn Loader> = Arc::new(DummyLoader);
376        let weak = Arc::downgrade(&loader);
377        let device =
378            unsafe { Device::load(fake_handle(), Some(mock_get_device_proc_addr), Some(loader)) };
379        // The Arc should be kept alive by the device.
380        assert!(weak.upgrade().is_some(), "loader should still be alive");
381        drop(device);
382        // After device is dropped, the Arc should be released.
383        assert!(weak.upgrade().is_none(), "loader should be dropped");
384    }
385
386    #[test]
387    #[ignore] // requires Vulkan runtime
388    fn device_wait_idle_succeeds() {
389        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
390        let (instance, device) = create_real_device();
391        unsafe { device.device_wait_idle() }.expect("device_wait_idle failed");
392        unsafe { device.destroy_device(None) };
393        unsafe { instance.destroy_instance(None) };
394    }
395
396    #[test]
397    #[ignore] // requires Vulkan runtime
398    fn get_device_queue_returns_non_null_queue() {
399        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
400        let (instance, device) = create_real_device();
401        let queue = unsafe { device.get_device_queue(0, 0) };
402        assert!(!queue.is_null(), "expected non-null queue handle");
403        unsafe { device.destroy_device(None) };
404        unsafe { instance.destroy_instance(None) };
405    }
406
407    #[test]
408    #[ignore] // requires Vulkan runtime
409    fn queue_wait_idle_succeeds() {
410        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
411        let (instance, device) = create_real_device();
412        let queue = unsafe { device.get_device_queue(0, 0) };
413        unsafe { device.queue_wait_idle(queue) }.expect("queue_wait_idle failed");
414        unsafe { device.destroy_device(None) };
415        unsafe { instance.destroy_instance(None) };
416    }
417
418    fn create_real_device() -> (crate::instance::Instance, Device) {
419        use crate::entry::Entry;
420        use crate::loader::LibloadingLoader;
421
422        let loader = LibloadingLoader::new().expect("failed to load Vulkan");
423        let entry = unsafe { Entry::new(loader) }.expect("failed to create Entry");
424
425        let api_version_1_0 = 1u32 << 22;
426        let app_info = vk::structs::ApplicationInfo {
427            s_type: vk::enums::StructureType::APPLICATION_INFO,
428            p_next: std::ptr::null(),
429            p_application_name: std::ptr::null(),
430            application_version: 0,
431            p_engine_name: std::ptr::null(),
432            engine_version: 0,
433            api_version: api_version_1_0,
434        };
435        let instance_create_info = vk::structs::InstanceCreateInfo {
436            s_type: vk::enums::StructureType::INSTANCE_CREATE_INFO,
437            p_next: std::ptr::null(),
438            flags: vk::bitmasks::InstanceCreateFlagBits::empty(),
439            p_application_info: &app_info,
440            enabled_layer_count: 0,
441            pp_enabled_layer_names: std::ptr::null(),
442            enabled_extension_count: 0,
443            pp_enabled_extension_names: std::ptr::null(),
444        };
445        let instance = unsafe { entry.create_instance(&instance_create_info, None) }
446            .expect("failed to create instance");
447
448        let physical_devices = unsafe { instance.enumerate_physical_devices() }
449            .expect("failed to enumerate physical devices");
450        let physical_device = physical_devices[0];
451
452        let queue_priority = 1.0f32;
453        let queue_create_info = vk::structs::DeviceQueueCreateInfo {
454            s_type: vk::enums::StructureType::DEVICE_QUEUE_CREATE_INFO,
455            p_next: std::ptr::null(),
456            flags: vk::bitmasks::DeviceQueueCreateFlagBits::empty(),
457            queue_family_index: 0,
458            queue_count: 1,
459            p_queue_priorities: &queue_priority,
460        };
461        let device_create_info = vk::structs::DeviceCreateInfo {
462            s_type: vk::enums::StructureType::DEVICE_CREATE_INFO,
463            p_next: std::ptr::null(),
464            flags: 0,
465            queue_create_info_count: 1,
466            p_queue_create_infos: &queue_create_info,
467            enabled_layer_count: 0,
468            pp_enabled_layer_names: std::ptr::null(),
469            enabled_extension_count: 0,
470            pp_enabled_extension_names: std::ptr::null(),
471            p_enabled_features: std::ptr::null(),
472        };
473        let device = unsafe { instance.create_device(physical_device, &device_create_info, None) }
474            .expect("failed to create device");
475
476        (instance, device)
477    }
478}