impeller-hal-vulkan 0.1.0

Vulkan rendering backend for impeller-rs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
//! Instance and device bring-up, and the capability detection everything
//! above the HAL branches on.
//!
//! Capability detection is the load-bearing part. The presentation layer
//! decides whether it can allocate its own scanout buffers or must import from
//! GBM, and whether the frame loop can stay explicit or needs a CPU wait
//! before commit, purely from what is reported here. Getting a bit wrong does
//! not fail loudly — it silently selects the slower path, or worse, selects
//! the fast path on a driver that cannot support it.

use crate::render::PipelineCache;
use crate::validation::{self, ValidationLog, ValidationMessage, VALIDATION_LAYER};
use ash::vk;
use impeller_hal::{Capabilities, DmaBufSupport, Error, Result, SampleCounts, SyncSupport};
use std::collections::HashSet;
use std::ffi::{c_char, CStr, CString};
use std::sync::Arc;

/// Which physical device to use.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DevicePreference {
    /// Prefer discrete, then integrated, then anything else.
    #[default]
    Auto,
    /// Prefer a software rasterizer.
    ///
    /// The deterministic reference used for golden comparison: a CPU device
    /// produces the same pixels everywhere, which is what makes it usable as
    /// the oracle other backends are diffed against.
    Software,
    /// A specific index into the enumeration order.
    Index(usize),
}

/// How to create a context.
#[derive(Debug, Clone, Copy, Default)]
pub struct ContextConfig {
    pub device: DevicePreference,
    /// Request the validation layer and capture what it reports.
    ///
    /// Off by default because it costs real time per call. Tests turn it on
    /// and assert the log is clean, which is what keeps "validation-clean"
    /// from depending on someone reading stderr.
    pub validation: bool,
}

/// Instance extensions a presentation target may need.
///
/// `VK_KHR_surface` first because every other one depends on it. The platform
/// ones follow, and the headless surface last: it creates a surface with no
/// window behind it, which is what makes the swapchain path — capability
/// queries, format negotiation, acquire, present, recreation — checkable on a
/// machine with no display at all.
///
/// Creating windows is not this project's business, any more than mode setting
/// is. A caller brings a surface; these are what let one exist.
pub fn surface_extensions() -> &'static [&'static str] {
    &[
        "VK_KHR_surface",
        "VK_KHR_wayland_surface",
        "VK_KHR_xcb_surface",
        "VK_KHR_xlib_surface",
        "VK_EXT_headless_surface",
    ]
}

/// Semaphores a submission waits on and signals.
///
/// Grouped rather than passed loose so that adding a timeline value or a second
/// stage mask later changes one type rather than every signature between here
/// and a presentation target.
///
/// Vulkan-specific on purpose. The HAL's own synchronization is a fence and an
/// exported `sync_file`, which is what crosses to a display controller; a
/// swapchain's semaphores never leave the device and have no counterpart on a
/// backend that has no swapchain. A Vulkan presentation target using Vulkan
/// semaphores is not the layering violation that a renderer asking "is this
/// Vulkan?" would be.
#[derive(Debug, Clone, Copy, Default)]
pub struct FrameSync<'a> {
    /// Waited on before the color attachment is written.
    pub wait: &'a [vk::Semaphore],
    /// Signalled when the submission completes.
    pub signal: &'a [vk::Semaphore],
    /// Leave the target in the layout a presentation engine reads from.
    ///
    /// Folded into the render pass rather than done as a transition afterwards,
    /// and not as an optimization: a separate transition is a separate
    /// submission, and nothing orders it after a render that has not been
    /// waited for. Doing it here makes the ordering the render pass's, which is
    /// where it can be expressed without a stall.
    pub presents: bool,
}

/// Extensions this backend asks for when the device offers them.
///
/// Absence is not an error — it selects a different path, and which one is
/// reported through [`Capabilities`] rather than inferred from the backend.
mod ext {
    pub const EXTERNAL_MEMORY_DMA_BUF: &str = "VK_EXT_external_memory_dma_buf";
    pub const EXTERNAL_MEMORY_FD: &str = "VK_KHR_external_memory_fd";
    pub const IMAGE_DRM_FORMAT_MODIFIER: &str = "VK_EXT_image_drm_format_modifier";
    pub const EXTERNAL_FENCE_FD: &str = "VK_KHR_external_fence_fd";
    pub const EXTERNAL_SEMAPHORE_FD: &str = "VK_KHR_external_semaphore_fd";
    pub const PHYSICAL_DEVICE_DRM: &str = "VK_EXT_physical_device_drm";
    /// Promoted to core in 1.2, so on the 1.1 baseline it must be requested
    /// explicitly as a dependency of the modifier extension.
    pub const IMAGE_FORMAT_LIST: &str = "VK_KHR_image_format_list";
    /// The separable blend modes, which no combination of blend factors can
    /// express. Unrelated to the DRM path; gated the same way because the
    /// answer to "can this device do it" is a capability either way.
    pub const BLEND_OPERATION_ADVANCED: &str = "VK_EXT_blend_operation_advanced";
    /// Presenting into a surface. Absent on a device that can render but not
    /// display, which is a normal thing for a compute-only or headless card to
    /// be.
    pub const SWAPCHAIN: &str = "VK_KHR_swapchain";
}

