| # Copyright 2021 Google LLC. |
| # Copyright 2021 The Khronos Group Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # Requirements to run the script: |
| # - Python3 (apt-get install -y python3.x) |
| # - GO (apt-get install -y golang-go) |
| # - cmake (version 3.13 or later) |
| # - ninja (apt-get install -y ninja-build) |
| # - git (sudo apt-get install -y git) |
| |
| # GO dependencies needed: |
| # - crypto/openpgp (go get -u golang.org/x/crypto/openpgp...) |
| |
| import os |
| import json |
| import tempfile |
| import subprocess |
| import sys |
| |
| from argparse import ArgumentParser |
| from shutil import which, copyfile |
| from pathlib import Path |
| from datetime import datetime |
| |
| # Check for correct python version (python3) before doing anything. |
| if sys.version_info.major < 3: |
| raise RuntimeError("Python version needs to be 3 or greater.") |
| |
| AP = ArgumentParser() |
| AP.add_argument( |
| "-d", |
| "--directory", |
| metavar="DIRECTORY", |
| type=str, |
| help="Path to directory that will be used as root for cloning and file saving.", |
| default=str(Path(tempfile.gettempdir()) / "deqp-swiftshader") |
| ) |
| AP.add_argument( |
| "-u", |
| "--url", |
| metavar="URL", |
| type=str, |
| help="URL of SwiftShader Git repository.", |
| default="https://swiftshader.googlesource.com/SwiftShader", |
| ) |
| AP.add_argument( |
| "-l", |
| "--vlayer_url", |
| metavar="VURL", |
| type=str, |
| help="URL of Validation Layers Git repository.", |
| default="https://github.com/KhronosGroup/Vulkan-ValidationLayers.git", |
| ) |
| AP.add_argument( |
| "-b", |
| "--sws_build_type", |
| metavar="SWS_BUILD_TYPE", |
| type=str, |
| help="SwiftShader build type.", |
| choices=["debug", "release"], |
| default="debug", |
| ) |
| AP.add_argument( |
| "-q", |
| "--deqp_vk", |
| metavar="DEQP_VK", |
| type=str, |
| help="Path to deqp-vk binary.", |
| ) |
| AP.add_argument( |
| "-v", |
| "--vk_gl_cts", |
| metavar="VK_GL_CTS", |
| type=str, |
| help="Path to vk-gl-cts source directory.", |
| ) |
| AP.add_argument( |
| "-w", |
| "--vk_gl_cts_build", |
| metavar="VK_GL_CTS_BUILD", |
| type=str, |
| help="Path to vk-gl-cts build directory.", |
| default=str(Path(tempfile.gettempdir()) / "deqp-swiftshader" / "vk-gl-cts-build"), |
| ) |
| AP.add_argument( |
| "-t", |
| "--vk_gl_cts_build_type", |
| metavar="VK_GL_CTS_BUILD_TYPE", |
| type=str, |
| help="vk-gl-cts build type.", |
| choices=["debug", "release"], |
| default="debug", |
| ) |
| AP.add_argument( |
| "-r", |
| "--recipe", |
| metavar="RECIPE", |
| type=str, |
| help="Recipes to only run parts of script.", |
| choices=["run-deqp", "check-comparison"], |
| default="run-deqp", |
| ) |
| AP.add_argument( |
| "-f", |
| "--files", |
| nargs=2, |
| metavar=("NEWER_FILE_PATH", "OLDER_FILE_PATH"), |
| type=str, |
| help="Compare two different run results.", |
| ) |
| AP.add_argument( |
| "-a", |
| "--validation", |
| metavar="VALIDATION", |
| type=str, |
| help="Enable vulkan validation layers.", |
| choices=["true", "false"], |
| default="false", |
| ) |
| AP.add_argument( |
| "-o", |
| "--result_output", |
| metavar="OUTPUT", |
| type=str, |
| help="Filename of the regres results.", |
| default=str("result_" + str(datetime.now().strftime('%m_%d_%Y_%H_%M_%S')) + ".json"), |
| ) |
| |
| ARGS = AP.parse_args() |
| |
| # Check that we have everything needed to run the script when using recipe run-deqp. |
| if ARGS.recipe == "run-deqp": |
| if which("go") is None: |
| raise RuntimeError("go not found. (apt-get install -y golang-go)") |
| if which("cmake") is None: |
| raise RuntimeError("CMake not found. (version 3.13 or later needed)") |
| if which("ninja") is None: |
| raise RuntimeError("Ninja not found. (apt-get install -y ninja-build)") |
| if which("git") is None: |
| raise RuntimeError("Git not found. (apt-get install -y git)") |
| if ARGS.vk_gl_cts is None: |
| raise RuntimeError("vk-gl-cts source directory must be provided. Use --help for more info.") |
| |
| PARENT_DIR = Path(ARGS.directory).resolve() |
| |
| SWS_SRC_DIR = PARENT_DIR / "SwiftShader" |
| SWS_BUILD_DIR = SWS_SRC_DIR / "build" |
| SWIFTSHADER_URL = ARGS.url |
| |
| LAYERS_PARENT_DIR = Path(ARGS.directory).resolve() |
| LAYERS_SRC_DIR = LAYERS_PARENT_DIR / "Vulkan_Validation_Layers" |
| LAYERS_URL = ARGS.vlayer_url |
| LAYERS_BUILD_DIR = LAYERS_SRC_DIR / "build" |
| |
| LINUX_SWS_ICD_DIR = SWS_BUILD_DIR / "Linux" |
| REGRES_DIR = SWS_SRC_DIR / "tests" / "regres" |
| RESULT_DIR = PARENT_DIR / "regres_results" |
| COMP_RESULTS_DIR = PARENT_DIR / "comparison_results" |
| |
| VK_GL_CTS_ROOT_DIR = Path(ARGS.vk_gl_cts) |
| VK_GL_CTS_BUILD_DIR = Path(ARGS.vk_gl_cts_build) |
| MUSTPASS_LIST = VK_GL_CTS_ROOT_DIR / "external" / "vulkancts" / "mustpass" / "master" / "vk-default.txt" |
| if ARGS.deqp_vk is None: |
| DEQP_VK_BINARY = VK_GL_CTS_BUILD_DIR / "external" / "vulkancts" / "modules" / "vulkan" / "deqp-vk" |
| else: |
| DEQP_VK_BINARY = str(ARGS.deqp_vk) |
| |
| new_pass = [] |
| new_fail = [] |
| new_crash = [] |
| new_notsupported = [] |
| has_been_removed = [] |
| status_change = [] |
| compatibility_warning = [] |
| quality_warning = [] |
| internal_errors = [] |
| waivers = [] |
| |
| class Result: |
| def __init__(self, filename): |
| self.filename = filename |
| self.f = open(filename) |
| # Skip the first four lines and check that the file order has not been changed. |
| tmp = "" |
| for i in range(4): |
| tmp = tmp + self.f.readline() |
| if "Tests" not in tmp: |
| raise RuntimeError("Skipped four lines, no starting line found. Has the file order changed?") |
| |
| # Reads one test item from the file. |
| def readResult(self): |
| while True: |
| tmp = "" |
| while "}" not in tmp: |
| tmp = tmp + self.f.readline() |
| if "Test" in tmp: |
| tmp = tmp[tmp.find("{") : tmp.find("}") + 1] |
| return json.loads(tmp) |
| else: |
| return None |
| |
| # Search for a test name. Returns the test data if found and otherwise False. |
| def searchTest(self, test): |
| line = self.f.readline() |
| while line: |
| if line.find(test) != -1: |
| # Found the test. |
| while "}" not in line: |
| line = line + self.f.readline() |
| |
| line = line[line.find("{") : line.find("}") + 1] |
| return json.loads(line) |
| line = self.f.readline() |
| |
| # Run deqp-vk with regres. |
| def runDeqp(deqp_path, testlist_path): |
| deqpVkParam = "--deqp-vk=" + deqp_path |
| validationLayerParam = "--validation=" + ARGS.validation |
| testListParam = "--test-list=" + testlist_path |
| run(["./run_testlist.sh", deqpVkParam, validationLayerParam, testListParam], working_dir=REGRES_DIR) |
| |
| # Run commands. |
| def run(command: str, working_dir: str = Path.cwd()) -> None: |
| """Run command using subprocess.run()""" |
| subprocess.run(command, cwd=working_dir, check=True) |
| |
| # Set VK_ICD_FILENAMES |
| def setVkIcdFilenames(): |
| os.environ["VK_ICD_FILENAMES"] = str(LINUX_SWS_ICD_DIR / "vk_swiftshader_icd.json") |
| print(f"VK_ICD_FILENAMES = {os.getenv('VK_ICD_FILENAMES')}") |
| |
| # Choose the category/status to write results to. |
| def writeToStatus(test): |
| if test['Status'] == "PASS": |
| new_pass.append(test['Test']) |
| elif test['Status'] == "FAIL": |
| new_fail.append(test['Test']) |
| elif test['Status'] == "NOT_SUPPORTED" or test['Status'] == "UNSUPPORTED": |
| new_notsupported.append(test['Test']) |
| elif test['Status'] == "CRASH": |
| new_crash.append(test['Test']) |
| elif test['Status'] == "COMPATIBILITY_WARNING": |
| compatibility_warning.append(test['Test']) |
| elif test['Status'] == "QUALITY_WARNING": |
| quality_warning.append(test['Test']) |
| elif test['Status'] == "INTERNAL_ERROR": |
| internal_errors.append(test['Test']) |
| elif test['Status'] == "WAIVER": |
| waivers.append(test['Test']) |
| else: |
| raise RuntimeError(f"Expected PASS, FAIL, NOT_SUPPORTED, UNSUPPORTED, CRASH, COMPATIBILITY_WARNING, " + |
| f"QUALITY_WARNING, INTERNAL_ERROR or WAIVER as status, " + |
| f"got {test['Status']}. Is there an unhandled status case?") |
| |
| # Compare two result.json files for regression. |
| def compareRuns(new_result, old_result): |
| print(f"Comparing files: {old_result} and {new_result}") |
| |
| r0 = Result(new_result) |
| r1 = Result(old_result) |
| |
| t0 = r0.readResult() |
| t1 = r1.readResult() |
| |
| done = False |
| |
| while not done: |
| # Old result file has ended, continue with new. |
| if t1 == None and t0 != None: |
| advance1 = False |
| writeToStatus(t0) |
| # New result file has ended, continue with old. |
| elif t0 == None and t1 != None: |
| advance0 = False |
| has_been_removed.append(t1['Test']) |
| # Both files have ended, stop iteration. |
| elif t1 == None and t0 == None: |
| done = True |
| # By default advance both files. |
| else: |
| advance0 = True |
| advance1 = True |
| |
| if t0['Test'] == t1['Test']: |
| # The normal case where both files are in sync. Just check if the status matches. |
| if t0['Status'] != t1['Status']: |
| status_change.append(f"{t0['Test']}, new status: {t0['Status']}, old status: {t1['Status']}") |
| print(f"Status changed: {t0['Test']} {t0['Status']} vs {t1['Status']}") |
| else: |
| # Create temporary objects for searching through the whole file. |
| tmp0 = Result(r0.filename) |
| tmp1 = Result(r1.filename) |
| |
| # Search the mismatching test cases from the opposite file. |
| s0 = tmp0.searchTest(t1['Test']) |
| s1 = tmp1.searchTest(t0['Test']) |
| |
| # Old test not in new results |
| if not s0: |
| print(f"Missing old test {t1['Test']} from new file: {r0.filename}\n") |
| has_been_removed.append(t1['Test']) |
| # Don't advance this file since we already read a test case other than the missing one. |
| advance0 = False |
| |
| # New test not in old results |
| if not s1: |
| print(f"Missing new test {t0['Test']} from old file: {r1.filename}\n") |
| writeToStatus(t0) |
| # Don't advance this file since we already read a test case other than the missing one. |
| advance1 = False |
| |
| if s0 and s1: |
| # This should never happen because the test cases are in alphabetical order. |
| # Print an error and bail out. |
| raise RuntimeError(f"Tests in different locations: {t0['Test']}\n") |
| |
| if not advance0 and not advance1: |
| # An exotic case where both tests are missing from the other file. |
| # Need to skip both. |
| advance0 = True |
| advance1 = True |
| |
| if advance0: |
| t0 = r0.readResult() |
| if advance1: |
| t1 = r1.readResult() |
| |
| result_file = str(COMP_RESULTS_DIR / "comparison_results_") + str(datetime.now().strftime('%m_%d_%Y_%H_%M_%S')) + ".txt" |
| print(f"Writing to file {result_file}") |
| COMP_RESULTS_DIR.mkdir(parents=True, exist_ok=True) |
| |
| with open(result_file, "w") as log_file: |
| log_file.write("New passes:\n") |
| for line in new_pass: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("New fails:\n") |
| for line in new_fail: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("New crashes:\n") |
| for line in new_crash: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("New not_supported:\n") |
| for line in new_notsupported: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("Tests removed:\n") |
| for line in has_been_removed: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("Status changes:\n") |
| for line in status_change: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("Compatibility warnings:\n") |
| for line in compatibility_warning: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("Quality warnings:\n") |
| for line in quality_warning: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("Internal errors:\n") |
| for line in internal_errors: |
| log_file.write(line + "\n") |
| log_file.write("\n") |
| |
| log_file.write("Waiver:\n") |
| for line in waivers: |
| log_file.write(line + "\n") |
| |
| print(f"Comparison done. Results have been written to: {COMP_RESULTS_DIR}") |
| |
| # Build VK-GL-CTS |
| def buildCts(): |
| VK_GL_CTS_BUILD_DIR.mkdir(parents=True, exist_ok=True) |
| |
| FETCH_SOURCES = str(VK_GL_CTS_ROOT_DIR / "external" / "fetch_sources.py") |
| run([which("python3"), FETCH_SOURCES], working_dir=VK_GL_CTS_ROOT_DIR) |
| |
| # Build VK-GL-CTS |
| buildType = "-DCMAKE_BUILD_TYPE=" + ARGS.vk_gl_cts_build_type |
| run([which("cmake"), "-GNinja", str(VK_GL_CTS_ROOT_DIR), buildType], working_dir=VK_GL_CTS_BUILD_DIR) |
| run([which("ninja"), "deqp-vk"], working_dir=VK_GL_CTS_BUILD_DIR) |
| print(f"vk-gl-cts built to: {VK_GL_CTS_BUILD_DIR}") |
| |
| # Clone and build SwiftShader and Vulkan validation layers. |
| def cloneSwsAndLayers(): |
| # Clone SwiftShader or update if it already exists. |
| if not SWS_SRC_DIR.exists(): |
| SWS_SRC_DIR.mkdir(parents=True, exist_ok=True) |
| run([which("git"), "clone", SWIFTSHADER_URL, SWS_SRC_DIR]) |
| else: |
| run([which("git"), "pull", "origin"], working_dir=SWS_SRC_DIR) |
| |
| # Build SwiftShader. |
| run([which("cmake"), |
| "-GNinja", |
| str(SWS_SRC_DIR), |
| "-DSWIFTSHADER_BUILD_EGL:BOOL=OFF", |
| "-DSWIFTSHADER_BUILD_GLESv2:BOOL=OFF", |
| "-DSWIFTSHADER_BUILD_TESTS:BOOL=OFF", |
| "-DINSTALL_GTEST=OFF", |
| "-DBUILD_TESTING:BOOL=OFF", |
| "-DENABLE_CTEST:BOOL=OFF", |
| "-DCMAKE_BUILD_TYPE=" + ARGS.sws_build_type], |
| working_dir=SWS_BUILD_DIR) |
| run([which("cmake"), "--build", ".", "--target", "vk_swiftshader"], working_dir=SWS_BUILD_DIR) |
| |
| # Set Vulkan validation layers if flag is set. |
| if ARGS.validation == "true": |
| # Clone Vulkan validation layers or update if they already exist. |
| if not LAYERS_SRC_DIR.exists(): |
| LAYERS_SRC_DIR.mkdir(parents=True, exist_ok=True) |
| run([which("git"), "clone", LAYERS_URL, LAYERS_SRC_DIR]) |
| else: |
| run([which("git"), "pull", "origin"], working_dir=LAYERS_SRC_DIR) |
| |
| # Build and set Vulkan validation layers. |
| LAYERS_BUILD_DIR.mkdir(parents=True, exist_ok=True) |
| UPDATE_DEPS = str(LAYERS_SRC_DIR / "scripts" / "update_deps.py") |
| run([which("python3"), UPDATE_DEPS], working_dir=LAYERS_BUILD_DIR) |
| run([which("cmake"), |
| "-GNinja", |
| "-C", |
| "helper.cmake", |
| LAYERS_SRC_DIR], |
| working_dir=LAYERS_BUILD_DIR) |
| run([which("cmake"), "--build", "."], working_dir=LAYERS_BUILD_DIR) |
| LAYERS_PATH = str(LAYERS_BUILD_DIR / "layers") |
| os.environ["VK_LAYER_PATH"] = LAYERS_PATH |
| print(f"Tools cloned and built in: {PARENT_DIR}") |
| |
| # Run cts with regres and move result files accordingly. |
| def runCts(): |
| setVkIcdFilenames() |
| |
| # Run cts and copy the resulting file to RESULT_DIR. |
| print("Running cts...") |
| runDeqp(str(DEQP_VK_BINARY), str(MUSTPASS_LIST)) |
| RESULT_DIR.mkdir(parents=True, exist_ok=True) |
| copyfile(str(REGRES_DIR / "results.json"), str(RESULT_DIR / ARGS.result_output)) |
| print("Run completed.") |
| print(f"Result file copied to: {RESULT_DIR}") |
| exit(0) |
| |
| # Recipe for running cts. |
| if ARGS.recipe == "run-deqp": |
| cloneSwsAndLayers() |
| if ARGS.deqp_vk is None: |
| buildCts() |
| runCts() |
| |
| # Recipe for only comparing the already existing result files. |
| if ARGS.recipe == "check-comparison": |
| if ARGS.files is None: |
| raise RuntimeError("No comparable files provided. Please provide them with flag --files. Use --help for more info.") |
| newFile, oldFile = ARGS.files |
| compareRuns(str(newFile), str(oldFile)) |