Remote Capture and Replay

This example is a bit different since it’s not ready-to-run. It provides a template for how you can capture and replay on a remote machine, instead of the local machine. It also shows how to use device protocols to automatically manage devices.

First we can enumerate which device protocols are currently supported.

protocols = rd.GetSupportedDeviceProtocols()

Each string in the list corresponds to a protocol that can be used for managing devices. If we’re using one we can call GetDeviceProtocolController() passing the protocol name and retrieve the controller.

The controller provides a few methods for managing devices. First we can call GetDevices() to return a list of device IDs. The format of these device IDs is protocol-dependent but will be equivalent to a normal hostname. Devices may have human-readable names obtainable via GetFriendlyName().

protocol = rd.GetDeviceProtocolController(protocol_to_use)

devices = protocol.GetDevices()

if len(devices) == 0:
    raise RuntimeError(f"no {protocol_to_use} devices connected")

# Choose the first device
dev = devices[0]
name = protocol.GetFriendlyName(dev)

print(f"Running test on {dev} - named {name}")

URL = protocol.GetProtocolName() + "://" + dev

The URL will be used the same as we would use a hostname, when connecting for target control or remote servers.

Note that protocols may have additional restrictions - be sure to check IsSupported() to check if the device is expected to function at all, and SupportsMultiplePrograms() to see if it supports launching multiple programs. If multiple programs are not supported, you should ensure all running capturable programs are closed before launching a new one.

To begin with we create a remote server connection using CreateRemoteServerConnection(). The URL is as constructed above for protocol-based connections, or a simple hostname/IP if we’re connecting directly to remote machine.

If the connection fails, normally we must fail but if we have a device protocol available we can attempt to launch the remote server automatically using StartRemoteServer().

if status == rd.ReplayStatus.NetworkIOFailed and protocol is not None:
  # If there's just no I/O, most likely the server is not running. If we have
  # a protocol, we can try to start the remote server
  print("Couldn't connect to remote server, trying to start it")

  status = protocol.StartRemoteServer(URL)

  if status != rd.ReplayStatus.Succeeded:
    raise RuntimeError(f"Couldn't launch remote server, got error {str(status)}")

  # Try to connect again!
  status,remote = rd.CreateRemoteServerConnection(URL)

Note

The remote server connection has a default timeout of 5 seconds. If the connection is unused for 5 seconds, the other side will disconnect and subsequent use of the interface will fail.

Once we have a remote server connection, we can browse the remote filesystem for the executable we want to launch using GetHomeFolder() and ListFolder().

Then once we’ve selected the executable, we can launch the remote program for capturing with ExecuteAndInject(). This function is almost identical to the local ExecuteAndInject() except that it is not possible to wait for the program to exit.

In our sample, we now place the remote server connection on a background thread that will ping it each second to keep the connection alive while we use a target control connection to trigger a capture in the application.

def ping_remote(remote, kill):
  success = True
  while success and not kill.is_set():
    success = remote.Ping()
    time.sleep(1)

kill = threading.Event()
ping_thread = threading.Thread(target=ping_remote, args=(remote,kill))
ping_thread.start()

To connect to and control an application we use CreateTargetControl() with the URL as before and the ident returned from ExecuteAndInject().

target = rd.CreateTargetControl(URL, result.ident, 'remote_capture.py', True)

# Here we wait for whichever condition you want
target.TriggerCapture(1)

There are a couple of ways to trigger a capture, both TriggerCapture() and QueueCapture() depending on whether you want a time-based or frame-based trigger. The application itself can also use the in-application API to trigger a capture.

The target control connection can be intermittently polled for messages using ReceiveMessage(), which keeps the connection alive and will return any new information such as the data for a new capture that has been created. A message of type NewCapture indicates a new capture has been created, and newCapture contains the information including the path.

msg = target.ReceiveMessage(None)

# Once msg.type == rd.TargetControlMessageType.NewCapture has been retrieved

cap_path = msg.newCapture.path
cap_id = msg.newCapture.captureId

Once the capture has been found we are finished with the target control connection so we can shut it down and stop the background thread that was keeping the remote server connection alive. Using the remote server connection we can copy the capture back to the local machine with CopyCaptureFromRemote(). Similarly if we wanted to load a previously made capture that wasn’t on the remote machine CopyCaptureToRemote() would be useful to copy it ready to be opened.

Finally to open the capture we use, and that returns a ReplayController which can be used as normal and will tunnel over the remote server connection. It can be useful to intermittently ping the remote server connection to check that it’s still valid, and remote server and controller calls can be interleaved as long as they don’t overlap on multiple threads.

Example Source

Download the example script.

import renderdoc as rd
import threading
import time

# This sample is intended as an example of how to do remote capture and replay
# as well as using device protocols to automatically enumerate remote targets.
#
# It is not complete since it requires filling in with custom logic to select
# the executable and trigger the capture at the desired time
raise RuntimeError("This sample should not be run directly, read the source")

protocols = rd.GetSupportedDeviceProtocols()

print(f"Supported device protocols: {protocols}")

