vulkane 0.10.0

Vulkan API bindings generated entirely from vk.xml, with a complete safe RAII wrapper covering compute and graphics: instance/device/queue, buffer, image, sampler, render pass, framebuffer, graphics + compute pipelines, swapchain, a VMA-style sub-allocator with TLSF + linear pools and defragmentation, sync primitives (fences, binary + timeline semaphores, sync2 barriers), query pools, and optional GLSL/WGSL/HLSL→SPIR-V compilation via naga or shaderc. Supports Vulkan 1.2.175 onward — swap vk.xml and rebuild.
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
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
//! Safe wrapper for `VkPhysicalDevice` — a GPU discovered by the
//! Vulkan loader.
//!
//! A [`PhysicalDevice`] represents a piece of hardware (or software
//! rasterizer) that can run Vulkan commands. Use it to:
//!
//! - Query properties: [`properties()`](PhysicalDevice::properties),
//!   [`memory_properties()`](PhysicalDevice::memory_properties)
//! - Find queue families:
//!   [`find_queue_family(QueueFlags::GRAPHICS)`](PhysicalDevice::find_queue_family)
//! - Find memory types:
//!   [`find_memory_type(bits, flags)`](PhysicalDevice::find_memory_type)
//! - Create a logical device:
//!   [`create_device(info)`](PhysicalDevice::create_device)
//!
//! ```ignore
//! let physical = instance
//!     .enumerate_physical_devices()?
//!     .into_iter()
//!     .find(|pd| pd.find_queue_family(QueueFlags::GRAPHICS).is_some())
//!     .ok_or("no compatible GPU")?;
//!
//! println!("Using: {}", physical.properties().device_name());
//! ```

use super::instance::{ApiVersion, ExtensionProperties, InstanceInner};
use super::{Device, DeviceCreateInfo, Error, Result, check};
use crate::raw::PNextChainable;
use crate::raw::bindings::*;
use std::ffi::CStr;
use std::sync::Arc;

/// A handle to a Vulkan physical device (a GPU or other implementation).
///
/// Physical devices are not destroyed; they are owned by the instance.
/// This handle is alive as long as its parent [`Instance`](super::Instance) is alive.
#[derive(Clone)]
pub struct PhysicalDevice {
    pub(crate) instance: Arc<InstanceInner>,
    pub(crate) handle: VkPhysicalDevice,
}

// Safety: VkPhysicalDevice is documented by the Vulkan spec as safe to
// share between threads. The InstanceInner is already Send + Sync.
unsafe impl Send for PhysicalDevice {}
unsafe impl Sync for PhysicalDevice {}

impl PhysicalDevice {
    pub(crate) fn new(instance: Arc<InstanceInner>, handle: VkPhysicalDevice) -> Self {
        Self { instance, handle }
    }

    /// Returns the raw `VkPhysicalDevice` handle.
    pub fn raw(&self) -> VkPhysicalDevice {
        self.handle
    }

    /// Returns a reference to the parent instance's dispatch table.
    /// Used by [`Allocator`](super::Allocator) to look up
    /// `vkGetPhysicalDeviceMemoryProperties`. Hidden from rustdoc — not
    /// part of the stable public API.
    #[doc(hidden)]
    pub fn instance(&self) -> &VkInstanceDispatchTable {
        &self.instance.dispatch
    }

    /// Query the physical device's supported Vulkan 1.0 feature bits.
    /// Combine with the [`DeviceFeatures`](super::DeviceFeatures) builder
    /// when enabling all device-supported features.
    pub fn supported_features(&self) -> VkPhysicalDeviceFeatures {
        let get = self
            .instance
            .dispatch
            .vkGetPhysicalDeviceFeatures
            .expect("vkGetPhysicalDeviceFeatures is required by Vulkan 1.0");
        // Safety: handle is valid; struct will be fully overwritten.
        let mut feats: VkPhysicalDeviceFeatures = unsafe { std::mem::zeroed() };
        unsafe { get(self.handle, &mut feats) };
        feats
    }

    /// Query the physical device's properties (name, vendor, API version, etc.).
    pub fn properties(&self) -> PhysicalDeviceProperties {
        let get = self
            .instance
            .dispatch
            .vkGetPhysicalDeviceProperties
            .expect("vkGetPhysicalDeviceProperties is required by Vulkan 1.0");

        // Safety: handle is valid (came from vkEnumeratePhysicalDevices),
        // raw is freshly-zeroed but Vulkan will overwrite all fields.
        let mut raw: VkPhysicalDeviceProperties = unsafe { std::mem::zeroed() };
        unsafe { get(self.handle, &mut raw) };
        PhysicalDeviceProperties { raw }
    }

    /// Query the physical device's queue family properties.
    pub fn queue_family_properties(&self) -> Vec<QueueFamilyProperties> {
        let get = self
            .instance
            .dispatch
            .vkGetPhysicalDeviceQueueFamilyProperties
            .expect("vkGetPhysicalDeviceQueueFamilyProperties is required by Vulkan 1.0");

        let mut count: u32 = 0;
        // Safety: count query, output ptr is null.
        unsafe { get(self.handle, &mut count, std::ptr::null_mut()) };

        // Safety: each element will be overwritten by the driver.
        let mut raw: Vec<VkQueueFamilyProperties> =
            vec![unsafe { std::mem::zeroed() }; count as usize];
        // Safety: raw has space for `count` elements.
        unsafe { get(self.handle, &mut count, raw.as_mut_ptr()) };

        raw.into_iter()
            .map(|r| QueueFamilyProperties { raw: r })
            .collect()
    }

    /// Query the physical device's memory properties (heaps and types).
    pub fn memory_properties(&self) -> MemoryProperties {
        let get = self
            .instance
            .dispatch
            .vkGetPhysicalDeviceMemoryProperties
            .expect("vkGetPhysicalDeviceMemoryProperties is required by Vulkan 1.0");

        // Safety: driver will overwrite all relevant fields.
        let mut raw: VkPhysicalDeviceMemoryProperties = unsafe { std::mem::zeroed() };
        unsafe { get(self.handle, &mut raw) };
        MemoryProperties { raw }
    }

    /// Create a logical [`Device`] from this physical device.
    pub fn create_device(&self, info: DeviceCreateInfo<'_>) -> Result<Device> {
        Device::new(self, info)
    }

    /// Enumerate the device-level extensions exposed by this physical device.
    pub fn enumerate_extension_properties(&self) -> Result<Vec<ExtensionProperties>> {
        let enumerate = self
            .instance
            .dispatch
            .vkEnumerateDeviceExtensionProperties
            .ok_or(Error::MissingFunction(
                "vkEnumerateDeviceExtensionProperties",
            ))?;

        let mut count: u32 = 0;
        // Safety: count query, output ptr is null. Layer name null = core extensions.
        check(unsafe {
            enumerate(
                self.handle,
                std::ptr::null(),
                &mut count,
                std::ptr::null_mut(),
            )
        })?;
        let mut raw: Vec<VkExtensionProperties> =
            vec![unsafe { std::mem::zeroed() }; count as usize];
        // Safety: raw has space for `count` elements.
        check(unsafe { enumerate(self.handle, std::ptr::null(), &mut count, raw.as_mut_ptr()) })?;
        Ok(raw.into_iter().map(ExtensionProperties::from_raw).collect())
    }

    /// Find the index of the first queue family that supports the given flags.
    pub fn find_queue_family(&self, required: QueueFlags) -> Option<u32> {
        self.queue_family_properties()
            .iter()
            .enumerate()
            .find(|(_, qf)| qf.queue_flags().contains(required))
            .map(|(i, _)| i as u32)
    }

