mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-04 00:50:40 +00:00
417 lines
16 KiB
Python
417 lines
16 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"]
|
|
|
|
# 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<void\(\)>|[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)
|