/// Extensions each wanted extension depends on, beyond what the 1.1 baseline
/// already provides as core.
///
/// The specification requires every dependency to appear in the enable list
/// too, and a device created without them is invalid even when it appears to
/// work. Omitting one here surfaces only under validation, which is why the
/// resolution below drops any extension whose dependencies are unavailable
/// rather than enabling it regardless.
fn required_dependencies(name: &str) -> &'static [&'static str] {
    match name {
        ext::IMAGE_DRM_FORMAT_MODIFIER => &[ext::IMAGE_FORMAT_LIST],
        ext::EXTERNAL_MEMORY_DMA_BUF => &[ext::EXTERNAL_MEMORY_FD],
        _ => &[],
    }
}

/// Whether this device can do every separable blend mode, coherently.
///
/// Three conditions, all required, and all reported as one flag because a
/// caller cannot do anything useful with two of the three. `allOperations`
/// covers the modes themselves; the coherent feature is what lets overlapping
/// draws share a pass without a barrier between them; and the attachment limit
/// has to cover what a pass actually binds. Anything short of all three reports
/// false, and the modes are then refused rather than approximated.
fn probe_advanced_blend(
    instance: &ash::Instance,
    physical_device: vk::PhysicalDevice,
    enabled: &HashSet<String>,
) -> bool {
    if !enabled.contains(ext::BLEND_OPERATION_ADVANCED) {
        return false;
    }

    let mut properties = vk::PhysicalDeviceBlendOperationAdvancedPropertiesEXT::default();
    let mut properties2 = vk::PhysicalDeviceProperties2::default().push_next(&mut properties);
    unsafe { instance.get_physical_device_properties2(physical_device, &mut properties2) };

    let mut features = vk::PhysicalDeviceBlendOperationAdvancedFeaturesEXT::default();
    let mut features2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut features);
    unsafe { instance.get_physical_device_features2(physical_device, &mut features2) };

    properties.advanced_blend_all_operations == vk::TRUE
        && features.advanced_blend_coherent_operations == vk::TRUE
        && properties.advanced_blend_max_color_attachments >= 1
}

/// Expand the wanted set to include dependencies, dropping anything whose
/// dependencies this device does not offer.
fn resolve_extensions(wanted: &[&str], available: &HashSet<String>) -> HashSet<String> {
    let mut enabled = HashSet::new();
    for name in wanted {
        if !available.contains(*name) {
            continue;
        }
        let deps = required_dependencies(name);
        if deps.iter().any(|d| !available.contains(*d)) {
            // Enabling this would produce an invalid device. Skipping it means
            // the capability it backs is reported false, which is the honest
            // answer.
            continue;
        }
        enabled.insert((*name).to_string());
        enabled.extend(deps.iter().map(|d| (*d).to_string()));
    }
    enabled
}

/// A Vulkan device, its queue, and what it can do.
pub struct VulkanContext {
    // The messenger must be destroyed before the log it points at is dropped,
    // and before the instance that owns it.
    debug_messenger: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
    validation_log: Arc<ValidationLog>,
    /// Whether the layer was asked to check synchronization as well as calls.
    ///
    /// Recorded because a clean validation log means two quite different things
    /// depending on it, and nothing else distinguishes them: a run with only
    /// core validation reports every call well formed and says nothing at all
    /// about whether one access was ordered against the next.
    sync_validation: bool,
    // Declaration order is destruction order: the device must outlive nothing
    // and the instance must outlive the device, so Drop tears down in reverse.
    // The allocator must release its memory before the device goes away, which
    // is why it is an Option -- Drop takes it and drops it explicitly first.
    allocator: Option<gpu_allocator::vulkan::Allocator>,
    pipelines: PipelineCache,
    command_pool: vk::CommandPool,
    device: ash::Device,
    physical_device: vk::PhysicalDevice,
    queue: vk::Queue,
    queue_family_index: u32,
    capabilities: Capabilities,
    /// Texture sampling objects, built the first time anything samples.
    ///
    /// Lazy rather than eager because a context that never draws an image
    /// should not allocate a sampler, a layout, and a one-pixel texture to sit
    /// unused -- and every context created by a test that only fills shapes is
    /// exactly that.
    descriptor_layout: Option<vk::DescriptorSetLayout>,
    /// Built on the first batch, like the sampling layout above it.
    material_layout: Option<vk::DescriptorSetLayout>,
    /// Fragment modules a caller registered, by the index they were given.
    ///
    /// Kept by the context rather than travelling with a batch because
    /// registering one leads to a pipeline, and a pipeline outlives every draw
    /// that uses it. The payload is kept rather than the module: a module is
    /// consumed by pipeline creation, and the same program may be needed again
    /// for a different blend, sample count or clip role.
    runtime_programs: Vec<Vec<u32>>,
    /// `minUniformBufferOffsetAlignment`, which sets the stride between one
    /// draw's paint and the next.
    uniform_alignment: u64,
    sampler: Option<vk::Sampler>,
    placeholder: Option<crate::resource::VulkanTexture>,
    /// Pool, view and set for the placeholder, owned by the context rather than
    /// by a submission so that a deferred submission may use it: a per-
    /// submission pool would have to survive until a fence the caller retires
    /// whenever it chooses.
    placeholder_binding: Option<(vk::DescriptorPool, vk::ImageView, vk::DescriptorSet)>,
    enabled_extensions: HashSet<String>,
    instance: ash::Instance,
    _entry: ash::Entry,
}

impl VulkanContext {
    /// Create a context, selecting a physical device by preference.
    pub fn new(preference: DevicePreference) -> Result<Self> {
        Self::with_config(ContextConfig {
            device: preference,
            validation: false,
        })
    }

