From dabd2a803a2cfe016ca912fb503ceb334529221a Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 27 Jan 2021 16:03:04 +0000 Subject: [PATCH] Give specific instructions on writing UI extensions * This goes through a worked example of a simple extension. --- docs/how/how_python_extension.rst | 8 +- docs/imgs/python_ext/Step1.png | Bin 0 -> 29329 bytes docs/imgs/python_ext/Step2.png | Bin 0 -> 14043 bytes docs/python_api/index.rst | 1 + .../ui_extension_tutorial/__init__.py | 151 +++++++++++ .../ui_extension_tutorial/extension.json | 9 + docs/python_api/ui_extensions.rst | 242 ++++++++++++++++++ 7 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 docs/imgs/python_ext/Step1.png create mode 100644 docs/imgs/python_ext/Step2.png create mode 100644 docs/python_api/ui_extension_tutorial/__init__.py create mode 100644 docs/python_api/ui_extension_tutorial/extension.json create mode 100644 docs/python_api/ui_extensions.rst diff --git a/docs/how/how_python_extension.rst b/docs/how/how_python_extension.rst index e23da0534..d245e9948 100644 --- a/docs/how/how_python_extension.rst +++ b/docs/how/how_python_extension.rst @@ -1,9 +1,9 @@ -How do I write a python extension? -================================== +How do I register a python extension? +===================================== RenderDoc supports python extensions using the :doc:`existing python API <../python_api/index>`. These extensions can be configured to be persistent and load whenever the RenderDoc UI does, allowing users to extend the program with custom functionality. -Example extensions can be found at the `community contributed repository `_. +Example extensions can be found at the `community contributed repository `_. More information on writing an extension is available at :doc:`../python_api/ui_extensions`. Creating extensions ------------------- @@ -62,4 +62,4 @@ A python extension when loaded will have a ``register`` function called in its m Optionally you can define an ``unregister`` function to be called if the extension is reloaded. This takes no parameters and is simply an opportunity to clean-up or remove anything persistent that shouldn't hang around, before ``register`` is called again. -Extensions use the full :doc:`python API <../python_api/index>` with all of the power that comes with it. There are interfaces geared specifically towards extensions in :doc:`the qrenderdoc module <../python_api/qrenderdoc/extensions>` which allow registering callbacks for menu items. \ No newline at end of file +Extensions use the full :doc:`python API <../python_api/index>` with all of the power that comes with it. There are interfaces geared specifically towards extensions in :doc:`the qrenderdoc module <../python_api/qrenderdoc/extensions>` which allow registering callbacks for menu items. diff --git a/docs/imgs/python_ext/Step1.png b/docs/imgs/python_ext/Step1.png new file mode 100644 index 0000000000000000000000000000000000000000..3c263a240cdded92899283cd515b8eba83cb7a7e GIT binary patch literal 29329 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 literal 0 HcmV?d00001 diff --git a/docs/imgs/python_ext/Step2.png b/docs/imgs/python_ext/Step2.png new file mode 100644 index 0000000000000000000000000000000000000000..4d69e98d29e3d8435f03466e386d226574cb42ab GIT binary patch literal 14043 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 1 and cols[0].resourceId != rd.ResourceId.Null(): + texid = cols[0].resourceId + + if texid == rd.ResourceId.Null(): + ctx.Extensions().MessageDialog("Couldn't find any bound target!", "Extension message") + return + else: + mqt = ctx.Extensions().GetMiniQtHelper() + texname = ctx.GetResourceName(texid) + + def get_minmax(r: rd.ReplayController): + minvals, maxvals = r.GetMinMax(texid, rd.Subresource(), rd.CompType.Typeless) + + msg = '{} has min {:.4} and max {:.4} in red'.format(texname, minvals.floatValue[0], maxvals.floatValue[0]) + + mqt.InvokeOntoUIThread(lambda: ctx.Extensions().MessageDialog(msg, "Extension message")) + + ctx.Replay().AsyncInvoke('', get_minmax) + + + +extiface_version = '' + + +def register(version: str, ctx: qrd.CaptureContext): + global extiface_version + extiface_version = version + + print("Registering my extension for RenderDoc version {}".format(version)) + + ctx.Extensions().RegisterWindowMenu(qrd.WindowMenu.Tools, ["My extension"], menu_callback) + ctx.Extensions().RegisterWindowMenu(qrd.WindowMenu.Window, ["Extension Window"], window_callback) + + +def unregister(): + print("Unregistering my extension") + + global cur_window + + if cur_window is not None: + # The window_closed() callback will unregister the capture viewer + cur_window.ctx.Extensions().GetMiniQtHelper().CloseToplevelWidget(cur_window.topWindow) + cur_window = None diff --git a/docs/python_api/ui_extension_tutorial/extension.json b/docs/python_api/ui_extension_tutorial/extension.json new file mode 100644 index 000000000..19e56ec0f --- /dev/null +++ b/docs/python_api/ui_extension_tutorial/extension.json @@ -0,0 +1,9 @@ +{ + "extension_api": 1, + "name": "Tutorial extension", + "version": "1.0", + "minimum_renderdoc": "1.12", + "description": "Tutorial extension from the documentation.", + "author": "Baldur Karlsson ", + "url": "https://github.com/baldurk/renderdoc" +} diff --git a/docs/python_api/ui_extensions.rst b/docs/python_api/ui_extensions.rst new file mode 100644 index 000000000..32d0ed3c7 --- /dev/null +++ b/docs/python_api/ui_extensions.rst @@ -0,0 +1,242 @@ +Writing UI extensions +===================== + +This document outlines how to get started writing a UI extension. For information on how to configure, register and install a UI extension see :doc:`../how/how_python_extension`. + +First steps +----------- + +We start off with the basic registration function. Create an ``__init__.py`` in your extension's root and fill it out: + +.. highlight:: python +.. code:: python + + import qrenderdoc as qrd + + extiface_version = '' + + def register(version: str, ctx: qrd.CaptureContext): + global extiface_version + extiface_version = version + + print("Registering my extension for RenderDoc version {}".format(version)) + + def unregister(): + print("Unregistering my extension") + +Here we create the minimum ``register()`` and ``unregister()`` functions required for an extension to load, that just print a message. We store the interface version in a global which we can use in future to do version-checks if we want to be compatible with more than one RenderDoc version, since the python interface is not fully forwards and backwards compatible. + +This doesn't really do much, let's register a tool menu item: + +.. highlight:: python +.. code:: python + + def menu_callback(ctx: qrd.CaptureContext, data): + ctx.Extensions().MessageDialog("Hello from the extension!", "Extension message") + + def register(version: str, ctx: qrd.CaptureContext): + # as above ... + + ctx.Extensions().RegisterWindowMenu(qrd.WindowMenu.Tools, ["My extension"], menu_callback) + +Now we have a new menu item which when clicked produces a popup message dialog! + +.. figure:: ../imgs/python_ext/Step1.png + + Python extension generating a message box + +This is a good proof of concept, but really we want something more directly usable. Instead of showing a message box, let's show a window which reacts to the selected drawcall by showing a series of breadcrumbs for marker labels. + +Adding a window and capture viewer +---------------------------------- + +First we create a class to handle our window and to derive from :py:class:`qrenderdoc.CaptureViewer` to get callbacks for events. + +.. highlight:: python +.. code:: python + + class Window(qrd.CaptureViewer): + def __init__(self, ctx: qrd.CaptureContext, version: str): + super().__init__() + + self.mqt: qrd.MiniQtHelper = ctx.Extensions().GetMiniQtHelper() + + self.ctx = ctx + self.version = version + self.topWindow = self.mqt.CreateToplevelWidget("Breadcrumbs", lambda c, w, d: window_closed()) + + ctx.AddCaptureViewer(self) + + def OnCaptureLoaded(self): + pass + + def OnCaptureClosed(self): + pass + + def OnSelectedEventChanged(self, event): + pass + + def OnEventChanged(self, event): + pass + +Here we implement stubs for the different events. More information on when they are sent can be found in the class documentation. We use the :py:class:`qrenderdoc.MiniQtHelper` to create a top-level window for ourselves with the 'breadcrumbs' title, then register oureslves as a capture viewer. The mini-Qt helper is useful to provide simple access to Qt widgets in a portable way from the RenderDoc UI, without relying on full Qt python bindings that may not be available depending on how RenderDoc was built. + +We will need to unregister ourselves as a capture viewer when the window is closed, which happens in the ``window_closed()`` callback that we'll define later. + +An empty window is not very useful, so let's give ourselves a label. More complex layouts and widgets are of course possible but for the moment we'll keep it simple: + +.. highlight:: python +.. code:: python + + vert = self.mqt.CreateVerticalContainer() + self.mqt.AddWidget(self.topWindow, vert) + + self.breadcrumbs = self.mqt.CreateLabel() + + self.mqt.AddWidget(vert, self.breadcrumbs) + +And finally we can fill in the event functions to set the breadcrumbs. We use ``@1234`` syntax for events which causes them to be clickable links that jump to that event. You can also convert a :py:class:`renderdoc.ResourceId` to a string with ``str()`` and it will similarly provide a link for that resource named with the current debug name. + +.. highlight:: python +.. code:: python + + def OnCaptureLoaded(self): + self.mqt.SetWidgetText(self.breadcrumbs, "Breadcrumbs:") + + def OnCaptureClosed(self): + self.mqt.SetWidgetText(self.breadcrumbs, "Breadcrumbs:") + + def OnSelectedEventChanged(self, event): + pass + + def OnEventChanged(self, event): + draw = self.ctx.GetDrawcall(event) + + breadcrumbs = '' + + if draw is not None: + breadcrumbs = '@{}: {}'.format(draw.eventId, draw.name) + + while draw.parent is not None: + draw = draw.parent + breadcrumbs = '@{}: {}'.format(draw.eventId, draw.name) + '\n' + breadcrumbs + + self.mqt.SetWidgetText(self.breadcrumbs, "Breadcrumbs:\n{}".format(breadcrumbs)) + +Finally we'll register a new menu item to display the window. We only allow one window at once, so if it still exists we'll just raise it. Otherwise we create a new one. This is also where we unregister the capture viewer: + +.. highlight:: python +.. code:: python + + from typing import Optional + + + cur_window: Optional[Window] = None + + + def window_closed(): + global cur_window + if cur_window is not None: + cur_window.ctx.RemoveCaptureViewer(cur_window) + cur_window = None + + + def open_window_callback(ctx: qrd.CaptureContext, data): + global cur_window + + mqt = ctx.Extensions().GetMiniQtHelper() + + if cur_window is None: + cur_window = Window(ctx, extiface_version) + if ctx.HasEventBrowser(): + ctx.AddDockWindow(cur_window.topWindow, qrd.DockReference.TopOf, ctx.GetEventBrowser().Widget(), 0.1) + else: + ctx.AddDockWindow(cur_window.topWindow, qrd.DockReference.MainToolArea, None) + + ctx.RaiseDockWindow(cur_window.topWindow) + + + def register(version: str, ctx: qrd.CaptureContext): + # as above ... + + ctx.Extensions().RegisterWindowMenu(qrd.WindowMenu.Window, ["Extension Window"], window_callback) + + + def unregister(): + print("Unregistering my extension") + + global cur_window + + if cur_window is not None: + # The window_closed() callback will unregister the capture viewer + cur_window.ctx.Extensions().GetMiniQtHelper().CloseToplevelWidget(cur_window.topWindow) + cur_window = None + +With that we now have a new little breadcrumbs window that docks itself above our event browser to show where we are in the frame: + +.. figure:: ../imgs/python_ext/Step2.png + + Python extension showing the current draw's breadcrumbs + +Calling onto replay thread +-------------------------- + +So far this has worked well, but we're only using information available on the UI thread. A good amount of useful information is cached on the UI thread including the current pipeline state and drawcalls, but for some work we might want to call into the underlying analysis functions. When we do this we must do it on the replay thread to avoid blocking the UI if the analysis work takes a long time. + +This can get quite complex so we will do something very simple, in the message box callback that we created earlier instead of displaying the message box immediately we will first figure out the minimum and maximum values for the current depth output or first colour output and display that. + +To start with we can identify the resource on the UI thread, so let's do that: + +.. highlight:: python +.. code:: python + + import renderdoc as rd + + def menu_callback(ctx: qrd.CaptureContext, data): + texid = rd.ResourceId.Null() + depth = ctx.CurPipelineState().GetDepthTarget() + + # Prefer depth if possible + if depth.resourceId != rd.ResourceId.Null(): + texid = depth.resourceId + else: + cols = ctx.CurPipelineState().GetOutputTargets() + + # See if we can get the first colour target instead + if len(cols) > 1 and cols[0].resourceId != rd.ResourceId.Null(): + texid = cols[0].resourceId + + if texid == rd.ResourceId.Null(): + ctx.Extensions().MessageDialog("Couldn't find any bound target!", "Extension message") + return + + +This all happens as before on the UI thread using UI-cached pipeline state data. If we can't find a resource we just bail out, but otherwise we have ``texid`` with the texture we want to analyse. + +To do this we invoke onto a different thread twice - first the UI thread invokes onto the replay thread to calculate the minimum and maximum values. Then that callback invokes back onto the UI thread to display a message. + +.. highlight:: python +.. code:: python + + if texid == rd.ResourceId.Null(): + ctx.Extensions().MessageDialog("Couldn't find any bound target!", "Extension message") + return + else: + mqt = ctx.Extensions().GetMiniQtHelper() + texname = ctx.GetResourceName(texid) + + def get_minmax(r: rd.ReplayController): + minvals, maxvals = r.GetMinMax(texid, rd.Subresource(), rd.CompType.Typeless) + + msg = '{} has min {:.4} and max {:.4} in red'.format(texname, minvals.floatValue[0], maxvals.floatValue[0]) + + mqt.InvokeOntoUIThread(lambda: ctx.Extensions().MessageDialog(msg, "Extension message")) + + ctx.Replay().AsyncInvoke('', get_minmax) + +Now that we've done that correctly our extension will be able to run in-depth replay analysis without calling functions from the wrong thread or stalling the UI. + +Conclusion +---------- + +Hopefully now from that worked example you have an idea of the basics of writing UI extensions. More complex examples can be found at the `community contributed repository `_ and the source code for this extension is available in the `github repository `_