    /// Find a "dedicated" compute queue family — one that supports
    /// `COMPUTE` but **not** `GRAPHICS`. On modern NVIDIA / AMD GPUs this
    /// returns the async-compute queue family, which can run compute work
    /// concurrently with the universal graphics+compute queue.
    ///
    /// If no dedicated compute family exists (most integrated GPUs and
    /// software rasterizers fall in this bucket), this falls back to the
    /// first family that supports `COMPUTE` at all — i.e. the same answer
    /// as `find_queue_family(QueueFlags::COMPUTE)`. Returns `None` only when
    /// the device exposes no compute-capable queues, which should not
    /// happen on any conformant Vulkan implementation.
    pub fn find_dedicated_compute_queue(&self) -> Option<u32> {
        let families = self.queue_family_properties();
        // Prefer compute-without-graphics.
        for (i, qf) in families.iter().enumerate() {
            let flags = qf.queue_flags();
            if flags.contains(QueueFlags::COMPUTE) && !flags.contains(QueueFlags::GRAPHICS) {
                return Some(i as u32);
            }
        }
        // Fallback: any compute queue.
        for (i, qf) in families.iter().enumerate() {
            if qf.queue_flags().contains(QueueFlags::COMPUTE) {
                return Some(i as u32);
            }
        }
        None
    }

    /// Find a "dedicated" transfer queue family — one that supports
    /// `TRANSFER` but **not** `GRAPHICS` or `COMPUTE`. On discrete GPUs
    /// this is typically the DMA / copy engine and is the right place to
    /// run staging-buffer uploads concurrently with compute work.
    ///
    /// Falls back to `find_queue_family(QueueFlags::TRANSFER)` (which the
    /// Vulkan spec guarantees succeeds for any graphics-or-compute family).
    pub fn find_dedicated_transfer_queue(&self) -> Option<u32> {
        let families = self.queue_family_properties();
        for (i, qf) in families.iter().enumerate() {
            let flags = qf.queue_flags();
            if flags.contains(QueueFlags::TRANSFER)
                && !flags.contains(QueueFlags::GRAPHICS)
                && !flags.contains(QueueFlags::COMPUTE)
            {
                return Some(i as u32);
            }
        }
        for (i, qf) in families.iter().enumerate() {
            if qf.queue_flags().contains(QueueFlags::TRANSFER) {
                return Some(i as u32);
            }
        }
        None
    }

    /// Enumerate the supported cooperative matrix shapes (`VK_KHR_cooperative_matrix`).
    ///
    /// Cooperative matrices are GPU primitives for matrix-multiply-and-
    /// accumulate operations — the building block of modern ML and
    /// signal-processing workloads. Each [`CooperativeMatrixProperties`]
    /// entry describes one supported `(M, N, K, A_type, B_type, C_type,
    /// Result_type)` shape that the device's compute units can execute
    /// natively.
    ///
    /// Returns an empty `Vec` if the device does not advertise
    /// `VK_KHR_cooperative_matrix`.
    ///
    /// # Why this is safe to call unconditionally
    ///
    /// The Vulkan loader will hand back a non-null function pointer for
    /// any KHR entry point it knows the *name* of, whether or not the
    /// device implements it; calling such a stub against a device that
    /// doesn't implement the extension can crash (notably on software
    /// rasterizers like Lavapipe). A non-null dispatch entry is therefore
    /// not evidence the call is legal.
    ///
    /// This method does not rely on one. It first asks the device itself
    /// — [`enumerate_extension_properties`](Self::enumerate_extension_properties)
    /// — whether it advertises `VK_KHR_cooperative_matrix`, and returns
    /// an empty `Vec` when it does not, so the loader stub is never
    /// reached. That is the same honest-gating discipline
    /// [`device_identity`](Self::device_identity) applies to
    /// `VK_EXT_pci_bus_info`, and it makes the extension check an
    /// invariant of the call rather than an obligation on the caller.
    ///
    /// An empty `Vec` is thus unambiguous: this device has no
    /// cooperative-matrix support to report. Callers that previously
    /// wrapped this in `unsafe { .. }` after checking the extension list
    /// themselves can delete both the check and the block.
    pub fn cooperative_matrix_properties(&self) -> Vec<CooperativeMatrixProperties> {
        // Gate on the device's own advertisement, not on the loader
        // handing us a function pointer — see the doc comment above.
        let advertised = self
            .enumerate_extension_properties()
            .map(|exts| exts.iter().any(|e| e.name() == "VK_KHR_cooperative_matrix"))
            .unwrap_or(false);
        if !advertised {
            return Vec::new();
        }

        let Some(get) = self
            .instance
            .dispatch
            .vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR
        else {
            return Vec::new();
        };
        let mut count: u32 = 0;
        // Safety: count query, output ptr is null.
        if unsafe { get(self.handle, &mut count, std::ptr::null_mut()) } != VkResult::SUCCESS {
            return Vec::new();
        }
        // Note: cannot use `mem::zeroed()` here because `VkScopeKHR` has
        // no zero variant and the generated `Default` produces
        // `SCOPE_DEVICE_KHR`. Initialize via the per-struct Default impl
        // and patch sType in one shot.
        let mut raw: Vec<VkCooperativeMatrixPropertiesKHR> = (0..count as usize)
            .map(|_| VkCooperativeMatrixPropertiesKHR {
                sType: VkStructureType::STRUCTURE_TYPE_COOPERATIVE_MATRIX_PROPERTIES_KHR,
                ..Default::default()
            })
            .collect();
        // Safety: raw has space for `count` elements.
        if unsafe { get(self.handle, &mut count, raw.as_mut_ptr()) } != VkResult::SUCCESS {
            return Vec::new();
        }
        raw.into_iter()
            .map(|r| CooperativeMatrixProperties { raw: r })
            .collect()
    }

    /// Query per-heap memory budget via `VK_EXT_memory_budget`.
    ///
    /// `VK_EXT_memory_budget` lets the driver report a soft per-heap
    /// "budget" the application should respect — exceeding it isn't an
    /// error, but the driver may start swapping or evicting if it's
    /// repeatedly violated. The reported `usage` is the driver's estimate
    /// of how many bytes are currently allocated from each heap.
    ///
    /// Returns `None` if `vkGetPhysicalDeviceMemoryProperties2` is not
    /// loaded (Vulkan 1.0 without `VK_KHR_get_physical_device_properties2`)
    /// — the call always returns *something* useful when the loader has
    /// `vkGetPhysicalDeviceMemoryProperties2` available, but the budget
    /// numbers will only be meaningful when `VK_EXT_memory_budget` is
    /// enabled at instance creation time.
    pub fn memory_budget(&self) -> Option<MemoryBudget> {
        let get2 = self
            .instance
            .dispatch
            .vkGetPhysicalDeviceMemoryProperties2?;

        // Output-direction chain: driver writes into both structs.
        let mut budget_chain = crate::safe::PNextChain::new();
        budget_chain.push(VkPhysicalDeviceMemoryBudgetPropertiesEXT::new_pnext());
        let mut props2 = VkPhysicalDeviceMemoryProperties2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2,
            pNext: budget_chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle is valid; props2 and the chain both live for
        // the call's duration.
        unsafe { get2(self.handle, &mut props2) };

        let budget = budget_chain.get::<VkPhysicalDeviceMemoryBudgetPropertiesEXT>()?;
        Some(MemoryBudget {
            heap_count: props2.memoryProperties.memoryHeapCount,
            budget: budget.heapBudget,
            usage: budget.heapUsage,
        })
    }