    /// Create a context with explicit configuration.
    pub fn with_config(config: ContextConfig) -> Result<Self> {
        let preference = config.device;
        // SAFETY: the loader is dlopened and the returned entry points are
        // used only for the lifetime of `entry`, which this struct owns.
        let entry = unsafe { ash::Entry::load() }.map_err(|e| Error::Backend {
            backend: "vulkan",
            detail: format!("loader: {e}"),
        })?;

        let app_name =
            CString::new("impeller-rs").expect("no interior NUL in an extension or layer name");
        let app_info = vk::ApplicationInfo::default()
            .application_name(&app_name)
            .engine_name(&app_name)
            // 1.1 is the floor the project targets. Later features are used
            // when present rather than required.
            .api_version(vk::make_api_version(0, 1, 1, 0));

        // The layer and the debug-utils extension are only requested when both
        // are actually present, so asking for validation on a machine without
        // the SDK degrades to running without it rather than failing to start.
        let want_validation = config.validation
            && layer_available(&entry, VALIDATION_LAYER)
            && instance_extension_available(&entry, "VK_EXT_debug_utils");

        let layer_name =
            CString::new(VALIDATION_LAYER).expect("no interior NUL in an extension or layer name");
        let layer_ptrs: Vec<*const c_char> = if want_validation {
            vec![layer_name.as_ptr()]
        } else {
            Vec::new()
        };
        // Surface extensions are requested wherever the loader offers them, so
        // that a caller who later wants to present into a window finds the
        // instance already able to. Enabling one that goes unused costs
        // nothing; discovering it was missing costs recreating the instance,
        // and by then the device and every resource on it exist too.
        let mut instance_extensions: Vec<CString> = Vec::new();
        if want_validation {
            instance_extensions.push(
                CString::new("VK_EXT_debug_utils")
                    .expect("no interior NUL in an extension or layer name"),
            );
            // Carries the request for synchronization validation below. Its
            // absence is not fatal: the chained struct is then ignored and what
            // is lost is the extra checking rather than the instance.
            if layer_extension_available(&entry, VALIDATION_LAYER, "VK_EXT_validation_features") {
                instance_extensions.push(
                    CString::new("VK_EXT_validation_features")
                        .expect("no interior NUL in an extension or layer name"),
                );
            }
        }
        for &name in surface_extensions() {
            if instance_extension_available(&entry, name) {
                instance_extensions.push(
                    CString::new(name).expect("no interior NUL in an extension or layer name"),
                );
            }
        }
        let ext_ptrs: Vec<*const c_char> = instance_extensions.iter().map(|s| s.as_ptr()).collect();

        // Synchronization validation, which the layer does not do by default.
        // Core validation checks that each call is well formed; this checks
        // that one access is ordered against the next -- a missing barrier, a
        // read of an image the GPU has not finished writing. That is the class
        // of mistake this renderer is most exposed to, because it synchronizes
        // explicitly rather than through a driver that hides it, and the class
        // whose symptom is a correct picture on the device it was written on
        // and a wrong one elsewhere.
        // Asked of the layer, not of the loader. `VK_EXT_validation_features` is
        // the validation layer's own extension, so enumerating without naming
        // the layer does not find it -- which reads as "unavailable" on a
        // machine where it is installed and working.
        let sync_validation_available = want_validation
            && layer_extension_available(&entry, VALIDATION_LAYER, "VK_EXT_validation_features");
        // Synchronization only. Best-practices checking is worth running by
        // hand and was -- it is what found the barrier scopes -- but it is not
        // left on: much of its advice is vendor-specific, so a suite that
        // failed on it would fail on somebody else's GPU for reasons that are
        // not defects.
        let sync_validation = [vk::ValidationFeatureEnableEXT::SYNCHRONIZATION_VALIDATION];
        let mut validation_features =
            vk::ValidationFeaturesEXT::default().enabled_validation_features(&sync_validation);

        let mut create_info = vk::InstanceCreateInfo::default()
            .application_info(&app_info)
            .enabled_layer_names(&layer_ptrs)
            .enabled_extension_names(&ext_ptrs);
        if sync_validation_available {
            create_info = create_info.push_next(&mut validation_features);
        }
        let instance = unsafe { entry.create_instance(&create_info, None) }
            .map_err(|e| backend_err("create_instance", e))?;

        let validation_log = Arc::new(ValidationLog::default());
        let debug_messenger = if want_validation {
            let loader = ash::ext::debug_utils::Instance::new(&entry, &instance);
            let info = validation::messenger_create_info()
                .user_data(Arc::as_ptr(&validation_log) as *mut std::ffi::c_void);
            match unsafe { loader.create_debug_utils_messenger(&info, None) } {
                Ok(m) => Some((loader, m)),
                Err(e) => {
                    unsafe { instance.destroy_instance(None) };
                    return Err(backend_err("create_debug_utils_messenger", e));
                }
            }
        } else {
            None
        };

        let physical_device = match select_physical_device(&instance, preference) {
            Ok(pd) => pd,
            Err(e) => {
                unsafe { instance.destroy_instance(None) };
                return Err(e);
            }
        };

        Self::finish(
            entry,
            instance,
            physical_device,
            debug_messenger,
            validation_log,
            sync_validation_available,
        )
    }

