August 13, 2020

Thread safety in Visionary Render plugins

API
Plugins
Visionary Render
Jamie Femia
Jamie Femia
Engineering Manager

When working with the APIs in Visionary Render (VisRen) to integrate a third party service, it is quite common for that service to involve communicating over the internet. This type of communication can take between tens and thousands of milliseconds, and so for real-time graphical applications like VisRen it is important that this type of operation is performed asynchronously - that is, it will not stop the rest of the application from running while it is waiting for the result.

The plugin API in VisRen provides a number of entry points for plugins to use - and most of these are executed by the main thread - the same thread that does the rendering. It does this because it needs to have the latest information available before drawing to the screen, and plugins need to be able to update that information too.

Asynchronous results

So how do we, as plugin authors, make sure we use this data safely? Usually it depends what we need to do with it. Let’s use an example like the Speech to Text plugin - this plugin takes results of speech to text, which are provided asynchronously, and puts them into the Lua state in VisRen, which needs to be updated on the main thread.

How do we achieve this?

There are three distinct “parts” we need for this.

  1. The code that receives the data from the service (asynchronously)
  2. A message queue to queue up the speech result from its callback (which is probably not the main thread)
  3. A function that does run on the main thread, which reads the queue and controls the Lua state

The first two are straightforward in this case - and C# makes this easy with its {% c-line %}ConcurrentQueue{% c-line-end %} class.

Queue up the speech results

We define a queue to receive spoken text, somewhere it is available to both the callbacks from the external service, and our main-thread function

-- CODE language-csharp line-numbers-- public ConcurrentQueue<string> SpeechQueue;


Then our speech recognizer callback can push into this queue safely

-- CODE language-csharp line-numbers-- Recognizer.Recognized += (s, e) => { if(e.Result.Reason == ResultReason.RecognizedSpeech) { SpeechQueue.Enqueue(e.Result.Text); } };

Put the queue contents into Lua

The VisRen Lua environment has a generic callback system which is ideal for this. There is a function for registering an arbitrary function as a named callback, {% c-line %}__registerCallback("some-arbirary-name", function() … end){% c-line-end %}, and a function for calling named callbacks, {% c-line %}__callback("some-arbirary-name"){% c-line-end %}.

We can make use of this and define our own callback for “onSpeech”, which we can call with the text from the queue.

We need to do this on the main thread, and the plugin API includes an “Update” callback, which is called once every frame before rendering takes place. This is a good place to gather results of asynchronous tasks and process them safely.

In the implementation of our update function we can do something like this to process the queue and trigger the callbacks:

-- CODE language-lua line-numbers-- while(SpeechQueue.TryDequeue(out var text)) { FFIVarHandle[] args = { FFI.MakeString("onSpeech"), FFI.MakeString(text) }; FFI.Invoke("__callback", args); }

The FFI (foreign function interface) is part of the native API and it enables direct control over the Lua environment in VisRen. Here we are calling the callback with the spoken text. So if we said “Hello”, this executes {% c-line %} __callback("onSpeech", "Hello"){% c-line-end %}in the Lua environment.

And there we have a thread-safe way of getting data, asynchronously, and passing it through to Lua where scripts in a scene can make use of it, by calling {% c-line %}__registerCallback("onSpeech", function(text) print(text) end){% c-line-end %}.