# Copyright (C) Microsoft Corporation. All rights reserved. # This file is distributed under the University of Illinois Open Source License. See LICENSE.TXT for details. import argparse import glob import shutil import os import subprocess import re try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET # Comment namespace to make working with ElementTree easier: def ReadXmlString(text): global xmlheader text = text.replace('xmlns=', 'xmlns_commented=') xmlheader = text[:text.find('\n')+1] # TODO: read xml and return xml root return ET.fromstring(text) def WriteXmlString(root): global xmlheader text = ET.tostring(root, encoding="utf-8") return xmlheader + text.replace('xmlns_commented=', 'xmlns=') def DeepCopyElement(e): cpy = ET.Element(e.tag, e.attrib) cpy.text = e.text cpy.tail = e.tail for child in e: cpy.append(DeepCopyElement(child)) return cpy sample_names = [ "D3D1211On12", "D3D12Bundles", "D3D12DynamicIndexing", "D3D12ExecuteIndirect", "D3D12Fullscreen", # "D3D12HelloWorld", "D3D12HelloWorld\\src\\HelloBundles", "D3D12HelloWorld\\src\\HelloConstBuffers", "D3D12HelloWorld\\src\\HelloFrameBuffering", "D3D12HelloWorld\\src\\HelloTexture", "D3D12HelloWorld\\src\\HelloTriangle", "D3D12HelloWorld\\src\\HelloWindow", "D3D12HeterogeneousMultiadapter", # WarpAssert in ReadPointerOperand: pInfoSrc->getName().startswith("dx.var.x"). # Fix tested - incorrect rendered result produced. # Worked around in sample shaders for now by moving to non-static locals. "D3D12Multithreading", # works and is visually impressive! "D3D12nBodyGravity", # expected empty cbuffer culling due to static members causes this to fail! FIXED! "D3D12PipelineStateCache", # Requires workaround for static globals "D3D12PredicationQueries", "D3D12ReservedResources", "D3D12Residency", # has problems on x86 due to mem limitations. The following seems to help: # change NumTextures to 1024 * 1, and add min with 1GB here: # UINT64 memoryToUse = UINT64 (float(min(memoryInfo.Budget, UINT64(1 << 30))) * 0.95f); # Still produces a bunch of these errors: # D3D12 ERROR: ID3D12CommandAllocator::Reset: A command allocator is being reset before previous executions associated with the allocator have completed. [ EXECUTION ERROR #552: COMMAND_ALLOCATOR_SYNC] # but at least it no longer crashes "D3D12SmallResources"] class Sample(object): def __init__(self, name): self.name = name self.preBuild = [] self.postBuild = [] samples = dict([(name, Sample(name)) for name in sample_names]) def SetSampleActions(): # Actions called with (name, args, dxil) # Actions for all projects: for name, sample in samples.items(): sample.postBuild.append(ActionCopyD3DBins) sample.postBuild.append(ActionCopySDKLayers) sample.postBuild.append(ActionCopyWarp12) sample.postBuild.append(ActionCopyCompilerBins) # Do this for all projects for now # TODO: limit ActionCopyCompilerBins action to ones that do run-time compilation. # # Runtime HLSL compilation: # for name in [ "D3D1211On12", # "D3D12HelloWorld\\src\\HelloTriangle", # "D3D12ExecuteIndirect", # ]: # samples[name].postBuild.append(ActionCopyCompilerBins) def CopyFiles(source_path, dest_path, filenames, symbols=False): for filename in filenames: renamed = filename try: filename, renamed = filename.split(';') print('Copying %s from %s to %s' % (filename, source_path, dest_path)) print('.. with new name: %s' % (renamed)) except: print('Copying %s from %s to %s' % (filename, source_path, dest_path)) try: shutil.copy2(os.path.join(source_path, filename), os.path.join(dest_path, renamed)) except: print('Error copying "%s" from "%s" to "%s"' % (filename, source_path, dest_path)) # sys.excepthook(*sys.exc_info()) continue if symbols and (filename.endswith('.exe') or filename.endswith('.dll')): symbol_filename = filename[:-4] + '.pdb' try: shutil.copy2(os.path.join(source_path, symbol_filename), os.path.join(dest_path, symbol_filename)) except: print('Error copying symbols "%s" from "%s" to "%s"' % (symbol_filename, source_path, dest_path)) def CopyBins(args, name, dxil, filenames, symbols=False): samples_path = args.samples config = dxil and 'DxilDebug' or 'Debug' CopyFiles(args.bins, os.path.join(PathToSampleSrc(samples_path, name), 'bin', args.arch, config), filenames, symbols) def ActionCopyCompilerBins(args, name, dxil): if dxil: CopyBins(args, name, dxil, [ 'dxcompiler.dll', 'dxil.dll', 'd3dcompiler_dxc_bridge.dll;d3dcompiler_47.dll', # Wrapper version that calls into dxcompiler.dll ], args.symbols) def ActionCopyD3DBins(args, name, dxil): if dxil: CopyBins(args, name, dxil, [ 'd3d12.dll', ], args.symbols) def ActionCopySDKLayers(args, name, dxil): CopyBins(args, name, dxil, [ 'D3D11_3SDKLayers.dll', 'D3D12SDKLayers.dll', 'DXGIDebug.dll', ], args.symbols) def ActionCopyWarp12(args, name, dxil): CopyBins(args, name, dxil, [ 'd3d12warp.dll', ], args.symbols) def MakeD3D12WarpCopy(bin_path): # Copy d3d10warp.dll to d3d12warp.dll shutil.copy2(os.path.join(bin_path, 'd3d10warp.dll'), os.path.join(bin_path, 'd3d12warp.dll')) def PathSplitAll(p): s = filter(None, os.path.split(p)) if len(s) > 1: return PathSplitAll(s[0]) + (s[1],) else: return (s[0],) def GetBinPath(args, name): return os.path.join(args.bins, name) def GetProjectBinFilePath(args, samples_path, sample_name, file_name): src = PathToSampleSrc(samples_path, sample_name) return os.path.join(src, "bin", args.arch, file_name) def ListRuntimeCompilePaths(args): return [os.path.join(args.bins, name) for name in [ 'fxc.exe', 'dxc.exe', 'dxcompiler.dll', 'dxil.dll', 'd3dcompiler_47.dll', 'd3d12.dll', 'D3D11_3SDKLayers.dll', 'D3D12SDKLayers.dll', 'DXGIDebug.dll', 'd3d12warp.dll', ]] def CheckEnvironment(args): if not args.bins: print("The -bins argument is needed to populate tool binaries.") exit(1) if not os.path.exists(args.bins): print("The -bins argument '" + args.bins + "' does not exist.") exit(1) for fn in ListRuntimeCompilePaths(args): if not os.path.exists(fn): print("Expected file '" + fn + "' not found.") exit(1) if os.path.getmtime(GetBinPath(args, 'fxc.exe')) != os.path.getmtime(GetBinPath(args, 'dxc.exe')): print("fxc.exe should be a copy of dxc.exe.") print("Please copy " + GetBinPath(args, 'dxc.exe') + " " + GetBinPath(args, 'fxc.exe')) exit(1) try: msbuild_version = subprocess.check_output(["msbuild", "-nologo", "-ver"]) print("msbuild version: " + msbuild_version) except Exception as E: print("Unable to get the version from msbuild: " + str(E)) print("This command should be run from a Developer Command Prompt") exit(1) def SampleIsNested(name): return "\\src\\" in name def PathToSampleSrc(basePath, sampleName): if SampleIsNested(sampleName): return os.path.join(basePath, "Samples", "Desktop", sampleName) return os.path.join(basePath, "Samples", "Desktop", sampleName, "src") reConfig = r'(Debug|Release|DxilDebug|DxilRelease)\|(Win32|x64)' def AddProjectConfigs(root, args): rxConfig = re.compile(reConfig) changed = False for e in root.findall('.//WindowsTargetPlatformVersion'): if e.text == '10.0.10240.0': e.text = '10.0.10586.0' changed = True # Override fxc path for Dxil configs: for config in ['DxilDebug', 'DxilRelease']: for arch in ['Win32', 'x64']: if not root.find('''./PropertyGroup[@Condition="'$(Configuration)|$(Platform)'=='%s|%s'"]/FXCToolPath''' % (config, arch)): e = ET.Element('PropertyGroup', {'Condition': "'$(Configuration)|$(Platform)'=='%s|%s'" % (config, arch)}) e.text = '\n ' e.tail = '\n ' root.insert(0, e) e = ET.SubElement(e, 'FXCToolPath') e.text = '$(DXC_BIN_PATH)' # args.bins e.tail = '\n ' # Extend ProjectConfiguration for Win32 and Dxil configs ig = root.find('./ItemGroup[@Label="ProjectConfigurations"]') or [] debug_config = release_config = None # ProjectConfiguration configs = {} for e in ig: try: m = rxConfig.match(e.attrib['Include']) if m: key = m.groups() configs[key] = e if m.group(1) == 'Debug': debug_config = key, e elif m.group(1) == 'Release': release_config = key, e except: continue parents = root.findall('''.//*[@Condition="'$(Configuration)|$(Platform)'=='%s|%s'"]/..''' % ('Debug', 'x64')) if not configs or not debug_config or not release_config: print('No ProjectConfigurations found') return False for arch in ['Win32', 'x64']: for config in ['Debug', 'DxilDebug', 'Release', 'DxilRelease']: if config in ('Debug', 'DxilDebug'): t_config = 'Debug', 'x64' else: t_config = 'Release', 'x64' config_condition = "'$(Configuration)|$(Platform)'=='%s|%s'" % t_config if not configs.get((config, arch), None): changed = True if config in ('Debug', 'DxilDebug'): e = DeepCopyElement(debug_config[1]) else: e = DeepCopyElement(release_config[1]) e.set('Include', '%s|%s' % (config, arch)) e.find('./Configuration').text = config e.find('./Platform').text = arch ig.append(e) for parent in reversed(parents): for n, e in reversed(list(enumerate(parent))): try: cond = e.attrib['Condition'] except KeyError: continue if cond == config_condition: e = DeepCopyElement(e) # Override DisableOptimizations for DxilDebug since this flag is problematic right now if e.tag == 'ItemDefinitionGroup' and config == 'DxilDebug': FxCompile = e.find('./FxCompile') or ET.SubElement(e, 'FxCompile') DisableOptimizations = FxCompile.find('./DisableOptimizations') or ET.SubElement(FxCompile, 'DisableOptimizations') DisableOptimizations.text = 'false' e.attrib['Condition'] = "'$(Configuration)|$(Platform)'=='%s|%s'" % (config, arch) parent.insert(n+1, e) return changed def AddSlnConfigs(sln_text): # sln: GlobalSection(SolutionConfigurationPlatforms) rxSlnConfig = re.compile(r'^\s+%s = \1\|\2$' % reConfig) # sln: GlobalSection(ProjectConfigurationPlatforms) rxActiveCfg = re.compile(r'^\s+\{[0-9A-Z-]+\}\.%s\.ActiveCfg = \1\|\2$' % reConfig) rxBuild = re.compile(r'^\s+\{[0-9A-Z-]+\}\.%s\.Build\.0 = \1\|\2$' % reConfig) sln_changed = [] line_set = set(sln_text.splitlines()) def add_line(lst, line): "Prevent duplicates from being added" if line not in line_set: lst.append(line) for line in sln_text.splitlines(): if line == 'VisualStudioVersion = 14.0.23107.0': sln_changed.append('VisualStudioVersion = 14.0.25123.0') continue m = rxSlnConfig.match(line) if not m: m = rxActiveCfg.match(line) if not m: m = rxBuild.match(line) if m: sln_changed.append(line) config = m.group(1) if config in ('Debug', 'Release') and m.group(2) == 'x64': add_line(sln_changed, line.replace('x64', 'Win32')) add_line(sln_changed, line.replace(config, 'Dxil' + config)) add_line(sln_changed, line.replace(config, 'Dxil' + config).replace('x64', 'Win32')) continue sln_changed.append(line) return '\n'.join(sln_changed) def PatchProjects(args): for name in sample_names + ['D3D12HelloWorld']: sample_path = PathToSampleSrc(args.samples, name) print("Patching " + name + " in " + sample_path) for proj_path in glob.glob(os.path.join(sample_path, "*.vcxproj")): # Consider looking for msbuild and other tool paths in registry, etg: # reg query HKLM\Software\Microsoft\MSBuild\ToolsVersions\14.0 with open (proj_path, "r") as proj_file: proj_text = proj_file.read() root = ReadXmlString(proj_text) if AddProjectConfigs(root, args): changed_text = WriteXmlString(root) print("Patching the Windows SDK version in " + proj_path) with open (proj_path, "w") as proj_file: proj_file.write(changed_text) # Extend project configs in solution file for sln_path in glob.glob(os.path.join(sample_path, "*.sln")): with open (sln_path, "r") as sln_file: sln_text = sln_file.read() changed_text = AddSlnConfigs(sln_text) if changed_text != sln_text: print("Adding additional configurations to " + sln_path) with open (sln_path, "w") as sln_file: sln_file.write(changed_text) def BuildSample(samples_path, name, x86, dxil): sample_path = PathToSampleSrc(samples_path, name) if not SampleIsNested(name): print("Building " + name + " in " + sample_path) Platform = x86 and 'Win32' or 'x64' Configuration = dxil and 'DxilDebug' or 'Debug' subprocess.check_call(["msbuild", "-nologo", '/p:Configuration=%s;Platform=%s' % (Configuration, Platform), '/t:Rebuild'], cwd=sample_path) def BuildSamples(args, dxil): samples_path = args.samples rxSample = args.sample and re.compile(args.sample, re.I) buildHelloWorld = False os.environ['DXC_BIN_PATH'] = args.bins for sample_name in sample_names: if rxSample and not rxSample.match(sample_name): continue if SampleIsNested(sample_name): buildHelloWorld = True continue BuildSample(samples_path, sample_name, args.x86, dxil) if buildHelloWorld: # HelloWorld containts sub-samples that must be built from the solution file. BuildSample(samples_path, "D3D12HelloWorld", args.x86, dxil) def PatchSample(args, name, dxil, afterBuild): if args.sample and not re.match(args.sample, name, re.I): return try: sample = samples[name] except: print("Error: selected sample missing from sample map '" + name + "'.") return if afterBuild: actions = sample.postBuild else: actions = sample.preBuild for action in actions: action(args, name, dxil) def PatchSamplesAfterBuild(args, dxil): for sample_name in sample_names: PatchSample(args, sample_name, dxil, True) def PatchSamplesBeforeBuild(args, dxil): for sample_name in sample_names: PatchSample(args, sample_name, dxil, False) def RunSampleTests(args): CheckEnvironment(args) print("Building Debug config ...") BuildSamples(args, False) print("Building DxilDebug config ...") BuildSamples(args, True) print("Applying patch to post-build binaries to enable dxc ...") PatchSamplesAfterBuild(args, False) PatchSamplesAfterBuild(args, True) print("TODO - run Debug config vs. DxilDebug config and verify results are the same") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run D3D sample tests...") parser.add_argument("-x86", action="store_true", help="add x86 targets") parser.add_argument("-samples", help="path to root of D3D12 samples") parser.add_argument("-bins", help="path to dxcompiler.dll and related binaries") parser.add_argument("-sample", help="choose a single sample to build/test (* wildcard supported)") parser.add_argument("-postbuild", action="store_true", help="only perform post-build operations") parser.add_argument("-patch", action="store_true", help="patch projects") parser.add_argument("-symbols", action="store_true", help="try to copy symbols for various dependencies") args = parser.parse_args() SetSampleActions() if args.x86: args.arch = "Win32" else: args.arch = "x64" if not args.samples: print("The -samples option must be used to indicate the root of D3D12 Samples.") print("Samples are available at this URL.") print("https://github.com/Microsoft/DirectX-Graphics-Samples") exit(1) if args.sample: print('Applying sample filter: %s' % args.sample) args.sample = re.escape(args.sample).replace('\\*', '.*') rxSample = re.compile(args.sample, re.I) for name in samples: if rxSample.match(name): print(' %s' % name) if args.postbuild: print("Applying patch to post-build binaries to enable dxc ...") PatchSamplesAfterBuild(args, False) PatchSamplesAfterBuild(args, True) elif args.patch: print("Patching projects ...") PatchProjects(args) else: RunSampleTests(args)