mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-08-29 09:56:39 +00:00
* We enforce default and copy constructors where possible, this isn't very pythonic but we can't use copy.deepcopy on our types. * The structs that have specialised constructors e.g. FloatVector or Subresource also have those documented though we have no way to enforce it.
468 lines
18 KiB
Python
468 lines
18 KiB
Python
#!/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"]
|
|
|
|
if sys.platform == 'win32' and sys.version_info[1] >= 8:
|
|
for sub in ['Release', 'Development']:
|
|
path = os.path.abspath(binpath + sub)
|
|
if os.path.exists(path):
|
|
os.add_dll_directory(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_typeval(ret: str, pattern: bool, typelist: List[str]):
|
|
orig_type = ret
|
|
|
|
# strip namespace
|
|
if ret[0:10] == 'renderdoc.':
|
|
ret = ret[10:].replace('.', '::')
|
|
|
|
# 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 == 'None':
|
|
ret = 'NULL'
|
|
elif ret == 'True':
|
|
ret = 'true'
|
|
elif ret == 'False':
|
|
ret = 'false'
|
|
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 = r'(std::function<void\(\)>|[A-Za-z_]+Callback)' if pattern else 'std::function/NamedCallback'
|
|
elif ret[0:5] == 'List[':
|
|
inner = make_c_typeval(ret[5:-1], pattern, typelist)
|
|
ret = '(const )?rdcarray<{}> ?[&*]?'.format(inner) if pattern else 'rdcarray<{}>'.format(inner)
|
|
elif ret[0:6] == 'Tuple[':
|
|
inners = [make_c_typeval(i.strip(), pattern, typelist) for i in ret[6:-1].split(',')]
|
|
if pattern:
|
|
inner = r',\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 ([^:=]*) ([^: =]+)(\s*=[^:]*)?:")
|
|
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 = ['', '']
|
|
default_val = ''
|
|
for p in params:
|
|
if len(funcargs[0]) > 0:
|
|
funcargs[0] += r',\s*'
|
|
funcargs[1] += ', '
|
|
|
|
default_val = p[2].lstrip()
|
|
if len(default_val) > 0 and default_val[0] == '=':
|
|
default_val = make_c_typeval(default_val[1:].strip(), False, typelist)
|
|
|
|
funcargs[0] += make_c_typeval(p[0], True, typelist) + ' ?' + p[1]
|
|
funcargs[1] += make_c_typeval(p[0], False, typelist) + ' ' + p[1]
|
|
|
|
if default_val != "":
|
|
funcargs[0] += f"\\s*=\\s*{default_val}"
|
|
funcargs[1] += f" = {default_val}"
|
|
|
|
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 = r'(?s){} ?{}{}\(\s*{}\)'.format(make_c_typeval(ret, True, typelist), global_pattern, objname, funcargs[0])
|
|
clean = '{} {}({})'.format(make_c_typeval(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 = r'(?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 {}: {} couldn't be found in C++ headers.\nCould be wrong types or missing default values. Try --debug-mismatches mismatches/".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', '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 + r'(\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 + r'[^{]*\{.*?^}', namespace, re.MULTILINE | re.DOTALL)
|
|
|
|
source = source.group(0)
|
|
|
|
instance = None
|
|
copyable = False
|
|
try:
|
|
instance = obj()
|
|
try:
|
|
dupe_instance = obj(instance)
|
|
copyable = True
|
|
except NotImplementedError:
|
|
pass
|
|
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 types that we can create, we expect by default to
|
|
# see a default constructor and a copy constructor,
|
|
# unless we see a note that the type is not copyable
|
|
if instance is not None:
|
|
lines = docstring.strip().splitlines()
|
|
if lines[0].strip() != f"{obj.__name__}()":
|
|
count += 1
|
|
print(
|
|
f"Error {count:3}: {obj.__name__} can be created, "
|
|
"expect default constructor as first real line of its docstring."
|
|
)
|
|
elif (
|
|
copyable
|
|
and lines[1].strip() != f"{obj.__name__}(other: {obj.__name__})"
|
|
):
|
|
count += 1
|
|
print(
|
|
f"Error {count:3}: {obj.__name__} can be copied, "
|
|
"expect copy constructor as second entry in its docstring:\n"
|
|
f"Actual > {lines[1]}\n"
|
|
f"Expected > {obj.__name__}(other: {obj.__name__})"
|
|
)
|
|
|
|
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(r'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)
|