    #[allow(clippy::too_many_arguments)]
    fn finish(
        entry: ash::Entry,
        instance: ash::Instance,
        physical_device: vk::PhysicalDevice,
        debug_messenger: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
        validation_log: Arc<ValidationLog>,
        sync_validation: bool,
    ) -> Result<Self> {
        let available = match device_extensions(&instance, physical_device) {
            Ok(set) => set,
            Err(e) => {
                unsafe { instance.destroy_instance(None) };
                return Err(e);
            }
        };

        // Request every extension the device offers that anything here can use.
        // Enabling one that goes unused costs nothing; discovering later that it
        // was not enabled costs a device recreation.
        let wanted = [
            ext::EXTERNAL_MEMORY_DMA_BUF,
            ext::EXTERNAL_MEMORY_FD,
            ext::IMAGE_DRM_FORMAT_MODIFIER,
            ext::EXTERNAL_FENCE_FD,
            ext::EXTERNAL_SEMAPHORE_FD,
            ext::PHYSICAL_DEVICE_DRM,
            ext::BLEND_OPERATION_ADVANCED,
            ext::SWAPCHAIN,
        ];
        let enabled = resolve_extensions(&wanted, &available);
        let advanced_blend = probe_advanced_blend(&instance, physical_device, &enabled);

        let queue_family_index = match select_queue_family(&instance, physical_device) {
            Ok(i) => i,
            Err(e) => {
                unsafe { instance.destroy_instance(None) };
                return Err(e);
            }
        };

        let enabled_cstrings: Vec<CString> = enabled
            .iter()
            .map(|s| CString::new(s.as_str()).expect("no interior NUL in an extension name"))
            .collect();
        let enabled_ptrs: Vec<*const c_char> =
            enabled_cstrings.iter().map(|s| s.as_ptr()).collect();

        let priorities = [1.0f32];
        let queue_info = vk::DeviceQueueCreateInfo::default()
            .queue_family_index(queue_family_index)
            .queue_priorities(&priorities);
        let queue_infos = [queue_info];
        let mut device_info = vk::DeviceCreateInfo::default()
            .queue_create_infos(&queue_infos)
            .enabled_extension_names(&enabled_ptrs);

        // Coherent advanced blending has to be asked for at device creation.
        // Without it, overlapping draws in one pass need an explicit barrier
        // between them; the pipeline below refuses the modes outright unless
        // this came back enabled, so there is no path where a batch silently
        // reads a destination another draw has not finished writing.
        let mut advanced_features = vk::PhysicalDeviceBlendOperationAdvancedFeaturesEXT::default()
            .advanced_blend_coherent_operations(true);
        if advanced_blend {
            device_info = device_info.push_next(&mut advanced_features);
        }

        let device = match unsafe { instance.create_device(physical_device, &device_info, None) } {
            Ok(d) => d,
            Err(e) => {
                unsafe { instance.destroy_instance(None) };
                return Err(backend_err("create_device", e));
            }
        };

        let queue = unsafe { device.get_device_queue(queue_family_index, 0) };
        let capabilities =
            detect_capabilities(&instance, physical_device, &enabled, advanced_blend);

        let allocator =
            gpu_allocator::vulkan::Allocator::new(&gpu_allocator::vulkan::AllocatorCreateDesc {
                instance: instance.clone(),
                device: device.clone(),
                physical_device,
                debug_settings: Default::default(),
                buffer_device_address: false,
                allocation_sizes: Default::default(),
            })
            .map_err(|e| Error::Backend {
                backend: "vulkan",
                detail: format!("allocator: {e}"),
            })?;

        let pool_info = vk::CommandPoolCreateInfo::default()
            .queue_family_index(queue_family_index)
            .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
        let command_pool = unsafe { device.create_command_pool(&pool_info, None) }
            .map_err(|e| backend_err("create_command_pool", e))?;

        let uniform_alignment = unsafe { instance.get_physical_device_properties(physical_device) }
            .limits
            .min_uniform_buffer_offset_alignment;

        Ok(Self {
            debug_messenger,
            validation_log,
            sync_validation,
            allocator: Some(allocator),
            pipelines: PipelineCache::default(),
            command_pool,
            device,
            physical_device,
            queue,
            queue_family_index,
            capabilities,
            descriptor_layout: None,
            material_layout: None,
            runtime_programs: Vec::new(),
            uniform_alignment,
            sampler: None,
            placeholder: None,
            placeholder_binding: None,
            enabled_extensions: enabled,
            instance,
            _entry: entry,
        })
    }

    pub fn capabilities(&self) -> &Capabilities {
        &self.capabilities
    }

    /// The descriptor set layout every pipeline is built against.
    /// The layout for the set carrying a draw's paint.
    ///
    /// Lazy like the sampling layout beside it, and for the same reason: a
    /// context that never records a batch should build nothing.
    /// Register a caller's fragment program and return the name for it.
    ///
    /// Nothing is built here. A pipeline needs a render pass, a blend mode and
    /// a sample count that only a draw knows, so the payload is kept and the
    /// pipelines appear as the combinations actually used do.
    pub fn register_program(&mut self, program: &impeller_hal::RuntimeProgram) -> Result<u32> {
        if program.spirv.is_empty() {
            return Err(impeller_hal::Error::Unsupported(
                "a runtime program needs a SPIR-V fragment module for this backend",
            ));
        }
        // The same payload gives the same name back. See the trait's note:
        // a program leads to pipelines keyed by it, so a caller registering
        // one per frame would grow the cache without bound.
        if let Some(existing) = self
            .runtime_programs
            .iter()
            .position(|held| held.as_slice() == program.spirv.as_slice())
        {
            return Ok(existing as u32);
        }
        self.runtime_programs.push(program.spirv.clone());
        Ok((self.runtime_programs.len() - 1) as u32)
    }

    /// The payload a program was registered with.
    pub(crate) fn runtime_program(&self, id: u32) -> Result<&[u32]> {
        self.runtime_programs
            .get(id as usize)
            .map(Vec::as_slice)
            .ok_or(impeller_hal::Error::Unsupported(
                "a draw names a runtime program that was never registered",
            ))
    }

    pub(crate) fn material_layout(&mut self) -> Result<vk::DescriptorSetLayout> {
        if self.material_layout.is_none() {
            self.material_layout = Some(crate::materials::create_layout(&self.device)?);
        }
        Ok(self.material_layout.expect("just created"))
    }

