Vulkan-Docs/scripts/validitygenerator.py
Jon Leech 476e3f422d Change log for March 18, 2019 Vulkan 1.1.104 spec update:
* Update release number to 104.

Public Issues:

  * Remove the incorrect line from "`Initial`" to "`Invalid`" state in the
    <<commandbuffer-lifecycle-diagram, Lifecycle of a command buffer>>
    diagram (public issue 881).
  * Add Fuchsia platform to <<boilerplate-wsi-header-table, Window System
    Extensions and Headers>> table (public pull request 933).
  * Change the type of
    slink:VkBufferDeviceAddressCreateInfoEXT::pname:deviceAddress from
    basetype:VkDeviceSize to basetype:VkDeviceAddress. These are both
    typedefs of code:uint64_t, so it is an ABI-compatible change (public
    issue 934).

Internal Issues:

  * Remove generated header files and update the CI tests to build a copy of
    the headers for use by the hpp-generate / hpp-compile CI stages. Targets
    to generate the headers will not be removed, but keeping these generated
    files in the repository increased the frequency of conflicts between
    branches when merging to master (internal issue 745).
  * Reword "`undefined: behavior if *action*" to "`must: not do *action*`"
    in the places the old terminology was used, and add a new
    <<writing-undefined, Describing Undefined Behavior>> section of the
    style guide to explain how to write such language in the future
    (internal issue 1579).
  * Move almost all Python scripts into the toplevel `scripts/` directory.
    Apply extensive internal edits to clean up and simplify the scripts, and
    try to follow PEP8 guidelines. Generalize the scripts with the use of a
    Conventions object controlling many aspects of output generation, to
    enable their use in other Khronos projects with similar requirements.
    Autogenerate extension interface refpages (these are experimental and
    may be retired going forward).

New Extensions:

  * `VK_AMD_display_native_hdr`
  * `VK_EXT_full_screen_exclusive` (internal issue 1439)
  * `VK_EXT_host_query_reset`
  * `VK_EXT_pipeline_creation_feedback` (internal issue 1560)
  * `VK_KHR_surface_protected_capabilities` (internal issue 1520)
2019-03-17 06:05:46 -07:00

1416 lines
59 KiB
Python

