diff options
author | Andres Gomez <agomez@igalia.com> | 2020-09-24 21:47:13 +0300 |
---|---|---|
committer | Andres Gomez <agomez@igalia.com> | 2020-11-02 22:22:33 +0200 |
commit | 79f6e056269cba9c28103b2bf187129ee48b719c (patch) | |
tree | 12869c0749bce2d8f8499e76224958b1ab5ef21c /framework | |
parent | 4bbbb004f960ff54aecd5ee590355f5bd8586748 (diff) |
framework/replay: split dump_trace_images in backends
This will allow writing new backends more easily and, additionally,
move the testtrace backend out from the framework so it is only used
for actual testing.
v2:
- Correct several typos in the apitrace backend.
- Correct comments in the package file.
- Added init check to the backends.
- Deal with exceptions from the backends in compare_replay.
- Deal with possible exceptions in query_traces_yaml.
v3:
- Do not default to "[]" for calls in the abstract class but do the
default initialization in the instance code (Dylan).
- Do not use "len()" to check for containers emptiness (Dylan).
- Do not use yoda style comparisons (Dylan).
- Use "format()" to improve text messages readability (Dylan).
- Combine exceptions detection in compare_replay (Dylan).
- Let AttributeError raise all the way up (Dylan).
- Stop using filter and lambda (Dylan).
- Use always "-1" in the gfxreconstruct backend when checking for
the last frame call returns a negative value.
v4:
- Make DumpBackend.dump() an abstractmethod by moving its code to
the dump_handler decorator, with the rest of the
logging (Alexandros).
Signed-off-by: Andres Gomez <agomez@igalia.com>
Reviewed-by: Dylan Baker <dylan@pnwbakers.com> [v3]
Part-of: <https://gitlab.freedesktop.org/mesa/piglit/-/merge_requests/353>
Diffstat (limited to 'framework')
-rw-r--r-- | framework/replay/README.md | 2 | ||||
-rw-r--r-- | framework/replay/__init__.py | 2 | ||||
-rw-r--r-- | framework/replay/backends/__init__.py | 117 | ||||
-rw-r--r-- | framework/replay/backends/abstract.py | 140 | ||||
-rw-r--r-- | framework/replay/backends/apitrace.py | 123 | ||||
-rw-r--r-- | framework/replay/backends/gfxreconstruct.py | 120 | ||||
-rw-r--r-- | framework/replay/backends/register.py | 33 | ||||
-rw-r--r-- | framework/replay/backends/renderdoc.py | 73 | ||||
-rwxr-xr-x | framework/replay/backends/renderdoc/renderdoc_dump_images.py (renamed from framework/replay/renderdoc_dump_images.py) | 0 | ||||
-rw-r--r-- | framework/replay/backends/testtrace.py | 78 | ||||
-rw-r--r-- | framework/replay/compare_replay.py | 9 | ||||
-rw-r--r-- | framework/replay/dump_trace_images.py | 191 | ||||
-rw-r--r-- | framework/replay/programs/dump.py | 5 | ||||
-rw-r--r-- | framework/replay/programs/query.py | 11 | ||||
-rw-r--r-- | framework/replay/query_traces_yaml.py | 26 | ||||
-rw-r--r-- | framework/replay/trace_utils.py | 74 |
16 files changed, 719 insertions, 285 deletions
diff --git a/framework/replay/README.md b/framework/replay/README.md index 9028fb80a..6b2647b6b 100644 --- a/framework/replay/README.md +++ b/framework/replay/README.md @@ -131,7 +131,7 @@ Examples: --yaml-file ./traces.yml \ traces \ --device-name gl-vmware-llvmpipe \ - --trace-types "apitrace,renderdoc" + --trace-extensions ".trace,.rdc" --checksum ``` diff --git a/framework/replay/__init__.py b/framework/replay/__init__.py index eea2d157a..9dea3fb2a 100644 --- a/framework/replay/__init__.py +++ b/framework/replay/__init__.py @@ -31,8 +31,6 @@ from .compare_replay import * from .download_utils import * -from .dump_trace_images import * from .image_checksum import * from .options import * from .query_traces_yaml import * -from .trace_utils import * diff --git a/framework/replay/backends/__init__.py b/framework/replay/backends/__init__.py new file mode 100644 index 000000000..8c2e5031f --- /dev/null +++ b/framework/replay/backends/__init__.py @@ -0,0 +1,117 @@ +# coding=utf-8 +# +# Copyright (c) 2014-2016, 2019 Intel Corporation +# Copyright © 2019-2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + +"""Provides a module like interface for dump backends. + +This package provides an abstract interface for working with dump backends, +which implement various functions as provided in the Registry class, and then +provide a Registry instance as REGISTRY, which maps individual objects to +objects that piglit expects to use. This module then provides a thin +abstraction layer so that piglit is never aware of what backend it's using, it +just asks for an object and receives one. + +Most consumers will want to import framework.replay.backends and work directly +with the helper functions here. For some more detailed uses (test cases +especially) the modules themselves may be used. + +When this module is loaded it will search through framework/replay/backends for +python modules (those ending in .py), and attempt to import them. Then it will +look for an attribute REGISTRY in those modules, and if it as a +framework.replay.register.Registry instance, it will add the name of that +module (sans .py) as a key, and the instance as a value to the DUMPBACKENDS +dictionary. Each of the helper functions in this module uses that dictionary to +find the function that a user actually wants. + +""" + +import os +import importlib + +from os import path + +from .register import Registry + +__all__ = [ + 'DUMPBACKENDS', + 'DumpBackendError', + 'DumpBackendNotImplementedError', + 'dump', +] + + +class DumpBackendError(Exception): + pass + + +class DumpBackendNotImplementedError(NotImplementedError): + pass + + +def _register(): + """Register backends. + + Walk through the list of backends and register them to a name in a + dictionary, so that they can be referenced from helper functions. + + """ + registry = {} + + for module in os.listdir(path.dirname(path.abspath(__file__))): + module, extension = path.splitext(module) + if extension == '.py': + mod = importlib.import_module( + 'framework.replay.backends.{}'.format(module)) + if hasattr(mod, 'REGISTRY') and isinstance(mod.REGISTRY, Registry): + registry[module] = mod.REGISTRY + + return registry + + +DUMPBACKENDS = _register() + + +def dump(trace_path, output_dir=None, calls=None): + """Wrapper for dumping traces. + + This function will attempt to determine how to dump the trace file (based + on file extension), and then pass the trace file path, output_dir and calls + into the appropriate instance, and then call dump in such instance. + + """ + name, extension = path.splitext(trace_path) + + for dump_backend in DUMPBACKENDS.values(): + if extension in dump_backend.extensions: + backend = dump_backend.backend + + if backend is None: + raise DumpBackendNotImplementedError( + 'DumpBackend for "{}" is not implemented'.format(extension)) + + instance = backend(trace_path, output_dir, calls) + return instance.dump() + + raise DumpBackendError( + 'No module supports file extensions "{}"'.format(extension)) diff --git a/framework/replay/backends/abstract.py b/framework/replay/backends/abstract.py new file mode 100644 index 000000000..905549b8c --- /dev/null +++ b/framework/replay/backends/abstract.py @@ -0,0 +1,140 @@ +# coding=utf-8 +# +# Copyright (c) 2014, 2016, 2019 Intel Corporation +# Copyright © 2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + + +""" Base class for dump backends + +This module provides a base class for replayer dump backend modules. + +""" + +import abc +import functools +import subprocess + +from os import path + +from framework import core +from framework.replay.options import OPTIONS + + +class DumpBackend(metaclass=abc.ABCMeta): + """ Base class for dump backends + + This class provides an basic ancestor for classes implementing dump + backends, providing a light public API. The goal of this API is to be "just + enough", not a generic writing solution. To that end it provides the + method, 'dump'. This method is designed to be just enough to write a + backend without needing format specific options. + + """ + def __init__(self, trace_path, output_dir=None, calls=None, **kwargs): + """ Generic constructor + + This method takes keyword arguments that define options for the + backends. Options should be prefixed to identify which backends they + apply to. For example, an apitrace specific value should be passed as + apitrace_*, while a file gfxrecon value should be passed as gfxrecon_*) + + Arguments: + + trace_path -- the path to the trace from which we want to dump calls as + images. + output_dir -- the place to write the images to. + calls -- an array of the calls in the trace for which we want to + dump images. + + """ + self._trace_path = trace_path + self._output_dir = output_dir + self._calls = calls or [] + + if self._output_dir is None: + self._output_dir = path.join('trace', OPTIONS.device_name, + path.dirname(self._trace_path)) + + + @staticmethod + def log(severity, msg, end='\n'): + print('[dump_trace_images] {}: {}'.format(severity, msg), flush=True, + end=end) + + + @staticmethod + def log_result(msg): + print(msg, flush=True) + + + @staticmethod + def _run_logged_command(cmd, env): + ret = subprocess.run(cmd, stdout=subprocess.PIPE, env=env) + logoutput = '[dump_trace_images] Running: {}\n'.format( + ' '.join(cmd)).encode() + ret.stdout + print(logoutput.decode(errors='replace')) + if ret.returncode: + raise RuntimeError( + '[dump_traces_images] Process failed with error code: {}'.format( + ret.returncode)) + + + @abc.abstractmethod + def _get_last_frame_call(self): + """Get the number of the last frame call from the trace""" + + + @abc.abstractmethod + def dump(self): + """ Dump the calls to images from the trace + + This method actually dumps the calls from a trace. + + """ + + +def dump_handler(func): + """ Decorator function for handling trace dumps. + + This will handle exceptions and log the result. + + """ + + @functools.wraps(func) + def _inner(self, *args, **kwargs): + try: + DumpBackend.log('Info', + 'Dumping trace {}'.format(self._trace_path), + end='...\n') + core.check_dir(self._output_dir) + func(self, *args, **kwargs) + DumpBackend.log_result('OK') + return True + except Exception as e: + DumpBackend.log_result('ERROR') + DumpBackend.log('Debug', '=== Failure log start ===') + print(e) + DumpBackend.log('Debug', '=== Failure log end ===') + return False + + return _inner diff --git a/framework/replay/backends/apitrace.py b/framework/replay/backends/apitrace.py new file mode 100644 index 000000000..692badeb8 --- /dev/null +++ b/framework/replay/backends/apitrace.py @@ -0,0 +1,123 @@ +# coding=utf-8 +# +# Copyright (c) 2014, 2016-2017, 2019-2020 Intel Corporation +# Copyright (c) 2019 Collabora Ltd +# Copyright © 2019-2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + + +""" Module providing an apitrace dump backend for replayer """ + +import os +import subprocess + +from os import path + +from framework import core, exceptions +from .abstract import DumpBackend, dump_handler +from .register import Registry + + +__all__ = [ + 'REGISTRY', + 'APITraceBackend', +] + + +class APITraceBackend(DumpBackend): + """ replayer's apitrace dump backend + + This backend uses apitrace for replaying its traces. + + It supports OpenGL/ES traces, to be replayed with eglretrace, and DXGI + traces, to be replayed with d3dretrace on Wine. + + However, the binary paths to the apitrace, eglretrace, d3dretrace and wine + binaries are configurable. Hence, using wine could be omitted, for example. + + """ + + def __init__(self, trace_path, output_dir=None, calls=None, **kwargs): + super(APITraceBackend, self).__init__(trace_path, output_dir, calls, + **kwargs) + extension = path.splitext(self._trace_path)[1] + + if extension == '.trace': + eglretrace_bin = core.get_option('PIGLIT_REPLAY_EGLRETRACE_BINARY', + ('replay', 'eglretrace_bin'), + default='eglretrace') + self._retrace_cmd = [eglretrace_bin] + elif extension == '.trace-dxgi': + wine_bin = core.get_option('PIGLIT_REPLAY_WINE_BINARY', + ('replay', 'wine_bin'), + default='wine') + wine_d3dretrace_bin = core.get_option( + 'PIGLIT_REPLAY_WINE_D3DRETRACE_BINARY', + ('replay', 'wine_d3dretrace_bin'), + default='d3dretrace') + self._retrace_cmd = [wine_bin, wine_d3dretrace_bin] + else: + raise exceptions.PiglitFatalError( + 'Invalid trace_path: "{}" tried to be dumped ' + 'by the APITraceBackend.\n'.format(self._trace_path)) + + + def _get_last_frame_call(self): + cmd_wrapper = self._retrace_cmd[:-1] + if cmd_wrapper: + apitrace_bin = core.get_option( + 'PIGLIT_REPLAY_WINE_APITRACE_BINARY', + ('replay', 'wine_apitrace_bin'), + default='apitrace') + else: + apitrace_bin = core.get_option('PIGLIT_REPLAY_APITRACE_BINARY', + ('replay', 'apitrace_bin'), + default='apitrace') + cmd = cmd_wrapper + [apitrace_bin, + 'dump', '--calls=frame', self._trace_path] + ret = subprocess.run(cmd, stdout=subprocess.PIPE) + logoutput = '[dump_trace_images] Running: {}\n'.format( + ' '.join(cmd)) + ret.stdout.decode(errors='replace') + print(logoutput) + for l in reversed(ret.stdout.decode(errors='replace').splitlines()): + s = l.split(None, 1) + if s and s[0].isnumeric(): + return int(s[0]) + return -1 + + @dump_handler + def dump(self): + outputprefix = '{}-'.format(path.join(self._output_dir, + path.basename(self._trace_path))) + if not self._calls: + self._calls = [str(self._get_last_frame_call())] + cmd = self._retrace_cmd + ['--headless', + '--snapshot=' + ','.join(self._calls), + '--snapshot-prefix=' + outputprefix, + self._trace_path] + self._run_logged_command(cmd, None) + + +REGISTRY = Registry( + extensions=['.trace', '.trace-dxgi'], + backend=APITraceBackend, +) diff --git a/framework/replay/backends/gfxreconstruct.py b/framework/replay/backends/gfxreconstruct.py new file mode 100644 index 000000000..07cb3b846 --- /dev/null +++ b/framework/replay/backends/gfxreconstruct.py @@ -0,0 +1,120 @@ +# coding=utf-8 +# +# Copyright (c) 2014, 2016-2017, 2019-2020 Intel Corporation +# Copyright (c) 2019 Collabora Ltd +# Copyright © 2019-2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + + +""" Module providing a GFXReconstruct dump backend for replayer """ + +import os +import subprocess + +from os import path + +from framework import core, exceptions +from .abstract import DumpBackend, dump_handler +from .register import Registry + + +__all__ = [ + 'REGISTRY', + 'GFXReconstructBackend', +] + + +class GFXReconstructBackend(DumpBackend): + """ replayer's GFXReconstruct dump backend + + This backend uses GFXReconstruct for replaying its traces. + + The path to the GFXReconstruct binary is configurable. + + It also admits configuration for passing extra parameters to + GFXReconstruct. + + """ + + def __init__(self, trace_path, output_dir=None, calls=None, **kwargs): + super(GFXReconstructBackend, self).__init__(trace_path, output_dir, + calls, **kwargs) + extension = path.splitext(self._trace_path)[1] + + if extension != '.gfxr': + raise exceptions.PiglitFatalError( + 'Invalid trace_path: "{}" tried to be dumped ' + 'by the GFXReconstructBackend.\n'.format(self._trace_path)) + + def _get_last_frame_call(self): + gfxrecon_info_bin = core.get_option( + 'PIGLIT_REPLAY_GFXRECON_INFO_BINARY', + ('replay', 'gfxrecon-info_bin'), + default='gfxrecon-info') + cmd = [gfxrecon_info_bin, self._trace_path] + ret = subprocess.run(cmd, stdout=subprocess.PIPE) + lines = ret.stdout.decode(errors='replace').splitlines() + print(ret.stdout.decode(errors='replace')) + if lines: + c = lines[0].split(': ', 1) + if len(c) > 1 and c[1].isnumeric(): + return int(c[1]) + return -1 + + @dump_handler + def dump(self): + from PIL import Image + outputprefix = path.join(self._output_dir, + path.basename(self._trace_path)) + if not self._calls: + # FIXME: The VK_LAYER_LUNARG_screenshot numbers the calls from 0 to + # (total-num-calls - 1) while gfxreconstruct does it from 1 to + # total-num-calls: + # https://github.com/LunarG/gfxreconstruct/issues/284 + self._calls = [str(max(-1, self._get_last_frame_call() - 1))] + gfxrecon_replay_bin = core.get_option( + 'PIGLIT_REPLAY_GFXRECON_REPLAY_BINARY', + ('replay', 'gfxrecon-replay_bin'), + default='gfxrecon-replay') + gfxrecon_replay_extra_args = core.get_option( + 'PIGLIT_REPLAY_GFXRECON_REPLAY_EXTRA_ARGS', + ('replay', 'gfxrecon-replay_extra_args'), + default='').split() + cmd = ([gfxrecon_replay_bin] + + gfxrecon_replay_extra_args + [self._trace_path]) + env = os.environ.copy() + env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot' + env['VK_SCREENSHOT_FRAMES'] = ','.join(self._calls) + env['VK_SCREENSHOT_DIR'] = self._output_dir + self._run_logged_command(cmd, env) + for c in self._calls: + ppm = '{}.ppm'.format(path.join(self._output_dir, c)) + outputfile = '{}-{}.png'.format(outputprefix, c) + print('Writing: {} to {}'.format(ppm, outputfile)) + Image.open(ppm).save(outputfile) + os.remove(ppm) + + +REGISTRY = Registry( + extensions=['.gfxr'], + backend=GFXReconstructBackend, +) diff --git a/framework/replay/backends/register.py b/framework/replay/backends/register.py new file mode 100644 index 000000000..e380cbd47 --- /dev/null +++ b/framework/replay/backends/register.py @@ -0,0 +1,33 @@ +# coding=utf-8 +# +# Copyright (c) 2015-2016, 2019 Intel Corporation +# Copyright © 2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + +"""An object for registering dump backends.""" + +import collections + +Registry = collections.namedtuple( + 'Registry', + ['extensions', 'backend'] +) diff --git a/framework/replay/backends/renderdoc.py b/framework/replay/backends/renderdoc.py new file mode 100644 index 000000000..c26e4f8ea --- /dev/null +++ b/framework/replay/backends/renderdoc.py @@ -0,0 +1,73 @@ +# coding=utf-8 +# +# Copyright (c) 2014, 2016-2017, 2019-2020 Intel Corporation +# Copyright (c) 2019 Collabora Ltd +# Copyright © 2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + + +""" Module providing a RenderDoc dump backend for replayer """ + +from os import path + +from framework import exceptions +from .abstract import DumpBackend, dump_handler +from .register import Registry + + +__all__ = [ + 'REGISTRY', + 'RenderDocBackend', +] + + +class RenderDocBackend(DumpBackend): + """ replayer's RenderDoc dump backend + + This backend uses RenderDoc for replaying its traces. + + """ + _get_last_frame_call = None # this silences the abstract-not-subclassed warning + + def __init__(self, trace_path, output_dir=None, calls=None, **kwargs): + super(RenderDocBackend, self).__init__(trace_path, output_dir, calls, + **kwargs) + extension = path.splitext(self._trace_path)[1] + + if extension != '.rdc': + raise exceptions.PiglitFatalError( + 'Invalid trace_path: "{}" tried to be dumped ' + 'by the RenderDocBackend.\n'.format(self._trace_path)) + + @dump_handler + def dump(self): + script_path = path.dirname(path.abspath(__file__)) + cmd = [path.join(script_path, 'renderdoc/renderdoc_dump_images.py'), + self._trace_path, self._output_dir] + cmd.extend(self._calls) + self._run_logged_command(cmd, None) + + +REGISTRY = Registry( + extensions=['.rdc'], + backend=RenderDocBackend, +) diff --git a/framework/replay/renderdoc_dump_images.py b/framework/replay/backends/renderdoc/renderdoc_dump_images.py index db0ada466..db0ada466 100755 --- a/framework/replay/renderdoc_dump_images.py +++ b/framework/replay/backends/renderdoc/renderdoc_dump_images.py diff --git a/framework/replay/backends/testtrace.py b/framework/replay/backends/testtrace.py new file mode 100644 index 000000000..08f8b717e --- /dev/null +++ b/framework/replay/backends/testtrace.py @@ -0,0 +1,78 @@ +# coding=utf-8 +# +# Copyright (c) 2014, 2016-2017, 2019-2020 Intel Corporation +# Copyright (c) 2019 Collabora Ltd +# Copyright © 2019-2020 Valve Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + + +""" Module providing a trace testing dump backend for replayer """ + +import os +import subprocess + +from os import path + +from framework import core +from .abstract import DumpBackend, dump_handler +from .register import Registry + + +__all__ = [ + 'REGISTRY', + 'TestTraceBackend', +] + + +class TestTraceBackend(DumpBackend): + """ replayer's dump backend for trace testing + + This is a simple dump backend which reads a RGBA color from a text file and + dumps a square image with that color. + + """ + _get_last_frame_call = None # this silences the abstract-not-subclassed warning + + def __init__(self, trace_path, output_dir=None, calls=[], **kwargs): + super(TestTraceBackend, self).__init__(trace_path, output_dir, calls, + **kwargs) + if len(self._calls) == 0: self._calls = ['0'] + + @dump_handler + def dump(self): + from PIL import Image + outputprefix = path.join(self._output_dir, path.basename(self._trace_path)) + with open(self._trace_path) as f: + rgba = f.read() + color = [int(rgba[0:2], 16), int(rgba[2:4], 16), + int(rgba[4:6], 16), int(rgba[6:8], 16)] + for c in self._calls: + outputfile = outputprefix + '-' + c + '.png' + print('Writing RGBA: {} to {}'.format(rgba, outputfile)) + Image.frombytes('RGBA', (32, 32), + bytes(color * 32 * 32)).save(outputfile) + + +REGISTRY = Registry( + extensions=['.testtrace'], + backend=TestTraceBackend, +) diff --git a/framework/replay/compare_replay.py b/framework/replay/compare_replay.py index e4f18cabf..84183ecfc 100644 --- a/framework/replay/compare_replay.py +++ b/framework/replay/compare_replay.py @@ -36,9 +36,9 @@ from os import path from framework import core from framework import status +from framework.replay import backends from framework.replay import query_traces_yaml as qty from framework.replay.download_utils import ensure_file -from framework.replay.dump_trace_images import dump_from_trace from framework.replay.image_checksum import hexdigest_from_image from framework.replay.options import OPTIONS @@ -56,7 +56,12 @@ class Result(Enum): def _replay(trace_path, results_path): - success = dump_from_trace(trace_path, results_path, []) + try: + success = backends.dump(trace_path, results_path, []) + except (backends.DumpBackendNotImplementedError, + backends.DumpBackendError) as e: + print(e) + success = False if not success: print("[check_image] Trace {} couldn't be replayed. " diff --git a/framework/replay/dump_trace_images.py b/framework/replay/dump_trace_images.py deleted file mode 100644 index f07cfd8e2..000000000 --- a/framework/replay/dump_trace_images.py +++ /dev/null @@ -1,191 +0,0 @@ -# coding=utf-8 -# -# Copyright (c) 2019 Collabora Ltd -# Copyright © 2019-2020 Valve Corporation. -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# SPDX-License-Identifier: MIT - -import os -import subprocess - -from os import path - -from framework import core -from framework.replay.options import OPTIONS -from framework.replay.trace_utils import trace_type_from_filename, TraceType - - -__all__ = ['dump_from_trace'] - - -def _log(severity, msg, end='\n'): - print('[dump_trace_images] {}: {}'.format(severity, msg), flush=True, - end=end) - -def _log_result(msg): - print(msg, flush=True) - -def _run_logged_command(cmd, env): - ret = subprocess.run(cmd, stdout=subprocess.PIPE, env=env) - logoutput = '[dump_trace_images] Running: {}\n'.format( - ' '.join(cmd)).encode() + ret.stdout - print(logoutput.decode(errors='replace')) - if ret.returncode: - raise RuntimeError( - '[dump_traces_images] Process failed with error code: {}'.format( - ret.returncode)) - -def _get_last_apitrace_frame_call(cmd_wrapper, trace_path): - if len(cmd_wrapper) > 0: - apitrace_bin = core.get_option('PIGLIT_REPLAY_WINE_APITRACE_BINARY', - ('replay', 'wine_apitrace_bin'), - default='apitrace') - else: - apitrace_bin = core.get_option('PIGLIT_REPLAY_APITRACE_BINARY', - ('replay', 'apitrace_bin'), - default='apitrace') - cmd = cmd_wrapper + [apitrace_bin, 'dump', '--calls=frame', trace_path] - ret = subprocess.run(cmd, stdout=subprocess.PIPE) - logoutput = '[dump_trace_images] Running: {}\n'.format( - ' '.join(cmd)) + ret.stdout.decode(errors='replace') - print(logoutput) - for l in reversed(ret.stdout.decode(errors='replace').splitlines()): - s = l.split(None, 1) - if len(s) >= 1 and s[0].isnumeric(): - return int(s[0]) - return -1 - -def _get_last_gfxreconstruct_frame_call(trace_path): - gfxrecon_info_bin = core.get_option('PIGLIT_REPLAY_GFXRECON_INFO_BINARY', - ('replay', 'gfxrecon-info_bin'), - default='gfxrecon-info') - cmd = [gfxrecon_info_bin, trace_path] - ret = subprocess.run(cmd, stdout=subprocess.PIPE) - lines = ret.stdout.decode(errors='replace').splitlines() - print(ret.stdout.decode(errors='replace')) - if len(lines) >= 1: - c = lines[0].split(': ', 1) - if len(c) >= 2 and c[1].isnumeric(): - return int(c[1]) - return -1 - -def _dump_with_apitrace(retrace_cmd, trace_path, output_dir, calls): - outputprefix = path.join(output_dir, path.basename(trace_path)) + '-' - if len(calls) == 0: - calls = [str(_get_last_apitrace_frame_call(retrace_cmd[:-1], - trace_path))] - cmd = retrace_cmd + ['--headless', - '--snapshot=' + ','.join(calls), - '--snapshot-prefix=' + outputprefix, - trace_path] - _run_logged_command(cmd, None) - -def _dump_with_renderdoc(trace_path, output_dir, calls): - script_path = path.dirname(path.abspath(__file__)) - cmd = [path.join(script_path, 'renderdoc_dump_images.py'), - trace_path, output_dir] - cmd.extend(calls) - _run_logged_command(cmd, None) - -def _dump_with_gfxreconstruct(trace_path, output_dir, calls): - from PIL import Image - outputprefix = path.join(output_dir, path.basename(trace_path)) - if len(calls) == 0: - # FIXME: The VK_LAYER_LUNARG_screenshot numbers the calls from - # 0 to (total-num-calls - 1) while gfxreconstruct does it from - # 1 to total-num-calls: - # https://github.com/LunarG/gfxreconstruct/issues/284 - calls = [str(_get_last_gfxreconstruct_frame_call(trace_path) - 1)] - gfxrecon_replay_bin = core.get_option( - 'PIGLIT_REPLAY_GFXRECON_REPLAY_BINARY', - ('replay', 'gfxrecon-replay_bin'), - default='gfxrecon-replay') - gfxrecon_replay_extra_args = core.get_option( - 'PIGLIT_REPLAY_GFXRECON_REPLAY_EXTRA_ARGS', - ('replay', 'gfxrecon-replay_extra_args'), - default='').split() - cmd = [gfxrecon_replay_bin] + gfxrecon_replay_extra_args + [trace_path] - env = os.environ.copy() - env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot' - env['VK_SCREENSHOT_FRAMES'] = ','.join(calls) - env['VK_SCREENSHOT_DIR'] = output_dir - _run_logged_command(cmd, env) - for c in calls: - ppm = path.join(output_dir, c) + '.ppm' - outputfile = outputprefix + '-' + c + '.png' - print('Writing: {} to {}'.format(ppm, outputfile)) - Image.open(ppm).save(outputfile) - os.remove(ppm) - -def _dump_with_testtrace(trace_path, output_dir, calls): - from PIL import Image - outputprefix = path.join(output_dir, path.basename(trace_path)) - with open(trace_path) as f: - rgba = f.read() - color = [int(rgba[0:2], 16), int(rgba[2:4], 16), - int(rgba[4:6], 16), int(rgba[6:8], 16)] - if len(calls) == 0: calls = ['0'] - for c in calls: - outputfile = outputprefix + '-' + c + '.png' - print('Writing RGBA: {} to {}'.format(rgba, outputfile)) - Image.frombytes('RGBA', (32, 32), - bytes(color * 32 * 32)).save(outputfile) - -def dump_from_trace(trace_path, output_dir=None, calls=[]): - _log('Info', 'Dumping trace {}'.format(trace_path), end='...\n') - if output_dir is None: - output_dir = path.join('trace', OPTIONS.device_name, - path.dirname(trace_path)) - core.check_dir(output_dir) - trace_type = trace_type_from_filename(path.basename(trace_path)) - try: - if trace_type == TraceType.APITRACE: - eglretrace_bin = core.get_option('PIGLIT_REPLAY_EGLRETRACE_BINARY', - ('replay', 'eglretrace_bin'), - default='eglretrace') - _dump_with_apitrace([eglretrace_bin], - trace_path, output_dir, calls) - elif trace_type == TraceType.APITRACE_DXGI: - wine_bin = core.get_option('PIGLIT_REPLAY_WINE_BINARY', - ('replay', 'wine_bin'), - default='wine') - wine_d3dretrace_bin = core.get_option( - 'PIGLIT_REPLAY_WINE_D3DRETRACE_BINARY', - ('replay', 'wine_d3dretrace_bin'), - default='d3dretrace') - _dump_with_apitrace([wine_bin, wine_d3dretrace_bin], - trace_path, output_dir, calls) - elif trace_type == TraceType.RENDERDOC: - _dump_with_renderdoc(trace_path, output_dir, calls) - elif trace_type == TraceType.GFXRECONSTRUCT: - _dump_with_gfxreconstruct(trace_path, output_dir, calls) - elif trace_type == TraceType.TESTTRACE: - _dump_with_testtrace(trace_path, output_dir, calls) - else: - raise RuntimeError('Unknown tracefile extension') - _log_result('OK') - return True - except Exception as e: - _log_result('ERROR') - _log('Debug', '=== Failure log start ===') - print(e) - _log('Debug', '=== Failure log end ===') - return False diff --git a/framework/replay/programs/dump.py b/framework/replay/programs/dump.py index 7a52ea0ac..5fc8cecd8 100644 --- a/framework/replay/programs/dump.py +++ b/framework/replay/programs/dump.py @@ -25,7 +25,7 @@ import argparse from framework import exceptions -from framework.replay import dump_trace_images +from framework.replay import backends from framework.replay import options from framework.programs import parsers as piglit_parsers from . import parsers @@ -43,8 +43,7 @@ def _dump_from_trace(args): else: calls = [] - return dump_trace_images.dump_from_trace(args.file_path, args.output, - calls) + return backends.dump(args.file_path, args.output, calls) @exceptions.handler diff --git a/framework/replay/programs/query.py b/framework/replay/programs/query.py index ba9b40585..977b8f5e8 100644 --- a/framework/replay/programs/query.py +++ b/framework/replay/programs/query.py @@ -45,7 +45,7 @@ def _traces(args): y = qty.load_yaml(args.yaml_file) t_list = qty.traces(y, - trace_types=args.trace_types, + trace_extensions=args.trace_extensions, device_name=args.device_name, checksum=args.checksum) @@ -97,12 +97,13 @@ def query(input_): parents=[parsers.DEVICE], help=('Outputs the trace files filtered by the optional arguments.')) parser_traces.add_argument( - '-t', '--trace-types', + '-t', '--trace-extensions', required=False, default=None, - help=('a comma separated list of trace types to look for ' - 'in recursive dir walks. ' - 'If none are provide, all types are used by default')) + help=('a comma separated list of trace extensions to look for ' + 'in recursive dir walks (e.g. ".trace,.rdc"). ' + 'If none are provide, all supported extensions ' + 'are used by default')) parser_traces.add_argument( '-c', '--checksum', action='store_true', diff --git a/framework/replay/query_traces_yaml.py b/framework/replay/query_traces_yaml.py index 1fb6c94aa..e3a27bdad 100644 --- a/framework/replay/query_traces_yaml.py +++ b/framework/replay/query_traces_yaml.py @@ -25,8 +25,9 @@ import yaml +from os import path + from framework import exceptions -from framework.replay.trace_utils import trace_type_from_filename, trace_type_from_name __all__ = ['download_url', @@ -70,14 +71,25 @@ def download_url(y): return None -def traces(y, trace_types=None, device_name=None, checksum=False): +def traces(y, trace_extensions=None, device_name=None, checksum=False): + + def _trace_extension(trace_path): + name, extension = path.splitext(trace_path) + + return extension + traces = y.get('traces', []) or [] - if trace_types is not None: - split_trace_types = [trace_type_from_name(t) for t - in trace_types.split(',')] - traces = filter(lambda t: trace_type_from_filename(t['path']) - in split_trace_types, traces) + if trace_extensions is not None: + extensions = trace_extensions.split(',') + + def _filter_trace_extension(trace): + try: + return _trace_extension(trace['path']) in extensions + except KeyError: + return False + + traces = [t for t in traces if _filter_trace_extension(t)] if device_name is not None: traces = [t for t in traces if device_name in trace_devices(t)] diff --git a/framework/replay/trace_utils.py b/framework/replay/trace_utils.py deleted file mode 100644 index b8836eaed..000000000 --- a/framework/replay/trace_utils.py +++ /dev/null @@ -1,74 +0,0 @@ -# coding=utf-8 -# -# Copyright (c) 2019 Collabora Ltd -# Copyright © 2019-2020 Valve Corporation. -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# SPDX-License-Identifier: MIT - - -from enum import Enum, auto - - -__all__ = ['TraceType', - 'all_trace_type_names', - 'trace_type_from_name', - 'trace_type_from_filename'] - - -class TraceType(Enum): - UNKNOWN = auto() - APITRACE = auto() - APITRACE_DXGI = auto() - RENDERDOC = auto() - GFXRECONSTRUCT = auto() - TESTTRACE = auto() - -_trace_type_info_map = { - TraceType.APITRACE : ('apitrace', '.trace'), - TraceType.APITRACE_DXGI : ('apitrace-dxgi', '.trace-dxgi'), - TraceType.RENDERDOC : ('renderdoc', '.rdc'), - TraceType.GFXRECONSTRUCT : ('gfxreconstruct', '.gfxr'), - TraceType.TESTTRACE : ('testtrace', '.testtrace') -} - - -def all_trace_type_names(): - s = [] - for t,(name, ext) in _trace_type_info_map.items(): - if t != TraceType.UNKNOWN: - s.append(name) - return s - - -def trace_type_from_name(tt_name): - for t,(name, ext) in _trace_type_info_map.items(): - if tt_name == name: - return t - - return TraceType.UNKNOWN - - -def trace_type_from_filename(trace_file): - for t,(name, ext) in _trace_type_info_map.items(): - if trace_file.endswith(ext): - return t - - return TraceType.UNKNOWN |