Skip to content

SE2 DXC build - #7

Merged
viktor-ferenczi merged 2 commits into
mainfrom
se2-dxc
Aug 27, 2026
Merged

SE2 DXC build#7
viktor-ferenczi merged 2 commits into
mainfrom
se2-dxc

Conversation

@viktor-ferenczi

Copy link
Copy Markdown
Contributor

Space Engineers 2 compiles every shader at runtime through Vortice.Dxc, which P/Invokes DxcCreateInstance from dxcompiler.dll. Until now the Linux port used hand-built libraries pinned inside the recompiled-game workspace. Move them here so they ship in se2-dependencies.tar.gz like every other SE2 native dependency.

Two files are staged, not three:

  • libdxcompiler.so — DirectX Shader Compiler at tag v1.9.2607, commit 0d3ee6b5, built from unpatched upstream sources.
  • libSE2DxcCompiler.so — the ABI shim, built in the same step from Sources/dxc-bridge/DxcCompilerBridge.cpp against the headers of the DXC tree just compiled.

libdxil.so is deliberately dropped. Microsoft open-sourced the DXIL validator hash, so ComputeHashRetail is compiled into libdxcompiler.so directly; compiling a shader with libdxil.so beside it and again with it absent produces byte-identical containers with the same non-zero hash, and LD_DEBUG=libs shows it is never dlopened either way. It also has no buildable source — it exists only as a prebuilt release artefact — so leaving it out is what makes a from-source payload possible.

Because the compiler is now compiled rather than unpacked from Microsoft's release download, no binary distribution agreement applies to the shipped payload any more. The LICENSE-MS.txt the recompiled-game workspace carries for exactly that reason is no longer needed. The archive gains Licenses/se2/DXC-LICENSE.txt (the NCSA/LLVM Release License) and DXC-README.txt (pin and provenance) instead.

The shim stays. DXC's Linux WCHAR is the 4-byte platform wchar_t while Vortice marshals the Windows 2-byte wire type, and building DXC with -fshort-wchar to close the gap is not an option: libdxcompiler.so imports eight wide-char functions from glibc (wcslen, wcscmp, wcsncmp, wcsncpy, wmemcmp, wmemcpy, mbstowcs, wcstombs) that a 2-byte wchar_t build would silently mismatch. Its vtable layout tracks the DXC version, which is why it is compiled next to the compiler rather than left in a consuming repo where the drift would go unnoticed.

One behavioural fix while moving it: the backend fallback was a hardcoded /usr/lib/libdxcompiler.so, which picked up whatever the host had installed — in practice a v1.10 Shader Model 6.10 preview, which aborts inside LLVM while compiling Render12 shaders. It now dlopens the bare SONAME and resolves it through DT_RUNPATH=$ORIGIN to the sibling staged beside it. SE2_DXCOMPILER_BACKEND still overrides the path.

Notes on the pin and the build:

  • v1.9.2607 is the newest stable release. Every v1.10.x release is flagged prerelease upstream. Its only breaking HLSL change over the previously shipped v1.9.2602.24 is that volatile is no longer accepted, and none of SE2's 660 shader sources use it.
  • The SPIR-V backend cannot be switched off on Linux: DXC's root CMakeLists.txt does an unconditional if(NOT WIN32) set(ENABLE_SPIRV_CODEGEN ON) endif() that shadows both the cache and the command line. external/SPIRV-Headers and external/SPIRV-Tools are therefore initialised alongside external/DirectX-Headers; only external/googletest stays uncloned. SPIRV-Tools links in statically, so DXC-BUNDLED-LICENSES.txt carries its Apache-2.0 text plus the SPIRV-Headers and DirectX-Headers ones.
  • Flag order in the cmake invocation is load-bearing: the -C cache file's set()s are non-FORCE, so a -D wins only when it precedes the -C.
  • The build tree (~270 MB) is deleted after staging, keeping the runner footprint around 550 MB. DXC_KEEP_BUILD_TREE=1 keeps it locally.

