1 // Copyright (c) 2021 The Vulkano developers
2 // Licensed under the Apache License, Version 2.0
3 // <LICENSE-APACHE or
4 // https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5 // license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
6 // at your option. All files in the project carrying such
7 // notice may not be copied, modified, or distributed except
8 // according to those terms.
9
10 use super::{write_file, IndexMap, VkRegistryData};
11 use heck::ToSnakeCase;
12 use proc_macro2::{Ident, Literal, TokenStream};
13 use quote::{format_ident, quote};
14 use std::fmt::Write as _;
15 use vk_parse::Extension;
16
17 // This is not included in vk.xml, so it's added here manually
required_if_supported(name: &str) -> bool18 fn required_if_supported(name: &str) -> bool {
19 #[allow(clippy::match_like_matches_macro)]
20 match name {
21 "VK_KHR_portability_subset" => true,
22 _ => false,
23 }
24 }
25
conflicts_extensions(name: &str) -> &'static [&'static str]26 fn conflicts_extensions(name: &str) -> &'static [&'static str] {
27 match name {
28 "VK_KHR_buffer_device_address" => &["VK_EXT_buffer_device_address"],
29 "VK_EXT_buffer_device_address" => &["VK_KHR_buffer_device_address"],
30 _ => &[],
31 }
32 }
33
write(vk_data: &VkRegistryData)34 pub fn write(vk_data: &VkRegistryData) {
35 write_device_extensions(vk_data);
36 write_instance_extensions(vk_data);
37 }
38
39 #[derive(Clone, Debug)]
40 struct ExtensionsMember {
41 name: Ident,
42 doc: String,
43 raw: String,
44 required_if_supported: bool,
45 requires: Vec<RequiresOneOf>,
46 conflicts_device_extensions: Vec<Ident>,
47 status: Option<ExtensionStatus>,
48 }
49
50 #[derive(Clone, Debug, Default, PartialEq, Eq)]
51 pub struct RequiresOneOf {
52 pub api_version: Option<(String, String)>,
53 pub device_extensions: Vec<Ident>,
54 pub instance_extensions: Vec<Ident>,
55 }
56
57 #[derive(Clone, Debug)]
58 enum Replacement {
59 Core((String, String)),
60 DeviceExtension(Ident),
61 InstanceExtension(Ident),
62 }
63
64 #[derive(Clone, Debug)]
65 enum ExtensionStatus {
66 Promoted(Replacement),
67 Deprecated(Option<Replacement>),
68 }
69
write_device_extensions(vk_data: &VkRegistryData)70 fn write_device_extensions(vk_data: &VkRegistryData) {
71 write_file(
72 "device_extensions.rs",
73 format!(
74 "vk.xml header version {}.{}.{}",
75 vk_data.header_version.0, vk_data.header_version.1, vk_data.header_version.2
76 ),
77 device_extensions_output(&extensions_members("device", &vk_data.extensions)),
78 );
79 }
80
write_instance_extensions(vk_data: &VkRegistryData)81 fn write_instance_extensions(vk_data: &VkRegistryData) {
82 write_file(
83 "instance_extensions.rs",
84 format!(
85 "vk.xml header version {}.{}.{}",
86 vk_data.header_version.0, vk_data.header_version.1, vk_data.header_version.2
87 ),
88 instance_extensions_output(&extensions_members("instance", &vk_data.extensions)),
89 );
90 }
91
device_extensions_output(members: &[ExtensionsMember]) -> TokenStream92 fn device_extensions_output(members: &[ExtensionsMember]) -> TokenStream {
93 let common = extensions_common_output(format_ident!("DeviceExtensions"), members);
94
95 let check_requirements_items = members.iter().map(|ExtensionsMember {
96 name,
97 requires,
98 conflicts_device_extensions,
99 required_if_supported,
100 ..
101 }| {
102 let name_string = name.to_string();
103
104 let requires_items = requires.iter().map(|require| {
105 let require_items = require.api_version.iter().map(|version| {
106 let version = format_ident!("V{}_{}", version.0, version.1);
107 quote! { api_version >= crate::Version::#version }
108 }).chain(require.instance_extensions.iter().map(|ext| {
109 quote! { instance_extensions.#ext }
110 })).chain(require.device_extensions.iter().map(|ext| {
111 quote! { device_extensions.#ext }
112 }));
113
114 let api_version_items = require.api_version.as_ref().map(|version| {
115 let version = format_ident!("V{}_{}", version.0, version.1);
116 quote! { Some(crate::Version::#version) }
117 }).unwrap_or_else(|| quote!{ None });
118 let device_extensions_items = require.device_extensions.iter().map(|ext| ext.to_string());
119 let instance_extensions_items = require.instance_extensions.iter().map(|ext| ext.to_string());
120
121 quote! {
122 if !(#(#require_items)||*) {
123 return Err(crate::device::ExtensionRestrictionError {
124 extension: #name_string,
125 restriction: crate::device::ExtensionRestriction::Requires(crate::RequiresOneOf {
126 api_version: #api_version_items,
127 device_extensions: &[#(#device_extensions_items),*],
128 instance_extensions: &[#(#instance_extensions_items),*],
129 ..Default::default()
130 }),
131 })
132 }
133 }
134 });
135 let conflicts_device_extensions_items = conflicts_device_extensions.iter().map(|extension| {
136 let string = extension.to_string();
137 quote! {
138 if self.#extension {
139 return Err(crate::device::ExtensionRestrictionError {
140 extension: #name_string,
141 restriction: crate::device::ExtensionRestriction::ConflictsDeviceExtension(#string),
142 });
143 }
144 }
145 });
146 let required_if_supported = if *required_if_supported {
147 quote! {
148 if supported.#name {
149 return Err(crate::device::ExtensionRestrictionError {
150 extension: #name_string,
151 restriction: crate::device::ExtensionRestriction::RequiredIfSupported,
152 });
153 }
154 }
155 } else {
156 quote! {}
157 };
158
159 quote! {
160 if self.#name {
161 if !supported.#name {
162 return Err(crate::device::ExtensionRestrictionError {
163 extension: #name_string,
164 restriction: crate::device::ExtensionRestriction::NotSupported,
165 });
166 }
167
168 #(#requires_items)*
169 #(#conflicts_device_extensions_items)*
170 } else {
171 #required_if_supported
172 }
173 }
174 });
175
176 quote! {
177 #common
178
179 impl DeviceExtensions {
180 /// Checks enabled extensions against the device version, instance extensions and each other.
181 pub(super) fn check_requirements(
182 &self,
183 supported: &DeviceExtensions,
184 api_version: crate::Version,
185 instance_extensions: &crate::instance::InstanceExtensions,
186 ) -> Result<(), crate::device::ExtensionRestrictionError> {
187 let device_extensions = self;
188 #(#check_requirements_items)*
189 Ok(())
190 }
191 }
192 }
193 }
194
instance_extensions_output(members: &[ExtensionsMember]) -> TokenStream195 fn instance_extensions_output(members: &[ExtensionsMember]) -> TokenStream {
196 let common = extensions_common_output(format_ident!("InstanceExtensions"), members);
197
198 let check_requirements_items =
199 members
200 .iter()
201 .map(|ExtensionsMember { name, requires, .. }| {
202 let name_string = name.to_string();
203
204 let requires_items = requires.iter().map(|require| {
205 let require_items = require
206 .api_version
207 .iter()
208 .map(|version| {
209 let version = format_ident!("V{}_{}", version.0, version.1);
210 quote! { api_version >= crate::Version::#version }
211 })
212 .chain(require.instance_extensions.iter().map(|ext| {
213 quote! { instance_extensions.#ext }
214 }))
215 .chain(require.device_extensions.iter().map(|ext| {
216 quote! { device_extensions.#ext }
217 }));
218
219 let api_version_items = require
220 .api_version
221 .as_ref()
222 .map(|version| {
223 let version = format_ident!("V{}_{}", version.0, version.1);
224 quote! { Some(crate::Version::#version) }
225 })
226 .unwrap_or_else(|| quote! { None });
227 let device_extensions_items =
228 require.device_extensions.iter().map(|ext| ext.to_string());
229 let instance_extensions_items = require
230 .instance_extensions
231 .iter()
232 .map(|ext| ext.to_string());
233
234 quote! {
235 if !(#(#require_items)||*) {
236 return Err(crate::instance::ExtensionRestrictionError {
237 extension: #name_string,
238 restriction: crate::instance::ExtensionRestriction::Requires(crate::RequiresOneOf {
239 api_version: #api_version_items,
240 device_extensions: &[#(#device_extensions_items),*],
241 instance_extensions: &[#(#instance_extensions_items),*],
242 ..Default::default()
243 }),
244 })
245 }
246 }
247 });
248
249 quote! {
250 if self.#name {
251 if !supported.#name {
252 return Err(crate::instance::ExtensionRestrictionError {
253 extension: #name_string,
254 restriction: crate::instance::ExtensionRestriction::NotSupported,
255 });
256 }
257
258 #(#requires_items)*
259 }
260 }
261 });
262
263 quote! {
264 #common
265
266 impl InstanceExtensions {
267 /// Checks enabled extensions against the instance version and each other.
268 pub(super) fn check_requirements(
269 &self,
270 supported: &InstanceExtensions,
271 api_version: crate::Version,
272 ) -> Result<(), crate::instance::ExtensionRestrictionError> {
273 let instance_extensions = self;
274 #(#check_requirements_items)*
275 Ok(())
276 }
277 }
278 }
279 }
280
extensions_common_output(struct_name: Ident, members: &[ExtensionsMember]) -> TokenStream281 fn extensions_common_output(struct_name: Ident, members: &[ExtensionsMember]) -> TokenStream {
282 let struct_items = members.iter().map(|ExtensionsMember { name, doc, .. }| {
283 quote! {
284 #[doc = #doc]
285 pub #name: bool,
286 }
287 });
288
289 let empty_items = members.iter().map(|ExtensionsMember { name, .. }| {
290 quote! {
291 #name: false,
292 }
293 });
294
295 let intersects_items = members.iter().map(|ExtensionsMember { name, .. }| {
296 quote! {
297 (self.#name && other.#name)
298 }
299 });
300
301 let contains_items = members.iter().map(|ExtensionsMember { name, .. }| {
302 quote! {
303 (self.#name || !other.#name)
304 }
305 });
306
307 let union_items = members.iter().map(|ExtensionsMember { name, .. }| {
308 quote! {
309 #name: self.#name || other.#name,
310 }
311 });
312
313 let intersection_items = members.iter().map(|ExtensionsMember { name, .. }| {
314 quote! {
315 #name: self.#name && other.#name,
316 }
317 });
318
319 let difference_items = members.iter().map(|ExtensionsMember { name, .. }| {
320 quote! {
321 #name: self.#name && !other.#name,
322 }
323 });
324
325 let symmetric_difference_items = members.iter().map(|ExtensionsMember { name, .. }| {
326 quote! {
327 #name: self.#name ^ other.#name,
328 }
329 });
330
331 let debug_items = members.iter().map(|ExtensionsMember { name, raw, .. }| {
332 quote! {
333 if self.#name {
334 if !first { write!(f, ", ")? }
335 else { first = false; }
336 f.write_str(#raw)?;
337 }
338 }
339 });
340
341 let arr_items = members.iter().map(|ExtensionsMember { name, raw, .. }| {
342 quote! {
343 (#raw, self.#name),
344 }
345 });
346 let arr_len = members.len();
347
348 let from_str_for_extensions_items =
349 members.iter().map(|ExtensionsMember { name, raw, .. }| {
350 let raw = Literal::string(raw);
351 quote! {
352 #raw => { extensions.#name = true; }
353 }
354 });
355
356 let from_extensions_for_vec_cstring_items =
357 members.iter().map(|ExtensionsMember { name, raw, .. }| {
358 quote! {
359 if x.#name { data.push(std::ffi::CString::new(#raw).unwrap()); }
360 }
361 });
362
363 quote! {
364 /// List of extensions that are enabled or available.
365 #[derive(Copy, Clone, PartialEq, Eq)]
366 pub struct #struct_name {
367 #(#struct_items)*
368
369 pub _ne: crate::NonExhaustive,
370 }
371
372 impl Default for #struct_name {
373 #[inline]
374 fn default() -> Self {
375 Self::empty()
376 }
377 }
378
379 impl #struct_name {
380 /// Returns an `Extensions` object with none of the members set.
381 #[inline]
382 pub const fn empty() -> Self {
383 Self {
384 #(#empty_items)*
385 _ne: crate::NonExhaustive(()),
386 }
387 }
388
389 /// Returns an `Extensions` object with none of the members set.
390 #[deprecated(since = "0.31.0", note = "Use `empty` instead.")]
391 #[inline]
392 pub const fn none() -> Self {
393 Self::empty()
394 }
395
396 /// Returns whether any members are set in both `self` and `other`.
397 #[inline]
398 pub const fn intersects(&self, other: &Self) -> bool {
399 #(#intersects_items)||*
400 }
401
402 /// Returns whether all members in `other` are set in `self`.
403 #[inline]
404 pub const fn contains(&self, other: &Self) -> bool {
405 #(#contains_items)&&*
406 }
407
408 /// Returns whether all members in `other` are set in `self`.
409 #[deprecated(since = "0.31.0", note = "Use `contains` instead.")]
410 #[inline]
411 pub const fn is_superset_of(&self, other: &Self) -> bool {
412 self.contains(other)
413 }
414
415 /// Returns the union of `self` and `other`.
416 #[inline]
417 pub const fn union(&self, other: &Self) -> Self {
418 Self {
419 #(#union_items)*
420 _ne: crate::NonExhaustive(()),
421 }
422 }
423
424 /// Returns the intersection of `self` and `other`.
425 #[inline]
426 pub const fn intersection(&self, other: &Self) -> Self {
427 Self {
428 #(#intersection_items)*
429 _ne: crate::NonExhaustive(()),
430 }
431 }
432
433 /// Returns `self` without the members set in `other`.
434 #[inline]
435 pub const fn difference(&self, other: &Self) -> Self {
436 Self {
437 #(#difference_items)*
438 _ne: crate::NonExhaustive(()),
439 }
440 }
441
442 /// Returns the members set in `self` or `other`, but not both.
443 #[inline]
444 pub const fn symmetric_difference(&self, other: &Self) -> Self {
445 Self {
446 #(#symmetric_difference_items)*
447 _ne: crate::NonExhaustive(()),
448 }
449 }
450 }
451
452 impl std::ops::BitAnd for #struct_name {
453 type Output = #struct_name;
454
455 #[inline]
456 fn bitand(self, rhs: Self) -> Self::Output {
457 self.union(&rhs)
458 }
459 }
460
461 impl std::ops::BitAndAssign for #struct_name {
462 #[inline]
463 fn bitand_assign(&mut self, rhs: Self) {
464 *self = self.union(&rhs);
465 }
466 }
467
468 impl std::ops::BitOr for #struct_name {
469 type Output = #struct_name;
470
471 #[inline]
472 fn bitor(self, rhs: Self) -> Self::Output {
473 self.intersection(&rhs)
474 }
475 }
476
477 impl std::ops::BitOrAssign for #struct_name {
478 #[inline]
479 fn bitor_assign(&mut self, rhs: Self) {
480 *self = self.intersection(&rhs);
481 }
482 }
483
484 impl std::ops::BitXor for #struct_name {
485 type Output = #struct_name;
486
487 #[inline]
488 fn bitxor(self, rhs: Self) -> Self::Output {
489 self.symmetric_difference(&rhs)
490 }
491 }
492
493 impl std::ops::BitXorAssign for #struct_name {
494 #[inline]
495 fn bitxor_assign(&mut self, rhs: Self) {
496 *self = self.symmetric_difference(&rhs);
497 }
498 }
499
500 impl std::ops::Sub for #struct_name {
501 type Output = #struct_name;
502
503 #[inline]
504 fn sub(self, rhs: Self) -> Self::Output {
505 self.difference(&rhs)
506 }
507 }
508
509 impl std::ops::SubAssign for #struct_name {
510 #[inline]
511 fn sub_assign(&mut self, rhs: Self) {
512 *self = self.difference(&rhs);
513 }
514 }
515
516 impl std::fmt::Debug for #struct_name {
517 #[allow(unused_assignments)]
518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
519 write!(f, "[")?;
520
521 let mut first = true;
522 #(#debug_items)*
523
524 write!(f, "]")
525 }
526 }
527
528 impl<'a> FromIterator<&'a str> for #struct_name {
529 fn from_iter<I>(iter: I) -> Self
530 where I: IntoIterator<Item = &'a str>
531 {
532 let mut extensions = Self::empty();
533 for name in iter {
534 match name {
535 #(#from_str_for_extensions_items)*
536 _ => (),
537 }
538 }
539 extensions
540 }
541 }
542
543 impl<'a> From<&'a #struct_name> for Vec<std::ffi::CString> {
544 fn from(x: &'a #struct_name) -> Self {
545 let mut data = Self::new();
546 #(#from_extensions_for_vec_cstring_items)*
547 data
548 }
549 }
550
551 impl IntoIterator for #struct_name {
552 type Item = (&'static str, bool);
553 type IntoIter = std::array::IntoIter<Self::Item, #arr_len>;
554
555 #[inline]
556 fn into_iter(self) -> Self::IntoIter {
557 [#(#arr_items)*].into_iter()
558 }
559 }
560 }
561 }
562
extensions_members(ty: &str, extensions: &IndexMap<&str, &Extension>) -> Vec<ExtensionsMember>563 fn extensions_members(ty: &str, extensions: &IndexMap<&str, &Extension>) -> Vec<ExtensionsMember> {
564 extensions
565 .values()
566 .filter(|ext| ext.ext_type.as_ref().unwrap() == ty)
567 .map(|ext| {
568 let raw = ext.name.to_owned();
569 let name = raw.strip_prefix("VK_").unwrap().to_snake_case();
570
571 let mut requires = Vec::new();
572
573 if let Some(core) = ext.requires_core.as_ref() {
574 let (major, minor) = core.split_once('.').unwrap();
575 requires.push(RequiresOneOf {
576 api_version: Some((major.to_owned(), minor.to_owned())),
577 ..Default::default()
578 });
579 }
580
581 if let Some(req) = ext.requires.as_ref() {
582 requires.extend(req.split(',').map(|mut vk_name| {
583 let mut dependencies = RequiresOneOf::default();
584
585 loop {
586 if let Some(version) = vk_name.strip_prefix("VK_VERSION_") {
587 let (major, minor) = version.split_once('_').unwrap();
588 dependencies.api_version = Some((major.to_owned(), minor.to_owned()));
589 break;
590 } else {
591 let ident = format_ident!(
592 "{}",
593 vk_name.strip_prefix("VK_").unwrap().to_snake_case()
594 );
595 let extension = extensions[vk_name];
596
597 match extension.ext_type.as_deref() {
598 Some("device") => &mut dependencies.device_extensions,
599 Some("instance") => &mut dependencies.instance_extensions,
600 _ => unreachable!(),
601 }
602 .insert(0, ident);
603
604 if let Some(promotedto) = extension.promotedto.as_ref() {
605 vk_name = promotedto.as_str();
606 } else {
607 break;
608 }
609 }
610 }
611
612 dependencies
613 }));
614 }
615
616 let conflicts_extensions = conflicts_extensions(&ext.name);
617
618 let mut member = ExtensionsMember {
619 name: format_ident!("{}", name),
620 doc: String::new(),
621 raw,
622 required_if_supported: required_if_supported(ext.name.as_str()),
623 requires,
624 conflicts_device_extensions: conflicts_extensions
625 .iter()
626 .filter(|&&vk_name| extensions[vk_name].ext_type.as_ref().unwrap() == "device")
627 .map(|vk_name| {
628 format_ident!("{}", vk_name.strip_prefix("VK_").unwrap().to_snake_case())
629 })
630 .collect(),
631 status: ext
632 .promotedto
633 .as_deref()
634 .and_then(|pr| {
635 if let Some(version) = pr.strip_prefix("VK_VERSION_") {
636 let (major, minor) = version.split_once('_').unwrap();
637 Some(ExtensionStatus::Promoted(Replacement::Core((
638 major.to_owned(),
639 minor.to_owned(),
640 ))))
641 } else {
642 let member = pr.strip_prefix("VK_").unwrap().to_snake_case();
643 match extensions[pr].ext_type.as_ref().unwrap().as_str() {
644 "device" => Some(ExtensionStatus::Promoted(
645 Replacement::DeviceExtension(format_ident!("{}", member)),
646 )),
647 "instance" => Some(ExtensionStatus::Promoted(
648 Replacement::InstanceExtension(format_ident!("{}", member)),
649 )),
650 _ => unreachable!(),
651 }
652 }
653 })
654 .or_else(|| {
655 ext.deprecatedby.as_deref().and_then(|depr| {
656 if depr.is_empty() {
657 Some(ExtensionStatus::Deprecated(None))
658 } else if let Some(version) = depr.strip_prefix("VK_VERSION_") {
659 let (major, minor) = version.split_once('_').unwrap();
660 Some(ExtensionStatus::Deprecated(Some(Replacement::Core((
661 major.parse().unwrap(),
662 minor.parse().unwrap(),
663 )))))
664 } else {
665 let member = depr.strip_prefix("VK_").unwrap().to_snake_case();
666 match extensions[depr].ext_type.as_ref().unwrap().as_str() {
667 "device" => Some(ExtensionStatus::Deprecated(Some(
668 Replacement::DeviceExtension(format_ident!("{}", member)),
669 ))),
670 "instance" => Some(ExtensionStatus::Deprecated(Some(
671 Replacement::InstanceExtension(format_ident!("{}", member)),
672 ))),
673 _ => unreachable!(),
674 }
675 }
676 })
677 }),
678 };
679 make_doc(&mut member);
680 member
681 })
682 .collect()
683 }
684
make_doc(ext: &mut ExtensionsMember)685 fn make_doc(ext: &mut ExtensionsMember) {
686 let writer = &mut ext.doc;
687 write!(writer, "- [Vulkan documentation](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/{}.html)", ext.raw).unwrap();
688
689 if ext.required_if_supported {
690 write!(
691 writer,
692 "\n- Must be enabled if it is supported by the physical device",
693 )
694 .unwrap();
695 }
696
697 if let Some(status) = ext.status.as_ref() {
698 match status {
699 ExtensionStatus::Promoted(replacement) => {
700 write!(writer, "\n- Promoted to ",).unwrap();
701
702 match replacement {
703 Replacement::Core(version) => {
704 write!(writer, "Vulkan {}.{}", version.0, version.1).unwrap();
705 }
706 Replacement::DeviceExtension(ext) => {
707 write!(writer, "[`{}`](crate::device::DeviceExtensions::{0})", ext)
708 .unwrap();
709 }
710 Replacement::InstanceExtension(ext) => {
711 write!(
712 writer,
713 "[`{}`](crate::instance::InstanceExtensions::{0})",
714 ext
715 )
716 .unwrap();
717 }
718 }
719 }
720 ExtensionStatus::Deprecated(replacement) => {
721 write!(writer, "\n- Deprecated ",).unwrap();
722
723 match replacement {
724 Some(Replacement::Core(version)) => {
725 write!(writer, "by Vulkan {}.{}", version.0, version.1).unwrap();
726 }
727 Some(Replacement::DeviceExtension(ext)) => {
728 write!(
729 writer,
730 "by [`{}`](crate::device::DeviceExtensions::{0})",
731 ext
732 )
733 .unwrap();
734 }
735 Some(Replacement::InstanceExtension(ext)) => {
736 write!(
737 writer,
738 "by [`{}`](crate::instance::InstanceExtensions::{0})",
739 ext
740 )
741 .unwrap();
742 }
743 None => {
744 write!(writer, "without a replacement").unwrap();
745 }
746 }
747 }
748 }
749 }
750
751 if !ext.requires.is_empty() {
752 write!(writer, "\n- Requires:").unwrap();
753 }
754
755 for require in &ext.requires {
756 let mut line = Vec::new();
757
758 if let Some((major, minor)) = require.api_version.as_ref() {
759 line.push(format!("Vulkan API version {}.{}", major, minor));
760 }
761
762 line.extend(require.device_extensions.iter().map(|ext| {
763 format!(
764 "device extension [`{}`](crate::device::DeviceExtensions::{0})",
765 ext
766 )
767 }));
768 line.extend(require.instance_extensions.iter().map(|ext| {
769 format!(
770 "instance extension [`{}`](crate::instance::InstanceExtensions::{0})",
771 ext
772 )
773 }));
774
775 if line.len() == 1 {
776 write!(writer, "\n - {}", line[0]).unwrap();
777 } else {
778 write!(writer, "\n - One of: {}", line.join(", ")).unwrap();
779 }
780 }
781
782 if !ext.conflicts_device_extensions.is_empty() {
783 let links: Vec<_> = ext
784 .conflicts_device_extensions
785 .iter()
786 .map(|ext| format!("[`{}`](crate::device::DeviceExtensions::{0})", ext))
787 .collect();
788 write!(
789 writer,
790 "\n- Conflicts with device extension{}: {}",
791 if ext.conflicts_device_extensions.len() > 1 {
792 "s"
793 } else {
794 ""
795 },
796 links.join(", ")
797 )
798 .unwrap();
799 }
800 }
801