1Introduction
zdxc is a from-scratch HLSL 2021 compiler written in Zig. It emits DXIL and SPIR-V and has no package dependencies. The same compiler is available through four interfaces:
| Interface | Use |
|---|---|
zdxc |
A dxc-style command-line compiler. |
libzdxc |
A small C API that returns compilation outputs in memory. |
zdxc Zig module |
Direct access to the compiler from a Zig build. |
dxc / dxcompiler |
Drop-in names and entry points for existing DXC clients. |
The source language is HLSL 2021; -HV 2021 is the only language version.
DXIL output is signed by zdxc itself, so no separate dxil library is needed.
2Getting started
Download
- Get the bundle
Download it for your platform from the releases.unzip zdxc-VERSION-PLATFORM.zip && cd zdxc-VERSION-PLATFORMbin/holdszdxcand the DXC drop-in;lib/andinclude/holdlibzdxcandzdxc.h. - Drop in for DXC
Putcp bin/dxc* bin/*dxcompiler* ENGINE/bin/dxcanddxcompilerwhere your engine looks for DXC's. There is nodxil.dllto copy. - Compile a shader
dxc's command line. Addbin/zdxc -T ps_6_0 -E main shader.hlsl -Fo shader.dxil-spirvfor SPIR-V, or compile many shaders at once with--job.
Build from source
- Get the source
One Zig codebase with no dependencies.git clone https://github.com/OWNER/zdxc.git && cd zdxc - Build
Needs Zig 0.17 and nothing else. You getzig build -Doptimize=ReleaseFastzdxc,libzdxc.aandzdxc.hinzig-out. - Drop in for DXC
Buildszig build dropin -Doptimize=ReleaseFastdxcanddxcompiler. Put them where your engine looks for DXC's; there is nodxil.dllto copy. - Compile a shader
dxc's command line. Addzig-out/bin/zdxc -T ps_6_0 -E main shader.hlsl -Fo shader.dxil-spirvfor SPIR-V, or compile many shaders at once with--job.
Requirements
Install Zig 0.17.0 or newer. zdxc has no library or package dependencies.
Build
Run the build from the zdxc package directory:
zig build -Doptimize=ReleaseFast
The default install contains:
zig-out/bin/zdxc command-line compiler
zig-out/lib/libzdxc.a static C library
zig-out/include/zdxc.h public C header
Build a shared C library instead with -Dlinkage=dynamic. Cross-compilation
uses Zig's usual target option:
zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast
Compile a shader
The command line follows dxc. Specify a target profile, entry point, input, and output file:
./zig-out/bin/zdxc -T cs_6_0 -E main shader.hlsl -Fo shader.dxil
./zig-out/bin/zdxc -T vs_6_0 -E main -spirv shader.hlsl -Fo shader.spv
Use zdxc -help for the complete option list and zdxc --version for the
compiler version.
3Command line
zdxc accepts dxc command-line arguments. A compilation can read its input from a file or, through the library interfaces, from memory.
| Option | Meaning |
|---|---|
-T <profile> |
Target profile, such as vs_6_0, ps_6_0, or cs_6_0. |
-E <name> |
Shader entry point. |
-D <name[=value]> |
Define a preprocessor macro. |
-I <directory> |
Add an include search directory. |
-Fo <file> |
Write the object to a file. |
-Fi <file> |
Write preprocessed output when used with -P. |
-Frs <file> |
Write a root-signature container. |
-P |
Preprocess instead of compiling. |
-spirv |
Emit SPIR-V instead of DXIL. |
-Zpr / -Zpc |
Select row- or column-major matrix packing. |
-O0 … -O3, -Od |
Select the optimization level. |
--job=<file> |
Compile a JSON batch of shaders in one process. |
DXIL is the default backend. Add -spirv for SPIR-V, with the -fvk-* and
-fspv-* options controlling Vulkan bindings, layout, and target environment.
Diagnostics and exit status
Diagnostics follow dxc's command-line format. Exit status is 0 for success, 1 for bad options or input/output failures, and 5 for source compilation errors. Warnings can appear in successful compilations.
4Batch compilation
zdxc --job=build/shaders.json compiles every shader a job file lists in
one process, on all cores. Starting a process costs more than compiling most
shaders (about 2 ms against a few hundred microseconds), so a build with many
shaders is much faster this way than with one compiler run per shader.
On the 1,481 corpus shaders zdxc compiles (10-core M-series Mac, release build):
| How | Wall time |
|---|---|
zdxc --job, all cores |
0.064 s |
zdxc --job, "threads": 1 |
0.24 s |
one zdxc per shader, 10 in parallel |
1.09 s |
one dxc per shader, 10 in parallel |
1.68 s |
The job file
{
"threads": 8,
"args": ["-HV", "2021", "-I", "shaders/include"],
"jobs": [
{ "args": ["-T", "cs_6_0", "shaders/blur.hlsl", "-Fo", "out/blur.dxo"] },
{ "args": ["-T", "ps_6_0", "-E", "PS", "shaders/mesh.hlsl", "-Fo", "out/mesh_ps.dxo"] },
{ "args": ["-T", "vs_6_0", "-E", "VS", "-spirv", "shaders/mesh.hlsl", "-Fo", "out/mesh_vs.spv"] }
]
}
jobs(required): each job'sargsis a dxc command line without the program name — any option zdxc accepts on its own command line.args(optional): options shared by every job.threads(optional): how many shaders compile at once. Defaults to the number of CPUs.
A job's full command line is the file's args, then any options given next
to --job on the real command line, then the job's own args. So
zdxc --job=shaders.json -O3 -Qstrip_debug adds -O3 -Qstrip_debug to every
job, and a job can override a shared option where a later option wins on the
command line. Unknown fields are errors, reported with their line and column.
Paths in the file are relative to the current directory, as on the command
line, not to the job file. --job cannot appear inside a job file.
Output and exit code
Every job writes its own -Fo / -Fh / ... outputs. What a job prints —
diagnostics, and text output such as -P without a file — comes out
exactly as one zdxc run of that command line would print it, kept together
and in job order, whatever order the jobs finish in. The outputs are
byte-identical to separate runs.
When jobs fail, the other jobs still run, and a last line says how many failed:
zdxc: 2 of 112 jobs failed
The exit code is 0 when every job succeeds; otherwise 1 if any job failed on its command line or on I/O (a missing input, an unwritable output), else 5 (compile errors) — the codes one zdxc run uses.
Reading and writing files is kept off the compiling threads: one thread reads the jobs' input files ahead of the compiles, and another writes their outputs after them. Files the jobs include are read once for the whole run, so a header that every shader includes is opened once (a file that changes while the run is going is not read again). An output that cannot be written is reported after every job's output, and fails its job.
5The C API
zdxc is also a library with a small C API, include/zdxc.h.
Unlike dxcompiler, it links statically: there is no DLL to ship or load, no
COM, and nothing to link besides the platform's C library.
zig build # zig-out/lib/libzdxc.a, zig-out/include/zdxc.h
zig build -Dlinkage=dynamic # libzdxc.so / .dylib / zdxc.dll instead
zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast
Link libzdxc.a like any static library (cc app.c -Izig-out/include zig-out/lib/libzdxc.a). It exports
only the zdxc_* functions; the compiler runtime it carries is hidden. For
a macOS binary you ship, give the target a minimum version
(-Dtarget=aarch64-macos.13.0), or the linker warns that the library was
built for a newer macOS than the app.
Compiling
A compilation is a dxc command line, with the outputs returned in memory:
#include <zdxc.h>
const char *args[] = {"-T", "ps_6_0", "-E", "PS", "-D", "SHADOWS=1"};
zdxc_compile_info info = {
.size = sizeof info,
.args = args,
.arg_count = 6,
.source = text,
.source_size = text_size,
.source_name = "mesh.hlsl",
};
zdxc_result *r = zdxc_compile(&info);
if (!r) return OUT_OF_MEMORY;
const char *diags = zdxc_result_output(r, ZDXC_OUTPUT_DIAGNOSTICS, NULL);
if (diags) fputs(diags, stderr); /* errors, or warnings on success */
if (zdxc_result_status(r) == ZDXC_STATUS_OK) {
size_t size;
const void *dxil = zdxc_result_output(r, ZDXC_OUTPUT_OBJECT, &size);
upload(dxil, size);
}
zdxc_result_free(r);
- Options are exactly the command line's (
-T,-E,-D,-I,-spirv,-fvk-*,-Zpr,-O3, ...), without the program name. - The source is in memory (
source, with or without a trailing NUL), or, whensourceis NULL, the input file named in the options. - Files — the input when given by name, and every
#includecandidate — are read from disk relative to the current directory, unless you pass aread_filecallback. The callback gets each candidate path (the including file's directory, then each-Idirectory) and returns 0 with the file's bytes, or nonzero to try the next candidate. Returned memory only has to last untilzdxc_compilereturns. - Status is
ZDXC_STATUS_OK(0),ZDXC_STATUS_FAILED(1: bad options, unreadable input) orZDXC_STATUS_COMPILE_FAILED(5: errors in the source) — the command line's exit codes.zdxc_compilereturns NULL only when out of memory. - Outputs are
OBJECT(a DXIL container or SPIR-V module),DIAGNOSTICS,PREPROCESSED(-P),ROOT_SIGNATURE(-Frs) andTEXT(-help,--version). Each is NUL-terminated for convenience and lives untilzdxc_result_free.zdxc_result_output_namegives the name an option assigned (-Fo,-Fi,-Frs); zdxc never writes files.
The output is byte-for-byte what the command line writes for the same options and files.
Threads
zdxc has no global state. Any number of threads may call zdxc_compile at
once, and a result may be read or freed on any thread.
Zig
Zig projects can skip the C API: depend on this package and import the
zdxc module, whose compile.compile(arena, request) is what the C API
wraps.
6The DXC drop-in
Programs that run DXC usually do it in one of two ways: they call the dxc
executable, or they load dxcompiler (dxcompiler.dll,
libdxcompiler.so, libdxcompiler.dylib) and call DxcCreateInstance.
SDL_shadercross, for one, does the second. The dropin step builds both on
top of zdxc, so these programs can use zdxc without being changed:
zig build dropin # zig-out/bin/dxc, zig-out/lib/libdxcompiler.{so,dylib}
zig build dropin -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast
# zig-out/bin/dxc.exe, dxcompiler.dll, lib/dxcompiler.lib
Put them where the program looks for DXC, in place of DXC's files. There is
no dxil.dll: zdxc signs DXIL itself.
dxcis the zdxc command line under DXC's name.dxcompilerexportsDxcCreateInstanceandDxcCreateInstance2, withdxcapi.h's ABI. On Windows it imports onlykernel32andntdll. On other platforms it links the C library and nothing else.
What the library implements
| Class | Interfaces |
|---|---|
CLSID_DxcCompiler |
IDxcCompiler3 (Compile), IDxcVersionInfo (reports 1.11) |
CLSID_DxcUtils (also reached as CLSID_DxcLibrary) |
IDxcUtils: blob creation, LoadFile, CreateDefaultIncludeHandler, GetBlobAsUtf8 / GetBlobAsWide, GetDxilContainerPart |
Compile takes the same arguments as the command line and follows DXC:
- Status. A compile error is a successful call whose status is
E_FAIL. A bad option has statusE_INVALIDARG. - Outputs. The result holds the same outputs DXC's does: the object,
the errors (UTF-8, empty when there are none), the shader hash for DXIL,
the root signature,
-Ptext, and the names given with-Fo,-Fiand-Frs. - Includes. Includes go through the
IDxcIncludeHandleryou pass, with paths spelled the way DXC spells them (./inc/common.hlsli). With no handler, includes fail, as they do in DXC. - Accepted-but-ignored options. Options that zdxc accepts but does not act on yet produce no note here. Many clients treat any diagnostic text as a warning.
Classes and methods that are not implemented return REGDB_E_CLASSNOTREG
or E_NOTIMPL. These include:
- the legacy
IDxcCompilerandIDxcCompiler2interfaces; Disassemble;CreateReflection,BuildArgumentsandGetPDBContents;- the validator, linker, container and PDB classes.
The result has no DXC_OUT_REFLECTION output (the reflection container
that CreateReflection reads).
Language support is zdxc's own: HLSL 2021, with -HV 2021 as the only
version.
7Zig integration
Declare zdxc as a package dependency, then make its module available to your executable or library:
const zdxc = b.dependency("zdxc", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zdxc", zdxc.module("zdxc"));
Import the module and call the same compilation layer used by the command line and C API:
const std = @import("std");
const zdxc = @import("zdxc");
pub fn compileShader(arena: std.mem.Allocator, io: std.Io, source: []const u8) !zdxc.compile.Result {
return zdxc.compile.compile(arena, .{
.args = &.{ "-T", "ps_6_0", "-E", "PS", "-D", "SHADOWS=1" },
.source = source,
.source_name = "mesh.hlsl",
.io = io,
});
}
The returned result contains the status, object, diagnostics, and any requested
auxiliary outputs. Its allocations belong to the supplied arena. The compiler
does not write files; -Fo, -Fi, and -Frs only name the corresponding
in-memory outputs.
8API reference
The public C API is declared in zdxc.h. Each symbol below has
its declaration, ownership rules, and a link to the corresponding source line.
ZDXC_API_VERSIONmacrosource
#define ZDXC_API_VERSION 1
The public C API version. The current version is 1.
zdxc_resulttypesource
typedef struct zdxc_result zdxc_result;
An opaque compilation result. It owns the object, diagnostics, and other
outputs returned by zdxc_compile. Read its outputs with
zdxc_result_output and release it with zdxc_result_free.
zdxc_statustypesource
typedef enum zdxc_status {
ZDXC_STATUS_OK = 0,
ZDXC_STATUS_FAILED = 1,
ZDXC_STATUS_COMPILE_FAILED = 5
} zdxc_status;
A compilation's outcome. The values match dxc's command-line exit codes.
ZDXC_STATUS_OK
Compilation succeeded. Diagnostics may still contain warnings.
ZDXC_STATUS_FAILED
Bad options or an input that could not be read.
ZDXC_STATUS_COMPILE_FAILED
The source contains compilation errors.
zdxc_outputtypesource
typedef enum zdxc_output {
ZDXC_OUTPUT_OBJECT = 0,
ZDXC_OUTPUT_DIAGNOSTICS = 1,
ZDXC_OUTPUT_PREPROCESSED = 2,
ZDXC_OUTPUT_ROOT_SIGNATURE = 3,
ZDXC_OUTPUT_TEXT = 4
} zdxc_output;
Selects one output from a compilation result.
ZDXC_OUTPUT_OBJECT
The DXIL container or SPIR-V module.
ZDXC_OUTPUT_DIAGNOSTICS
Errors, warnings, and notes in command-line format.
ZDXC_OUTPUT_PREPROCESSED
Preprocessed source requested with -P.
ZDXC_OUTPUT_ROOT_SIGNATURE
The root-signature container requested with -Frs.
ZDXC_OUTPUT_TEXT
Other command-line text, such as -help or --version.
zdxc_read_file_fncallbacksource
typedef int (*zdxc_read_file_fn)(void *user, const char *path, const char **data, size_t *size);
Reads the main input or an #include candidate. On success, set data and
size and return 0. Return nonzero to let zdxc try the next candidate. The
returned bytes need only remain valid until zdxc_compile returns.
zdxc_compile_infostructsource
typedef struct zdxc_compile_info {
uint32_t size;
const char *const *args;
size_t arg_count;
const char *source;
size_t source_size;
const char *source_name;
zdxc_read_file_fn read_file;
void *user;
} zdxc_compile_info;
Describes one in-memory or file-based compilation. Initialize size to
sizeof(zdxc_compile_info) so later API versions can add fields.
size
The structure size in bytes; use sizeof(zdxc_compile_info).
args
dxc command-line options without the program name.
arg_count
Number of arguments in args.
source
The main source text, or NULL to read the named input file.
source_size
Source byte length; a trailing NUL is allowed.
source_name
Name used in diagnostics and relative include resolution.
read_file
Optional file callback; NULL reads from disk.
user
Opaque pointer passed to read_file.
zdxc_compilefunctionsource
zdxc_result *zdxc_compile(const zdxc_compile_info *info);
Runs one compilation. It returns NULL only for an invalid or too-small descriptor, or when memory is exhausted. Every compilation failure is reported by the result's status and diagnostics.
The compiler has no global state. Independent calls may run concurrently on any number of threads.
zdxc_result_statusfunctionsource
zdxc_status zdxc_result_status(const zdxc_result *result);
Returns the compilation's zdxc_status.
zdxc_result_outputfunctionsource
const void *zdxc_result_output(const zdxc_result *result, zdxc_output output, size_t *size);
Returns the selected output's bytes. The bytes are followed by a NUL for
convenience; size does not count that terminator and may be NULL. If the
output was not produced, returns NULL and stores zero in size.
The pointer remains valid until zdxc_result_free. Binary object outputs can
contain interior NUL bytes; always use the returned size.
zdxc_result_output_namefunctionsource
const char *zdxc_result_output_name(const zdxc_result *result, zdxc_output output);
Returns the file name assigned by -Fo, -Fi, or -Frs, or NULL when the
output has no assigned name. zdxc does not write that file itself.
zdxc_result_freefunctionsource
void zdxc_result_free(zdxc_result *result);
Releases a result and all of its outputs. Any output pointers previously obtained from it become invalid.
zdxc_versionfunctionsource
const char *zdxc_version(void);
Returns zdxc's version as a NUL-terminated string, such as 0.1.0.