blob: 9f54ab2d2f3739a4456227c2620d2fbf5b2363fb [file] [log] [blame] [edit]
# 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)