Skip to main content

vulkan_rust/
entry.rs

1use std::sync::Arc;
2
3use std::ffi::CStr;
4
5use crate::error::{LoadError, VkResult, check, enumerate_two_call};
6use crate::instance::Instance;
7use crate::loader::Loader;
8use crate::version::Version;
9use crate::vk;
10use vk::Handle;
11
12/// Entry point into the Vulkan API.
13///
14/// Loads the Vulkan shared library, resolves the bootstrap function pointers,
15/// and provides access to entry-level commands (instance creation, version
16/// query, layer/extension enumeration).
17///
18/// The `Entry` keeps the shared library alive via `Arc<dyn Loader>` for the
19/// lifetime of all derived objects.
20///
21/// **Guide:** [Hello Triangle, Part 1](https://hiddentale.github.io/vulkan_rust/getting-started/hello-triangle-1.html)
22/// covers creating an `Entry` and bootstrapping the API.
23///
24/// # Examples
25///
26/// ```no_run
27/// use vulkan_rust::{Entry, LibloadingLoader};
28///
29/// let loader = unsafe { LibloadingLoader::new() }.expect("Vulkan not found");
30/// let entry = unsafe { Entry::new(loader) }.expect("entry creation failed");
31///
32/// let version = entry.version().expect("version query failed");
33/// println!("Vulkan {version}");
34/// ```
35pub struct Entry {
36    _loader: Arc<dyn Loader>,
37    get_instance_proc_addr: vk::commands::PFN_vkGetInstanceProcAddr,
38    get_device_proc_addr: vk::commands::PFN_vkGetDeviceProcAddr,
39    commands: vk::commands::EntryCommands,
40}
41
42impl Entry {
43    /// Create a new `Entry` from the given loader.
44    ///
45    /// Resolves `vkGetInstanceProcAddr` from the library, then uses it to
46    /// bootstrap `vkGetDeviceProcAddr` and all entry-level commands.
47    ///
48    /// # Safety
49    ///
50    /// The loader must return valid Vulkan function pointers. The loaded
51    /// shared library must remain valid for the lifetime of this `Entry`
52    /// and any objects created from it.
53    ///
54    /// # Examples
55    ///
56    /// ```no_run
57    /// use vulkan_rust::{Entry, LibloadingLoader};
58    ///
59    /// let loader = unsafe { LibloadingLoader::new() }.expect("Vulkan not found");
60    /// let entry = unsafe { Entry::new(loader) }.expect("entry creation failed");
61    /// ```
62    pub unsafe fn new(loader: impl Loader + 'static) -> Result<Self, LoadError> {
63        let loader: Arc<dyn Loader> = Arc::new(loader);
64
65        // SAFETY: loader returns a valid fn ptr or null; null is checked before transmute.
66        let get_instance_proc_addr: vk::commands::PFN_vkGetInstanceProcAddr = unsafe {
67            let ptr = loader.load(c"vkGetInstanceProcAddr");
68            if ptr.is_null() {
69                return Err(LoadError::MissingEntryPoint);
70            }
71            std::mem::transmute(ptr)
72        };
73
74        let get_instance_proc_addr_fn =
75            get_instance_proc_addr.expect("vkGetInstanceProcAddr not loaded");
76        let null_instance = vk::Instance::null();
77
78        // SAFETY: loader returns a valid fn ptr or null; null becomes None.
79        let get_device_proc_addr: vk::commands::PFN_vkGetDeviceProcAddr =
80            unsafe { std::mem::transmute(loader.load(c"vkGetDeviceProcAddr")) };
81
82        // SAFETY: get_instance_proc_addr_fn is valid; null instance queries entry-level commands.
83        let commands = unsafe {
84            vk::commands::EntryCommands::load(|name| {
85                std::mem::transmute(get_instance_proc_addr_fn(null_instance, name.as_ptr()))
86            })
87        };
88
89        Ok(Self {
90            _loader: loader,
91            get_instance_proc_addr,
92            get_device_proc_addr,
93            commands,
94        })
95    }
96
97    /// Returns the raw `vkGetInstanceProcAddr` function pointer.
98    ///
99    /// Needed by OpenXR's `XR_KHR_vulkan_enable2` which requires the
100    /// application to provide this function pointer.
101    pub fn get_instance_proc_addr(&self) -> vk::commands::PFN_vkGetInstanceProcAddr {
102        self.get_instance_proc_addr
103    }
104
105    /// Returns the raw `vkGetDeviceProcAddr` function pointer.
106    pub fn get_device_proc_addr(&self) -> vk::commands::PFN_vkGetDeviceProcAddr {
107        self.get_device_proc_addr
108    }
109
110    /// Returns a reference to the loaded entry-level commands.
111    pub(crate) fn commands(&self) -> &vk::commands::EntryCommands {
112        &self.commands
113    }
114
115    /// Query the Vulkan instance version supported by the loader.
116    ///
117    /// Requires Vulkan 1.1+. On a 1.0-only system where
118    /// `vkEnumerateInstanceVersion` is unavailable, returns `1.0.0`.
119    ///
120    /// # Examples
121    ///
122    /// ```no_run
123    /// # let entry = vulkan_rust::test_helpers::create_test_entry().unwrap();
124    /// let version = entry.version().expect("version query failed");
125    /// assert!(version.major >= 1);
126    /// println!("Vulkan {version}");
127    /// ```
128    pub fn version(&self) -> VkResult<Version> {
129        let fp = match self.commands.enumerate_instance_version {
130            Some(fp) => fp,
131            None => {
132                return Ok(Version {
133                    major: 1,
134                    minor: 0,
135                    patch: 0,
136                });
137            }
138        };
139        let mut raw = 0u32;
140        // SAFETY: fp is vkEnumerateInstanceVersion; raw is a valid output pointer.
141        check(unsafe { fp(&mut raw) })?;
142        Ok(Version::from_raw(raw))
143    }
144
145    /// Create a Vulkan instance.
146    ///
147    /// # Safety
148    ///
149    /// `create_info` must be a valid, fully populated `InstanceCreateInfo`.
150    /// The caller is responsible for calling `instance.destroy_instance`
151    /// when done.
152    ///
153    /// # Examples
154    ///
155    /// ```no_run
156    /// use vulkan_rust::{Entry, LibloadingLoader, Version};
157    /// use vulkan_rust::vk::*;
158    ///
159    /// let loader = unsafe { LibloadingLoader::new() }.expect("Vulkan not found");
160    /// let entry = unsafe { Entry::new(loader) }.expect("entry creation failed");
161    ///
162    /// let app_info = ApplicationInfo::builder()
163    ///     .api_version(Version::new(1, 0, 0).to_raw());
164    /// let create_info = InstanceCreateInfo::builder()
165    ///     .application_info(&*app_info);
166    /// let instance = unsafe { entry.create_instance(&create_info, None) }
167    ///     .expect("instance creation failed");
168    /// // Use instance...
169    /// unsafe { instance.destroy_instance(None) };
170    /// ```
171    pub unsafe fn create_instance(
172        &self,
173        create_info: &vk::InstanceCreateInfo,
174        allocator: Option<&vk::AllocationCallbacks>,
175    ) -> VkResult<Instance> {
176        // SAFETY: caller guarantees create_info is valid (this fn is unsafe).
177        let raw = unsafe { self.create_instance_raw(create_info, allocator) }?;
178        // SAFETY: raw is a freshly created valid instance handle.
179        let instance = unsafe {
180            Instance::load(
181                raw,
182                self.get_instance_proc_addr,
183                self.get_device_proc_addr,
184                Some(self._loader.clone()),
185            )
186        };
187        Ok(instance)
188    }
189
190    /// Create a Vulkan instance and return the raw handle.
191    ///
192    /// Use this when you need the `VkInstance` handle without the wrapper,
193    /// for example when passing it to OpenXR which manages the instance
194    /// lifetime externally.
195    ///
196    /// # Safety
197    ///
198    /// `create_info` must be a valid, fully populated `InstanceCreateInfo`.
199    /// The caller is responsible for destroying the instance with
200    /// `vkDestroyInstance` when done.
201    pub unsafe fn create_instance_raw(
202        &self,
203        create_info: &vk::InstanceCreateInfo,
204        allocator: Option<&vk::AllocationCallbacks>,
205    ) -> VkResult<vk::Instance> {
206        let fp = self
207            .commands
208            .create_instance
209            .expect("vkCreateInstance not loaded");
210        let mut instance = vk::Instance::null();
211        // SAFETY: caller guarantees create_info is valid (this fn is unsafe).
212        let result = unsafe {
213            fp(
214                create_info,
215                allocator.map_or(std::ptr::null(), |a| a),
216                &mut instance,
217            )
218        };
219        check(result)?;
220        Ok(instance)
221    }
222
223    /// Enumerate available instance layer properties.
224    ///
225    /// # Safety
226    ///
227    /// The Vulkan loader must be in a valid state.
228    pub unsafe fn enumerate_instance_layer_properties(&self) -> VkResult<Vec<vk::LayerProperties>> {
229        let fp = self
230            .commands
231            .enumerate_instance_layer_properties
232            .expect("vkEnumerateInstanceLayerProperties not loaded");
233        // SAFETY: fp is vkEnumerateInstanceLayerProperties; two-call pattern handles allocation.
234        enumerate_two_call(|count, data| unsafe { fp(count, data) })
235    }
236
237    /// Enumerate available instance extension properties.
238    ///
239    /// Pass `None` for `layer_name` to enumerate extensions provided by the
240    /// loader and implicit layers. Pass a layer name to enumerate extensions
241    /// provided by that layer.
242    ///
243    /// # Safety
244    ///
245    /// If `layer_name` is `Some`, it must name a layer that is present.
246    pub unsafe fn enumerate_instance_extension_properties(
247        &self,
248        layer_name: Option<&CStr>,
249    ) -> VkResult<Vec<vk::ExtensionProperties>> {
250        let fp = self
251            .commands
252            .enumerate_instance_extension_properties
253            .expect("vkEnumerateInstanceExtensionProperties not loaded");
254        let layer_ptr = layer_name.map_or(std::ptr::null(), |n| n.as_ptr());
255        // SAFETY: fp is vkEnumerateInstanceExtensionProperties; layer_ptr is null or valid CStr.
256        enumerate_two_call(|count, data| unsafe { fp(layer_ptr, count, data) })
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use std::ffi::{CStr, c_char, c_void};
264
265    /// Mock loader that returns null for everything,simulates missing library.
266    struct NullLoader;
267
268    unsafe impl Loader for NullLoader {
269        unsafe fn load(&self, _name: &CStr) -> *const c_void {
270            std::ptr::null()
271        }
272    }
273
274    /// Mock loader that returns a fake `vkGetInstanceProcAddr` which itself
275    /// returns null for everything. This lets us construct an Entry without
276    /// a real Vulkan runtime.
277    struct FakeEntryLoader;
278
279    unsafe extern "system" fn mock_get_instance_proc_addr(
280        _instance: vk::Instance,
281        _name: *const c_char,
282    ) -> vk::PFN_vkVoidFunction {
283        None
284    }
285
286    unsafe impl Loader for FakeEntryLoader {
287        unsafe fn load(&self, name: &CStr) -> *const c_void {
288            if name == c"vkGetInstanceProcAddr" {
289                mock_get_instance_proc_addr as *const c_void
290            } else {
291                std::ptr::null()
292            }
293        }
294    }
295
296    #[test]
297    fn new_returns_missing_entry_point_when_loader_returns_null() {
298        let result = unsafe { Entry::new(NullLoader) };
299        match result {
300            Err(LoadError::MissingEntryPoint) => {}
301            Err(other) => panic!("expected MissingEntryPoint, got {other}"),
302            Ok(_) => panic!("expected error, got Ok"),
303        }
304    }
305
306    #[test]
307    fn new_succeeds_with_fake_loader() {
308        let entry = unsafe { Entry::new(FakeEntryLoader) }.expect("should create Entry");
309        assert!(entry.get_instance_proc_addr().is_some());
310    }
311
312    #[test]
313    fn version_returns_1_0_when_enumerate_instance_version_is_none() {
314        // FakeEntryLoader returns null for all commands, so
315        // enumerate_instance_version will be None → 1.0 fallback.
316        let entry = unsafe { Entry::new(FakeEntryLoader) }.expect("should create Entry");
317        let version = entry.version().expect("version should succeed");
318        assert_eq!(version.major, 1);
319        assert_eq!(version.minor, 0);
320        assert_eq!(version.patch, 0);
321    }
322
323    #[test]
324    fn commands_returns_reference() {
325        let entry = unsafe { Entry::new(FakeEntryLoader) }.expect("should create Entry");
326        let _ = entry.commands();
327    }
328
329    #[test]
330    fn get_instance_proc_addr_returns_some() {
331        let entry = unsafe { Entry::new(FakeEntryLoader) }.expect("should create Entry");
332        assert!(entry.get_instance_proc_addr().is_some());
333    }
334
335    #[test]
336    fn get_device_proc_addr_returns_none_from_fake_loader() {
337        // FakeEntryLoader returns null for vkGetDeviceProcAddr
338        let entry = unsafe { Entry::new(FakeEntryLoader) }.expect("should create Entry");
339        assert!(entry.get_device_proc_addr().is_none());
340    }
341
342    // -- Rich mock loader that returns fake entry-level function pointers ----
343
344    /// Mock loader where `vkGetInstanceProcAddr` dispatches to fake
345    /// implementations of entry-level commands.
346    struct RichEntryLoader;
347
348    unsafe extern "system" fn rich_get_instance_proc_addr(
349        _instance: vk::Instance,
350        name: *const c_char,
351    ) -> vk::PFN_vkVoidFunction {
352        let name = unsafe { CStr::from_ptr(name) };
353        match name.to_bytes() {
354            b"vkEnumerateInstanceVersion" => Some(unsafe {
355                std::mem::transmute::<
356                    unsafe extern "system" fn(*mut u32) -> vk::Result,
357                    unsafe extern "system" fn(),
358                >(mock_enumerate_instance_version)
359            }),
360            b"vkEnumerateInstanceLayerProperties" => Some(unsafe {
361                std::mem::transmute::<
362                    unsafe extern "system" fn(*mut u32, *mut vk::LayerProperties) -> vk::Result,
363                    unsafe extern "system" fn(),
364                >(mock_enumerate_instance_layer_properties)
365            }),
366            b"vkEnumerateInstanceExtensionProperties" => Some(unsafe {
367                std::mem::transmute::<
368                    unsafe extern "system" fn(
369                        *const c_char,
370                        *mut u32,
371                        *mut vk::ExtensionProperties,
372                    ) -> vk::Result,
373                    unsafe extern "system" fn(),
374                >(mock_enumerate_instance_extension_properties)
375            }),
376            b"vkCreateInstance" => Some(unsafe {
377                std::mem::transmute::<
378                    unsafe extern "system" fn(
379                        *const vk::InstanceCreateInfo,
380                        *const vk::AllocationCallbacks,
381                        *mut vk::Instance,
382                    ) -> vk::Result,
383                    unsafe extern "system" fn(),
384                >(mock_create_instance)
385            }),
386            _ => None,
387        }
388    }
389
390    unsafe extern "system" fn mock_enumerate_instance_version(
391        p_api_version: *mut u32,
392    ) -> vk::Result {
393        unsafe { *p_api_version = crate::Version::new(1, 3, 290).to_raw() };
394        vk::Result::SUCCESS
395    }
396
397    unsafe extern "system" fn mock_enumerate_instance_layer_properties(
398        p_count: *mut u32,
399        _p_properties: *mut vk::LayerProperties,
400    ) -> vk::Result {
401        unsafe { *p_count = 0 };
402        vk::Result::SUCCESS
403    }
404
405    unsafe extern "system" fn mock_enumerate_instance_extension_properties(
406        _p_layer_name: *const c_char,
407        p_count: *mut u32,
408        _p_properties: *mut vk::ExtensionProperties,
409    ) -> vk::Result {
410        unsafe { *p_count = 0 };
411        vk::Result::SUCCESS
412    }
413
414    unsafe extern "system" fn mock_create_instance(
415        _p_create_info: *const vk::InstanceCreateInfo,
416        _p_allocator: *const vk::AllocationCallbacks,
417        p_instance: *mut vk::Instance,
418    ) -> vk::Result {
419        // Write a non-null sentinel handle.
420        unsafe { *p_instance = std::mem::transmute::<usize, vk::Instance>(0x1234_usize) };
421        vk::Result::SUCCESS
422    }
423
424    unsafe impl Loader for RichEntryLoader {
425        unsafe fn load(&self, name: &CStr) -> *const c_void {
426            match name.to_bytes() {
427                b"vkGetInstanceProcAddr" => rich_get_instance_proc_addr as *const c_void,
428                b"vkGetDeviceProcAddr" => std::ptr::null(),
429                _ => std::ptr::null(),
430            }
431        }
432    }
433
434    #[test]
435    fn version_returns_parsed_version_when_fp_available() {
436        let entry = unsafe { Entry::new(RichEntryLoader) }.expect("should create Entry");
437        let version = entry.version().expect("version should succeed");
438        assert_eq!(version.major, 1);
439        assert_eq!(version.minor, 3);
440        assert_eq!(version.patch, 290);
441    }
442
443    #[test]
444    fn enumerate_layer_properties_with_mock() {
445        let entry = unsafe { Entry::new(RichEntryLoader) }.expect("should create Entry");
446        let layers =
447            unsafe { entry.enumerate_instance_layer_properties() }.expect("should succeed");
448        assert!(layers.is_empty());
449    }
450
451    #[test]
452    fn enumerate_extension_properties_with_mock() {
453        let entry = unsafe { Entry::new(RichEntryLoader) }.expect("should create Entry");
454        let extensions =
455            unsafe { entry.enumerate_instance_extension_properties(None) }.expect("should succeed");
456        assert!(extensions.is_empty());
457    }
458
459    #[test]
460    fn enumerate_extension_properties_with_layer_name() {
461        let entry = unsafe { Entry::new(RichEntryLoader) }.expect("should create Entry");
462        let extensions =
463            unsafe { entry.enumerate_instance_extension_properties(Some(c"VK_LAYER_test")) }
464                .expect("should succeed");
465        assert!(extensions.is_empty());
466    }
467
468    #[test]
469    fn create_instance_raw_with_mock() {
470        let entry = unsafe { Entry::new(RichEntryLoader) }.expect("should create Entry");
471        let create_info: vk::InstanceCreateInfo = unsafe { std::mem::zeroed() };
472        let raw = unsafe { entry.create_instance_raw(&create_info, None) }.expect("should succeed");
473        assert!(!raw.is_null());
474    }
475
476    // -- Error-path mock loader -----------------------------------------------
477
478    /// Mock loader where entry-level commands return Vulkan errors.
479    struct FailingEntryLoader;
480
481    unsafe extern "system" fn failing_get_instance_proc_addr(
482        _instance: vk::Instance,
483        name: *const c_char,
484    ) -> vk::PFN_vkVoidFunction {
485        let name = unsafe { CStr::from_ptr(name) };
486        match name.to_bytes() {
487            b"vkEnumerateInstanceVersion" => Some(unsafe {
488                std::mem::transmute::<
489                    unsafe extern "system" fn(*mut u32) -> vk::Result,
490                    unsafe extern "system" fn(),
491                >(failing_enumerate_instance_version)
492            }),
493            b"vkEnumerateInstanceLayerProperties" => Some(unsafe {
494                std::mem::transmute::<
495                    unsafe extern "system" fn(*mut u32, *mut vk::LayerProperties) -> vk::Result,
496                    unsafe extern "system" fn(),
497                >(failing_enumerate_instance_layer_properties)
498            }),
499            b"vkEnumerateInstanceExtensionProperties" => Some(unsafe {
500                std::mem::transmute::<
501                    unsafe extern "system" fn(
502                        *const c_char,
503                        *mut u32,
504                        *mut vk::ExtensionProperties,
505                    ) -> vk::Result,
506                    unsafe extern "system" fn(),
507                >(failing_enumerate_instance_extension_properties)
508            }),
509            b"vkCreateInstance" => Some(unsafe {
510                std::mem::transmute::<
511                    unsafe extern "system" fn(
512                        *const vk::InstanceCreateInfo,
513                        *const vk::AllocationCallbacks,
514                        *mut vk::Instance,
515                    ) -> vk::Result,
516                    unsafe extern "system" fn(),
517                >(failing_create_instance)
518            }),
519            _ => None,
520        }
521    }
522
523    unsafe extern "system" fn failing_enumerate_instance_version(
524        _p_api_version: *mut u32,
525    ) -> vk::Result {
526        vk::Result::ERROR_OUT_OF_HOST_MEMORY
527    }
528
529    unsafe extern "system" fn failing_enumerate_instance_layer_properties(
530        _p_count: *mut u32,
531        _p_properties: *mut vk::LayerProperties,
532    ) -> vk::Result {
533        vk::Result::ERROR_OUT_OF_HOST_MEMORY
534    }
535
536    unsafe extern "system" fn failing_enumerate_instance_extension_properties(
537        _p_layer_name: *const c_char,
538        _p_count: *mut u32,
539        _p_properties: *mut vk::ExtensionProperties,
540    ) -> vk::Result {
541        vk::Result::ERROR_OUT_OF_HOST_MEMORY
542    }
543
544    unsafe extern "system" fn failing_create_instance(
545        _p_create_info: *const vk::InstanceCreateInfo,
546        _p_allocator: *const vk::AllocationCallbacks,
547        _p_instance: *mut vk::Instance,
548    ) -> vk::Result {
549        vk::Result::ERROR_INITIALIZATION_FAILED
550    }
551
552    unsafe impl Loader for FailingEntryLoader {
553        unsafe fn load(&self, name: &CStr) -> *const c_void {
554            match name.to_bytes() {
555                b"vkGetInstanceProcAddr" => failing_get_instance_proc_addr as *const c_void,
556                _ => std::ptr::null(),
557            }
558        }
559    }
560
561    #[test]
562    fn version_propagates_error() {
563        let entry = unsafe { Entry::new(FailingEntryLoader) }.expect("should create Entry");
564        let result = entry.version();
565        assert_eq!(result.unwrap_err(), vk::Result::ERROR_OUT_OF_HOST_MEMORY);
566    }
567
568    #[test]
569    fn create_instance_raw_propagates_error() {
570        let entry = unsafe { Entry::new(FailingEntryLoader) }.expect("should create Entry");
571        let create_info: vk::InstanceCreateInfo = unsafe { std::mem::zeroed() };
572        let result = unsafe { entry.create_instance_raw(&create_info, None) };
573        assert_eq!(result.unwrap_err(), vk::Result::ERROR_INITIALIZATION_FAILED);
574    }
575
576    #[test]
577    fn enumerate_layer_properties_propagates_error() {
578        let entry = unsafe { Entry::new(FailingEntryLoader) }.expect("should create Entry");
579        let result = unsafe { entry.enumerate_instance_layer_properties() };
580        assert_eq!(result.unwrap_err(), vk::Result::ERROR_OUT_OF_HOST_MEMORY);
581    }
582
583    #[test]
584    fn enumerate_extension_properties_propagates_error() {
585        let entry = unsafe { Entry::new(FailingEntryLoader) }.expect("should create Entry");
586        let result = unsafe { entry.enumerate_instance_extension_properties(None) };
587        assert_eq!(result.unwrap_err(), vk::Result::ERROR_OUT_OF_HOST_MEMORY);
588    }
589
590    #[test]
591    #[ignore] // requires Vulkan runtime
592    fn new_succeeds_with_real_loader() {
593        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
594        let loader = crate::loader::LibloadingLoader::new().expect("failed to load Vulkan library");
595        let entry = unsafe { Entry::new(loader) }.expect("failed to create Entry");
596        assert!(entry.get_instance_proc_addr().is_some());
597        assert!(entry.get_device_proc_addr().is_some());
598    }
599
600    #[test]
601    #[ignore] // requires Vulkan runtime
602    fn version_returns_at_least_1_0() {
603        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
604        let entry = create_entry();
605        let version = entry.version().expect("failed to query version");
606        assert!(version.major >= 1);
607        println!("Vulkan {version}");
608    }
609
610    #[test]
611    #[ignore] // requires Vulkan runtime
612    fn enumerate_layer_properties_succeeds() {
613        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
614        let entry = create_entry();
615        let layers = unsafe { entry.enumerate_instance_layer_properties() }
616            .expect("failed to enumerate layers");
617        println!("found {} layers", layers.len());
618    }
619
620    #[test]
621    #[ignore] // requires Vulkan runtime
622    fn enumerate_extension_properties_succeeds() {
623        let _vk = crate::VK_TEST_MUTEX.lock().expect("VK_TEST_MUTEX poisoned");
624        let entry = create_entry();
625        let extensions = unsafe { entry.enumerate_instance_extension_properties(None) }
626            .expect("failed to enumerate extensions");
627        assert!(!extensions.is_empty(), "expected at least one extension");
628        println!("found {} extensions", extensions.len());
629    }
630
631    /// Helper to create an Entry for integration tests.
632    fn create_entry() -> Entry {
633        let loader = crate::loader::LibloadingLoader::new().expect("failed to load Vulkan library");
634        unsafe { Entry::new(loader) }.expect("failed to create Entry")
635    }
636}