Verified locally: both staged libraries resolve only glibc, libstdc++, libgcc_s and libm and carry DT_RUNPATH=$ORIGIN; a trivial compute shader compiled in a directory containing no libdxil.so yields a signed container with DXIL hash 19a5d3029c0cd4a5e1bf3d242c351712; the same shader compiled through libSE2DxcCompiler.so from a 2-byte-WCHAR caller produces a byte-identical container, with LD_DEBUG confirming the backend resolved out of $ORIGIN; ./build.sh --only=dxc stages cleanly and the expected-artefact assertion passes.

CI wall-clock roughly doubles — DXC alone is 30-60 minutes.

Space Engineers 2 compiles every shader at runtime through Vortice.Dxc,
which P/Invokes DxcCreateInstance from dxcompiler.dll. Until now the
Linux port used hand-built libraries pinned inside the recompiled-game
workspace. Move them here so they ship in se2-dependencies.tar.gz like
every other SE2 native dependency.

Two files are staged, not three:

* libdxcompiler.so — DirectX Shader Compiler at tag v1.9.2607, commit
  0d3ee6b5, built from unpatched upstream sources.
* libSE2DxcCompiler.so — the ABI shim, built in the same step from
  Sources/dxc-bridge/DxcCompilerBridge.cpp against the headers of the
  DXC tree just compiled.

libdxil.so is deliberately dropped. Microsoft open-sourced the DXIL
validator hash, so ComputeHashRetail is compiled into libdxcompiler.so
directly; compiling a shader with libdxil.so beside it and again with it
absent produces byte-identical containers with the same non-zero hash,
and LD_DEBUG=libs shows it is never dlopened either way. It also has no
buildable source — it exists only as a prebuilt release artefact — so
leaving it out is what makes a from-source payload possible.

Because the compiler is now compiled rather than unpacked from
Microsoft's release download, no binary distribution agreement applies
to the shipped payload any more. The LICENSE-MS.txt the recompiled-game
workspace carries for exactly that reason is no longer needed. The
archive gains Licenses/se2/DXC-LICENSE.txt (the NCSA/LLVM Release
License) and DXC-README.txt (pin and provenance) instead.

The shim stays. DXC's Linux WCHAR is the 4-byte platform wchar_t while
Vortice marshals the Windows 2-byte wire type, and building DXC with
-fshort-wchar to close the gap is not an option: libdxcompiler.so
imports eight wide-char functions from glibc (wcslen, wcscmp, wcsncmp,
wcsncpy, wmemcmp, wmemcpy, mbstowcs, wcstombs) that a 2-byte wchar_t
build would silently mismatch. Its vtable layout tracks the DXC version,
which is why it is compiled next to the compiler rather than left in a
consuming repo where the drift would go unnoticed.

One behavioural fix while moving it: the backend fallback was a
hardcoded /usr/lib/libdxcompiler.so, which picked up whatever the host
had installed — in practice a v1.10 Shader Model 6.10 preview, which
aborts inside LLVM while compiling Render12 shaders. It now dlopens the
bare SONAME and resolves it through DT_RUNPATH=$ORIGIN to the sibling
staged beside it. SE2_DXCOMPILER_BACKEND still overrides the path.

Notes on the pin and the build:

* v1.9.2607 is the newest stable release. Every v1.10.x release is
  flagged prerelease upstream. Its only breaking HLSL change over the
  previously shipped v1.9.2602.24 is that `volatile` is no longer
  accepted, and none of SE2's 660 shader sources use it.
* The SPIR-V backend cannot be switched off on Linux: DXC's root
  CMakeLists.txt does an unconditional
  `if(NOT WIN32) set(ENABLE_SPIRV_CODEGEN ON) endif()` that shadows both
  the cache and the command line. external/SPIRV-Headers and
  external/SPIRV-Tools are therefore initialised alongside
  external/DirectX-Headers; only external/googletest stays uncloned.
  SPIRV-Tools links in statically, so DXC-BUNDLED-LICENSES.txt carries
  its Apache-2.0 text plus the SPIRV-Headers and DirectX-Headers ones.
