↑
ZDXC documentation0.0.0 (alpha)

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

Quick start

Download

  1. Get the bundle
    unzip zdxc-VERSION-PLATFORM.zip && cd zdxc-VERSION-PLATFORM
    Download it for your platform from the releases. bin/ holds zdxc and the DXC drop-in; lib/ and include/ hold libzdxc and zdxc.h.
  2. Drop in for DXC
    cp bin/dxc* bin/*dxcompiler* ENGINE/bin/
    Put dxc and dxcompiler where your engine looks for DXC's. There is no dxil.dll to copy.
  3. Compile a shader
    bin/zdxc -T ps_6_0 -E main shader.hlsl -Fo shader.dxil
    dxc's command line. Add -spirv for SPIR-V, or compile many shaders at once with --job.

Build from source

  1. Get the source
    git clone https://github.com/OWNER/zdxc.git && cd zdxc
    One Zig codebase with no dependencies.
  2. Build
    zig build -Doptimize=ReleaseFast
    Needs Zig 0.17 and nothing else. You get zdxc, libzdxc.a and zdxc.h in zig-out.
  3. Drop in for DXC
    zig build dropin -Doptimize=ReleaseFast
    Builds dxc and dxcompiler. Put them where your engine looks for DXC's; there is no dxil.dll to copy.
  4. Compile a shader
    zig-out/bin/zdxc -T ps_6_0 -E main shader.hlsl -Fo shader.dxil
    dxc's command line. Add -spirv for 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"] }
  ]
}

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);

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.

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:

Classes and methods that are not implemented return REGDB_E_CLASSNOTREG or E_NOTIMPL. These include:

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.