• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2#
3# Copyright (c) 2019 Collabora, Ltd.
4#
5# SPDX-License-Identifier: Apache-2.0
6#
7# Author(s):    Ryan Pavlik <ryan.pavlik@collabora.com>
8#
9# Purpose:      This script checks some "business logic" in the XML registry.
10
11import argparse
12import re
13import sys
14from pathlib import Path
15from collections import defaultdict, deque, namedtuple
16
17from check_spec_links import VulkanEntityDatabase as OrigEntityDatabase
18from reg import Registry
19from spec_tools.consistency_tools import XMLChecker
20from spec_tools.util import findNamedElem, getElemName, getElemType
21from apiconventions import APIConventions
22from parse_dependency import dependencyNames
23
24# These are extensions which do not follow the usual naming conventions,
25# specifying the alternate convention they follow
26EXTENSION_ENUM_NAME_SPELLING_CHANGE = {
27    'VK_EXT_swapchain_colorspace': 'VK_EXT_SWAPCHAIN_COLOR_SPACE',
28}
29
30# These are extensions whose names *look* like they end in version numbers,
31# but do not
32EXTENSION_NAME_VERSION_EXCEPTIONS = (
33    'VK_AMD_gpu_shader_int16',
34    'VK_EXT_index_type_uint8',
35    'VK_EXT_shader_image_atomic_int64',
36    'VK_KHR_video_decode_h264',
37    'VK_KHR_video_decode_h265',
38    'VK_KHR_video_encode_h264',
39    'VK_KHR_video_encode_h265',
40    'VK_KHR_external_fence_win32',
41    'VK_KHR_external_memory_win32',
42    'VK_KHR_external_semaphore_win32',
43    'VK_KHR_shader_atomic_int64',
44    'VK_KHR_shader_float16_int8',
45    'VK_KHR_spirv_1_4',
46    'VK_NV_external_memory_win32',
47    'VK_RESERVED_do_not_use_146',
48    'VK_RESERVED_do_not_use_94',
49)
50
51# These are APIs which can be required by an extension despite not having
52# suffixes matching the vendor ID of that extension.
53# Most are external types.
54# We could make this an (extension name, api name) set to be more specific.
55EXTENSION_API_NAME_EXCEPTIONS = {
56    'AHardwareBuffer',
57    'ANativeWindow',
58    'CAMetalLayer',
59    'IOSurfaceRef',
60    'MTLBuffer_id',
61    'MTLCommandQueue_id',
62    'MTLDevice_id',
63    'MTLSharedEvent_id',
64    'MTLTexture_id',
65    'VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE',
66    'VkFlags64',
67    'VkPipelineCacheCreateFlagBits',
68    'VkPipelineColorBlendStateCreateFlagBits',
69    'VkPipelineDepthStencilStateCreateFlagBits',
70    'VkPipelineLayoutCreateFlagBits',
71}
72
73# These are APIs which contain _RESERVED_ intentionally
74EXTENSION_NAME_RESERVED_EXCEPTIONS = {
75    'VK_STRUCTURE_TYPE_PRIVATE_VENDOR_INFO_RESERVED_OFFSET_0_NV'
76}
77
78# Exceptions to pointer parameter naming rules
79# Keyed by (entity name, type, name).
80CHECK_PARAM_POINTER_NAME_EXCEPTIONS = {
81    ('vkGetDrmDisplayEXT', 'VkDisplayKHR', 'display') : None,
82}
83
84# Exceptions to pNext member requiring an optional attribute
85CHECK_MEMBER_PNEXT_OPTIONAL_EXCEPTIONS = (
86    'VkVideoEncodeInfoKHR',
87    'VkVideoEncodeRateControlLayerInfoKHR',
88)
89
90# Exceptions to VK_INCOMPLETE being required for, and only applicable to, array
91# enumeration functions
92CHECK_ARRAY_ENUMERATION_RETURN_CODE_EXCEPTIONS = (
93    'vkGetDeviceFaultInfoEXT',
94    'vkEnumerateDeviceLayerProperties',
95)
96
97# Exceptions to unknown structure type constants.
98# This is most likely an error in this script, not the XML.
99# It does not understand Vulkan SC (alternate 'api') types.
100CHECK_TYPE_STYPE_EXCEPTIONS = (
101    'VK_STRUCTURE_TYPE_PERFORMANCE_QUERY_RESERVATION_INFO_KHR',
102    'VK_STRUCTURE_TYPE_PIPELINE_POOL_SIZE',
103    'VK_STRUCTURE_TYPE_FAULT_DATA',
104    'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_SC_1_0_FEATURES',
105    'VK_STRUCTURE_TYPE_DEVICE_OBJECT_RESERVATION_CREATE_INFO',
106    'VK_STRUCTURE_TYPE_PIPELINE_OFFLINE_CREATE_INFO',
107    'VK_STRUCTURE_TYPE_FAULT_CALLBACK_INFO',
108    'VK_STRUCTURE_TYPE_COMMAND_POOL_MEMORY_RESERVATION_CREATE_INFO',
109    'VK_STRUCTURE_TYPE_DEVICE_SEMAPHORE_SCI_SYNC_POOL_RESERVATION_CREATE_INFO_NV',
110    'VK_STRUCTURE_TYPE_COMMAND_POOL_MEMORY_CONSUMPTION',
111    'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_SC_1_0_PROPERTIES',
112)
113
114def get_extension_commands(reg):
115    extension_cmds = set()
116    for ext in reg.extensions:
117        for cmd in ext.findall('./require/command[@name]'):
118            extension_cmds.add(cmd.get('name'))
119    return extension_cmds
120
121
122def get_enum_value_names(reg, enum_type):
123    names = set()
124    result_elem = reg.groupdict[enum_type].elem
125    for val in result_elem.findall('./enum[@name]'):
126        names.add(val.get('name'))
127    return names
128
129
130# Regular expression matching an extension name ending in a (possible) version number
131EXTNAME_RE = re.compile(r'(?P<base>(\w+[A-Za-z]))(?P<version>\d+)')
132
133DESTROY_PREFIX = 'vkDestroy'
134TYPEENUM = 'VkStructureType'
135
136SPECIFICATION_DIR = Path(__file__).parent.parent
137REVISION_RE = re.compile(r' *[*] Revision (?P<num>[1-9][0-9]*),.*')
138
139
140def get_extension_source(extname):
141    fn = f'{extname}.adoc'
142    return str(SPECIFICATION_DIR / 'appendices' / fn)
143
144
145class EntityDatabase(OrigEntityDatabase):
146
147    def __init__(self, args):
148        """Retain command-line arguments for later use in makeRegistry"""
149        self.args = args
150
151        super().__init__()
152
153    # Override base class method to not exclude 'disabled' extensions
154    def getExclusionSet(self):
155        """Return a set of "support=" attribute strings that should not be included in the database.
156
157        Called only during construction."""
158
159        return set(())
160
161    def makeRegistry(self):
162        try:
163            import lxml.etree as etree
164            HAS_LXML = True
165        except ImportError:
166            HAS_LXML = False
167        if not HAS_LXML:
168            return super().makeRegistry()
169
170        if len(self.args.files) > 0:
171            registryFile = self.args.files[0]
172        else:
173            registryFile = str(SPECIFICATION_DIR / 'xml/vk.xml')
174
175        registry = Registry()
176        registry.filename = registryFile
177        registry.loadElementTree(etree.parse(registryFile))
178        return registry
179
180
181class Checker(XMLChecker):
182    def __init__(self, args):
183        manual_types_to_codes = {
184            # These are hard-coded "manual" return codes:
185            # the codes of the value (string, list, or tuple)
186            # are available for a command if-and-only-if
187            # the key type is passed as an input.
188            'VkFormat': 'VK_ERROR_FORMAT_NOT_SUPPORTED'
189        }
190        forward_only = {
191            # Like the above, but these are only valid in the
192            # "type implies return code" direction
193        }
194        reverse_only = {
195            # like the above, but these are only valid in the
196            # "return code implies type or its descendant" direction
197            # "XrDuration": "XR_TIMEOUT_EXPIRED"
198        }
199        # Some return codes are related in that only one of a set
200        # may be returned by a command
201        # (eg. XR_ERROR_SESSION_RUNNING and XR_ERROR_SESSION_NOT_RUNNING)
202        self.exclusive_return_code_sets = tuple(
203            # set(("XR_ERROR_SESSION_NOT_RUNNING", "XR_ERROR_SESSION_RUNNING")),
204        )
205
206        # This is used to report collisions.
207        conventions = APIConventions()
208        db = EntityDatabase(args)
209
210        self.extension_cmds = get_extension_commands(db.registry)
211        self.return_codes = get_enum_value_names(db.registry, 'VkResult')
212        self.structure_types = get_enum_value_names(db.registry, TYPEENUM)
213
214        # Dict of entity name to a list of messages to suppress. (Exclude any context data and "Warning:"/"Error:")
215        # Keys are entity names, values are tuples or lists of message text to suppress.
216        suppressions = {}
217
218        # Structures explicitly allowed to have 'limittype' attributes
219        self.allowedStructs = set((
220            'VkFormatProperties',
221            'VkFormatProperties2',
222            'VkPhysicalDeviceProperties',
223            'VkPhysicalDeviceProperties2',
224            'VkPhysicalDeviceLimits',
225            'VkQueueFamilyProperties',
226            'VkQueueFamilyProperties2',
227            'VkSparseImageFormatProperties',
228            'VkSparseImageFormatProperties2',
229        ))
230
231        # Substructures of allowed structures. This can be found by looking
232        # at tags, but there are so few cases that it is hardwired for now.
233        self.nestedStructs = set((
234            'VkPhysicalDeviceLimits',
235            'VkPhysicalDeviceSparseProperties',
236            'VkPhysicalDeviceProperties',
237            'VkQueueFamilyProperties',
238            'VkSparseImageFormatProperties',
239        ))
240
241        # Structures all of whose (non pNext/sType) members are required to
242        # have 'limittype' attributes, as are their descendants
243        self.requiredStructs = set((
244            'VkPhysicalDeviceProperties',
245            'VkPhysicalDeviceProperties2',
246            'VkPhysicalDeviceLimits',
247            'VkSparseImageFormatProperties',
248            'VkSparseImageFormatProperties2',
249        ))
250
251        # Structures which have already have their limittype attributes validated
252        self.validatedLimittype = set()
253
254        # Initialize superclass
255        super().__init__(entity_db=db, conventions=conventions,
256                         manual_types_to_codes=manual_types_to_codes,
257                         forward_only_types_to_codes=forward_only,
258                         reverse_only_types_to_codes=reverse_only,
259                         suppressions=suppressions,
260                         display_warnings=args.warn)
261
262    def check(self):
263        """Extends base class behavior with additional checks"""
264
265        # This test is not run on a per-structure basis, but loops over
266        # specific structures
267        self.check_type_required_limittype()
268
269        super().check()
270
271    def check_command(self, name, info):
272        """Extends base class behavior with additional checks"""
273
274        if name[0:5] == 'vkCmd':
275            if info.elem.get('tasks') is None:
276                self.record_error(f'{name} is a vkCmd* command, but is missing a "tasks" attribute')
277
278        super().check_command(name, info)
279
280    def check_command_return_codes_basic(self, name, info,
281                                         successcodes, errorcodes):
282        """Check a command's return codes for consistency.
283
284        Called on every command."""
285        # Check that all extension commands can return the code associated
286        # with trying to use an extension that was not enabled.
287        # if name in self.extension_cmds and UNSUPPORTED not in errorcodes:
288        #     self.record_error('Missing expected return code',
289        #                       UNSUPPORTED,
290        #                       'implied due to being an extension command')
291
292        codes = successcodes.union(errorcodes)
293
294        # Check that all return codes are recognized.
295        unrecognized = codes - self.return_codes
296        if unrecognized:
297            self.record_error('Unrecognized return code(s):',
298                              unrecognized)
299
300        elem = info.elem
301        params = [(getElemName(elt), elt) for elt in elem.findall('param')]
302
303        def is_count_output(name, elt):
304            # Must end with Count or Size,
305            # not be const,
306            # and be a pointer (detected by naming convention)
307            return (name.endswith('Count') or name.endswith('Size')) \
308                and (elt.tail is None or 'const' not in elt.tail) \
309                and (name.startswith('p'))
310
311        countParams = [elt
312                       for name, elt in params
313                       if is_count_output(name, elt)]
314        if countParams:
315            assert(len(countParams) == 1)
316            if 'VK_INCOMPLETE' not in successcodes:
317                message = "Apparent enumeration of an array without VK_INCOMPLETE in successcodes for command {}.".format(name)
318                if name in CHECK_ARRAY_ENUMERATION_RETURN_CODE_EXCEPTIONS:
319                    self.record_warning('(Allowed exception)', message)
320                else:
321                    self.record_error(message)
322
323        elif 'VK_INCOMPLETE' in successcodes:
324            message = "VK_INCOMPLETE in successcodes of command {} that is apparently not an array enumeration.".format(name)
325            if name in CHECK_ARRAY_ENUMERATION_RETURN_CODE_EXCEPTIONS:
326                self.record_warning('(Allowed exception)', message)
327            else:
328                self.record_error(message)
329
330    def check_param(self, param):
331        """Check a member of a struct or a param of a function.
332
333        Called from check_params."""
334        super().check_param(param)
335
336        if not self.is_api_type(param):
337            return
338
339        param_text = ''.join(param.itertext())
340        param_name = getElemName(param)
341
342        # Make sure the number of leading 'p' matches the pointer count.
343        pointercount = param.find('type').tail
344        if pointercount:
345            pointercount = pointercount.count('*')
346        if pointercount:
347            prefix = 'p' * pointercount
348            if not param_name.startswith(prefix):
349                param_type = param.find('type').text
350                message = "Apparently incorrect pointer-related name prefix for {} - expected it to start with '{}'".format(
351                    param_text, prefix)
352                if (self.entity, param_type, param_name) in CHECK_PARAM_POINTER_NAME_EXCEPTIONS:
353                    self.record_warning('(Allowed exception)', message, elem=param)
354                else:
355                    self.record_error(message, elem=param)
356
357        # Make sure no members have optional="false" attributes
358        optional = param.get('optional')
359        if optional == 'false':
360            message = f'{self.entity}.{param_name} member has disallowed \'optional="false"\' attribute (remove this attribute)'
361            self.record_error(message, elem=param)
362
363        # Make sure pNext members have optional="true" attributes
364        if param_name == self.conventions.nextpointer_member_name:
365            optional = param.get('optional')
366            if optional is None or optional != 'true':
367                message = f'{self.entity}.pNext member is missing \'optional="true"\' attribute'
368                if self.entity in CHECK_MEMBER_PNEXT_OPTIONAL_EXCEPTIONS:
369                    self.record_warning('(Allowed exception)', message, elem=param)
370                else:
371                    self.record_error(message, elem=param)
372
373    def check_type_stype(self, name, info, type_elts):
374        """Check a struct type's sType member"""
375        if len(type_elts) > 1:
376            self.record_error(
377                'Have more than one member of type', TYPEENUM)
378        else:
379            type_elt = type_elts[0]
380            val = type_elt.get('values')
381            if val and val not in self.structure_types:
382                message = f'{self.entity} has unknown structure type constant {val}'
383                if val in CHECK_TYPE_STYPE_EXCEPTIONS:
384                    self.record_warning('(Allowed exception)', message)
385                else:
386                    self.record_error(message)
387
388    def check_type_pnext(self, name, info):
389        """Check a struct type's pNext member, if present"""
390
391        next_name = self.conventions.nextpointer_member_name
392        next_member = findNamedElem(info.elem.findall('member'), next_name)
393        if next_member is not None:
394            # Ensure that the 'optional' attribute is set to 'true'
395            optional = next_member.get('optional')
396            if optional is None or optional != 'true':
397                message = f'{name}.{next_name} member is missing \'optional="true"\' attribute'
398                if name in CHECK_MEMBER_PNEXT_OPTIONAL_EXCEPTIONS:
399                    self.record_warning('(Allowed exception)', message)
400                else:
401                    self.record_error(message)
402
403    def __isLimittypeStruct(self, name, info, allowedStructs):
404        """Check if a type element is a structure allowed to have 'limittype' attributes
405           name - name of a structure
406           info - corresponding TypeInfo object
407           allowedStructs - set of struct names explicitly allowed"""
408
409        # Is this an explicitly allowed struct?
410        if name in allowedStructs:
411            return True
412
413        # Is this a struct extending an explicitly allowed struct?
414        extends = info.elem.get('structextends')
415        if extends is not None:
416            # See if any name in the structextends attribute is an allowed
417            # struct
418            if len(set(extends.split(',')) & allowedStructs) > 0:
419                return True
420
421        return False
422
423    def __validateStructLimittypes(self, name, info, requiredLimittype):
424        """Validate 'limittype' attributes for a single struct.
425           info - TypeInfo for a struct <type>
426           requiredLimittype - True if members *must* have a limittype"""
427
428        # Do not re-check structures
429        if name in self.validatedLimittype:
430            return {}
431        self.validatedLimittype.add(name)
432
433        limittypeDiags = namedtuple('limittypeDiags', ['missing', 'invalid'])
434        badFields = defaultdict(lambda : limittypeDiags(missing=[], invalid=[]))
435        validLimittypes = { 'min', 'max', 'not', 'pot', 'mul', 'bits', 'bitmask', 'range', 'struct', 'exact', 'noauto' }
436        for member in info.getMembers():
437            memberName = member.findtext('name')
438            if memberName in ['sType', 'pNext']:
439                continue
440            limittype = member.get('limittype')
441            if limittype is None:
442                # Do not tag this as missing if it is not required
443                if requiredLimittype:
444                    badFields[info.elem.get('name')].missing.append(memberName)
445            elif limittype == 'struct':
446                typeName = member.findtext('type')
447                memberType = self.reg.typedict[typeName]
448                badFields.update(self.__validateStructLimittypes(typeName, memberType, requiredLimittype))
449            else:
450                for value in limittype.split(','):
451                    if value not in validLimittypes:
452                        badFields[info.elem.get('name')].invalid.append(memberName)
453
454        return badFields
455
456    def check_type_disallowed_limittype(self, name, info):
457        """Check if a struct type's members cannot have the 'limittype' attribute"""
458
459        # If not allowed to have limittypes, verify this for each member
460        if not self.__isLimittypeStruct(name, info, self.allowedStructs.union(self.nestedStructs)):
461            for member in info.getMembers():
462                if member.get('limittype') is not None:
463                    memname = member.findtext('name')
464                    self.record_error(f'{name} member {memname} has disallowed limittype attribute')
465
466    def check_type_optional_value(self, name, info):
467        """Check if a struct type's members have disallowed 'optional' attribute values"""
468
469        for member in info.getMembers():
470            # Make sure no members have optional="false" attributes
471            optional = member.get('optional')
472            if optional == 'false':
473                memname = member.findtext('name')
474                message = f'{name} member {memname} has disallowed \'optional="false"\' attribute (remove this attribute)'
475                self.record_error(message, elem=member)
476
477    def check_type_required_limittype(self):
478        """Check struct type members which must have the 'limittype' attribute
479
480        Called from check."""
481
482        for name in self.allowedStructs:
483            # Assume that only extending structs of structs explicitly
484            # requiring limittypes also require them
485            requiredLimittype = (name in self.requiredStructs)
486
487            info = self.reg.typedict[name]
488
489            self.set_error_context(entity=name, elem=info.elem)
490
491            badFields = self.__validateStructLimittypes(name, info, requiredLimittype)
492            for extendingStructName in self.reg.validextensionstructs[name]:
493                extendingStruct = self.reg.typedict[extendingStructName]
494                badFields.update(self.__validateStructLimittypes(extendingStructName, extendingStruct, requiredLimittype))
495
496            if badFields:
497                for key in sorted(badFields.keys()):
498                    diags = badFields[key]
499                    if diags.missing:
500                        self.record_error(f'{name} missing limittype for members {", ".join(badFields[key].missing)}')
501                    if diags.invalid:
502                        self.record_error(f'{name} has invalid limittype for members {", ".join(badFields[key].invalid)}')
503
504    def check_type(self, name, info, category):
505        """Check a type's XML data for consistency.
506
507        Called from check."""
508
509        if category == 'struct':
510            type_elts = [elt
511                         for elt in info.elem.findall('member')
512                         if getElemType(elt) == TYPEENUM]
513
514            if type_elts:
515                self.check_type_stype(name, info, type_elts)
516                self.check_type_pnext(name, info)
517
518            # Check for disallowed limittypes on all structures
519            self.check_type_disallowed_limittype(name, info)
520
521            # Check for disallowed 'optional' values
522            self.check_type_optional_value(name, info)
523        elif category == 'bitmask':
524            if 'Flags' in name:
525                expected_require = name.replace('Flags', 'FlagBits')
526                require = info.elem.get('require')
527                if require is not None and expected_require != require:
528                    self.record_error('Unexpected require attribute value:',
529                                      'got', require,
530                                      'but expected', expected_require)
531        super().check_type(name, info, category)
532
533    def check_suffixes(self, name, info, supported, name_exceptions):
534        """Check suffixes of new APIs required by an extension, which should
535           match the author ID of the extension.
536
537           Called from check_extension.
538
539           name - extension name
540           info - extdict entry for name
541           supported - True if extension supported by API being checked
542           name_exceptions - set of API names to not check, in addition to
543                             the global exception list."""
544
545        def has_suffix(apiname, author):
546            return apiname[-len(author):] == author
547
548        def has_any_suffixes(apiname, authors):
549            for author in authors:
550                if has_suffix(apiname, author):
551                    return True
552            return False
553
554        def check_names(elems, author, alt_authors, name_exceptions):
555            """Check names in a list of elements for consistency
556
557               elems - list of elements to check
558               author - author ID of the <extension> tag
559               alt_authors - set of other allowed author IDs
560               name_exceptions - additional set of allowed exceptions"""
561
562            for elem in elems:
563                apiname = elem.get('name', 'NO NAME ATTRIBUTE')
564                suffix = apiname[-len(author):]
565
566                if (not has_suffix(apiname, author) and
567                    apiname not in EXTENSION_API_NAME_EXCEPTIONS and
568                    apiname not in name_exceptions):
569
570                    msg = f'Extension {name} <{elem.tag}> {apiname} does not have expected suffix {author}'
571
572                    # Explicit 'aliased' deprecations not matching the
573                    # naming rules are allowed, but warned.
574                    if has_any_suffixes(apiname, alt_authors):
575                        self.record_warning('Allowed alternate author ID:', msg)
576                    elif not supported:
577                        self.record_warning('Allowed inconsistency for disabled extension:', msg)
578                    elif elem.get('deprecated') == 'aliased':
579                        self.record_warning('Allowed aliasing deprecation:', msg)
580                    else:
581                        msg += '\n\
582This may be due to an extension interaction not having the correct <require depends="">\n\
583Other exceptions can be added to xml_consistency.py:EXTENSION_API_NAME_EXCEPTIONS'
584                        self.record_error(msg)
585
586        elem = info.elem
587
588        self.set_error_context(entity=name, elem=elem)
589
590        # Extract author ID from the extension name.
591        author = name.split('_')[1]
592
593        # Loop over each <require> tag checking the API name suffixes in
594        # that tag for consistency.
595        # Names in tags whose 'depends' attribute includes extensions with
596        # different author IDs may be suffixed with those IDs.
597        for req_elem in elem.findall('./require'):
598            depends = req_elem.get('depends', '')
599            alt_authors = set()
600            if len(depends) > 0:
601                for name in dependencyNames(depends):
602                    # Skip core versions
603                    if name[0:11] != 'VK_VERSION_':
604                        # Extract author ID from extension name
605                        id = name.split('_')[1]
606                        alt_authors.add(id)
607
608            check_names(req_elem.findall('./command'), author, alt_authors, name_exceptions)
609            check_names(req_elem.findall('./type'), author, alt_authors, name_exceptions)
610            check_names(req_elem.findall('./enum'), author, alt_authors, name_exceptions)
611
612    def check_extension(self, name, info, supported):
613        """Check an extension's XML data for consistency.
614
615        Called from check."""
616
617        elem = info.elem
618        enums = elem.findall('./require/enum[@name]')
619
620        # If extension name is not on the exception list and matches the
621        # versioned-extension pattern, map the extension name to the version
622        # name with the version as a separate word. Otherwise just map it to
623        # the upper-case version of the extension name.
624
625        matches = EXTNAME_RE.fullmatch(name)
626        ext_versioned_name = False
627        if name in EXTENSION_ENUM_NAME_SPELLING_CHANGE:
628            ext_enum_name = EXTENSION_ENUM_NAME_SPELLING_CHANGE.get(name)
629        elif matches is None or name in EXTENSION_NAME_VERSION_EXCEPTIONS:
630            # This is the usual case, either a name that does not look
631            # versioned, or one that does but is on the exception list.
632            ext_enum_name = name.upper()
633        else:
634            # This is a versioned extension name.
635            # Treat the version number as a separate word.
636            base = matches.group('base')
637            version = matches.group('version')
638            ext_enum_name = base.upper() + '_' + version
639            # Keep track of this case
640            ext_versioned_name = True
641
642        # Look for the expected SPEC_VERSION token name
643        version_name = f'{ext_enum_name}_SPEC_VERSION'
644        version_elem = findNamedElem(enums, version_name)
645
646        if version_elem is None:
647            # Did not find a SPEC_VERSION enum matching the extension name
648            if ext_versioned_name:
649                suffix = '\n\
650    Make sure that trailing version numbers in extension names are treated\n\
651    as separate words in extension enumerant names. If this is an extension\n\
652    whose name ends in a number which is not a version, such as "...h264"\n\
653    or "...int16", add it to EXTENSION_NAME_VERSION_EXCEPTIONS in\n\
654    scripts/xml_consistency.py.'
655            else:
656                suffix = ''
657            self.record_error(f'Missing version enum {version_name}{suffix}')
658        elif supported:
659            # Skip unsupported / disabled extensions for these checks
660
661            fn = get_extension_source(name)
662            revisions = []
663            with open(fn, 'r', encoding='utf-8') as fp:
664                for line in fp:
665                    line = line.rstrip()
666                    match = REVISION_RE.match(line)
667                    if match:
668                        revisions.append(int(match.group('num')))
669            ver_from_xml = version_elem.get('value')
670            if revisions:
671                ver_from_text = str(max(revisions))
672                if ver_from_xml != ver_from_text:
673                    self.record_error('Version enum mismatch: spec text indicates', ver_from_text,
674                                      'but XML says', ver_from_xml)
675            else:
676                if ver_from_xml == '1':
677                    self.record_warning(
678                        "Cannot find version history in spec text - make sure it has lines starting exactly like '  * Revision 1, ....'",
679                        filename=fn)
680                else:
681                    self.record_warning("Cannot find version history in spec text, but XML reports a non-1 version number", ver_from_xml,
682                                        " - make sure the spec text has lines starting exactly like '  * Revision 1, ....'",
683                                        filename=fn)
684
685            for enum in enums:
686                enum_name = enum.get('name')
687                if '_RESERVED_' in enum_name and enum_name not in EXTENSION_NAME_RESERVED_EXCEPTIONS:
688                    self.record_error(enum_name, 'should not contain _RESERVED_ for a supported extension.\n\
689If this is intentional, add it to EXTENSION_NAME_RESERVED_EXCEPTIONS in scripts/xml_consistency.py.')
690
691        name_define = f'{ext_enum_name}_EXTENSION_NAME'
692        name_elem = findNamedElem(enums, name_define)
693        if name_elem is None:
694            self.record_error('Missing name enum', name_define)
695        else:
696            # Note: etree handles the XML entities here and turns &quot; back into "
697            expected_name = f'"{name}"'
698            name_val = name_elem.get('value')
699            if name_val != expected_name:
700                self.record_error('Incorrect name enum: expected', expected_name,
701                                  'got', name_val)
702
703        self.check_suffixes(name, info, supported, { version_name, name_define })
704
705        # More general checks
706        super().check_extension(name, info, supported)
707
708    def check_format(self):
709        """Check an extension's XML data for consistency.
710
711        Called from check."""
712
713        astc3d_formats = [
714                'VK_FORMAT_ASTC_3x3x3_UNORM_BLOCK_EXT',
715                'VK_FORMAT_ASTC_3x3x3_SRGB_BLOCK_EXT',
716                'VK_FORMAT_ASTC_3x3x3_SFLOAT_BLOCK_EXT',
717                'VK_FORMAT_ASTC_4x3x3_UNORM_BLOCK_EXT',
718                'VK_FORMAT_ASTC_4x3x3_SRGB_BLOCK_EXT',
719                'VK_FORMAT_ASTC_4x3x3_SFLOAT_BLOCK_EXT',
720                'VK_FORMAT_ASTC_4x4x3_UNORM_BLOCK_EXT',
721                'VK_FORMAT_ASTC_4x4x3_SRGB_BLOCK_EXT',
722                'VK_FORMAT_ASTC_4x4x3_SFLOAT_BLOCK_EXT',
723                'VK_FORMAT_ASTC_4x4x4_UNORM_BLOCK_EXT',
724                'VK_FORMAT_ASTC_4x4x4_SRGB_BLOCK_EXT',
725                'VK_FORMAT_ASTC_4x4x4_SFLOAT_BLOCK_EXT',
726                'VK_FORMAT_ASTC_5x4x4_UNORM_BLOCK_EXT',
727                'VK_FORMAT_ASTC_5x4x4_SRGB_BLOCK_EXT',
728                'VK_FORMAT_ASTC_5x4x4_SFLOAT_BLOCK_EXT',
729                'VK_FORMAT_ASTC_5x5x4_UNORM_BLOCK_EXT',
730                'VK_FORMAT_ASTC_5x5x4_SRGB_BLOCK_EXT',
731                'VK_FORMAT_ASTC_5x5x4_SFLOAT_BLOCK_EXT',
732                'VK_FORMAT_ASTC_5x5x5_UNORM_BLOCK_EXT',
733                'VK_FORMAT_ASTC_5x5x5_SRGB_BLOCK_EXT',
734                'VK_FORMAT_ASTC_5x5x5_SFLOAT_BLOCK_EXT',
735                'VK_FORMAT_ASTC_6x5x5_UNORM_BLOCK_EXT',
736                'VK_FORMAT_ASTC_6x5x5_SRGB_BLOCK_EXT',
737                'VK_FORMAT_ASTC_6x5x5_SFLOAT_BLOCK_EXT',
738                'VK_FORMAT_ASTC_6x6x5_UNORM_BLOCK_EXT',
739                'VK_FORMAT_ASTC_6x6x5_SRGB_BLOCK_EXT',
740                'VK_FORMAT_ASTC_6x6x5_SFLOAT_BLOCK_EXT',
741                'VK_FORMAT_ASTC_6x6x6_UNORM_BLOCK_EXT',
742                'VK_FORMAT_ASTC_6x6x6_SRGB_BLOCK_EXT',
743                'VK_FORMAT_ASTC_6x6x6_SFLOAT_BLOCK_EXT'
744        ]
745
746        # Need to build list of formats from rest of <enums>
747        enum_formats = []
748        for enum in self.reg.groupdict['VkFormat'].elem:
749            if enum.get('alias') is None and enum.get('name') != 'VK_FORMAT_UNDEFINED':
750                enum_formats.append(enum.get('name'))
751
752        found_formats = []
753        for name, info in self.reg.formatsdict.items():
754            found_formats.append(name)
755            self.set_error_context(entity=name, elem=info.elem)
756
757            if name not in enum_formats:
758                self.record_error('The <format> has no matching <enum> for', name)
759
760            # Check never just 1 plane
761            plane_elems = info.elem.findall('plane')
762            if len(plane_elems) == 1:
763                self.record_error('The <format> has only 1 <plane> for', name)
764
765            valid_chroma = ['420', '422', '444']
766            if info.elem.get('chroma') and info.elem.get('chroma') not in valid_chroma:
767                self.record_error('The <format> has chroma is not a valid value for', name)
768
769            # The formatsgenerator.py assumes only 1 <spirvimageformat> tag.
770            # If this changes in the future, remove this warning and update generator script
771            spirv_image_format = info.elem.findall('spirvimageformat')
772            if len(spirv_image_format) > 1:
773                self.record_error('More than 1 <spirvimageformat> but formatsgenerator.py is not updated, for format', name)
774
775        # Re-loop to check the other way if the <format> is missing
776        for enum in self.reg.groupdict['VkFormat'].elem:
777            name = enum.get('name')
778            if enum.get('alias') is None and name != 'VK_FORMAT_UNDEFINED':
779                if name not in found_formats and name not in astc3d_formats:
780                    self.set_error_context(entity=name, elem=enum)
781                    self.record_error('The <enum> has no matching <format> for ', name)
782
783        super().check_format()
784
785        # This should be called from check() but as a first pass, do it here
786        # Check for invalid version names in e.g.
787        #    <enable version="VK_VERSION_1_2"/>
788        # Could also consistency check struct / extension tags here
789        for capname in self.reg.spirvcapdict:
790            for elem in self.reg.spirvcapdict[capname].elem.findall('enable'):
791                version = elem.get('version')
792                if version is not None and version not in self.reg.apidict:
793                    self.set_error_context(entity=capname, elem=elem)
794                    self.record_error(f'<spirvcapability> {capname} enabled by a nonexistent version {version}')
795
796if __name__ == '__main__':
797
798    parser = argparse.ArgumentParser()
799    parser.add_argument('-warn', action='store_true',
800                        help='Enable display of warning messages')
801    parser.add_argument('files', metavar='filename', nargs='*',
802                        help='XML filename to check')
803
804    args = parser.parse_args()
805
806    ckr = Checker(args)
807    ckr.check()
808
809    if ckr.fail:
810        sys.exit(1)
811