* Flag order in the cmake invocation is load-bearing: the -C cache
  file's set()s are non-FORCE, so a -D wins only when it precedes the -C.
* The build tree (~270 MB) is deleted after staging, keeping the runner
  footprint around 550 MB. DXC_KEEP_BUILD_TREE=1 keeps it locally.

Verified locally: both staged libraries resolve only glibc, libstdc++,
libgcc_s and libm and carry DT_RUNPATH=$ORIGIN; a trivial compute shader
compiled in a directory containing no libdxil.so yields a signed
container with DXIL hash 19a5d3029c0cd4a5e1bf3d242c351712; the same
shader compiled through libSE2DxcCompiler.so from a 2-byte-WCHAR caller
produces a byte-identical container, with LD_DEBUG confirming the
backend resolved out of $ORIGIN; ./build.sh --only=dxc stages cleanly and
the expected-artefact assertion passes.

CI wall-clock roughly doubles — DXC alone is 30-60 minutes.
A cold run is about 31 minutes now, roughly 19 of them the DirectX Shader
Compiler, and every push paid it in full even when nothing that feeds a
given library had changed. Cache each dependency's staged output and
restore it before build.sh runs.

The mechanism is the stamp files the scripts already keep. Restoring
build/<dep>.stamp next to the staged .so files makes the script's existing
"outputs present and stamp matches" check fire, so the step exits without
a source tree at all. A fully warm run builds nothing and packages all
three archives in under five seconds.

The cache key IS the stamp: every Scripts/build_*.sh gains --print-stamp,
which prints its stamp and exits without touching the network or the build
tree, and the workflow keys on a hash of that plus the image label and the
runner's gcc/glibc versions. Keying off the scripts' own value rather than
hashFiles() means the two cannot drift apart, and that editing a comment
does not cost a rebuild. The toolchain is in the key because the staged
libraries link against the runner's gcc and glibc and ubuntu-24.04 is
patched in place.

The scripts re-derive and compare their stamp on every run regardless, so
the CI key is only a performance hint: too loose costs a rebuild, it
cannot ship a stale binary.

Two supporting changes:

* FFmpeg had no stamp and no early exit — it relied on an incremental make
  inside a 203 MB build tree, which is not something a cache of shipped
  files can stand in for. It now has both. Its stamp folds in a hash of
  the configure flags, which meant hoisting the flag array (minus --prefix,
  a build-tree path that must not affect the key) above the rationale
  comment so --print-stamp can hash it without downloading anything. The
  resulting CONFIGURE_FLAGS is byte-for-byte what it was.
* SDL3 caches its build prefix rather than the shipped file, because
  build_sdl3.sh deliberately re-stages libSDL3.so into both game trees on
  every run.

Restore and save are separate workflow steps rather than the combined
cache action: the combined one only writes on a fully successful job, so a
failure in a late dependency — or in the release step after a 30-minute
build — would discard everything the run had already built.

Note for future dependency bumps: GitHub scopes caches by branch. A run
reads its own branch, the default branch, and for a PR its base branch,
but a cache written by a PR lives on that PR's merge ref and cannot be
read by main or by any other PR. So a bump is built twice, once on the PR
and once on main after the merge; everything the PR did not touch restores
from main's caches either way.

Also corrects the DXC build-time figures throughout: 30-60 minutes came
from the original task description, but the first CI run measured the
whole cold pipeline at 30m45s, which puts DXC at roughly 19 minutes on a
4-vCPU runner.

Verified by replaying the workflow's CACHE_PATHS_* lists into an empty
build directory and running the pipeline against it: all seven steps
report a cache hit, the 45 staged files are byte-identical to a locally
built tree, and the steam archive is byte-identical to the last published
release.
@viktor-ferenczi
viktor-ferenczi merged commit b2b0de9 into main Aug 27, 2026
1 check passed
@viktor-ferenczi
viktor-ferenczi deleted the se2-dxc branch August 27, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant