Building and running
The toolchain, every build target and what it is for, how to drive the thing once it is running, and the handful of workflows — regenerating art, recompiling shaders, running the tests — that are not just "build it".
The short version
cmake --workflow --preset debug # configure + build + test
./cmake-build-debug/voxelcut.exe
The bare commands work too and produce the same build directory:
cmake -B cmake-build-debug -G Ninja
cmake --build cmake-build-debug
What has to be on the machine
| Dependency | How it is found | Notes |
|---|---|---|
| CMake 3.20+, Ninja, MinGW GCC | PATH | C++26 (gnu++26). A CLion machine bundles all three; anywhere else Ninja and the compiler must be on PATH, since CLion injects them into the cache and a fresh checkout has nothing to inherit |
| vcpkg — glfw3, glm, stb | VCPKG_ROOT env var, or -DVCPKG_ROOT=<path> | Triplet x64-mingw-dynamic, set automatically. No vcpkg path is hardcoded |
| Vulkan loader + glslc | find_package(Vulkan) | Honours VULKAN_SDK when set, but also finds a system install, so the SDK is not a hard requirement |
The build type defaults to Debug when the caller does not pick one. That
matters more than it looks: an empty CMAKE_BUILD_TYPE means no optimisation
and no NDEBUG, so a build that looks like a release is one in every
way that counts — unoptimised, hot-reload live, per-frame logging compiled in and printing
by default. CLion always passes a type, which is exactly what hides the problem.
Targets
| Target | Built by default | What it is |
|---|---|---|
voxelcut | Yes | The executable. Depends on compile_shaders and stage_resources, so a plain build always leaves a runnable tree |
voxel_tests | Yes | The test binary. Deliberately links only the pieces that need no Vulkan, which is what keeps voxel and generation logic renderer-free |
compile_shaders | Yes | Runs glslc over shaders/*.{vert,frag,comp} into <build>/resources/shaders/*.spv |
stage_resources | Yes | Copies authored assets from the source resources/ tree into the build's. A target rather than a post-build step, so regenerating art with no code change still reaches the build tree — see below |
gen_textures | No | The placeholder-art generator. EXCLUDE_FROM_ALL; run it only to regenerate the committed PNGs |
Running it
./cmake-build-debug/voxelcut.exe # opens the default world
./cmake-build-debug/voxelcut.exe --log-level debug # ...and says what it is doing
With no arguments the executable opens the default world, generating and serialising it on first run — see Q8. Everything below is optional.
Controls
| Input | Does |
|---|---|
Mouse, WASD | Look and move — walking in physics mode, flying in free-fly |
Space / Left Shift | Jump when grounded; up and down in free-fly |
F | Physics player ↔ free-fly camera. The viewpoint is seeded on the switch, so it does not jump |
G | Edit ↔ game interaction mode. Edit places and removes whole voxels; game strikes them, wearing them down |
M | Cycle the material to place |
| LMB / RMB | Remove or strike / place, on the targeted voxel |
F3 | Toggle the metrics readout, top-left |
Esc | Quit — which flushes the world first |
The metrics line is on by default and shows frame rate and frame time, the mean and the worst frame of the last second, the mesh counts (drawable / awaiting remesh / jobs in flight), the camera position and the number of chunks edited but not yet written back. The worst-frame figure is there because the mean is precisely what hides the problems this engine keeps producing: an upload stall or a burst of remeshes is a spike, and a spike barely moves an average.
Command-line arguments
| Argument | Builds | What it does |
|---|---|---|
--log-level <level> | All | trace, debug, info, warn, error, off. See below |
--world <path> | Debug only | Open a world from this path instead of the default, so a scratch world sits beside the one you have been building in |
$VOXELCUT_LOG sets the same level as --log-level, and the flag
wins. The env var exists for launchers that make passing arguments awkward, which is most
IDE run configurations.
An unknown argument or an unparseable level fails the run rather than being ignored, because a mistyped level silently doing nothing is how you end up convinced the logging is broken.
Arguments are parsed before anything is constructed — before the window, before
the engine. Parsing them afterwards meant the window's own startup logs had already been
printed at the default level by the time the flag was read, so --log-level off
still emitted three lines, and a bad level was not rejected until after the work had
begun.
Logging
Engine code logs through the macros in src/Log.h,
never printf or cout. Every message carries a level, and that level
is filtered twice: once at compile time, once at run time.
| Level | Macro | What goes here |
|---|---|---|
trace | VC_LOG_TRACE | Per-frame detail — command buffers, image acquisition, individual allocations. Hundreds of lines a second |
debug | VC_LOG_DEBUG | Per-operation detail — a chunk written, a region flushed |
info | VC_LOG_INFO, VC_LOG | Milestones — a world opened, a pipeline built, residency resolved |
warn | VC_LOG_WARN | Something recoverable went wrong and the run continues |
error | VC_LOG_ERR | Something failed |
off | — | Nothing at all |
VC_LOG is exactly VC_LOG_INFO, kept as a spelling because it
reads fine for a milestone and there are a great many of them already.
Why levels at all
The interesting messages — a world opening, a pipeline rebuilding, an error — were being
buried under a per-frame torrent from the Vulkan layer, six lines a frame and several hundred
a second. A twelve-second run printed 7,425 lines; the same run now prints 19,
and the detail is still there under --log-level trace, where the same run prints
11,576. Nothing was deleted. It was demoted.
The two filters
| Build | Compiled in | Printed by default |
|---|---|---|
| Debug | trace and above — everything | info and above |
Release (NDEBUG) | info and above | Nothing — off |
The compile-time filter is absolute: below kCompiledLevel a call costs
nothing, not a branch and not the evaluation of its arguments. trace and
debug are the ones with a per-frame cost, so those are the ones release drops.
The runtime filter is what makes the compile-time one bearable. Detail can stay in the source, where it is useful when something is wrong, without being on by default.
Release is silent, but not mute. A shipped build printing to a console nobody is
reading is noise, and there is no level at which unsolicited output is what a player wants —
so the runtime default is off. But info and above are still
compiled in, which is the whole point: a release build that cannot say why it failed
is a release build nobody can debug. --log-level info turns them on without a
special build.
The consequence worth knowing: --log-level trace on a release build gives you
info and above, not trace. The compile-time filter has already removed them, and
there is nothing left for the runtime one to let through. This is not an error and does not
report one.
Where it goes
warn and error go to stderr, everything else to stdout —
so they survive a pipe that only keeps one, and interleave correctly with the Vulkan
validation layer's own output. Whole lines are serialised behind a mutex: generation and
meshing both run on worker threads, and two half-lines spliced together is worse than
either alone.
Tests
ctest --test-dir cmake-build-debug --output-on-failure
./cmake-build-debug/voxel_tests.exe # or run it directly for the full output
The tests are hand-rolled rather than a framework: one int-returning
function per case, returning 1 on failure, summed in main(). To add one,
write testXxx() and add failures += testXxx();. There is no way
to run a single case short of editing main().
Some checks are static_asserts rather than functions, because what they
assert is that something is not possible — that a chunk hands out no mutable
volume, that its set is unreachable. A runtime test can only exercise the
paths that exist.
Adding a source file
Append it to both relevant add_executable blocks: voxelcut,
and voxel_tests as well if it is non-renderer logic the tests exercise. A file
that only voxelcut knows about will compile fine and then fail to link the
tests the moment a test touches it.
Assets: compiled at build time, loaded at runtime
Shaders are compiled to SPIR-V by the build and read from files at runtime,
not embedded in the binary. resources::loadSpirv("shaders/<name>.<stage>.spv")
returns the words; the buffer must outlive the pipeline creation that copies it.
The runtime resolves a resources/ root relative to the executable —
$VOXELCUT_RESOURCES, then <exeDir>/resources, then
<exeDir>, then <cwd>/resources — so assets are found whatever
the working directory is, and dev and installed builds share one layout. Authored art
lives in the source resources/ tree; compiled shaders are written
straight into the build's.
Regenerating placeholder art. gen_textures writes a tile for
every material in the table, so a material with no art shows as a visibly ugly tile
rather than crashing or — worse — silently borrowing a neighbour's and looking almost
right.
cmake --build cmake-build-debug --target gen_textures
./cmake-build-debug/gen_textures.exe resources # writes into the SOURCE tree
It writes into the source tree, so the next ordinary build stages the result. That staging is a target of its own precisely because of this workflow: as a post-build step it only ran when the executable relinked, so regenerating art without touching code left the build tree holding the old tiles and the next run died on a missing file.
Debug-only conveniences
Three things exist only when NDEBUG is not defined, and all three compile
out entirely in a release build:
- Hot-reload — the engine polls the runtime shader and texture files and rebuilds the pipeline and texture array when they change. Recompile shaders or regenerate art while the game is running and it picks them up without a restart, which is what makes tuning a shader constant an edit rather than a session.
--world— a shipped build has no business opening a world from an arbitrary path on the command line.- The
traceanddebuglog levels — see above. Note that logging itself is no longer debug-only:infoand above survive into release, silent until asked for.
Packaging
cpack -G ZIP # from the build dir; also driven by the dist workflow preset
Bundles the executable, the DLLs it actually needs and the whole resources/
tree into one zip. glfw3.dll and libwinpthread-1.dll ship;
vulkan-1.dll deliberately does not, since the loader is part of any Windows
install with a GPU driver and shipping a copy invites version skew. libgcc and libstdc++
are linked statically into the executable instead.