This repository provides a self-contained, Nix-powered developer environment for working on LLVM's HLSL features, Microsoft's DirectXShaderCompiler (DXC), and related test suites. It utilizes git submodules (with shallow cloning) and a maskfile.md for task automation.
Nix is a powerful package manager and build system. In this project, we use it (via flake.nix) to provide a perfectly reproducible development environment. When you run nix develop, Nix automatically downloads and configures exact versions of all necessary build tools and dependencies (like CMake, Ninja, Python, and specific C++ toolchains) without polluting your host operating system. This ensures that every developer has the exact same environment, eliminating "works on my machine" issues.
-
Enter the Nix Shell:
nix develop
-
Initialize Submodules: We use
maskas our task runner. Let's pull down a shallow clone of the dependencies to save time and disk space. This command automatically uses the--recursiveflag to ensure thatDirectXShaderCompiler's own nested submodules (likeSPIRV-ToolsandDirectX-Headers) are fully checked out:mask setup
-
Configure and Build the Projects: Once cloned, use the included tasks to configure and build the compilers. The tasks act on the checkout you are standing in, so
cdinto it first:cd llvm-project && mask configure && mask build cd ../DirectXShaderCompiler && mask configure && mask build
Or drive them from anywhere with
--in:mask build --in llvm-project clang mask build --in DirectXShaderCompiler
Every task that touches a checkout works the same way in a wt worktree as it
does in the submodule itself. There is no per-worktree setup: mask finds the
workspace maskfile.md from anywhere inside the dev shell, and the tasks work
out what you mean from the current directory.
cd llvm-project && wt switch --create texture-store # -> llvm-project.texture-store
mask build # builds llvm-project.texture-store/build
mask lit clang/test/CodeGenHLSL/RootSignatureBuild artifacts always live inside the worktree they belong to
(<worktree>/build, <worktree>/build-dist), so two agents working in two
worktrees never share a build directory, and wt remove takes the artifacts
with it. Concurrent mask invocations that would write to the same build
directory are serialised with a lock rather than corrupting it, and all
worktrees share one sccache instance, so the second build of the same upstream
sources is mostly cache hits.
mask ls # every worktree, its branch, whether it is built, its pins
mask info # what the current directory resolves to, and against whatA checkout rarely builds alone: an offload-test-suite build needs an
llvm-project worktree, and running its suites needs a dxc. Every task takes
the same dependency flags:
mask configure --llvm llvm-project.texture-store # build against that LLVM
mask test clang-vk --dxc DirectXShaderCompiler.my-fix # run against that DXC
mask build --in offload-test-suite.my-feature --llvm ../llvm-project.texture-storeA dependency is resolved in this order, first match wins:
--llvm/--dxc/--offload/--goldenon the command line$HLSL_LLVM/$HLSL_DXC/$HLSL_OFFLOAD/$HLSL_GOLDENin the environment (handy for an agent that wants one setting for a whole session)- a pin recorded by
mask link, or by the last successfulmask configure - a worktree of that repository checked out on the same branch name
- the submodule checkout in the workspace root
Step 3 is what makes the common case terse — configure once with the flags, and
every later mask build / mask test in that worktree keeps using them:
cd offload-test-suite.my-feature
mask link --llvm texture-store --dxc DirectXShaderCompiler
mask build && mask test clang-vkPins live in .hlsl-dev/pins/ at the workspace root, never inside the
checkouts, so git status in a worktree stays clean. mask unlink forgets
them.
A worktree spec can be a path, a directory name
(llvm-project.texture-store), just the suffix (texture-store), or a branch
name. --dxc additionally accepts a directory containing dxc/dxv, or nix
for the compiler that ships with the dev shell.
CodeGraph gives agents a symbol
graph of a checkout — callers, callees, impact radius — instead of grepping
2 GB of source. One index per worktree, for llvm-project,
DirectXShaderCompiler and offload-test-suite alike:
mask codegraph # index/refresh the current worktree
mask codegraph --in llvm-project.my-feature # ... or a named one
mask codegraph --all # every worktree of every repository
mask codegraph --fresh # rebuild from scratchEach repository has its own scope, in scripts/codegraph-<kind>.json:
| repository | indexed | left out | files |
|---|---|---|---|
| llvm-project | clang/, llvm/ |
the test/ corpora, docs, bindings, the other 20 subprojects |
~11,400 of 116k |
| DirectXShaderCompiler | lib/, include/, tools/, utils/, projects/, unittests/ |
test/, tools/clang/test/, external/, docs, CI |
~3,700 of 18k |
| offload-test-suite | everything | third-party/, docs, CI |
~90 of 1.7k |
Sharing between worktrees. The index stores project-root-relative paths
and records no absolute root, so it is portable between checkouts of the same
repository: a worktree that has no index yet is seeded from a copy of another
worktree's database and then re-parses only what its branch changed — seconds
instead of the ~4 minutes a cold index takes. It is not shareable in place
(no symlinking .codegraph/ between worktrees): the database is a live SQLite
WAL file that the indexer and its background daemon rewrite to match the tree
it sits in, so two worktrees on different branches would thrash it and fight
over the daemon lock.
Nothing lands in git. .codegraph/ and codegraph.json are added to the
clone's info/exclude, which is shared by every worktree of the submodule and
is never committed. The one file CodeGraph forces us to touch is the tracked
.gitignore: its built-in ignore list drops every directory named target or
coverage case-insensitively — which would silently hide all of
llvm/lib/Target (the DirectX and SPIR-V backends included) and DXC's
include/llvm/Target — and only a root .gitignore negation overrides a
built-in default. mask codegraph appends a
marked block for that and marks the file skip-worktree, so it stays out of
git status and out of commits. The offload test suite has no such directory,
so its .gitignore is never touched.
The only time you have to think about that hidden edit is when a git operation
wants to change .gitignore itself — a checkout, rebase or pull will
then stop with Your local changes to the following files would be overwritten by checkout: .gitignore (or Entry '.gitignore' not uptodate. Cannot merge.).
That is what mask codegraph --restore-gitignore is for: it restores the file to its
committed state and unhides it, so the git operation goes through. Re-run mask codegraph afterwards to put the block back.
mask codegraph --restore-gitignore
git rebase origin/main
mask codegraphBuilding LLVM with the offload test suite as an external project gives you
check-hlsl-* out of one tree, but it also means every offload change costs an
LLVM-sized build directory. The suite's standalone mode
(offload-test-suite/docs/offload-distribution.md) splits that in two: LLVM is
built and installed once, and the test suite is then a small top-level CMake
project that links against it — a ~20 second configure and a ~2 minute build,
per worktree.
# Once per llvm-project worktree: install the distribution
# (clang, lit tooling, the LLVM libraries the offload tools link against).
mask dist --in llvm-project # -> llvm-project/build-dist/install
# Then, in as many offload worktrees as you like:
cd offload-test-suite.my-feature
mask configure --llvm llvm-project # standalone is the default mode
mask build
mask test clang-vkOne distribution serves every offload worktree pointed at that llvm worktree.
After changing Clang, mask dist again (it is incremental) and the offload
builds pick the new toolchain up.
To test a Clang and an offload change together, point the offload worktree at the llvm worktree that has the Clang change:
mask dist --in llvm-project.my-clang-fix
cd offload-test-suite.my-feature && mask configure --llvm my-clang-fix && mask test clang-d3d12If you already have a distribution built elsewhere — a shared one, or an unpacked CI artifact — point at it instead of building one:
mask configure --dist-prefix /path/to/llvm-prefixThe integrated layout is still available for an offload worktree when you want to exercise the in-tree build:
mask configure --mode integrated --llvm llvm-project.texture-storeIn that mode the offload worktree has no build directory of its own: the llvm
worktree's build tree is configured to pull it in as OffloadTest, and
mask build / mask test operate there. Only one offload worktree can occupy
an llvm build tree at a time, which is why standalone is the default.
mask test # the whole check-hlsl umbrella
mask test clang-vk # one suite, through its ninja target
mask test clang-vk Feature/HLSLLib # a subdirectory
mask test clang-vk Feature/HLSLLib/log2.32.test
mask test clang-vk 'log2.*' # anything that is not a path becomes a lit --filter
mask lit clang/test/CodeGenHLSL/some_test.hlsl # any lit test, from an llvm worktree
mask build check-clang # or the usual ninja targetsExtra lit arguments go through --lit-args; use = when the value itself
starts with a dash: mask test clang-vk Feature --lit-args=--time-tests.
Switching DXC does not require a rebuild — DXC_DIR only feeds the lit
configuration, so mask test <suite> --dxc <worktree> regenerates the build
tree in seconds and compiles nothing.
The check-hlsl-vk and check-hlsl-clang-vk suites compile HLSL to SPIR-V and
then execute it, so they need a real Vulkan driver (an "ICD"). The dev shell
picks one for you.
The Vulkan loader loads every ICD manifest it can find and calls into each
one from vkEnumeratePhysicalDevices, so a single broken driver takes down the
whole process. Under WSL this happens by default: Mesa's dzn
(Vulkan-on-D3D12) driver is installed, fails to create a D3D12 device, and then
segfaults during enumeration — every test dies before it starts.
This cannot be fixed from offloader. Its -adapter-regex flag (and lit's
OFFLOADTEST_GPU_NAME, which forwards to it) filters the device list after
enumeration, i.e. after the crash. The only effective lever is the loader's
VK_DRIVER_FILES, which restricts it to an explicit set of manifests.
So the dev shell defaults to lavapipe, Mesa's CPU rasterizer: slow, but it
works everywhere and gives reproducible results. offload-test-suite's
lit.cfg.py already forwards VK_DRIVER_FILES into the test environment, so the
setting reaches offloader without any test-suite changes.
mask vk-info # what am I running against right now?
mask vk-list # what can I choose?
mask vk-use system # switch to the real GPU
mask vk-use lavapipe # switch back to the CPU rasterizermask vk-use records your choice in .env (gitignored). direnv watches that
file, so the change applies on your next prompt — no manual reload. .env is
loaded by .envrc, so it is a direnv-only convenience; if you use plain
nix develop, pass the variable explicitly instead:
HLSL_VK_DRIVER=system nix developAccepted values are system (let the loader discover drivers itself — use this
on a machine with a working native driver), lavapipe, any Mesa ICD short name
from mask vk-list (radeon, intel, dzn, …), or an absolute path to an ICD
manifest. All of it is just a wrapper around the HLSL_VK_DRIVER environment
variable.
For a one-off run you can bypass the shell setting entirely, since lit forwards the loader's own variable:
VK_DRIVER_FILES=/path/to/some_icd.x86_64.json mask test vkmask test vk # DXC on Vulkan
mask test clang-vk # Clang on Vulkan
# A single test
mask test clang-vk Feature/HLSLLib/log2.32.testNote: lavapipe is a software rasterizer and is not fully conformant. It is considerably slower than a GPU, and a test that fails only under lavapipe is more likely to be a driver limitation than a compiler bug — confirm on real hardware before filing an issue.
By default, submodules are cloned with a depth of 2 (shallow = true in .gitmodules). This is enough for local testing, but it can be restrictive when preparing Pull Requests or checking out old branches.
To easily update all submodules to the latest commits on their respective default remote branches (e.g., main or master), run:
mask update-submodulesTo fetch the full commit history of a submodule, use the fetch-history task:
# Example: Fetching history for LLVM
mask fetch-history llvm-project
# Example: Fetching history for DXC
mask fetch-history DirectXShaderCompilerIf you previously fetched the full history and now want to free up some disk space by truncating it back to a shallow depth (depth 2), run:
mask truncate-history llvm-projectIf the placeholder URLs for offload-test-suite or offload-golden-images in .gitmodules are incorrect, edit the .gitmodules file with the correct repository URLs, then run git submodule sync and mask setup.
| Where | What |
|---|---|
maskfile.md |
the task surface: ls, info, link, configure, build, dist, test, lit, clean, … |
scripts/hlsl-dev.sh |
worktree detection, dependency resolution, pins, locks, and the CMake invocations |
flake.nix |
the dev shell, and the CMake flag templates for each build flavour |
The flag lists in flake.nix are templates: placeholders such as
$HD_LLVM_SRC, $HD_DXC_BIN_DIR or $HD_OFFLOAD_SRC are left unexpanded in
the environment and filled in per invocation, once the tasks have resolved
which worktrees a command applies to. That is what lets one flag list serve
every worktree of a repository instead of a single hard-coded checkout — tune
build options in flake.nix, and every worktree picks them up on its next
configure.
mask itself is wrapped in the dev shell so that it finds this maskfile.md
from any directory (a maskfile.md in the current directory, or an explicit
--maskfile, still wins).
| Variable | Effect |
|---|---|
HLSL_WT |
act on this worktree, as if --in had been passed |
HLSL_LLVM, HLSL_DXC, HLSL_OFFLOAD, HLSL_GOLDEN |
default dependencies for this shell |
HLSL_MODE |
standalone or integrated for offload worktrees |
HLSL_BUILD_TYPE, HLSL_BUILD_DIR |
build type / build directory |
HLSL_DIST_PREFIX |
an already-installed LLVM distribution to build against |
HLSL_VK_DRIVER |
Vulkan ICD selection (see above) |
They are the same knobs as the flags, which makes them convenient for an agent that wants one setting to apply to a whole session:
export HLSL_LLVM=llvm-project.texture-store
mask build && mask test clang-vk # both use that LLVM