#!/usr/bin/python3 -i
#
# Copyright (c) 2013-2019 The Khronos Group Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pdb
import re
import sys
from collections import OrderedDict, namedtuple
from functools import reduce
from generator import OutputGenerator, write
class UnhandledCaseError(RuntimeError):
def __init__(self):
super().__init__('Got a case in the validity generator that we have not explicitly handled.')
def _genericIterateIntersection(a, b):
"""Iterate through all elements in a that are also in b.
Somewhat like a set's intersection(),
but not type-specific so it can work with OrderedDicts, etc.
It also returns a generator instead of a set,
so you can pick what container type you'd like,
if any.
"""
return (x for x in a if x in b)
def _make_ordered_dict(gen):
"""Make an ordered dict (with None as the values) from a generator."""
return OrderedDict(((x, None) for x in gen))
def _orderedDictIntersection(a, b):
return _make_ordered_dict(_genericIterateIntersection(a, b))
def _genericIsDisjoint(a, b):
"""Return true if nothing in a is also in b.
Like a set's is_disjoint(),
but not type-specific so it can work with OrderedDicts, etc.
"""
for _ in _genericIterateIntersection(a, b):
return False
# if we never enter the loop...
return True
class LengthEntry:
"""An entry in a (comma-separated) len attribute"""
def __init__(self, val):
self.other_param_name = None
self.null_terminated = False
self.number = None
if val == 'null-terminated':
self.null_terminated = True
return
if val.isdigit():
self.number = int(val)
return
# Must be another param name.
self.other_param_name = val
@staticmethod
def parse_len_from_param(param):
"""Get a list of LengthEntry."""
return [LengthEntry(elt) for elt in param.get('len').split(',')]
def _getElemName(elem, default=None):
"""Get the name associated with an element, either a name child or name attribute."""
name_elem = elem.find('name')
if name_elem is not None:
return name_elem.text
# Fallback if there is no child.
return elem.get('name', default)
def _getElemType(elem, default=None):
"""Get the type associated with an element, either a type child or type attribute."""
type_elem = elem.find('type')
if type_elem is not None:
return type_elem.text
# Fallback if there is no child.
return elem.get('type', default)
def _findNamedElem(elems, name):
"""Traverse a collection of elements with 'name' nodes or attributes, looking for and returning one with the right name.
NOTE: Many places where this is used might be better served by changing to a dictionary.
"""
for elem in elems:
if _getElemName(elem) == name:
return elem
return None
def _findNamedObject(collection, name):
"""Traverse a collection of elements with 'name' attributes, looking for and returning one with the right name.
NOTE: Many places where this is used might be better served by changing to a dictionary.
"""
for elt in collection:
if elt.name == name:
return elt
return None
class ValidityCollection:
"""Combines validity for a single entity."""
def __init__(self, entity, conventions):
self.entity = entity
self.conventions = conventions
self.lines = []
def possiblyAddExtensionRequirement(self, entity_data, entity_preface):
"""Add an extension-related validity statement if required.
entity_preface is a string that goes between "must be enabled prior to "
and the name of the entity, and normally ends in a macro.
For instance, might be "calling flink:" for a function.
"""
if entity_data.extensionname:
msg = 'The {} extension must: be enabled prior to {}{}'.format(
self.conventions.formatExtension(entity_data.extensionname), entity_preface, self.entity)
self.addValidityEntry(msg, 'extension', 'notenabled')
def addValidityEntry(self, msg, *args):
"""Add a validity entry, optionally with a VUID anchor.
If any trailing arguments are supplied,
an anchor is generated by concatenating them with dashes
at the end of the VUID anchor name.
"""
parts = ['*']
if args:
parts.append('[[{}]]'.format(
'-'.join(['VUID', self.entity] + list(args))))
parts.append(msg)
self.lines.append(' '.join(parts))
def addText(self, msg):
"""Add already formatted validity text."""
if not msg:
return
msg = msg.rstrip()
if not msg:
return
self.lines.append(msg)
@property
def text(self):
"""Access validity statements as a single string."""
if not self.lines:
return None
return '\n'.join(self.lines) + '\n'
class ValidityOutputGenerator(OutputGenerator):
"""ValidityOutputGenerator - subclass of OutputGenerator.
Generates AsciiDoc includes of valid usage information, for reference
pages and the Vulkan specification. Similar to DocOutputGenerator.
---- methods ----
ValidityOutputGenerator(errFile, warnFile, diagFile) - args as for
OutputGenerator. Defines additional internal state.
---- methods overriding base class ----
beginFile(genOpts)
endFile()
beginFeature(interface, emit)
endFeature()
genCmd(cmdinfo)
"""
def __init__(self,
errFile = sys.stderr,
warnFile = sys.stderr,
diagFile = sys.stdout):
OutputGenerator.__init__(self, errFile, warnFile, diagFile)
@property
def null(self):
"""Preferred spelling of NULL.
Delegates to the object implementing ConventionsBase.
"""
return self.conventions.null
@property
def structtype_member_name(self):
"""Return name of the structure type member.
Delegates to the object implementing ConventionsBase.
"""
return self.conventions.structtype_member_name
@property
def nextpointer_member_name(self):
"""Return name of the structure pointer chain member.
Delegates to the object implementing ConventionsBase.
"""
return self.conventions.nextpointer_member_name
def makeProseList(self, elements, connective='and'):
"""Make a (comma-separated) list for use in prose.
Adds a connective (by default, 'and')
before the last element if there are more than 1.
Delegates to the object implementing ConventionsBase.
"""
return self.conventions.makeProseList(elements, connective)
def makeValidityCollection(self, entity_name):
"""Create a ValidityCollection object, passing along our Conventions."""
return ValidityCollection(entity_name, self.conventions)
def beginFile(self, genOpts):
if not genOpts.conventions:
raise RuntimeError('Must specify conventions object to generator options')
self.conventions = genOpts.conventions
OutputGenerator.beginFile(self, genOpts)
def endFile(self):
OutputGenerator.endFile(self)
def beginFeature(self, interface, emit):
# Start processing in superclass
OutputGenerator.beginFeature(self, interface, emit)
self.currentExtension = interface.get('name')
def endFeature(self):
# Finish processing in superclass
OutputGenerator.endFeature(self)
@property
def struct_macro(self):
"""Get the appropriate format macro for a structure."""
# delegate to conventions
return self.conventions.struct_macro
def makeStructName(self, name):
"""Prepend the appropriate format macro for a structure to a structure type name."""
# delegate to conventions
return self.conventions.makeStructName(name)
def makeParameterName(self, name):
"""Prepend the appropriate format macro for a parameter/member to a parameter name."""
return 'pname:' + name
def makeBaseTypeName(self, name):
"""Prepend the appropriate format macro for a 'base type' to a type name."""
return 'basetype:' + name
def makeEnumerationName(self, name):
"""Prepend the appropriate format macro for an enumeration type to a enum type name."""
return 'elink:' + name
def makeFuncPointerName(self, name):
"""Prepend the appropriate format macro for a function pointer type to a type name."""
return 'tlink:' + name
def makeExternalTypeName(self, name):
"""Prepend the appropriate format macro for an external type like uint32_t to a type name."""
# delegate to conventions
return self.conventions.makeExternalTypeName(name)
def makeEnumerantName(self, name):
"""Prepend the appropriate format macro for an enumerate (value) to a enum value name."""
return 'ename:' + name
def makeAnchor(self, blockname, pname, category):
"""Create a unique namespaced Valid Usage anchor name.
blockname - command or structure
pname - parameter or member (may be None)
category - distinct implicit VU type
"""
# For debugging
# return '* '
if pname is not None:
return '* [[VUID-%s-%s-%s]] ' % (blockname, pname, category)
return '* [[VUID-%s-%s]] ' % (blockname, category)
def writeInclude(self, directory, basename, validity, threadsafety, commandpropertiesentry, successcodes, errorcodes):
"""Generate an include file.
directory - subdirectory to put file in (relative pathname)
basename - base name of the file
contents - contents of the file (Asciidoc boilerplate aside)
"""
# Create subdirectory, if needed
directory = self.genOpts.directory + '/' + directory
self.makeDir(directory)
# Create validity file
filename = directory + '/' + basename + '.txt'
self.logMsg('diag', '# Generating include file:', filename)
fp = open(filename, 'w', encoding='utf-8')
# Asciidoc anchor
write(self.genOpts.conventions.warning_comment, file=fp)
# Valid Usage
if validity:
write('.Valid Usage (Implicit)', file=fp)
write('****', file=fp)
write(validity, file=fp, end='')
write('****', file=fp)
write('', file=fp)
# Host Synchronization
if threadsafety:
write('.Host Synchronization', file=fp)
write('****', file=fp)
write(threadsafety, file=fp, end='')
write('****', file=fp)
write('', file=fp)
# Command Properties - contained within a block, to avoid table numbering
if commandpropertiesentry:
write('.Command Properties', file=fp)
write('****', file=fp)
write('[options="header", width="100%"]', file=fp)
write('|====', file=fp)
write('|<<VkCommandBufferLevel,Command Buffer Levels>>|<<vkCmdBeginRenderPass,Render Pass Scope>>|<<VkQueueFlagBits,Supported Queue Types>>|<<synchronization-pipeline-stages-types,Pipeline Type>>', file=fp)
write(commandpropertiesentry, file=fp)
write('|====', file=fp)
write('****', file=fp)
write('', file=fp)
# Success Codes - contained within a block, to avoid table numbering
has_success = (successcodes is not None and len(successcodes) > 0)
has_errors = (errorcodes is not None and len(errorcodes) > 0)
if has_success or has_errors:
write('.Return Codes', file=fp)
write('****', file=fp)
if has_success:
write('ifndef::doctype-manpage[]', file=fp)
write('<<fundamentals-successcodes,Success>>::', file=fp)
write('endif::doctype-manpage[]', file=fp)
write('ifdef::doctype-manpage[]', file=fp)
write('On success, this command returns::', file=fp)
write('endif::doctype-manpage[]', file=fp)
write(successcodes, file=fp)
if has_errors:
write('ifndef::doctype-manpage[]', file=fp)
write('<<fundamentals-errorcodes,Failure>>::', file=fp)
write('endif::doctype-manpage[]', file=fp)
write('ifdef::doctype-manpage[]', file=fp)
write('On failure, this command returns::', file=fp)
write('endif::doctype-manpage[]', file=fp)
write(errorcodes, file=fp)
write('****', file=fp)
write('', file=fp)
fp.close()
def paramIsPointer(self, param):
"""Check if the parameter passed in is a pointer."""
tail = param.find('type').tail
return tail is not None and '*' in tail
def paramIsStaticArray(self, param):
"""Check if the parameter passed in is a static array."""
tail = param.find('name').tail
return tail and tail[0] == '['
def staticArrayLength(self, param):
"""Get the length of a parameter that's been identified as a static array."""
paramenumsize = param.find('enum')
if paramenumsize is not None:
return paramenumsize.text
return param.find('name').tail[1:-1]
def paramIsArray(self, param):
"""Check if the parameter passed in is a pointer to an array."""
return param.get('len') is not None
def getHandleParent(self, typename):
"""Get the parent of a handle object."""
types = self.registry.tree.findall("types/type")
elem = _findNamedElem(types, typename)
if elem:
return elem.get('parent')
return None
def iterateHandleAncestors(self, typename):
"""Iterate through the ancestors of a handle type."""
current = self.getHandleParent(typename)
while current is not None:
yield current
current = self.getHandleParent(current)
def getHandleAncestors(self, typename):
"""Get the ancestors of a handle object."""
ancestors = []
current = typename
while True:
current = self.getHandleParent(current)
if current is None:
return ancestors
ancestors.append(current)
def getHandleDispatchableAncestors(self, typename):
"""Get the ancestors of a handle object."""
ancestors = []
current = typename
while True:
current = self.getHandleParent(current)
if current is None:
return ancestors
if self.isHandleTypeDispatchable(current):
ancestors.append(current)
def isHandleTypeDispatchable(self, handlename):
"""Check if a parent object is dispatchable or not."""
handle = self.registry.tree.find("types/type/[name='" + handlename + "'][@category='handle']")
if handle is not None and handle.find('type').text == 'VK_DEFINE_HANDLE':
return True
else:
return False
def isHandleOptional(self, param, params):
# Simple, if it's optional, return true
if param.get('optional') is not None:
return True
# If no validity is being generated, it usually means that validity is complex and not absolute, so let's say yes.
if param.get('noautovalidity') is not None:
return True
# If the parameter is an array and we haven't already returned, find out if any of the len parameters are optional
if self.paramIsArray(param):
for length in LengthEntry.parse_len_from_param(param):
if not length.other_param_name:
# don't care about constants or "null-terminated"
continue
other_param = _findNamedElem(params, length.other_param_name)
if other_param is None:
self.logMsg('warn', length.other_param_name, 'is listed as a length for parameter', param, 'but no such parameter exists')
if other_param and other_param.get('optional'):
return True
return False
def getTypeCategory(self, typename):
"""Get the category of a type."""
types = self.registry.tree.findall("types/type")
elem = _findNamedElem(types, typename)
if elem is not None:
return elem.get('category')
return None
def makeAsciiDocPreChunk(self, blockname, param, params):
"""Make a chunk of text for the end of a parameter if it is an array."""
param_name = _getElemName(param)
paramtype = _getElemType(param)
# General pre-amble. Check optionality and add stuff.
asciidoc = self.makeAnchor(blockname, param_name, 'parameter')
if self.paramIsStaticArray(param):
asciidoc += 'Any given element of '
elif self.paramIsArray(param):
lengths = param.get('len').split(',')
# Find all the parameters that are called out as optional, so we can document that they might be zero, and the array may be ignored
optionallengths = []
for length in lengths:
if (length) != 'null-terminated' and (length) != '1':
for otherparam in params:
if otherparam.find('name').text == length:
if otherparam.get('optional'):
if self.paramIsPointer(otherparam):
optionallengths.append('the value referenced by ' + self.makeParameterName(length))
else:
optionallengths.append(self.makeParameterName(length))
# Document that these arrays may be ignored if any of the length values are 0
if len(optionallengths) != 0 or param.get('optional'):
asciidoc += 'If '
if len(optionallengths) != 0:
if len(optionallengths) == 1:
asciidoc += optionallengths[0]
asciidoc += ' is '
else:
asciidoc += ', or '.join(optionallengths)
asciidoc += ' are '
asciidoc += 'not `0`, '
if len(optionallengths) != 0 and param.get('optional'):
asciidoc += 'and '
if param.get('optional'):
asciidoc += self.makeParameterName(param_name)
asciidoc += ' is not `NULL`, '
elif param.get('optional'):
# Don't generate this stub for bitflags
if self.getTypeCategory(paramtype) != 'bitmask':
if param.get('optional').split(',')[0] == 'true':
asciidoc += 'If '
asciidoc += self.makeParameterName(param_name)
asciidoc += ' is not '
if self.paramIsArray(param) or self.paramIsPointer(param) or self.isHandleTypeDispatchable(paramtype):
asciidoc += '`NULL`'
elif self.getTypeCategory(paramtype) == 'handle':
asciidoc += 'dlink:VK_NULL_HANDLE'
else:
asciidoc += '`0`'
asciidoc += ', '
return asciidoc
def createValidationLineForParameterIntroChunk(self, blockname, param, params, typetext):
"""Make the generic asciidoc line chunk portion used for all parameters.
May return an empty string if nothing to validate.
"""
# Vulkan says 'must: be a valid pointer' a lot, OpenXR just says
# 'must: be a pointer'.
valid_text = self.genOpts.conventions.valid_pointer_prefix
if valid_text == '':
valid_pointer_text = 'pointer'
else:
valid_pointer_text = valid_text + ' pointer'
asciidoc = ''
param_name = _getElemName(param)
paramtype = _getElemType(param)
asciidoc += self.makeAsciiDocPreChunk(blockname, param, params)
asciidoc += self.makeParameterName(param_name)
asciidoc += ' must: be '
if self.paramIsArray(param):
# Arrays. These are hard to get right, apparently
lengths = param.get('len').split(',')
if lengths[0] == 'null-terminated':
asciidoc += 'a null-terminated '
elif lengths[0] == '1':
asciidoc += 'a ' + valid_pointer_text + ' to '
else:
asciidoc += 'a ' + valid_pointer_text + ' to an array of '
# Handle equations, which are currently denoted with latex
if 'latexmath:' in lengths[0]:
asciidoc += lengths[0]
else:
asciidoc += self.makeParameterName(lengths[0])
asciidoc += ' '
for length in lengths[1:]:
if length == 'null-terminated':
# This should always be the last thing.
# If it ever isn't for some bizarre reason, then this will need some massaging.
asciidoc += 'null-terminated '
elif length == '1':
asciidoc += valid_pointer_text + 's to '
else:
asciidoc += valid_pointer_text + 's to arrays of '
# Handle equations, which are currently denoted with latex
if 'latexmath:' in length:
asciidoc += length
else:
asciidoc += self.makeParameterName(length)
asciidoc += ' '
# Void pointers don't actually point at anything - remove the word "to"
if paramtype == 'void':
if lengths[-1] == '1':
if len(lengths) > 1:
asciidoc = asciidoc[:-5] # Take care of the extra s added by the post array chunk function. #HACK#
else:
asciidoc = asciidoc[:-4]
else:
# An array of void values is a byte array.
asciidoc += 'byte'
elif paramtype == 'char':
# A null terminated array of chars is a string
if lengths[-1] == 'null-terminated':
asciidoc += 'UTF-8 string'
else:
# Else it's just a bunch of chars
asciidoc += 'char value'
elif param.text is not None:
# If a value is "const" that means it won't get modified, so it must be valid going into the function.
if 'const' in param.text:
typecategory = self.getTypeCategory(paramtype)
if (typecategory not in ('struct', 'union', 'basetype') and typecategory is not None) \
or not self.isStructAlwaysValid(blockname, paramtype):
asciidoc += 'valid '
asciidoc += typetext
# pluralize
if len(lengths) > 1 or (lengths[0] != '1' and lengths[0] != 'null-terminated'):
asciidoc += 's'
elif self.paramIsPointer(param):
# Handle pointers - which are really special case arrays (i.e. they don't have a length)
#TODO should do something here if someone ever uses some intricate comma-separated `optional`
pointercount = param.find('type').tail.count('*')
# Treat void* as an int
if paramtype == 'void':
pointercount -= 1
# Could be multi-level pointers (e.g. ppData - pointer to a pointer). Handle that.
asciidoc += 'a '
asciidoc += (valid_pointer_text + ' to a ') * pointercount
# Handle void* and pointers to it
if paramtype == 'void':
# If there is only void*, it is just optional int - we don't need any language.
if pointercount == 0 and param.get('optional') is not None:
return '' # early return
if param.get('optional') is None or param.get('optional').split(',')[pointercount]:
# The last void* is just optional int (e.g. to be filled by the impl.)
typetext = 'pointer value'
# If a value is "const" that means it won't get modified, so it must be valid going into the function.
if param.text is not None and paramtype != 'void':
if 'const' in param.text:
asciidoc += 'valid '
asciidoc += typetext
else:
# Non-pointer, non-optional things must be valid
asciidoc += 'a valid '
asciidoc += typetext
if asciidoc != '':
asciidoc += '\n'
# Add additional line for non-optional bitmasks
isOutputParam = self.paramIsPointer(param) and not (param.text and 'const' in param.text)
if self.getTypeCategory(paramtype) == 'bitmask' and not isOutputParam:
isMandatory = param.get('optional') is None #TODO does not really handle if someone tries something like optional="true,false"
if isMandatory:
asciidoc += self.makeAnchor(blockname, param_name, 'requiredbitmask')
if self.paramIsArray(param):
asciidoc += 'Each element of '
asciidoc += 'pname:'
asciidoc += param_name
asciidoc += ' must: not be `0`'
asciidoc += '\n'
return asciidoc
def makeAsciiDocLineForParameter(self, blockname, param, params, typetext):
if param.get('noautovalidity') is not None:
return ''
asciidoc = self.createValidationLineForParameterIntroChunk(blockname, param, params, typetext)
return asciidoc
def isStructAlwaysValid(self, blockname, structname):
"""Try to do check if a structure is always considered valid (i.e. there's no rules to its acceptance)."""
struct = self.registry.tree.find("types/type[@name='" + structname + "']")
if struct.get('returnedonly'):
return True
params = struct.findall('member')
for param in params:
param_name = _getElemName(param)
paramtype = _getElemType(param)
typecategory = self.getTypeCategory(paramtype)
if param_name in (self.structtype_member_name, self.nextpointer_member_name):
return False
if param.get('noautovalidity'):
return False
if paramtype in ('void', 'char') or self.paramIsArray(param) or self.paramIsPointer(param):
if self.makeAsciiDocLineForParameter(blockname, param, params, '') != '':
return False
elif typecategory in ('handle', 'enum', 'bitmask'):
return False
elif typecategory in ('struct', 'union'):
if self.isStructAlwaysValid(blockname, paramtype) is False:
return False
return True
def fix_an(self, text):
"""Fix 'a' vs 'an' in a string"""
return re.sub(r' a ([a-z]+:)?([aAeEiIoOxX])', r' an \1\2', text)
def createValidationLineForParameter(self, blockname, param, params, typecategory):
"""Make an entire asciidoc line for a given parameter."""
asciidoc = ''
param_name = _getElemName(param)
paramtype = _getElemType(param)
type_name = paramtype
is_array = self.paramIsArray(param)
is_pointer = self.paramIsPointer(param)
needs_recursive_validity = (is_array or
is_pointer or
not self.isStructAlwaysValid(blockname, type_name))
if type_name in ('void', 'char'):
# Chars and void are special cases - needs care inside the generator functions
# A null-terminated char array is a string, else it's chars.
# An array of void values is a byte array, a void pointer is just a pointer to nothing in particular
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, '')
elif typecategory == 'bitmask':
bitsname = type_name.replace('Flags', 'FlagBits')
if self.registry.tree.find("enums[@name='" + bitsname + "']") is None:
# Empty bit mask: presumably just a placeholder (or only in an extension not enabled for this build)
asciidoc += self.makeAnchor(blockname,
param_name, 'zerobitmask')
asciidoc += self.makeParameterName(param_name)
asciidoc += ' must: be `0`'
asciidoc += '\n'
else:
const_in_text = param.text is not None and 'const' in param.text
if is_array:
if const_in_text:
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, 'combinations of ' + self.makeEnumerationName(bitsname) + ' value')
else:
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeEnumerationName(paramtype) + ' value')
elif is_pointer:
if const_in_text:
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, 'combination of ' + self.makeEnumerationName(bitsname) + ' values')
else:
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeEnumerationName(paramtype) + ' value')
else:
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, 'combination of ' + self.makeEnumerationName(bitsname) + ' values')
elif typecategory == 'handle':
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeStructName(paramtype) + ' handle')
elif typecategory == 'enum':
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeEnumerationName(paramtype) + ' value')
elif typecategory == 'struct':
if self.paramIsArray(param) or self.paramIsPointer(param) or not self.isStructAlwaysValid(blockname, paramtype):
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeStructName(paramtype) + ' structure')
elif typecategory == 'union':
if self.paramIsArray(param) or self.paramIsPointer(param) or not self.isStructAlwaysValid(blockname, paramtype):
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeStructName(paramtype) + ' union')
elif self.paramIsArray(param) or self.paramIsPointer(param):
asciidoc += self.makeAsciiDocLineForParameter(blockname, param, params, self.makeBaseTypeName(paramtype) + ' value')
return asciidoc
def makeAsciiDocHandleParent(self, blockname, param, params):
"""Make an asciidoc validity entry for a handle's parent object."""
asciidoc = ''
param_name = _getElemName(param)
paramtype = _getElemType(param)
# Deal with handle parents
handleparent = self.getHandleParent(paramtype)
if handleparent is not None:
parentreference = None
for otherparam in params:
if otherparam.find('type').text == handleparent:
parentreference = otherparam.find('name').text
if parentreference:
asciidoc += self.makeAnchor(blockname, param_name, 'parent')
if self.isHandleOptional(param, params):
if self.paramIsArray(param):
asciidoc += 'Each element of '
asciidoc += self.makeParameterName(param_name)
asciidoc += ' that is a valid handle'
else:
asciidoc += 'If '
asciidoc += self.makeParameterName(param_name)
asciidoc += ' is a valid handle, it'
else:
if self.paramIsArray(param):
asciidoc += 'Each element of '
asciidoc += self.makeParameterName(param_name)
asciidoc += ' must: have been created, allocated, or retrieved from '
asciidoc += self.makeParameterName(parentreference)
asciidoc += '\n'
return asciidoc
def makeAsciiDocHandlesCommonAncestor(self, blockname, handles, params):
"""Make an asciidoc validity entry for a common ancestors between handles."""
asciidoc = ''
if len(handles) > 1:
ancestormap = {}
anyoptional = False
# Find all the ancestors
for param in handles:
paramtype = _getElemType(param)
if not self.paramIsPointer(param) or (param.text and 'const' in param.text):
ancestors = self.getHandleDispatchableAncestors(paramtype)
ancestormap[param] = ancestors
anyoptional |= self.isHandleOptional(param, params)
# Remove redundant ancestor lists
for param in handles:
paramtype = _getElemType(param)
removals = []
for ancestors in ancestormap.items():
if paramtype in ancestors[1]:
removals.append(ancestors[0])
if removals != []:
for removal in removals:
del(ancestormap[removal])
# Intersect
if len(ancestormap.values()) > 1:
current = list(ancestormap.values())[0]
for ancestors in list(ancestormap.values())[1:]:
current = [val for val in current if val in ancestors]
if len(current) > 0:
commonancestor = current[0]
if len(ancestormap.keys()) > 1:
asciidoc += self.makeAnchor(blockname, None, 'commonparent')
parametertexts = []
for param in ancestormap.keys():
param_name = _getElemName(param)
parametertext = self.makeParameterName(param_name)
if self.paramIsArray(param):
parametertext = 'the elements of ' + parametertext
parametertexts.append(parametertext)
parametertexts.sort()
if len(parametertexts) > 2:
asciidoc += 'Each of '
else:
asciidoc += 'Both of '
asciidoc += ", ".join(parametertexts[:-1])
asciidoc += ', and '
asciidoc += parametertexts[-1]
if anyoptional is True:
asciidoc += ' that are valid handles'
asciidoc += ' must: have been created, allocated, or retrieved from the same '
asciidoc += self.makeStructName(commonancestor)
asciidoc += '\n'
return asciidoc
def makeStructureType(self, structname, param):
"""Generate an asciidoc validity line for the type value of a struct."""
param_name = _getElemName(param)
paramtype = _getElemType(param)
asciidoc = self.makeAnchor(structname, param_name, 'sType')
asciidoc += self.makeParameterName(param_name)
asciidoc += ' must: be '
values = param.get('values')
if values:
# Extract each enumerant value. They could be validated in the
# same fashion as validextensionstructs in
# makeStructureExtensionPointer, although that's not relevant in
# the current extension struct model.
valuelist = [ self.makeEnumerantName(v) for v in values.split(',') ]
else:
structuretype = ''
for elem in re.findall(r'(([A-Z][a-z]+)|([A-Z][A-Z]+))', structname):
if elem[0] == 'Vk':
structuretype += 'VK_STRUCTURE_TYPE_'
else:
structuretype += elem[0].upper()
structuretype += '_'
valuelist = [ self.makeEnumerantName(structuretype[:-1]) ]
if len(valuelist) > 0:
if len(valuelist) == 1:
asciidoc += valuelist[0]
else:
asciidoc += (', ').join(valuelist[:-1]) + ', or ' + valuelist[-1]
asciidoc += '\n'
return asciidoc
def makeStructureExtensionPointer(self, blockname, param):
"""Generate an asciidoc validity line for the pointer chain member value of a struct."""
param_name = _getElemName(param)
if param.get('validextensionstructs') is not None:
self.logMsg('warn', blockname, 'validextensionstructs is deprecated/removed', '\n')
asciidoc = self.makeAnchor(blockname, param_name, self.nextpointer_member_name)
validextensionstructs = self.registry.validextensionstructs.get(blockname)
extensionstructs = []
if validextensionstructs is not None:
# Check each structure name and skip it if not required by the
# generator. This allows tagging extension structs in the XML
# that are only included in validity when needed for the spec
# being targeted.
for struct in validextensionstructs:
# Unpleasantly breaks encapsulation. Should be a method in the registry class
type = self.registry.lookupElementInfo(struct, self.registry.typedict)
if type is None:
self.logMsg('warn', 'makeStructureExtensionPointer: struct', struct, 'is in a validextensionstructs= attribute but is not in the registry')
elif type.required:
extensionstructs.append('slink:' + struct)
else:
self.logMsg('diag', 'makeStructureExtensionPointer: struct', struct, 'IS NOT required')
if not extensionstructs:
asciidoc += self.makeParameterName(param_name)
asciidoc += ' must: be '
asciidoc += self.null
elif len(extensionstructs) == 1:
asciidoc += self.makeParameterName(param_name)
asciidoc += ' must: be '
asciidoc += self.null
asciidoc +=' or a pointer to a valid instance of '
asciidoc += extensionstructs[0]
else:
asciidoc += 'Each '
asciidoc += self.makeParameterName(param_name)
asciidoc += ' member of any structure (including this one) in the '
asciidoc += 'pname:' + self.nextpointer_member_name
asciidoc += ' chain must: be either '
asciidoc += self.null
asciidoc += ' or a pointer to a valid instance of '
asciidoc += self.makeProseList(extensionstructs, 'or')
asciidoc += '\n'
asciidoc += self.makeAnchor(blockname, self.structtype_member_name, 'unique')
asciidoc += 'Each pname:' + self.structtype_member_name + ' member in the '
asciidoc += 'pname:' + self.nextpointer_member_name
asciidoc += ' chain must: be unique'
asciidoc += '\n'
return asciidoc
def makeValidUsageStatementsReturnedOnly(self, cmd, blockname, params):
"""Generate all the valid usage information for a given struct
that's only ever filled out by the implementation other than the
structure type and pointer chain members.
"""
# Start the asciidoc block for this
asciidoc = ''
for param in params:
param_name = _getElemName(param)
paramtype = _getElemType(param)
# Valid usage ID tags (VUID) are generated for various
# conditions based on the name of the block (structure or
# command), name of the element (member or parameter), and type
# of VU statement.
if param.get('noautovalidity') is None:
# Generate language to independently validate a parameter
if self.conventions.is_structure_type_member(paramtype, param_name):
asciidoc += self.makeStructureType(blockname, param)
elif self.conventions.is_nextpointer_member(paramtype, param_name) and cmd.get('structextends') is None:
asciidoc += self.makeStructureExtensionPointer(blockname, param)
# In case there's nothing to report, return None
if asciidoc == '':
return None
return asciidoc
def makeValidUsageStatements(self, cmd, blockname, params):
"""Generate all the valid usage information for a given struct or command."""
# Start the asciidoc block for this
asciidoc = ''
handles = []
anyparentedhandlesoptional = False
parentdictionary = {}
arraylengths = set()
for param in params:
param_name = _getElemName(param)
paramtype = _getElemType(param)
# Valid usage ID tags (VUID) are generated for various
# conditions based on the name of the block (structure or
# command), name of the element (member or parameter), and type
# of VU statement.
# Get the type's category
typecategory = self.getTypeCategory(paramtype)
if param.get('noautovalidity') is None:
# Generate language to independently validate a parameter
if self.conventions.is_structure_type_member(paramtype, param_name):
asciidoc += self.makeStructureType(blockname, param)
elif self.conventions.is_nextpointer_member(paramtype, param_name):
if cmd.get('structextends') is None:
asciidoc += self.makeStructureExtensionPointer(blockname, param)
else:
asciidoc += self.createValidationLineForParameter(blockname, param, params, typecategory)
# Ensure that any parenting is properly validated, and list that a handle was found
if typecategory == 'handle':
handles.append(param)
# Get the array length for this parameter
arraylength = param.get('len')
if arraylength is not None:
arraylengths.update(set(arraylength.split(',')))
# For any vkQueue* functions, there might be queue type data
if 'vkQueue' in blockname:
# The queue type must be valid
queuetypes = cmd.get('queues')
if queuetypes:
queuebits = []
for queuetype in re.findall(r'([^,]+)', queuetypes):
queuebits.append(queuetype.replace('_',' '))
asciidoc += self.makeAnchor(blockname, None, 'queuetype')
asciidoc += 'The pname:queue must: support '
if len(queuebits) == 1:
asciidoc += queuebits[0]
else:
asciidoc += (', ').join(queuebits[:-1])
asciidoc += ', or '
asciidoc += queuebits[-1]
asciidoc += ' operations'
asciidoc += '\n'
if 'vkCmd' in blockname:
# The commandBuffer parameter must be being recorded
asciidoc += self.makeAnchor(blockname, 'commandBuffer', 'recording')
asciidoc += 'pname:commandBuffer must: be in the <<commandbuffers-lifecycle, recording state>>'
asciidoc += '\n'
#
# Start of valid queue type validation - command pool must have been
# allocated against a queue with at least one of the valid queue types
asciidoc += self.makeAnchor(blockname, 'commandBuffer', 'cmdpool')
#
# This test for vkCmdFillBuffer is a hack, since we have no path
# to conditionally have queues enabled or disabled by an extension.
# As the VU stuff is all moving out (hopefully soon), this hack solves the issue for now
if blockname == 'vkCmdFillBuffer':
if 'VK_KHR_maintenance1' in self.registry.requiredextensions:
asciidoc += 'The sname:VkCommandPool that pname:commandBuffer was allocated from must: support transfer, graphics or compute operations\n'
else:
asciidoc += 'The sname:VkCommandPool that pname:commandBuffer was allocated from must: support graphics or compute operations\n'
else:
# The queue type must be valid
queuetypes = cmd.get('queues')
queuebits = []
for queuetype in re.findall(r'([^,]+)', queuetypes):
queuebits.append(queuetype.replace('_',' '))
asciidoc += 'The sname:VkCommandPool that pname:commandBuffer was allocated from must: support '
if len(queuebits) == 1:
asciidoc += queuebits[0]
else:
asciidoc += (', ').join(queuebits[:-1])
asciidoc += ', or '
asciidoc += queuebits[-1]
asciidoc += ' operations'
asciidoc += '\n'
# Must be called inside/outside a renderpass appropriately
renderpass = cmd.get('renderpass')
if renderpass != 'both':
asciidoc += self.makeAnchor(blockname, None, 'renderpass')
asciidoc += 'This command must: only be called '
asciidoc += renderpass
asciidoc += ' of a render pass instance'
asciidoc += '\n'
# Must be in the right level command buffer
cmdbufferlevel = cmd.get('cmdbufferlevel')
if cmdbufferlevel != 'primary,secondary':
asciidoc += self.makeAnchor(blockname, None, 'bufferlevel')
asciidoc += 'pname:commandBuffer must: be a '
asciidoc += cmdbufferlevel
asciidoc += ' sname:VkCommandBuffer'
asciidoc += '\n'
# Any non-optional arraylengths should specify they must be greater than 0
for param in params:
param_name = _getElemName(param)
for arraylength in arraylengths:
if param_name == arraylength and param.get('optional') is None:
# Get all the array dependencies
arrays = cmd.findall("param/[@len='" + arraylength + "'][@optional='true']")
# Get all the optional array dependencies, including those not generating validity for some reason
optionalarrays = cmd.findall("param/[@len='" + arraylength + "'][@optional='true']")
optionalarrays.extend(cmd.findall("param/[@len='" + arraylength + "'][@noautovalidity='true']"))
# If arraylength can ever be not a legal part of an
# asciidoc anchor name, this will need to be altered.
asciidoc += self.makeAnchor(blockname, arraylength, 'arraylength')
# Allow lengths to be arbitrary if all their dependents are optional
if len(optionalarrays) == len(arrays) and len(optionalarrays) != 0:
asciidoc += 'If '
if len(optionalarrays) > 1:
asciidoc += 'any of '
for array in optionalarrays[:-1]:
asciidoc += self.makeParameterName(optionalarrays.find('name').text)
asciidoc += ', '
if len(optionalarrays) > 1:
asciidoc += 'and '
asciidoc += self.makeParameterName(optionalarrays[-1].find('name').text)
asciidoc += ' are '
else:
asciidoc += self.makeParameterName(optionalarrays[-1].find('name').text)
asciidoc += ' is '
asciidoc += 'not ' + self.null + ', '
if self.paramIsPointer(param):
asciidoc += 'the value referenced by '
elif self.paramIsPointer(param):
asciidoc += 'The value referenced by '
asciidoc += self.makeParameterName(arraylength)
asciidoc += ' must: be greater than `0`'
asciidoc += '\n'
# Find the parents of all objects referenced in this command
for param in handles:
paramtype = _getElemType(param)
# Don't detect a parent for return values!
if not self.paramIsPointer(param) or (param.text is not None and 'const' in param.text):
parent = self.getHandleParent(paramtype)
if parent is not None:
asciidoc += self.makeAsciiDocHandleParent(blockname, param, params)
# Find the common ancestor of all objects referenced in this command
asciidoc += self.makeAsciiDocHandlesCommonAncestor(blockname, handles, params)
# In case there's nothing to report, return None
if asciidoc == '':
return None
# Delimit the asciidoc block
return asciidoc
def makeThreadSafetyBlock(self, cmd, paramtext):
"""Generate C function pointer typedef for <command> Element"""
paramdecl = ''
# Find and add any parameters that are thread unsafe
explicitexternsyncparams = cmd.findall(paramtext + "[@externsync]")
if explicitexternsyncparams is not None:
for param in explicitexternsyncparams:
externsyncattribs = param.get('externsync')
param_name = _getElemName(param)
for externsyncattrib in externsyncattribs.split(','):
paramdecl += '* '
paramdecl += 'Host access to '
if externsyncattrib == 'true':
if self.paramIsArray(param):
paramdecl += 'each member of ' + \
self.makeParameterName(param_name)
elif self.paramIsPointer(param):
paramdecl += 'the object referenced by ' + \
self.makeParameterName(param_name)
else:
paramdecl += self.makeParameterName(param_name)
else:
paramdecl += 'pname:'
paramdecl += externsyncattrib
paramdecl += ' must: be externally synchronized\n'
# Vulkan-specific
# For any vkCmd* functions, the command pool is externally synchronized
if cmd.find('proto/name') is not None and 'vkCmd' in cmd.find('proto/name').text:
paramdecl += '* '
paramdecl += 'Host access to the sname:VkCommandPool that pname:commandBuffer was allocated from must: be externally synchronized'
paramdecl += '\n'
# Find and add any "implicit" parameters that are thread unsafe
implicitexternsyncparams = cmd.find('implicitexternsyncparams')
if implicitexternsyncparams is not None:
for elem in implicitexternsyncparams:
paramdecl += '* '
paramdecl += 'Host access to '
paramdecl += elem.text
paramdecl += ' must: be externally synchronized\n'
if not paramdecl:
return None
return paramdecl
def makeCommandPropertiesTableEntry(self, cmd, name):
if 'vkCmd' in name:
# Must be called inside/outside a renderpass appropriately
cmdbufferlevel = cmd.get('cmdbufferlevel')
cmdbufferlevel = (' + \n').join(cmdbufferlevel.title().split(','))
renderpass = cmd.get('renderpass')
renderpass = renderpass.capitalize()
#
# This test for vkCmdFillBuffer is a hack, since we have no path
# to conditionally have queues enabled or disabled by an extension.
# As the VU stuff is all moving out (hopefully soon), this hack solves the issue for now
if name == 'vkCmdFillBuffer':
if 'VK_KHR_maintenance1' in self.registry.requiredextensions:
queues = 'Transfer + \nGraphics + \nCompute'
else:
queues = 'Graphics + \nCompute'
else:
queues = cmd.get('queues')
queues = (' + \n').join(queues.title().split(','))
pipeline = cmd.get('pipeline')
if pipeline:
pipeline = pipeline.capitalize()
else:
pipeline = ''
return '|' + cmdbufferlevel + '|' + renderpass + '|' + queues + '|' + pipeline
elif 'vkQueue' in name:
# Must be called inside/outside a renderpass appropriately
queues = cmd.get('queues')
if queues is None:
queues = 'Any'
else:
queues = (' + \n').join(queues.upper().split(','))
return '|-|-|' + queues + '|-'
return None
# Check each enumerant name in the enums list and remove it if not
# required by the generator. This allows specifying success and error
# codes for extensions that are only included in validity when needed
# for the spec being targeted.
def findRequiredEnums(self, enums):
out = []
for enum in enums:
# Unpleasantly breaks encapsulation. Should be a method in the registry class
ei = self.registry.lookupElementInfo(enum, self.registry.enumdict)
if ei is None:
self.logMsg('warn', 'findRequiredEnums: enum', enum,
'is in an attribute list but is not in the registry')
elif ei.required:
out.append(enum)
else:
self.logMsg('diag', 'findRequiredEnums: enum', enum, 'IS NOT required, skipping')
return out
def makeReturnCodeList(self, attrib, cmd, name):
"""Return a list of possible return codes for a function.
attrib is either 'successcodes' or 'errorcodes'.
"""
return_lines = []
RETURN_CODE_FORMAT = '* ename:{}'
codes_attr = cmd.get(attrib)
if codes_attr:
codes = self.findRequiredEnums(codes_attr.split(','))
if codes:
return_lines.extend((RETURN_CODE_FORMAT.format(code)
for code in codes))
applicable_ext_codes = (ext_code
for ext_code in self.registry.commandextensionsuccesses
if ext_code.command == name)
for ext_code in applicable_ext_codes:
line = RETURN_CODE_FORMAT.format(ext_code.value)
if ext_code.extension:
line += ' [only if {} is enabled]'.format(
self.conventions.formatExtension(ext_code.extension))
return_lines.append(line)
if return_lines:
return '\n'.join(return_lines)
return None
def makeSuccessCodes(self, cmd, name):
return self.makeReturnCodeList('successcodes', cmd, name)
def makeErrorCodes(self, cmd, name):
return self.makeReturnCodeList('errorcodes', cmd, name)
def genCmd(self, cmdinfo, name, alias):
"""Command generation."""
OutputGenerator.genCmd(self, cmdinfo, name, alias)
# @@@ (Jon) something needs to be done here to handle aliases, probably
# Get all the parameters
params = cmdinfo.elem.findall('param')
validity = self.makeValidUsageStatements(cmdinfo.elem, name, params)
threadsafety = self.makeThreadSafetyBlock(cmdinfo.elem, 'param')
# Vulkan-specific
commandpropertiesentry = self.makeCommandPropertiesTableEntry(cmdinfo.elem, name)
successcodes = self.makeSuccessCodes(cmdinfo.elem, name)
errorcodes = self.makeErrorCodes(cmdinfo.elem, name)
self.writeInclude('protos', name, validity, threadsafety, commandpropertiesentry, successcodes, errorcodes)
def genStruct(self, typeinfo, typename, alias):
"""Struct Generation."""
OutputGenerator.genStruct(self, typeinfo, typename, alias)
# @@@ (Jon) something needs to be done here to handle aliases, probably
# Anything that's only ever returned can't be set by the user, so shouldn't have any validity information.
if typeinfo.elem.get('returnedonly') is None:
params = typeinfo.elem.findall('member')
validity = self.makeValidUsageStatements(typeinfo.elem, typename, params)
threadsafety = self.makeThreadSafetyBlock(typeinfo.elem, 'member')
self.writeInclude('structs', typename, validity, threadsafety, None, None, None)
else:
# Need to generate structure type and next pointer chain member validation
params = typeinfo.elem.findall('member')
validity = self.makeValidUsageStatementsReturnedOnly(typeinfo.elem, typename, params)
self.writeInclude('structs', typename, validity, None, None, None, None)
def genGroup(self, groupinfo, groupName, alias):
"""Group (e.g. C "enum" type) generation.
For the validity generator, this just tags individual enumerants
as required or not.
"""
OutputGenerator.genGroup(self, groupinfo, groupName, alias)
# @@@ (Jon) something needs to be done here to handle aliases, probably
groupElem = groupinfo.elem
# Loop over the nested 'enum' tags. Keep track of the minimum and
# maximum numeric values, if they can be determined; but only for
# core API enumerants, not extension enumerants. This is inferred
# by looking for 'extends' attributes.
for elem in groupElem.findall('enum'):
name = elem.get('name')
ei = self.registry.lookupElementInfo(name, self.registry.enumdict)
# Tag enumerant as required or not
ei.required = self.isEnumRequired(elem)
def genType(self, typeinfo, typename, alias):
"""Type Generation."""
OutputGenerator.genType(self, typeinfo, typename, alias)
# @@@ (Jon) something needs to be done here to handle aliases, probably
category = typeinfo.elem.get('category')
if category in ('struct', 'union'):
self.genStruct(typeinfo, typename, alias)