DirectX Compiler APIs

If you are used to building HLSL with fxc.exe, your build process can now use dxc.exe to compile shaders with the new DirectX Shader compiler. This is convenient for standalone projects. Games, middleware and asset pipelines will often have a need to do this multiple times and/or from source code that doesn't exist as a file on disk, and prefer to use a more direct mechanism.

Compiling a shader in-memory is the main use case for the dxcompiler.dll APIs. While the design for d3dcompiler_47.dll was a set of plain Win32 APIs like D3DCompile, we opted to use a design more in line with Direct3D or other components such xmllite, with the following goals in mind.

  • Provide a simple mechanism for extensibility of the library as a whole.
  • Provide a robust mechanism for componentizing and versioning the areas of functionality within the library.
  • Remove some patterns that made it difficult to interoperate with other COM or .NET components.
  • Simplify some scenarios, especially the case of inclusion handlers.
  • Allow extensibility of the language for common scenarios.
  • Enable people to write languages that target DXIL.

There will be a future post where we cover these in more detail. For this post, I want to focus on the mechanics of writing a program that can compile code on the fly. We'll walk through a simple example that does this, omitting error checks for clarity.

Let's start a .cpp program with the following code.

[code lang="cpp"]
#include <windows.h>
#include "include/dxc/dxcapi.h"

void main() {
// More code
}

Now we'll create an object to represent the program text, representing a blob of bytes with some specific text encoding. This is a very simple interface that provides a potentially large amount of text, specifying the encoding so there is no ambiguity (API arguments for short strings will typically be null-terminated wchar_t pointers). The interface provides lifetime management of the buffer (via the AddRef/Release mechanism), and allows efficient sub-blobs to be created.

We'll be lazy productive here and create a stock implementation that can reference some data on the stack - there are other convenience routines to create these from a file, from another blob, and other variations.

[code lang="cpp"]
const char Program[] = "float4 main() : SV_Target { return 1; }";
IDxcLibrary *pLibrary;
IDxcBlobEncoding *pSource;
DxcCreateInstance(CLSID_DxcLibrary, __uuidof(IDxcLibrary), (void **)&pLibrary);
pLibrary->CreateBlobWithEncodingFromPinned(Program, _countof(Program), CP_UTF8, &pSource);

Now, we'll create a compiler object and pass this program to it, with some extra arguments to make it more interesting.

[code lang="cpp"]
LPCWSTR ppArgs[] = { L"/Zi" }; // debug info
IDxcCompiler *pCompiler;
DxcCreateInstance(CLSID_DxcCompiler, __uuidof(IDxcCompiler), (void **)&pCompiler);

IDxcOperationResult *pResult;
pCompiler->Compile(
pSource,          // program text
L"myfile.hlsl",   // file name, mostly for error messages
L"main",          // entry point function
L"ps_6_0",        // target profile
ppArgs,           // compilation arguments
_countof(ppArgs), // number of compilation arguments
nullptr, 0,       // name/value defines and their count
nullptr,          // handler for #include directives
&pResult);

At this point, unless something happened that prevented compilation altogether, we'll have an IDxcOperationResult instance. This provides us with the full results of compilation: whether a program was produced, and a buffer with errors and warnings that may have occurred. Here's one way we can handle that.

[code lang="cpp"]
HRESULT hrCompilation;
pResult->GetStatus(hrCompilation);

if (SUCCEEDED(hrCompilation)) {
IDxcBlob *pResult;
pResult->GetResult(&pResult);
// Save to a file, disassemble and print, store somewhere ...
pResult->Release();
}
else {
IDxcBlobEncoding *pPrintBlob, *pPrintBlob16;
pResult->GetErrorBuffer(&pPrintBlob);
// We can use the library to get our preferred encoding.
pLibrary->GetBlobAsUtf16(pPrintBlob, &pPrintBlob16);
wprintf(L"%*s", (int)pPrintBlob16->GetBufferSize()/2,(LPCWSTR)pPrintBlob16->GetBufferPointer());
pPrintBlob->Release();
pPrintBlob16->Release();
}

You will generally find we have a more fine-grained API than with d3dcompiler; the extra precision you get will lead to fewer buffer copies and more sharing - and, importantly, fewer surprises if you run on a system with a different default character encoding.

Enjoy!