    /// The smallest offset a dynamic uniform buffer binding may use.
    ///
    /// Read once at creation rather than per submission: it is a property of
    /// the device, and querying it inside the record path would mean a call
    /// into the loader for a number that cannot change.
    pub(crate) fn uniform_alignment(&self) -> u64 {
        self.uniform_alignment
    }

    pub(crate) fn descriptor_layout(&mut self) -> Result<vk::DescriptorSetLayout> {
        if self.descriptor_layout.is_none() {
            self.descriptor_layout = Some(crate::sampling::create_descriptor_layout(&self.device)?);
        }
        Ok(self.descriptor_layout.expect("just created"))
    }

    /// The one sampler every image draw uses.
    pub(crate) fn sampler(&mut self) -> Result<vk::Sampler> {
        if self.sampler.is_none() {
            self.sampler = Some(crate::sampling::create_sampler(&self.device)?);
        }
        Ok(self.sampler.expect("just created"))
    }

    /// A one-pixel opaque white texture, bound where a draw samples nothing.
    ///
    /// Left in a shader-readable layout for good: nothing writes to it after
    /// creation, so it needs no transition at any later point and cannot be
    /// caught in the wrong layout by a pass that happened to run first.
    ///
    /// White rather than transparent so that a bug binding it in place of a
    /// real texture shows up as a blank shape rather than as nothing at all.
    pub(crate) fn placeholder_texture(&mut self) -> Result<vk::Image> {
        if self.placeholder.is_none() {
            let mut texture = self.create_texture(&impeller_hal::TextureDescriptor::offscreen(
                impeller_hal::Extent2D::new(1, 1),
                impeller_hal::PixelFormat::Rgba8Unorm,
            ))?;
            self.write_texture(&mut texture, &[255, 255, 255, 255])?;

            let device = self.device.clone();
            let cmd = self.begin_one_shot()?;
            crate::resource::transition(
                &device,
                cmd,
                texture.raw_image(),
                texture.layout(),
                vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
            );
            if let Err(e) = self.submit_one_shot(cmd) {
                self.destroy_texture(texture);
                return Err(e);
            }
            texture.set_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
            self.placeholder = Some(texture);
        }
        // The image handle rather than the texture, so a caller holding it is
        // not also holding a borrow of the context it needs to keep using.
        Ok(self.placeholder.as_ref().expect("just created").raw_image())
    }

    /// The descriptor set bound where a draw samples nothing.
    /// The view of the one-pixel white texture, for a binding nothing fills.
    ///
    /// A layout declaring four images has to have four written whatever a draw
    /// reads, so the ones a program does not declare get this. It is the same
    /// view the placeholder set uses, made on the same first call.
    pub(crate) fn placeholder_view(&mut self) -> Result<vk::ImageView> {
        self.placeholder_set()?;
        Ok(self
            .placeholder_binding
            .expect("placeholder_set just made it")
            .1)
    }

    pub(crate) fn placeholder_set(&mut self) -> Result<vk::DescriptorSet> {
        if let Some((_, _, set)) = self.placeholder_binding {
            return Ok(set);
        }
        let image = self.placeholder_texture()?;
        let layout = self.descriptor_layout()?;
        let sampler = self.sampler()?;
        let binding = crate::sampling::create_placeholder_binding(
            &self.device.clone(),
            image,
            layout,
            sampler,
        )?;
        self.placeholder_binding = Some(binding);
        Ok(binding.2)
    }

    /// Whether the validation layer is actually installed and reporting.
    ///
    /// Requesting validation on a machine without the layer yields false here
    /// rather than an error, so tests can skip instead of failing on a machine
    /// that simply lacks the SDK.
    /// Whether the layer was asked to check synchronization as well as calls.
    ///
    /// Core validation checks that each call is well formed. Synchronization
    /// validation checks that one access is ordered against the next, which is
    /// a separate feature the layer does not enable by default -- and it is the
    /// one that matters most to a renderer that synchronizes explicitly, since
    /// a missing barrier renders correctly on the device it was written on.
    ///
    /// A caller asserting a clean log wants to know this: without it the log
    /// being empty says only that every call was well formed.
    pub fn sync_validation_active(&self) -> bool {
        self.sync_validation
    }

    pub fn validation_active(&self) -> bool {
        self.debug_messenger.is_some()
    }

    /// Everything the layer has reported for this context so far.
    pub fn validation_messages(&self) -> Vec<ValidationMessage> {
        self.validation_log.messages()
    }

    /// Whether the layer has reported no errors.
    pub fn validation_clean(&self) -> bool {
        self.validation_log.is_clean()
    }

    /// A handle on the log that outlives this context.
    ///
    /// Every other accessor here reads the log *through* the context, which
    /// cannot see the last thing the layer has to say. A child object that
    /// outlives its device is reported at `vkDestroyDevice`, which happens
    /// inside this context's own drop, and by then there is nothing left to
    /// ask -- so a whole class of fault was structurally invisible to every
    /// test rather than merely untested. Holding this across the drop is what
    /// makes teardown assertable.
    ///
    /// The messenger is destroyed after the device, deliberately, so the
    /// callback is still installed while those reports are made.
    pub fn validation_log(&self) -> Arc<ValidationLog> {
        Arc::clone(&self.validation_log)
    }

    pub fn queue_family_index(&self) -> u32 {
        self.queue_family_index
    }

    /// Whether a device extension was enabled at device creation.
    pub fn has_extension(&self, name: &str) -> bool {
        self.enabled_extensions.contains(name)
    }

