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::*;
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::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::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::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::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::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::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::PipelineCache,
131        create_info: &vk::GraphicsPipelineCreateInfo,
132        allocator: Option<&vk::AllocationCallbacks>,
133    ) -> crate::VkResult<vk::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::PipelineCache,
149        create_info: &vk::ComputePipelineCreateInfo,
150        allocator: Option<&vk::AllocationCallbacks>,
151    ) -> crate::VkResult<vk::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::DeviceMemory,
169        offset: u64,
170        size: u64,
171        flags: vk::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::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::Handle;
207
208    fn fake_handle() -> vk::Device {
209        vk::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::Device,
215        _name: *const c_char,
216    ) -> vk::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(_device: vk::Device) -> vk::Result {
282        vk::Result::SUCCESS
283    }
284
285    unsafe extern "system" fn mock_destroy_device(
286        _device: vk::Device,
287        _p_allocator: *const vk::AllocationCallbacks,
288    ) {
289    }
290
291    /// `vkGetDeviceProcAddr` that resolves a few commands for richer testing.
292    unsafe extern "system" fn rich_get_device_proc_addr(
293        _device: vk::Device,
294        name: *const c_char,
295    ) -> vk::PFN_vkVoidFunction {
296        let name = unsafe { std::ffi::CStr::from_ptr(name) };
297        match name.to_bytes() {
298            b"vkDeviceWaitIdle" => Some(unsafe {
299                std::mem::transmute::<
300                    unsafe extern "system" fn(vk::Device) -> vk::Result,
301                    unsafe extern "system" fn(),
302                >(mock_device_wait_idle)
303            }),
304            b"vkDestroyDevice" => Some(unsafe {
305                std::mem::transmute::<
306                    unsafe extern "system" fn(vk::Device, *const vk::AllocationCallbacks),
307                    unsafe extern "system" fn(),
308                >(mock_destroy_device)
309            }),
310            _ => None,
311        }
312    }
313
314    #[test]
315    fn load_with_rich_mock_populates_some_commands() {
316        let device = unsafe { Device::load(fake_handle(), Some(rich_get_device_proc_addr), None) };
317        assert!(
318            device.commands().device_wait_idle.is_some(),
319            "device_wait_idle should be loaded"
320        );
321        assert!(
322            device.commands().destroy_device.is_some(),
323            "destroy_device should be loaded"
324        );
325        // Commands not returned by the mock should still be None.
326        assert!(device.commands().create_buffer.is_none());
327    }
328
329    #[test]
330    fn from_raw_parts_with_rich_mock_populates_commands() {
331        let device =
332            unsafe { Device::from_raw_parts(fake_handle(), Some(rich_get_device_proc_addr)) };
333        assert!(device.commands().device_wait_idle.is_some());
334        assert!(device.commands().destroy_device.is_some());
335    }
336
337    #[test]
338    fn device_wait_idle_succeeds_with_mock() {
339        let device =
340            unsafe { Device::from_raw_parts(fake_handle(), Some(rich_get_device_proc_addr)) };
341        let result = unsafe { device.device_wait_idle() };
342        assert!(result.is_ok());
343    }
344
345    #[test]
346    fn destroy_device_succeeds_with_mock() {
347        let device =
348            unsafe { Device::from_raw_parts(fake_handle(), Some(rich_get_device_proc_addr)) };
349        // Should not panic; the mock destroy is a no-op.
350        unsafe { device.destroy_device(None) };
351    }
352
353    #[test]
354    fn from_raw_parts_stores_no_loader() {
355        let device =
356            unsafe { Device::from_raw_parts(fake_handle(), Some(mock_get_device_proc_addr)) };
357        // from_raw_parts passes None for loader, verify handle is correct.
358        assert_eq!(device.handle().as_raw(), fake_handle().as_raw());
359    }
360
361    #[test]
362    fn load_with_loader_keeps_arc_alive() {
363        use std::ffi::{CStr, c_void};
364        struct DummyLoader;
365        unsafe impl Loader for DummyLoader {
366            unsafe fn load(&self, _name: &CStr) -> *const c_void {
367                std::ptr::null()
368            }
369        }
370        let loader: Arc<dyn Loader> = Arc::new(DummyLoader);
371        let weak = Arc::downgrade(&loader);
372        let device =
373            unsafe { Device::load(fake_handle(), Some(mock_get_device_proc_addr), Some(loader)) };
374        // The Arc should be kept alive by the device.
375        assert!(weak.upgrade().is_some(), "loader should still be alive");
376        drop(device);
377        // After device is dropped, the Arc should be released.
378        assert!(weak.upgrade().is_none(), "loader should be dropped");
379    }
380
381    #[test]
382    #[ignore] // requires Vulkan runtime
383    fn device_wait_idle_succeeds() {
384        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
385        let (instance, device) = create_real_device();
386        unsafe { device.device_wait_idle() }.expect("device_wait_idle failed");
387        unsafe { device.destroy_device(None) };
388        unsafe { instance.destroy_instance(None) };
389    }
390
391    #[test]
392    #[ignore] // requires Vulkan runtime
393    fn get_device_queue_returns_non_null_queue() {
394        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
395        let (instance, device) = create_real_device();
396        let queue = unsafe { device.get_device_queue(0, 0) };
397        assert!(!queue.is_null(), "expected non-null queue handle");
398        unsafe { device.destroy_device(None) };
399        unsafe { instance.destroy_instance(None) };
400    }
401
402    #[test]
403    #[ignore] // requires Vulkan runtime
404    fn queue_wait_idle_succeeds() {
405        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
406        let (instance, device) = create_real_device();
407        let queue = unsafe { device.get_device_queue(0, 0) };
408        unsafe { device.queue_wait_idle(queue) }.expect("queue_wait_idle failed");
409        unsafe { device.destroy_device(None) };
410        unsafe { instance.destroy_instance(None) };
411    }
412
413    fn create_real_device() -> (crate::instance::Instance, Device) {
414        use crate::entry::Entry;
415        use crate::loader::LibloadingLoader;
416
417        let loader = LibloadingLoader::new().expect("failed to load Vulkan");
418        let entry = unsafe { Entry::new(loader) }.expect("failed to create Entry");
419
420        let api_version_1_0 = crate::Version::new(1, 0, 0).to_raw();
421        let app_info = vk::ApplicationInfo {
422            s_type: vk::StructureType::APPLICATION_INFO,
423            p_next: std::ptr::null(),
424            p_application_name: std::ptr::null(),
425            application_version: 0,
426            p_engine_name: std::ptr::null(),
427            engine_version: 0,
428            api_version: api_version_1_0,
429        };
430        let instance_create_info = vk::InstanceCreateInfo {
431            s_type: vk::StructureType::INSTANCE_CREATE_INFO,
432            p_next: std::ptr::null(),
433            flags: vk::InstanceCreateFlagBits::empty(),
434            p_application_info: &app_info,
435            enabled_layer_count: 0,
436            pp_enabled_layer_names: std::ptr::null(),
437            enabled_extension_count: 0,
438            pp_enabled_extension_names: std::ptr::null(),
439        };
440        let instance = unsafe { entry.create_instance(&instance_create_info, None) }
441            .expect("failed to create instance");
442
443        let physical_devices = unsafe { instance.enumerate_physical_devices() }
444            .expect("failed to enumerate physical devices");
445        let physical_device = physical_devices[0];
446
447        let queue_priority = 1.0f32;
448        let queue_create_info = vk::DeviceQueueCreateInfo {
449            s_type: vk::StructureType::DEVICE_QUEUE_CREATE_INFO,
450            p_next: std::ptr::null(),
451            flags: vk::DeviceQueueCreateFlagBits::empty(),
452            queue_family_index: 0,
453            queue_count: 1,
454            p_queue_priorities: &queue_priority,
455        };
456        let device_create_info = vk::DeviceCreateInfo {
457            s_type: vk::StructureType::DEVICE_CREATE_INFO,
458            p_next: std::ptr::null(),
459            flags: 0,
460            queue_create_info_count: 1,
461            p_queue_create_infos: &queue_create_info,
462            enabled_layer_count: 0,
463            pp_enabled_layer_names: std::ptr::null(),
464            enabled_extension_count: 0,
465            pp_enabled_extension_names: std::ptr::null(),
466            p_enabled_features: std::ptr::null(),
467        };
468        let device = unsafe { instance.create_device(physical_device, &device_create_info, None) }
469            .expect("failed to create device");
470
471        (instance, device)
472    }
473}