1#!/usr/bin/env python3 2# Copyright (c) 2015-2016 The Khronos Group Inc. 3# Copyright (c) 2015-2016 Valve Corporation 4# Copyright (c) 2015-2016 LunarG, Inc. 5# Copyright (c) 2015-2016 Google Inc. 6# 7# Permission is hereby granted, free of charge, to any person obtaining a copy 8# of this software and/or associated documentation files (the "Materials"), to 9# deal in the Materials without restriction, including without limitation the 10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11# sell copies of the Materials, and to permit persons to whom the Materials 12# are furnished to do so, subject to the following conditions: 13# 14# The above copyright notice(s) and this permission notice shall be included 15# in all copies or substantial portions of the Materials. 16# 17# THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20# 21# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE 24# USE OR OTHER DEALINGS IN THE MATERIALS 25# 26# Author: Tobin Ehlis <tobin@lunarg.com> 27 28import argparse 29import os 30import sys 31import vulkan 32import platform 33 34# vk_layer_documentation_generate.py overview 35# This script is intended to generate documentation based on vulkan layers 36# It parses known validation layer headers for details of the validation checks 37# It parses validation layer source files for specific code where checks are implemented 38# structs in a human-readable txt format, as well as utility functions 39# to print enum values as strings 40 41# NOTE : Initially the script is performing validation of a hand-written document 42# Right now it does 3 checks: 43# 1. Verify ENUM codes declared in source are documented 44# 2. Verify ENUM codes in document are declared in source 45# 3. Verify API function names in document are in the actual API header (vulkan.py) 46# Currently script will flag errors in all of these cases 47 48# TODO : Need a formal specification of the syntax for doc generation 49# Initially, these are the basics: 50# 1. Validation checks have unique ENUM values defined in validation layer header 51# 2. ENUM includes comments for 1-line overview of check and more detailed description 52# 3. Actual code implementing checks includes ENUM value in callback 53# 4. Code to test checks should include reference to ENUM 54 55 56# TODO : Need list of known validation layers to use as default input 57# Just a couple of flat lists right now, but may need to make this input file 58# or at least a more dynamic data structure 59layer_inputs = { 'draw_state' : {'header' : 'layers/core_validation.h', 60 'source' : 'layers/core_validation.cpp', 61 'generated' : False, 62 'error_enum' : 'DRAW_STATE_ERROR'}, 63 'shader_checker' : {'header' : 'layers/core_validation.h', 64 'source' : 'layers/core_validation.cpp', 65 'generated' : False, 66 'error_enum' : 'SHADER_CHECKER_ERROR'}, 67 'mem_tracker' : {'header' : 'layers/core_validation.h', 68 'source' : 'layers/core_validation.cpp', 69 'generated' : False, 70 'error_enum' : 'MEM_TRACK_ERROR'}, 71 'threading' : {'header' : 'layers/threading.h', 72 'source' : 'dbuild/layers/threading.cpp', 73 'generated' : True, 74 'error_enum' : 'THREADING_CHECKER_ERROR'}, 75 'object_tracker' : {'header' : 'layers/object_tracker.h', 76 'source' : 'dbuild/layers/object_tracker.cpp', 77 'generated' : True, 78 'error_enum' : 'OBJECT_TRACK_ERROR',}, 79 'device_limits' : {'header' : 'layers/device_limits.h', 80 'source' : 'layers/device_limits.cpp', 81 'generated' : False, 82 'error_enum' : 'DEV_LIMITS_ERROR',}, 83 'image' : {'header' : 'layers/image.h', 84 'source' : 'layers/image.cpp', 85 'generated' : False, 86 'error_enum' : 'IMAGE_ERROR',}, 87 'swapchain' : {'header' : 'layers/swapchain.h', 88 'source' : 'layers/swapchain.cpp', 89 'generated' : False, 90 'error_enum' : 'SWAPCHAIN_ERROR',}, 91 } 92 93builtin_headers = [layer_inputs[ln]['header'] for ln in layer_inputs] 94builtin_source = [layer_inputs[ln]['source'] for ln in layer_inputs] 95 96# List of extensions in layers that are included in documentation, but not in vulkan.py API set 97layer_extension_functions = ['objTrackGetObjects', 'objTrackGetObjectsOfType'] 98 99def handle_args(): 100 parser = argparse.ArgumentParser(description='Generate layer documenation from source.') 101 parser.add_argument('--in_headers', required=False, default=builtin_headers, help='The input layer header files from which code will be generated.') 102 parser.add_argument('--in_source', required=False, default=builtin_source, help='The input layer source files from which code will be generated.') 103 parser.add_argument('--layer_doc', required=False, default='layers/vk_validation_layer_details.md', help='Existing layer document to be validated against actual layers.') 104 parser.add_argument('--validate', action='store_true', default=False, help='Validate that there are no mismatches between layer documentation and source. This includes cross-checking the validation checks, and making sure documented Vulkan API calls exist.') 105 parser.add_argument('--print_structs', action='store_true', default=False, help='Primarily a debug option that prints out internal data structs used to generate layer docs.') 106 parser.add_argument('--print_doc_checks', action='store_true', default=False, help='Primarily a debug option that prints out all of the checks that are documented.') 107 return parser.parse_args() 108 109# Little helper class for coloring cmd line output 110class bcolors: 111 112 def __init__(self): 113 self.GREEN = '\033[0;32m' 114 self.RED = '\033[0;31m' 115 self.ENDC = '\033[0m' 116 if 'Linux' != platform.system(): 117 self.GREEN = '' 118 self.RED = '' 119 self.ENDC = '' 120 121 def green(self): 122 return self.GREEN 123 124 def red(self): 125 return self.RED 126 127 def endc(self): 128 return self.ENDC 129 130# Class to parse the layer source code and store details in internal data structs 131class LayerParser: 132 def __init__(self, header_file_list, source_file_list): 133 self.header_files = header_file_list 134 self.source_files = source_file_list 135 self.layer_dict = {} 136 self.api_dict = {} 137 138 # Parse layer header files into internal dict data structs 139 def parse(self): 140 # For each header file, parse details into dicts 141 # TODO : Should have a global dict element to track overall list of checks 142 store_enum = False 143 for layer_name in layer_inputs: 144 hf = layer_inputs[layer_name]['header'] 145 self.layer_dict[layer_name] = {} # initialize a new dict for this layer 146 self.layer_dict[layer_name]['CHECKS'] = [] # enum of checks is stored in a list 147 #print('Parsing header file %s as layer name %s' % (hf, layer_name)) 148 with open(hf) as f: 149 for line in f: 150 if True in [line.strip().startswith(comment) for comment in ['//', '/*']]: 151 #print("Skipping comment line: %s" % line) 152 # For now skipping lines starting w/ comment, may use these to capture 153 # documentation in the future 154 continue 155 156 # Find enums 157 if store_enum: 158 if '}' in line: # we're done with enum definition 159 store_enum = False 160 continue 161 # grab the enum name as a unique check 162 if ',' in line: 163 # TODO : When documentation for a check is contained in the source, 164 # this is where we should also capture that documentation so that 165 # it can then be transformed into desired doc format 166 enum_name = line.split(',')[0].strip() 167 # Flag an error if we have already seen this enum 168 if enum_name in self.layer_dict[layer_name]['CHECKS']: 169 print('ERROR : % layer has duplicate error enum: %s' % (layer_name, enum_name)) 170 self.layer_dict[layer_name]['CHECKS'].append(enum_name) 171 # If the line includes 'typedef', 'enum', and the expected enum name, start capturing enums 172 if False not in [ex in line for ex in ['typedef', 'enum', layer_inputs[layer_name]['error_enum']]]: 173 store_enum = True 174 175 # For each source file, parse into dicts 176 for sf in self.source_files: 177 #print('Parsing source file %s' % sf) 178 pass 179 # TODO : In the source file we want to see where checks actually occur 180 # Need to build function tree of checks so that we know all of the 181 # checks that occur under a top-level Vulkan API call 182 # Eventually in the validation we can flag ENUMs that aren't being 183 # used in the source, and we can document source code lines as well 184 # as Vulkan API calls where each specific ENUM check is made 185 186 def print_structs(self): 187 print('This is where I print the data structs') 188 for layer in self.layer_dict: 189 print('Layer %s has %i checks:\n%s' % (layer, len(self.layer_dict[layer]['CHECKS'])-1, "\n\t".join(self.layer_dict[layer]['CHECKS']))) 190 191# Class to parse hand-written md layer documentation into a dict and then validate its contents 192class LayerDoc: 193 def __init__(self, source_file): 194 self.layer_doc_filename = source_file 195 self.txt_color = bcolors() 196 # Main data struct to store info from layer doc 197 self.layer_doc_dict = {} 198 # Comprehensive list of all validation checks recorded in doc 199 self.enum_list = [] 200 201 # Parse the contents of doc into data struct 202 def parse(self): 203 layer_name = 'INIT' 204 parse_layer_details = False 205 detail_trigger = '| Check | ' 206 parse_pending_work = False 207 pending_trigger = ' Pending Work' 208 parse_overview = False 209 overview_trigger = ' Overview' 210 enum_prefix = '' 211 212 with open(self.layer_doc_filename) as f: 213 for line in f: 214 if parse_pending_work: 215 if '.' in line and line.strip()[0].isdigit(): 216 todo_item = line.split('.')[1].strip() 217 self.layer_doc_dict[layer_name]['pending'].append(todo_item) 218 if pending_trigger in line and '##' in line: 219 parse_layer_details = False 220 parse_pending_work = True 221 parse_overview = False 222 self.layer_doc_dict[layer_name]['pending'] = [] 223 if parse_layer_details: 224 # Grab details but skip the fomat line with a bunch of '-' chars 225 if '|' in line and line.count('-') < 20: 226 detail_sections = line.split('|') 227 #print("Details elements from line %s: %s" % (line, detail_sections)) 228 check_name = '%s%s' % (enum_prefix, detail_sections[3].strip()) 229 if '_NA' in check_name: 230 # TODO : Should clean up these NA checks in the doc, skipping them for now 231 continue 232 self.enum_list.append(check_name) 233 self.layer_doc_dict[layer_name][check_name] = {} 234 self.layer_doc_dict[layer_name][check_name]['summary_txt'] = detail_sections[1].strip() 235 self.layer_doc_dict[layer_name][check_name]['details_txt'] = detail_sections[2].strip() 236 self.layer_doc_dict[layer_name][check_name]['api_list'] = detail_sections[4].split() 237 self.layer_doc_dict[layer_name][check_name]['tests'] = detail_sections[5].split() 238 self.layer_doc_dict[layer_name][check_name]['notes'] = detail_sections[6].strip() 239 # strip any unwanted commas from api and test names 240 self.layer_doc_dict[layer_name][check_name]['api_list'] = [a.strip(',') for a in self.layer_doc_dict[layer_name][check_name]['api_list']] 241 self.layer_doc_dict[layer_name][check_name]['tests'] = [a.strip(',') for a in self.layer_doc_dict[layer_name][check_name]['tests']] 242 # Trigger details parsing when we have table header 243 if detail_trigger in line: 244 parse_layer_details = True 245 parse_pending_work = False 246 parse_overview = False 247 enum_txt = line.split('|')[3] 248 if '*' in enum_txt: 249 enum_prefix = enum_txt.split()[-1].strip('*').strip() 250 #print('prefix: %s' % enum_prefix) 251 if parse_overview: 252 self.layer_doc_dict[layer_name]['overview'] += line 253 if overview_trigger in line and '##' in line: 254 parse_layer_details = False 255 parse_pending_work = False 256 parse_overview = True 257 layer_name = line.split()[1] 258 self.layer_doc_dict[layer_name] = {} 259 self.layer_doc_dict[layer_name]['overview'] = '' 260 261 # Verify that checks and api references in layer doc match reality 262 # Report API calls from doc that are not found in API 263 # Report checks from doc that are not in actual layers 264 # Report checks from layers that are not captured in doc 265 def validate(self, layer_dict): 266 # Count number of errors found and return it 267 errors_found = 0 268 # First we'll go through the doc datastructures and flag any issues 269 for chk in self.enum_list: 270 doc_layer_found = False 271 for real_layer in layer_dict: 272 if chk in layer_dict[real_layer]['CHECKS']: 273 #print('Found actual layer check %s in doc' % (chk)) 274 doc_layer_found = True 275 continue 276 if not doc_layer_found: 277 print(self.txt_color.red() + 'Actual layers do not contain documented check: %s' % (chk) + self.txt_color.endc()) 278 errors_found += 1 279 # Now go through API names in doc and verify they're real 280 # First we're going to transform proto names from vulkan.py into single list 281 core_api_names = [p.name for p in vulkan.core.protos] 282 wsi_s_names = [p.name for p in vulkan.ext_khr_surface.protos] 283 wsi_ds_names = [p.name for p in vulkan.ext_khr_device_swapchain.protos] 284 dbg_rpt_names = [p.name for p in vulkan.lunarg_debug_report.protos] 285 api_names = core_api_names + wsi_s_names + wsi_ds_names + dbg_rpt_names 286 for ln in self.layer_doc_dict: 287 for chk in self.layer_doc_dict[ln]: 288 if chk in ['overview', 'pending']: 289 continue 290 for api in self.layer_doc_dict[ln][chk]['api_list']: 291 if api[2:] not in api_names and api not in layer_extension_functions: 292 print(self.txt_color.red() + 'Doc references invalid function: %s' % (api) + self.txt_color.endc()) 293 errors_found += 1 294 # Now go through all of the actual checks in the layers and make sure they're covered in the doc 295 for ln in layer_dict: 296 for chk in layer_dict[ln]['CHECKS']: 297 if chk not in self.enum_list: 298 print(self.txt_color.red() + 'Doc is missing check: %s' % (chk) + self.txt_color.endc()) 299 errors_found += 1 300 301 return errors_found 302 303 # Print all of the checks captured in the doc 304 def print_checks(self): 305 print('Checks captured in doc:\n%s' % ('\n\t'.join(self.enum_list))) 306 307def main(argv=None): 308 # Parse args 309 opts = handle_args() 310 # Create parser for layer files 311 layer_parser = LayerParser(opts.in_headers, opts.in_source) 312 # Parse files into internal data structs 313 layer_parser.parse() 314 315 # Generate requested types of output 316 if opts.print_structs: # Print details of internal data structs 317 layer_parser.print_structs() 318 319 layer_doc = LayerDoc(opts.layer_doc) 320 layer_doc.parse() 321 if opts.print_doc_checks: 322 layer_doc.print_checks() 323 324 if opts.validate: 325 num_errors = layer_doc.validate(layer_parser.layer_dict) 326 if (0 == num_errors): 327 txt_color = bcolors() 328 print(txt_color.green() + 'No mismatches found between %s and implementation' % (os.path.basename(opts.layer_doc)) + txt_color.endc()) 329 else: 330 return num_errors 331 return 0 332 333if __name__ == "__main__": 334 sys.exit(main()) 335 336