    pub fn raw_device(&self) -> &ash::Device {
        &self.device
    }

    /// The loader, for a presentation target that needs an instance extension
    /// this crate does not itself use.
    pub fn raw_entry(&self) -> &ash::Entry {
        &self._entry
    }

    pub fn raw_instance(&self) -> &ash::Instance {
        &self.instance
    }

    pub fn raw_physical_device(&self) -> vk::PhysicalDevice {
        self.physical_device
    }

    pub fn raw_queue(&self) -> vk::Queue {
        self.queue
    }

    pub(crate) fn pipeline_cache(&self) -> &PipelineCache {
        &self.pipelines
    }

    pub(crate) fn pipeline_cache_mut(&mut self) -> &mut PipelineCache {
        &mut self.pipelines
    }

    pub(crate) fn allocator_mut(&mut self) -> &mut gpu_allocator::vulkan::Allocator {
        self.allocator
            .as_mut()
            .expect("allocator is taken only in Drop")
    }

    /// Submit a recorded command buffer with an exportable fence, without
    /// waiting.
    pub(crate) fn submit_exportable(
        &self,
        cmd: vk::CommandBuffer,
        framebuffer: vk::Framebuffer,
        view: vk::ImageView,
        sync: FrameSync<'_>,
    ) -> Result<crate::fence::VulkanFence> {
        unsafe { self.device.end_command_buffer(cmd) }
            .map_err(|e| backend_err("end_command_buffer", e))?;

        // The fence is plain: it is what decides when this submission's
        // resources may be released, and exporting would reset it.
        let info = vk::FenceCreateInfo::default();
        let fence = unsafe { self.device.create_fence(&info, None) }
            .map_err(|e| backend_err("create_fence", e))?;

        // The semaphore is the exportable half. Signalled by the same
        // submission, so its payload represents exactly the same completion,
        // and resetting it on export costs nothing because nothing else waits
        // on it.
        let can_export = self.has_extension(ext::EXTERNAL_SEMAPHORE_FD);
        let semaphore = if can_export {
            let mut export_info = vk::ExportSemaphoreCreateInfo::default()
                .handle_types(vk::ExternalSemaphoreHandleTypeFlags::SYNC_FD);
            let info = vk::SemaphoreCreateInfo::default().push_next(&mut export_info);
            match unsafe { self.device.create_semaphore(&info, None) } {
                Ok(semaphore) => Some(semaphore),
                Err(e) => {
                    unsafe { self.device.destroy_fence(fence, None) };
                    return Err(backend_err("create_semaphore", e));
                }
            }
        } else {
            None
        };

        let cmds = [cmd];
        // The exportable semaphore and whatever the caller asked to signal are
        // both signalled by this one submission, so their payloads represent
        // exactly the same completion. A presentation target waits on its own;
        // a scanout path exports ours.
        let signals: Vec<vk::Semaphore> = semaphore
            .into_iter()
            .chain(sync.signal.iter().copied())
            .collect();
        // Color output is the only stage that touches the attachment, so
        // earlier stages may run before the wait is satisfied. Waiting at the
        // top of the pipe instead would serialize vertex work behind an image
        // the vertex stage never reads.
        let wait_stages: Vec<vk::PipelineStageFlags> =
            vec![vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT; sync.wait.len()];
        let submit = vk::SubmitInfo::default()
            .command_buffers(&cmds)
            .wait_semaphores(sync.wait)
            .wait_dst_stage_mask(&wait_stages)
            .signal_semaphores(&signals);
        if let Err(e) = unsafe { self.device.queue_submit(self.queue, &[submit], fence) } {
            unsafe {
                if let Some(semaphore) = semaphore {
                    self.device.destroy_semaphore(semaphore, None);
                }
                self.device.destroy_fence(fence, None);
            }
            return Err(backend_err("queue_submit", e));
        }

        let loader = can_export
            .then(|| ash::khr::external_semaphore_fd::Device::new(&self.instance, &self.device));
        Ok(crate::fence::VulkanFence::new(
            self.device.clone(),
            fence,
            self.command_pool,
            cmd,
            framebuffer,
            view,
            semaphore,
            loader,
        ))
    }

    /// Allocate and begin a command buffer for a single submission.
    pub(crate) fn begin_one_shot(&self) -> Result<vk::CommandBuffer> {
        let info = vk::CommandBufferAllocateInfo::default()
            .command_pool(self.command_pool)
            .level(vk::CommandBufferLevel::PRIMARY)
            .command_buffer_count(1);
        let buffers = unsafe { self.device.allocate_command_buffers(&info) }
            .map_err(|e| backend_err("allocate_command_buffers", e))?;
        let cmd = buffers[0];

        let begin = vk::CommandBufferBeginInfo::default()
            .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
        unsafe { self.device.begin_command_buffer(cmd, &begin) }
            .map_err(|e| backend_err("begin_command_buffer", e))?;
        Ok(cmd)
    }

    /// End, submit, and wait for a one-shot command buffer, then free it.
    ///
    /// Blocking here is correct for setup and readback and wrong for the frame
    /// loop, which is fence-driven and never waits on the device. Nothing in
    /// this path runs per frame.
    pub(crate) fn submit_one_shot(&self, cmd: vk::CommandBuffer) -> Result<()> {
        let result = self.submit_and_wait(cmd);
        // SAFETY: the submission was waited on above, or never made, so the
        // buffer is no longer in use either way.
        unsafe { self.device.free_command_buffers(self.command_pool, &[cmd]) };
        result
    }

