1#!/usr/bin/python3 2 3# Copyright (c) 2019 Collabora Ltd 4# Copyright © 2019-2020 Valve Corporation. 5# 6# Permission is hereby granted, free of charge, to any person obtaining a 7# copy of this software and associated documentation files (the "Software"), 8# to deal in the Software without restriction, including without limitation 9# the rights to use, copy, modify, merge, publish, distribute, sublicense, 10# and/or sell copies of the Software, and to permit persons to whom the 11# Software is furnished to do so, subject to the following conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22# OTHER DEALINGS IN THE SOFTWARE. 23# 24# SPDX-License-Identifier: MIT 25 26import argparse 27import os 28import sys 29import subprocess 30from pathlib import Path 31from traceutil import trace_type_from_filename, TraceType 32 33def log(severity, msg, end='\n'): 34 print("[dump_trace_images] %s: %s" % (severity, msg), flush=True, end=end) 35 36def log_result(msg): 37 print(msg, flush=True) 38 39def run_logged_command(cmd, env, log_path): 40 ret = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) 41 logoutput = ("[dump_trace_images] Running: %s\n" % " ".join(cmd)).encode() + \ 42 ret.stdout 43 log_path.parent.mkdir(parents=True, exist_ok=True) 44 with log_path.open(mode='wb') as log: 45 log.write(logoutput) 46 if ret.returncode: 47 raise RuntimeError( 48 logoutput.decode(errors='replace') + 49 "[dump_traces_images] Process failed with error code: %d" % ret.returncode) 50 51def get_last_apitrace_frame_call(cmd_wrapper, trace_path): 52 cmd = cmd_wrapper + ["apitrace", "dump", "--calls=frame", str(trace_path)] 53 ret = subprocess.run(cmd, stdout=subprocess.PIPE) 54 for l in reversed(ret.stdout.decode(errors='replace').splitlines()): 55 s = l.split(None, 1) 56 if len(s) >= 1 and s[0].isnumeric(): 57 return int(s[0]) 58 return -1 59 60def get_last_gfxreconstruct_frame_call(trace_path): 61 cmd = ["gfxrecon-info", str(trace_path)] 62 ret = subprocess.run(cmd, stdout=subprocess.PIPE) 63 lines = ret.stdout.decode(errors='replace').splitlines() 64 if len(lines) >= 1: 65 c = lines[0].split(": ", 1) 66 if len(c) >= 2 and c[1].isnumeric(): 67 return int(c[1]) 68 return -1 69 70def dump_with_apitrace(retrace_cmd, trace_path, calls, device_name): 71 outputdir = str(trace_path.parent / "test" / device_name) 72 os.makedirs(outputdir, exist_ok=True) 73 outputprefix = str(Path(outputdir) / trace_path.name) + "-" 74 if len(calls) == 0: 75 calls = [str(get_last_apitrace_frame_call(retrace_cmd[:-1], trace_path))] 76 cmd = retrace_cmd + ["--headless", 77 "--snapshot=" + ','.join(calls), 78 "--snapshot-prefix=" + outputprefix, str(trace_path)] 79 log_path = Path(outputdir) / (trace_path.name + ".log") 80 run_logged_command(cmd, None, log_path) 81 82def dump_with_renderdoc(trace_path, calls, device_name): 83 outputdir = str(trace_path.parent / "test" / device_name) 84 script_path = Path(os.path.dirname(os.path.abspath(__file__))) 85 cmd = [str(script_path / "renderdoc_dump_images.py"), str(trace_path), outputdir] 86 cmd.extend(calls) 87 log_path = Path(outputdir) / (trace_path.name + ".log") 88 run_logged_command(cmd, None, log_path) 89 90def dump_with_gfxreconstruct(trace_path, calls, device_name): 91 from PIL import Image 92 outputdir_path = trace_path.parent / "test" / device_name 93 outputdir_path.mkdir(parents=True, exist_ok=True) 94 outputprefix = str(outputdir_path / trace_path.name) + "-" 95 if len(calls) == 0: 96 # FIXME: The VK_LAYER_LUNARG_screenshot numbers the calls from 97 # 0 to (total-num-calls - 1) while gfxreconstruct does it from 98 # 1 to total-num-calls: 99 # https://github.com/LunarG/gfxreconstruct/issues/284 100 calls = [str(get_last_gfxreconstruct_frame_call(trace_path) - 1)] 101 cmd = ["gfxrecon-replay", str(trace_path)] 102 log_path = outputdir_path / (trace_path.name + ".log") 103 env = os.environ.copy() 104 env["VK_INSTANCE_LAYERS"] = "VK_LAYER_LUNARG_screenshot" 105 env["VK_SCREENSHOT_FRAMES"] = ",".join(calls) 106 env["VK_SCREENSHOT_DIR"] = str(outputdir_path) 107 run_logged_command(cmd, env, log_path) 108 for c in calls: 109 ppm = str(outputdir_path / c) + ".ppm" 110 outputfile = outputprefix + c + ".png" 111 with log_path.open(mode='w') as log: 112 log.write("Writing: %s to %s" % (ppm, outputfile)) 113 Image.open(ppm).save(outputfile) 114 os.remove(ppm) 115 116def dump_with_testtrace(trace_path, calls, device_name): 117 from PIL import Image 118 outputdir_path = trace_path.parent / "test" / device_name 119 outputdir_path.mkdir(parents=True, exist_ok=True) 120 with trace_path.open() as f: 121 rgba = f.read() 122 color = [int(rgba[0:2], 16), int(rgba[2:4], 16), 123 int(rgba[4:6], 16), int(rgba[6:8], 16)] 124 if len(calls) == 0: calls = ["0"] 125 for c in calls: 126 outputfile = str(outputdir_path / trace_path.name) + "-" + c + ".png" 127 log_path = outputdir_path / (trace_path.name + ".log") 128 with log_path.open(mode='w') as log: 129 log.write("Writing RGBA: %s to %s" % (rgba, outputfile)) 130 Image.frombytes('RGBA', (32, 32), bytes(color * 32 * 32)).save(outputfile) 131 132def dump_from_trace(trace_path, calls, device_name): 133 log("Info", "Dumping trace %s" % trace_path, end='... ') 134 trace_type = trace_type_from_filename(trace_path.name) 135 try: 136 if trace_type == TraceType.APITRACE: 137 dump_with_apitrace(["eglretrace"], trace_path, calls, device_name) 138 elif trace_type == TraceType.APITRACE_DXGI: 139 dump_with_apitrace(["wine", "d3dretrace"], trace_path, calls, device_name) 140 elif trace_type == TraceType.RENDERDOC: 141 dump_with_renderdoc(trace_path, calls, device_name) 142 elif trace_type == TraceType.GFXRECONSTRUCT: 143 dump_with_gfxreconstruct(trace_path, calls, device_name) 144 elif trace_type == TraceType.TESTTRACE: 145 dump_with_testtrace(trace_path, calls, device_name) 146 else: 147 raise RuntimeError("Unknown tracefile extension") 148 log_result("OK") 149 return True 150 except Exception as e: 151 log_result("ERROR") 152 log("Debug", "=== Failure log start ===") 153 print(e) 154 log("Debug", "=== Failure log end ===") 155 return False 156 157def main(): 158 parser = argparse.ArgumentParser() 159 parser.add_argument('tracepath', help="trace to dump") 160 parser.add_argument('--device-name', required=True, 161 help="the name of the graphics device used to produce images") 162 parser.add_argument('--calls', required=False, 163 help="the call numbers from the trace to dump (default: last frame)") 164 165 args = parser.parse_args() 166 if args.calls is not None: 167 args.calls = args.calls.split(",") 168 else: 169 args.calls = [] 170 171 success = dump_from_trace(Path(args.tracepath), args.calls, args.device_name) 172 173 sys.exit(0 if success else 1) 174 175if __name__ == "__main__": 176 main() 177