# Protocols are optional - they allow automatic detection and management of
# devices.
if protocol_to_use is not None:
    # the protocol must be supported
    if protocol_to_use not in protocols:
        raise RuntimeError(f"{protocol_to_use} protocol not supported")

    protocol = rd.GetDeviceProtocolController(protocol_to_use)

    devices = protocol.GetDevices()

    if len(devices) == 0:
        raise RuntimeError(f"no {protocol_to_use} devices connected")

    # Choose the first device
    dev = devices[0]
    name = protocol.GetFriendlyName(dev)

    print(f"Running test on {dev} - named {name}")

    URL = protocol.GetProtocolName() + "://" + dev

    # Protocols can enumerate devices which are not supported. Capture/replay
    # is not guaranteed to work on these devices
    if not protocol.IsSupported(URL):
        raise RuntimeError(f"{dev} doesn't support capture/replay - too old?")

    # Protocol devices may be single-use and not support multiple captured programs
    # If so, trying to execute a program for capture is an error
    if not protocol.SupportsMultiplePrograms(URL):
        # check to see if anything is running. Just use the URL
        ident = rd.EnumerateRemoteTargets(URL, 0)

        if ident != 0:
            raise RuntimeError(f"{name} already has a program running on {ident}")
else:
    # If you're not using a protocol then the URL can simply be a hostname.
    # The remote server must be running already - how that is done is up
    # to you. Everything else will work the same over a normal TCP connection
    protocol = None
    URL = hostname

# Let's try to connect
status,remote = rd.CreateRemoteServerConnection(URL)

if status == rd.ReplayStatus.NetworkIOFailed and protocol is not None:
    # If there's just no I/O, most likely the server is not running. If we have
    # a protocol, we can try to start the remote server
    print("Couldn't connect to remote server, trying to start it")

    status = protocol.StartRemoteServer(URL)

    if status != rd.ReplayStatus.Succeeded:
        raise RuntimeError(f"Couldn't launch remote server, got error {str(status)}")

    # Try to connect again!
    status,remote = rd.CreateRemoteServerConnection(URL)

if status != rd.ReplayStatus.Succeeded:
    raise RuntimeError(f"Couldn't connect to remote server, got error {str(status)}")

# We now have a remote connection. This works regardless of whether it's a device
# with a protocol or not. In fact we are done with the protocol at this point
protocol = None

print("Got connection to remote server")

# GetHomeFolder() gives you a good default path to start with.
# ListFolder() lists the contents of a folder and can recursively
# browse the remote filesystem.
home = remote.GetHomeFolder()
paths = remote.ListFolder(home)

print(f"Executables in home folder '{home}':")

for p in paths:
    print("  - " + p.filename)

# Select your executable, perhaps hardcoded or browsing using the above
# functions
exe,workingDir,cmdLine,env,opts = select_executable()

print(f"Running {exe}")

result = remote.ExecuteAndInject(exe, workingDir, cmdLine, env, opts)

if result.status != rd.ReplayStatus.Succeeded:
    remote.ShutdownServerAndConnection()
    raise RuntimeError(f"Couldn't launch {exe}, got error {str(result.status)}")

# Spin up a thread to keep the remote server connection alive while we make a capture,
# as it will time out after 5 seconds of inactivity
def ping_remote(remote, kill):
    success = True
    while success and not kill.is_set():
        success = remote.Ping()
        time.sleep(1)

kill = threading.Event()
ping_thread = threading.Thread(target=ping_remote, args=(remote,kill))
ping_thread.start()

# Create target control connection
target = rd.CreateTargetControl(URL, result.ident, 'remote_capture.py', True)

if target is None:
    kill.set()
    ping_thread.join()
    remote.ShutdownServerAndConnection()
    raise RuntimeError(f"Couldn't connect to target control for {exe}")

print("Connected - waiting for desired capture")

# Wait for the capture condition we want
capture_condition()

print("Triggering capture")

target.TriggerCapture(1)

# Pump messages, keep waiting until we get a capture message. Time out after 30 seconds
msg = None
start = time.clock()
while msg is None or msg.type != rd.TargetControlMessageType.NewCapture:
    msg = target.ReceiveMessage(None)

    if time.clock() - start > 30:
        break

# Close the target connection, we're done either way
target.Shutdown()
target = None

# Stop the background ping thread
kill.set()
ping_thread.join()

# If we didn't get a capture, error now
if msg.type != rd.TargetControlMessageType.NewCapture:
    remote.ShutdownServerAndConnection()
    raise RuntimeError("Didn't get new capture notification after triggering capture")

cap_path = msg.newCapture.path
cap_id = msg.newCapture.captureId

print(f"Got new capture at {cap_path} which is frame {msg.newCapture.frameNumber} with {msg.newCapture.api}")

# We could save the capture locally
# remote.CopyCaptureFromRemote(cap_path, local_path, None)


# Open a replay. It's recommended to set no proxy preference, but you could
# call remote.LocalProxies and choose an index.
#
# The path must be remote - if the capture isn't freshly created then you need
# to copy it with remote.CopyCaptureToRemote()
status,controller = remote.OpenCapture(rd.RemoteServer.NoPreference, cap_path, rd.ReplayOptions(), None)

if status != rd.ReplayStatus.Succeeded:
    remote.ShutdownServerAndConnection()
    raise RuntimeError(f"Couldn't open {cap_path}, got error {str(result.status)}")

# We can now use replay as normal.
#
# The replay is tunnelled over the remote connection, so you don't have to keep
# pinging the remote connection while using the controller. Use of the remote
# connection and controller can be interleaved though you should only access
# them from one thread at once. If they are both unused for 5 seconds though,
# the timeout will happen, so if the controller is idle it's advisable to ping
# the remote connection

sampleCode(controller)

print("Shutting down")

controller.Shutdown()

# We can still use remote here - e.g. capture again, replay something else,
# save the capture, etc

remote.ShutdownServerAndConnection()