    fn submit_and_wait(&self, cmd: vk::CommandBuffer) -> Result<()> {
        unsafe { self.device.end_command_buffer(cmd) }
            .map_err(|e| backend_err("end_command_buffer", e))?;

        let fence_info = vk::FenceCreateInfo::default();
        let fence = unsafe { self.device.create_fence(&fence_info, None) }
            .map_err(|e| backend_err("create_fence", e))?;

        let cmds = [cmd];
        let submit = vk::SubmitInfo::default().command_buffers(&cmds);
        let submitted = unsafe { self.device.queue_submit(self.queue, &[submit], fence) };

        let result = match submitted {
            Ok(()) => unsafe {
                self.device
                    .wait_for_fences(&[fence], true, u64::MAX)
                    .map_err(|e| match e {
                        vk::Result::TIMEOUT => Error::Timeout {
                            what: "a fence to signal",
                        },
                        vk::Result::ERROR_DEVICE_LOST => Error::DeviceLost,
                        other => backend_err("wait_for_fences", other),
                    })
            },
            Err(e) => Err(backend_err("queue_submit", e)),
        };

        unsafe { self.device.destroy_fence(fence, None) };
        result
    }
}

impl Drop for VulkanContext {
    fn drop(&mut self) {
        // The allocator must free its memory while the device is still alive,
        // so it is dropped explicitly before anything else is destroyed.
        self.pipelines.destroy(&self.device);
        // Before the allocator, since the placeholder holds an allocation.
        if let Some(texture) = self.placeholder.take() {
            self.destroy_texture(texture);
        }
        // SAFETY: nothing is in flight, and no descriptor set still refers to
        // either of these -- every other pool is destroyed with the submission
        // that made it.
        unsafe {
            if let Some((pool, view, _)) = self.placeholder_binding.take() {
                self.device.destroy_descriptor_pool(pool, None);
                self.device.destroy_image_view(view, None);
            }
            if let Some(sampler) = self.sampler.take() {
                self.device.destroy_sampler(sampler, None);
            }
            // Both layouts, and they are separate fields because they are
            // separate sets: the texture and sampler in one, the material's
            // uniform block in the other. This one was missing for as long as
            // the second set has existed, and the leak it left was one layout
            // per device -- invisible to every test, since a context is
            // destroyed at the end of a process that is about to exit anyway,
            // and reported by the validation layer at `vkDestroyDevice` as a
            // child object outliving its parent.
            for layout in [self.descriptor_layout.take(), self.material_layout.take()]
                .into_iter()
                .flatten()
            {
                self.device.destroy_descriptor_set_layout(layout, None);
            }
        }
        drop(self.allocator.take());
        // SAFETY: every submission this context made was waited on before the
        // call that made it returned, so nothing is in flight. Objects are
        // destroyed inside-out: pool, then device, then the instance that
        // created it.
        unsafe {
            self.device.destroy_command_pool(self.command_pool, None);
            self.device.destroy_device(None);
            // Before the instance, and before the log the callback points at.
            if let Some((loader, messenger)) = self.debug_messenger.take() {
                loader.destroy_debug_utils_messenger(messenger, None);
            }
            self.instance.destroy_instance(None);
        }
    }
}

impl std::fmt::Debug for VulkanContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("VulkanContext")
            .field("device", &self.capabilities.device_name)
            .field("driver", &self.capabilities.driver_name)
            .finish_non_exhaustive()
    }
}

fn layer_available(entry: &ash::Entry, name: &str) -> bool {
    let Ok(layers) = (unsafe { entry.enumerate_instance_layer_properties() }) else {
        return false;
    };
    layers.iter().any(|l| {
        // SAFETY: the loader guarantees a NUL-terminated name.
        unsafe { CStr::from_ptr(l.layer_name.as_ptr()) }
            .to_str()
            .map(|s| s == name)
            .unwrap_or(false)
    })
}

/// Whether a layer provides an instance extension.
///
/// Separate from the loader-wide query because a layer's own extensions are
/// only listed when that layer is named: asking the loader about one and
/// getting "no" says nothing about whether the layer has it.
fn layer_extension_available(entry: &ash::Entry, layer: &str, name: &str) -> bool {
    let Ok(layer_name) = CString::new(layer) else {
        return false;
    };
    let Ok(exts) = (unsafe { entry.enumerate_instance_extension_properties(Some(&layer_name)) })
    else {
        return false;
    };
    exts.iter().any(|e| {
        // SAFETY: the loader guarantees a NUL-terminated name.
        unsafe { CStr::from_ptr(e.extension_name.as_ptr()) }
            .to_str()
            .map(|s| s == name)
            .unwrap_or(false)
    })
}

fn instance_extension_available(entry: &ash::Entry, name: &str) -> bool {
    let Ok(exts) = (unsafe { entry.enumerate_instance_extension_properties(None) }) else {
        return false;
    };
    exts.iter().any(|e| {
        // SAFETY: the loader guarantees a NUL-terminated name.
        unsafe { CStr::from_ptr(e.extension_name.as_ptr()) }
            .to_str()
            .map(|s| s == name)
            .unwrap_or(false)
    })
}

fn backend_err(what: &str, e: vk::Result) -> Error {
    Error::Backend {
        backend: "vulkan",
        detail: format!("{what}: {e:?}"),
    }
}

fn device_extensions(instance: &ash::Instance, pd: vk::PhysicalDevice) -> Result<HashSet<String>> {
    let props = unsafe { instance.enumerate_device_extension_properties(pd) }
        .map_err(|e| backend_err("enumerate_device_extension_properties", e))?;
    Ok(props
        .iter()
        .filter_map(|p| {
            // SAFETY: the driver guarantees a NUL-terminated name.
            let raw = unsafe { CStr::from_ptr(p.extension_name.as_ptr()) };
            raw.to_str().ok().map(|s| s.to_owned())
        })
        .collect())
}

