Interrupting shader compilation


Unfortunately, there really isn't a way to interrupt compilation once it has started. Why would you ever want to do that? Well, if you're doing compilation (and you really should try to compile offline, although it's impossible in some cases), the user may decide to switch to a different application while you're busy. At that point, the operating system may decide to suspend your app, and so you have a limited amount of time to respond. If you are simply iterating through a long list of shaders to load and compile, you may run into problems.

One way to do this is to set a flag and interrupt your loop. But file access and compilation can easily be done in the background in parallel, so a simple way of doing this is leveraging the PPL library and its support for cancellation.

Here are some snippets for a simple Windows 8/8.1/10 project that will demonstrate how to put things together.

First, the UI for the project will be a two buttons to start and cancel compilation, and a block of text to display status.

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
 <Grid.RowDefinitions>
  <RowDefinition Height="Auto" />
  <RowDefinition Height="Auto" />
  <RowDefinition Height="*" />
 </Grid.RowDefinitions>
 <Button x:Name="StartButton" Click="StartButton_Click">Start</Button>
 <Button x:Name="CancelButton" Click="CancelButton_Click" Grid.Row="1">Cancel</Button>
 <TextBlock x:Name="MyBlock" Grid.Row="2" FontSize="24">Content</TextBlock>
</Grid>

Now, we'll add some include files and linker pragmas to access the compiler, along with some handy namespaces. These would go in your MainPage.xaml.cpp file, in addition to whatever the project template put there.

#include <atlbase.h>
#include <ppltasks.h>
#include <d3d11.h>
#include <d3dcompiler.h>

#pragma comment(lib, "d3dcompiler.lib")

using namespace std;
using namespace concurrency;
using namespace Windows::Storage;

For simplicity, we'll declare some globals to manage a simple setup: some files we want to load sources from, some tasks that we'll use for compilation, and the cancellation token source we would use for example when suspending.

const wchar_t* ShaderFileNames[] = {
 L"shader1.hlsl",
 L"shader2.hlsl",
 ...
 L"shader9.hlsl",
 L"shader10.hlsl",
};

CComPtr<ID3DBlob> ShaderBlobs[_countof(ShaderFileNames)];
task<void> setupShaderTasks[_countof(ShaderFileNames)];
cancellation_token_source cts;
task<void> setupShaderAllDone;

This is the event handler for StartButton_Click.

this->MyBlock->Text = L"Starting compilation";
for (int i = 0; i < _countof(ShaderFileNames); ++i) {
  setupShaderTasks[i] =
    create_task([i]() -> IAsyncOperation<StorageFile^>^
  {
    return Windows::Storage::ApplicationData::Current
      ->LocalFolder->GetFileAsync(
        Platform::StringReference(ShaderFileNames[i]));
    }, task_continuation_context::use_arbitrary())
  .then([i](StorageFile^ file) -> IAsyncOperation<String^>^
  {
    return Windows::Storage::FileIO::ReadTextAsync(file);
  }, task_continuation_context::use_arbitrary())
    .then([i](String^ data) -> void
  {
    // ReadTextAsync reads Unicode, but we compile ANSI.
    CW2A ansiData(data->Data());
    size_t ansiDataLen = strlen(ansiData);

    for (int j = 0; j < 100; ++j) {
      // Because we run a long loop, check for cancellation directly.
      if (is_task_cancellation_requested())
        cancel_current_task();

      // Play with flags, make variations if needed.

      // Here we'll just recompile into the same shader
      // to simulate doing more work.
      ShaderBlobs[i] = nullptr;

      HRESULT hr = D3DCompile(ansiData, ansiDataLen,
        "hlsl.hlsl", nullptr, nullptr, "main",
        "ps_5_0", 0, 0, &ShaderBlobs[i], nullptr);
      if (FAILED(hr))
        throw COMException::CreateException(hr);
    }
  }, task_continuation_context::use_arbitrary());
}

// Create a task to wait until all work is done.
setupShaderAllDone = when_all(
  &setupShaderTasks[0], setupShaderTasks + _countof(setupShaderTasks),
  cts.get_token());
setupShaderAllDone.then([this](task<void> t) -> void
{
  try {
    t.get();
    this->MyBlock->Text = L"task completed successfully";
  }
  catch (task_canceled) {
    this->MyBlock->Text = L"task canceled";
  }
  catch (...) {
    this->MyBlock->Text = L"task failed";
  }
}, task_continuation_context::use_current());

The handler for CancelButton_Click is a lot simpler. It simply calls cancel on the cancellation source.

cts.cancel();

This is all that's required. Go ahead and run the app if you're following along, or keep reading for a more detailed explanation.

When we click the start button, we will create tasks to open each file and compile their content. We use create_task with a lambda to execute and capture the index to the file we'll process, then chain calls to GetFileAsync, ReadTextAsync, and another lambda to invoke compilation. We are using the results of the tasks (rather than the tasks themselves) from one step to another, so exceptions thrown and cancellations will flow naturally from one continuation to another and interrupt execution (these are called value-based continuations). The only place we need to check is when we're going to run a lot of computation ourselves without giving PPL the chance to interrupt us - that's why the loop with D3DCompile is doing this itself.

Note also that we use an arbitrary continuation context for all this work, which means that it will get scheduled on a background worker thread, leaving our UI thread responsive.

Finally, we aggregate all these separate compilation tasks into a when_all task, passing the cancellation token we would like to use, and specifying use_current() to make sure we look at the resulting task on the UI thread, so we can directly update the feedback text.

Also note that we take a task parameter in the final setupShaderAllDone task, which means that we have to look at exceptions and cancellations ourselves. This is something we purposefully want to do, as we get to handle these cases ourselves, in this sample simply by updating the status text.

You can adapt this sample code to your game to make sure you stay responsive at all times, and don't introduce by accident states from which your app can't respond quickly.

Enjoy!

Comments (0)

Skip to main content