hcttest-samples.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. # Copyright (C) Microsoft Corporation. All rights reserved.
  2. # This file is distributed under the University of Illinois Open Source License. See LICENSE.TXT for details.
  3. import argparse
  4. import glob
  5. import shutil
  6. import os
  7. import subprocess
  8. import re
  9. try:
  10. import xml.etree.cElementTree as ET
  11. except ImportError:
  12. import xml.etree.ElementTree as ET
  13. # Comment namespace to make working with ElementTree easier:
  14. def ReadXmlString(text):
  15. global xmlheader
  16. text = text.replace('xmlns=', 'xmlns_commented=')
  17. xmlheader = text[:text.find('\n')+1]
  18. # TODO: read xml and return xml root
  19. return ET.fromstring(text)
  20. def WriteXmlString(root):
  21. global xmlheader
  22. text = ET.tostring(root, encoding="utf-8")
  23. return xmlheader + text.replace('xmlns_commented=', 'xmlns=')
  24. def DeepCopyElement(e):
  25. cpy = ET.Element(e.tag, e.attrib)
  26. cpy.text = e.text
  27. cpy.tail = e.tail
  28. for child in e:
  29. cpy.append(DeepCopyElement(child))
  30. return cpy
  31. sample_names = [
  32. "D3D1211On12",
  33. "D3D12Bundles",
  34. "D3D12DynamicIndexing",
  35. "D3D12ExecuteIndirect",
  36. "D3D12Fullscreen",
  37. # "D3D12HelloWorld",
  38. "D3D12HelloWorld\\src\\HelloBundles",
  39. "D3D12HelloWorld\\src\\HelloConstBuffers",
  40. "D3D12HelloWorld\\src\\HelloFrameBuffering",
  41. "D3D12HelloWorld\\src\\HelloTexture",
  42. "D3D12HelloWorld\\src\\HelloTriangle",
  43. "D3D12HelloWorld\\src\\HelloWindow",
  44. "D3D12HeterogeneousMultiadapter",
  45. # WarpAssert in ReadPointerOperand: pInfoSrc->getName().startswith("dx.var.x").
  46. # Fix tested - incorrect rendered result produced.
  47. # Worked around in sample shaders for now by moving to non-static locals.
  48. "D3D12Multithreading", # works and is visually impressive!
  49. "D3D12nBodyGravity", # expected empty cbuffer culling due to static members causes this to fail! FIXED!
  50. "D3D12PipelineStateCache", # Requires workaround for static globals
  51. "D3D12PredicationQueries",
  52. "D3D12ReservedResources",
  53. "D3D12Residency", # has problems on x86 due to mem limitations. The following seems to help:
  54. # change NumTextures to 1024 * 1, and add min with 1GB here:
  55. # UINT64 memoryToUse = UINT64 (float(min(memoryInfo.Budget, UINT64(1 << 30))) * 0.95f);
  56. # Still produces a bunch of these errors:
  57. # 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]
  58. # but at least it no longer crashes
  59. "D3D12SmallResources"]
  60. class Sample(object):
  61. def __init__(self, name):
  62. self.name = name
  63. self.preBuild = []
  64. self.postBuild = []
  65. samples = dict([(name, Sample(name)) for name in sample_names])
  66. def SetSampleActions():
  67. # Actions called with (name, args, dxil)
  68. # Actions for all projects:
  69. for name, sample in samples.items():
  70. sample.postBuild.append(ActionCopyD3DBins)
  71. sample.postBuild.append(ActionCopySDKLayers)
  72. sample.postBuild.append(ActionCopyWarp12)
  73. sample.postBuild.append(ActionCopyCompilerBins) # Do this for all projects for now
  74. # TODO: limit ActionCopyCompilerBins action to ones that do run-time compilation.
  75. # # Runtime HLSL compilation:
  76. # for name in [ "D3D1211On12",
  77. # "D3D12HelloWorld\\src\\HelloTriangle",
  78. # "D3D12ExecuteIndirect",
  79. # ]:
  80. # samples[name].postBuild.append(ActionCopyCompilerBins)
  81. def CopyFiles(source_path, dest_path, filenames, symbols=False):
  82. for filename in filenames:
  83. renamed = filename
  84. try:
  85. filename, renamed = filename.split(';')
  86. print('Copying %s from %s to %s' % (filename, source_path, dest_path))
  87. print('.. with new name: %s' % (renamed))
  88. except:
  89. print('Copying %s from %s to %s' % (filename, source_path, dest_path))
  90. try:
  91. shutil.copy2(os.path.join(source_path, filename), os.path.join(dest_path, renamed))
  92. except:
  93. print('Error copying "%s" from "%s" to "%s"' % (filename, source_path, dest_path))
  94. # sys.excepthook(*sys.exc_info())
  95. continue
  96. if symbols and (filename.endswith('.exe') or filename.endswith('.dll')):
  97. symbol_filename = filename[:-4] + '.pdb'
  98. try:
  99. shutil.copy2(os.path.join(source_path, symbol_filename), os.path.join(dest_path, symbol_filename))
  100. except:
  101. print('Error copying symbols "%s" from "%s" to "%s"' % (symbol_filename, source_path, dest_path))
  102. def CopyBins(args, name, dxil, filenames, symbols=False):
  103. samples_path = args.samples
  104. config = dxil and 'DxilDebug' or 'Debug'
  105. CopyFiles(args.bins, os.path.join(PathToSampleSrc(samples_path, name), 'bin', args.arch, config), filenames, symbols)
  106. def ActionCopyCompilerBins(args, name, dxil):
  107. if dxil:
  108. CopyBins(args, name, dxil, [
  109. 'dxcompiler.dll',
  110. 'dxil.dll',
  111. 'd3dcompiler_dxc_bridge.dll;d3dcompiler_47.dll', # Wrapper version that calls into dxcompiler.dll
  112. ], args.symbols)
  113. def ActionCopyD3DBins(args, name, dxil):
  114. if dxil:
  115. CopyBins(args, name, dxil, [
  116. 'd3d12.dll',
  117. ], args.symbols)
  118. def ActionCopySDKLayers(args, name, dxil):
  119. CopyBins(args, name, dxil, [
  120. 'D3D11_3SDKLayers.dll',
  121. 'D3D12SDKLayers.dll',
  122. 'DXGIDebug.dll',
  123. ], args.symbols)
  124. def ActionCopyWarp12(args, name, dxil):
  125. CopyBins(args, name, dxil, [
  126. 'd3d12warp.dll',
  127. ], args.symbols)
  128. def MakeD3D12WarpCopy(bin_path):
  129. # Copy d3d10warp.dll to d3d12warp.dll
  130. shutil.copy2(os.path.join(bin_path, 'd3d10warp.dll'), os.path.join(bin_path, 'd3d12warp.dll'))
  131. def PathSplitAll(p):
  132. s = filter(None, os.path.split(p))
  133. if len(s) > 1:
  134. return PathSplitAll(s[0]) + (s[1],)
  135. else:
  136. return (s[0],)
  137. def GetBinPath(args, name):
  138. return os.path.join(args.bins, name)
  139. def GetProjectBinFilePath(args, samples_path, sample_name, file_name):
  140. src = PathToSampleSrc(samples_path, sample_name)
  141. return os.path.join(src, "bin", args.arch, file_name)
  142. def ListRuntimeCompilePaths(args):
  143. return [os.path.join(args.bins, name) for name in [
  144. 'fxc.exe',
  145. 'dxc.exe',
  146. 'dxcompiler.dll',
  147. 'dxil.dll',
  148. 'd3dcompiler_47.dll',
  149. 'd3d12.dll',
  150. 'D3D11_3SDKLayers.dll',
  151. 'D3D12SDKLayers.dll',
  152. 'DXGIDebug.dll',
  153. 'd3d12warp.dll',
  154. ]]
  155. def CheckEnvironment(args):
  156. if not args.bins:
  157. print("The -bins argument is needed to populate tool binaries.")
  158. exit(1)
  159. if not os.path.exists(args.bins):
  160. print("The -bins argument '" + args.bins + "' does not exist.")
  161. exit(1)
  162. for fn in ListRuntimeCompilePaths(args):
  163. if not os.path.exists(fn):
  164. print("Expected file '" + fn + "' not found.")
  165. exit(1)
  166. if os.path.getmtime(GetBinPath(args, 'fxc.exe')) != os.path.getmtime(GetBinPath(args, 'dxc.exe')):
  167. print("fxc.exe should be a copy of dxc.exe.")
  168. print("Please copy " + GetBinPath(args, 'dxc.exe') + " " + GetBinPath(args, 'fxc.exe'))
  169. exit(1)
  170. try:
  171. msbuild_version = subprocess.check_output(["msbuild", "-nologo", "-ver"])
  172. print("msbuild version: " + msbuild_version)
  173. except Exception as E:
  174. print("Unable to get the version from msbuild: " + str(E))
  175. print("This command should be run from a Developer Command Prompt")
  176. exit(1)
  177. def SampleIsNested(name):
  178. return "\\src\\" in name
  179. def PathToSampleSrc(basePath, sampleName):
  180. if SampleIsNested(sampleName):
  181. return os.path.join(basePath, "Samples", "Desktop", sampleName)
  182. return os.path.join(basePath, "Samples", "Desktop", sampleName, "src")
  183. reConfig = r'(Debug|Release|DxilDebug|DxilRelease)\|(Win32|x64)'
  184. def AddProjectConfigs(root, args):
  185. rxConfig = re.compile(reConfig)
  186. changed = False
  187. for e in root.findall('.//WindowsTargetPlatformVersion'):
  188. if e.text == '10.0.10240.0':
  189. e.text = '10.0.10586.0'
  190. changed = True
  191. # Override fxc path for Dxil configs:
  192. for config in ['DxilDebug', 'DxilRelease']:
  193. for arch in ['Win32', 'x64']:
  194. if not root.find('''./PropertyGroup[@Condition="'$(Configuration)|$(Platform)'=='%s|%s'"]/FXCToolPath''' % (config, arch)):
  195. e = ET.Element('PropertyGroup', {'Condition': "'$(Configuration)|$(Platform)'=='%s|%s'" % (config, arch)})
  196. e.text = '\n '
  197. e.tail = '\n '
  198. root.insert(0, e)
  199. e = ET.SubElement(e, 'FXCToolPath')
  200. e.text = '$(DXC_BIN_PATH)' # args.bins
  201. e.tail = '\n '
  202. # Extend ProjectConfiguration for Win32 and Dxil configs
  203. ig = root.find('./ItemGroup[@Label="ProjectConfigurations"]') or []
  204. debug_config = release_config = None # ProjectConfiguration
  205. configs = {}
  206. for e in ig:
  207. try:
  208. m = rxConfig.match(e.attrib['Include'])
  209. if m:
  210. key = m.groups()
  211. configs[key] = e
  212. if m.group(1) == 'Debug':
  213. debug_config = key, e
  214. elif m.group(1) == 'Release':
  215. release_config = key, e
  216. except:
  217. continue
  218. parents = root.findall('''.//*[@Condition="'$(Configuration)|$(Platform)'=='%s|%s'"]/..''' % ('Debug', 'x64'))
  219. if not configs or not debug_config or not release_config:
  220. print('No ProjectConfigurations found')
  221. return False
  222. for arch in ['Win32', 'x64']:
  223. for config in ['Debug', 'DxilDebug', 'Release', 'DxilRelease']:
  224. if config in ('Debug', 'DxilDebug'):
  225. t_config = 'Debug', 'x64'
  226. else:
  227. t_config = 'Release', 'x64'
  228. config_condition = "'$(Configuration)|$(Platform)'=='%s|%s'" % t_config
  229. if not configs.get((config, arch), None):
  230. changed = True
  231. if config in ('Debug', 'DxilDebug'):
  232. e = DeepCopyElement(debug_config[1])
  233. else:
  234. e = DeepCopyElement(release_config[1])
  235. e.set('Include', '%s|%s' % (config, arch))
  236. e.find('./Configuration').text = config
  237. e.find('./Platform').text = arch
  238. ig.append(e)
  239. for parent in reversed(parents):
  240. for n, e in reversed(list(enumerate(parent))):
  241. try:
  242. cond = e.attrib['Condition']
  243. except KeyError:
  244. continue
  245. if cond == config_condition:
  246. e = DeepCopyElement(e)
  247. # Override DisableOptimizations for DxilDebug since this flag is problematic right now
  248. if e.tag == 'ItemDefinitionGroup' and config == 'DxilDebug':
  249. FxCompile = e.find('./FxCompile') or ET.SubElement(e, 'FxCompile')
  250. DisableOptimizations = FxCompile.find('./DisableOptimizations') or ET.SubElement(FxCompile, 'DisableOptimizations')
  251. DisableOptimizations.text = 'false'
  252. e.attrib['Condition'] = "'$(Configuration)|$(Platform)'=='%s|%s'" % (config, arch)
  253. parent.insert(n+1, e)
  254. return changed
  255. def AddSlnConfigs(sln_text):
  256. # sln: GlobalSection(SolutionConfigurationPlatforms)
  257. rxSlnConfig = re.compile(r'^\s+%s = \1\|\2$' % reConfig)
  258. # sln: GlobalSection(ProjectConfigurationPlatforms)
  259. rxActiveCfg = re.compile(r'^\s+\{[0-9A-Z-]+\}\.%s\.ActiveCfg = \1\|\2$' % reConfig)
  260. rxBuild = re.compile(r'^\s+\{[0-9A-Z-]+\}\.%s\.Build\.0 = \1\|\2$' % reConfig)
  261. sln_changed = []
  262. line_set = set(sln_text.splitlines())
  263. def add_line(lst, line):
  264. "Prevent duplicates from being added"
  265. if line not in line_set:
  266. lst.append(line)
  267. for line in sln_text.splitlines():
  268. if line == 'VisualStudioVersion = 14.0.23107.0':
  269. sln_changed.append('VisualStudioVersion = 14.0.25123.0')
  270. continue
  271. m = rxSlnConfig.match(line)
  272. if not m:
  273. m = rxActiveCfg.match(line)
  274. if not m:
  275. m = rxBuild.match(line)
  276. if m:
  277. sln_changed.append(line)
  278. config = m.group(1)
  279. if config in ('Debug', 'Release') and m.group(2) == 'x64':
  280. add_line(sln_changed, line.replace('x64', 'Win32'))
  281. add_line(sln_changed, line.replace(config, 'Dxil' + config))
  282. add_line(sln_changed, line.replace(config, 'Dxil' + config).replace('x64', 'Win32'))
  283. continue
  284. sln_changed.append(line)
  285. return '\n'.join(sln_changed)
  286. def PatchProjects(args):
  287. for name in sample_names + ['D3D12HelloWorld']:
  288. sample_path = PathToSampleSrc(args.samples, name)
  289. print("Patching " + name + " in " + sample_path)
  290. for proj_path in glob.glob(os.path.join(sample_path, "*.vcxproj")):
  291. # Consider looking for msbuild and other tool paths in registry, etg:
  292. # reg query HKLM\Software\Microsoft\MSBuild\ToolsVersions\14.0
  293. with open (proj_path, "r") as proj_file:
  294. proj_text = proj_file.read()
  295. root = ReadXmlString(proj_text)
  296. if AddProjectConfigs(root, args):
  297. changed_text = WriteXmlString(root)
  298. print("Patching the Windows SDK version in " + proj_path)
  299. with open (proj_path, "w") as proj_file:
  300. proj_file.write(changed_text)
  301. # Extend project configs in solution file
  302. for sln_path in glob.glob(os.path.join(sample_path, "*.sln")):
  303. with open (sln_path, "r") as sln_file:
  304. sln_text = sln_file.read()
  305. changed_text = AddSlnConfigs(sln_text)
  306. if changed_text != sln_text:
  307. print("Adding additional configurations to " + sln_path)
  308. with open (sln_path, "w") as sln_file:
  309. sln_file.write(changed_text)
  310. def BuildSample(samples_path, name, x86, dxil):
  311. sample_path = PathToSampleSrc(samples_path, name)
  312. if not SampleIsNested(name):
  313. print("Building " + name + " in " + sample_path)
  314. Platform = x86 and 'Win32' or 'x64'
  315. Configuration = dxil and 'DxilDebug' or 'Debug'
  316. subprocess.check_call(["msbuild", "-nologo",
  317. '/p:Configuration=%s;Platform=%s' % (Configuration, Platform),
  318. '/t:Rebuild'],
  319. cwd=sample_path)
  320. def BuildSamples(args, dxil):
  321. samples_path = args.samples
  322. rxSample = args.sample and re.compile(args.sample, re.I)
  323. buildHelloWorld = False
  324. os.environ['DXC_BIN_PATH'] = args.bins
  325. for sample_name in sample_names:
  326. if rxSample and not rxSample.match(sample_name):
  327. continue
  328. if SampleIsNested(sample_name):
  329. buildHelloWorld = True
  330. continue
  331. BuildSample(samples_path, sample_name, args.x86, dxil)
  332. if buildHelloWorld:
  333. # HelloWorld containts sub-samples that must be built from the solution file.
  334. BuildSample(samples_path, "D3D12HelloWorld", args.x86, dxil)
  335. def PatchSample(args, name, dxil, afterBuild):
  336. if args.sample and not re.match(args.sample, name, re.I):
  337. return
  338. try:
  339. sample = samples[name]
  340. except:
  341. print("Error: selected sample missing from sample map '" + name + "'.")
  342. return
  343. if afterBuild: actions = sample.postBuild
  344. else: actions = sample.preBuild
  345. for action in actions:
  346. action(args, name, dxil)
  347. def PatchSamplesAfterBuild(args, dxil):
  348. for sample_name in sample_names:
  349. PatchSample(args, sample_name, dxil, True)
  350. def PatchSamplesBeforeBuild(args, dxil):
  351. for sample_name in sample_names:
  352. PatchSample(args, sample_name, dxil, False)
  353. def RunSampleTests(args):
  354. CheckEnvironment(args)
  355. print("Building Debug config ...")
  356. BuildSamples(args, False)
  357. print("Building DxilDebug config ...")
  358. BuildSamples(args, True)
  359. print("Applying patch to post-build binaries to enable dxc ...")
  360. PatchSamplesAfterBuild(args, False)
  361. PatchSamplesAfterBuild(args, True)
  362. print("TODO - run Debug config vs. DxilDebug config and verify results are the same")
  363. if __name__ == "__main__":
  364. parser = argparse.ArgumentParser(description="Run D3D sample tests...")
  365. parser.add_argument("-x86", action="store_true", help="add x86 targets")
  366. parser.add_argument("-samples", help="path to root of D3D12 samples")
  367. parser.add_argument("-bins", help="path to dxcompiler.dll and related binaries")
  368. parser.add_argument("-sample", help="choose a single sample to build/test (* wildcard supported)")
  369. parser.add_argument("-postbuild", action="store_true", help="only perform post-build operations")
  370. parser.add_argument("-patch", action="store_true", help="patch projects")
  371. parser.add_argument("-symbols", action="store_true", help="try to copy symbols for various dependencies")
  372. args = parser.parse_args()
  373. SetSampleActions()
  374. if args.x86:
  375. args.arch = "Win32"
  376. else:
  377. args.arch = "x64"
  378. if not args.samples:
  379. print("The -samples option must be used to indicate the root of D3D12 Samples.")
  380. print("Samples are available at this URL.")
  381. print("https://github.com/Microsoft/DirectX-Graphics-Samples")
  382. exit(1)
  383. if args.sample:
  384. print('Applying sample filter: %s' % args.sample)
  385. args.sample = re.escape(args.sample).replace('\\*', '.*')
  386. rxSample = re.compile(args.sample, re.I)
  387. for name in samples:
  388. if rxSample.match(name):
  389. print(' %s' % name)
  390. if args.postbuild:
  391. print("Applying patch to post-build binaries to enable dxc ...")
  392. PatchSamplesAfterBuild(args, False)
  393. PatchSamplesAfterBuild(args, True)
  394. elif args.patch:
  395. print("Patching projects ...")
  396. PatchProjects(args)
  397. else:
  398. RunSampleTests(args)