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

DependencyHow it is foundNotes
CMake 3.20+, Ninja, MinGW GCCPATHC++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, stbVCPKG_ROOT env var, or -DVCPKG_ROOT=<path>Triplet x64-mingw-dynamic, set automatically. No vcpkg path is hardcoded
Vulkan loader + glslcfind_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

TargetBuilt by defaultWhat it is
voxelcutYesThe executable. Depends on compile_shaders and stage_resources, so a plain build always leaves a runnable tree
voxel_testsYesThe test binary. Deliberately links only the pieces that need no Vulkan, which is what keeps voxel and generation logic renderer-free
compile_shadersYesRuns glslc over shaders/*.{vert,frag,comp} into <build>/resources/shaders/*.spv
stage_resourcesYesCopies 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_texturesNoThe 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

InputDoes
Mouse, WASDLook and move — walking in physics mode, flying in free-fly
Space / Left ShiftJump when grounded; up and down in free-fly
FPhysics player ↔ free-fly camera. The viewpoint is seeded on the switch, so it does not jump
GEdit ↔ game interaction mode. Edit places and removes whole voxels; game strikes them, wearing them down
MCycle the material to place
LMB / RMBRemove or strike / place, on the targeted voxel
F3Toggle the metrics readout, top-left
EscQuit — 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

ArgumentBuildsWhat it does
--log-level <level>Alltrace, debug, info, warn, error, off. See below
--world <path>Debug onlyOpen 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.

LevelMacroWhat goes here
traceVC_LOG_TRACEPer-frame detail — command buffers, image acquisition, individual allocations. Hundreds of lines a second
debugVC_LOG_DEBUGPer-operation detail — a chunk written, a region flushed
infoVC_LOG_INFO, VC_LOGMilestones — a world opened, a pipeline built, residency resolved
warnVC_LOG_WARNSomething recoverable went wrong and the run continues
errorVC_LOG_ERRSomething failed
offNothing 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

BuildCompiled inPrinted by default
Debugtrace and above — everythinginfo and above
Release (NDEBUG)info and aboveNothing — 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:

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.