fn select_physical_device(
    instance: &ash::Instance,
    preference: DevicePreference,
) -> Result<vk::PhysicalDevice> {
    let devices = unsafe { instance.enumerate_physical_devices() }
        .map_err(|e| backend_err("enumerate_physical_devices", e))?;
    if devices.is_empty() {
        return Err(Error::Backend {
            backend: "vulkan",
            detail: "no physical devices".into(),
        });
    }

    match preference {
        DevicePreference::Index(i) => devices.get(i).copied().ok_or(Error::Backend {
            backend: "vulkan",
            detail: format!("device index {i} out of range ({} present)", devices.len()),
        }),
        DevicePreference::Auto => Ok(*devices
            .iter()
            .max_by_key(|pd| {
                let props = unsafe { instance.get_physical_device_properties(**pd) };
                match props.device_type {
                    vk::PhysicalDeviceType::DISCRETE_GPU => 3,
                    vk::PhysicalDeviceType::INTEGRATED_GPU => 2,
                    vk::PhysicalDeviceType::VIRTUAL_GPU => 1,
                    _ => 0,
                }
            })
            .expect("non-empty")),
        DevicePreference::Software => devices
            .iter()
            .find(|pd| {
                let props = unsafe { instance.get_physical_device_properties(**pd) };
                props.device_type == vk::PhysicalDeviceType::CPU
            })
            .copied()
            .ok_or(Error::Unsupported("no software rasterizer present")),
    }
}

fn select_queue_family(instance: &ash::Instance, pd: vk::PhysicalDevice) -> Result<u32> {
    let families = unsafe { instance.get_physical_device_queue_family_properties(pd) };
    families
        .iter()
        .position(|f| f.queue_flags.contains(vk::QueueFlags::GRAPHICS))
        .map(|i| i as u32)
        .ok_or(Error::Unsupported("no graphics queue family"))
}

fn detect_capabilities(
    instance: &ash::Instance,
    pd: vk::PhysicalDevice,
    enabled: &HashSet<String>,
    advanced_blend: bool,
) -> Capabilities {
    // Modifier queries need the extension enabled; without it the only honest
    // answer about layouts is that nothing is known.
    let modifiers_known = enabled.contains(ext::IMAGE_DRM_FORMAT_MODIFIER);
    let props = unsafe { instance.get_physical_device_properties(pd) };
    let limits = props.limits;

    // Sample counts a target can use are the intersection of what color and
    // depth attachments support. Reporting the color count alone would promise
    // a configuration that fails once depth is attached.
    let sample_mask =
        limits.framebuffer_color_sample_counts & limits.framebuffer_depth_sample_counts;

    // dma-buf export needs both the fd machinery and the dma-buf handle type;
    // either alone is useless. Negotiating a scanout-capable layout
    // additionally needs explicit modifiers.
    let fd = enabled.contains(ext::EXTERNAL_MEMORY_FD);
    let dma_buf = enabled.contains(ext::EXTERNAL_MEMORY_DMA_BUF);
    let modifiers = enabled.contains(ext::IMAGE_DRM_FORMAT_MODIFIER);

    // Every one of the four matters and the set is not arbitrary: a layer is
    // drawn into and composited, so it needs to be an attachment and to blend
    // as one; and it is then sampled, with a linear filter, by the pass above
    // it. A device offering three of the four cannot carry a floating-point
    // layer through the pipeline this renderer has.
    let wanted = vk::FormatFeatureFlags::COLOR_ATTACHMENT
        | vk::FormatFeatureFlags::COLOR_ATTACHMENT_BLEND
        | vk::FormatFeatureFlags::SAMPLED_IMAGE
        | vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR;
    let float_render_targets = unsafe {
        instance
            .get_physical_device_format_properties(pd, vk::Format::R16G16B16A16_SFLOAT)
            .optimal_tiling_features
            .contains(wanted)
    };

    Capabilities {
        advanced_blend,
        float_render_targets,
        max_texture_size: limits.max_image_dimension2_d,
        sample_counts: SampleCounts::from_mask(sample_mask.as_raw()),
        dma_buf: DmaBufSupport {
            import: fd && dma_buf,
            export: fd && dma_buf,
            modifiers,
        },
        sync: SyncSupport {
            // Export comes from a semaphore, not a fence: exporting a SYNC_FD
            // resets what it came from, and a reset fence can never be waited
            // on for retirement. So this reports the extension actually used.
            export_sync_file: enabled.contains(ext::EXTERNAL_SEMAPHORE_FD),
            // Import is the other direction — taking an out-fence from a
            // display commit and waiting on it — which is a fence operation.
            import_sync_file: enabled.contains(ext::EXTERNAL_FENCE_FD),
        },
        render_formats: if modifiers_known {
            crate::external::render_formats(instance, pd)
        } else {
            Vec::new()
        },
        device_name: device_name(&props),
        driver_name: format!("vulkan {}", api_version_string(props.api_version)),
        software: props.device_type == vk::PhysicalDeviceType::CPU,
    }
}

fn device_name(props: &vk::PhysicalDeviceProperties) -> String {
    // SAFETY: the driver guarantees a NUL-terminated name.
    unsafe { CStr::from_ptr(props.device_name.as_ptr()) }
        .to_string_lossy()
        .into_owned()
}

fn api_version_string(v: u32) -> String {
    format!(
        "{}.{}.{}",
        vk::api_version_major(v),
        vk::api_version_minor(v),
        vk::api_version_patch(v)
    )
}