#!/usr/bin/env python3 # -*- coding: utf-8 -*- # import sys import re import os import glob import argparse import inspect from enum import EnumMeta from typing import List import struct os.chdir(os.path.realpath(os.path.dirname(__file__))) # path to module libraries for windows if struct.calcsize("P") == 8: binpath = '../x64/' else: binpath = '../Win32/' # Prioritise release over development builds sys.path.insert(0, os.path.abspath(binpath + 'Development/pymodules')) sys.path.insert(0, os.path.abspath(binpath + 'Release/pymodules')) # Add the build paths to PATH so renderdoc.dll can be located os.environ["PATH"] = os.path.abspath(binpath + 'Development/') + os.pathsep + os.environ["PATH"] os.environ["PATH"] = os.path.abspath(binpath + 'Release/') + os.pathsep + os.environ["PATH"] # path to module libraries for linux sys.path.insert(0, os.path.abspath('../build/lib')) import renderdoc as rd import qrenderdoc as qrd parser = argparse.ArgumentParser() parser.add_argument('-p', '--path', help="Add a path to interface files to search (can be used multiple times)", action='append') parser.add_argument('-d', '--dump-combined', help="Set a path to dump the combined header that's being searched through", type=str) parser.add_argument('--debug-mismatches', help="Set a path to a folder, each mismatch creates a file in here with the failure", type=str) parser.add_argument('-v', '--verbose', help="Run verbosely", action="store_true") args = parser.parse_args() paths = ['../renderdoc/api/replay', '../qrenderdoc/Code/Interface'] if args.path is not None: paths += args.path if args.verbose: print("Searching for interface files in: {}".format(paths)) headers = '' for path in paths: for f in glob.glob(os.path.join(os.path.abspath(path), '**', '*.h'), recursive=True): if args.verbose: print("Adding interface file {}".format(f)) with open(f, 'r') as file: headers += file.read() if args.dump_combined is not None: with open(args.dump_combined, 'w') as file: file.write(headers) if args.debug_mismatches is not None and os.path.isdir(args.debug_mismatches): for f in glob.glob(os.path.join(os.path.abspath(args.debug_mismatches), '*')): if args.verbose: print("Removing {} in debug mismatches folder".format(f)) os.unlink(f) def make_c_type(ret: str, pattern: bool, typelist: List[str]): orig_type = ret # strip namespace if ret[0:10] == 'renderdoc.': ret = ret[10:] # Handle pipelines that are renamed if ret == 'D3D11State': ret = 'D3D11Pipe::State' elif ret == 'D3D12State': ret = 'D3D12Pipe::State' elif ret == 'GLState': ret = 'GLPipe::State' elif ret == 'VKState': ret = 'VKPipe::State' if ret in ['bool', 'void']: pass elif ret == 'str': ret = '(const )?rdc(inflexible)?str ?[&*]?' if pattern else 'rdcstr' elif ret == 'int': ret = '(u?int[163264]{2}|size)_t' if pattern else 'int' # ambiguous elif ret == 'float': ret = 'float|double' if pattern else 'float' elif ret == 'bytes': ret = '(const )?bytebuf ?[&*]?' if pattern else 'bytebuf' elif ret == 'List[Tuple[str,str]]': # special case ret = 'rdcstrpairs' elif ret == 'Tuple[str,str]': # special case ret = 'rdcstrpair' elif ret[0:9] == 'Callable[': ret = '(std::function|[A-Za-z_]+Callback)' if pattern else 'std::function/NamedCallback' elif ret[0:5] == 'List[': inner = make_c_type(ret[5:-1], pattern, typelist) ret = '(const )?rdcarray<{}> ?[&*]?'.format(inner) if pattern else 'rdcarray<{}>'.format(inner) elif ret[0:6] == 'Tuple[': inners = [make_c_type(i.strip(), pattern, typelist) for i in ret[6:-1].split(',')] if pattern: inner = ',\s*'.join(inners) else: inner = ', '.join(inners) tuple_len = len(inners) if tuple_len > 2 and len(list(set(inners))) == 1: inner = inners[0] ret = '(const )?rdcfixedarray<{}, {}> ?[&*]?'.format(inner, tuple_len) if pattern else 'rdcfixedarray<{}, {}>'.format(inner, tuple_len) else: ret = '(const )?rdcpair<{}> ?[&*]?'.format(inner) if pattern else 'rdcpair<{}>'.format(inner) elif pattern: if ret[-8:] == 'Callback': ret = '(RENDERDOC_)?{}'.format(ret) else: if orig_type not in typelist: typelist.append(orig_type) ret = '(const )?I?{} ?[&*]?'.format(ret) return ret RTYPE_PATTERN = re.compile(r":rtype: (.*)") PARAM_PATTERN = re.compile(r":param ([^:]*) ([^: ]*):") TYPE_PATTERN = re.compile(r":type: (.*)") DATA_PATTERN = re.compile(r"\.\. data:: (.*)") count = 0 def check_function(parent_name, objname, obj, source, global_func, typelist): global count, args if args.verbose: print("Checking {} function {}.{}".format('global' if global_func else 'member', parent_name, objname)) if obj.__class__ is staticmethod: obj = obj.__func__ docstring = obj.__doc__ params = PARAM_PATTERN.findall(docstring) funcargs = ['', ''] for p in params: if len(funcargs[0]) > 0: funcargs[0] += ',\s*' funcargs[1] += ', ' funcargs[0] += make_c_type(p[0], True, typelist) + ' ?' + p[1] + "(\s*=[^,]*)?" funcargs[1] += make_c_type(p[0], False, typelist) + ' ' + p[1] result = RTYPE_PATTERN.search(docstring) if result is not None: ret = result.group(1) else: ret = 'void' global_pattern = '' if global_func: global_pattern = '(RENDERDOC_CC\s*RENDERDOC_)?' pattern = '(?s){} ?{}{}\(\s*{}\)'.format(make_c_type(ret, True, typelist), global_pattern, objname, funcargs[0]) clean = '{} {}({})'.format(make_c_type(ret, False, typelist), objname, funcargs[1]) match = re.search(pattern, source, re.MULTILINE | re.DOTALL) pattern2 = None # global functions returning strings can't return an rdcstr, they have to return const char * if match is None and ret == 'str': pattern2 = '(?s)const char \*{}{}\(\s*{}\)'.format(global_pattern, objname, funcargs[0]) match = re.search(pattern2, source, re.MULTILINE | re.DOTALL) if match is None: count += 1 print("Error {:3} in {}: {}".format(count, parent_name, clean)) if args.debug_mismatches is not None and os.path.isdir(args.debug_mismatches): with open(os.path.join(os.path.abspath(args.debug_mismatches), '{:03}-{}.{}.txt'.format(count, parent_name, objname)), 'w') as file: file.write("# Failed to find matching declaration for {}.{}\n".format(parent_name, objname)) file.write("#\n") file.write("# Matched against {}\n".format(pattern)) if pattern2 is not None: file.write("# Matched against {}\n".format(pattern2)) file.write("#\n") file.write("#\n") file.write("\n") file.write("\n") file.write(source) def check_used_types(objname, module, used_types): global count if args.verbose: print('Checking {} referenced types: {}'.format(objname, module, used_types)) for t in used_types: type_name = t parent = module while True: if t in dir(parent): break # Allow some types that are opaque if parent == rd and t in ['ANativeWindow', 'NSView', 'CALayer', 'wl_display', 'wl_surface', 'HWND', 'xcb_connection_t', 'xcb_window_t', 'Display', 'Drawable']: break if parent == qrd and t in ['QWidget']: break idx = t.find('.') if idx >= 0: parent_name = t[0:idx] if parent_name in dir(parent): parent = parent.__dict__[parent_name] elif parent_name == 'renderdoc': parent = rd elif parent_name == 'qrenderdoc': parent = qrd t = t[idx+1:] continue count += 1 print("Error {:3} in {}: Unrecognised reference {}".format(count, objname, type_name)) if type_name in dir(rd): print(" - Maybe missing namespace to refer to renderdoc.{}?".format(type_name)) break check_mods = ['renderdoc', 'qrenderdoc'] for mod_name in check_mods: mod = sys.modules[mod_name] if args.verbose: print("===== Checks for {} =====".format(mod_name)) for objname in dir(mod): if re.search('__|SWIG|ResourceId_Null|rdcarray_of|Structured.*List', objname): continue # skip some functions that have special bindings and won't be easily found if objname in ['CreateRemoteServerConnection', 'DumpObject', 'GetDefaultCaptureOptions', 'GetSupportedDeviceProtocols']: if args.verbose: print("Skipping {}".format(objname)) continue obj = mod.__dict__[objname] docstring = obj.__doc__ if 'INTERNAL:' in docstring: continue qualname = '{}.{}'.format(mod_name, objname) if isinstance(obj, EnumMeta): if args.verbose: print("Skipping enum {}".format(qualname)) # don't check enums continue elif isinstance(obj, type): if args.verbose: print("Checking class {}".format(qualname)) # Grab the source to just this class to search in source = re.search('(struct|class|union) I?' + objname + '(\n|\s*:[^A-Za-z][\s:a-zA-Z]*\n)\{.*?^}', headers, re.MULTILINE | re.DOTALL) namespace = None if source is None and objname[0:2] in ['VK', 'GL']: pipe = objname[0:2] + 'Pipe' objname = objname[2:] namespace = re.search('namespace ' + pipe + '.* namespace ' + pipe, headers, re.MULTILINE | re.DOTALL) namespace = namespace.group(0) if source is None and objname[0:5] in ['D3D11', 'D3D12']: pipe = objname[0:5] + 'Pipe' objname = objname[5:] namespace = re.search('namespace ' + pipe + '.* namespace ' + pipe, headers, re.MULTILINE | re.DOTALL) namespace = namespace.group(0) if source is None and namespace is not None: source = re.search('(struct|class|union) I?' + objname + '[^{]*\{.*?^}', namespace, re.MULTILINE | re.DOTALL) source = source.group(0) instance = None try: instance = obj() except TypeError: pass # a couple of manual cases that need parameters if qualname == 'renderdoc.SDObject' and instance is None: instance = obj("", "") if qualname == 'renderdoc.SDChunk' and instance is None: instance = obj("") instance_warned = False for member_name in obj.__dict__.keys(): if '__' in member_name or member_name in ['this', 'thisown']: continue member = obj.__dict__[member_name] # Skip some known functions that cannot be easily matched this way if '{}.{}'.format(objname, member_name) in [ # pointer output remapped to tuple return 'RemoteHost.Connect', # class-local callbacks 'CaptureContext.EditShader', # Renamed in the interface from DuplicateAndAddChild 'SDObject.AddChild', # not defined in the actual code since we have typed AsInt32 etc. # Instead defined in the swig interface 'SDObject.AsInt', 'SDObject.AsFloat', 'SDObject.AsString']: if args.verbose: print("Skipping {}.{}".format(objname, member_name)) continue if callable(member): used_types = [] check_function(qualname, member_name, member, source, False, used_types) check_used_types('{}.{}'.format(qualname, member_name), mod, used_types) elif instance and '__get__' in dir(member): value = getattr(instance, member_name) type_name = type(value).__name__ if type(value).__module__ != mod_name: type_name = type(value).__module__ + '.' + type_name type_name = re.sub('(.*)rdcarray_of_(.*)', 'List[\\1\\2]', type_name) type_name = re.sub('(renderdoc\.)?u?int[163264]{2}_t', 'int', type_name) type_name = re.sub('(renderdoc\.)?rdcstr', 'str', type_name) type_name = re.sub('Pipe_', '', type_name) type_name = re.sub('StructuredBufferList', 'List[bytes]', type_name) type_name = re.sub('StructuredObjectList', 'List[SDObject]', type_name) type_name = re.sub('StructuredChunkList', 'List[SDChunk]', type_name) type_name = re.sub('^builtins.', '', type_name) if 'importlib._bootstrap' in type_name: type_name = re.sub('^importlib._bootstrap.', '', type_name) real_module = [ m for m in check_mods if type_name in sys.modules[m].__dict__ ][0] if real_module != mod_name: type_name = f"{real_module}.{type_name}" type_name = re.sub('datetime.datetime', 'datetime', type_name) if type_name == 'NoneType': if args.verbose: print(f"Skipping {objname}.{member_name} as pointer-assumed from None") continue if objname == 'ResourceId' and member_name == 'Null' and 'function' in type_name: if args.verbose: print("Skipping ResourceId") continue if args.verbose: print('Checking struct member {}.{}'.format(qualname, member_name)) result = TYPE_PATTERN.search(member.__doc__) if result is not None: type_decl = result.group(1) else: type_decl = None if type_decl is None: count += 1 print("Error {:3}: {}.{} is missing :type: declaration, should be {}".format(count, qualname, member_name, type_name)) else: type_decl = re.sub('Tuple\[.*\]', 'tuple', type_decl) if type_decl != type_name: count += 1 print("Error {:3}: {}.{} has wrong :type: declaration {}, should be {}".format(count, qualname, member_name, type_decl, type_name)) elif instance is None and '__get__' in dir(member): if not instance_warned: print(f"WARNING: Couldn't create a {qualname} to check some members") instance_warned = True elif type(member) == int: datas = DATA_PATTERN.findall(docstring) if member_name not in datas: count += 1 print("Error {:3}: {}.{} is missing a .. data: declaration in object docstring".format(count, qualname, member_name)) elif callable(obj): used_types = [] # check the function in all headers globally check_function(mod_name, objname, obj, headers, True, used_types) check_used_types(qualname, mod, used_types) if count > 0: print("{} problems detected".format(count)) sys.exit(count) print("No problems detected") sys.exit(0)