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