Map compile tools for GoldSrc, aimed squarely at Counter-Strike 1.6.
This is a fork of seedee/SDHLT, which is itself descended from Vluzacn's ZHLT and from Valve's original tools. The goal here is narrow. Compiles should be faster, the output should be reproducible, and when a map is broken the compiler should say what is broken and where, instead of leaving a pointfile and a shrug.
Everything claimed below was measured on real maps. The numbers, including the
ones that came out negative, live in docs/BENCHMARKS.md.
Download the latest Windows package from
Releases and unzip it anywhere.
Inside you get resdhlt-gui.exe and a tools/ folder.
If you use the GUI, that is the whole installation. Open it, point it at your
.map, press compile.
If you compile from an editor such as J.A.C.K. or Hammer, open its compile
configuration and set the four tool paths to sdHLCSG.exe, sdHLBSP.exe,
sdHLVIS.exe and sdHLRAD.exe from tools/. Then add tools/sdhlt.wad to your
WAD list, which is required for the tool textures to work, and tools/sdhlt.fgd
to your FGD list.
RAD is over 95% of a compile, so that is where the work went.
The default sky sampling level dropped from 7 to 6. That alone is about 1.65x
faster RAD for a worst case difference of 1/255 on a single luxel. Pass
-skylevel 7 to get upstream lighting back exactly.
On top of that, a series of changes to the lighting inner loops takes another
5 to 13% off RAD's CPU time depending on the map. Samples whose PVS cannot reach
any sky brush now skip the sky loop instead of casting thousands of rays that
were always going to be occluded. The BSP walk in TestLine_r iterates instead
of recursing on its tail positions, which matters because it is entered around
1.85 billion times per map. Ray tests against opaque entities check one bounding
box for the whole list before walking it. The sample interpolation works out a
phong normal once per sample rather than once per candidate patch, and reuses its
scratch buffers per thread rather than allocating about 1.2 million times per
map. Every one of those was verified to produce a byte identical .bsp.
The optional AVX2 RAD build is another 4 to 5% faster. Releases publish it as a
separate -avx2 ZIP; the normal Windows ZIP and new source builds are portable
so they do not silently crash on an older CPU.
Threading was broken in ways that cost far more than any of the above. Linux
builds ran single threaded unless you passed -threads explicitly. Windows
machines with more than 32 logical processors fell back to one thread. Passing
-threads 5000 overflowed a stack buffer and crashed. All three are fixed, and
koth_sandy went from 2.88s to 1.71s purely by using the cores that were already
there.
Two compiles of one map used to produce different BSP files, because CSG numbered
planes and ordered faces by whichever thread finished first. Compiles are now
reproducible by default, which is what makes "verified byte identical" a
meaningful statement anywhere in this repository. Pass -nodeterministic to CSG
if you want the old behaviour back.
Leaks point at the hole. The classic pointfile was a side effect of the
outside flood fill. It recorded whatever order the recursion happened to unwind
in, so it wanders across the map, doubles back, and never marks the place where
the inside actually opens onto the void. ReSDHLT builds the trail separately once
the leak is proved: a shortest path over the portal graph from the leaked entity
to the outside, simplified so the .lin file is a handful of clean segments. The
hole itself gets its coordinates printed and a dense marker star written into the
.pts, so it cannot be missed in the editor. Every hull that leaks is reported
once at the end instead of repeating the same warning four times. -allleaks
surveys the map and marks every hole, so a leaky map can be sealed in one pass
through the editor rather than one hole per compile.
Lightmap atlas overflow is caught before the lighting runs. GoldSrc packs
every lit face into 64 pages of 128x128 luxels and aborts the map load with
AllocBlock: full when they do not fit. That used to be discovered after a full
compile, or in game. RAD now runs the engine's own allocator up front. Past 95%
of the budget it prints a breakdown by texture ranked by lightmap footprint, so
the textures worth rescaling are named:
!!! ERROR: LIGHTMAP ATLAS OVERFLOW - map exceeds the engine's 64 page limit
usage 71 / 64 pages (111%)
cause too many lightmapped luxels, so the engine aborts with "AllocBlock: full"
action raise the texture scale on the biggest consumers below, or make them smaller
Lightmap atlas budget by texture (top consumers):
texture faces luxels % budget
--------------------------------------------------------------
dev_r3_cs2y2 2,258 242,850 72.0%
dev_c3_dhmsl0 1,702 119,917 36.6%
-lmoptimize in BSP goes one step further and reorders faces so the allocator
wastes fewer pages. It measures three legal orders against the engine's own
packer and keeps the best, so it can never come out worse than before. It is off
by default, which keeps the default output byte identical to previous versions.
-texchart in CSG reports what each texture costs the BSP. Useful when you
are hunting for the thing that blew up your texture data or your face count.
-gpu forces RAD's compatible direct-light gathering and Sparse transfer-factor
construction through Vulkan compute. -gpuauto is the recommended switch: it
measures each phase separately and keeps small work on the CPU, without opening
the Vulkan device until a phase crosses its workload threshold. Direct-light
speed depends on how many lights the map has;
transfer-factor speed depends on how many visible patch pairs MakeScales has
to evaluate. Same room, same settings, only the light count changing, on a GTX
1060 against six CPU threads with -extra:
| lights | CPU | -gpu |
speedup |
|---|---|---|---|
| 1 | 0.66s | 1.32s | 0.50x, slower |
| 256 | 1.59s | 1.29s | 1.23x |
| 512 | 2.27s | 1.31s | 1.73x |
| 1024 | 3.91s | 1.72s | 2.27x |
CPU time grows linearly with the light count, because every sample walks the
lights its PVS can see one at a time. Time on the device barely moves across a
1024x increase. The crossover sits around 150 to 200 lights, so a heavily lit map
gains and a map with a single light_environment loses.
The output is byte identical to the CPU path on almost everything measured. The one exception differed by a single byte in 95,982, by one step out of 255, because the kernel normalises in float. It declines and hands the work back to the CPU, saying why, when the map has opaque entities, studio model shadows, more light styles than the kernel has slots, a BSP deeper than its traversal stack, or when no Vulkan driver is present.
The transfer kernel is used with -vismatrix sparse, the default. It walks the
sparse visibility pairs directly instead of testing the full patches-squared
matrix. Patch and winding data are uploaded once per compile and the pair/result
buffers are reused across every batch. It falls back to the CPU implementation for RGB transfers, translucent
patches, and custom bounce shadows. A style-0 single-bounce accumulation
kernel is also available; multi-bounce runs deliberately stay on the CPU for
deterministic lightmap bytes because a GPU float round-off can compound on the
next iteration. The bounce phase is much smaller than constructing transfers
(on ze_cardinal, 12 bounces totalled about 6 seconds while MakeScales alone
took 58.43 seconds).
It is off by default. Building it needs nothing extra, since the Khronos headers and the compiled SPIR-V are both in the tree and the Vulkan loader is opened by name at runtime.
gui/ is a dark theme compiler front end written in Rust with egui. Pick a map,
pick a preset, press compile, watch the log. Every option carries a tooltip
saying what it does and when to use it, and there is a tab summarising the
recommendations that the benchmarks actually support.
It updates itself from GitHub Releases. The check runs asynchronously on launch, but installation always requires an explicit click and is blocked while a compile is in progress. The menu has a switch to turn checks off.
cd gui
cargo run --releaseBEVELHINT never did anything. The brush parser tested for it in a place it
could not reach, so a texture people had been using for years was silently inert.
It works now, and sdhlt.fgd documents it along with SOLIDHINT.
zhlt_embedlightmap used to break every surface GoldSrc identifies by texture
name. Baking renames the texture, so water stopped waving, conveyors stopped
scrolling, and transparent surfaces stopped being transparent. The baked names
now preserve the prefix the engine looks for. The same feature also produces 24
to 34% less texture data than it used to.
CSG accepts 512 WAD paths instead of 128. The old ceiling is easy to hit with a large texture library, and CSG aborted rather than ignoring the excess.
| Tool | Flag | What it does |
|---|---|---|
| CSG | -texchart |
Report what each texture costs the BSP |
| CSG | -mergeentities |
Fold equivalent static brush entities into one |
| CSG | -nodeterministic |
Restore the old thread ordered, irreproducible output |
| BSP | -lmoptimize |
Reorder faces to waste fewer lightmap atlas pages |
| BSP | -allleaks |
Mark every hole, not just the first one found |
| RAD | -skylevel N |
Sky sampling fineness, 4 to 8, default 6 |
| RAD | -gpu |
Compute direct lighting and Sparse transfer factors with Vulkan |
| RAD | -gpuauto |
Select CPU or Vulkan independently for each phase by workload |
| RAD | -gpu-gather |
Enable only compatible direct-light gathering on Vulkan |
| RAD | -gpu-transfers |
Enable only Sparse transfer-factor construction on Vulkan |
| RAD | -nogpu-gather |
Exclude gather when using -gpu or -gpuauto |
| RAD | -nogpu-transfers |
Exclude Sparse transfers when using -gpu or -gpuauto |
| RAD | -gpuadapter N |
Pick the Vulkan device by index |
| RAD | -noallocblockcheck |
Compile even when the map overflows the lightmap atlas |
| RAD | -profile |
Report where RAD spends its time, no external profiler needed |
| RAD | -raybench |
Benchmark real sky rays through RAD's BSP tracer |
| RAD | -workbench |
Measure per-face work balance and scheduling overhead |
cmake --preset portable
cmake --build --preset portable
ctest --preset portableFor the faster RAD binary on a known AVX2 machine, replace portable with
avx2. Both presets use Ninja and RelWithDebInfo, and keep their executables in
build/<preset>/bin so one variant can never contaminate the other. The
traditional cmake -B build -S . flow remains supported, defaults to a portable
Release build, and still places binaries in tools/ for map-editor integrations.
Install/package smoke tests run through CTest.
| Option | Default | What it is for |
|---|---|---|
SDHLT_ARCH |
empty | Instruction set for RAD. Use avx2 only on a compatible CPU |
SDHLT_ARCH_ALL |
OFF |
Apply the above to CSG, BSP and VIS as well. Read below first |
SDHLT_GPU |
ON |
Build the Vulkan backend behind -gpu |
SDHLT_LTO |
OFF |
Link time optimisation |
SDHLT_PROFILE |
OFF |
Counters inside the ray casting functions |
SDHLT_COMPILER_CACHE |
ON |
Use sccache/ccache when it is installed |
SDHLT_STRONG_WARNINGS |
ON |
Enable strong warnings without treating them as errors |
SDHLT_WARNINGS_AS_ERRORS |
OFF |
Opt into /WX or -Werror for staged warning cleanup |
SDHLT_OUTPUT_IN_BUILD_TREE |
OFF |
Isolate executables per build tree; presets turn this on |
Two warnings about SDHLT_ARCH. An AVX2 build will not start at all on a CPU
older than roughly 2013, and the failure is a silent crash rather than a message,
so distribute the portable build unless the target CPU is known. And
SDHLT_ARCH_ALL is off for a reason: building CSG, BSP and VIS with AVX2 changes
their floating point results, which on koth_sandy produced a .bsp whose vis
data made RAD abort. RAD only writes light data, so it is the safe one to
vectorise.
Windows releases contain portable and AVX2 runtime ZIPs, matching symbol ZIPs,
per-asset .sha256 files, detached Ed25519 .sig files, and a combined
SHA256SUMS.txt. The GUI remains in both runtime packages; only RAD's CPU
instruction target differs. Release CI requires the base64-encoded 32-byte
private seed in the repository secret RESDHLT_ED25519_PRIVATE_KEY_B64; the
public half is pinned in gui/src/update.rs and the updater rejects unsigned or
tampered packages. To rotate it, run
cargo run --bin resdhlt-release-signer -- --generate-key, update the pinned
public key in the signer/updater, and replace the CI secret together.
Editing a compute shader under src/sdhlt/sdHLRAD/gpu/shaders/ means
regenerating the embedded SPIR-V with python scripts/gen_spirv.py, which needs
glslc or glslang on the PATH. Nobody else needs either.
docs/BENCHMARKS.mdis the important one. Every measurement taken, including the ideas that were tried and abandoned, with enough detail to reproduce them.docs/FPS_Y_TOOL_TEXTURES.mdcovers how to actually lowerwpolyand raise in game FPS. Start here if you make maps, because the compiler cannot do this part for you and it is the largest lever there is.docs/PERFILAR_RAD.mdexplains how to profile RAD on your own hardware and what is already ruled out.docs/MERGE_DE_ENTIDADES.mdcovers-mergeentities.CHANGELOG.mdhas the full history.
Some documents are in Spanish. That is what the people who use this fork read.
scripts/compilebench.py times a full CSG, BSP, VIS and RAD run and fingerprints
every BSP lump, so a change can be shown not to alter the output:
python3 scripts/compilebench.py --tools tools --map yourmap.map --runs 3
python3 scripts/compilebench.py --compare before.json after.jsonscripts/bspcheck.py validates a compiled BSP for planarity, convexity,
degenerate faces and surface area:
python3 scripts/bspcheck.py yourmap.bspThis fork exists because other people did the hard part first.
seedee maintains
SDHLT, which is what ReSDHLT forked from and
what most of this codebase still is. Studio model shadows, BEVELHINT,
SPLITFACE, info_portal, the % minlight flag, -worldextent and the portal
file handling for J.A.C.K. are all his work. Thanks for the base and for keeping
these tools alive.
speedrun16dev wrote
hltools, a modern GoldSrc toolchain
written from scratch. The Vulkan compute backend behind -gpu, the leak
diagnostics rework and the lightmap atlas budget check in this fork were all
ported from there, with the design intact. hltools is GPL-2.0 like this project,
and the port is documented in
src/sdhlt/sdHLRAD/gpu/THIRD_PARTY.md.
If you are starting fresh rather than maintaining an old pipeline, go look at
hltools first.
Further back stand Vluzacn, whose ZHLT v34 is the ancestor of every tool in this family, Sean "Zoner" Cavanaugh, and Valve, whose original compile tools were released with permission.
GPL-2.0, inherited from ZHLT and SDHLT. See LICENSE.md.
The Vulkan headers vendored under src/sdhlt/sdHLRAD/gpu/vulkan/ and
src/sdhlt/sdHLRAD/gpu/vk_video/ are unmodified Khronos headers under
Apache-2.0 or MIT.