    /// Query this device's stable identity — UUIDs, the LUID (where the
    /// platform marks it valid), and the PCI bus address (where the
    /// device advertises `VK_EXT_pci_bus_info`).
    ///
    /// This is the **join key** for correlating a `VkPhysicalDevice` with
    /// the same GPU as seen by out-of-band sources and other APIs:
    /// `device_uuid` matches NVML / CUDA / OpenGL (`nvmlDeviceGetUUID`);
    /// `device_luid` matches a DXGI adapter or D3DKMT node on Windows;
    /// `pci` matches a Linux `/sys/bus/pci/devices/...` node (and thus
    /// amdgpu `gpu_busy_percent`). Vulkan itself exposes **no GPU
    /// load / utilization / queue-depth query** beyond the VRAM
    /// [`memory_budget`](Self::memory_budget) — identity is the hook that
    /// lets a caller go ask the right vendor/OS source out of band.
    ///
    /// Returns `None` only when `vkGetPhysicalDeviceProperties2` is
    /// unavailable (Vulkan 1.0 with no
    /// `VK_KHR_get_physical_device_properties2`). Otherwise the UUID
    /// fields are always populated; [`device_luid`](DeviceIdentity::device_luid)
    /// is `Some` only when the platform reports it valid (Windows), and
    /// [`pci`](DeviceIdentity::pci) is `Some` only when the device
    /// advertises `VK_EXT_pci_bus_info`.
    pub fn device_identity(&self) -> Option<DeviceIdentity> {
        let get2 = self.instance.dispatch.vkGetPhysicalDeviceProperties2?;

        // Only chain the PCI-bus-info struct when the device actually
        // advertises the extension. A driver that doesn't implement it
        // leaves the struct untouched, so chaining it unconditionally
        // would report a bogus `0000:00:00.0` instead of an honest
        // `None`.
        let has_pci = self
            .enumerate_extension_properties()
            .map(|exts| exts.iter().any(|e| e.name() == "VK_EXT_pci_bus_info"))
            .unwrap_or(false);

        let mut chain = crate::safe::PNextChain::new();
        chain.push(VkPhysicalDeviceIDProperties::new_pnext());
        if has_pci {
            chain.push(VkPhysicalDevicePCIBusInfoPropertiesEXT::new_pnext());
        }
        let mut props2 = VkPhysicalDeviceProperties2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
            pNext: chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle valid; props2 + chain live for the call.
        unsafe { get2(self.handle, &mut props2) };

        let id = chain.get::<VkPhysicalDeviceIDProperties>()?;
        let pci = if has_pci {
            chain
                .get::<VkPhysicalDevicePCIBusInfoPropertiesEXT>()
                .map(|p| PciBusInfo {
                    domain: p.pciDomain,
                    bus: p.pciBus,
                    device: p.pciDevice,
                    function: p.pciFunction,
                })
        } else {
            None
        };

        Some(DeviceIdentity {
            device_uuid: id.deviceUUID,
            driver_uuid: id.driverUUID,
            device_luid: (id.deviceLUIDValid != 0).then_some(id.deviceLUID),
            device_node_mask: id.deviceNodeMask,
            pci,
        })
    }

    /// The Vulkan version whose features are actually reachable through
    /// this handle: `min(instance apiVersion, device apiVersion)`.
    ///
    /// This is the version that governs property queries, and it is
    /// **not** the same as
    /// [`properties().api_version()`](PhysicalDeviceProperties::api_version).
    /// A Vulkan implementation must behave as the version the *instance*
    /// requested in `VkApplicationInfo::apiVersion`, so an instance
    /// created at 1.0 will leave 1.1+ `pNext` property structs untouched
    /// even on a device that reports 1.3 — the caller reads back a
    /// zeroed struct that looks like a real answer.
    ///
    /// Query methods that chain version-gated structs
    /// ([`subgroup_properties`](Self::subgroup_properties),
    /// [`driver_properties`](Self::driver_properties)) gate on this, so
    /// they return an honest `None` rather than a zeroed reading. If one
    /// of them declines on hardware you expect to support the feature,
    /// raise the `api_version` in
    /// [`InstanceCreateInfo`](super::InstanceCreateInfo) — which defaults
    /// to [`ApiVersion::V1_0`](super::ApiVersion::V1_0).
    pub fn effective_api_version(&self) -> ApiVersion {
        let device = self.properties().api_version();
        let instance = self.instance.api_version;
        if (instance.major(), instance.minor()) <= (device.major(), device.minor()) {
            instance
        } else {
            device
        }
    }

    /// Query the device's subgroup ("wave" / "warp") properties —
    /// `VkPhysicalDeviceSubgroupProperties` (Vulkan 1.1 core), plus the
    /// `VkPhysicalDeviceSubgroupSizeControlProperties` size range
    /// (Vulkan 1.3 core, or `VK_EXT_subgroup_size_control`) when the
    /// device exposes it.
    ///
    /// Subgroup width is the single most important specialization axis
    /// for a compute kernel: it is 32 on NVIDIA, 64 on AMD GCN/CDNA
    /// (32 or 64 on RDNA), and 8/16/32 on Intel. A kernel that reduces
    /// across a subgroup either reads the width at runtime
    /// (`gl_SubgroupSize` / `WaveGetLaneCount()`) or is compiled for a
    /// fixed width and pinned with
    /// [`ComputePipelineOptions::required_subgroup_size`](super::ComputePipelineOptions::required_subgroup_size).
    /// This method is what makes the second option usable: pinning a
    /// size is only legal within the
    /// [`size_control`](SubgroupProperties::size_control) range, and
    /// until now Vulkane let you set that field without offering any way
    /// to read the bounds it must satisfy.
    ///
    /// Returns `None` when the
    /// [`effective_api_version`](Self::effective_api_version) is below
    /// 1.1 (`VkPhysicalDeviceSubgroupProperties` is 1.1 core and has no
    /// extension form, so a 1.0 implementation leaves the struct
    /// untouched and reporting `subgroup_size: 0` would be a lie), or
    /// when `vkGetPhysicalDeviceProperties2` is unavailable.
    /// [`size_control`](SubgroupProperties::size_control) is `Some` only
    /// when the device actually advertises it.
    ///
    /// Note the gate is on the **effective** version, so this declines on
    /// a 1.0-created [`Instance`](super::Instance) even against a 1.3
    /// device. [`InstanceCreateInfo::api_version`](super::InstanceCreateInfo::api_version)
    /// defaults to 1.0, so raise it if this returns `None` unexpectedly.
    pub fn subgroup_properties(&self) -> Option<SubgroupProperties> {
        let get2 = self.instance.dispatch.vkGetPhysicalDeviceProperties2?;

        // VkPhysicalDeviceSubgroupProperties is Vulkan 1.1 core with no
        // extension form. A 1.0 implementation ignores the unrecognized
        // pNext struct and leaves it zeroed, which would read back as a
        // subgroup size of 0 — decline honestly instead.
        let api = self.effective_api_version();
        if api.major() < 1 || (api.major() == 1 && api.minor() < 1) {
            return None;
        }

        // Size control is 1.3 core, or the EXT before that. Chain it only
        // when one of those holds, so an untouched struct can't be
        // mistaken for a real min/max of 0.
        let has_size_control = (api.major() > 1 || api.minor() >= 3)
            || self
                .enumerate_extension_properties()
                .map(|exts| {
                    exts.iter()
                        .any(|e| e.name() == "VK_EXT_subgroup_size_control")
                })
                .unwrap_or(false);

        let mut chain = crate::safe::PNextChain::new();
        chain.push(VkPhysicalDeviceSubgroupProperties::new_pnext());
        if has_size_control {
            chain.push(VkPhysicalDeviceSubgroupSizeControlProperties::new_pnext());
        }
        let mut props2 = VkPhysicalDeviceProperties2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
            pNext: chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle valid; props2 + chain live for the call.
        unsafe { get2(self.handle, &mut props2) };

        let sg = chain.get::<VkPhysicalDeviceSubgroupProperties>()?;
        let size_control = if has_size_control {
            chain
                .get::<VkPhysicalDeviceSubgroupSizeControlProperties>()
                .map(|sc| SubgroupSizeControl {
                    min_subgroup_size: sc.minSubgroupSize,
                    max_subgroup_size: sc.maxSubgroupSize,
                    max_compute_workgroup_subgroups: sc.maxComputeWorkgroupSubgroups,
                    required_subgroup_size_stages: super::ShaderStageFlags(
                        sc.requiredSubgroupSizeStages,
                    ),
                })
        } else {
            None
        };

        Some(SubgroupProperties {
            subgroup_size: sg.subgroupSize,
            supported_stages: super::ShaderStageFlags(sg.supportedStages),
            supported_operations: SubgroupFeatureFlags(sg.supportedOperations),
            quad_operations_in_all_stages: sg.quadOperationsInAllStages != 0,
            size_control,
        })
    }

    /// Query the driver's identity — `VkPhysicalDeviceDriverProperties`
    /// (Vulkan 1.2 core, or `VK_KHR_driver_properties`).
    ///
    /// [`PhysicalDeviceProperties::driver_version`](PhysicalDeviceProperties::driver_version)
    /// is a bare `u32` whose bit-packing is **vendor-defined** — NVIDIA
    /// packs it (22,14,6,10), AMD (22,10,10,10), Intel-on-Windows
    /// (18,14) — so it cannot be decoded portably and is only good for
    /// equality comparison. This method returns what a caller actually
    /// wants instead: a [`driver_id`](DriverProperties::driver_id)
    /// enum naming the ICD (`MESA_RADV` vs `AMD_PROPRIETARY` vs
    /// `NVIDIA_PROPRIETARY` vs `MESA_LLVMPIPE` …), a human-readable
    /// driver name and version string, and the Vulkan CTS
    /// [`conformance_version`](DriverProperties::conformance_version)
    /// the driver claims to pass.
    ///
    /// Two things this is for: **stable cache keys** — a
    /// `(driver_id, driver_info)` pair is a portable, human-legible
    /// identity for "the compiler that produced this SPIR-V binary,"
    /// which a raw vendor-packed `u32` is not — and **quirk gating**,
    /// since ICDs for the same hardware genuinely differ (RADV and
    /// AMDVLK do not make the same codegen choices).
    ///
    /// Returns `None` when `vkGetPhysicalDeviceProperties2` is
    /// unavailable, or when the
    /// [`effective_api_version`](Self::effective_api_version) is below
    /// 1.2 and the device does not advertise `VK_KHR_driver_properties`
    /// — rather than reporting a zeroed struct as though the driver had
    /// answered. As with
    /// [`subgroup_properties`](Self::subgroup_properties), a 1.0-created
    /// [`Instance`](super::Instance) declines regardless of the device.
    pub fn driver_properties(&self) -> Option<DriverProperties> {
        let get2 = self.instance.dispatch.vkGetPhysicalDeviceProperties2?;

        let api = self.effective_api_version();
        let supported = (api.major() > 1 || api.minor() >= 2)
            || self
                .enumerate_extension_properties()
                .map(|exts| exts.iter().any(|e| e.name() == "VK_KHR_driver_properties"))
                .unwrap_or(false);
        if !supported {
            return None;
        }

        let mut chain = crate::safe::PNextChain::new();
        chain.push(VkPhysicalDeviceDriverProperties::new_pnext());
        let mut props2 = VkPhysicalDeviceProperties2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
            pNext: chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle valid; props2 + chain live for the call.
        unsafe { get2(self.handle, &mut props2) };

        let d = chain.get::<VkPhysicalDeviceDriverProperties>()?;
        // Safety: both are null-terminated char arrays per the spec.
        let driver_name = unsafe { CStr::from_ptr(d.driverName.as_ptr()) }
            .to_string_lossy()
            .into_owned();
        let driver_info = unsafe { CStr::from_ptr(d.driverInfo.as_ptr()) }
            .to_string_lossy()
            .into_owned();

        // Advertising the extension is necessary but NOT sufficient: an
        // implementation running as 1.0 may advertise
        // `VK_KHR_driver_properties` as a *device* extension and still
        // leave the struct untouched, because the instance was created
        // below the version that processes it. (Observed on the AMD
        // proprietary driver: advertised at instance 1.0, populated only
        // at instance 1.2+.) A conforming driver that genuinely answers
        // always names itself, so an empty name means "not answered" —
        // decline rather than hand back a hollow struct whose `driver_id`
        // is really just the zero-initializer.
        if driver_name.is_empty() {
            return None;
        }

        Some(DriverProperties {
            driver_id: d.driverID,
            driver_name,
            driver_info,
            conformance_version: ConformanceVersion {
                major: d.conformanceVersion.major,
                minor: d.conformanceVersion.minor,
                subminor: d.conformanceVersion.subminor,
                patch: d.conformanceVersion.patch,
            },
        })
    }

    /// Query the shader arithmetic capabilities that gate reduced-precision
    /// kernels — `shaderFloat16` / `shaderInt8`
    /// (`VkPhysicalDeviceShaderFloat16Int8Features`, Vulkan 1.2 core) and the
    /// 16-/8-bit storage-buffer access features (Vulkan 1.1 and 1.2 core
    /// respectively).
    ///
    /// These decide whether a half-precision or quantized kernel can exist on
    /// a device at all, which makes them a specialization axis rather than a
    /// tuning knob: a kernel built against `shaderFloat16` is not merely
    /// slower without it, it is invalid. Compute precision is a distinct
    /// question from *storage* precision — a device can accept 16-bit data in
    /// a storage buffer while doing the arithmetic in f32 — so the two are
    /// reported separately rather than collapsed.
    ///
    /// Returns `None` when `vkGetPhysicalDeviceFeatures2` is unavailable, or
    /// when the [`effective_api_version`](Self::effective_api_version) is
    /// below 1.2 and the device advertises neither
    /// `VK_KHR_shader_float16_int8` nor `VK_KHR_8bit_storage` — rather than
    /// reporting an untouched all-`false` struct as though the driver had
    /// answered. As elsewhere, a 1.0-created [`Instance`](super::Instance)
    /// declines regardless of the device.
    pub fn shader_arithmetic_features(&self) -> Option<ShaderArithmeticFeatures> {
        let get2 = self.instance.dispatch.vkGetPhysicalDeviceFeatures2?;

        let api = self.effective_api_version();
        let core_1_2 = api.major() > 1 || api.minor() >= 2;
        let exts = self.enumerate_extension_properties().unwrap_or_default();
        let has = |name: &str| exts.iter().any(|e| e.name() == name);
        if !core_1_2 && !has("VK_KHR_shader_float16_int8") && !has("VK_KHR_8bit_storage") {
            return None;
        }

        let mut chain = crate::safe::PNextChain::new();
        chain.push(VkPhysicalDeviceShaderFloat16Int8Features::new_pnext());
        chain.push(VkPhysicalDevice16BitStorageFeatures::new_pnext());
        chain.push(VkPhysicalDevice8BitStorageFeatures::new_pnext());
        let mut features2 = VkPhysicalDeviceFeatures2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
            pNext: chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle valid; features2 + chain live for the call.
        unsafe { get2(self.handle, &mut features2) };

        let f16i8 = chain.get::<VkPhysicalDeviceShaderFloat16Int8Features>()?;
        let s16 = chain.get::<VkPhysicalDevice16BitStorageFeatures>();
        let s8 = chain.get::<VkPhysicalDevice8BitStorageFeatures>();

        Some(ShaderArithmeticFeatures {
            shader_float16: f16i8.shaderFloat16 != 0,
            shader_int8: f16i8.shaderInt8 != 0,
            storage_buffer_16bit: s16.is_some_and(|s| s.storageBuffer16BitAccess != 0),
            storage_buffer_8bit: s8.is_some_and(|s| s.storageBuffer8BitAccess != 0),
        })
    }

    /// Query shader integer-dot-product acceleration properties
    /// (`VK_KHR_shader_integer_dot_product`, core in Vulkan 1.3).
    ///
    /// Describes which integer-dot-product SPIR-V ops the device
    /// accelerates natively. For ML workloads the 8-bit and 4×8-bit
    /// packed variants are what you typically care about: they map
    /// directly onto int8-quantized matmul and convolution kernels.
    ///
    /// Returns `None` if `vkGetPhysicalDeviceProperties2` is not
    /// available (Vulkan 1.0 without
    /// `VK_KHR_get_physical_device_properties2`). The boolean fields
    /// will be `false` across the board on devices that do not
    /// implement the extension — a safe all-zeros reading.
    pub fn shader_integer_dot_product_properties(
        &self,
    ) -> Option<ShaderIntegerDotProductProperties> {
        let get2 = self.instance.dispatch.vkGetPhysicalDeviceProperties2?;

        let mut chain = crate::safe::PNextChain::new();
        chain.push(VkPhysicalDeviceShaderIntegerDotProductProperties::new_pnext());
        let mut props2 = VkPhysicalDeviceProperties2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
            pNext: chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle valid; props2 + chain live for the call.
        unsafe { get2(self.handle, &mut props2) };

        let raw = chain.get::<VkPhysicalDeviceShaderIntegerDotProductProperties>()?;
        Some(ShaderIntegerDotProductProperties {
            dot_product_8bit_unsigned: raw.integerDotProduct8BitUnsignedAccelerated != 0,
            dot_product_8bit_signed: raw.integerDotProduct8BitSignedAccelerated != 0,
            dot_product_8bit_mixed: raw.integerDotProduct8BitMixedSignednessAccelerated != 0,
            dot_product_4x8bit_packed_unsigned: raw
                .integerDotProduct4x8BitPackedUnsignedAccelerated
                != 0,
            dot_product_4x8bit_packed_signed: raw.integerDotProduct4x8BitPackedSignedAccelerated
                != 0,
            dot_product_4x8bit_packed_mixed: raw
                .integerDotProduct4x8BitPackedMixedSignednessAccelerated
                != 0,
            dot_product_16bit_unsigned: raw.integerDotProduct16BitUnsignedAccelerated != 0,
            dot_product_16bit_signed: raw.integerDotProduct16BitSignedAccelerated != 0,
            dot_product_32bit_unsigned: raw.integerDotProduct32BitUnsignedAccelerated != 0,
            dot_product_32bit_signed: raw.integerDotProduct32BitSignedAccelerated != 0,
            dot_product_64bit_unsigned: raw.integerDotProduct64BitUnsignedAccelerated != 0,
            dot_product_64bit_signed: raw.integerDotProduct64BitSignedAccelerated != 0,
            dot_product_accumulating_sat_8bit_signed: raw
                .integerDotProductAccumulatingSaturating8BitSignedAccelerated
                != 0,
            dot_product_accumulating_sat_8bit_unsigned: raw
                .integerDotProductAccumulatingSaturating8BitUnsignedAccelerated
                != 0,
            dot_product_accumulating_sat_4x8bit_packed_signed: raw
                .integerDotProductAccumulatingSaturating4x8BitPackedSignedAccelerated
                != 0,
            dot_product_accumulating_sat_4x8bit_packed_unsigned: raw
                .integerDotProductAccumulatingSaturating4x8BitPackedUnsignedAccelerated
                != 0,
        })
    }

    /// Query `VK_KHR_ray_tracing_pipeline` runtime properties — SBT
    /// handle size, alignment, recursion limits.
    ///
    /// All are required to lay out a shader binding table correctly.
    /// Returns `None` if `vkGetPhysicalDeviceProperties2` is not
    /// available; returns a struct with all-zero values on a driver
    /// that doesn't implement the extension.
    pub fn ray_tracing_pipeline_properties(
        &self,
    ) -> Option<super::ray_tracing_pipeline::RayTracingPipelineProperties> {
        let get2 = self.instance.dispatch.vkGetPhysicalDeviceProperties2?;
        let mut chain = crate::safe::PNextChain::new();
        chain.push(VkPhysicalDeviceRayTracingPipelinePropertiesKHR::new_pnext());
        let mut props2 = VkPhysicalDeviceProperties2 {
            sType: VkStructureType::STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
            pNext: chain.head_mut(),
            ..Default::default()
        };
        // Safety: handle valid; props2 + chain live for the call.
        unsafe { get2(self.handle, &mut props2) };
        let raw = chain.get::<VkPhysicalDeviceRayTracingPipelinePropertiesKHR>()?;
        Some(super::ray_tracing_pipeline::RayTracingPipelineProperties {
            shader_group_handle_size: raw.shaderGroupHandleSize,
            max_ray_recursion_depth: raw.maxRayRecursionDepth,
            max_shader_group_stride: raw.maxShaderGroupStride,
            shader_group_base_alignment: raw.shaderGroupBaseAlignment,
            shader_group_handle_alignment: raw.shaderGroupHandleAlignment,
            max_ray_dispatch_invocation_count: raw.maxRayDispatchInvocationCount,
            max_ray_hit_attribute_size: raw.maxRayHitAttributeSize,
        })
    }

    /// The number of nanoseconds per timestamp tick on this device.
    ///
    /// `vkCmdWriteTimestamp` writes a `u64` count of implementation-defined
    /// ticks; multiply by this value to get nanoseconds. Returns `0.0` on
    /// devices that do not support timestamps at all (which is rare — most
    /// modern GPUs do).
    pub fn timestamp_period(&self) -> f32 {
        self.properties().timestamp_period()
    }

    /// Find the index of the first memory type that has all the required
    /// property flags AND is allowed by the memory_type_bits mask.
    ///
    /// `memory_type_bits` typically comes from a `VkMemoryRequirements`
    /// returned by `vkGetBufferMemoryRequirements` etc.
    pub fn find_memory_type(
        &self,
        memory_type_bits: u32,
        required: super::MemoryPropertyFlags,
    ) -> Option<u32> {
        let props = self.memory_properties();
        for i in 0..props.type_count() {
            let allowed = (memory_type_bits & (1 << i)) != 0;
            if allowed && props.memory_type(i).property_flags().contains(required) {
                return Some(i);
            }
        }
        None
    }
}

/// Strongly-typed wrapper around `VkPhysicalDeviceProperties`.
#[derive(Clone)]
pub struct PhysicalDeviceProperties {
    raw: VkPhysicalDeviceProperties,
}

impl PhysicalDeviceProperties {
    /// Vulkan API version supported by the device.
    pub fn api_version(&self) -> ApiVersion {
        ApiVersion(self.raw.apiVersion)
    }

    /// Driver version (vendor-specific encoding).
    pub fn driver_version(&self) -> u32 {
        self.raw.driverVersion
    }

    /// PCI vendor ID.
    pub fn vendor_id(&self) -> u32 {
        self.raw.vendorID
    }

    /// PCI device ID.
    pub fn device_id(&self) -> u32 {
        self.raw.deviceID
    }

    /// The kind of physical device (discrete GPU, integrated, virtual, CPU, ...).
    pub fn device_type(&self) -> PhysicalDeviceType {
        PhysicalDeviceType(self.raw.deviceType)
    }

    /// Number of nanoseconds per timestamp tick. See
    /// [`PhysicalDevice::timestamp_period`].
    pub fn timestamp_period(&self) -> f32 {
        self.raw.limits.timestampPeriod
    }

    /// Maximum push constant size in bytes guaranteed by this device.
    /// Vulkan guarantees at least 128 bytes; most desktop GPUs report 256.
    pub fn max_push_constants_size(&self) -> u32 {
        self.raw.limits.maxPushConstantsSize
    }

    /// Human-readable device name.
    pub fn device_name(&self) -> String {
        // Safety: deviceName is a null-terminated array of c_char per spec.
        unsafe {
            CStr::from_ptr(self.raw.deviceName.as_ptr())
                .to_string_lossy()
                .into_owned()
        }
    }
}

/// Strongly-typed wrapper around `VkPhysicalDeviceType`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhysicalDeviceType(pub VkPhysicalDeviceType);

impl PhysicalDeviceType {
    pub const OTHER: Self = Self(VkPhysicalDeviceType::PHYSICAL_DEVICE_TYPE_OTHER);
    pub const INTEGRATED_GPU: Self =
        Self(VkPhysicalDeviceType::PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU);
    pub const DISCRETE_GPU: Self = Self(VkPhysicalDeviceType::PHYSICAL_DEVICE_TYPE_DISCRETE_GPU);
    pub const VIRTUAL_GPU: Self = Self(VkPhysicalDeviceType::PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU);
    pub const CPU: Self = Self(VkPhysicalDeviceType::PHYSICAL_DEVICE_TYPE_CPU);
}

impl std::fmt::Debug for PhysicalDeviceProperties {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PhysicalDeviceProperties")
            .field("device_name", &self.device_name())
            .field("device_type", &self.device_type())
            .field("api_version", &self.api_version())
            .field("driver_version", &self.driver_version())
            .field("vendor_id", &self.vendor_id())
            .field("device_id", &self.device_id())
            .finish()
    }
}

/// Strongly-typed wrapper around `VkQueueFlags`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueueFlags(pub u32);

impl QueueFlags {
    pub const GRAPHICS: Self = Self(0x1);
    pub const COMPUTE: Self = Self(0x2);
    pub const TRANSFER: Self = Self(0x4);
    pub const SPARSE_BINDING: Self = Self(0x8);

    pub const fn contains(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }
}

impl std::ops::BitOr for QueueFlags {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

/// Strongly-typed wrapper around `VkQueueFamilyProperties`.
#[derive(Clone)]
pub struct QueueFamilyProperties {
    raw: VkQueueFamilyProperties,
}

impl QueueFamilyProperties {
    pub fn queue_flags(&self) -> QueueFlags {
        QueueFlags(self.raw.queueFlags)
    }

    pub fn queue_count(&self) -> u32 {
        self.raw.queueCount
    }

    pub fn timestamp_valid_bits(&self) -> u32 {
        self.raw.timestampValidBits
    }
}

impl std::fmt::Debug for QueueFamilyProperties {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("QueueFamilyProperties")
            .field("queue_flags", &self.queue_flags())
            .field("queue_count", &self.queue_count())
            .finish()
    }
}

/// Strongly-typed wrapper around `VkPhysicalDeviceMemoryProperties`.
#[derive(Clone)]
pub struct MemoryProperties {
    raw: VkPhysicalDeviceMemoryProperties,
}

impl MemoryProperties {
    pub fn type_count(&self) -> u32 {
        self.raw.memoryTypeCount
    }

    pub fn heap_count(&self) -> u32 {
        self.raw.memoryHeapCount
    }

    pub fn memory_type(&self, index: u32) -> MemoryType {
        MemoryType {
            raw: self.raw.memoryTypes[index as usize],
        }
    }

    pub fn memory_heap(&self, index: u32) -> MemoryHeap {
        MemoryHeap {
            raw: self.raw.memoryHeaps[index as usize],
        }
    }
}

/// A memory type description.
#[derive(Clone)]
pub struct MemoryType {
    raw: VkMemoryType,
}

impl MemoryType {
    pub fn property_flags(&self) -> super::MemoryPropertyFlags {
        super::MemoryPropertyFlags(self.raw.propertyFlags)
    }

    pub fn heap_index(&self) -> u32 {
        self.raw.heapIndex
    }
}

/// A memory heap description.
#[derive(Clone)]
pub struct MemoryHeap {
    raw: VkMemoryHeap,
}

impl MemoryHeap {
    pub fn size(&self) -> u64 {
        self.raw.size
    }

    pub fn flags(&self) -> MemoryHeapFlags {
        MemoryHeapFlags(self.raw.flags)
    }
}

/// Strongly-typed wrapper around `VkMemoryHeapFlags`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryHeapFlags(pub u32);

impl MemoryHeapFlags {
    pub const DEVICE_LOCAL: Self = Self(0x1);

    pub const fn contains(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }
}

/// Per-heap memory budget snapshot from `VK_EXT_memory_budget`.
///
/// `budget[i]` is the soft cap the driver suggests respecting for heap
/// `i`; `usage[i]` is the driver's estimate of currently-allocated bytes.
/// Both arrays are length `heap_count`. Heap indices in this struct match
/// the indices returned by [`PhysicalDevice::memory_properties`].
#[derive(Debug, Clone)]
pub struct MemoryBudget {
    pub heap_count: u32,
    pub budget: [u64; 16],
    pub usage: [u64; 16],
}

/// Safe view of
/// [`VkPhysicalDeviceShaderIntegerDotProductProperties`](crate::raw::bindings::VkPhysicalDeviceShaderIntegerDotProductProperties).
///
/// Each field is `true` when the device accelerates that SPIR-V dot-product
/// variant natively. For ML workloads the 8-bit and 4×8-bit-packed signals
/// are the high-value ones — they gate whether int8 quantized matmul /
/// convolution compiles down to hardware SIMD-dot (e.g. DP4a on AMD,
/// __dp4a on NVIDIA) or a slower fallback.
#[derive(Debug, Clone, Copy, Default)]
pub struct ShaderIntegerDotProductProperties {
    pub dot_product_8bit_unsigned: bool,
    pub dot_product_8bit_signed: bool,
    pub dot_product_8bit_mixed: bool,
    pub dot_product_4x8bit_packed_unsigned: bool,
    pub dot_product_4x8bit_packed_signed: bool,
    pub dot_product_4x8bit_packed_mixed: bool,
    pub dot_product_16bit_unsigned: bool,
    pub dot_product_16bit_signed: bool,
    pub dot_product_32bit_unsigned: bool,
    pub dot_product_32bit_signed: bool,
    pub dot_product_64bit_unsigned: bool,
    pub dot_product_64bit_signed: bool,
    pub dot_product_accumulating_sat_8bit_signed: bool,
    pub dot_product_accumulating_sat_8bit_unsigned: bool,
    pub dot_product_accumulating_sat_4x8bit_packed_signed: bool,
    pub dot_product_accumulating_sat_4x8bit_packed_unsigned: bool,
}

impl ShaderIntegerDotProductProperties {
    /// `true` if the device accelerates *any* int8 or 4×8-bit-packed
    /// dot-product variant — the minimum bar for hardware-accelerated
    /// int8-quantized inference.
    pub fn has_any_int8_acceleration(&self) -> bool {
        self.dot_product_8bit_signed
            || self.dot_product_8bit_unsigned
            || self.dot_product_8bit_mixed
            || self.dot_product_4x8bit_packed_signed
            || self.dot_product_4x8bit_packed_unsigned
            || self.dot_product_4x8bit_packed_mixed
    }
}

impl MemoryBudget {
    /// Total budget summed across all heaps.
    pub fn total_budget(&self) -> u64 {
        self.budget[..self.heap_count as usize].iter().sum()
    }
    /// Total usage summed across all heaps.
    pub fn total_usage(&self) -> u64 {
        self.usage[..self.heap_count as usize].iter().sum()
    }
}

/// Stable identity of a physical device, from
/// [`PhysicalDevice::device_identity`].
///
/// Sourced from `VkPhysicalDeviceIDProperties` (Vulkan 1.1 core) plus,
/// when the device advertises `VK_EXT_pci_bus_info`, its PCI bus address.
/// The point of this struct is **out-of-band correlation**: Vulkan can
/// tell you *which* GPU you hold but not how busy it is, so identity is
/// what lets a caller match this device against a vendor/OS telemetry
/// source (NVML by UUID, DXGI/D3DKMT by LUID, Linux sysfs by PCI address)
/// — or against the same device exposed through CUDA, D3D, or OpenGL.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceIdentity {
    /// Universally-unique device identifier, stable across processes,
    /// reboots, and driver reinstalls. The same value the device reports
    /// to CUDA / OpenGL and to `nvmlDeviceGetUUID`. Always populated.
    pub device_uuid: [u8; 16],
    /// UUID of the driver build. Devices driven by the same driver share
    /// this; useful for telling two ICDs apart.
    pub driver_uuid: [u8; 16],
    /// Locally-unique device identifier — `Some` only on platforms that
    /// mark it valid (Windows, via `deviceLUIDValid`). Pair with
    /// [`device_node_mask`](Self::device_node_mask) to match a DXGI
    /// adapter (`IDXGIAdapter::GetDesc`) or a D3DKMT node. `None` on
    /// Linux and other LUID-less platforms — match by
    /// [`device_uuid`](Self::device_uuid) or [`pci`](Self::pci) there.
    pub device_luid: Option<[u8; 8]>,
    /// Node mask scoping [`device_luid`](Self::device_luid) within a
    /// linked-adapter set. Meaningful only when `device_luid` is `Some`.
    pub device_node_mask: u32,
    /// PCI bus address, present only when the device advertises
    /// `VK_EXT_pci_bus_info`. `None` on software rasterizers and any
    /// platform that doesn't expose PCI topology.
    pub pci: Option<PciBusInfo>,
}

/// PCI bus address of a physical device — `domain:bus:device.function`,
/// from `VK_EXT_pci_bus_info`.
///
/// On Linux this maps directly to the
/// `/sys/bus/pci/devices/<domain>:<bus>:<device>.<function>` sysfs node
/// (and so to the amdgpu `gpu_busy_percent` file); on any platform it
/// pins the device to a stable hardware slot for correlation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PciBusInfo {
    pub domain: u32,
    pub bus: u32,
    pub device: u32,
    pub function: u32,
}

/// Which subgroup operation classes a device supports, from
/// `VkPhysicalDeviceSubgroupProperties::supportedOperations`.
///
/// `BASIC` is the floor guaranteed by Vulkan 1.1; everything above it is
/// optional. A reduction kernel wants at least `ARITHMETIC`; a scan or a
/// prefix-sum additionally wants `SHUFFLE_RELATIVE` or `CLUSTERED`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubgroupFeatureFlags(pub u32);

impl SubgroupFeatureFlags {
    /// `subgroupBarrier`, `subgroupElect`, `subgroupMemoryBarrier` —
    /// mandatory on every Vulkan 1.1 implementation.
    pub const BASIC: Self = Self(SUBGROUP_FEATURE_BASIC_BIT);
    /// `subgroupAll` / `subgroupAny` / `subgroupAllEqual`.
    pub const VOTE: Self = Self(SUBGROUP_FEATURE_VOTE_BIT);
    /// `subgroupAdd` / `subgroupMul` / `subgroupMin` / `subgroupMax` and
    /// their inclusive/exclusive scans — the class a cross-lane reduction
    /// (`WaveActiveSum`) needs.
    pub const ARITHMETIC: Self = Self(SUBGROUP_FEATURE_ARITHMETIC_BIT);
    /// `subgroupBallot` and friends.
    pub const BALLOT: Self = Self(SUBGROUP_FEATURE_BALLOT_BIT);
    /// `subgroupShuffle` / `subgroupShuffleXor`.
    pub const SHUFFLE: Self = Self(SUBGROUP_FEATURE_SHUFFLE_BIT);
    /// `subgroupShuffleUp` / `subgroupShuffleDown`.
    pub const SHUFFLE_RELATIVE: Self = Self(SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT);
    /// Clustered reductions over power-of-two lane groups.
    pub const CLUSTERED: Self = Self(SUBGROUP_FEATURE_CLUSTERED_BIT);
    /// Quad (2×2) shuffles and broadcasts.
    pub const QUAD: Self = Self(SUBGROUP_FEATURE_QUAD_BIT);
    /// `VK_KHR_shader_subgroup_rotate` (Vulkan 1.4 core).
    pub const ROTATE: Self = Self(SUBGROUP_FEATURE_ROTATE_BIT);
    /// Clustered rotate, from the same extension.
    pub const ROTATE_CLUSTERED: Self = Self(SUBGROUP_FEATURE_ROTATE_CLUSTERED_BIT);
    /// `VK_NV_shader_subgroup_partitioned`.
    pub const PARTITIONED_NV: Self = Self(SUBGROUP_FEATURE_PARTITIONED_BIT_NV);

    pub const fn contains(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }
}

impl std::ops::BitOr for SubgroupFeatureFlags {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

/// A device's subgroup ("wave" on AMD/HLSL, "warp" on NVIDIA) properties.
///
/// Returned by [`PhysicalDevice::subgroup_properties`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubgroupProperties {
    /// Lanes per subgroup — 32 on NVIDIA, 64 on AMD GCN/CDNA, 32 or 64
    /// on RDNA, 8/16/32 on Intel. This is the default width a shader
    /// sees; where [`size_control`](Self::size_control) is present the
    /// pipeline may pin a different one within its range.
    pub subgroup_size: u32,
    /// Which shader stages may use subgroup operations at all. Compute is
    /// guaranteed; the rest are optional.
    pub supported_stages: super::ShaderStageFlags,
    /// Which classes of subgroup operation the device implements.
    pub supported_operations: SubgroupFeatureFlags,
    /// Whether quad operations work in every stage in
    /// [`supported_stages`](Self::supported_stages), not just fragment
    /// and compute.
    pub quad_operations_in_all_stages: bool,
    /// The pinnable subgroup-size range, when the device exposes
    /// `VK_EXT_subgroup_size_control` (Vulkan 1.3 core). `None` means
    /// the size is fixed at [`subgroup_size`](Self::subgroup_size) and
    /// [`ComputePipelineOptions::required_subgroup_size`](super::ComputePipelineOptions::required_subgroup_size)
    /// must be left unset.
    pub size_control: Option<SubgroupSizeControl>,
}

/// The subgroup-size range a pipeline may pin, from
/// `VkPhysicalDeviceSubgroupSizeControlProperties`.
///
/// This is the range
/// [`ComputePipelineOptions::required_subgroup_size`](super::ComputePipelineOptions::required_subgroup_size)
/// must fall within — use [`permits`](Self::permits) to check a
/// candidate before building the pipeline.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubgroupSizeControl {
    /// Smallest pinnable subgroup size. A power of two, per the spec.
    pub min_subgroup_size: u32,
    /// Largest pinnable subgroup size. A power of two, per the spec.
    pub max_subgroup_size: u32,
    /// Upper bound on subgroups per compute workgroup.
    pub max_compute_workgroup_subgroups: u32,
    /// Which stages accept a pinned size. Check for
    /// [`ShaderStageFlags::COMPUTE`](super::ShaderStageFlags::COMPUTE)
    /// before pinning one on a compute pipeline.
    pub required_subgroup_size_stages: super::ShaderStageFlags,
}

impl SubgroupSizeControl {
    /// Whether `size` is a legal
    /// [`required_subgroup_size`](super::ComputePipelineOptions::required_subgroup_size)
    /// on this device: a power of two within
    /// `[min_subgroup_size, max_subgroup_size]`.
    ///
    /// This checks the size range only. Also confirm the stage accepts a
    /// pinned size — see
    /// [`permits_in_compute`](Self::permits_in_compute), which checks
    /// both.
    pub const fn permits(&self, size: u32) -> bool {
        size.is_power_of_two() && size >= self.min_subgroup_size && size <= self.max_subgroup_size
    }

    /// Whether `size` may be pinned on a **compute** pipeline: both
    /// [`permits`](Self::permits) and the compute stage appearing in
    /// [`required_subgroup_size_stages`](Self::required_subgroup_size_stages).
    pub const fn permits_in_compute(&self, size: u32) -> bool {
        self.permits(size)
            && self
                .required_subgroup_size_stages
                .contains(super::ShaderStageFlags::COMPUTE)
    }
}

/// Shader arithmetic capabilities that gate reduced-precision kernels.
///
/// Returned by [`PhysicalDevice::shader_arithmetic_features`]. Compute
/// precision and storage precision are separate questions and are reported
/// separately: a device may accept 16-bit data in a storage buffer while
/// performing the arithmetic itself in f32.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ShaderArithmeticFeatures {
    /// `shaderFloat16` — half-precision *arithmetic* in shaders.
    pub shader_float16: bool,
    /// `shaderInt8` — 8-bit integer *arithmetic* in shaders.
    pub shader_int8: bool,
    /// `storageBuffer16BitAccess` — 16-bit types readable/writable in storage
    /// buffers, independent of whether arithmetic on them is supported.
    pub storage_buffer_16bit: bool,
    /// `storageBuffer8BitAccess` — likewise for 8-bit types.
    pub storage_buffer_8bit: bool,
}

/// Driver identity — which ICD is behind this physical device, and what
/// Vulkan conformance level it claims.
///
/// Returned by [`PhysicalDevice::driver_properties`]. Prefer this over
/// [`PhysicalDeviceProperties::driver_version`](PhysicalDeviceProperties::driver_version),
/// whose bit-packing is vendor-defined and therefore not portably
/// decodable.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DriverProperties {
    /// Which ICD this is — `DRIVER_ID_MESA_RADV`,
    /// `DRIVER_ID_AMD_PROPRIETARY`, `DRIVER_ID_NVIDIA_PROPRIETARY`,
    /// `DRIVER_ID_MESA_LLVMPIPE`, … Two ICDs driving the *same* hardware
    /// are distinct here, which is what makes this the right axis for
    /// gating a driver-specific workaround.
    pub driver_id: VkDriverId,
    /// Vendor's name for the driver, e.g. `"radv"`, `"NVIDIA"`.
    pub driver_name: String,
    /// Free-form version detail, e.g. `"Mesa 24.1.2"`. Together with
    /// [`driver_id`](Self::driver_id) this forms a stable, legible
    /// shader-cache key.
    pub driver_info: String,
    /// The Vulkan CTS version the driver claims conformance to. All-zero
    /// on a driver that makes no claim.
    pub conformance_version: ConformanceVersion,
}

/// A Vulkan CTS conformance version, `major.minor.subminor.patch`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct ConformanceVersion {
    pub major: u8,
    pub minor: u8,
    pub subminor: u8,
    pub patch: u8,
}

impl std::fmt::Display for ConformanceVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}.{}.{}.{}",
            self.major, self.minor, self.subminor, self.patch
        )
    }
}

/// A Vulkan physical device group: a set of one or more physical devices
/// that share `VkDeviceMemory` allocations and can run in tandem with
/// per-allocation / per-submission `device_mask` parameters.
///
/// Single-device "groups" of length 1 are the overwhelmingly common case
/// and behave identically to a non-grouped [`PhysicalDevice`]. Multi-GPU
/// systems (e.g. dual SLI / CrossFire / explicit-multi-GPU) expose
/// genuine groups via
/// [`Instance::enumerate_physical_device_groups`](super::Instance::enumerate_physical_device_groups).
///
/// Use [`PhysicalDeviceGroup::create_device`] to create a [`Device`]
/// that internally tracks every physical device in the group. Single
/// physical devices created via [`PhysicalDevice::create_device`]
/// produce a [`Device`] that internally wraps a singleton group, so
/// every code path in the safe wrapper sees the same shape.
#[derive(Clone)]
#[allow(dead_code)] // `instance` keeps the parent alive even if unread.
pub struct PhysicalDeviceGroup {
    pub(crate) instance: Arc<InstanceInner>,
    pub(crate) physical_devices: Vec<PhysicalDevice>,
    pub(crate) subset_allocation: bool,
}

impl PhysicalDeviceGroup {
    /// Returns the physical devices in this group, in the order
    /// `vkEnumeratePhysicalDeviceGroups` reported them. Always at
    /// least one element; usually exactly one on consumer hardware.
    pub fn physical_devices(&self) -> &[PhysicalDevice] {
        &self.physical_devices
    }

    /// Number of physical devices in this group.
    pub fn count(&self) -> u32 {
        self.physical_devices.len() as u32
    }

    /// `true` if the implementation supports subset memory allocations
    /// across this group (allowing per-device-mask allocation flags).
    /// Always `false` on single-device groups.
    pub fn supports_subset_allocation(&self) -> bool {
        self.subset_allocation
    }

    /// Create a logical [`Device`] from this group.
    pub fn create_device(&self, info: DeviceCreateInfo<'_>) -> Result<Device> {
        Device::new_group(self, info)
    }
}

impl std::fmt::Debug for PhysicalDeviceGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PhysicalDeviceGroup")
            .field("count", &self.count())
            .field("subset_allocation", &self.subset_allocation)
            .finish()
    }
}

/// One supported cooperative-matrix shape, as returned by
/// [`PhysicalDevice::cooperative_matrix_properties`].
#[derive(Clone)]
pub struct CooperativeMatrixProperties {
    raw: VkCooperativeMatrixPropertiesKHR,
}

impl CooperativeMatrixProperties {
    pub fn m_size(&self) -> u32 {
        self.raw.MSize
    }
    pub fn n_size(&self) -> u32 {
        self.raw.NSize
    }
    pub fn k_size(&self) -> u32 {
        self.raw.KSize
    }
    /// Component type of operand A. The value is the raw
    /// `VkComponentTypeKHR` enum.
    pub fn a_type(&self) -> VkComponentTypeKHR {
        self.raw.AType
    }
    pub fn b_type(&self) -> VkComponentTypeKHR {
        self.raw.BType
    }
    pub fn c_type(&self) -> VkComponentTypeKHR {
        self.raw.CType
    }
    pub fn result_type(&self) -> VkComponentTypeKHR {
        self.raw.ResultType
    }
    /// Whether the implementation saturates accumulator overflow.
    pub fn saturating_accumulation(&self) -> bool {
        self.raw.saturatingAccumulation != 0
    }
    pub fn scope(&self) -> VkScopeKHR {
        self.raw.scope
    }
}

impl std::fmt::Debug for CooperativeMatrixProperties {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CooperativeMatrixProperties")
            .field("M", &self.m_size())
            .field("N", &self.n_size())
            .field("K", &self.k_size())
            .field("AType", &self.a_type())
            .field("BType", &self.b_type())
            .field("CType", &self.c_type())
            .field("ResultType", &self.result_type())
            .finish()
    }
}

// Re-use Error so callers don't need a separate import.
#[allow(dead_code)]
fn _ensure_error_is_used(_: Error) {}